@@ -101,6 +101,7 @@ func (s AccountService) ChangeEmail(ctx context.Context, identityID gid.GID, req
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
identity := &coredata.Identity{}
|
||||
|
||||
err := identity.LoadByID(ctx, tx, identityID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -164,6 +165,7 @@ func (s AccountService) VerifyEmail(ctx context.Context, token string) error {
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
identity := &coredata.Identity{}
|
||||
|
||||
err := identity.LoadByID(ctx, tx, payload.Data.IdentityID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -208,6 +210,7 @@ func (s *AccountService) ListPendingInvitations(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
profile := coredata.MembershipProfile{}
|
||||
|
||||
err := profile.LoadByID(ctx, conn, scope, userID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -227,7 +230,6 @@ func (s *AccountService) ListPendingInvitations(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -244,6 +246,7 @@ func (s AccountService) ChangePassword(ctx context.Context, identityID gid.GID,
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
identity := &coredata.Identity{}
|
||||
|
||||
err := identity.LoadByID(ctx, tx, identityID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -294,6 +297,7 @@ func (s AccountService) CountSessions(ctx context.Context, identityID gid.GID) (
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
sessions := coredata.Sessions{}
|
||||
|
||||
count, err = sessions.CountByIdentityID(ctx, conn, identityID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count sessions: %w", err)
|
||||
@@ -324,7 +328,6 @@ func (s AccountService) ListSessions(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -411,7 +414,6 @@ func (s AccountService) ListPersonalAPIKeys(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -426,6 +428,7 @@ func (s AccountService) CountPersonalAPIKeys(ctx context.Context, identityID gid
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
personalAccessTokens := coredata.PersonalAPIKeys{}
|
||||
|
||||
count, err = personalAccessTokens.CountByIdentityID(ctx, conn, identityID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count personal access tokens: %w", err)
|
||||
@@ -452,6 +455,7 @@ func (s *AccountService) RevealPersonalAPIKeyToken(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewPersonalAPIKeyNotFoundError(personalAPIKeyID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load personal api key: %w", err)
|
||||
}
|
||||
|
||||
@@ -470,7 +474,6 @@ func (s *AccountService) RevealPersonalAPIKeyToken(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -488,6 +491,7 @@ func (s AccountService) GetIdentityForMembership(ctx context.Context, membership
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
membership := &coredata.Membership{}
|
||||
|
||||
err := membership.LoadByID(ctx, conn, scope, membershipID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -509,7 +513,6 @@ func (s AccountService) GetIdentityForMembership(ctx context.Context, membership
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -557,7 +560,6 @@ func (s *AccountService) CreatePersonalAPIKey(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -574,6 +576,7 @@ func (s *AccountService) DeletePersonalAPIKey(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
personalAPIKey := &coredata.PersonalAPIKey{}
|
||||
|
||||
err := personalAPIKey.LoadByID(ctx, tx, personalAPIKeyID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -599,6 +602,7 @@ func (s *AccountService) DeletePersonalAPIKey(
|
||||
|
||||
func (s AccountService) ListOrganizations(ctx context.Context, identityID gid.GID) ([]*coredata.Organization, error) {
|
||||
var organizations coredata.Organizations
|
||||
|
||||
orderBy := page.OrderBy[coredata.OrganizationOrderField]{
|
||||
Field: coredata.OrganizationOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
@@ -616,7 +620,6 @@ func (s AccountService) ListOrganizations(ctx context.Context, identityID gid.GI
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -657,7 +660,6 @@ func (s AccountService) GetMembershipForOrganization(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -736,7 +738,6 @@ func (s *AccountService) ListProfilesForIdentity(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -757,6 +758,7 @@ func (s AccountService) CountProfiles(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
profiles := coredata.MembershipProfiles{}
|
||||
|
||||
count, err = profiles.CountByIdentityID(ctx, conn, identityID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count profiles: %w", err)
|
||||
|
||||
@@ -75,7 +75,6 @@ func (s *APIKeyService) GetAPIKey(ctx context.Context, keyID gid.GID) (*coredata
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ func (req ResetPasswordRequest) Validate() error {
|
||||
v := validator.New()
|
||||
v.Check(req.Token, "token", validator.NotEmpty())
|
||||
v.Check(req.Password, "password", PasswordValidator())
|
||||
|
||||
return v.Error()
|
||||
}
|
||||
|
||||
@@ -107,6 +108,7 @@ func (req ChangePasswordRequest) Validate() error {
|
||||
v.Check(req.CurrentPassword, "currentPassword", validator.NotEmpty(), validator.MaxLen(255))
|
||||
|
||||
v.Check(req.NewPassword, "newPassword", PasswordValidator())
|
||||
|
||||
return v.Error()
|
||||
}
|
||||
|
||||
@@ -202,6 +204,7 @@ func (s *AuthService) ActivateAccount(
|
||||
|
||||
// Expire other pending invitations for user
|
||||
invitations := &coredata.Invitations{}
|
||||
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByUserID(
|
||||
ctx,
|
||||
@@ -258,6 +261,7 @@ func (s AuthService) ResetPassword(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
identity := &coredata.Identity{}
|
||||
|
||||
err := identity.LoadByEmail(ctx, tx, payload.Data.Email)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -433,6 +437,7 @@ func (s AuthService) OpenSessionWithSAML(ctx context.Context, identityID gid.GID
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Tx) (err error) {
|
||||
session = coredata.NewRootSession(identityID, coredata.AuthMethodSAML, s.sessionDuration)
|
||||
|
||||
err = session.Insert(ctx, conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert session: %w", err)
|
||||
@@ -441,7 +446,6 @@ func (s AuthService) OpenSessionWithSAML(ctx context.Context, identityID gid.GID
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -456,6 +460,7 @@ func (s AuthService) OpenSessionWithOIDC(ctx context.Context, identityID gid.GID
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Tx) (err error) {
|
||||
session = coredata.NewRootSession(identityID, authMethod, s.sessionDuration)
|
||||
|
||||
err = session.Insert(ctx, conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert session: %w", err)
|
||||
@@ -464,7 +469,6 @@ func (s AuthService) OpenSessionWithOIDC(ctx context.Context, identityID gid.GID
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -528,6 +532,7 @@ func (s AuthService) OpenSessionWithPassword(ctx context.Context, identityID gid
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Tx) (err error) {
|
||||
session = coredata.NewRootSession(identityID, coredata.AuthMethodPassword, s.sessionDuration)
|
||||
|
||||
err = session.Insert(ctx, conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert session: %w", err)
|
||||
@@ -536,7 +541,6 @@ func (s AuthService) OpenSessionWithPassword(ctx context.Context, identityID gid
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -562,6 +566,7 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
hashedToken := HashToken(tokenString)
|
||||
|
||||
token := &coredata.Token{
|
||||
ID: gid.New(gid.NilTenant, coredata.TokenEntityType),
|
||||
HashedValue: hashedToken,
|
||||
@@ -590,8 +595,10 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques
|
||||
}
|
||||
|
||||
emailPresenterCfg := emails.DefaultPresenterConfig(s.bucket, s.baseURL)
|
||||
|
||||
if req.CompliancePageID != nil {
|
||||
var err error
|
||||
|
||||
emailPresenterCfg, err = s.CompliancePageService.EmailPresenterConfig(ctx, *req.CompliancePageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get compliance page email presenter config: %w", err)
|
||||
@@ -701,6 +708,7 @@ func (s AuthService) OpenSessionWithMagicLink(ctx context.Context, tokenString s
|
||||
}
|
||||
|
||||
session = coredata.NewRootSession(identity.ID, coredata.AuthMethodMagicLink, s.sessionDuration)
|
||||
|
||||
err = session.Insert(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert session: %w", err)
|
||||
|
||||
@@ -101,8 +101,10 @@ func (a *Authorizer) authorize(ctx context.Context, tx pg.Tx, params AuthorizePa
|
||||
*params.Session,
|
||||
membership.ID,
|
||||
); err != nil {
|
||||
var errSessionNotFound *ErrSessionNotFound
|
||||
var errSessionExpired *ErrSessionExpired
|
||||
var (
|
||||
errSessionNotFound *ErrSessionNotFound
|
||||
errSessionExpired *ErrSessionExpired
|
||||
)
|
||||
|
||||
if errors.As(err, &errSessionNotFound) || errors.As(err, &errSessionExpired) {
|
||||
return NewAssumptionRequiredError(params.Principal, membership.ID)
|
||||
@@ -222,6 +224,7 @@ func (a *Authorizer) buildPrincipalAttributes(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot load principal attributes: %w", err)
|
||||
}
|
||||
|
||||
maps.Copy(attrs, entityAttrs)
|
||||
}
|
||||
}
|
||||
@@ -252,6 +255,7 @@ func (a *Authorizer) buildResourceAttributes(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot load resource attributes: %w", err)
|
||||
}
|
||||
|
||||
maps.Copy(attrs, entityAttrs)
|
||||
|
||||
if params.ResourceAttributes != nil {
|
||||
@@ -312,6 +316,7 @@ func (a *Authorizer) recordAuditLog(
|
||||
"cannot parse organization id for audit log",
|
||||
log.Error(err),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -331,6 +336,7 @@ func (a *Authorizer) recordAuditLog(
|
||||
"cannot marshal audit log metadata",
|
||||
log.Error(err),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ func (e *OAuth2Error) Error() string {
|
||||
if e.description != "" {
|
||||
return e.code + ": " + e.description
|
||||
}
|
||||
|
||||
return e.code
|
||||
}
|
||||
|
||||
@@ -43,6 +44,7 @@ func (e *OAuth2Error) Is(target error) bool {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return e.code == t.code
|
||||
}
|
||||
|
||||
@@ -66,6 +68,7 @@ func NewError(code *OAuth2Error, opts ...ErrorOption) *OAuth2Error {
|
||||
for _, opt := range opts {
|
||||
opt(e)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
|
||||
@@ -88,24 +88,28 @@ func (h *gcHandler) cleanup(ctx context.Context) error {
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
var authCode coredata.OAuth2AuthorizationCode
|
||||
|
||||
authCodesDeleted, err := authCode.DeleteExpired(ctx, tx, now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete expired authorization codes: %w", err)
|
||||
}
|
||||
|
||||
var accessToken coredata.OAuth2AccessToken
|
||||
|
||||
accessTokensDeleted, err := accessToken.DeleteExpired(ctx, tx, now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete expired access tokens: %w", err)
|
||||
}
|
||||
|
||||
var refreshToken coredata.OAuth2RefreshToken
|
||||
|
||||
refreshTokensDeleted, err := refreshToken.DeleteExpired(ctx, tx, now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete expired refresh tokens: %w", err)
|
||||
}
|
||||
|
||||
var deviceCode coredata.OAuth2DeviceCode
|
||||
|
||||
deviceCodesDeleted, err := deviceCode.DeleteExpired(ctx, tx, now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete expired device codes: %w", err)
|
||||
|
||||
@@ -154,6 +154,7 @@ func NewService(
|
||||
opts ...Option,
|
||||
) *Service {
|
||||
var activeIdx []int
|
||||
|
||||
for i, k := range signingKeys {
|
||||
if k.Active {
|
||||
activeIdx = append(activeIdx, i)
|
||||
@@ -185,6 +186,7 @@ func NewService(
|
||||
func (s *Service) signingKey() *SigningKey {
|
||||
n := s.rrCounter.Add(1)
|
||||
idx := s.activeSigningIdx[n%uint64(len(s.activeSigningIdx))]
|
||||
|
||||
return &s.signingKeys[idx]
|
||||
}
|
||||
|
||||
@@ -772,6 +774,7 @@ func (s *Service) PollDeviceCode(
|
||||
|
||||
// Rate limiting.
|
||||
var slowDown bool
|
||||
|
||||
if deviceCode.LastPolledAt != nil {
|
||||
elapsed := now.Sub(ref.UnrefOrZero(deviceCode.LastPolledAt))
|
||||
if elapsed < time.Duration(deviceCode.PollInterval)*time.Second {
|
||||
@@ -1091,6 +1094,7 @@ func (s *Service) RegisterClient(
|
||||
)
|
||||
|
||||
var membership coredata.Membership
|
||||
|
||||
err := s.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
@@ -1171,9 +1175,12 @@ func (s *Service) IntrospectToken(
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load access token: %w", err)
|
||||
}
|
||||
|
||||
hasAccess = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1182,9 +1189,12 @@ func (s *Service) IntrospectToken(
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load refresh token: %w", err)
|
||||
}
|
||||
|
||||
hasRefresh = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1197,18 +1207,22 @@ func (s *Service) IntrospectToken(
|
||||
if err := loadRefresh(ctx, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hasRefresh {
|
||||
return nil
|
||||
}
|
||||
|
||||
return loadAccess(ctx, conn)
|
||||
}
|
||||
|
||||
if err := loadAccess(ctx, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hasAccess {
|
||||
return nil
|
||||
}
|
||||
|
||||
return loadRefresh(ctx, conn)
|
||||
},
|
||||
); err != nil {
|
||||
@@ -1220,6 +1234,7 @@ func (s *Service) IntrospectToken(
|
||||
if now.After(accessToken.ExpiresAt) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &IntrospectResult{
|
||||
ClientID: accessToken.ClientID,
|
||||
IdentityID: accessToken.IdentityID,
|
||||
@@ -1232,6 +1247,7 @@ func (s *Service) IntrospectToken(
|
||||
if refreshToken.RevokedAt != nil || now.After(refreshToken.ExpiresAt) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &IntrospectResult{
|
||||
ClientID: refreshToken.ClientID,
|
||||
IdentityID: refreshToken.IdentityID,
|
||||
@@ -1299,10 +1315,12 @@ func (s *Service) RevokeToken(
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
if tokenTypeHint != nil && *tokenTypeHint == coredata.OAuth2TokenTypeHintRefreshToken {
|
||||
refreshToken := coredata.OAuth2RefreshToken{}
|
||||
|
||||
err := refreshToken.LoadByHashedValueAndClientID(ctx, tx, hashedValue, clientID)
|
||||
if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load refresh token: %w", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
now := time.Now()
|
||||
if err := refreshToken.Revoke(ctx, tx, now); err != nil {
|
||||
@@ -1320,10 +1338,12 @@ func (s *Service) RevokeToken(
|
||||
}
|
||||
|
||||
accessToken := coredata.OAuth2AccessToken{}
|
||||
|
||||
err = accessToken.LoadByHashedValueAndClientID(ctx, tx, hashedValue, clientID)
|
||||
if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load access token: %w", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if err := accessToken.Delete(ctx, tx); err != nil {
|
||||
return fmt.Errorf("cannot delete access token: %w", err)
|
||||
@@ -1334,22 +1354,27 @@ func (s *Service) RevokeToken(
|
||||
}
|
||||
|
||||
accessToken := coredata.OAuth2AccessToken{}
|
||||
|
||||
err := accessToken.LoadByHashedValueAndClientID(ctx, tx, hashedValue, clientID)
|
||||
if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load access token: %w", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if err := accessToken.Delete(ctx, tx); err != nil {
|
||||
return fmt.Errorf("cannot delete access token: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
refreshToken := coredata.OAuth2RefreshToken{}
|
||||
|
||||
err = refreshToken.LoadByHashedValueAndClientID(ctx, tx, hashedValue, clientID)
|
||||
if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load refresh token: %w", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
now := time.Now()
|
||||
if err := refreshToken.Revoke(ctx, tx, now); err != nil {
|
||||
@@ -1452,6 +1477,7 @@ func (s *Service) Authorize(
|
||||
requestedScopes,
|
||||
); err == nil {
|
||||
var err error
|
||||
|
||||
code, err = s.issueAuthorizationCode(
|
||||
ctx,
|
||||
tx,
|
||||
@@ -1517,6 +1543,7 @@ func (s *Service) GetConsentByID(
|
||||
consentID gid.GID,
|
||||
) (*coredata.OAuth2Consent, error) {
|
||||
var consent coredata.OAuth2Consent
|
||||
|
||||
if err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
@@ -1645,6 +1672,7 @@ func (s *Service) ApproveConsent(
|
||||
}
|
||||
|
||||
result.IsDeviceFlow = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ func (gc *GarbageCollector) cleanup(ctx context.Context) error {
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
var state coredata.OIDCState
|
||||
|
||||
deleted, err := state.DeleteExpired(ctx, tx, now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete expired oidc states: %w", err)
|
||||
|
||||
@@ -126,6 +126,7 @@ func (c *idTokenClaims) hasAudience(clientID string) bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -136,6 +137,7 @@ func (c *idTokenClaims) isEmailVerified() bool {
|
||||
case string:
|
||||
return strings.EqualFold(v, "true")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -228,11 +230,13 @@ func NewService(
|
||||
|
||||
func (s *Service) Run(ctx context.Context) error {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
ctx, cancel := context.WithCancelCause(ctx)
|
||||
defer cancel(context.Canceled)
|
||||
|
||||
gcCtx, stopGC := context.WithCancel(context.WithoutCancel(ctx))
|
||||
gc := NewGarbageCollector(s.pg, s.logger)
|
||||
|
||||
wg.Go(
|
||||
func() {
|
||||
if err := gc.Run(gcCtx); err != nil {
|
||||
@@ -260,7 +264,9 @@ func (s *Service) EnabledProviders() []coredata.OIDCProvider {
|
||||
for p := range s.providers {
|
||||
providers = append(providers, p)
|
||||
}
|
||||
|
||||
slices.Sort(providers)
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
@@ -306,6 +312,7 @@ func (s *Service) InitiateLogin(
|
||||
if err := oidcState.Insert(ctx, tx); err != nil {
|
||||
return fmt.Errorf("cannot store oidc state: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
@@ -337,6 +344,7 @@ func (s *Service) HandleCallback(
|
||||
}
|
||||
|
||||
var oidcState coredata.OIDCState
|
||||
|
||||
err := s.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
@@ -344,6 +352,7 @@ func (s *Service) HandleCallback(
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return NewInvalidStateError()
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load oidc state: %w", err)
|
||||
}
|
||||
|
||||
@@ -403,12 +412,14 @@ func (s *Service) HandleCallback(
|
||||
}
|
||||
|
||||
var identity *coredata.Identity
|
||||
|
||||
now := time.Now()
|
||||
|
||||
err = s.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
identity = &coredata.Identity{}
|
||||
|
||||
err := identity.LoadByEmail(ctx, tx, email)
|
||||
if err != nil {
|
||||
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
@@ -475,6 +486,7 @@ func (s *Service) verifyAndParseIDToken(ctx context.Context, info *providerInfo,
|
||||
}
|
||||
|
||||
signedContent := parts[0] + "." + parts[1]
|
||||
|
||||
signature, err := base64.RawURLEncoding.DecodeString(parts[2])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot decode signature: %w", err)
|
||||
@@ -525,6 +537,7 @@ func (s *Service) getSigningKey(ctx context.Context, jwksURL string, kid string)
|
||||
}
|
||||
|
||||
entry = &jwksEntry{keys: keys, fetchedAt: time.Now()}
|
||||
|
||||
s.jwksMu.Lock()
|
||||
s.jwksCache[jwksURL] = entry
|
||||
s.jwksMu.Unlock()
|
||||
@@ -543,6 +556,7 @@ func (s *Service) getSigningKey(ctx context.Context, jwksURL string, kid string)
|
||||
}
|
||||
|
||||
entry = &jwksEntry{keys: keys, fetchedAt: time.Now()}
|
||||
|
||||
s.jwksMu.Lock()
|
||||
s.jwksCache[jwksURL] = entry
|
||||
s.jwksMu.Unlock()
|
||||
@@ -566,6 +580,7 @@ func fetchJWKS(ctx context.Context, httpClient *http.Client, jwksURL string) ([]
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch jwks: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
@@ -592,12 +607,14 @@ func parseJWK(k jwk) (crypto.PublicKey, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot decode RSA modulus: %w", err)
|
||||
}
|
||||
|
||||
eBytes, err := base64.RawURLEncoding.DecodeString(k.E)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot decode RSA exponent: %w", err)
|
||||
}
|
||||
|
||||
n := new(big.Int).SetBytes(nBytes)
|
||||
|
||||
e := 0
|
||||
for _, b := range eBytes {
|
||||
e = e<<8 + int(b)
|
||||
@@ -607,6 +624,7 @@ func parseJWK(k jwk) (crypto.PublicKey, error) {
|
||||
|
||||
case "EC":
|
||||
var curve elliptic.Curve
|
||||
|
||||
switch k.Crv {
|
||||
case "P-256":
|
||||
curve = elliptic.P256()
|
||||
@@ -622,6 +640,7 @@ func parseJWK(k jwk) (crypto.PublicKey, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot decode EC X: %w", err)
|
||||
}
|
||||
|
||||
yBytes, err := base64.RawURLEncoding.DecodeString(k.Y)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot decode EC Y: %w", err)
|
||||
@@ -647,6 +666,7 @@ func verifySignature(alg string, key crypto.PublicKey, signedContent []byte, sig
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot verify RS256 signature: expected RSA public key")
|
||||
}
|
||||
|
||||
return rsa.VerifyPKCS1v15(rsaKey, crypto.SHA256, hash[:], signature)
|
||||
|
||||
case "ES256":
|
||||
@@ -676,6 +696,7 @@ func verifySignature(alg string, key crypto.PublicKey, signedContent []byte, sig
|
||||
if !ecdsa.VerifyASN1(ecKey, hash[:], derSig) {
|
||||
return fmt.Errorf("cannot verify ECDSA signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
default:
|
||||
@@ -693,5 +714,6 @@ func generateRandomString(length int) (string, error) {
|
||||
if _, err := io.ReadFull(cryptoRandReader, b); err != nil {
|
||||
return "", fmt.Errorf("cannot generate random bytes: %w", err)
|
||||
}
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
@@ -190,12 +190,15 @@ func (req UpdateOrganizationRequest) Validate() error {
|
||||
v.Check(req.Email, "email", validator.SafeText(255))
|
||||
v.Check(req.HeadquarterAddress, "headquarter_address", validator.SafeText(2048))
|
||||
v.Check(req.LogoFile, "logo_file", validator.NotEmpty())
|
||||
|
||||
if req.LogoFile != nil {
|
||||
if err := fv.Validate(req.LogoFile.Filename, req.LogoFile.ContentType, req.LogoFile.Size); err != nil {
|
||||
return fmt.Errorf("invalid logo file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
v.Check(req.HorizontalLogoFile, "horizontal_logo_file", validator.NotEmpty())
|
||||
|
||||
if req.HorizontalLogoFile != nil {
|
||||
if err := fv.Validate(req.HorizontalLogoFile.Filename, req.HorizontalLogoFile.ContentType, req.HorizontalLogoFile.Size); err != nil {
|
||||
return fmt.Errorf("invalid horizontal logo file: %w", err)
|
||||
@@ -250,10 +253,10 @@ func (s *OrganizationService) UpdateMembership(
|
||||
scope := coredata.NewScopeFromObjectID(organizationID)
|
||||
|
||||
membership := coredata.Membership{}
|
||||
|
||||
if err := s.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
|
||||
if err := membership.LoadByID(ctx, tx, scope, membershipID); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewMembershipNotFoundError(membershipID)
|
||||
@@ -273,6 +276,7 @@ func (s *OrganizationService) UpdateMembership(
|
||||
|
||||
if membership.Role == coredata.MembershipRoleOwner && 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)
|
||||
@@ -334,6 +338,7 @@ func (s *OrganizationService) RemoveUser(
|
||||
|
||||
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)
|
||||
@@ -382,6 +387,7 @@ func (s *OrganizationService) InviteUser(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
organization := coredata.Organization{}
|
||||
|
||||
err := organization.LoadByID(ctx, tx, scope, req.OrganizationID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -448,7 +454,6 @@ func (s *OrganizationService) InviteUser(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -556,7 +561,6 @@ func (s *OrganizationService) CreateOrganization(
|
||||
"organization-id": organization.ID.String(),
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot upload logo file: %w", err)
|
||||
}
|
||||
@@ -594,7 +598,6 @@ func (s *OrganizationService) CreateOrganization(
|
||||
"organization-id": organization.ID.String(),
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot upload logo file: %w", err)
|
||||
}
|
||||
@@ -606,6 +609,7 @@ func (s *OrganizationService) CreateOrganization(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
identity := &coredata.Identity{}
|
||||
|
||||
err := identity.LoadByID(ctx, tx, identityID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load identity: %w", err)
|
||||
@@ -737,7 +741,6 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati
|
||||
"organization-id": organizationID.String(),
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot upload logo file: %w", err)
|
||||
}
|
||||
@@ -775,7 +778,6 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati
|
||||
"organization-id": organizationID.String(),
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot upload logo file: %w", err)
|
||||
}
|
||||
@@ -811,6 +813,7 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati
|
||||
return fmt.Errorf("invalid email address: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
organization.Email = *req.Email
|
||||
}
|
||||
|
||||
@@ -871,6 +874,7 @@ func (s *OrganizationService) DeleteOrganization(ctx context.Context, organizati
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
organization := &coredata.Organization{}
|
||||
|
||||
err := organization.LoadByID(ctx, tx, scope, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load organization: %w", err)
|
||||
@@ -970,7 +974,6 @@ func (s *OrganizationService) CreateUser(ctx context.Context, req *CreateUserReq
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1017,6 +1020,7 @@ func (s *OrganizationService) UpdateUser(ctx context.Context, req *UpdateUserReq
|
||||
}
|
||||
|
||||
membership := &coredata.Membership{}
|
||||
|
||||
var webhookPayload *webhooktypes.User
|
||||
|
||||
if err := membership.LoadByIdentityIDAndOrganizationID(ctx, conn, scope, profile.IdentityID, profile.OrganizationID); err != nil {
|
||||
@@ -1042,7 +1046,6 @@ func (s *OrganizationService) UpdateUser(ctx context.Context, req *UpdateUserReq
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1077,7 +1080,6 @@ func (s *OrganizationService) UpdateUserState(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1102,7 +1104,6 @@ func (s *OrganizationService) GetProfile(ctx context.Context, profileID gid.GID)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1162,7 +1163,6 @@ func (s *OrganizationService) GetProfileForIdentityAndOrganization(ctx context.C
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1191,7 +1191,6 @@ func (s *OrganizationService) ListProfiles(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1213,6 +1212,7 @@ func (s OrganizationService) CountProfiles(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
profiles := coredata.MembershipProfiles{}
|
||||
|
||||
count, err = profiles.CountByOrganizationID(ctx, conn, scope, organizationID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count profiles: %w", err)
|
||||
@@ -1235,6 +1235,7 @@ func (s *OrganizationService) GetOrganizationForMembership(ctx context.Context,
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
membership := &coredata.Membership{}
|
||||
|
||||
err := membership.LoadByID(ctx, conn, scope, membershipID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -1256,7 +1257,6 @@ func (s *OrganizationService) GetOrganizationForMembership(ctx context.Context,
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1425,6 +1425,7 @@ func (s OrganizationService) CountSAMLConfigurations(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
samlConfigurations := coredata.SAMLConfigurations{}
|
||||
|
||||
count, err = samlConfigurations.CountByOrganizationID(ctx, conn, scope, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count saml configurations: %w", err)
|
||||
@@ -1478,6 +1479,7 @@ func (s OrganizationService) CountSCIMEvents(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
scimEvents := coredata.SCIMEvents{}
|
||||
|
||||
count, err = scimEvents.CountByOrganizationID(ctx, conn, scope, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count scim events: %w", err)
|
||||
@@ -1510,10 +1512,10 @@ func (s OrganizationService) GetSCIMConfiguration(
|
||||
|
||||
return fmt.Errorf("cannot load SCIM configuration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1551,12 +1553,13 @@ func (s OrganizationService) CreateSCIMConfiguration(
|
||||
if err == coredata.ErrResourceAlreadyExists {
|
||||
return scim.NewSCIMConfigurationAlreadyExistsError(organizationID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot insert SCIM configuration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -1575,6 +1578,7 @@ func (s OrganizationService) DeleteSCIMConfiguration(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
config := &coredata.SCIMConfiguration{}
|
||||
|
||||
err := config.LoadByID(ctx, tx, scope, configID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -1589,6 +1593,7 @@ func (s OrganizationService) DeleteSCIMConfiguration(
|
||||
}
|
||||
|
||||
profiles := &coredata.MembershipProfiles{}
|
||||
|
||||
err = profiles.ResetSCIMSources(ctx, tx, scope, config.OrganizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot reset user sources: %w", err)
|
||||
@@ -1596,6 +1601,7 @@ func (s OrganizationService) DeleteSCIMConfiguration(
|
||||
|
||||
// Delete SCIM bridge and its connector if they exist
|
||||
bridge := &coredata.SCIMBridge{}
|
||||
|
||||
err = bridge.LoadBySCIMConfigurationID(ctx, tx, scope, configID)
|
||||
if err != nil && err != coredata.ErrResourceNotFound {
|
||||
return fmt.Errorf("cannot load SCIM bridge: %w", err)
|
||||
@@ -1608,6 +1614,7 @@ func (s OrganizationService) DeleteSCIMConfiguration(
|
||||
// bridge alone is sufficient to unbind SCIM from the connector.
|
||||
if bridge.ConnectorID != nil {
|
||||
accessSources := &coredata.AccessSources{}
|
||||
|
||||
count, err := accessSources.CountByConnectorID(ctx, tx, scope, *bridge.ConnectorID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count access sources for connector: %w", err)
|
||||
@@ -1615,6 +1622,7 @@ func (s OrganizationService) DeleteSCIMConfiguration(
|
||||
|
||||
if count == 0 {
|
||||
connector := &coredata.Connector{ID: *bridge.ConnectorID}
|
||||
|
||||
err = connector.Delete(ctx, tx, scope)
|
||||
if err != nil && err != coredata.ErrResourceNotFound {
|
||||
return fmt.Errorf("cannot delete connector: %w", err)
|
||||
@@ -1680,7 +1688,6 @@ func (s OrganizationService) RegenerateSCIMToken(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -1724,7 +1731,6 @@ func (s OrganizationService) UpdateSCIMBridge(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1773,6 +1779,7 @@ func (s OrganizationService) CountSCIMEventsByConfigID(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
scimEvents := coredata.SCIMEvents{}
|
||||
|
||||
count, err = scimEvents.CountBySCIMConfigurationID(ctx, conn, scope, scimConfigurationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count scim events: %w", err)
|
||||
@@ -1833,6 +1840,7 @@ func (s OrganizationService) CreateSAMLConfiguration(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
organization := &coredata.Organization{}
|
||||
|
||||
err := organization.LoadByID(ctx, tx, scope, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load organization: %w", err)
|
||||
@@ -1872,12 +1880,14 @@ func (s OrganizationService) UpdateSAMLConfiguration(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
organization := &coredata.Organization{}
|
||||
|
||||
err := organization.LoadByID(ctx, tx, scope, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load organization: %w", err)
|
||||
}
|
||||
|
||||
config = &coredata.SAMLConfiguration{}
|
||||
|
||||
err = config.LoadByID(ctx, tx, scope, configID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load saml configuration: %w", err)
|
||||
@@ -1938,7 +1948,6 @@ func (s OrganizationService) UpdateSAMLConfiguration(
|
||||
}
|
||||
|
||||
return config, nil
|
||||
|
||||
}
|
||||
|
||||
func (s OrganizationService) GetOrganization(ctx context.Context, organizationID gid.GID) (*coredata.Organization, error) {
|
||||
@@ -1990,7 +1999,6 @@ func (s OrganizationService) GetSCIMBridgeByID(ctx context.Context, bridgeID gid
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2021,7 +2029,6 @@ func (s OrganizationService) GetConnectorMetadataByID(ctx context.Context, conne
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2050,7 +2057,6 @@ func (s OrganizationService) GetSCIMBridgeByOrganizationID(ctx context.Context,
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2079,20 +2085,24 @@ func (s OrganizationService) CreateSCIMBridge(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
organization := &coredata.Organization{}
|
||||
|
||||
err := organization.LoadByID(ctx, tx, scope, organizationID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewOrganizationNotFoundError(organizationID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load organization: %w", err)
|
||||
}
|
||||
|
||||
config := &coredata.SCIMConfiguration{}
|
||||
|
||||
err = config.LoadByID(ctx, tx, scope, scimConfigurationID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return scim.NewSCIMConfigurationNotFoundError(scimConfigurationID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load SCIM configuration: %w", err)
|
||||
}
|
||||
|
||||
@@ -2102,6 +2112,7 @@ func (s OrganizationService) CreateSCIMBridge(
|
||||
|
||||
// Load and validate the connector (metadata only, no decryption needed)
|
||||
existingConnector := &coredata.Connector{}
|
||||
|
||||
err = existingConnector.LoadMetadataByID(ctx, tx, scope, connectorID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -2118,6 +2129,7 @@ func (s OrganizationService) CreateSCIMBridge(
|
||||
|
||||
// Map connector provider to bridge type
|
||||
var bridgeType coredata.SCIMBridgeType
|
||||
|
||||
switch existingConnector.Provider {
|
||||
case coredata.ConnectorProviderGoogleWorkspace:
|
||||
bridgeType = coredata.SCIMBridgeTypeGoogleWorkspace
|
||||
@@ -2146,7 +2158,6 @@ func (s OrganizationService) CreateSCIMBridge(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2164,6 +2175,7 @@ func (s OrganizationService) DeleteSCIMBridge(ctx context.Context, organizationI
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
organization := &coredata.Organization{}
|
||||
|
||||
err := organization.LoadByID(ctx, tx, scope, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load organization: %w", err)
|
||||
@@ -2184,7 +2196,6 @@ func (s OrganizationService) DeleteSCIMBridge(ctx context.Context, organizationI
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2256,6 +2267,7 @@ func (s *OrganizationService) CountAuditLogEntries(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
entries := coredata.AuditLogEntries{}
|
||||
|
||||
count, err = entries.CountByOrganizationID(ctx, conn, scope, organizationID, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count audit log entries: %w", err)
|
||||
|
||||
@@ -135,12 +135,14 @@ func (e *Evaluator) statementMatches(stmt *Statement, req AuthorizationRequest)
|
||||
// Check resource match (if resources are specified)
|
||||
if len(stmt.Resources) > 0 {
|
||||
resourceMatched := false
|
||||
|
||||
for _, pattern := range stmt.Resources {
|
||||
if pattern.MatchesResource(req.Resource) {
|
||||
resourceMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !resourceMatched {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -390,12 +390,15 @@ func TestEvaluator_Evaluate_MatchedStatementAndPolicy(t *testing.T) {
|
||||
if result.MatchedStatement == nil {
|
||||
t.Fatal("Expected matched statement")
|
||||
}
|
||||
|
||||
if result.MatchedStatement.SID != "allow-get" {
|
||||
t.Errorf("Expected SID 'allow-get', got %q", result.MatchedStatement.SID)
|
||||
}
|
||||
|
||||
if result.MatchedPolicy == nil {
|
||||
t.Fatal("Expected matched policy")
|
||||
}
|
||||
|
||||
if result.MatchedPolicy.ID != "test-policy" {
|
||||
t.Errorf("Expected policy ID 'test-policy', got %q", result.MatchedPolicy.ID)
|
||||
}
|
||||
@@ -414,6 +417,7 @@ func TestEvaluator_Evaluate_MatchedStatementAndPolicy(t *testing.T) {
|
||||
if result.MatchedStatement == nil {
|
||||
t.Fatal("Expected matched statement")
|
||||
}
|
||||
|
||||
if result.MatchedStatement.SID != "deny-delete" {
|
||||
t.Errorf("Expected SID 'deny-delete', got %q", result.MatchedStatement.SID)
|
||||
}
|
||||
@@ -432,6 +436,7 @@ func TestEvaluator_Evaluate_MatchedStatementAndPolicy(t *testing.T) {
|
||||
if result.MatchedStatement != nil {
|
||||
t.Error("Expected no matched statement")
|
||||
}
|
||||
|
||||
if result.MatchedPolicy != nil {
|
||||
t.Error("Expected no matched policy")
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ func (m *ActionMatcher) Matches(pattern, target string) bool {
|
||||
if patternParts[1] == "*" {
|
||||
return patternParts[0] == targetParts[0] || patternParts[0] == "*"
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
case 3:
|
||||
@@ -74,6 +75,7 @@ func (m *ActionMatcher) matchPart(pattern, target string) bool {
|
||||
if pattern == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
return pattern == target
|
||||
}
|
||||
|
||||
@@ -84,5 +86,6 @@ func (m *ActionMatcher) MatchesAny(patterns []string, target string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
case ConditionNotEquals:
|
||||
@@ -136,6 +137,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case ConditionIn:
|
||||
@@ -153,6 +155,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -160,6 +163,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
case ConditionNotIn:
|
||||
@@ -175,6 +179,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -182,6 +187,7 @@ func (c Condition) Evaluate(ctx ConditionContext) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
default:
|
||||
@@ -196,12 +202,14 @@ func resolveKey(key string, ctx ConditionContext) (string, bool) {
|
||||
if len(key) > 10 && key[:10] == "principal." {
|
||||
attrKey := key[10:]
|
||||
val, ok := ctx.Principal[attrKey]
|
||||
|
||||
return val, ok
|
||||
}
|
||||
|
||||
if len(key) > 9 && key[:9] == "resource." {
|
||||
attrKey := key[9:]
|
||||
val, ok := ctx.Resource[attrKey]
|
||||
|
||||
return val, ok
|
||||
}
|
||||
|
||||
|
||||
@@ -284,9 +284,11 @@ func TestConditionHelpers(t *testing.T) {
|
||||
if c.Operator != ConditionEquals {
|
||||
t.Errorf("Expected ConditionEquals, got %v", c.Operator)
|
||||
}
|
||||
|
||||
if c.Key != "principal.id" {
|
||||
t.Errorf("Expected principal.id, got %v", c.Key)
|
||||
}
|
||||
|
||||
if len(c.Values) != 2 {
|
||||
t.Errorf("Expected 2 values, got %d", len(c.Values))
|
||||
}
|
||||
|
||||
@@ -51,7 +51,9 @@ func (ps *PolicySet) Merge(other *PolicySet) *PolicySet {
|
||||
for role, policies := range other.RolePolicies {
|
||||
ps.RolePolicies[role] = append(ps.RolePolicies[role], policies...)
|
||||
}
|
||||
|
||||
ps.IdentityScopedPolicies = append(ps.IdentityScopedPolicies, other.IdentityScopedPolicies...)
|
||||
|
||||
return ps
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ func extractUserAttributes(assertion *saml.Assertion, config *coredata.SAMLConfi
|
||||
|
||||
fullname = email.String()
|
||||
role = nil
|
||||
|
||||
return email, fullname, role, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -72,11 +72,13 @@ func NewService(
|
||||
|
||||
func (s *Service) Run(ctx context.Context) error {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
ctx, cancel := context.WithCancelCause(ctx)
|
||||
defer cancel(context.Canceled)
|
||||
|
||||
gcCtx, stopGC := context.WithCancel(context.WithoutCancel(ctx))
|
||||
gc := NewGarbageCollector(s.pg, s.logger)
|
||||
|
||||
wg.Go(func() {
|
||||
if err := gc.Run(gcCtx); err != nil {
|
||||
cancel(fmt.Errorf("saml garbage collector crashed: %w", err))
|
||||
@@ -112,6 +114,7 @@ func (s *Service) InitiateLogin(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
config := &coredata.SAMLConfiguration{}
|
||||
|
||||
err := config.LoadByID(ctx, tx, coredata.NewNoScope(), configID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -349,10 +352,12 @@ func (s *Service) HandleAssertion(
|
||||
|
||||
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)
|
||||
@@ -371,6 +376,7 @@ func (s *Service) HandleAssertion(
|
||||
|
||||
// Expire pending invitations for user (in case source switched to SAML)
|
||||
invitations := &coredata.Invitations{}
|
||||
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByUserID(
|
||||
ctx,
|
||||
@@ -385,7 +391,6 @@ func (s *Service) HandleAssertion(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -436,6 +441,7 @@ func (s *Service) validateAssertion(assertion *saml.Assertion, config *coredata.
|
||||
expectedAudience := baseurl.MustParse(s.baseURL).WithPath("/api/connect/v1/saml/2.0/metadata").MustString()
|
||||
|
||||
audienceValid := false
|
||||
|
||||
for _, restriction := range assertion.Conditions.AudienceRestrictions {
|
||||
if restriction.Audience.Value == expectedAudience {
|
||||
audienceValid = true
|
||||
|
||||
@@ -204,6 +204,7 @@ func (v *SAMLDomainVerifier) checkDNSTXTRecord(emailDomain string, expectedValue
|
||||
msg.Question = []dns.RR{&dns.TXT{Hdr: dns.Header{Name: fqdn, Class: dns.ClassINET}}}
|
||||
|
||||
client := dns.NewClient()
|
||||
|
||||
resp, _, err := client.Exchange(context.Background(), msg, "udp", v.resolverAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query TXT record for %q: %w", emailDomain, err)
|
||||
|
||||
@@ -67,6 +67,7 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate
|
||||
}
|
||||
|
||||
scimUsersByEmail := make(map[string]*scimclient.User)
|
||||
|
||||
for i := range scimUsers {
|
||||
email := strings.ToLower(scimUsers[i].UserName)
|
||||
scimUsersByEmail[email] = &scimUsers[i]
|
||||
@@ -86,6 +87,7 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate
|
||||
errs = append(errs, fmt.Errorf("cannot create user %q: %w", pu.ExternalID, err))
|
||||
continue
|
||||
}
|
||||
|
||||
created++
|
||||
} else {
|
||||
needsUpdate := existingSCIM.Active != pu.Active ||
|
||||
@@ -107,6 +109,7 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate
|
||||
errs = append(errs, fmt.Errorf("cannot update user %q: %w", pu.ExternalID, err))
|
||||
continue
|
||||
}
|
||||
|
||||
updated++
|
||||
} else {
|
||||
skipped++
|
||||
@@ -124,7 +127,9 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate
|
||||
errs = append(errs, fmt.Errorf("cannot delete user %q: %w", scimUser.ExternalID, err))
|
||||
continue
|
||||
}
|
||||
|
||||
deleted++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -136,6 +141,7 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate
|
||||
errs = append(errs, fmt.Errorf("cannot deactivate user %q: %w", scimUser.ExternalID, err))
|
||||
continue
|
||||
}
|
||||
|
||||
deactivated++
|
||||
}
|
||||
|
||||
@@ -148,5 +154,6 @@ func (s *Bridge) isExcluded(email string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ func NewClient(httpClient *http.Client, endpoint, token string) *Client {
|
||||
|
||||
func (c *Client) ListUsers(ctx context.Context) (Users, error) {
|
||||
var allUsers Users
|
||||
|
||||
startIndex := 1
|
||||
count := 100
|
||||
|
||||
@@ -107,6 +108,7 @@ func (c *Client) listUsersPage(ctx context.Context, startIndex, count int) (User
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("cannot fetch users: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -131,6 +133,7 @@ func (c *Client) CreateUser(ctx context.Context, user *User) error {
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/Users", c.endpoint)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create request: %w", err)
|
||||
@@ -143,6 +146,7 @@ func (c *Client) CreateUser(ctx context.Context, user *User) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create user: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
@@ -162,6 +166,7 @@ func (c *Client) UpdateUser(ctx context.Context, userID string, user *User) erro
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/Users/%s", c.endpoint, url.PathEscape(userID))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, reqURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create request: %w", err)
|
||||
@@ -174,6 +179,7 @@ func (c *Client) UpdateUser(ctx context.Context, userID string, user *User) erro
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update user: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -244,6 +250,7 @@ func (c *Client) DeactivateUser(ctx context.Context, userID string) error {
|
||||
}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/Users/%s", c.endpoint, url.PathEscape(userID))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, reqURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create request: %w", err)
|
||||
@@ -256,6 +263,7 @@ func (c *Client) DeactivateUser(ctx context.Context, userID string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot deactivate user: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||
@@ -268,6 +276,7 @@ func (c *Client) DeactivateUser(ctx context.Context, userID string) error {
|
||||
|
||||
func (c *Client) DeleteUser(ctx context.Context, userID string) error {
|
||||
reqURL := fmt.Sprintf("%s/Users/%s", c.endpoint, url.PathEscape(userID))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, reqURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create request: %w", err)
|
||||
@@ -279,6 +288,7 @@ func (c *Client) DeleteUser(ctx context.Context, userID string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete user: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {
|
||||
|
||||
@@ -57,6 +57,7 @@ func TestUser_UnmarshalJSON(t *testing.T) {
|
||||
}`)
|
||||
|
||||
var user scimclient.User
|
||||
|
||||
err := json.Unmarshal(data, &user)
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -96,6 +97,7 @@ func TestUser_UnmarshalJSON(t *testing.T) {
|
||||
}`)
|
||||
|
||||
var user scimclient.User
|
||||
|
||||
err := json.Unmarshal(data, &user)
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -123,6 +125,7 @@ func TestUser_UnmarshalJSON(t *testing.T) {
|
||||
}`)
|
||||
|
||||
var user scimclient.User
|
||||
|
||||
err := json.Unmarshal(data, &user)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -55,6 +55,7 @@ func (p *Provider) isExcluded(email string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -65,6 +66,7 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) {
|
||||
}
|
||||
|
||||
var allUsers scimclient.Users
|
||||
|
||||
pageToken := ""
|
||||
|
||||
for {
|
||||
@@ -128,12 +130,16 @@ func (p *Provider) extractOrganizationFields(raw any, user *scimclient.User) {
|
||||
return
|
||||
}
|
||||
|
||||
var primary *admin.UserOrganization
|
||||
var first *admin.UserOrganization
|
||||
var (
|
||||
primary *admin.UserOrganization
|
||||
first *admin.UserOrganization
|
||||
)
|
||||
|
||||
for i := range orgs {
|
||||
if first == nil {
|
||||
first = &orgs[i]
|
||||
}
|
||||
|
||||
if orgs[i].Primary {
|
||||
primary = &orgs[i]
|
||||
break
|
||||
@@ -144,6 +150,7 @@ func (p *Provider) extractOrganizationFields(raw any, user *scimclient.User) {
|
||||
if org == nil {
|
||||
org = first
|
||||
}
|
||||
|
||||
if org == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ func (p *Provider) isExcluded(email string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -115,6 +116,7 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) {
|
||||
}
|
||||
|
||||
var allUsers scimclient.Users
|
||||
|
||||
for range graphMaxPages {
|
||||
users, next, err := p.fetchPage(ctx, endpoint)
|
||||
if err != nil {
|
||||
@@ -126,9 +128,11 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) {
|
||||
if email == "" {
|
||||
email = u.UserPrincipalName
|
||||
}
|
||||
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.isExcluded(email) {
|
||||
continue
|
||||
}
|
||||
@@ -151,6 +155,7 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) {
|
||||
if next == "" {
|
||||
return allUsers, nil
|
||||
}
|
||||
|
||||
endpoint = next
|
||||
}
|
||||
|
||||
@@ -177,12 +182,14 @@ func (p *Provider) fetchPage(ctx context.Context, endpoint string) ([]graphUser,
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("cannot create graph users request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("cannot list graph users: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
|
||||
@@ -77,18 +77,23 @@ func NewBridgeRunner(
|
||||
if cfg.Interval == 0 {
|
||||
cfg.Interval = 15 * time.Minute
|
||||
}
|
||||
|
||||
if cfg.PollInterval == 0 {
|
||||
cfg.PollInterval = 30 * time.Second
|
||||
}
|
||||
|
||||
if cfg.SyncTimeout == 0 {
|
||||
cfg.SyncTimeout = 5 * time.Minute
|
||||
}
|
||||
|
||||
if cfg.MaxBackoff == 0 {
|
||||
cfg.MaxBackoff = DefaultMaxBackoff
|
||||
}
|
||||
|
||||
if cfg.MaxConsecutiveFailures == 0 {
|
||||
cfg.MaxConsecutiveFailures = DefaultMaxConsecutiveFailures
|
||||
}
|
||||
|
||||
if cfg.StaleSyncThreshold == 0 {
|
||||
cfg.StaleSyncThreshold = DefaultStaleSyncThreshold
|
||||
}
|
||||
@@ -161,6 +166,7 @@ func (r *BridgeRunner) processBridge(ctx context.Context) error {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, "sync failed")
|
||||
|
||||
return r.transitionToFailed(ctx, bridge, scope, err, duration, logger)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ func (r *BridgeRunner) calculateBackoff(consecutiveFailures int) time.Duration {
|
||||
// Cap the shift exponent to prevent integer overflow from the shift itself.
|
||||
// Bit 63 is the sign bit, so shifting by 63+ produces negative or zero values.
|
||||
const maxShift = 62
|
||||
|
||||
shiftAmount := min(consecutiveFailures, maxShift)
|
||||
|
||||
backoff := r.cfg.Interval * time.Duration(1<<shiftAmount)
|
||||
|
||||
@@ -33,8 +33,10 @@ type SyncStats struct {
|
||||
}
|
||||
|
||||
func (r *BridgeRunner) acquireNextBridge(ctx context.Context) (*coredata.SCIMBridge, coredata.Scoper, error) {
|
||||
var bridge *coredata.SCIMBridge
|
||||
var scope coredata.Scoper
|
||||
var (
|
||||
bridge *coredata.SCIMBridge
|
||||
scope coredata.Scoper
|
||||
)
|
||||
|
||||
err := r.pg.WithTx(
|
||||
ctx,
|
||||
@@ -53,7 +55,6 @@ func (r *BridgeRunner) acquireNextBridge(ctx context.Context) (*coredata.SCIMBri
|
||||
return bridge.Update(ctx, tx, scope)
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -90,6 +91,7 @@ func (r *BridgeRunner) transitionToSuccess(
|
||||
"cannot update bridge after successful sync",
|
||||
log.Error(err),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -178,6 +180,7 @@ func (r *BridgeRunner) transitionToFailed(
|
||||
log.String("new_state", string(bridge.State)),
|
||||
log.Error(err),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ func (r *BridgeRunner) executeSync(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
var err error
|
||||
|
||||
idp, token, dbConnector, err = r.prepareSync(
|
||||
ctx,
|
||||
tx,
|
||||
@@ -56,7 +57,6 @@ func (r *BridgeRunner) executeSync(
|
||||
scope,
|
||||
logger,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot prepare sync: %w", err)
|
||||
}
|
||||
@@ -126,6 +126,7 @@ func (r *BridgeRunner) prepareSync(
|
||||
}
|
||||
|
||||
scimConfig.HashedToken = HashToken(token)
|
||||
|
||||
scimConfig.UpdatedAt = time.Now()
|
||||
if err := scimConfig.Update(ctx, tx, scope); err != nil {
|
||||
return nil, "", nil, fmt.Errorf("cannot update SCIM configuration token: %w", err)
|
||||
@@ -192,6 +193,7 @@ func (r *BridgeRunner) createOAuth2BridgeProvider(
|
||||
}
|
||||
|
||||
providerName := dbConnector.Provider.String()
|
||||
|
||||
refreshCfg := r.connectorRegistry.GetOAuth2RefreshConfig(providerName)
|
||||
if refreshCfg == nil {
|
||||
logger.WarnCtx(
|
||||
@@ -200,10 +202,12 @@ func (r *BridgeRunner) createOAuth2BridgeProvider(
|
||||
log.String("connector_id", dbConnector.ID.String()),
|
||||
log.String("connector_provider", providerName),
|
||||
)
|
||||
|
||||
httpClient, err := oauth2Conn.ClientWithOptions(ctx, httpClientOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create HTTP client: %w", err)
|
||||
}
|
||||
|
||||
return factory(httpClient), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ func ParseUserFilter(expr scimfilter.Expression) (*coredata.MembershipProfileFil
|
||||
return nil, scimerrors.ScimErrorBadRequest(
|
||||
fmt.Sprintf("logical operator '%s' is not supported, only 'and' is supported", e.Operator))
|
||||
}
|
||||
|
||||
stack = append(stack, e.Left, e.Right)
|
||||
|
||||
case *scimfilter.NotExpression:
|
||||
|
||||
@@ -106,12 +106,13 @@ func (s *Service) ValidateToken(ctx context.Context, token string) (*coredata.SC
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewSCIMInvalidTokenError()
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load SCIM configuration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -128,6 +129,7 @@ func (s *Service) CreateUser(
|
||||
if attrs.UserName == "" {
|
||||
return scim.Resource{}, scimerrors.ScimErrorBadRequest("userName is required")
|
||||
}
|
||||
|
||||
if attrs.Email == "" {
|
||||
return scim.Resource{}, scimerrors.ScimErrorBadRequest("a valid email is required (via emails array or userName)")
|
||||
}
|
||||
@@ -136,6 +138,7 @@ func (s *Service) CreateUser(
|
||||
if err != nil {
|
||||
return scim.Resource{}, scimerrors.ScimErrorBadRequest("invalid email format")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
profileState := coredata.ProfileStateActive
|
||||
@@ -148,8 +151,10 @@ func (s *Service) CreateUser(
|
||||
externalIdPtr = &attrs.ExternalID
|
||||
}
|
||||
|
||||
var membership *coredata.Membership
|
||||
var profile *coredata.MembershipProfile
|
||||
var (
|
||||
membership *coredata.Membership
|
||||
profile *coredata.MembershipProfile
|
||||
)
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(config.OrganizationID)
|
||||
|
||||
@@ -205,6 +210,7 @@ func (s *Service) CreateUser(
|
||||
// Migrate the existing membership to the new identity
|
||||
// so the user's role is preserved.
|
||||
oldIdentityID := profile.IdentityID
|
||||
|
||||
existingMembership := &coredata.Membership{}
|
||||
if err := existingMembership.LoadByIdentityIDAndOrganizationID(
|
||||
ctx,
|
||||
@@ -214,6 +220,7 @@ func (s *Service) CreateUser(
|
||||
config.OrganizationID,
|
||||
); err == nil {
|
||||
existingMembership.IdentityID = identity.ID
|
||||
|
||||
existingMembership.UpdatedAt = now
|
||||
if err := existingMembership.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot update membership identity: %w", err)
|
||||
@@ -225,6 +232,7 @@ func (s *Service) CreateUser(
|
||||
profile.IdentityID = identity.ID
|
||||
profile.EmailAddress = emailAddr
|
||||
applyUserAttributes(profile, attrs, externalIdPtr, profileState, now)
|
||||
|
||||
if err := profile.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot update profile: %w", err)
|
||||
}
|
||||
@@ -248,8 +256,10 @@ func (s *Service) CreateUser(
|
||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||
return scimerrors.ScimErrorUniqueness
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot insert profile: %w", err)
|
||||
}
|
||||
|
||||
eventType = coredata.WebhookEventTypeUserCreated
|
||||
}
|
||||
} else {
|
||||
@@ -270,6 +280,7 @@ func (s *Service) CreateUser(
|
||||
}
|
||||
|
||||
applyUserAttributes(profile, attrs, externalIdPtr, profileState, now)
|
||||
|
||||
if err := profile.Update(ctx, tx, scope); err != nil {
|
||||
return fmt.Errorf("cannot update profile: %w", err)
|
||||
}
|
||||
@@ -277,6 +288,7 @@ func (s *Service) CreateUser(
|
||||
|
||||
if !attrs.Active {
|
||||
invitations := &coredata.Invitations{}
|
||||
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByUserID(
|
||||
ctx,
|
||||
@@ -322,7 +334,6 @@ func (s *Service) CreateUser(
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
@@ -347,6 +358,7 @@ func (s *Service) GetUser(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return scimerrors.ScimErrorResourceNotFound(profileID.String())
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
@@ -357,7 +369,6 @@ func (s *Service) GetUser(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
@@ -398,7 +409,6 @@ func (s *Service) ListUsers(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -418,6 +428,7 @@ func (s *Service) ReplaceUser(
|
||||
attributes scim.ResourceAttributes,
|
||||
) (scim.Resource, error) {
|
||||
attrs := ParseUserFromReplaceAttributes(attributes)
|
||||
|
||||
profile, err := s.updateUser(ctx, config, profileID, attrs)
|
||||
if err != nil {
|
||||
return scim.Resource{}, err
|
||||
@@ -504,6 +515,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.ExternalID = attrs.ExternalID
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -513,6 +525,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.Kind = attrs.UserType
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -522,6 +535,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.Nickname = attrs.Nickname
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -531,6 +545,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.Locale = attrs.Locale
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -540,6 +555,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.Timezone = attrs.Timezone
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -549,6 +565,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.ProfileUrl = attrs.ProfileUrl
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -558,6 +575,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.PreferredLanguage = attrs.PreferredLanguage
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -567,6 +585,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.GivenName = attrs.GivenName
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -576,6 +595,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.FamilyName = attrs.FamilyName
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -585,6 +605,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.FormattedName = attrs.FormattedName
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -594,6 +615,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.MiddleName = attrs.MiddleName
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -603,6 +625,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.HonorificPrefix = attrs.HonorificPrefix
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -612,6 +635,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.HonorificSuffix = attrs.HonorificSuffix
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -621,6 +645,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.EmployeeNumber = attrs.EmployeeNumber
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -630,6 +655,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.Department = attrs.Department
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -639,6 +665,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.CostCenter = attrs.CostCenter
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -648,6 +675,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.EnterpriseOrganization = attrs.EnterpriseOrganization
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -657,6 +685,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.Division = attrs.Division
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -666,6 +695,7 @@ func (s *Service) updateUser(
|
||||
} else {
|
||||
profile.ManagerValue = attrs.ManagerValue
|
||||
}
|
||||
|
||||
profile.UpdatedAt = now
|
||||
}
|
||||
|
||||
@@ -688,6 +718,7 @@ func (s *Service) updateUser(
|
||||
|
||||
if shouldDeactivate {
|
||||
invitations := &coredata.Invitations{}
|
||||
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByUserID(
|
||||
ctx,
|
||||
@@ -727,7 +758,6 @@ func (s *Service) updateUser(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -788,6 +818,7 @@ func (s *Service) DeleteUser(
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return scimerrors.ScimErrorResourceNotFound(profileID.String())
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
@@ -796,6 +827,7 @@ func (s *Service) DeleteUser(
|
||||
}
|
||||
|
||||
invitations := &coredata.Invitations{}
|
||||
|
||||
onlyPending := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
|
||||
if err := invitations.ExpireByUserID(
|
||||
ctx,
|
||||
@@ -808,6 +840,7 @@ func (s *Service) DeleteUser(
|
||||
}
|
||||
|
||||
var membership *coredata.Membership
|
||||
|
||||
m := &coredata.Membership{}
|
||||
if err := m.LoadByIdentityIDAndOrganizationID(
|
||||
ctx, tx, scope, profile.IdentityID, config.OrganizationID,
|
||||
@@ -859,10 +892,10 @@ func (s *Service) LogEvent(
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert SCIM event: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
s.logger.ErrorCtx(ctx, "cannot log SCIM event", log.Error(err))
|
||||
}
|
||||
@@ -941,6 +974,7 @@ func ParseUserFromAttributes(attributes scim.ResourceAttributes) scimUserAttribu
|
||||
attrs.HonorificPrefix, _ = name["honorificPrefix"].(string)
|
||||
attrs.HonorificSuffix, _ = name["honorificSuffix"].(string)
|
||||
}
|
||||
|
||||
attrs.GivenName = givenName
|
||||
attrs.FamilyName = familyName
|
||||
|
||||
@@ -955,6 +989,7 @@ func ParseUserFromAttributes(attributes scim.ResourceAttributes) scimUserAttribu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attrs.Email == "" {
|
||||
if emailMap, ok := emails[0].(map[string]any); ok {
|
||||
if value, ok := emailMap["value"].(string); ok {
|
||||
@@ -974,6 +1009,7 @@ func ParseUserFromAttributes(attributes scim.ResourceAttributes) scimUserAttribu
|
||||
if attrs.FullName == "" {
|
||||
attrs.FullName = strings.TrimSpace(givenName + " " + familyName)
|
||||
}
|
||||
|
||||
if attrs.FullName == "" {
|
||||
attrs.FullName = attrs.UserName
|
||||
}
|
||||
@@ -991,6 +1027,7 @@ func ParseUserFromAttributes(attributes scim.ResourceAttributes) scimUserAttribu
|
||||
attrs.Department, _ = enterprise["department"].(string)
|
||||
attrs.CostCenter, _ = enterprise["costCenter"].(string)
|
||||
attrs.EnterpriseOrganization, _ = enterprise["organization"].(string)
|
||||
|
||||
attrs.Division, _ = enterprise["division"].(string)
|
||||
if manager, ok := enterprise["manager"].(map[string]any); ok {
|
||||
attrs.ManagerValue, _ = manager["value"].(string)
|
||||
@@ -1028,25 +1065,31 @@ type scimReplaceAttributes struct {
|
||||
|
||||
func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) scimReplaceAttributes {
|
||||
var attrs scimReplaceAttributes
|
||||
|
||||
displayName, _ := attributes["displayName"].(string)
|
||||
|
||||
var givenName, familyName string
|
||||
if name, ok := attributes["name"].(map[string]any); ok {
|
||||
givenName, _ = name["givenName"].(string)
|
||||
|
||||
familyName, _ = name["familyName"].(string)
|
||||
if fn, ok := name["formatted"].(string); ok {
|
||||
attrs.FormattedName = &fn
|
||||
}
|
||||
|
||||
if mn, ok := name["middleName"].(string); ok {
|
||||
attrs.MiddleName = &mn
|
||||
}
|
||||
|
||||
if hp, ok := name["honorificPrefix"].(string); ok {
|
||||
attrs.HonorificPrefix = &hp
|
||||
}
|
||||
|
||||
if hs, ok := name["honorificSuffix"].(string); ok {
|
||||
attrs.HonorificSuffix = &hs
|
||||
}
|
||||
}
|
||||
|
||||
attrs.GivenName = &givenName
|
||||
attrs.FamilyName = &familyName
|
||||
|
||||
@@ -1059,6 +1102,7 @@ func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) scimRepl
|
||||
if a, ok := attributes["active"].(bool); ok {
|
||||
activeVal = a
|
||||
}
|
||||
|
||||
attrs.Active = &activeVal
|
||||
|
||||
t, _ := attributes["title"].(string)
|
||||
@@ -1075,18 +1119,23 @@ func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) scimRepl
|
||||
if ut, ok := attributes["userType"].(string); ok {
|
||||
attrs.UserType = &ut
|
||||
}
|
||||
|
||||
if nn, ok := attributes["nickName"].(string); ok {
|
||||
attrs.Nickname = &nn
|
||||
}
|
||||
|
||||
if l, ok := attributes["locale"].(string); ok {
|
||||
attrs.Locale = &l
|
||||
}
|
||||
|
||||
if tz, ok := attributes["timezone"].(string); ok {
|
||||
attrs.Timezone = &tz
|
||||
}
|
||||
|
||||
if pu, ok := attributes["profileUrl"].(string); ok {
|
||||
attrs.ProfileUrl = &pu
|
||||
}
|
||||
|
||||
if pl, ok := attributes["preferredLanguage"].(string); ok {
|
||||
attrs.PreferredLanguage = &pl
|
||||
}
|
||||
@@ -1095,18 +1144,23 @@ func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) scimRepl
|
||||
if en, ok := enterprise["employeeNumber"].(string); ok {
|
||||
attrs.EmployeeNumber = &en
|
||||
}
|
||||
|
||||
if dept, ok := enterprise["department"].(string); ok {
|
||||
attrs.Department = &dept
|
||||
}
|
||||
|
||||
if cc, ok := enterprise["costCenter"].(string); ok {
|
||||
attrs.CostCenter = &cc
|
||||
}
|
||||
|
||||
if org, ok := enterprise["organization"].(string); ok {
|
||||
attrs.EnterpriseOrganization = &org
|
||||
}
|
||||
|
||||
if div, ok := enterprise["division"].(string); ok {
|
||||
attrs.Division = &div
|
||||
}
|
||||
|
||||
if manager, ok := enterprise["manager"].(map[string]any); ok {
|
||||
if mv, ok := manager["value"].(string); ok {
|
||||
attrs.ManagerValue = &mv
|
||||
@@ -1118,8 +1172,11 @@ func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) scimRepl
|
||||
}
|
||||
|
||||
func ParseUserFromPatchOperations(operations []scim.PatchOperation) scimReplaceAttributes {
|
||||
var attrs scimReplaceAttributes
|
||||
var givenName, familyName string
|
||||
var (
|
||||
attrs scimReplaceAttributes
|
||||
givenName, familyName string
|
||||
)
|
||||
|
||||
empty := ""
|
||||
|
||||
for _, op := range operations {
|
||||
@@ -1186,6 +1243,7 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) scimReplaceA
|
||||
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:user:manager.value":
|
||||
attrs.ManagerValue = &empty
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1200,72 +1258,94 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) scimReplaceA
|
||||
if a, ok := valueMap["active"].(bool); ok {
|
||||
attrs.Active = &a
|
||||
}
|
||||
|
||||
if name, ok := valueMap["displayName"].(string); ok {
|
||||
attrs.FullName = name
|
||||
}
|
||||
|
||||
if nameMap, ok := valueMap["name"].(map[string]any); ok {
|
||||
if gn, ok := nameMap["givenName"].(string); ok {
|
||||
givenName = gn
|
||||
}
|
||||
|
||||
if fn, ok := nameMap["familyName"].(string); ok {
|
||||
familyName = fn
|
||||
}
|
||||
|
||||
if fm, ok := nameMap["formatted"].(string); ok {
|
||||
attrs.FormattedName = &fm
|
||||
}
|
||||
|
||||
if mn, ok := nameMap["middleName"].(string); ok {
|
||||
attrs.MiddleName = &mn
|
||||
}
|
||||
|
||||
if hp, ok := nameMap["honorificPrefix"].(string); ok {
|
||||
attrs.HonorificPrefix = &hp
|
||||
}
|
||||
|
||||
if hs, ok := nameMap["honorificSuffix"].(string); ok {
|
||||
attrs.HonorificSuffix = &hs
|
||||
}
|
||||
}
|
||||
|
||||
if un, ok := valueMap["userName"].(string); ok && un != "" {
|
||||
attrs.UserName = &un
|
||||
}
|
||||
|
||||
if eid, ok := valueMap["externalId"].(string); ok && eid != "" {
|
||||
attrs.ExternalID = &eid
|
||||
}
|
||||
|
||||
if t, ok := valueMap["title"].(string); ok {
|
||||
attrs.Title = &t
|
||||
}
|
||||
|
||||
if ut, ok := valueMap["userType"].(string); ok {
|
||||
attrs.UserType = &ut
|
||||
}
|
||||
|
||||
if nn, ok := valueMap["nickName"].(string); ok {
|
||||
attrs.Nickname = &nn
|
||||
}
|
||||
|
||||
if l, ok := valueMap["locale"].(string); ok {
|
||||
attrs.Locale = &l
|
||||
}
|
||||
|
||||
if tz, ok := valueMap["timezone"].(string); ok {
|
||||
attrs.Timezone = &tz
|
||||
}
|
||||
|
||||
if pu, ok := valueMap["profileUrl"].(string); ok {
|
||||
attrs.ProfileUrl = &pu
|
||||
}
|
||||
|
||||
if pl, ok := valueMap["preferredLanguage"].(string); ok {
|
||||
attrs.PreferredLanguage = &pl
|
||||
}
|
||||
|
||||
if enterprise, ok := valueMap["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"].(map[string]any); ok {
|
||||
if en, ok := enterprise["employeeNumber"].(string); ok {
|
||||
attrs.EmployeeNumber = &en
|
||||
}
|
||||
|
||||
if dept, ok := enterprise["department"].(string); ok {
|
||||
attrs.Department = &dept
|
||||
}
|
||||
|
||||
if cc, ok := enterprise["costCenter"].(string); ok {
|
||||
attrs.CostCenter = &cc
|
||||
}
|
||||
|
||||
if org, ok := enterprise["organization"].(string); ok {
|
||||
attrs.EnterpriseOrganization = &org
|
||||
}
|
||||
|
||||
if div, ok := enterprise["division"].(string); ok {
|
||||
attrs.Division = &div
|
||||
}
|
||||
|
||||
if manager, ok := enterprise["manager"].(map[string]any); ok {
|
||||
if mv, ok := manager["value"].(string); ok {
|
||||
attrs.ManagerValue = &mv
|
||||
@@ -1328,6 +1408,7 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) scimReplaceA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1346,19 +1427,24 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) scimReplaceA
|
||||
givenName = gn
|
||||
attrs.GivenName = &givenName
|
||||
}
|
||||
|
||||
if fn, ok := nameMap["familyName"].(string); ok {
|
||||
familyName = fn
|
||||
attrs.FamilyName = &familyName
|
||||
}
|
||||
|
||||
if fm, ok := nameMap["formatted"].(string); ok {
|
||||
attrs.FormattedName = &fm
|
||||
}
|
||||
|
||||
if mn, ok := nameMap["middleName"].(string); ok {
|
||||
attrs.MiddleName = &mn
|
||||
}
|
||||
|
||||
if hp, ok := nameMap["honorificPrefix"].(string); ok {
|
||||
attrs.HonorificPrefix = &hp
|
||||
}
|
||||
|
||||
if hs, ok := nameMap["honorificSuffix"].(string); ok {
|
||||
attrs.HonorificSuffix = &hs
|
||||
}
|
||||
@@ -1430,18 +1516,23 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) scimReplaceA
|
||||
if en, ok := enterprise["employeeNumber"].(string); ok {
|
||||
attrs.EmployeeNumber = &en
|
||||
}
|
||||
|
||||
if dept, ok := enterprise["department"].(string); ok {
|
||||
attrs.Department = &dept
|
||||
}
|
||||
|
||||
if cc, ok := enterprise["costCenter"].(string); ok {
|
||||
attrs.CostCenter = &cc
|
||||
}
|
||||
|
||||
if org, ok := enterprise["organization"].(string); ok {
|
||||
attrs.EnterpriseOrganization = &org
|
||||
}
|
||||
|
||||
if div, ok := enterprise["division"].(string); ok {
|
||||
attrs.Division = &div
|
||||
}
|
||||
|
||||
if manager, ok := enterprise["manager"].(map[string]any); ok {
|
||||
if mv, ok := manager["value"].(string); ok {
|
||||
attrs.ManagerValue = &mv
|
||||
@@ -1491,6 +1582,7 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) scimReplaceA
|
||||
if givenName != "" && attrs.GivenName == nil {
|
||||
attrs.GivenName = &givenName
|
||||
}
|
||||
|
||||
if familyName != "" && attrs.FamilyName == nil {
|
||||
attrs.FamilyName = &familyName
|
||||
}
|
||||
|
||||
@@ -156,6 +156,7 @@ func NewService(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create SAML service: %w", err)
|
||||
}
|
||||
|
||||
svc.SAMLService = samlService
|
||||
|
||||
svc.OIDCService = oidc.NewService(
|
||||
@@ -207,10 +208,12 @@ func (s *Service) IsSignUpEnabled() bool {
|
||||
|
||||
func (s *Service) Run(ctx context.Context) error {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
ctx, cancel := context.WithCancelCause(ctx)
|
||||
defer cancel(context.Canceled)
|
||||
|
||||
samlCtx, stopSAML := context.WithCancel(context.WithoutCancel(ctx))
|
||||
|
||||
wg.Go(
|
||||
func() {
|
||||
if err := s.SAMLService.Run(samlCtx); err != nil {
|
||||
@@ -220,6 +223,7 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
)
|
||||
|
||||
oidcCtx, stopOIDC := context.WithCancel(context.WithoutCancel(ctx))
|
||||
|
||||
wg.Go(
|
||||
func() {
|
||||
if err := s.OIDCService.Run(oidcCtx); err != nil {
|
||||
@@ -229,6 +233,7 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
)
|
||||
|
||||
domainVerifierCtx, stopDomainVerifier := context.WithCancel(context.WithoutCancel(ctx))
|
||||
|
||||
wg.Go(
|
||||
func() {
|
||||
if err := s.samlDomainVerifier.Run(domainVerifierCtx); err != nil {
|
||||
@@ -238,6 +243,7 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
)
|
||||
|
||||
scimCtx, stopSCIM := context.WithCancel(context.WithoutCancel(ctx))
|
||||
|
||||
wg.Go(
|
||||
func() {
|
||||
if err := s.SCIMService.Run(scimCtx); err != nil {
|
||||
@@ -247,6 +253,7 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
)
|
||||
|
||||
oauth2Ctx, stopOAuth2Server := context.WithCancel(context.WithoutCancel(ctx))
|
||||
|
||||
wg.Go(
|
||||
func() {
|
||||
if err := s.OAuth2ServerService.Run(oauth2Ctx); err != nil {
|
||||
|
||||
@@ -57,6 +57,7 @@ func (s SessionService) GetSession(ctx context.Context, sessionID gid.GID) (*cor
|
||||
if now.After(session.ExpiredAt) {
|
||||
session.ExpireReason = new(coredata.ExpireReasonIdleTimeout)
|
||||
session.ExpiredAt = now
|
||||
|
||||
session.UpdatedAt = now
|
||||
if err := session.Update(ctx, tx); err != nil {
|
||||
return fmt.Errorf("cannot update session: %w", err)
|
||||
@@ -68,7 +69,6 @@ func (s SessionService) GetSession(ctx context.Context, sessionID gid.GID) (*cor
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -95,6 +95,7 @@ func (s SessionService) CloseSession(ctx context.Context, sessionID gid.GID) err
|
||||
|
||||
session.ExpireReason = new(coredata.ExpireReasonClosed)
|
||||
session.ExpiredAt = time.Now()
|
||||
|
||||
session.UpdatedAt = time.Now()
|
||||
if err := session.Update(ctx, conn); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -116,6 +117,7 @@ func (s SessionService) RevokeSession(ctx context.Context, identityID gid.GID, s
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
identity := &coredata.Identity{}
|
||||
|
||||
err := identity.LoadByID(ctx, tx, identityID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -126,6 +128,7 @@ func (s SessionService) RevokeSession(ctx context.Context, identityID gid.GID, s
|
||||
}
|
||||
|
||||
session := &coredata.Session{}
|
||||
|
||||
err = session.LoadByID(ctx, tx, sessionID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -146,6 +149,7 @@ func (s SessionService) RevokeSession(ctx context.Context, identityID gid.GID, s
|
||||
|
||||
session.ExpireReason = new(coredata.ExpireReasonRevoked)
|
||||
session.ExpiredAt = now
|
||||
|
||||
session.UpdatedAt = now
|
||||
if err := session.Update(ctx, tx); err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -156,7 +160,6 @@ func (s SessionService) RevokeSession(ctx context.Context, identityID gid.GID, s
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -168,6 +171,7 @@ func (s SessionService) RevokeAllSessions(ctx context.Context, currentSessionID
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
session := coredata.Session{}
|
||||
|
||||
err := session.LoadByID(ctx, tx, currentSessionID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -178,6 +182,7 @@ func (s SessionService) RevokeAllSessions(ctx context.Context, currentSessionID
|
||||
}
|
||||
|
||||
sessions := coredata.Sessions{}
|
||||
|
||||
count, err = sessions.ExpireAllForIdentityExceptOneSession(ctx, tx, session.IdentityID, session.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot expire all sessions: %w", err)
|
||||
@@ -195,6 +200,7 @@ func (s SessionService) UpdateSessionInfo(ctx context.Context, sessionID gid.GID
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
session := &coredata.Session{}
|
||||
|
||||
err := session.LoadByID(ctx, tx, sessionID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -226,6 +232,7 @@ func (s SessionService) UpdateSessionData(ctx context.Context, sessionID gid.GID
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
session := &coredata.Session{}
|
||||
|
||||
err := session.LoadByID(ctx, tx, sessionID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -258,6 +265,7 @@ func (s SessionService) GetActiveSessionForMembership(ctx context.Context, rootS
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
rootSession := &coredata.Session{}
|
||||
|
||||
err := rootSession.LoadByID(ctx, tx, rootSessionID)
|
||||
if err != nil {
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
@@ -276,6 +284,7 @@ func (s SessionService) GetActiveSessionForMembership(ctx context.Context, rootS
|
||||
}
|
||||
|
||||
membership := &coredata.Membership{}
|
||||
|
||||
err = membership.LoadByID(ctx, tx, coredata.NewScopeFromObjectID(membershipID), membershipID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
@@ -293,7 +302,6 @@ func (s SessionService) GetActiveSessionForMembership(ctx context.Context, rootS
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -324,6 +332,7 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewSessionNotFoundError(rootSessionID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load session: %w", err)
|
||||
}
|
||||
|
||||
@@ -345,6 +354,7 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewProfileNotFoundError(gid.Nil)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
@@ -357,6 +367,7 @@ func (s SessionService) OpenPasswordChildSessionForOrganization(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewMembershipNotFoundError(organizationID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
@@ -426,6 +437,7 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewSessionNotFoundError(rootSessionID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load session: %w", err)
|
||||
}
|
||||
|
||||
@@ -447,6 +459,7 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewProfileNotFoundError(gid.Nil)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
@@ -459,6 +472,7 @@ func (s SessionService) OpenSAMLChildSessionForOrganization(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewMembershipNotFoundError(organizationID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
@@ -514,6 +528,7 @@ func (s SessionService) AssumeOrganizationSession(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewSessionNotFoundError(sessionID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load session: %w", err)
|
||||
}
|
||||
|
||||
@@ -533,6 +548,7 @@ func (s SessionService) AssumeOrganizationSession(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewProfileNotFoundError(gid.Nil)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load profile: %w", err)
|
||||
}
|
||||
|
||||
@@ -544,10 +560,12 @@ func (s SessionService) AssumeOrganizationSession(
|
||||
if err == coredata.ErrResourceNotFound {
|
||||
return NewMembershipNotFoundError(organizationID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load membership: %w", err)
|
||||
}
|
||||
|
||||
samlConfig := &coredata.SAMLConfiguration{}
|
||||
|
||||
err := samlConfig.LoadByOrganizationIDAndEmailDomain(
|
||||
ctx,
|
||||
tx,
|
||||
@@ -611,7 +629,6 @@ func (s SessionService) AssumeOrganizationSession(
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ func PasswordValidator() validator.ValidatorFunc {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user