mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-18 15:02:35 +00:00
Compare commits
1 Commits
query-rang
...
platform-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba4e93050e |
@@ -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))
|
||||
}
|
||||
|
||||
31
pkg/modules/identity/identity.go
Normal file
31
pkg/modules/identity/identity.go
Normal 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
|
||||
}
|
||||
111
pkg/modules/identity/implidentity/module.go
Normal file
111
pkg/modules/identity/implidentity/module.go
Normal 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)
|
||||
}
|
||||
152
pkg/modules/identity/implidentity/store.go
Normal file
152
pkg/modules/identity/implidentity/store.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
111
pkg/sqlmigration/066_add_identity.go
Normal file
111
pkg/sqlmigration/066_add_identity.go
Normal 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
|
||||
}
|
||||
132
pkg/sqlmigration/067_update_user_identity.go
Normal file
132
pkg/sqlmigration/067_update_user_identity.go
Normal 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
|
||||
}
|
||||
112
pkg/sqlmigration/068_migrate_user_roles.go
Normal file
112
pkg/sqlmigration/068_migrate_user_roles.go
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
62
pkg/types/identitytypes/identity.go
Normal file
62
pkg/types/identitytypes/identity.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user