Simplify SCIM types/error management
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -44,18 +44,6 @@ func NewSCIMConfigurationAlreadyExistsError(organizationID gid.GID) *ErrSCIMConf
|
|||||||
return &ErrSCIMConfigurationAlreadyExists{OrganizationID: organizationID}
|
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{}
|
type ErrSCIMInvalidToken struct{}
|
||||||
|
|
||||||
func (e *ErrSCIMInvalidToken) Error() string {
|
func (e *ErrSCIMInvalidToken) Error() string {
|
||||||
@@ -65,35 +53,3 @@ func (e *ErrSCIMInvalidToken) Error() string {
|
|||||||
func NewSCIMInvalidTokenError() *ErrSCIMInvalidToken {
|
func NewSCIMInvalidTokenError() *ErrSCIMInvalidToken {
|
||||||
return &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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/elimity-com/scim"
|
"github.com/elimity-com/scim"
|
||||||
|
scimerrors "github.com/elimity-com/scim/errors"
|
||||||
"github.com/elimity-com/scim/optional"
|
"github.com/elimity-com/scim/optional"
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
@@ -93,19 +94,19 @@ func (s *Service) CreateUser(
|
|||||||
config *coredata.SCIMConfiguration,
|
config *coredata.SCIMConfiguration,
|
||||||
attributes scim.ResourceAttributes,
|
attributes scim.ResourceAttributes,
|
||||||
ipAddress net.IP,
|
ipAddress net.IP,
|
||||||
) (*coredata.Membership, error) {
|
) (scim.Resource, error) {
|
||||||
user := ParseUserFromAttributes(attributes)
|
user := ParseUserFromAttributes(attributes)
|
||||||
email := user.GetPrimaryEmail()
|
email := user.Email
|
||||||
if 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)
|
emailAddr, err := mail.ParseAddr(email)
|
||||||
if err != nil {
|
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()
|
now := time.Now()
|
||||||
|
|
||||||
var membership *coredata.Membership
|
var membership *coredata.Membership
|
||||||
@@ -195,10 +196,10 @@ func (s *Service) CreateUser(
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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
|
// GetUser gets a user by membership ID
|
||||||
@@ -207,12 +208,10 @@ func (s *Service) GetUser(
|
|||||||
config *coredata.SCIMConfiguration,
|
config *coredata.SCIMConfiguration,
|
||||||
membershipID gid.GID,
|
membershipID gid.GID,
|
||||||
ipAddress net.IP,
|
ipAddress net.IP,
|
||||||
) (*coredata.Membership, *coredata.Identity, *coredata.MembershipProfile, error) {
|
) (scim.Resource, error) {
|
||||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||||
|
|
||||||
var membership *coredata.Membership
|
var membership *coredata.Membership
|
||||||
var identity *coredata.Identity
|
|
||||||
var profile *coredata.MembershipProfile
|
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -221,26 +220,14 @@ func (s *Service) GetUser(
|
|||||||
err := membership.LoadByID(ctx, conn, scope, membershipID)
|
err := membership.LoadByID(ctx, conn, scope, membershipID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == coredata.ErrResourceNotFound {
|
if err == coredata.ErrResourceNotFound {
|
||||||
return NewSCIMUserNotFoundError(membershipID)
|
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||||
}
|
}
|
||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify membership belongs to this organization
|
// Verify membership belongs to this organization
|
||||||
if membership.OrganizationID != config.OrganizationID {
|
if membership.OrganizationID != config.OrganizationID {
|
||||||
return NewSCIMUserNotFoundError(membershipID)
|
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||||
}
|
|
||||||
|
|
||||||
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 nil
|
return nil
|
||||||
@@ -248,10 +235,10 @@ func (s *Service) GetUser(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
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
|
// ListUsers lists all users in an organization, with optional filter support
|
||||||
@@ -262,7 +249,7 @@ func (s *Service) ListUsers(
|
|||||||
startIndex int,
|
startIndex int,
|
||||||
count int,
|
count int,
|
||||||
ipAddress net.IP,
|
ipAddress net.IP,
|
||||||
) ([]*coredata.Membership, int, error) {
|
) ([]scim.Resource, int, error) {
|
||||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||||
|
|
||||||
var memberships coredata.Memberships
|
var memberships coredata.Memberships
|
||||||
@@ -318,33 +305,44 @@ func (s *Service) ListUsers(
|
|||||||
return nil, 0, err
|
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
|
// ReplaceUser replaces a user via SCIM PUT
|
||||||
// Returns the membership, a boolean indicating if user was deactivated, and an error
|
|
||||||
func (s *Service) ReplaceUser(
|
func (s *Service) ReplaceUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
config *coredata.SCIMConfiguration,
|
config *coredata.SCIMConfiguration,
|
||||||
membershipID gid.GID,
|
membershipID gid.GID,
|
||||||
attributes scim.ResourceAttributes,
|
attributes scim.ResourceAttributes,
|
||||||
ipAddress net.IP,
|
ipAddress net.IP,
|
||||||
) (*coredata.Membership, bool, error) {
|
) (scim.Resource, error) {
|
||||||
user := ParseUserFromReplaceAttributes(attributes)
|
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
|
// PatchUser patches a user via SCIM PATCH
|
||||||
// Returns the membership, a boolean indicating if user was deactivated, and an error
|
|
||||||
func (s *Service) PatchUser(
|
func (s *Service) PatchUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
config *coredata.SCIMConfiguration,
|
config *coredata.SCIMConfiguration,
|
||||||
membershipID gid.GID,
|
membershipID gid.GID,
|
||||||
operations []scim.PatchOperation,
|
operations []scim.PatchOperation,
|
||||||
ipAddress net.IP,
|
ipAddress net.IP,
|
||||||
) (*coredata.Membership, bool, error) {
|
) (scim.Resource, error) {
|
||||||
user := ParseUserFromPatchOperations(operations)
|
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(
|
func (s *Service) updateUser(
|
||||||
@@ -366,14 +364,14 @@ func (s *Service) updateUser(
|
|||||||
err := membership.LoadByID(ctx, tx, scope, membershipID)
|
err := membership.LoadByID(ctx, tx, scope, membershipID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == coredata.ErrResourceNotFound {
|
if err == coredata.ErrResourceNotFound {
|
||||||
return NewSCIMUserNotFoundError(membershipID)
|
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||||
}
|
}
|
||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify membership belongs to this organization
|
// Verify membership belongs to this organization
|
||||||
if membership.OrganizationID != config.OrganizationID {
|
if membership.OrganizationID != config.OrganizationID {
|
||||||
return NewSCIMUserNotFoundError(membershipID)
|
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle deactivation - Okta sends PATCH with active=false to deprovision users
|
// Handle deactivation - Okta sends PATCH with active=false to deprovision users
|
||||||
@@ -410,7 +408,7 @@ func (s *Service) updateUser(
|
|||||||
profile := &coredata.MembershipProfile{}
|
profile := &coredata.MembershipProfile{}
|
||||||
err = profile.LoadByMembershipID(ctx, tx, scope, membershipID)
|
err = profile.LoadByMembershipID(ctx, tx, scope, membershipID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fullName := user.GetFullName()
|
fullName := user.FullName
|
||||||
if fullName != "" {
|
if fullName != "" {
|
||||||
profile.FullName = fullName
|
profile.FullName = fullName
|
||||||
profile.UpdatedAt = now
|
profile.UpdatedAt = now
|
||||||
@@ -453,14 +451,14 @@ func (s *Service) DeleteUser(
|
|||||||
err := membership.LoadByID(ctx, tx, scope, membershipID)
|
err := membership.LoadByID(ctx, tx, scope, membershipID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == coredata.ErrResourceNotFound {
|
if err == coredata.ErrResourceNotFound {
|
||||||
return NewSCIMUserNotFoundError(membershipID)
|
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||||
}
|
}
|
||||||
return fmt.Errorf("cannot load membership: %w", err)
|
return fmt.Errorf("cannot load membership: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify membership belongs to this organization
|
// Verify membership belongs to this organization
|
||||||
if membership.OrganizationID != config.OrganizationID {
|
if membership.OrganizationID != config.OrganizationID {
|
||||||
return NewSCIMUserNotFoundError(membershipID)
|
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = membership.Delete(ctx, tx, scope, membershipID)
|
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 a User from SCIM resource attributes
|
||||||
|
// ParseUserFromAttributes extracts user data from SCIM create attributes
|
||||||
func ParseUserFromAttributes(attributes scim.ResourceAttributes) *User {
|
func ParseUserFromAttributes(attributes scim.ResourceAttributes) *User {
|
||||||
userName, _ := attributes["userName"].(string)
|
userName, _ := attributes["userName"].(string)
|
||||||
displayName, _ := attributes["displayName"].(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
|
fullName := displayName
|
||||||
if fullName == "" {
|
if fullName == "" {
|
||||||
fullName = strings.TrimSpace(givenName + " " + familyName)
|
fullName = strings.TrimSpace(givenName + " " + familyName)
|
||||||
@@ -579,26 +578,13 @@ func ParseUserFromAttributes(attributes scim.ResourceAttributes) *User {
|
|||||||
fullName = userName
|
fullName = userName
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &User{
|
return &User{
|
||||||
UserName: userName,
|
Email: email,
|
||||||
DisplayName: displayName,
|
FullName: fullName,
|
||||||
Name: &Name{
|
}
|
||||||
GivenName: givenName,
|
|
||||||
FamilyName: familyName,
|
|
||||||
Formatted: fullName,
|
|
||||||
},
|
|
||||||
Emails: []Email{
|
|
||||||
{
|
|
||||||
Value: email,
|
|
||||||
Primary: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
// ParseUserFromReplaceAttributes extracts user data from SCIM replace (PUT) attributes
|
||||||
}
|
|
||||||
|
|
||||||
// ParseUserFromReplaceAttributes extracts a User from SCIM replace attributes
|
|
||||||
func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) *User {
|
func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) *User {
|
||||||
displayName, _ := attributes["displayName"].(string)
|
displayName, _ := attributes["displayName"].(string)
|
||||||
|
|
||||||
@@ -619,19 +605,16 @@ func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) *User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &User{
|
return &User{
|
||||||
DisplayName: fullName,
|
FullName: fullName,
|
||||||
Active: &active,
|
Active: &active,
|
||||||
Name: &Name{
|
|
||||||
GivenName: givenName,
|
|
||||||
FamilyName: familyName,
|
|
||||||
Formatted: fullName,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseUserFromPatchOperations extracts a User from SCIM patch operations
|
// ParseUserFromPatchOperations extracts user data from SCIM patch operations
|
||||||
func ParseUserFromPatchOperations(operations []scim.PatchOperation) *User {
|
func ParseUserFromPatchOperations(operations []scim.PatchOperation) *User {
|
||||||
user := &User{}
|
user := &User{}
|
||||||
|
var givenName, familyName string
|
||||||
|
|
||||||
for _, op := range operations {
|
for _, op := range operations {
|
||||||
if strings.EqualFold(op.Op, "replace") || strings.EqualFold(op.Op, "add") {
|
if strings.EqualFold(op.Op, "replace") || strings.EqualFold(op.Op, "add") {
|
||||||
path := ""
|
path := ""
|
||||||
@@ -645,37 +628,29 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) *User {
|
|||||||
}
|
}
|
||||||
case "displayname":
|
case "displayname":
|
||||||
if name, ok := op.Value.(string); ok {
|
if name, ok := op.Value.(string); ok {
|
||||||
user.DisplayName = name
|
user.FullName = name
|
||||||
}
|
}
|
||||||
case "name.givenname":
|
case "name.givenname":
|
||||||
if user.Name == nil {
|
|
||||||
user.Name = &Name{}
|
|
||||||
}
|
|
||||||
if name, ok := op.Value.(string); ok {
|
if name, ok := op.Value.(string); ok {
|
||||||
user.Name.GivenName = name
|
givenName = name
|
||||||
}
|
}
|
||||||
case "name.familyname":
|
case "name.familyname":
|
||||||
if user.Name == nil {
|
|
||||||
user.Name = &Name{}
|
|
||||||
}
|
|
||||||
if name, ok := op.Value.(string); ok {
|
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
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
// MembershipToResource converts a Membership to a SCIM resource
|
func membershipToResource(m *coredata.Membership, active bool) 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
|
|
||||||
return scim.Resource{
|
return scim.Resource{
|
||||||
ID: m.ID.String(),
|
ID: m.ID.String(),
|
||||||
ExternalID: optional.NewString(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{
|
Meta: scim.Meta{
|
||||||
Created: &created,
|
Created: &m.CreatedAt,
|
||||||
LastModified: &modified,
|
LastModified: &m.UpdatedAt,
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
scimerrors "github.com/elimity-com/scim/errors"
|
||||||
scimfilter "github.com/scim2/filter-parser/v2"
|
scimfilter "github.com/scim2/filter-parser/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,17 +45,17 @@ func ParseUserFilter(expr scimfilter.Expression) (*UserFilter, error) {
|
|||||||
}
|
}
|
||||||
case *scimfilter.LogicalExpression:
|
case *scimfilter.LogicalExpression:
|
||||||
if e.Operator != scimfilter.AND {
|
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 {
|
if err := parseLogicalExpression(e, filter); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
case *scimfilter.NotExpression:
|
case *scimfilter.NotExpression:
|
||||||
return nil, &ErrUnsupportedFilter{Reason: "NOT expressions are not supported"}
|
return nil, scimerrors.ScimErrorBadRequest("NOT expressions are not supported")
|
||||||
case *scimfilter.ValuePath:
|
case *scimfilter.ValuePath:
|
||||||
return nil, &ErrUnsupportedFilter{Reason: "value path expressions are not supported"}
|
return nil, scimerrors.ScimErrorBadRequest("value path expressions are not supported")
|
||||||
default:
|
default:
|
||||||
return nil, &ErrUnsupportedFilter{Reason: "unknown filter expression type"}
|
return nil, scimerrors.ScimErrorBadRequest("unknown filter expression type")
|
||||||
}
|
}
|
||||||
|
|
||||||
return filter, nil
|
return filter, nil
|
||||||
@@ -63,7 +64,7 @@ func ParseUserFilter(expr scimfilter.Expression) (*UserFilter, error) {
|
|||||||
func parseAttributeExpression(e *scimfilter.AttributeExpression, filter *UserFilter) error {
|
func parseAttributeExpression(e *scimfilter.AttributeExpression, filter *UserFilter) error {
|
||||||
// Only support "eq" operator
|
// Only support "eq" operator
|
||||||
if e.Operator != scimfilter.EQ {
|
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)
|
// Get the attribute name (lowercase for comparison)
|
||||||
@@ -72,14 +73,14 @@ func parseAttributeExpression(e *scimfilter.AttributeExpression, filter *UserFil
|
|||||||
// Extract the string value
|
// Extract the string value
|
||||||
value, ok := e.CompareValue.(string)
|
value, ok := e.CompareValue.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return &ErrUnsupportedFilter{Reason: "filter value must be a string"}
|
return scimerrors.ScimErrorBadRequest("filter value must be a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch attrName {
|
switch attrName {
|
||||||
case "username":
|
case "username":
|
||||||
filter.UserName = &value
|
filter.UserName = &value
|
||||||
default:
|
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
|
return nil
|
||||||
@@ -92,7 +93,7 @@ func parseLogicalExpression(e *scimfilter.LogicalExpression, filter *UserFilter)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return &ErrUnsupportedFilter{Reason: "nested logical expressions are not supported"}
|
return scimerrors.ScimErrorBadRequest("nested logical expressions are not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process right expression
|
// Process right expression
|
||||||
@@ -101,224 +102,15 @@ func parseLogicalExpression(e *scimfilter.LogicalExpression, filter *UserFilter)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return &ErrUnsupportedFilter{Reason: "nested logical expressions are not supported"}
|
return scimerrors.ScimErrorBadRequest("nested logical expressions are not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SCIM 2.0 User Resource
|
// User represents parsed SCIM user attributes with extracted values.
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7643#section-4.1
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Schemas []string `json:"schemas"`
|
Email string
|
||||||
ID string `json:"id,omitempty"`
|
FullName string
|
||||||
ExternalID string `json:"externalId,omitempty"`
|
Active *bool
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,24 +131,17 @@ func (h *scimResourceHandler) Create(r *http.Request, attributes scim.ResourceAt
|
|||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
config := scimConfigFromContext(ctx)
|
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 {
|
if err != nil {
|
||||||
var invalidReq *scimservice.ErrSCIMInvalidRequest
|
var scimErr scimerrors.ScimError
|
||||||
var alreadyExists *scimservice.ErrSCIMUserAlreadyExists
|
if errors.As(err, &scimErr) {
|
||||||
|
return scim.Resource{}, err
|
||||||
if errors.As(err, &invalidReq) {
|
|
||||||
return scim.Resource{}, scimerrors.ScimErrorBadRequest(invalidReq.Detail)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.As(err, &alreadyExists) {
|
|
||||||
return scim.Resource{}, scimerrors.ScimErrorUniqueness
|
|
||||||
}
|
|
||||||
|
|
||||||
h.handler.logger.ErrorCtx(ctx, "cannot create user", log.Error(err))
|
h.handler.logger.ErrorCtx(ctx, "cannot create user", log.Error(err))
|
||||||
return scim.Resource{}, scimerrors.ScimErrorInternal
|
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) {
|
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)
|
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 {
|
if err != nil {
|
||||||
var notFound *scimservice.ErrSCIMUserNotFound
|
var scimErr scimerrors.ScimError
|
||||||
if errors.As(err, ¬Found) {
|
if errors.As(err, &scimErr) {
|
||||||
return scim.Resource{}, scimerrors.ScimErrorResourceNotFound(id)
|
return scim.Resource{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
h.handler.logger.ErrorCtx(ctx, "cannot get user", log.Error(err))
|
h.handler.logger.ErrorCtx(ctx, "cannot get user", log.Error(err))
|
||||||
return scim.Resource{}, scimerrors.ScimErrorInternal
|
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) {
|
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
|
// Parse SCIM filter AST into our filter type
|
||||||
filter, err := scimservice.ParseUserFilter(params.FilterValidator.GetFilter())
|
filter, err := scimservice.ParseUserFilter(params.FilterValidator.GetFilter())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var unsupportedFilter *scimservice.ErrUnsupportedFilter
|
var scimErr scimerrors.ScimError
|
||||||
if errors.As(err, &unsupportedFilter) {
|
if errors.As(err, &scimErr) {
|
||||||
return scim.Page{}, scimerrors.ScimErrorBadRequest(err.Error())
|
return scim.Page{}, err
|
||||||
}
|
}
|
||||||
h.handler.logger.ErrorCtx(ctx, "cannot parse filter", log.Error(err))
|
h.handler.logger.ErrorCtx(ctx, "cannot parse filter", log.Error(err))
|
||||||
return scim.Page{}, scimerrors.ScimErrorInternal
|
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 {
|
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))
|
h.handler.logger.ErrorCtx(ctx, "cannot list users", log.Error(err))
|
||||||
return scim.Page{}, scimerrors.ScimErrorInternal
|
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{
|
return scim.Page{
|
||||||
TotalResults: totalCount,
|
TotalResults: totalCount,
|
||||||
Resources: resources,
|
Resources: resources,
|
||||||
@@ -219,22 +210,17 @@ func (h *scimResourceHandler) Replace(r *http.Request, id string, attributes sci
|
|||||||
return scim.Resource{}, scimerrors.ScimErrorResourceNotFound(id)
|
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 {
|
if err != nil {
|
||||||
var notFound *scimservice.ErrSCIMUserNotFound
|
var scimErr scimerrors.ScimError
|
||||||
if errors.As(err, ¬Found) {
|
if errors.As(err, &scimErr) {
|
||||||
return scim.Resource{}, scimerrors.ScimErrorResourceNotFound(id)
|
return scim.Resource{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
h.handler.logger.ErrorCtx(ctx, "cannot update user", log.Error(err))
|
h.handler.logger.ErrorCtx(ctx, "cannot update user", log.Error(err))
|
||||||
return scim.Resource{}, scimerrors.ScimErrorInternal
|
return scim.Resource{}, scimerrors.ScimErrorInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
if deactivated {
|
return resource, nil
|
||||||
return scimservice.MembershipToResourceWithActive(membership, false), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return scimservice.MembershipToResource(membership), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *scimResourceHandler) Patch(r *http.Request, id string, operations []scim.PatchOperation) (scim.Resource, error) {
|
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)
|
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 {
|
if err != nil {
|
||||||
var notFound *scimservice.ErrSCIMUserNotFound
|
var scimErr scimerrors.ScimError
|
||||||
if errors.As(err, ¬Found) {
|
if errors.As(err, &scimErr) {
|
||||||
return scim.Resource{}, scimerrors.ScimErrorResourceNotFound(id)
|
return scim.Resource{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
h.handler.logger.ErrorCtx(ctx, "cannot patch user", log.Error(err))
|
h.handler.logger.ErrorCtx(ctx, "cannot patch user", log.Error(err))
|
||||||
return scim.Resource{}, scimerrors.ScimErrorInternal
|
return scim.Resource{}, scimerrors.ScimErrorInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
if deactivated {
|
return resource, nil
|
||||||
return scimservice.MembershipToResourceWithActive(membership, false), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return scimservice.MembershipToResource(membership), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *scimResourceHandler) Delete(r *http.Request, id string) error {
|
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))
|
err = h.handler.iam.SCIMService.DeleteUser(ctx, config, membershipID, getIPAddress(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var notFound *scimservice.ErrSCIMUserNotFound
|
var scimErr scimerrors.ScimError
|
||||||
if errors.As(err, ¬Found) {
|
if errors.As(err, &scimErr) {
|
||||||
return scimerrors.ScimErrorResourceNotFound(id)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
h.handler.logger.ErrorCtx(ctx, "cannot delete user", log.Error(err))
|
h.handler.logger.ErrorCtx(ctx, "cannot delete user", log.Error(err))
|
||||||
return scimerrors.ScimErrorInternal
|
return scimerrors.ScimErrorInternal
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user