IAM: Add email, login field validation to User create/update API (#112391)
* wip
* wip
* wip
(cherry picked from commit 8cedf25892)
* Search seems to be working, the validation is still wip
* Use keyword.Name analyzer for Filterable fields
* Only string fields should be indexed with keyword analyzer
* Change search query for email and login fields to use term query
* Remove unnecessary Exact from the resource protobuf definitions
Co-Authored-By: Ryan McKinley <ryantxu@gmail.com>
* Add legacy search support to the API
* Tests for legacy search, validate and integration tests for user
* Lint
* Add snapshot tests to userDocumentBuilder
* Address CodeQL issues
* Improvements, handle Mode2, tests should pass
* Change default limit from 0 to 1 for requests
* Cleanup
* Add fixme
* Update pkg/registry/apis/iam/register.go
Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
* Update pkg/registry/apis/iam/user/legacy_search.go
Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
---------
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
This commit is contained in:
co-authored by
Ryan McKinley
Stephanie Hingtgen
parent
f191acf811
commit
ad9d8098ef
@@ -5,13 +5,15 @@ import (
|
||||
"fmt"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
)
|
||||
|
||||
func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error {
|
||||
func ValidateOnCreate(ctx context.Context, userSearchClient resourcepb.ResourceIndexClient, obj *iamv0alpha1.User) error {
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return apierrors.NewUnauthorized("no identity found")
|
||||
@@ -28,27 +30,22 @@ func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error {
|
||||
return apierrors.NewBadRequest("user must have either login or email")
|
||||
}
|
||||
|
||||
err = validateRole(obj)
|
||||
if err != nil {
|
||||
if err := validateRole(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateEmail(ctx, userSearchClient, requester.GetNamespace(), obj.Name, obj.Spec.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateLogin(ctx, userSearchClient, requester.GetNamespace(), obj.Name, obj.Spec.Login); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRole(obj *iamv0alpha1.User) error {
|
||||
if obj.Spec.Role == "" {
|
||||
return apierrors.NewBadRequest("role is required")
|
||||
}
|
||||
|
||||
if !identity.RoleType(obj.Spec.Role).IsValid() {
|
||||
return apierrors.NewBadRequest(fmt.Sprintf("invalid role '%s'", obj.Spec.Role))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateOnUpdate(ctx context.Context, oldObj, newObj *iamv0alpha1.User) error {
|
||||
func ValidateOnUpdate(ctx context.Context, userSearchClient resourcepb.ResourceIndexClient, oldObj, newObj *iamv0alpha1.User) error {
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return apierrors.NewUnauthorized("no identity found")
|
||||
@@ -93,10 +90,109 @@ func ValidateOnUpdate(ctx context.Context, oldObj, newObj *iamv0alpha1.User) err
|
||||
return apierrors.NewBadRequest("user must have either login or email")
|
||||
}
|
||||
|
||||
err = validateRole(newObj)
|
||||
if err != nil {
|
||||
if err := validateRole(newObj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newObj.Spec.Email != oldObj.Spec.Email {
|
||||
if err := validateEmail(ctx, userSearchClient, requester.GetNamespace(), newObj.Name, newObj.Spec.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if newObj.Spec.Login != oldObj.Spec.Login {
|
||||
if err := validateLogin(ctx, userSearchClient, requester.GetNamespace(), newObj.Name, newObj.Spec.Login); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRole(obj *iamv0alpha1.User) error {
|
||||
if obj.Spec.Role == "" {
|
||||
return apierrors.NewBadRequest("role is required")
|
||||
}
|
||||
|
||||
if !identity.RoleType(obj.Spec.Role).IsValid() {
|
||||
return apierrors.NewBadRequest(fmt.Sprintf("invalid role '%s'", obj.Spec.Role))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEmail(ctx context.Context, searchClient resourcepb.ResourceIndexClient, namespace, name, email string) error {
|
||||
req := createUserSearchRequest(namespace, []*resourcepb.Requirement{
|
||||
{
|
||||
Key: "fields.email",
|
||||
Operator: string(selection.Equals),
|
||||
Values: []string{email},
|
||||
},
|
||||
}, []string{"name", "email", "login"})
|
||||
|
||||
resp, err := searchClient.Search(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME(mgyongyosi): Improve the exact match validation
|
||||
|
||||
if resp.TotalHits > 0 {
|
||||
// If the found user is the same as the one being created/updated, it's not a conflict.
|
||||
// This is required for Mode 2 when the resource is written to LegacyStorage and UnifiedStorage.
|
||||
rows := resp.Results.Rows
|
||||
if len(rows) > 0 && rows[0].Key.Name == name {
|
||||
return nil
|
||||
}
|
||||
return apierrors.NewConflict(iamv0alpha1.UserResourceInfo.GroupResource(),
|
||||
name,
|
||||
fmt.Errorf("email '%s' is already taken", email))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLogin(ctx context.Context, searchClient resourcepb.ResourceIndexClient, namespace, name, login string) error {
|
||||
req := createUserSearchRequest(namespace, []*resourcepb.Requirement{
|
||||
{
|
||||
Key: "fields.login",
|
||||
Operator: string(selection.Equals),
|
||||
Values: []string{login},
|
||||
},
|
||||
}, []string{"name", "email", "login"})
|
||||
resp, err := searchClient.Search(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME(mgyongyosi): Improve the exact match validation
|
||||
|
||||
if resp.TotalHits > 0 {
|
||||
// If the found user is the same as the one being created/updated, it's not a conflict.
|
||||
// This is required for Mode 2 when the resource is written to LegacyStorage and UnifiedStorage.
|
||||
rows := resp.Results.Rows
|
||||
if len(rows) > 0 && rows[0].Key.Name == name {
|
||||
return nil
|
||||
}
|
||||
return apierrors.NewConflict(iamv0alpha1.UserResourceInfo.GroupResource(),
|
||||
name,
|
||||
fmt.Errorf("login '%s' is already taken", login))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createUserSearchRequest(namespace string, requirements []*resourcepb.Requirement, fields []string) *resourcepb.ResourceSearchRequest {
|
||||
userGvr := iamv0alpha1.UserResourceInfo.GroupResource()
|
||||
return &resourcepb.ResourceSearchRequest{
|
||||
Options: &resourcepb.ListOptions{
|
||||
Key: &resourcepb.ResourceKey{
|
||||
Group: userGvr.Group,
|
||||
Resource: userGvr.Resource,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Fields: requirements,
|
||||
},
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user