Move source and state from membership to profile
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -188,6 +188,7 @@ func (s *AccountService) AcceptInvitation(
|
||||
) (*coredata.Invitation, *coredata.Membership, error) {
|
||||
var (
|
||||
now = time.Now()
|
||||
profile = &coredata.MembershipProfile{}
|
||||
membership = &coredata.Membership{}
|
||||
invitation = &coredata.Invitation{}
|
||||
)
|
||||
@@ -228,47 +229,24 @@ func (s *AccountService) AcceptInvitation(
|
||||
tenantID := invitation.OrganizationID.TenantID()
|
||||
scope := coredata.NewScope(invitation.OrganizationID.TenantID())
|
||||
|
||||
existingMembership := &coredata.Membership{}
|
||||
if err := existingMembership.LoadByIdentityAndOrg(
|
||||
existingProfile := &coredata.MembershipProfile{}
|
||||
if err := existingProfile.LoadByIdentityIDAndOrganizationID(
|
||||
ctx,
|
||||
tx,
|
||||
scope,
|
||||
identityID,
|
||||
invitation.OrganizationID,
|
||||
); err != nil && err != coredata.ErrResourceNotFound {
|
||||
return fmt.Errorf("cannot load existing membership: %w", err)
|
||||
}
|
||||
|
||||
if existingMembership.ID != gid.Nil && existingMembership.State == coredata.MembershipStateInactive {
|
||||
existingMembership.State = coredata.MembershipStateActive
|
||||
existingMembership.Role = invitation.Role
|
||||
existingMembership.UpdatedAt = now
|
||||
|
||||
if err := existingMembership.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot reactivate membership: %w", err)
|
||||
); err != nil {
|
||||
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load existing profile: %w", err)
|
||||
}
|
||||
|
||||
membership = existingMembership
|
||||
} else {
|
||||
membership = &coredata.Membership{
|
||||
ID: gid.New(tenantID, coredata.MembershipEntityType),
|
||||
IdentityID: identityID,
|
||||
OrganizationID: invitation.OrganizationID,
|
||||
Role: invitation.Role,
|
||||
Source: coredata.MembershipSourceManual,
|
||||
State: coredata.MembershipStateActive,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := membership.Insert(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot create membership: %w", err)
|
||||
}
|
||||
|
||||
profile := &coredata.MembershipProfile{
|
||||
profile = &coredata.MembershipProfile{
|
||||
ID: gid.New(tenantID, coredata.MembershipProfileEntityType),
|
||||
IdentityID: identity.ID,
|
||||
OrganizationID: invitation.OrganizationID,
|
||||
Source: coredata.ProfileSourceManual,
|
||||
State: coredata.ProfileStateActive,
|
||||
FullName: identity.FullName,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
@@ -277,6 +255,51 @@ func (s *AccountService) AcceptInvitation(
|
||||
if err := profile.Insert(ctx, tx); err != nil {
|
||||
return fmt.Errorf("cannot insert profile: %w", err)
|
||||
}
|
||||
} else {
|
||||
if existingProfile.State == coredata.ProfileStateInactive {
|
||||
existingProfile.State = coredata.ProfileStateActive
|
||||
|
||||
if err := existingProfile.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot reactivate profile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
profile = existingProfile
|
||||
}
|
||||
|
||||
existingMembership := &coredata.Membership{}
|
||||
if err := existingMembership.LoadByIdentityAndOrg(
|
||||
ctx,
|
||||
tx,
|
||||
scope,
|
||||
identityID,
|
||||
invitation.OrganizationID,
|
||||
); err != nil {
|
||||
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load existing membership: %w", err)
|
||||
}
|
||||
|
||||
membership = &coredata.Membership{
|
||||
ID: gid.New(tenantID, coredata.MembershipEntityType),
|
||||
IdentityID: identityID,
|
||||
OrganizationID: invitation.OrganizationID,
|
||||
Role: invitation.Role,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := membership.Insert(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot create membership: %w", err)
|
||||
}
|
||||
} else {
|
||||
existingMembership.Role = invitation.Role
|
||||
existingMembership.UpdatedAt = now
|
||||
|
||||
if err := existingMembership.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot assign membership role: %w", err)
|
||||
}
|
||||
|
||||
membership = existingMembership
|
||||
}
|
||||
|
||||
invitation.AcceptedAt = &now
|
||||
@@ -384,32 +407,6 @@ func (s *AccountService) CountPendingInvitations(
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *AccountService) CountMemberships(
|
||||
ctx context.Context,
|
||||
identityID gid.GID,
|
||||
) (int, error) {
|
||||
var count int
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) (err error) {
|
||||
memberships := coredata.Memberships{}
|
||||
count, err = memberships.CountByIdentityID(ctx, conn, identityID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count memberships: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s AccountService) ChangePassword(ctx context.Context, identityID gid.GID, req *ChangePasswordRequest) error {
|
||||
if err := req.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid request: %w", err)
|
||||
@@ -857,7 +854,7 @@ func (s *AccountService) ListProfilesForIdentity(
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
if err := profiles.LoadByIdentityID(ctx, conn, coredata.NewNoScope(), identityID, cursor, filter); err != nil {
|
||||
if err := profiles.LoadByIdentityID(ctx, conn, identityID, cursor, filter); err != nil {
|
||||
return fmt.Errorf("cannot load profiles: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -81,11 +81,10 @@ func (a *Authorizer) authorize(ctx context.Context, conn pg.Conn, params Authori
|
||||
resourceOrgID := resourceAttrs["organization_id"]
|
||||
|
||||
// Find role for resource's organization
|
||||
memberships, err := a.loadMemberships(ctx, conn, params.Principal)
|
||||
membership, err := a.loadMembership(ctx, conn, params.Principal, resourceOrgID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load memberships for principal: %w", err)
|
||||
}
|
||||
membership := findMembershipForOrg(memberships, resourceOrgID)
|
||||
|
||||
// Check whether the viewer is currently assuming the org of the accessed resource
|
||||
if membership != nil && params.Session != nil {
|
||||
@@ -144,12 +143,31 @@ func (a *Authorizer) authorize(ctx context.Context, conn pg.Conn, params Authori
|
||||
return NewInsufficientPermissionsError(params.Principal, params.Resource, params.Action)
|
||||
}
|
||||
|
||||
func (a *Authorizer) loadMemberships(ctx context.Context, conn pg.Conn, principalID gid.GID) (coredata.Memberships, error) {
|
||||
var memberships coredata.Memberships
|
||||
if err := memberships.LoadAllByIdentityID(ctx, conn, principalID); err != nil {
|
||||
return nil, fmt.Errorf("cannot load memberships: %w", err)
|
||||
func (a *Authorizer) loadMembership(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
principalID gid.GID,
|
||||
resourceOrgID string,
|
||||
) (*coredata.Membership, error) {
|
||||
if resourceOrgID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return memberships, nil
|
||||
|
||||
orgID, err := gid.ParseGID(resourceOrgID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse gid: %w", err)
|
||||
}
|
||||
|
||||
membership := &coredata.Membership{}
|
||||
if err := membership.LoadActiveByIdentityIDAndOrganizationID(ctx, conn, principalID, orgID); err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("cannot load active membership: %w", err)
|
||||
}
|
||||
|
||||
return membership, nil
|
||||
}
|
||||
|
||||
func (a *Authorizer) getActiveChildSessionForMembership(
|
||||
@@ -240,13 +258,3 @@ func (a *Authorizer) buildPoliciesForRole(role string) []*policy.Policy {
|
||||
|
||||
return policies
|
||||
}
|
||||
|
||||
func findMembershipForOrg(memberships coredata.Memberships, orgID string) *coredata.Membership {
|
||||
for _, m := range memberships {
|
||||
if m.OrganizationID.String() == orgID && m.State == coredata.MembershipStateActive {
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -123,28 +123,28 @@ func (e ErrMembershipNotFound) Error() string {
|
||||
return fmt.Sprintf("membership %q not found", e.MembershipID)
|
||||
}
|
||||
|
||||
type ErrMembershipInactive struct {
|
||||
MembershipID gid.GID
|
||||
type ErrUserInactive struct {
|
||||
ProfileID gid.GID
|
||||
}
|
||||
|
||||
func NewMembershipInactiveError(membershipID gid.GID) error {
|
||||
return &ErrMembershipInactive{MembershipID: membershipID}
|
||||
func NewUserInactiveError(profileID gid.GID) error {
|
||||
return &ErrUserInactive{ProfileID: profileID}
|
||||
}
|
||||
|
||||
func (e ErrMembershipInactive) Error() string {
|
||||
return fmt.Sprintf("membership %q is inactive", e.MembershipID)
|
||||
func (e ErrUserInactive) Error() string {
|
||||
return fmt.Sprintf("user %q is inactive", e.ProfileID)
|
||||
}
|
||||
|
||||
type ErrMembershipManagedBySCIM struct {
|
||||
MembershipID gid.GID
|
||||
type ErrUserManagedBySCIM struct {
|
||||
ProfileID gid.GID
|
||||
}
|
||||
|
||||
func NewMembershipManagedBySCIMError(membershipID gid.GID) error {
|
||||
return &ErrMembershipManagedBySCIM{MembershipID: membershipID}
|
||||
func NewUserManagedBySCIMError(profileID gid.GID) error {
|
||||
return &ErrUserManagedBySCIM{ProfileID: profileID}
|
||||
}
|
||||
|
||||
func (e ErrMembershipManagedBySCIM) Error() string {
|
||||
return fmt.Sprintf("membership %q is managed by SCIM and cannot be deleted manually", e.MembershipID)
|
||||
func (e ErrUserManagedBySCIM) Error() string {
|
||||
return fmt.Sprintf("user %q is managed by SCIM and cannot be deleted manually", e.ProfileID)
|
||||
}
|
||||
|
||||
type ErrLastActiveOwner struct {
|
||||
@@ -156,7 +156,7 @@ func NewLastActiveOwnerError(membershipID gid.GID) error {
|
||||
}
|
||||
|
||||
func (e ErrLastActiveOwner) Error() string {
|
||||
return fmt.Sprintf("cannot remove membership %q: last active owner of the organization", e.MembershipID)
|
||||
return fmt.Sprintf("cannot remove profile %q: last active owner of the organization", e.MembershipID)
|
||||
}
|
||||
|
||||
type ErrOrganizationNotFound struct{ OrganizationID gid.GID }
|
||||
@@ -220,17 +220,17 @@ func (e ErrSessionExpired) Error() string {
|
||||
return fmt.Sprintf("session %q expired", e.SessionID)
|
||||
}
|
||||
|
||||
type ErrMembershipAlreadyExists struct {
|
||||
type ErrUserAlreadyExists struct {
|
||||
IdentityID gid.GID
|
||||
OrganizationID gid.GID
|
||||
}
|
||||
|
||||
func NewMembershipAlreadyExistsError(identityID gid.GID, organizationID gid.GID) error {
|
||||
return &ErrMembershipAlreadyExists{IdentityID: identityID, OrganizationID: organizationID}
|
||||
func NewUserAlreadyExistsError(identityID gid.GID, organizationID gid.GID) error {
|
||||
return &ErrUserAlreadyExists{IdentityID: identityID, OrganizationID: organizationID}
|
||||
}
|
||||
|
||||
func (e ErrMembershipAlreadyExists) Error() string {
|
||||
return fmt.Sprintf("membership already exists for identity %q in organization %q", e.IdentityID, e.OrganizationID)
|
||||
func (e ErrUserAlreadyExists) Error() string {
|
||||
return fmt.Sprintf("user already exists for identity %q in organization %q", e.IdentityID, e.OrganizationID)
|
||||
}
|
||||
|
||||
type ErrSAMLConfigurationNotFound struct{ ConfigID gid.GID }
|
||||
|
||||
@@ -212,33 +212,6 @@ func NewOrganizationService(svc *Service) *OrganizationService {
|
||||
return &OrganizationService{Service: svc}
|
||||
}
|
||||
|
||||
func (s *OrganizationService) CountMemberships(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
) (int, error) {
|
||||
var count int
|
||||
scope := coredata.NewScopeFromObjectID(organizationID)
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) (err error) {
|
||||
memberships := coredata.Memberships{}
|
||||
count, err = memberships.CountByOrganizationID(ctx, conn, scope, organizationID, coredata.NewMembershipFilter())
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count memberships: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *OrganizationService) UpdateMempership(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
@@ -282,48 +255,53 @@ func (s *OrganizationService) UpdateMempership(
|
||||
func (s *OrganizationService) RemoveMember(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
membershipID gid.GID,
|
||||
profileID gid.GID,
|
||||
) error {
|
||||
scope := coredata.NewScopeFromObjectID(organizationID)
|
||||
|
||||
return s.pg.WithTx(
|
||||
ctx,
|
||||
func(tx pg.Conn) error {
|
||||
membership := coredata.Membership{}
|
||||
profile := coredata.MembershipProfile{}
|
||||
|
||||
if err := membership.LoadByID(ctx, tx, scope, membershipID); err != nil {
|
||||
if err := profile.LoadByID(ctx, tx, scope, profileID); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewMembershipNotFoundError(membershipID)
|
||||
return NewProfileNotFoundError(profileID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
if profile.OrganizationID != organizationID {
|
||||
return NewMembershipNotFoundError(profile.ID)
|
||||
}
|
||||
|
||||
if profile.Source == coredata.ProfileSourceSCIM {
|
||||
return NewUserManagedBySCIMError(profileID)
|
||||
}
|
||||
|
||||
membership := &coredata.Membership{}
|
||||
if err := membership.LoadByIdentityAndOrg(ctx, tx, scope, profile.IdentityID, profile.OrganizationID); err != nil {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
if membership.OrganizationID != organizationID {
|
||||
return NewMembershipNotFoundError(membership.ID)
|
||||
}
|
||||
|
||||
if membership.Source == coredata.MembershipSourceSCIM {
|
||||
return NewMembershipManagedBySCIMError(membershipID)
|
||||
}
|
||||
|
||||
if membership.Role == coredata.MembershipRoleOwner && membership.State == coredata.MembershipStateActive {
|
||||
memberships := coredata.Memberships{}
|
||||
filter := coredata.NewMembershipFilter().
|
||||
WithRole(coredata.MembershipRoleOwner).
|
||||
WithState(coredata.MembershipStateActive)
|
||||
count, err := memberships.CountByOrganizationID(ctx, tx, scope, organizationID, filter)
|
||||
if membership.Role == coredata.MembershipRoleOwner && profile.State == coredata.ProfileStateActive {
|
||||
profiles := coredata.MembershipProfiles{}
|
||||
count, err := profiles.CountActiveOwnerByOrganizationID(ctx, tx, scope, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count active owners: %w", err)
|
||||
}
|
||||
|
||||
if count <= 1 {
|
||||
return NewLastActiveOwnerError(membershipID)
|
||||
return NewLastActiveOwnerError(profileID)
|
||||
}
|
||||
}
|
||||
|
||||
err := membership.Delete(ctx, tx, scope, membershipID)
|
||||
if err != nil {
|
||||
if err := profile.Delete(ctx, tx, scope, profileID); err != nil {
|
||||
return fmt.Errorf("cannot delete profile: %w", err)
|
||||
}
|
||||
|
||||
if err := membership.Delete(ctx, tx, scope, membership.ID); err != nil {
|
||||
return fmt.Errorf("cannot delete membership: %w", err)
|
||||
}
|
||||
|
||||
@@ -466,14 +444,14 @@ func (s *OrganizationService) InviteMember(
|
||||
|
||||
identityExists := identity.ID != gid.Nil
|
||||
if identityExists {
|
||||
membership := &coredata.Membership{}
|
||||
err = membership.LoadByIdentityAndOrg(ctx, tx, scope, identity.ID, organizationID)
|
||||
profile := &coredata.MembershipProfile{}
|
||||
err = profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, identity.ID, organizationID)
|
||||
if err != nil && err != coredata.ErrResourceNotFound {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
if membership.ID != gid.Nil && membership.State == coredata.MembershipStateActive {
|
||||
return NewMembershipAlreadyExistsError(identity.ID, organizationID)
|
||||
if profile.ID != gid.Nil && profile.State == coredata.ProfileStateActive {
|
||||
return NewUserAlreadyExistsError(identity.ID, organizationID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,13 +527,22 @@ func (s *OrganizationService) CreateOrganization(
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
profile = &coredata.MembershipProfile{
|
||||
ID: gid.New(tenantID, coredata.MembershipProfileEntityType),
|
||||
IdentityID: identityID,
|
||||
OrganizationID: organization.ID,
|
||||
Source: coredata.ProfileSourceManual,
|
||||
State: coredata.ProfileStateActive,
|
||||
FullName: req.Name,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
membership = &coredata.Membership{
|
||||
ID: gid.New(tenantID, coredata.MembershipEntityType),
|
||||
IdentityID: identityID,
|
||||
OrganizationID: organizationID,
|
||||
Role: coredata.MembershipRoleOwner,
|
||||
Source: coredata.MembershipSourceManual,
|
||||
State: coredata.MembershipStateActive,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
@@ -688,25 +675,16 @@ func (s *OrganizationService) CreateOrganization(
|
||||
organization.HorizontalLogoFileID = &horizontalLogoFile.ID
|
||||
}
|
||||
|
||||
err = membership.Insert(ctx, tx, scope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create membership: %w", err)
|
||||
}
|
||||
|
||||
profile := &coredata.MembershipProfile{
|
||||
ID: gid.New(tenantID, coredata.MembershipProfileEntityType),
|
||||
IdentityID: identity.ID,
|
||||
OrganizationID: organization.ID,
|
||||
FullName: identity.FullName,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err = profile.Insert(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert profile: %w", err)
|
||||
}
|
||||
|
||||
err = membership.Insert(ctx, tx, scope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert membership: %w", err)
|
||||
}
|
||||
|
||||
if err := organizationContext.Insert(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot insert organization context: %w", err)
|
||||
}
|
||||
@@ -1508,10 +1486,10 @@ func (s OrganizationService) DeleteSCIMConfiguration(
|
||||
return scim.NewSCIMConfigurationNotFoundError(configID)
|
||||
}
|
||||
|
||||
memberships := &coredata.Memberships{}
|
||||
err = memberships.ResetSCIMSources(ctx, tx, scope, config.OrganizationID)
|
||||
profiles := &coredata.MembershipProfiles{}
|
||||
err = profiles.ResetSCIMSources(ctx, tx, scope, config.OrganizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot reset membership sources: %w", err)
|
||||
return fmt.Errorf("cannot reset user sources: %w", err)
|
||||
}
|
||||
|
||||
// Delete SCIM bridge and its connector if they exist
|
||||
|
||||
@@ -89,12 +89,12 @@ func (e ErrSAMLAutoSignupDisabled) Error() string {
|
||||
return fmt.Sprintf("SAML auto-signup is disabled for configuration %q", e.ConfigID)
|
||||
}
|
||||
|
||||
type ErrMembershipInactive struct{ MembershipID gid.GID }
|
||||
type ErrUserInactive struct{ ProfileID gid.GID }
|
||||
|
||||
func NewMembershipInactiveError(membershipID gid.GID) error {
|
||||
return &ErrMembershipInactive{MembershipID: membershipID}
|
||||
func NewUserInactiveError(profileID gid.GID) error {
|
||||
return &ErrUserInactive{ProfileID: profileID}
|
||||
}
|
||||
|
||||
func (e ErrMembershipInactive) Error() string {
|
||||
return fmt.Sprintf("membership %q is inactive", e.MembershipID)
|
||||
func (e ErrUserInactive) Error() string {
|
||||
return fmt.Sprintf("user %q is inactive", e.ProfileID)
|
||||
}
|
||||
|
||||
@@ -296,16 +296,23 @@ func (s *Service) HandleAssertion(
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||
|
||||
err = profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, identity.ID, config.OrganizationID)
|
||||
if err != nil && err != coredata.ErrResourceNotFound {
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
if err := profile.LoadByIdentityIDAndOrganizationID(
|
||||
ctx,
|
||||
tx,
|
||||
scope,
|
||||
identity.ID,
|
||||
config.OrganizationID,
|
||||
); err != nil {
|
||||
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
if profile.ID == gid.Nil {
|
||||
profile = &coredata.MembershipProfile{
|
||||
ID: gid.New(membership.ID.TenantID(), coredata.MembershipProfileEntityType),
|
||||
IdentityID: identity.ID,
|
||||
OrganizationID: config.OrganizationID,
|
||||
Source: coredata.ProfileSourceSAML,
|
||||
State: coredata.ProfileStateActive,
|
||||
FullName: fullname,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
@@ -315,25 +322,28 @@ func (s *Service) HandleAssertion(
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert membership profile: %w", err)
|
||||
}
|
||||
} else {
|
||||
if profile.State == coredata.ProfileStateInactive {
|
||||
return NewUserInactiveError(membership.ID)
|
||||
}
|
||||
}
|
||||
|
||||
err = membership.LoadByIdentityAndOrg(ctx, tx, scope, identity.ID, config.OrganizationID)
|
||||
if err != nil && err != coredata.ErrResourceNotFound {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
if err := membership.LoadByIdentityAndOrg(
|
||||
ctx,
|
||||
tx,
|
||||
scope,
|
||||
identity.ID,
|
||||
config.OrganizationID,
|
||||
); err != nil {
|
||||
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
if membership.ID != gid.Nil && membership.State == coredata.MembershipStateInactive {
|
||||
return NewMembershipInactiveError(membership.ID)
|
||||
}
|
||||
|
||||
if membership.ID == gid.Nil {
|
||||
membership = &coredata.Membership{
|
||||
ID: gid.New(config.ID.TenantID(), coredata.MembershipEntityType),
|
||||
IdentityID: identity.ID,
|
||||
OrganizationID: config.OrganizationID,
|
||||
Role: coredata.MembershipRoleEmployee,
|
||||
Source: coredata.MembershipSourceSAML,
|
||||
State: coredata.MembershipStateActive,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
@@ -358,34 +368,26 @@ func (s *Service) HandleAssertion(
|
||||
}
|
||||
}
|
||||
|
||||
if membership.Source != coredata.MembershipSourceSCIM {
|
||||
needsUpdate := false
|
||||
if profile.Source != coredata.ProfileSourceSCIM {
|
||||
profile.FullName = fullname
|
||||
profile.UpdatedAt = now
|
||||
if profile.Source == coredata.ProfileSourceManual {
|
||||
profile.Source = coredata.ProfileSourceSAML
|
||||
}
|
||||
err = profile.Update(ctx, tx, scope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update profile: %w", err)
|
||||
}
|
||||
|
||||
if role != nil {
|
||||
membership.Role = *role
|
||||
membership.UpdatedAt = now
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if membership.Source == coredata.MembershipSourceManual {
|
||||
membership.Source = coredata.MembershipSourceSAML
|
||||
membership.UpdatedAt = now
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
err = membership.Update(ctx, tx, scope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update membership: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
profile.FullName = fullname
|
||||
profile.UpdatedAt = now
|
||||
err = profile.Update(ctx, tx, scope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update profile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
)
|
||||
|
||||
func ParseUserFilter(expr scimfilter.Expression) (*coredata.MembershipFilter, error) {
|
||||
filter := coredata.NewMembershipFilter()
|
||||
func ParseUserFilter(expr scimfilter.Expression) (*coredata.MembershipProfileFilter, error) {
|
||||
filter := coredata.NewMembershipProfileFilter(nil)
|
||||
|
||||
if expr == nil {
|
||||
return filter, nil
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -138,103 +139,120 @@ func (s *Service) CreateUser(
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
membershipState := coredata.MembershipStateActive
|
||||
profileState := coredata.ProfileStateActive
|
||||
if !active {
|
||||
membershipState = coredata.MembershipStateInactive
|
||||
profileState = coredata.ProfileStateInactive
|
||||
}
|
||||
|
||||
var membership *coredata.Membership
|
||||
var profile *coredata.MembershipProfile
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||
|
||||
err = s.pg.WithTx(ctx, func(tx pg.Conn) error {
|
||||
// Check if identity exists
|
||||
identity := &coredata.Identity{}
|
||||
err := identity.LoadByEmail(ctx, tx, emailAddr)
|
||||
if err := identity.LoadByEmail(ctx, tx, emailAddr); err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
// Create new identity
|
||||
identity = &coredata.Identity{
|
||||
ID: gid.New(gid.NilTenant, coredata.IdentityEntityType),
|
||||
EmailAddress: emailAddr,
|
||||
FullName: fullName,
|
||||
HashedPassword: nil,
|
||||
EmailAddressVerified: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
// Create new identity
|
||||
identity = &coredata.Identity{
|
||||
ID: gid.New(gid.NilTenant, coredata.IdentityEntityType),
|
||||
EmailAddress: emailAddr,
|
||||
FullName: fullName,
|
||||
HashedPassword: nil,
|
||||
EmailAddressVerified: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
err = identity.Insert(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert identity: %w", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = identity.Insert(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert identity: %w", err)
|
||||
// Check if profile exists
|
||||
profile = &coredata.MembershipProfile{}
|
||||
if err := profile.LoadByIdentityIDAndOrganizationID(
|
||||
ctx,
|
||||
tx,
|
||||
coredata.NewScopeFromObjectID(config.OrganizationID),
|
||||
identity.ID,
|
||||
config.OrganizationID,
|
||||
); err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
profile = &coredata.MembershipProfile{
|
||||
ID: gid.New(config.OrganizationID.TenantID(), coredata.MembershipProfileEntityType),
|
||||
IdentityID: identity.ID,
|
||||
OrganizationID: config.OrganizationID,
|
||||
Source: coredata.ProfileSourceSCIM,
|
||||
State: profileState,
|
||||
FullName: fullName,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err = profile.Insert(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert profile: %w", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
} else {
|
||||
profile.Source = coredata.ProfileSourceSCIM
|
||||
profile.State = profileState
|
||||
profile.UpdatedAt = now
|
||||
if err := profile.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot update profile: %w", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
}
|
||||
|
||||
// Check if membership exists
|
||||
membership = &coredata.Membership{}
|
||||
err = membership.LoadByIdentityAndOrg(ctx, tx, scope, identity.ID, config.OrganizationID)
|
||||
if err := membership.LoadByIdentityAndOrg(
|
||||
ctx,
|
||||
tx,
|
||||
scope,
|
||||
identity.ID,
|
||||
config.OrganizationID,
|
||||
); err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
// Create new membership
|
||||
membership = &coredata.Membership{
|
||||
ID: gid.New(config.OrganizationID.TenantID(), coredata.MembershipEntityType),
|
||||
IdentityID: identity.ID,
|
||||
OrganizationID: config.OrganizationID,
|
||||
Role: coredata.MembershipRoleEmployee,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
// Create new membership
|
||||
membership = &coredata.Membership{
|
||||
ID: gid.New(config.OrganizationID.TenantID(), coredata.MembershipEntityType),
|
||||
IdentityID: identity.ID,
|
||||
OrganizationID: config.OrganizationID,
|
||||
Role: coredata.MembershipRoleEmployee,
|
||||
Source: coredata.MembershipSourceSCIM,
|
||||
State: membershipState,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
err = membership.Insert(ctx, tx, scope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert membership: %w", err)
|
||||
}
|
||||
|
||||
err = membership.Insert(ctx, tx, scope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert membership: %w", err)
|
||||
}
|
||||
// Expire all pending invitations for email in organization
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
err := invitations.ExpireByEmailAndOrganization(
|
||||
ctx,
|
||||
tx,
|
||||
coredata.NewScopeFromObjectID(config.OrganizationID),
|
||||
emailAddr,
|
||||
config.OrganizationID,
|
||||
onlyPending,
|
||||
)
|
||||
|
||||
// Create membership profile
|
||||
membershipProfile := &coredata.MembershipProfile{
|
||||
ID: gid.New(membership.ID.TenantID(), coredata.MembershipProfileEntityType),
|
||||
IdentityID: identity.ID,
|
||||
OrganizationID: config.OrganizationID,
|
||||
FullName: fullName,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err = membershipProfile.Insert(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert membership profile: %w", err)
|
||||
}
|
||||
|
||||
// Expire all pending invitations for email in organization
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
err := invitations.ExpireByEmailAndOrganization(
|
||||
ctx,
|
||||
tx,
|
||||
coredata.NewScopeFromObjectID(config.OrganizationID),
|
||||
emailAddr,
|
||||
config.OrganizationID,
|
||||
onlyPending,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot expire pending invitations by email")
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
} else {
|
||||
// Update existing membership - follow what SCIM tells us
|
||||
membership.Source = coredata.MembershipSourceSCIM
|
||||
membership.State = membershipState
|
||||
membership.UpdatedAt = now
|
||||
|
||||
err = membership.Update(ctx, tx, scope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update membership: %w", err)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot expire pending invitations by email")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,32 +263,45 @@ func (s *Service) CreateUser(
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
|
||||
return membershipToResource(membership), nil
|
||||
return userToResource(profile), nil
|
||||
}
|
||||
|
||||
func (s *Service) GetUser(
|
||||
ctx context.Context,
|
||||
config *coredata.SCIMConfiguration,
|
||||
membershipID gid.GID,
|
||||
profileID gid.GID,
|
||||
) (scim.Resource, error) {
|
||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||
|
||||
var membership *coredata.Membership
|
||||
var (
|
||||
profile *coredata.MembershipProfile
|
||||
membership *coredata.Membership
|
||||
)
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
membership = &coredata.Membership{}
|
||||
err := membership.LoadByID(ctx, conn, scope, membershipID)
|
||||
if err != nil {
|
||||
profile = &coredata.MembershipProfile{}
|
||||
if err := profile.LoadByID(ctx, conn, scope, profileID); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||
return scimerrors.ScimErrorResourceNotFound(profileID.String())
|
||||
}
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
if membership.OrganizationID != config.OrganizationID {
|
||||
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||
if profile.OrganizationID != config.OrganizationID {
|
||||
return scimerrors.ScimErrorResourceNotFound(profileID.String())
|
||||
}
|
||||
|
||||
membership = &coredata.Membership{}
|
||||
if err := membership.LoadByIdentityAndOrg(
|
||||
ctx,
|
||||
conn,
|
||||
scope,
|
||||
profile.IdentityID,
|
||||
profile.OrganizationID,
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -281,7 +312,7 @@ func (s *Service) GetUser(
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
|
||||
return membershipToResource(membership), nil
|
||||
return userToResource(profile), nil
|
||||
}
|
||||
|
||||
func (s *Service) ListUsers(
|
||||
@@ -301,31 +332,31 @@ func (s *Service) ListUsers(
|
||||
// when they don't exist in the identity provider.
|
||||
// 2. When a manual user exists in the identity provider but not in the
|
||||
// SCIM list, CreateUser is called which enrolls them into SCIM management.
|
||||
filter.WithSource(coredata.MembershipSourceSCIM)
|
||||
filter.WithSource(coredata.ProfileSourceSCIM)
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||
|
||||
var memberships coredata.Memberships
|
||||
var profiles coredata.MembershipProfiles
|
||||
var totalCount int
|
||||
|
||||
err = s.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
var err error
|
||||
totalCount, err = memberships.CountByOrganizationID(ctx, conn, scope, config.OrganizationID, filter)
|
||||
totalCount, err = profiles.CountByOrganizationID(ctx, conn, scope, config.OrganizationID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count memberships: %w", err)
|
||||
return fmt.Errorf("cannot count profiles: %w", err)
|
||||
}
|
||||
|
||||
orderBy := page.OrderBy[coredata.MembershipOrderField]{
|
||||
Field: coredata.MembershipOrderFieldCreatedAt,
|
||||
orderBy := page.OrderBy[coredata.MembershipProfileOrderField]{
|
||||
Field: coredata.MembershipProfileOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
cursor := page.NewCursor(count, nil, page.Head, orderBy)
|
||||
|
||||
err = memberships.LoadByOrganizationID(ctx, conn, scope, config.OrganizationID, cursor, filter)
|
||||
err = profiles.LoadByOrganizationID(ctx, conn, scope, config.OrganizationID, cursor, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load memberships: %w", err)
|
||||
return fmt.Errorf("cannot load profiles: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -336,9 +367,9 @@ func (s *Service) ListUsers(
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
resources := make([]scim.Resource, 0, len(memberships))
|
||||
for _, m := range memberships {
|
||||
resources = append(resources, membershipToResource(m))
|
||||
resources := make([]scim.Resource, 0, len(profiles))
|
||||
for _, p := range profiles {
|
||||
resources = append(resources, userToResource(p))
|
||||
}
|
||||
|
||||
return resources, totalCount, nil
|
||||
@@ -347,61 +378,95 @@ func (s *Service) ListUsers(
|
||||
func (s *Service) ReplaceUser(
|
||||
ctx context.Context,
|
||||
config *coredata.SCIMConfiguration,
|
||||
membershipID gid.GID,
|
||||
profileID gid.GID,
|
||||
attributes scim.ResourceAttributes,
|
||||
) (scim.Resource, error) {
|
||||
fullName, active := ParseUserFromReplaceAttributes(attributes)
|
||||
membership, err := s.updateUser(ctx, config, membershipID, fullName, active)
|
||||
profile, err := s.updateUser(ctx, config, profileID, fullName, active)
|
||||
if err != nil {
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
|
||||
return membershipToResource(membership), nil
|
||||
return userToResource(profile), nil
|
||||
}
|
||||
|
||||
func (s *Service) PatchUser(
|
||||
ctx context.Context,
|
||||
config *coredata.SCIMConfiguration,
|
||||
membershipID gid.GID,
|
||||
profileID gid.GID,
|
||||
operations []scim.PatchOperation,
|
||||
) (scim.Resource, error) {
|
||||
fullName, active := ParseUserFromPatchOperations(operations)
|
||||
membership, err := s.updateUser(ctx, config, membershipID, fullName, active)
|
||||
profile, err := s.updateUser(ctx, config, profileID, fullName, active)
|
||||
if err != nil {
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
|
||||
return membershipToResource(membership), nil
|
||||
return userToResource(profile), nil
|
||||
}
|
||||
|
||||
func (s *Service) updateUser(
|
||||
ctx context.Context,
|
||||
config *coredata.SCIMConfiguration,
|
||||
membershipID gid.GID,
|
||||
profileID gid.GID,
|
||||
fullName string,
|
||||
active *bool,
|
||||
) (*coredata.Membership, error) {
|
||||
) (*coredata.MembershipProfile, error) {
|
||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||
now := time.Now()
|
||||
|
||||
var membership *coredata.Membership
|
||||
var (
|
||||
membership *coredata.Membership
|
||||
profile *coredata.MembershipProfile
|
||||
)
|
||||
|
||||
err := s.pg.WithTx(
|
||||
ctx,
|
||||
func(tx pg.Conn) error {
|
||||
membership = &coredata.Membership{}
|
||||
if err := membership.LoadByID(ctx, tx, scope, membershipID); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||
profile = &coredata.MembershipProfile{}
|
||||
if err := profile.LoadByID(ctx, tx, scope, profileID); err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return scimerrors.ScimErrorResourceNotFound(profileID.String())
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
if profile.OrganizationID != config.OrganizationID {
|
||||
return scimerrors.ScimErrorResourceNotFound(profileID.String())
|
||||
}
|
||||
|
||||
membership = &coredata.Membership{}
|
||||
if err := membership.LoadByIdentityAndOrg(ctx, tx, scope, profile.IdentityID, profile.OrganizationID); err != nil {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
if membership.OrganizationID != config.OrganizationID {
|
||||
return scimerrors.ScimErrorResourceNotFound(membershipID.String())
|
||||
shouldReactivate := active != nil && *active && profile.State == coredata.ProfileStateInactive
|
||||
shouldDeactivate := active != nil && !*active && profile.State == coredata.ProfileStateActive
|
||||
|
||||
if fullName != "" {
|
||||
profile.FullName = fullName
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
needsUpdate := false
|
||||
if shouldReactivate {
|
||||
profile.State = coredata.ProfileStateActive
|
||||
profile.UpdatedAt = now
|
||||
} else if shouldDeactivate {
|
||||
profile.State = coredata.ProfileStateInactive
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
if profile.Source != coredata.ProfileSourceSCIM {
|
||||
profile.Source = coredata.ProfileSourceSCIM
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
if err := profile.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot update membership profile: %w", err)
|
||||
}
|
||||
|
||||
needsUpdate := shouldReactivate || shouldDeactivate
|
||||
|
||||
if active != nil {
|
||||
identity := &coredata.Identity{}
|
||||
@@ -409,11 +474,8 @@ func (s *Service) updateUser(
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
}
|
||||
|
||||
if *active && membership.State == coredata.MembershipStateInactive {
|
||||
membership.State = coredata.MembershipStateActive
|
||||
if shouldReactivate {
|
||||
membership.Role = coredata.MembershipRoleEmployee
|
||||
needsUpdate = true
|
||||
|
||||
// Expire all pending invitations for email in organization
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
@@ -427,10 +489,7 @@ func (s *Service) updateUser(
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot expire pending invitations by email: %w", err)
|
||||
}
|
||||
} else if !*active && membership.State == coredata.MembershipStateActive {
|
||||
membership.State = coredata.MembershipStateInactive
|
||||
needsUpdate = true
|
||||
|
||||
} else if shouldDeactivate {
|
||||
// Expire all pending invitations for email in organization
|
||||
invitations := &coredata.Invitations{}
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
@@ -447,11 +506,6 @@ func (s *Service) updateUser(
|
||||
}
|
||||
}
|
||||
|
||||
if membership.Source != coredata.MembershipSourceSCIM {
|
||||
membership.Source = coredata.MembershipSourceSCIM
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
membership.UpdatedAt = now
|
||||
if err := membership.Update(ctx, tx, scope); err != nil {
|
||||
@@ -459,18 +513,6 @@ func (s *Service) updateUser(
|
||||
}
|
||||
}
|
||||
|
||||
profile := &coredata.MembershipProfile{}
|
||||
if err := profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, membership.IdentityID, membership.OrganizationID); err == nil {
|
||||
if fullName != "" {
|
||||
profile.FullName = fullName
|
||||
profile.UpdatedAt = now
|
||||
|
||||
if err := profile.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot update membership profile: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
@@ -479,7 +521,7 @@ func (s *Service) updateUser(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return membership, nil
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteUser(
|
||||
@@ -538,19 +580,14 @@ func (s *Service) LogEvent(
|
||||
config *coredata.SCIMConfiguration,
|
||||
method string,
|
||||
path string,
|
||||
membershipID *gid.GID,
|
||||
userName string,
|
||||
ipAddress net.IP,
|
||||
statusCode int,
|
||||
errorMessage *string,
|
||||
) {
|
||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||
|
||||
var mID gid.GID
|
||||
if membershipID != nil {
|
||||
mID = *membershipID
|
||||
}
|
||||
|
||||
event := s.createEvent(config, method, path, mID, ipAddress, statusCode, errorMessage)
|
||||
event := s.createEvent(config, method, path, userName, ipAddress, statusCode, errorMessage)
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
@@ -572,7 +609,7 @@ func (s *Service) createEvent(
|
||||
config *coredata.SCIMConfiguration,
|
||||
method string,
|
||||
path string,
|
||||
membershipID gid.GID,
|
||||
userName string,
|
||||
ipAddress net.IP,
|
||||
statusCode int,
|
||||
errorMessage *string,
|
||||
@@ -586,13 +623,10 @@ func (s *Service) createEvent(
|
||||
StatusCode: statusCode,
|
||||
ErrorMessage: errorMessage,
|
||||
IPAddress: ipAddress,
|
||||
UserName: userName,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if membershipID != gid.Nil {
|
||||
event.MembershipID = &membershipID
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
@@ -730,28 +764,28 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) (fullName st
|
||||
return fullName, active
|
||||
}
|
||||
|
||||
func membershipToResource(m *coredata.Membership) scim.Resource {
|
||||
func userToResource(p *coredata.MembershipProfile) scim.Resource {
|
||||
return scim.Resource{
|
||||
ID: m.ID.String(),
|
||||
ExternalID: optional.NewString(m.ID.String()),
|
||||
ID: p.ID.String(),
|
||||
ExternalID: optional.NewString(p.ID.String()),
|
||||
Attributes: scim.ResourceAttributes{
|
||||
"userName": m.EmailAddress.String(),
|
||||
"displayName": m.FullName,
|
||||
"active": m.State == coredata.MembershipStateActive,
|
||||
"userName": p.EmailAddress.String(),
|
||||
"displayName": p.FullName,
|
||||
"active": p.State == coredata.ProfileStateActive,
|
||||
"name": map[string]any{
|
||||
"formatted": m.FullName,
|
||||
"formatted": p.FullName,
|
||||
},
|
||||
"emails": []map[string]any{
|
||||
{
|
||||
"value": m.EmailAddress.String(),
|
||||
"value": p.EmailAddress.String(),
|
||||
"type": "work",
|
||||
"primary": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Meta: scim.Meta{
|
||||
Created: &m.CreatedAt,
|
||||
LastModified: &m.UpdatedAt,
|
||||
Created: &p.CreatedAt,
|
||||
LastModified: &p.UpdatedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +324,7 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
|
||||
now = time.Now()
|
||||
rootSession = &coredata.Session{}
|
||||
identity = &coredata.Identity{}
|
||||
profile = &coredata.MembershipProfile{}
|
||||
membership = &coredata.Membership{}
|
||||
childSession = &coredata.Session{}
|
||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||
@@ -353,6 +354,18 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
}
|
||||
|
||||
err = profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, rootSession.IdentityID, organizationID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewProfileNotFoundError(organizationID)
|
||||
}
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
if profile.State == coredata.ProfileStateInactive {
|
||||
return NewUserInactiveError(profile.ID)
|
||||
}
|
||||
|
||||
err = membership.LoadByIdentityInOrganization(ctx, tx, rootSession.IdentityID, organizationID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -361,10 +374,6 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
if membership.State == coredata.MembershipStateInactive {
|
||||
return NewMembershipInactiveError(membership.ID)
|
||||
}
|
||||
|
||||
tenantID := scope.GetTenantID()
|
||||
childSession = &coredata.Session{
|
||||
ID: gid.New(tenantID, coredata.SessionEntityType),
|
||||
@@ -417,6 +426,7 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
|
||||
now = time.Now()
|
||||
rootSession = &coredata.Session{}
|
||||
identity = &coredata.Identity{}
|
||||
profile = &coredata.MembershipProfile{}
|
||||
membership = &coredata.Membership{}
|
||||
childSession = &coredata.Session{}
|
||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||
@@ -446,6 +456,18 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
}
|
||||
|
||||
err = profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, rootSession.IdentityID, organizationID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewProfileNotFoundError(organizationID)
|
||||
}
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
if profile.State == coredata.ProfileStateInactive {
|
||||
return NewUserInactiveError(profile.ID)
|
||||
}
|
||||
|
||||
err = membership.LoadByIdentityInOrganization(ctx, tx, rootSession.IdentityID, organizationID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -454,10 +476,6 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
if membership.State == coredata.MembershipStateInactive {
|
||||
return NewMembershipInactiveError(membership.ID)
|
||||
}
|
||||
|
||||
tenantID := scope.GetTenantID()
|
||||
childSession = &coredata.Session{
|
||||
ID: gid.New(tenantID, coredata.SessionEntityType),
|
||||
@@ -497,6 +515,7 @@ func (s SessionService) AssumeOrganizationSession(
|
||||
now = time.Now()
|
||||
rootSession = &coredata.Session{}
|
||||
identity = &coredata.Identity{}
|
||||
profile = &coredata.MembershipProfile{}
|
||||
membership = &coredata.Membership{}
|
||||
childSession = &coredata.Session{}
|
||||
scope = coredata.NewScopeFromObjectID(organizationID)
|
||||
@@ -524,6 +543,17 @@ func (s SessionService) AssumeOrganizationSession(
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
}
|
||||
|
||||
if err := profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, rootSession.IdentityID, organizationID); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewProfileNotFoundError(organizationID)
|
||||
}
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
if profile.State == coredata.ProfileStateInactive {
|
||||
return NewUserInactiveError(profile.ID)
|
||||
}
|
||||
|
||||
if err := membership.LoadByIdentityInOrganization(ctx, tx, rootSession.IdentityID, organizationID); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewMembershipNotFoundError(organizationID)
|
||||
@@ -531,10 +561,6 @@ func (s SessionService) AssumeOrganizationSession(
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
if membership.State == coredata.MembershipStateInactive {
|
||||
return NewMembershipInactiveError(membership.ID)
|
||||
}
|
||||
|
||||
samlConfig := &coredata.SAMLConfiguration{}
|
||||
err := samlConfig.LoadByOrganizationIDAndEmailDomain(
|
||||
ctx,
|
||||
|
||||
Reference in New Issue
Block a user