Compare commits

...

1 Commits

Author SHA1 Message Date
vikrantgupta25
ba4e93050e feat(authz): introducing identity 2026-02-18 17:50:27 +05:30
20 changed files with 899 additions and 118 deletions

View File

@@ -170,14 +170,14 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId))
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, valuer.MustNewUUID(orgId))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password))
cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password), user.WithRole(types.RoleViewer))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration user: %w", err))
}

View File

@@ -0,0 +1,31 @@
package identity
import (
"context"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
// GetRoles gets all roles for an identity
GetRoles(ctx context.Context, identityID valuer.UUID) ([]types.Role, error)
// GetRolesForIdentities gets roles for multiple identities (batch)
GetRolesForIdentities(ctx context.Context, identityIDs []valuer.UUID) (map[valuer.UUID][]types.Role, error)
// CountByRoleAndOrgID counts identities with a specific role in an org
CountByRoleAndOrgID(ctx context.Context, role types.Role, orgID valuer.UUID) (int64, error)
// CreateIdentityWithRoles creates an identity and assigns roles to it
CreateIdentityWithRoles(ctx context.Context, id valuer.UUID, orgID valuer.UUID, roles []types.Role) error
// AddRole adds a role to an identity
AddRole(ctx context.Context, identityID valuer.UUID, orgID valuer.UUID, role types.Role) error
// RemoveRole removes a role from an identity
RemoveRole(ctx context.Context, identityID valuer.UUID, orgID valuer.UUID, role types.Role) error
// DeleteIdentity deletes an identity and its associated roles
DeleteIdentity(ctx context.Context, identityID valuer.UUID) error
}

View File

@@ -0,0 +1,111 @@
package implidentity
import (
"context"
"github.com/SigNoz/signoz/pkg/modules/identity"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/identitytypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store *store
}
func NewModule(sqlstore sqlstore.SQLStore) identity.Module {
return &module{
store: newStore(sqlstore),
}
}
func (m *module) CreateIdentityWithRoles(ctx context.Context, id valuer.UUID, orgID valuer.UUID, roles []types.Role) error {
return m.store.RunInTx(ctx, func(ctx context.Context) error {
// Create identity
ident := identitytypes.NewStorableIdentity(id, orgID)
if err := m.store.CreateIdentity(ctx, ident); err != nil {
return err
}
// Create identity_role mappings for each role
for _, role := range roles {
roleName := roletypes.MustGetSigNozManagedRoleFromExistingRole(role)
identityRole := identitytypes.NewStorableIdentityRole(id, roleName)
if err := m.store.CreateIdentityRole(ctx, identityRole); err != nil {
return err
}
}
return nil
})
}
func (m *module) AddRole(ctx context.Context, identityID valuer.UUID, orgID valuer.UUID, role types.Role) error {
roleName := roletypes.MustGetSigNozManagedRoleFromExistingRole(role)
identityRole := identitytypes.NewStorableIdentityRole(identityID, roleName)
return m.store.CreateIdentityRole(ctx, identityRole)
}
func (m *module) RemoveRole(ctx context.Context, identityID valuer.UUID, orgID valuer.UUID, role types.Role) error {
roleName := roletypes.MustGetSigNozManagedRoleFromExistingRole(role)
return m.store.DeleteIdentityRole(ctx, identityID, roleName)
}
func (m *module) GetRoles(ctx context.Context, identityID valuer.UUID) ([]types.Role, error) {
roleNames, err := m.store.GetRolesByIdentityID(ctx, identityID)
if err != nil {
return nil, err
}
roles := make([]types.Role, 0, len(roleNames))
for _, roleName := range roleNames {
roles = append(roles, roletypes.GetRoleFromManagedRoleName(roleName))
}
return roles, nil
}
func (m *module) GetRolesForIdentities(ctx context.Context, identityIDs []valuer.UUID) (map[valuer.UUID][]types.Role, error) {
roleNamesMap, err := m.store.GetRolesForIdentityIDs(ctx, identityIDs)
if err != nil {
return nil, err
}
result := make(map[valuer.UUID][]types.Role)
for _, id := range identityIDs {
if roleNames, ok := roleNamesMap[id.StringValue()]; ok {
roles := make([]types.Role, 0, len(roleNames))
for _, roleName := range roleNames {
roles = append(roles, roletypes.GetRoleFromManagedRoleName(roleName))
}
result[id] = roles
} else {
result[id] = []types.Role{}
}
}
return result, nil
}
func (m *module) DeleteIdentity(ctx context.Context, identityID valuer.UUID) error {
return m.store.RunInTx(ctx, func(ctx context.Context) error {
// Delete identity_role first (FK constraint)
if err := m.store.DeleteIdentityRoles(ctx, identityID); err != nil {
return err
}
// Delete identity
if err := m.store.DeleteIdentity(ctx, identityID); err != nil {
return err
}
return nil
})
}
func (m *module) CountByRoleAndOrgID(ctx context.Context, role types.Role, orgID valuer.UUID) (int64, error) {
roleName := roletypes.MustGetSigNozManagedRoleFromExistingRole(role)
return m.store.CountByRoleNameAndOrgID(ctx, roleName, orgID)
}

View File

@@ -0,0 +1,152 @@
package implidentity
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/identitytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func newStore(sqlstore sqlstore.SQLStore) *store {
return &store{sqlstore: sqlstore}
}
func (s *store) CreateIdentity(ctx context.Context, identity *identitytypes.StorableIdentity) error {
_, err := s.
sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(identity).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create identity")
}
return nil
}
func (s *store) CreateIdentityRole(ctx context.Context, identityRole *identitytypes.StorableIdentityRole) error {
_, err := s.
sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(identityRole).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create identity role")
}
return nil
}
func (s *store) DeleteIdentityRole(ctx context.Context, identityID valuer.UUID, roleName string) error {
_, err := s.
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(&identitytypes.StorableIdentityRole{}).
Where("identity_id = ?", identityID).
Where("role_name = ?", roleName).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete identity role")
}
return nil
}
func (s *store) DeleteIdentityRoles(ctx context.Context, identityID valuer.UUID) error {
_, err := s.
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(&identitytypes.StorableIdentityRole{}).
Where("identity_id = ?", identityID).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete identity roles")
}
return nil
}
func (s *store) DeleteIdentity(ctx context.Context, identityID valuer.UUID) error {
_, err := s.
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(&identitytypes.StorableIdentity{}).
Where("id = ?", identityID).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete identity")
}
return nil
}
func (s *store) GetRolesByIdentityID(ctx context.Context, identityID valuer.UUID) ([]string, error) {
var roleNames []string
err := s.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model((*identitytypes.StorableIdentityRole)(nil)).
Column("role_name").
Where("identity_id = ?", identityID).
Scan(ctx, &roleNames)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get roles for identity %s", identityID)
}
return roleNames, nil
}
func (s *store) GetRolesForIdentityIDs(ctx context.Context, identityIDs []valuer.UUID) (map[string][]string, error) {
if len(identityIDs) == 0 {
return map[string][]string{}, nil
}
var results []identitytypes.StorableIdentityRole
err := s.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&results).
Column("identity_id", "role_name").
Where("identity_id IN (?)", bun.In(identityIDs)).
Scan(ctx)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get roles for identities")
}
roleMap := make(map[string][]string)
for _, r := range results {
roleMap[r.IdentityID.StringValue()] = append(roleMap[r.IdentityID.StringValue()], r.RoleName)
}
return roleMap, nil
}
func (s *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error {
return s.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
return cb(ctx)
})
}
func (s *store) CountByRoleNameAndOrgID(ctx context.Context, roleName string, orgID valuer.UUID) (int64, error) {
count, err := s.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model((*identitytypes.StorableIdentityRole)(nil)).
Join("JOIN identity ON identity.id = identity_role.identity_id").
Where("identity_role.role_name = ?", roleName).
Where("identity.org_id = ?", orgID).
Count(ctx)
if err != nil {
return 0, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to count identities by role")
}
return int64(count), nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/identity"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
@@ -28,9 +29,10 @@ type module struct {
authDomain authdomain.Module
tokenizer tokenizer.Tokenizer
orgGetter organization.Getter
identity identity.Module
}
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, user user.Module, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter) session.Module {
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, user user.Module, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter, identity identity.Module) session.Module {
return &module{
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
authNs: authNs,
@@ -39,6 +41,7 @@ func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.A
authDomain: authDomain,
tokenizer: tokenizer,
orgGetter: orgGetter,
identity: identity,
}
}
@@ -141,21 +144,33 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
roleMapping := authDomain.AuthDomainConfig().RoleMapping
role := roleMapping.NewRoleFromCallbackIdentity(callbackIdentity)
user, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, role, callbackIdentity.OrgID)
newUser, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, callbackIdentity.OrgID)
if err != nil {
return "", err
}
user, err = module.user.GetOrCreateUser(ctx, user)
createdUser, err := module.user.GetOrCreateUser(ctx, newUser, user.WithRole(role))
if err != nil {
return "", err
}
if err := user.ErrIfRoot(); err != nil {
if err := createdUser.ErrIfRoot(); err != nil {
return "", errors.WithAdditionalf(err, "root user can only authenticate via password")
}
token, err := module.tokenizer.CreateToken(ctx, authtypes.NewIdentity(user.ID, user.OrgID, user.Email, user.Role), map[string]string{})
// Get roles from identity module
userRoles, err := module.identity.GetRoles(ctx, createdUser.IdentityID)
if err != nil {
return "", err
}
// Use first role for token creation (backward compatibility with current token system)
var primaryRole types.Role
if len(userRoles) > 0 {
primaryRole = userRoles[0]
}
token, err := module.tokenizer.CreateToken(ctx, authtypes.NewIdentity(createdUser.ID, createdUser.OrgID, createdUser.Email, primaryRole), map[string]string{})
if err != nil {
return "", err
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/identity"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
root "github.com/SigNoz/signoz/pkg/modules/user"
@@ -33,10 +34,11 @@ type Module struct {
authz authz.AuthZ
analytics analytics.Analytics
config user.Config
identity identity.Module
}
// This module is a WIP, don't take inspiration from this.
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, authz authz.AuthZ, analytics analytics.Analytics, config user.Config) root.Module {
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, authz authz.AuthZ, analytics analytics.Analytics, config user.Config, identity identity.Module) root.Module {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
return &Module{
store: store,
@@ -47,6 +49,7 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
analytics: analytics,
authz: authz,
config: config,
identity: identity,
}
}
@@ -56,7 +59,7 @@ func (m *Module) AcceptInvite(ctx context.Context, token string, password string
return nil, err
}
user, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.OrgID)
user, err := types.NewUser(invite.Name, invite.Email, invite.OrgID)
if err != nil {
return nil, err
}
@@ -66,7 +69,7 @@ func (m *Module) AcceptInvite(ctx context.Context, token string, password string
return nil, err
}
err = m.CreateUser(ctx, user, root.WithFactorPassword(factorPassword))
err = m.CreateUser(ctx, user, root.WithFactorPassword(factorPassword), root.WithRole(invite.Role))
if err != nil {
return nil, err
}
@@ -172,14 +175,21 @@ func (m *Module) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID)
func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
createUserOpts := root.NewCreateUserOptions(opts...)
role := createUserOpts.Role
// since assign is idempotant multiple calls to assign won't cause issues in case of retries.
err := module.authz.Grant(ctx, input.OrgID, roletypes.MustGetSigNozManagedRoleFromExistingRole(input.Role), authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil))
err := module.authz.Grant(ctx, input.OrgID, roletypes.MustGetSigNozManagedRoleFromExistingRole(role), authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil))
if err != nil {
return err
}
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
// Create identity with roles via identity module
if err := module.identity.CreateIdentityWithRoles(ctx, input.ID, input.OrgID, []types.Role{role}); err != nil {
return err
}
// Create user
if err := module.store.CreateUser(ctx, input); err != nil {
return err
}
@@ -195,7 +205,7 @@ func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ..
return err
}
traitsOrProperties := types.NewTraitsFromUser(input)
traitsOrProperties := types.NewTraitsFromUser(input, []types.Role{role})
module.analytics.IdentifyUser(ctx, input.OrgID.String(), input.ID.String(), traitsOrProperties)
module.analytics.TrackUser(ctx, input.OrgID.String(), input.ID.String(), "User Created", traitsOrProperties)
@@ -212,40 +222,7 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
return nil, errors.WithAdditionalf(err, "cannot update root user")
}
requestor, err := m.store.GetUser(ctx, valuer.MustNewUUID(updatedBy))
if err != nil {
return nil, err
}
if user.Role != "" && user.Role != existingUser.Role && requestor.Role != types.RoleAdmin {
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "only admins can change roles")
}
// Make sure that the request is not demoting the last admin user.
if user.Role != "" && user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin {
adminUsers, err := m.store.GetUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
if err != nil {
return nil, err
}
if len(adminUsers) == 1 {
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot demote the last admin")
}
}
if user.Role != "" && user.Role != existingUser.Role {
err = m.authz.ModifyGrant(ctx,
orgID,
roletypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role),
roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role),
authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil),
)
if err != nil {
return nil, err
}
}
existingUser.Update(user.DisplayName, user.Role)
existingUser.Update(user.DisplayName)
if err := m.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
return nil, err
}
@@ -258,7 +235,9 @@ func (module *Module) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user
return err
}
traits := types.NewTraitsFromUser(user)
// Get roles from identity module for analytics
roles, _ := module.identity.GetRoles(ctx, user.IdentityID)
traits := types.NewTraitsFromUser(user, roles)
module.analytics.IdentifyUser(ctx, user.OrgID.String(), user.ID.String(), traits)
module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Updated", traits)
@@ -283,19 +262,37 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "integration user cannot be deleted")
}
// don't allow to delete the last admin user
adminUsers, err := module.store.GetUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
// Get user's roles from identity module
userRoles, err := module.identity.GetRoles(ctx, user.IdentityID)
if err != nil {
return err
}
if len(adminUsers) == 1 && user.Role == types.RoleAdmin {
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin")
// Check if user has admin role
hasAdminRole := slices.Contains(userRoles, types.RoleAdmin)
// don't allow to delete the last admin user
if hasAdminRole {
adminCount, err := module.identity.CountByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
if err != nil {
return err
}
if adminCount == 1 {
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin")
}
}
// since revoke is idempotant multiple calls to revoke won't cause issues in case of retries
err = module.authz.Revoke(ctx, orgID, roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role), authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
if err != nil {
// Revoke all roles for the user
for _, userRole := range userRoles {
err = module.authz.Revoke(ctx, orgID, roletypes.MustGetSigNozManagedRoleFromExistingRole(userRole), authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
if err != nil {
return err
}
}
// Delete identity and identity_role records
if err := module.identity.DeleteIdentity(ctx, user.IdentityID); err != nil {
return err
}
@@ -545,7 +542,7 @@ func (module *Module) CreateFirstUser(ctx context.Context, organization *types.O
return err
}
err = module.createUserWithoutGrant(ctx, user, root.WithFactorPassword(password))
err = module.createUserWithoutGrant(ctx, user, root.WithFactorPassword(password), root.WithRole(types.RoleAdmin))
if err != nil {
return err
}
@@ -575,7 +572,15 @@ func (module *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
createUserOpts := root.NewCreateUserOptions(opts...)
role := createUserOpts.Role
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
// Create identity with roles via identity module
if err := module.identity.CreateIdentityWithRoles(ctx, input.ID, input.OrgID, []types.Role{role}); err != nil {
return err
}
// Create user
if err := module.store.CreateUser(ctx, input); err != nil {
return err
}
@@ -591,7 +596,7 @@ func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.U
return err
}
traitsOrProperties := types.NewTraitsFromUser(input)
traitsOrProperties := types.NewTraitsFromUser(input, []types.Role{role})
module.analytics.IdentifyUser(ctx, input.OrgID.String(), input.ID.String(), traitsOrProperties)
module.analytics.TrackUser(ctx, input.OrgID.String(), input.ID.String(), "User Created", traitsOrProperties)

View File

@@ -2,11 +2,13 @@ package impluser
import (
"context"
"slices"
"time"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/identity"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types"
@@ -21,6 +23,7 @@ type service struct {
module user.Module
orgGetter organization.Getter
authz authz.AuthZ
identity identity.Module
config user.RootConfig
stopC chan struct{}
}
@@ -31,6 +34,7 @@ func NewService(
module user.Module,
orgGetter organization.Getter,
authz authz.AuthZ,
identity identity.Module,
config user.RootConfig,
) user.Service {
return &service{
@@ -39,6 +43,7 @@ func NewService(
module: module,
orgGetter: orgGetter,
authz: authz,
identity: identity,
config: config,
stopC: make(chan struct{}),
}
@@ -112,20 +117,39 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID
}
if existingUser != nil {
oldRole := existingUser.Role
// Get user's current roles from identity module
userRoles, err := s.identity.GetRoles(ctx, existingUser.IdentityID)
if err != nil {
return err
}
existingUser.PromoteToRoot()
if err := s.module.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
return err
}
if oldRole != types.RoleAdmin {
if err := s.authz.ModifyGrant(ctx,
orgID,
roletypes.MustGetSigNozManagedRoleFromExistingRole(oldRole),
roletypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin),
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil),
); err != nil {
// If user doesn't have admin role, modify the grant and update identity roles
if !slices.Contains(userRoles, types.RoleAdmin) {
// Update authz grants and identity roles
for _, oldRole := range userRoles {
// Update authz grant
if err := s.authz.ModifyGrant(ctx,
orgID,
roletypes.MustGetSigNozManagedRoleFromExistingRole(oldRole),
roletypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin),
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil),
); err != nil {
return err
}
// Update identity role
if err := s.identity.RemoveRole(ctx, existingUser.IdentityID, orgID, oldRole); err != nil {
return err
}
}
// Add admin role to identity
if err := s.identity.AddRole(ctx, existingUser.IdentityID, orgID, types.RoleAdmin); err != nil {
return err
}
}
@@ -144,7 +168,7 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID
return err
}
return s.module.CreateUser(ctx, newUser, user.WithFactorPassword(factorPassword))
return s.module.CreateUser(ctx, newUser, user.WithFactorPassword(factorPassword), user.WithRole(types.RoleAdmin))
}
func (s *service) updateExistingRootUser(ctx context.Context, orgID valuer.UUID, existingRoot *types.User) error {

View File

@@ -192,24 +192,6 @@ func (store *store) GetUserByEmailAndOrgID(ctx context.Context, email valuer.Ema
return user, nil
}
func (store *store) GetUsersByRoleAndOrgID(ctx context.Context, role types.Role, orgID valuer.UUID) ([]*types.User, error) {
var users []*types.User
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&users).
Where("org_id = ?", orgID).
Where("role = ?", role).
Scan(ctx)
if err != nil {
return nil, err
}
return users, nil
}
func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *types.User) error {
_, err := store.
sqlstore.
@@ -218,7 +200,6 @@ func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *typ
Model(user).
Column("display_name").
Column("email").
Column("role").
Column("is_root").
Column("updated_at").
Where("org_id = ?", orgID).
@@ -619,6 +600,7 @@ func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "root user for org %s not found", orgID)
}
return user, nil
}

View File

@@ -7,6 +7,7 @@ import (
type createUserOptions struct {
FactorPassword *types.FactorPassword
Role types.Role
}
type CreateUserOption func(*createUserOptions)
@@ -17,9 +18,16 @@ func WithFactorPassword(factorPassword *types.FactorPassword) CreateUserOption {
}
}
func WithRole(role types.Role) CreateUserOption {
return func(o *createUserOptions) {
o.Role = role
}
}
func NewCreateUserOptions(opts ...CreateUserOption) *createUserOptions {
o := &createUserOptions{
FactorPassword: nil,
Role: types.RoleViewer, // default role
}
for _, opt := range opts {

View File

@@ -13,6 +13,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/identity"
"github.com/SigNoz/signoz/pkg/modules/identity/implidentity"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/organization"
@@ -54,6 +56,7 @@ type Modules struct {
Preference preference.Module
User user.Module
UserGetter user.Getter
Identity identity.Module
SavedView savedview.Module
Apdex apdex.Module
Dashboard dashboard.Module
@@ -88,7 +91,8 @@ func NewModules(
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User)
identityModule := implidentity.NewModule(sqlstore)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User, identityModule)
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
@@ -101,11 +105,12 @@ func NewModules(
Dashboard: dashboard,
User: user,
UserGetter: userGetter,
Identity: identityModule,
QuickFilter: quickfilter,
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
RawDataExport: implrawdataexport.NewModule(querier),
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter, identityModule),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Services: implservices.NewModule(querier, telemetryStore),
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),

View File

@@ -23,6 +23,7 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/global/signozglobal"
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
"github.com/SigNoz/signoz/pkg/modules/identity"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/preference/implpreference"
@@ -169,6 +170,9 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddAnonymousPublicDashboardTransactionFactory(sqlstore),
sqlmigration.NewAddRootUserFactory(sqlstore, sqlschema),
sqlmigration.NewAddUserEmailOrgIDIndexFactory(sqlstore, sqlschema),
sqlmigration.NewAddIdentityFactory(sqlstore, sqlschema),
sqlmigration.NewUpdateUserIdentity(sqlstore, sqlschema),
sqlmigration.NewMigrateUserRolesFactory(sqlstore, sqlschema),
)
}
@@ -220,9 +224,9 @@ func NewSharderProviderFactories() factory.NamedMap[factory.ProviderFactory[shar
)
}
func NewStatsReporterProviderFactories(telemetryStore telemetrystore.TelemetryStore, collectors []statsreporter.StatsCollector, orgGetter organization.Getter, userGetter user.Getter, tokenizer tokenizer.Tokenizer, build version.Build, analyticsConfig analytics.Config) factory.NamedMap[factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config]] {
func NewStatsReporterProviderFactories(telemetryStore telemetrystore.TelemetryStore, collectors []statsreporter.StatsCollector, orgGetter organization.Getter, userGetter user.Getter, identityModule identity.Module, tokenizer tokenizer.Tokenizer, build version.Build, analyticsConfig analytics.Config) factory.NamedMap[factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config]] {
return factory.MustNewNamedMap(
analyticsstatsreporter.NewFactory(telemetryStore, collectors, orgGetter, userGetter, tokenizer, build, analyticsConfig),
analyticsstatsreporter.NewFactory(telemetryStore, collectors, orgGetter, userGetter, identityModule, tokenizer, build, analyticsConfig),
noopstatsreporter.NewFactory(),
)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfmanagertest"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/modules/identity/implidentity"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -75,10 +76,12 @@ func TestNewProviderFactories(t *testing.T) {
})
assert.NotPanics(t, func() {
userGetter := impluser.NewGetter(impluser.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual), instrumentationtest.New().ToProviderSettings()))
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual)), nil)
sqlStore := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual)
userGetter := impluser.NewGetter(impluser.NewStore(sqlStore, instrumentationtest.New().ToProviderSettings()))
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlStore), nil)
identityModule := implidentity.NewModule(sqlStore)
telemetryStore := telemetrystoretest.New(telemetrystore.Config{Provider: "clickhouse"}, sqlmock.QueryMatcherEqual)
NewStatsReporterProviderFactories(telemetryStore, []statsreporter.StatsCollector{}, orgGetter, userGetter, tokenizertest.NewMockTokenizer(t), version.Build{}, analytics.Config{Enabled: true})
NewStatsReporterProviderFactories(telemetryStore, []statsreporter.StatsCollector{}, orgGetter, userGetter, identityModule, tokenizertest.NewMockTokenizer(t), version.Build{}, analytics.Config{Enabled: true})
})
assert.NotPanics(t, func() {

View File

@@ -389,7 +389,7 @@ func New(
// Initialize all modules
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard)
userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root)
userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, modules.Identity, config.User.Root)
// Initialize all handlers for the modules
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway, telemetryMetadataStore, authz)
@@ -424,7 +424,7 @@ func New(
ctx,
providerSettings,
config.StatsReporter,
NewStatsReporterProviderFactories(telemetrystore, statsCollectors, orgGetter, userGetter, tokenizer, version.Info, config.Analytics),
NewStatsReporterProviderFactories(telemetrystore, statsCollectors, orgGetter, userGetter, modules.Identity, tokenizer, version.Info, config.Analytics),
config.StatsReporter.Provider(),
)
if err != nil {

View File

@@ -0,0 +1,111 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addIdentity struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddIdentityFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_identity"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return &addIdentity{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
})
}
func (migration *addIdentity) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addIdentity) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
sqls := [][]byte{}
tableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "identity",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "status", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"id"},
},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("org_id"),
ReferencedTableName: sqlschema.TableName("organizations"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
sqls = append(sqls, tableSQLs...)
tableSQLs = migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "identity_role",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "identity_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "role_name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"id"},
},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("identity_id"),
ReferencedTableName: sqlschema.TableName("identity"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
sqls = append(sqls, tableSQLs...)
indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{
TableName: "identity_role",
ColumnNames: []sqlschema.ColumnName{"identity_id", "role_name"},
})
sqls = append(sqls, indexSQLs...)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *addIdentity) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -0,0 +1,132 @@
package sqlmigration
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type updateUserIdentity struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewUpdateUserIdentity(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("update_user_identity"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return &updateUserIdentity{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
})
}
func (migration *updateUserIdentity) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *updateUserIdentity) Up(ctx context.Context, db *bun.DB) error {
// 1. Disable FK enforcement
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, false); err != nil {
return err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
// 2. Fetch existing users
type existingUser struct {
bun.BaseModel `bun:"table:users"`
ID string `bun:"id"`
OrgID string `bun:"org_id"`
}
var users []*existingUser
if err := tx.NewSelect().Model(&users).Scan(ctx); err != nil {
return err
}
// 3. Create identity records for each user
now := time.Now()
for _, user := range users {
if _, err := tx.NewInsert().
Table("identity").
Value("id", "?", user.ID).
Value("status", "?", "active").
Value("org_id", "?", user.OrgID).
Value("created_at", "?", now).
Value("updated_at", "?", now).
Exec(ctx); err != nil {
return err
}
}
// 4. Get current table structure
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("users"))
if err != nil {
return err
}
// 5. Get existing indices to preserve them after recreation
indices, err := migration.sqlschema.GetIndices(ctx, sqlschema.TableName("users"))
if err != nil {
return err
}
// 6. Build all SQL statements
sqls := [][]byte{}
// Add identity_id column using AddColumn with ColumnName("id") as value
identityIdColumn := &sqlschema.Column{
Name: "identity_id",
DataType: sqlschema.DataTypeText,
Nullable: false,
}
sqls = append(sqls, migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, identityIdColumn, sqlschema.ColumnName("id"))...)
// Add FK constraint to table definition
table.ForeignKeyConstraints = append(table.ForeignKeyConstraints, &sqlschema.ForeignKeyConstraint{
ReferencingColumnName: sqlschema.ColumnName("identity_id"),
ReferencedTableName: sqlschema.TableName("identity"),
ReferencedColumnName: sqlschema.ColumnName("id"),
})
// Recreate table to apply FK constraint
sqls = append(sqls, migration.sqlschema.Operator().RecreateTable(table, uniqueConstraints)...)
// Recreate indices that were lost during table recreation
for _, index := range indices {
sqls = append(sqls, migration.sqlschema.Operator().CreateIndex(index)...)
}
// 7. Execute all SQL statements
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
// 8. Re-enable FK enforcement
return migration.sqlschema.ToggleFKEnforcement(ctx, db, true)
}
func (migration *updateUserIdentity) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -0,0 +1,112 @@
package sqlmigration
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
// Role name mapping from existing roles to managed role names
var existingRoleToManagedRole = map[string]string{
"ADMIN": "signoz-admin",
"EDITOR": "signoz-editor",
"VIEWER": "signoz-viewer",
}
type migrateUserRoles struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewMigrateUserRolesFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("migrate_user_roles"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return &migrateUserRoles{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
})
}
func (migration *migrateUserRoles) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *migrateUserRoles) Up(ctx context.Context, db *bun.DB) error {
// 1. Disable FK enforcement
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, false); err != nil {
return err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
// 2. Fetch existing users with their roles
type existingUser struct {
bun.BaseModel `bun:"table:users"`
ID string `bun:"id"`
Role string `bun:"role"`
}
var users []*existingUser
if err := tx.NewSelect().Model(&users).Scan(ctx); err != nil {
return err
}
// 3. Create identity_role records for each user using role_name directly
now := time.Now()
for _, user := range users {
roleName := existingRoleToManagedRole[user.Role]
if _, err := tx.NewInsert().
Table("identity_role").
Value("id", "?", valuer.GenerateUUID().StringValue()).
Value("identity_id", "?", user.ID).
Value("role_name", "?", roleName).
Value("created_at", "?", now).
Value("updated_at", "?", now).
Exec(ctx); err != nil {
return err
}
}
// 4. Get current table structure
table, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("users"))
if err != nil {
return err
}
// 5. Drop role column
roleColumn := &sqlschema.Column{Name: "role"}
dropColumnSQLs := migration.sqlschema.Operator().DropColumn(table, roleColumn)
for _, sql := range dropColumnSQLs {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
// 6. Re-enable FK enforcement
return migration.sqlschema.ToggleFKEnforcement(ctx, db, true)
}
func (migration *migrateUserRoles) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/analytics/segmentanalytics"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/identity"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/statsreporter"
@@ -39,6 +40,9 @@ type provider struct {
// used to get users
userGetter user.Getter
// used to get identity roles
identity identity.Module
// used to get tokenizer
tokenizer tokenizer.Tokenizer
@@ -55,9 +59,9 @@ type provider struct {
stopC chan struct{}
}
func NewFactory(telemetryStore telemetrystore.TelemetryStore, collectors []statsreporter.StatsCollector, orgGetter organization.Getter, userGetter user.Getter, tokenizer tokenizer.Tokenizer, build version.Build, analyticsConfig analytics.Config) factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config] {
func NewFactory(telemetryStore telemetrystore.TelemetryStore, collectors []statsreporter.StatsCollector, orgGetter organization.Getter, userGetter user.Getter, identity identity.Module, tokenizer tokenizer.Tokenizer, build version.Build, analyticsConfig analytics.Config) factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config] {
return factory.NewProviderFactory(factory.MustNewName("analytics"), func(ctx context.Context, settings factory.ProviderSettings, config statsreporter.Config) (statsreporter.StatsReporter, error) {
return New(ctx, settings, config, telemetryStore, collectors, orgGetter, userGetter, tokenizer, build, analyticsConfig)
return New(ctx, settings, config, telemetryStore, collectors, orgGetter, userGetter, identity, tokenizer, build, analyticsConfig)
})
}
@@ -69,6 +73,7 @@ func New(
collectors []statsreporter.StatsCollector,
orgGetter organization.Getter,
userGetter user.Getter,
identity identity.Module,
tokenizer tokenizer.Tokenizer,
build version.Build,
analyticsConfig analytics.Config,
@@ -87,6 +92,7 @@ func New(
collectors: collectors,
orgGetter: orgGetter,
userGetter: userGetter,
identity: identity,
analytics: analytics,
tokenizer: tokenizer,
build: build,
@@ -166,6 +172,17 @@ func (provider *provider) Report(ctx context.Context) error {
continue
}
// Get roles for all users in batch
identityIDs := make([]valuer.UUID, len(users))
for i, user := range users {
identityIDs[i] = user.IdentityID
}
rolesMap, err := provider.identity.GetRolesForIdentities(ctx, identityIDs)
if err != nil {
provider.settings.Logger().WarnContext(ctx, "failed to get roles", "error", err, "org_id", org.ID)
rolesMap = make(map[valuer.UUID][]types.Role)
}
maxLastObservedAtPerUserID, err := provider.tokenizer.ListMaxLastObservedAtByOrgID(ctx, org.ID)
if err != nil {
provider.settings.Logger().WarnContext(ctx, "failed to list max last observed at per user id", "error", err, "org_id", org.ID)
@@ -173,7 +190,8 @@ func (provider *provider) Report(ctx context.Context) error {
}
for _, user := range users {
traits := types.NewTraitsFromUser(user)
roles := rolesMap[user.IdentityID]
traits := types.NewTraitsFromUser(user, roles)
if maxLastObservedAt, ok := maxLastObservedAtPerUserID[user.ID]; ok {
traits["auth_token.last_observed_at.max.time"] = maxLastObservedAt.UTC()
traits["auth_token.last_observed_at.max.time_unix"] = maxLastObservedAt.Unix()

View File

@@ -0,0 +1,62 @@
package identitytypes
import (
"time"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
// IdentityStatus represents the status of an identity
type IdentityStatus string
const (
IdentityStatusActive IdentityStatus = "active"
IdentityStatusInactive IdentityStatus = "inactive"
)
// StorableIdentity represents the database entity for a user's identity
type StorableIdentity struct {
bun.BaseModel `bun:"table:identity"`
ID valuer.UUID `bun:"id,pk,type:text" json:"id"`
Status IdentityStatus `bun:"status" json:"status"`
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
CreatedAt time.Time `bun:"created_at" json:"createdAt"`
UpdatedAt time.Time `bun:"updated_at" json:"updatedAt"`
}
// StorableIdentityRole represents the relationship between identity and role in the database
type StorableIdentityRole struct {
bun.BaseModel `bun:"table:identity_role"`
ID valuer.UUID `bun:"id,pk,type:text" json:"id"`
IdentityID valuer.UUID `bun:"identity_id" json:"identityId"`
RoleName string `bun:"role_name" json:"roleName"`
CreatedAt time.Time `bun:"created_at" json:"createdAt"`
UpdatedAt time.Time `bun:"updated_at" json:"updatedAt"`
}
// NewStorableIdentity creates a new storable identity
func NewStorableIdentity(id valuer.UUID, orgID valuer.UUID) *StorableIdentity {
now := time.Now()
return &StorableIdentity{
ID: id,
Status: IdentityStatusActive,
OrgID: orgID,
CreatedAt: now,
UpdatedAt: now,
}
}
// NewStorableIdentityRole creates a new identity-role mapping
func NewStorableIdentityRole(identityID valuer.UUID, roleName string) *StorableIdentityRole {
now := time.Now()
return &StorableIdentityRole{
ID: valuer.GenerateUUID(),
IdentityID: identityID,
RoleName: roleName,
CreatedAt: now,
UpdatedAt: now,
}
}

View File

@@ -287,3 +287,17 @@ func MustGetSigNozManagedRoleFromExistingRole(role types.Role) string {
return managedRole
}
// GetRoleFromManagedRoleName converts a managed role name back to the legacy role type
func GetRoleFromManagedRoleName(managedRoleName string) types.Role {
switch managedRoleName {
case SigNozAdminRoleName:
return types.RoleAdmin
case SigNozEditorRoleName:
return types.RoleEditor
case SigNozViewerRoleName:
return types.RoleViewer
default:
return types.RoleViewer // default to viewer for unknown roles
}
}

View File

@@ -31,7 +31,7 @@ type User struct {
Identifiable
DisplayName string `bun:"display_name" json:"displayName"`
Email valuer.Email `bun:"email" json:"email"`
Role Role `bun:"role" json:"role"`
IdentityID valuer.UUID `bun:"identity_id" json:"identityId"`
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
IsRoot bool `bun:"is_root" json:"isRoot"`
TimeAuditable
@@ -45,26 +45,23 @@ type PostableRegisterOrgAndAdmin struct {
OrgName string `json:"orgName"`
}
func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUID) (*User, error) {
func NewUser(displayName string, email valuer.Email, orgID valuer.UUID) (*User, error) {
if email.IsZero() {
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
}
if role == "" {
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "role is required")
}
if orgID.IsZero() {
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgID is required")
}
id := valuer.GenerateUUID()
return &User{
Identifiable: Identifiable{
ID: valuer.GenerateUUID(),
ID: id,
},
DisplayName: displayName,
Email: email,
Role: role,
IdentityID: id, // identity_id = user.id (1:1 mapping)
OrgID: orgID,
IsRoot: false,
TimeAuditable: TimeAuditable{
@@ -83,13 +80,14 @@ func NewRootUser(displayName string, email valuer.Email, orgID valuer.UUID) (*Us
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgID is required")
}
id := valuer.GenerateUUID()
return &User{
Identifiable: Identifiable{
ID: valuer.GenerateUUID(),
ID: id,
},
DisplayName: displayName,
Email: email,
Role: RoleAdmin,
IdentityID: id, // identity_id = user.id (1:1 mapping)
OrgID: orgID,
IsRoot: true,
TimeAuditable: TimeAuditable{
@@ -100,21 +98,18 @@ func NewRootUser(displayName string, email valuer.Email, orgID valuer.UUID) (*Us
}
// Update applies mutable fields from the input to the user. Immutable fields
// (email, is_root, org_id, id) are preserved. Only non-zero input fields are applied.
func (u *User) Update(displayName string, role Role) {
// (email, is_root, org_id, id, identity_id) are preserved. Only non-zero input fields are applied.
func (u *User) Update(displayName string) {
if displayName != "" {
u.DisplayName = displayName
}
if role != "" {
u.Role = role
}
u.UpdatedAt = time.Now()
}
// PromoteToRoot promotes the user to a root user with admin role.
// PromoteToRoot promotes the user to a root user.
// Note: The actual role assignment is handled via identity module.
func (u *User) PromoteToRoot() {
u.IsRoot = true
u.Role = RoleAdmin
u.UpdatedAt = time.Now()
}
@@ -133,10 +128,10 @@ func (u *User) ErrIfRoot() error {
return nil
}
func NewTraitsFromUser(user *User) map[string]any {
func NewTraitsFromUser(user *User, roles []Role) map[string]any {
return map[string]any{
"name": user.DisplayName,
"role": user.Role,
"roles": roles,
"email": user.Email.String(),
"display_name": user.DisplayName,
"created_at": user.CreatedAt,
@@ -186,9 +181,6 @@ type UserStore interface {
// Get users by email.
GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*User, error)
// Get users by role and org.
GetUsersByRoleAndOrgID(ctx context.Context, role Role, orgID valuer.UUID) ([]*User, error)
// List users by org.
ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*User, error)