diff --git a/pkg/iam/scim/errors.go b/pkg/iam/scim/errors.go index 598b754b7..4fe6ffad5 100644 --- a/pkg/iam/scim/errors.go +++ b/pkg/iam/scim/errors.go @@ -44,18 +44,6 @@ func NewSCIMConfigurationAlreadyExistsError(organizationID gid.GID) *ErrSCIMConf return &ErrSCIMConfigurationAlreadyExists{OrganizationID: organizationID} } -type ErrSCIMUserNotFound struct { - ID gid.GID -} - -func (e *ErrSCIMUserNotFound) Error() string { - return fmt.Sprintf("SCIM user %s not found", e.ID) -} - -func NewSCIMUserNotFoundError(id gid.GID) *ErrSCIMUserNotFound { - return &ErrSCIMUserNotFound{ID: id} -} - type ErrSCIMInvalidToken struct{} func (e *ErrSCIMInvalidToken) Error() string { @@ -65,35 +53,3 @@ func (e *ErrSCIMInvalidToken) Error() string { func NewSCIMInvalidTokenError() *ErrSCIMInvalidToken { return &ErrSCIMInvalidToken{} } - -type ErrSCIMInvalidRequest struct { - Detail string -} - -func (e *ErrSCIMInvalidRequest) Error() string { - return fmt.Sprintf("invalid SCIM request: %s", e.Detail) -} - -func NewSCIMInvalidRequestError(detail string) *ErrSCIMInvalidRequest { - return &ErrSCIMInvalidRequest{Detail: detail} -} - -type ErrSCIMUserAlreadyExists struct { - Email string -} - -func (e *ErrSCIMUserAlreadyExists) Error() string { - return fmt.Sprintf("user with email %s already exists in this organization", e.Email) -} - -func NewSCIMUserAlreadyExistsError(email string) *ErrSCIMUserAlreadyExists { - return &ErrSCIMUserAlreadyExists{Email: email} -} - -type ErrUnsupportedFilter struct { - Reason string -} - -func (e *ErrUnsupportedFilter) Error() string { - return fmt.Sprintf("unsupported filter: %s", e.Reason) -} diff --git a/pkg/iam/scim/service.go b/pkg/iam/scim/service.go index 4c4780f40..63a24e190 100644 --- a/pkg/iam/scim/service.go +++ b/pkg/iam/scim/service.go @@ -25,6 +25,7 @@ import ( "time" "github.com/elimity-com/scim" + scimerrors "github.com/elimity-com/scim/errors" "github.com/elimity-com/scim/optional" "go.gearno.de/kit/log" "go.gearno.de/kit/pg" @@ -93,19 +94,19 @@ func (s *Service) CreateUser( config *coredata.SCIMConfiguration, attributes scim.ResourceAttributes, ipAddress net.IP, -) (*coredata.Membership, error) { +) (scim.Resource, error) { user := ParseUserFromAttributes(attributes) - email := user.GetPrimaryEmail() + email := user.Email if email == "" { - return nil, NewSCIMInvalidRequestError("userName or email is required") + return scim.Resource{}, scimerrors.ScimErrorBadRequest("userName or email is required") } emailAddr, err := mail.ParseAddr(email) if err != nil { - return nil, NewSCIMInvalidRequestError("invalid email format") + return scim.Resource{}, scimerrors.ScimErrorBadRequest("invalid email format") } - fullName := user.GetFullName() + fullName := user.FullName now := time.Now() var membership *coredata.Membership @@ -195,10 +196,10 @@ func (s *Service) CreateUser( }) if err != nil { - return nil, err + return scim.Resource{}, err } - return membership, nil + return membershipToResource(membership, true), nil } // GetUser gets a user by membership ID @@ -207,12 +208,10 @@ func (s *Service) GetUser( config *coredata.SCIMConfiguration, membershipID gid.GID, ipAddress net.IP, -) (*coredata.Membership, *coredata.Identity, *coredata.MembershipProfile, error) { +) (scim.Resource, error) { scope := coredata.NewScopeFromObjectID(config.OrganizationID) var membership *coredata.Membership - var identity *coredata.Identity - var profile *coredata.MembershipProfile err := s.pg.WithConn( ctx, @@ -221,26 +220,14 @@ func (s *Service) GetUser( err := membership.LoadByID(ctx, conn, scope, membershipID) if err != nil { if err == coredata.ErrResourceNotFound { - return NewSCIMUserNotFoundError(membershipID) + return scimerrors.ScimErrorResourceNotFound(membershipID.String()) } return fmt.Errorf("cannot load membership: %w", err) } // Verify membership belongs to this organization if membership.OrganizationID != config.OrganizationID { - return NewSCIMUserNotFoundError(membershipID) - } - - identity = &coredata.Identity{} - err = identity.LoadByID(ctx, conn, membership.IdentityID) - if err != nil { - return fmt.Errorf("cannot load identity: %w", err) - } - - profile = &coredata.MembershipProfile{} - err = profile.LoadByMembershipID(ctx, conn, scope, membershipID) - if err != nil && err != coredata.ErrResourceNotFound { - return fmt.Errorf("cannot load membership profile: %w", err) + return scimerrors.ScimErrorResourceNotFound(membershipID.String()) } return nil @@ -248,10 +235,10 @@ func (s *Service) GetUser( ) if err != nil { - return nil, nil, nil, err + return scim.Resource{}, err } - return membership, identity, profile, nil + return membershipToResource(membership, true), nil } // ListUsers lists all users in an organization, with optional filter support @@ -262,7 +249,7 @@ func (s *Service) ListUsers( startIndex int, count int, ipAddress net.IP, -) ([]*coredata.Membership, int, error) { +) ([]scim.Resource, int, error) { scope := coredata.NewScopeFromObjectID(config.OrganizationID) var memberships coredata.Memberships @@ -318,33 +305,44 @@ func (s *Service) ListUsers( return nil, 0, err } - return memberships, totalCount, nil + resources := make([]scim.Resource, 0, len(memberships)) + for _, m := range memberships { + resources = append(resources, membershipToResource(m, true)) + } + + return resources, totalCount, nil } // ReplaceUser replaces a user via SCIM PUT -// Returns the membership, a boolean indicating if user was deactivated, and an error func (s *Service) ReplaceUser( ctx context.Context, config *coredata.SCIMConfiguration, membershipID gid.GID, attributes scim.ResourceAttributes, ipAddress net.IP, -) (*coredata.Membership, bool, error) { +) (scim.Resource, error) { user := ParseUserFromReplaceAttributes(attributes) - return s.updateUser(ctx, config, membershipID, user, "PUT", ipAddress) + membership, deactivated, err := s.updateUser(ctx, config, membershipID, user, "PUT", ipAddress) + if err != nil { + return scim.Resource{}, err + } + return membershipToResource(membership, !deactivated), nil } // PatchUser patches a user via SCIM PATCH -// Returns the membership, a boolean indicating if user was deactivated, and an error func (s *Service) PatchUser( ctx context.Context, config *coredata.SCIMConfiguration, membershipID gid.GID, operations []scim.PatchOperation, ipAddress net.IP, -) (*coredata.Membership, bool, error) { +) (scim.Resource, error) { user := ParseUserFromPatchOperations(operations) - return s.updateUser(ctx, config, membershipID, user, "PATCH", ipAddress) + membership, deactivated, err := s.updateUser(ctx, config, membershipID, user, "PATCH", ipAddress) + if err != nil { + return scim.Resource{}, err + } + return membershipToResource(membership, !deactivated), nil } func (s *Service) updateUser( @@ -366,14 +364,14 @@ func (s *Service) updateUser( err := membership.LoadByID(ctx, tx, scope, membershipID) if err != nil { if err == coredata.ErrResourceNotFound { - return NewSCIMUserNotFoundError(membershipID) + return scimerrors.ScimErrorResourceNotFound(membershipID.String()) } return fmt.Errorf("cannot load membership: %w", err) } // Verify membership belongs to this organization if membership.OrganizationID != config.OrganizationID { - return NewSCIMUserNotFoundError(membershipID) + return scimerrors.ScimErrorResourceNotFound(membershipID.String()) } // Handle deactivation - Okta sends PATCH with active=false to deprovision users @@ -410,7 +408,7 @@ func (s *Service) updateUser( profile := &coredata.MembershipProfile{} err = profile.LoadByMembershipID(ctx, tx, scope, membershipID) if err == nil { - fullName := user.GetFullName() + fullName := user.FullName if fullName != "" { profile.FullName = fullName profile.UpdatedAt = now @@ -453,14 +451,14 @@ func (s *Service) DeleteUser( err := membership.LoadByID(ctx, tx, scope, membershipID) if err != nil { if err == coredata.ErrResourceNotFound { - return NewSCIMUserNotFoundError(membershipID) + return scimerrors.ScimErrorResourceNotFound(membershipID.String()) } return fmt.Errorf("cannot load membership: %w", err) } // Verify membership belongs to this organization if membership.OrganizationID != config.OrganizationID { - return NewSCIMUserNotFoundError(membershipID) + return scimerrors.ScimErrorResourceNotFound(membershipID.String()) } err = membership.Delete(ctx, tx, scope, membershipID) @@ -537,6 +535,7 @@ func (s *Service) createEvent( } // ParseUserFromAttributes extracts a User from SCIM resource attributes +// ParseUserFromAttributes extracts user data from SCIM create attributes func ParseUserFromAttributes(attributes scim.ResourceAttributes) *User { userName, _ := attributes["userName"].(string) displayName, _ := attributes["displayName"].(string) @@ -570,7 +569,7 @@ func ParseUserFromAttributes(attributes scim.ResourceAttributes) *User { } } - // Build full name + // Build full name: prefer displayName, then given+family, then userName fullName := displayName if fullName == "" { fullName = strings.TrimSpace(givenName + " " + familyName) @@ -579,26 +578,13 @@ func ParseUserFromAttributes(attributes scim.ResourceAttributes) *User { fullName = userName } - user := &User{ - UserName: userName, - DisplayName: displayName, - Name: &Name{ - GivenName: givenName, - FamilyName: familyName, - Formatted: fullName, - }, - Emails: []Email{ - { - Value: email, - Primary: true, - }, - }, + return &User{ + Email: email, + FullName: fullName, } - - return user } -// ParseUserFromReplaceAttributes extracts a User from SCIM replace attributes +// ParseUserFromReplaceAttributes extracts user data from SCIM replace (PUT) attributes func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) *User { displayName, _ := attributes["displayName"].(string) @@ -619,19 +605,16 @@ func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) *User { } return &User{ - DisplayName: fullName, - Active: &active, - Name: &Name{ - GivenName: givenName, - FamilyName: familyName, - Formatted: fullName, - }, + FullName: fullName, + Active: &active, } } -// ParseUserFromPatchOperations extracts a User from SCIM patch operations +// ParseUserFromPatchOperations extracts user data from SCIM patch operations func ParseUserFromPatchOperations(operations []scim.PatchOperation) *User { user := &User{} + var givenName, familyName string + for _, op := range operations { if strings.EqualFold(op.Op, "replace") || strings.EqualFold(op.Op, "add") { path := "" @@ -645,37 +628,29 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) *User { } case "displayname": if name, ok := op.Value.(string); ok { - user.DisplayName = name + user.FullName = name } case "name.givenname": - if user.Name == nil { - user.Name = &Name{} - } if name, ok := op.Value.(string); ok { - user.Name.GivenName = name + givenName = name } case "name.familyname": - if user.Name == nil { - user.Name = &Name{} - } if name, ok := op.Value.(string); ok { - user.Name.FamilyName = name + familyName = name } } } } + + // If no displayName was set but we have name parts, build full name + if user.FullName == "" && (givenName != "" || familyName != "") { + user.FullName = strings.TrimSpace(givenName + " " + familyName) + } + return user } -// MembershipToResource converts a Membership to a SCIM resource -func MembershipToResource(m *coredata.Membership) scim.Resource { - return MembershipToResourceWithActive(m, true) -} - -// MembershipToResourceWithActive converts a Membership to a SCIM resource with a custom active state -func MembershipToResourceWithActive(m *coredata.Membership, active bool) scim.Resource { - created := m.CreatedAt - modified := m.UpdatedAt +func membershipToResource(m *coredata.Membership, active bool) scim.Resource { return scim.Resource{ ID: m.ID.String(), ExternalID: optional.NewString(m.ID.String()), @@ -695,42 +670,8 @@ func MembershipToResourceWithActive(m *coredata.Membership, active bool) scim.Re }, }, Meta: scim.Meta{ - Created: &created, - LastModified: &modified, - }, - } -} - -// MembershipToResourceFull converts a Membership with full identity and profile to a SCIM resource -func MembershipToResourceFull(m *coredata.Membership, identity *coredata.Identity, profile *coredata.MembershipProfile) scim.Resource { - fullName := identity.FullName - if profile != nil && profile.FullName != "" { - fullName = profile.FullName - } - - created := m.CreatedAt - modified := m.UpdatedAt - return scim.Resource{ - ID: m.ID.String(), - ExternalID: optional.NewString(m.ID.String()), - Attributes: scim.ResourceAttributes{ - "userName": identity.EmailAddress.String(), - "displayName": fullName, - "active": true, - "name": map[string]interface{}{ - "formatted": fullName, - }, - "emails": []map[string]interface{}{ - { - "value": identity.EmailAddress.String(), - "type": "work", - "primary": true, - }, - }, - }, - Meta: scim.Meta{ - Created: &created, - LastModified: &modified, + Created: &m.CreatedAt, + LastModified: &m.UpdatedAt, }, } } diff --git a/pkg/iam/scim/types.go b/pkg/iam/scim/types.go index e7b87cc17..10e0ec862 100644 --- a/pkg/iam/scim/types.go +++ b/pkg/iam/scim/types.go @@ -18,6 +18,7 @@ import ( "fmt" "strings" + scimerrors "github.com/elimity-com/scim/errors" scimfilter "github.com/scim2/filter-parser/v2" ) @@ -44,17 +45,17 @@ func ParseUserFilter(expr scimfilter.Expression) (*UserFilter, error) { } case *scimfilter.LogicalExpression: if e.Operator != scimfilter.AND { - return nil, &ErrUnsupportedFilter{Reason: fmt.Sprintf("logical operator '%s' is not supported, only 'and' is supported", e.Operator)} + return nil, scimerrors.ScimErrorBadRequest(fmt.Sprintf("logical operator '%s' is not supported, only 'and' is supported", e.Operator)) } if err := parseLogicalExpression(e, filter); err != nil { return nil, err } case *scimfilter.NotExpression: - return nil, &ErrUnsupportedFilter{Reason: "NOT expressions are not supported"} + return nil, scimerrors.ScimErrorBadRequest("NOT expressions are not supported") case *scimfilter.ValuePath: - return nil, &ErrUnsupportedFilter{Reason: "value path expressions are not supported"} + return nil, scimerrors.ScimErrorBadRequest("value path expressions are not supported") default: - return nil, &ErrUnsupportedFilter{Reason: "unknown filter expression type"} + return nil, scimerrors.ScimErrorBadRequest("unknown filter expression type") } return filter, nil @@ -63,7 +64,7 @@ func ParseUserFilter(expr scimfilter.Expression) (*UserFilter, error) { func parseAttributeExpression(e *scimfilter.AttributeExpression, filter *UserFilter) error { // Only support "eq" operator if e.Operator != scimfilter.EQ { - return &ErrUnsupportedFilter{Reason: fmt.Sprintf("operator '%s' is not supported, only 'eq' is supported", e.Operator)} + return scimerrors.ScimErrorBadRequest(fmt.Sprintf("operator '%s' is not supported, only 'eq' is supported", e.Operator)) } // Get the attribute name (lowercase for comparison) @@ -72,14 +73,14 @@ func parseAttributeExpression(e *scimfilter.AttributeExpression, filter *UserFil // Extract the string value value, ok := e.CompareValue.(string) if !ok { - return &ErrUnsupportedFilter{Reason: "filter value must be a string"} + return scimerrors.ScimErrorBadRequest("filter value must be a string") } switch attrName { case "username": filter.UserName = &value default: - return &ErrUnsupportedFilter{Reason: fmt.Sprintf("attribute '%s' is not supported for filtering, only 'userName' is supported", e.AttributePath.AttributeName)} + return scimerrors.ScimErrorBadRequest(fmt.Sprintf("attribute '%s' is not supported for filtering, only 'userName' is supported", e.AttributePath.AttributeName)) } return nil @@ -92,7 +93,7 @@ func parseLogicalExpression(e *scimfilter.LogicalExpression, filter *UserFilter) return err } } else { - return &ErrUnsupportedFilter{Reason: "nested logical expressions are not supported"} + return scimerrors.ScimErrorBadRequest("nested logical expressions are not supported") } // Process right expression @@ -101,224 +102,15 @@ func parseLogicalExpression(e *scimfilter.LogicalExpression, filter *UserFilter) return err } } else { - return &ErrUnsupportedFilter{Reason: "nested logical expressions are not supported"} + return scimerrors.ScimErrorBadRequest("nested logical expressions are not supported") } return nil } -// SCIM 2.0 User Resource -// https://datatracker.ietf.org/doc/html/rfc7643#section-4.1 +// User represents parsed SCIM user attributes with extracted values. type User struct { - Schemas []string `json:"schemas"` - ID string `json:"id,omitempty"` - ExternalID string `json:"externalId,omitempty"` - UserName string `json:"userName"` - Name *Name `json:"name,omitempty"` - DisplayName string `json:"displayName,omitempty"` - Emails []Email `json:"emails,omitempty"` - Active *bool `json:"active,omitempty"` - Meta *Meta `json:"meta,omitempty"` -} - -type Name struct { - Formatted string `json:"formatted,omitempty"` - FamilyName string `json:"familyName,omitempty"` - GivenName string `json:"givenName,omitempty"` - MiddleName string `json:"middleName,omitempty"` - HonorificPrefix string `json:"honorificPrefix,omitempty"` - HonorificSuffix string `json:"honorificSuffix,omitempty"` -} - -type Email struct { - Value string `json:"value"` - Type string `json:"type,omitempty"` - Primary bool `json:"primary,omitempty"` -} - -type Meta struct { - ResourceType string `json:"resourceType,omitempty"` - Created string `json:"created,omitempty"` - LastModified string `json:"lastModified,omitempty"` - Location string `json:"location,omitempty"` - Version string `json:"version,omitempty"` -} - -// SCIM 2.0 List Response -// https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2 -type ListResponse struct { - Schemas []string `json:"schemas"` - TotalResults int `json:"totalResults"` - StartIndex int `json:"startIndex,omitempty"` - ItemsPerPage int `json:"itemsPerPage,omitempty"` - Resources []User `json:"Resources"` -} - -// SCIM 2.0 Error Response -// https://datatracker.ietf.org/doc/html/rfc7644#section-3.12 -type ErrorResponse struct { - Schemas []string `json:"schemas"` - Detail string `json:"detail,omitempty"` - Status string `json:"status"` - ScimType string `json:"scimType,omitempty"` -} - -// SCIM 2.0 Patch Operation -// https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2 -type PatchOp struct { - Schemas []string `json:"schemas"` - Operations []Operation `json:"Operations"` -} - -type Operation struct { - Op string `json:"op"` - Path string `json:"path,omitempty"` - Value interface{} `json:"value,omitempty"` -} - -// SCIM 2.0 Service Provider Config -// https://datatracker.ietf.org/doc/html/rfc7643#section-5 -type ServiceProviderConfig struct { - Schemas []string `json:"schemas"` - DocumentationUri string `json:"documentationUri,omitempty"` - Patch Supported `json:"patch"` - Bulk BulkSupported `json:"bulk"` - Filter FilterSupported `json:"filter"` - ChangePassword Supported `json:"changePassword"` - Sort Supported `json:"sort"` - Etag Supported `json:"etag"` - AuthenticationSchemes []AuthScheme `json:"authenticationSchemes"` - Meta *Meta `json:"meta,omitempty"` -} - -type Supported struct { - Supported bool `json:"supported"` -} - -type BulkSupported struct { - Supported bool `json:"supported"` - MaxOperations int `json:"maxOperations"` - MaxPayloadSize int `json:"maxPayloadSize"` -} - -type FilterSupported struct { - Supported bool `json:"supported"` - MaxResults int `json:"maxResults"` -} - -type AuthScheme struct { - Type string `json:"type"` - Name string `json:"name"` - Description string `json:"description"` - SpecUri string `json:"specUri,omitempty"` - DocumentationUri string `json:"documentationUri,omitempty"` - Primary bool `json:"primary,omitempty"` -} - -// SCIM 2.0 Schemas response -type SchemasResponse struct { - Schemas []string `json:"schemas"` - TotalResults int `json:"totalResults"` - Resources []Schema `json:"Resources"` -} - -type Schema struct { - ID string `json:"id"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Attributes []SchemaAttribute `json:"attributes,omitempty"` - Meta *Meta `json:"meta,omitempty"` -} - -type SchemaAttribute struct { - Name string `json:"name"` - Type string `json:"type"` - MultiValued bool `json:"multiValued"` - Description string `json:"description,omitempty"` - Required bool `json:"required"` - CaseExact bool `json:"caseExact,omitempty"` - Mutability string `json:"mutability,omitempty"` - Returned string `json:"returned,omitempty"` - Uniqueness string `json:"uniqueness,omitempty"` - SubAttributes []SchemaAttribute `json:"subAttributes,omitempty"` -} - -// SCIM Schema URIs -const ( - SchemaURIUser = "urn:ietf:params:scim:schemas:core:2.0:User" - SchemaURIListResponse = "urn:ietf:params:scim:api:messages:2.0:ListResponse" - SchemaURIError = "urn:ietf:params:scim:api:messages:2.0:Error" - SchemaURIPatchOp = "urn:ietf:params:scim:api:messages:2.0:PatchOp" - SchemaURIServiceProviderConfig = "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig" - SchemaURISchema = "urn:ietf:params:scim:schemas:core:2.0:Schema" -) - -func NewUser() *User { - return &User{ - Schemas: []string{SchemaURIUser}, - } -} - -func NewListResponse(users []User, totalResults int) *ListResponse { - return &ListResponse{ - Schemas: []string{SchemaURIListResponse}, - TotalResults: totalResults, - StartIndex: 1, - ItemsPerPage: len(users), - Resources: users, - } -} - -func NewErrorResponse(status int, detail string, scimType string) *ErrorResponse { - return &ErrorResponse{ - Schemas: []string{SchemaURIError}, - Detail: detail, - Status: fmt.Sprintf("%d", status), - ScimType: scimType, - } -} - -func (u *User) GetPrimaryEmail() string { - for _, email := range u.Emails { - if email.Primary { - return email.Value - } - } - if len(u.Emails) > 0 { - return u.Emails[0].Value - } - return u.UserName -} - -func (u *User) GetFullName() string { - if u.DisplayName != "" { - return u.DisplayName - } - if u.Name != nil { - if u.Name.Formatted != "" { - return u.Name.Formatted - } - parts := []string{} - if u.Name.GivenName != "" { - parts = append(parts, u.Name.GivenName) - } - if u.Name.FamilyName != "" { - parts = append(parts, u.Name.FamilyName) - } - if len(parts) > 0 { - return join(parts, " ") - } - } - return u.UserName -} - -func join(parts []string, sep string) string { - if len(parts) == 0 { - return "" - } - result := parts[0] - for i := 1; i < len(parts); i++ { - result += sep + parts[i] - } - return result + Email string + FullName string + Active *bool } diff --git a/pkg/server/api/connect/v1/scim_handler.go b/pkg/server/api/connect/v1/scim_handler.go index 75910881d..e336bb75b 100644 --- a/pkg/server/api/connect/v1/scim_handler.go +++ b/pkg/server/api/connect/v1/scim_handler.go @@ -131,24 +131,17 @@ func (h *scimResourceHandler) Create(r *http.Request, attributes scim.ResourceAt ctx := r.Context() config := scimConfigFromContext(ctx) - membership, err := h.handler.iam.SCIMService.CreateUser(ctx, config, attributes, getIPAddress(r)) + resource, err := h.handler.iam.SCIMService.CreateUser(ctx, config, attributes, getIPAddress(r)) if err != nil { - var invalidReq *scimservice.ErrSCIMInvalidRequest - var alreadyExists *scimservice.ErrSCIMUserAlreadyExists - - if errors.As(err, &invalidReq) { - return scim.Resource{}, scimerrors.ScimErrorBadRequest(invalidReq.Detail) + var scimErr scimerrors.ScimError + if errors.As(err, &scimErr) { + return scim.Resource{}, err } - - if errors.As(err, &alreadyExists) { - return scim.Resource{}, scimerrors.ScimErrorUniqueness - } - h.handler.logger.ErrorCtx(ctx, "cannot create user", log.Error(err)) return scim.Resource{}, scimerrors.ScimErrorInternal } - return scimservice.MembershipToResource(membership), nil + return resource, nil } func (h *scimResourceHandler) Get(r *http.Request, id string) (scim.Resource, error) { @@ -160,18 +153,17 @@ func (h *scimResourceHandler) Get(r *http.Request, id string) (scim.Resource, er return scim.Resource{}, scimerrors.ScimErrorResourceNotFound(id) } - membership, identity, profile, err := h.handler.iam.SCIMService.GetUser(ctx, config, membershipID, getIPAddress(r)) + resource, err := h.handler.iam.SCIMService.GetUser(ctx, config, membershipID, getIPAddress(r)) if err != nil { - var notFound *scimservice.ErrSCIMUserNotFound - if errors.As(err, ¬Found) { - return scim.Resource{}, scimerrors.ScimErrorResourceNotFound(id) + var scimErr scimerrors.ScimError + if errors.As(err, &scimErr) { + return scim.Resource{}, err } - h.handler.logger.ErrorCtx(ctx, "cannot get user", log.Error(err)) return scim.Resource{}, scimerrors.ScimErrorInternal } - return scimservice.MembershipToResourceFull(membership, identity, profile), nil + return resource, nil } func (h *scimResourceHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) { @@ -185,25 +177,24 @@ func (h *scimResourceHandler) GetAll(r *http.Request, params scim.ListRequestPar // Parse SCIM filter AST into our filter type filter, err := scimservice.ParseUserFilter(params.FilterValidator.GetFilter()) if err != nil { - var unsupportedFilter *scimservice.ErrUnsupportedFilter - if errors.As(err, &unsupportedFilter) { - return scim.Page{}, scimerrors.ScimErrorBadRequest(err.Error()) + var scimErr scimerrors.ScimError + if errors.As(err, &scimErr) { + return scim.Page{}, err } h.handler.logger.ErrorCtx(ctx, "cannot parse filter", log.Error(err)) return scim.Page{}, scimerrors.ScimErrorInternal } - memberships, totalCount, err := h.handler.iam.SCIMService.ListUsers(ctx, config, filter, params.StartIndex, params.Count, getIPAddress(r)) + resources, totalCount, err := h.handler.iam.SCIMService.ListUsers(ctx, config, filter, params.StartIndex, params.Count, getIPAddress(r)) if err != nil { + var scimErr scimerrors.ScimError + if errors.As(err, &scimErr) { + return scim.Page{}, err + } h.handler.logger.ErrorCtx(ctx, "cannot list users", log.Error(err)) return scim.Page{}, scimerrors.ScimErrorInternal } - resources := make([]scim.Resource, 0, len(memberships)) - for _, m := range memberships { - resources = append(resources, scimservice.MembershipToResource(m)) - } - return scim.Page{ TotalResults: totalCount, Resources: resources, @@ -219,22 +210,17 @@ func (h *scimResourceHandler) Replace(r *http.Request, id string, attributes sci return scim.Resource{}, scimerrors.ScimErrorResourceNotFound(id) } - membership, deactivated, err := h.handler.iam.SCIMService.ReplaceUser(ctx, config, membershipID, attributes, getIPAddress(r)) + resource, err := h.handler.iam.SCIMService.ReplaceUser(ctx, config, membershipID, attributes, getIPAddress(r)) if err != nil { - var notFound *scimservice.ErrSCIMUserNotFound - if errors.As(err, ¬Found) { - return scim.Resource{}, scimerrors.ScimErrorResourceNotFound(id) + var scimErr scimerrors.ScimError + if errors.As(err, &scimErr) { + return scim.Resource{}, err } - h.handler.logger.ErrorCtx(ctx, "cannot update user", log.Error(err)) return scim.Resource{}, scimerrors.ScimErrorInternal } - if deactivated { - return scimservice.MembershipToResourceWithActive(membership, false), nil - } - - return scimservice.MembershipToResource(membership), nil + return resource, nil } func (h *scimResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) { @@ -246,22 +232,17 @@ func (h *scimResourceHandler) Patch(r *http.Request, id string, operations []sci return scim.Resource{}, scimerrors.ScimErrorResourceNotFound(id) } - membership, deactivated, err := h.handler.iam.SCIMService.PatchUser(ctx, config, membershipID, operations, getIPAddress(r)) + resource, err := h.handler.iam.SCIMService.PatchUser(ctx, config, membershipID, operations, getIPAddress(r)) if err != nil { - var notFound *scimservice.ErrSCIMUserNotFound - if errors.As(err, ¬Found) { - return scim.Resource{}, scimerrors.ScimErrorResourceNotFound(id) + var scimErr scimerrors.ScimError + if errors.As(err, &scimErr) { + return scim.Resource{}, err } - h.handler.logger.ErrorCtx(ctx, "cannot patch user", log.Error(err)) return scim.Resource{}, scimerrors.ScimErrorInternal } - if deactivated { - return scimservice.MembershipToResourceWithActive(membership, false), nil - } - - return scimservice.MembershipToResource(membership), nil + return resource, nil } func (h *scimResourceHandler) Delete(r *http.Request, id string) error { @@ -275,11 +256,10 @@ func (h *scimResourceHandler) Delete(r *http.Request, id string) error { err = h.handler.iam.SCIMService.DeleteUser(ctx, config, membershipID, getIPAddress(r)) if err != nil { - var notFound *scimservice.ErrSCIMUserNotFound - if errors.As(err, ¬Found) { - return scimerrors.ScimErrorResourceNotFound(id) + var scimErr scimerrors.ScimError + if errors.As(err, &scimErr) { + return err } - h.handler.logger.ErrorCtx(ctx, "cannot delete user", log.Error(err)) return scimerrors.ScimErrorInternal }