mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-30 11:50:43 +01:00
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(authdomain): support custom roles in SSO group mapping Role mappings now reference SigNoz roles by name (custom or managed) instead of the legacy ADMIN/EDITOR/VIEWER enum. Legacy values sent by a client are normalized to their managed names (signoz-admin/editor/viewer), and a migration normalizes existing stored mappings. - normalize legacy role names on unmarshal; resolve all matched roles on SSO callback (union of matched groups, else default, else viewer) - validate referenced roles exist on auth domain create/update - block deleting a role referenced by any auth domain mapping, naming the referencing domains (OnBeforeRoleDelete now also passes the role name) - migration 096 to rewrite legacy names in existing auth_domain mappings * refactor(sqlmigration): decode SSO role mapping into a typed struct Decode the roleMapping object into a small frozen struct instead of poking at json.RawMessage per field. The top-level config stays a raw map so the other config fields are preserved untouched. * fix(authdomain): validate SSO role attribute claim against existing roles When the role mapping uses the IDP role attribute, the claimed role is assigned only if it exists in the org (resolved case-insensitively, with legacy ADMIN/EDITOR/VIEWER mapped to their managed names); otherwise the mapping falls through to group mappings and the default role. This lets custom roles be assigned via the attribute and avoids failing login on an unknown claim. Also normalize legacy role names case-insensitively and fold the single-use managed-role lookup into NormalizeRoleName.
252 lines
8.1 KiB
Go
252 lines
8.1 KiB
Go
package impluser
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
"github.com/SigNoz/signoz/pkg/flagger"
|
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
|
"github.com/SigNoz/signoz/pkg/types"
|
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
|
"github.com/SigNoz/signoz/pkg/valuer"
|
|
)
|
|
|
|
type getter struct {
|
|
store types.UserStore
|
|
userRoleStore authtypes.UserRoleStore
|
|
flagger flagger.Flagger
|
|
}
|
|
|
|
func NewGetter(store types.UserStore, userRoleStore authtypes.UserRoleStore, flagger flagger.Flagger) user.Getter {
|
|
return &getter{store: store, userRoleStore: userRoleStore, flagger: flagger}
|
|
}
|
|
|
|
func (module *getter) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, []*authtypes.UserRole, error) {
|
|
rootUser, err := module.store.GetRootUserByOrgID(ctx, orgID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
userRoles, err := module.userRoleStore.GetUserRolesByUserID(ctx, rootUser.ID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return rootUser, userRoles, nil
|
|
}
|
|
|
|
func (module *getter) ListDeprecatedUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.DeprecatedUser, error) {
|
|
users, err := module.store.ListUsersByOrgID(ctx, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// filter root users if feature flag `hide_root_users` is true
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
hideRootUsers := module.flagger.BooleanOrEmpty(ctx, flagger.FeatureHideRootUser, evalCtx)
|
|
|
|
if hideRootUsers {
|
|
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.IsRoot })
|
|
}
|
|
|
|
userIDs := make([]valuer.UUID, len(users))
|
|
for idx, user := range users {
|
|
userIDs[idx] = user.ID
|
|
}
|
|
|
|
userRoles, err := module.userRoleStore.ListUserRolesByOrgIDAndUserIDs(ctx, orgID, userIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build userID → role name mapping directly from the joined Role
|
|
userIDToRoleNames := make(map[valuer.UUID][]string)
|
|
for _, ur := range userRoles {
|
|
if ur.Role != nil {
|
|
userIDToRoleNames[ur.UserID] = append(userIDToRoleNames[ur.UserID], ur.Role.Name)
|
|
}
|
|
}
|
|
|
|
deprecatedUsers := make([]*types.DeprecatedUser, 0, len(users))
|
|
for _, user := range users {
|
|
roleNames := userIDToRoleNames[user.ID]
|
|
|
|
if len(roleNames) == 0 {
|
|
return nil, errors.Newf(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found for user: %s", user.ID.String())
|
|
}
|
|
|
|
role := authtypes.SigNozManagedRoleToExistingLegacyRole[roleNames[0]]
|
|
deprecatedUsers = append(deprecatedUsers, types.NewDeprecatedUserFromUserAndRole(user, role))
|
|
}
|
|
|
|
return deprecatedUsers, nil
|
|
}
|
|
|
|
func (module *getter) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) {
|
|
users, err := module.store.ListUsersByOrgID(ctx, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// filter root users if feature flag `hide_root_users` is true
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
hideRootUsers := module.flagger.BooleanOrEmpty(ctx, flagger.FeatureHideRootUser, evalCtx)
|
|
|
|
if hideRootUsers {
|
|
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.IsRoot })
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (module *getter) GetDeprecatedUserByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.DeprecatedUser, error) {
|
|
user, err := module.store.GetByOrgIDAndID(ctx, orgID, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userRoles, err := module.GetRolesByUserID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(userRoles) == 0 {
|
|
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
|
}
|
|
|
|
if userRoles[0].Role == nil {
|
|
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
|
}
|
|
|
|
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
|
|
|
|
return types.NewDeprecatedUserFromUserAndRole(user, role), nil
|
|
}
|
|
|
|
func (module *getter) GetUserByOrgIDAndID(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) (*types.User, error) {
|
|
return module.store.GetByOrgIDAndID(ctx, orgID, userID)
|
|
}
|
|
|
|
func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.DeprecatedUser, error) {
|
|
user, err := module.store.GetUser(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userRoles, err := module.GetRolesByUserID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(userRoles) == 0 {
|
|
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
|
}
|
|
|
|
if userRoles[0].Role == nil {
|
|
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
|
}
|
|
|
|
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
|
|
|
|
return types.NewDeprecatedUserFromUserAndRole(user, role), nil
|
|
}
|
|
|
|
func (module *getter) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) {
|
|
return module.store.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
|
|
}
|
|
|
|
func (module *getter) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) {
|
|
count, err := module.store.CountByOrgID(ctx, orgID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (module *getter) CountByOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, statuses []string) (map[valuer.String]int64, error) {
|
|
counts, err := module.store.CountByOrgIDAndStatuses(ctx, orgID, statuses)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return counts, nil
|
|
}
|
|
|
|
func (module *getter) GetFactorPasswordByUserID(ctx context.Context, userID valuer.UUID) (*types.FactorPassword, error) {
|
|
factorPassword, err := module.store.GetPasswordByUserID(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return factorPassword, nil
|
|
}
|
|
|
|
// GetNonDeletedUserByEmailAndOrgID restricts that only one non-deleted user email can exist for an org ID, if found more, it throws an error.
|
|
func (module *getter) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error) {
|
|
existingUsers, err := module.store.GetNonDeletedUsersByEmailAndOrgID(ctx, email, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(existingUsers) > 1 {
|
|
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "Multiple non-deleted users found for email %s in org_id: %s", email.StringValue(), orgID.StringValue())
|
|
}
|
|
|
|
if len(existingUsers) == 1 {
|
|
return existingUsers[0], nil
|
|
}
|
|
|
|
return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "No non-deleted user found with email %s in org_id: %s", email.StringValue(), orgID.StringValue())
|
|
|
|
}
|
|
|
|
func (module *getter) GetRolesByUserID(ctx context.Context, userID valuer.UUID) ([]*authtypes.UserRole, error) {
|
|
userRoles, err := module.userRoleStore.GetUserRolesByUserID(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, ur := range userRoles {
|
|
if ur.Role == nil {
|
|
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
|
}
|
|
}
|
|
|
|
return userRoles, nil
|
|
}
|
|
|
|
func (module *getter) GetResetPasswordTokenByOrgIDAndUserID(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) (*types.ResetPasswordToken, error) {
|
|
return module.store.GetResetPasswordTokenByOrgIDAndUserID(ctx, orgID, userID)
|
|
}
|
|
|
|
func (module *getter) GetUsersByOrgIDAndRoleID(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID) ([]*types.User, error) {
|
|
return module.store.GetUsersByOrgIDAndRoleID(ctx, orgID, roleID)
|
|
}
|
|
|
|
func (module *getter) VerifyResetPasswordToken(ctx context.Context, token string) error {
|
|
resetPasswordToken, err := module.store.GetResetPasswordToken(ctx, token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resetPasswordToken.IsExpired() {
|
|
return errors.New(errors.TypeUnauthenticated, types.ErrCodeResetPasswordTokenExpired, "reset password token has expired")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (module *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID, roleID valuer.UUID, _ string) error {
|
|
users, err := module.GetUsersByOrgIDAndRoleID(ctx, orgID, roleID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(users) > 0 {
|
|
return errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleHasUserAssignees, "role has active user assignments, remove them before deleting")
|
|
}
|
|
return nil
|
|
}
|