Compare commits

..

5 Commits

Author SHA1 Message Date
swapnil-signoz
871a4c74df refactor: normalising account_id if empty 2026-03-18 01:06:32 +05:30
swapnil-signoz
49bbaccbe2 Merge branch 'main' into fix/cloudintegration-index 2026-03-18 00:04:58 +05:30
Vikrant Gupta
5bafdeb373 fix(user): add config for user invite token expiry (#10618)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* fix(user): increase expiry for reset password token for invites

* fix(user): increase expiry for reset password token for invites

* fix(user): increase expiry for reset password token for invites

* fix(user): increase expiry for reset password token for invites
2026-03-17 16:57:29 +00:00
swapnil-signoz
1502338f17 refactor: removing std errors pkg 2026-03-17 20:22:38 +05:30
swapnil-signoz
fdf75f03ac fix: adding migration for fixing wrong cloud integration unique index 2026-03-17 17:12:44 +05:30
18 changed files with 389 additions and 186 deletions

View File

@@ -308,6 +308,9 @@ user:
allow_self: true
# The duration within which a user can reset their password.
max_token_lifetime: 6h
invite:
# The duration within which a user can accept their invite.
max_token_lifetime: 48h
root:
# Whether to enable the root user. When enabled, a root user is provisioned
# on startup using the email and password below. The root user cannot be

View File

@@ -186,7 +186,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint lists all users",
Request: nil,
RequestContentType: "",
Response: make([]*types.User, 0),
Response: make([]*types.GettableUser, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
@@ -203,7 +203,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint returns the user I belong to",
Request: nil,
RequestContentType: "",
Response: new(types.User),
Response: new(types.GettableUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
@@ -220,7 +220,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint returns the user by id",
Request: nil,
RequestContentType: "",
Response: new(types.User),
Response: new(types.GettableUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
@@ -237,7 +237,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint updates the user by id",
Request: new(types.User),
RequestContentType: "application/json",
Response: new(types.User),
Response: new(types.GettableUser),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},

View File

@@ -17,8 +17,8 @@ func NewStore(sqlstore sqlstore.SQLStore) authtypes.AuthNStore {
return &store{sqlstore: sqlstore}
}
func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.StorableUser, *types.FactorPassword, error) {
user := new(types.StorableUser)
func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, error) {
user := new(types.User)
factorPassword := new(types.FactorPassword)
err := store.

View File

@@ -30,7 +30,7 @@ func (module *module) Create(ctx context.Context, timestamp int64, name string,
funnel.CreatedBy = userID.String()
// Set up the user relationship
funnel.CreatedByUser = &types.StorableUser{
funnel.CreatedByUser = &types.User{
Identifiable: types.Identifiable{
ID: userID,
},

View File

@@ -27,7 +27,12 @@ type OrgConfig struct {
}
type PasswordConfig struct {
Reset ResetConfig `mapstructure:"reset"`
Invite InviteConfig `mapstructure:"invite"`
Reset ResetConfig `mapstructure:"reset"`
}
type InviteConfig struct {
MaxTokenLifetime time.Duration `mapstructure:"max_token_lifetime"`
}
type ResetConfig struct {
@@ -46,6 +51,9 @@ func newConfig() factory.Config {
AllowSelf: false,
MaxTokenLifetime: 6 * time.Hour,
},
Invite: InviteConfig{
MaxTokenLifetime: 48 * time.Hour,
},
},
Root: RootConfig{
Enabled: false,
@@ -61,6 +69,10 @@ func (c Config) Validate() error {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::password::reset::max_token_lifetime must be positive")
}
if c.Password.Invite.MaxTokenLifetime <= 0 {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::password::invite::max_token_lifetime must be positive")
}
if c.Root.Enabled {
if c.Root.Email.IsZero() {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::root::email is required when root user is enabled")

View File

@@ -21,15 +21,11 @@ func NewGetter(store types.UserStore, flagger flagger.Flagger) user.Getter {
}
func (module *getter) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, error) {
storableUser, err := module.store.GetRootUserByOrgID(ctx, orgID)
if err != nil {
return nil, err
}
return types.NewUserFromStorable(storableUser), nil
return module.store.GetRootUserByOrgID(ctx, orgID)
}
func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) {
storableUsers, err := module.store.ListUsersByOrgID(ctx, orgID)
users, err := module.store.ListUsersByOrgID(ctx, orgID)
if err != nil {
return nil, err
}
@@ -39,46 +35,46 @@ func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*ty
hideRootUsers := module.flagger.BooleanOrEmpty(ctx, flagger.FeatureHideRootUser, evalCtx)
if hideRootUsers {
storableUsers = slices.DeleteFunc(storableUsers, func(user *types.StorableUser) bool { return user.IsRoot })
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.IsRoot })
}
return types.NewUsersFromStorables(storableUsers), nil
return users, nil
}
func (module *getter) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.User, error) {
storableUsers, err := module.store.GetUsersByEmail(ctx, email)
users, err := module.store.GetUsersByEmail(ctx, email)
if err != nil {
return nil, err
}
return types.NewUsersFromStorables(storableUsers), nil
return users, nil
}
func (module *getter) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.User, error) {
storableUser, err := module.store.GetByOrgIDAndID(ctx, orgID, id)
user, err := module.store.GetByOrgIDAndID(ctx, orgID, id)
if err != nil {
return nil, err
}
return types.NewUserFromStorable(storableUser), nil
return user, nil
}
func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.User, error) {
storableUser, err := module.store.GetUser(ctx, id)
user, err := module.store.GetUser(ctx, id)
if err != nil {
return nil, err
}
return types.NewUserFromStorable(storableUser), nil
return user, nil
}
func (module *getter) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) {
storableUsers, err := module.store.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
users, err := module.store.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
if err != nil {
return nil, err
}
return types.NewUsersFromStorables(storableUsers), nil
return users, nil
}
func (module *getter) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) {

View File

@@ -51,7 +51,7 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
func (m *Module) AcceptInvite(ctx context.Context, token string, password string) (*types.User, error) {
// get the user by reset password token
storableUser, err := m.store.GetUserByResetPasswordToken(ctx, token)
user, err := m.store.GetUserByResetPasswordToken(ctx, token)
if err != nil {
return nil, err
}
@@ -63,23 +63,21 @@ func (m *Module) AcceptInvite(ctx context.Context, token string, password string
}
// query the user again
storableUser, err = m.store.GetByOrgIDAndID(ctx, storableUser.OrgID, storableUser.ID)
user, err = m.store.GetByOrgIDAndID(ctx, user.OrgID, user.ID)
if err != nil {
return nil, err
}
return types.NewUserFromStorable(storableUser), nil
return user, nil
}
func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.Invite, error) {
// get the user
storableUser, err := m.store.GetUserByResetPasswordToken(ctx, token)
user, err := m.store.GetUserByResetPasswordToken(ctx, token)
if err != nil {
return nil, err
}
user := types.NewUserFromStorable(storableUser)
// create a dummy invite obj for backward compatibility
invite := &types.Invite{
Identifiable: types.Identifiable{
@@ -111,11 +109,10 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
for idx, invite := range bulkInvites.Invites {
emails[idx] = invite.Email.StringValue()
}
storableUsers, err := m.store.GetUsersByEmailsOrgIDAndStatuses(ctx, orgID, emails, []string{types.UserStatusActive.StringValue(), types.UserStatusPendingInvite.StringValue()})
users, err := m.store.GetUsersByEmailsOrgIDAndStatuses(ctx, orgID, emails, []string{types.UserStatusActive.StringValue(), types.UserStatusPendingInvite.StringValue()})
if err != nil {
return nil, err
}
users := types.NewUsersFromStorables(storableUsers)
if len(users) > 0 {
if err := users[0].ErrIfRoot(); err != nil {
@@ -206,7 +203,7 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
resetLink := userWithToken.ResetPasswordToken.FactorPasswordResetLink(frontendBaseUrl)
tokenLifetime := m.config.Password.Reset.MaxTokenLifetime
tokenLifetime := m.config.Password.Invite.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
if err := m.emailing.SendHTML(ctx, userWithToken.User.Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{
@@ -223,13 +220,12 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
// find all the users with pending_invite status
storableUsers, err := m.store.ListUsersByOrgID(ctx, valuer.MustNewUUID(orgID))
users, err := m.store.ListUsersByOrgID(ctx, valuer.MustNewUUID(orgID))
if err != nil {
return nil, err
}
pendingStorableUsers := slices.DeleteFunc(storableUsers, func(user *types.StorableUser) bool { return user.Status != types.UserStatusPendingInvite })
pendingUsers := types.NewUsersFromStorables(pendingStorableUsers)
pendingUsers := slices.DeleteFunc(users, func(user *types.User) bool { return user.Status != types.UserStatusPendingInvite })
var invites []*types.Invite
@@ -272,7 +268,7 @@ func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ..
}
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
if err := module.store.CreateUser(ctx, types.NewStorableUser(input)); err != nil {
if err := module.store.CreateUser(ctx, input); err != nil {
return err
}
@@ -295,13 +291,11 @@ func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ..
}
func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.User, updatedBy string) (*types.User, error) {
existingStorableUser, err := m.store.GetUser(ctx, valuer.MustNewUUID(id))
existingUser, err := m.store.GetUser(ctx, valuer.MustNewUUID(id))
if err != nil {
return nil, err
}
existingUser := types.NewUserFromStorable(existingStorableUser)
if err := existingUser.ErrIfRoot(); err != nil {
return nil, errors.WithAdditionalf(err, "cannot update root user")
}
@@ -356,8 +350,7 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
}
func (module *Module) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error {
storableUser := types.NewStorableUser(user)
if err := module.store.UpdateUser(ctx, orgID, storableUser); err != nil {
if err := module.store.UpdateUser(ctx, orgID, user); err != nil {
return err
}
@@ -373,13 +366,11 @@ func (module *Module) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user
}
func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error {
storableUser, err := module.store.GetUser(ctx, valuer.MustNewUUID(id))
user, err := module.store.GetUser(ctx, valuer.MustNewUUID(id))
if err != nil {
return err
}
user := types.NewUserFromStorable(storableUser)
if err := user.ErrIfRoot(); err != nil {
return errors.WithAdditionalf(err, "cannot delete root user")
}
@@ -421,13 +412,11 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
}
func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID valuer.UUID) (*types.ResetPasswordToken, error) {
storableUser, err := module.store.GetUser(ctx, userID)
user, err := module.store.GetUser(ctx, userID)
if err != nil {
return nil, err
}
user := types.NewUserFromStorable(storableUser)
if err := user.ErrIfRoot(); err != nil {
return nil, errors.WithAdditionalf(err, "cannot reset password for root user")
}
@@ -471,7 +460,11 @@ func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID
}
// create a new token
resetPasswordToken, err := types.NewResetPasswordToken(password.ID, time.Now().Add(module.config.Password.Reset.MaxTokenLifetime))
tokenLifetime := module.config.Password.Reset.MaxTokenLifetime
if user.Status == types.UserStatusPendingInvite {
tokenLifetime = module.config.Password.Invite.MaxTokenLifetime
}
resetPasswordToken, err := types.NewResetPasswordToken(password.ID, time.Now().Add(tokenLifetime))
if err != nil {
return nil, err
}
@@ -511,6 +504,9 @@ func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, ema
resetLink := token.FactorPasswordResetLink(frontendBaseURL)
tokenLifetime := module.config.Password.Reset.MaxTokenLifetime
if user.Status == types.UserStatusPendingInvite {
tokenLifetime = module.config.Password.Invite.MaxTokenLifetime
}
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
if err := module.emailing.SendHTML(
@@ -545,13 +541,11 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
return err
}
storableUser, err := module.store.GetUser(ctx, valuer.MustNewUUID(password.UserID))
user, err := module.store.GetUser(ctx, valuer.MustNewUUID(password.UserID))
if err != nil {
return err
}
user := types.NewUserFromStorable(storableUser)
// handle deleted user
if err := user.ErrIfDeleted(); err != nil {
return errors.WithAdditionalf(err, "deleted users cannot reset their password")
@@ -582,7 +576,7 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
if err := user.UpdateStatus(types.UserStatusActive); err != nil {
return err
}
if err := module.store.UpdateUser(ctx, user.OrgID, types.NewStorableUser(user)); err != nil {
if err := module.store.UpdateUser(ctx, user.OrgID, user); err != nil {
return err
}
}
@@ -600,13 +594,11 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
}
func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, oldpasswd string, passwd string) error {
storableUser, err := module.store.GetUser(ctx, userID)
user, err := module.store.GetUser(ctx, userID)
if err != nil {
return err
}
user := types.NewUserFromStorable(storableUser)
if err := user.ErrIfDeleted(); err != nil {
return errors.WithAdditionalf(err, "cannot change password for deleted user")
}
@@ -758,13 +750,11 @@ func (module *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
// this function restricts that only one non-deleted user email can exist for an org ID, if found more, it throws an error
func (module *Module) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error) {
existingStorableUsers, err := module.store.GetUsersByEmailAndOrgID(ctx, email, orgID)
existingUsers, err := module.store.GetUsersByEmailAndOrgID(ctx, email, orgID)
if err != nil {
return nil, err
}
existingUsers := types.NewUsersFromStorables(existingStorableUsers)
// filter out the deleted users
existingUsers = slices.DeleteFunc(existingUsers, func(user *types.User) bool { return user.ErrIfDeleted() != nil })
@@ -783,7 +773,7 @@ func (module *Module) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, emai
func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
createUserOpts := root.NewCreateUserOptions(opts...)
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
if err := module.store.CreateUser(ctx, types.NewStorableUser(input)); err != nil {
if err := module.store.CreateUser(ctx, input); err != nil {
return err
}
@@ -819,7 +809,7 @@ func (module *Module) activatePendingUser(ctx context.Context, user *types.User)
if err := user.UpdateStatus(types.UserStatusActive); err != nil {
return err
}
err = module.store.UpdateUser(ctx, user.OrgID, types.NewStorableUser(user))
err = module.store.UpdateUser(ctx, user.OrgID, user)
if err != nil {
return err
}

View File

@@ -129,11 +129,10 @@ func (s *service) reconcileByName(ctx context.Context) error {
}
func (s *service) reconcileRootUser(ctx context.Context, orgID valuer.UUID) error {
existingStorableRoot, err := s.store.GetRootUserByOrgID(ctx, orgID)
existingRoot, err := s.store.GetRootUserByOrgID(ctx, orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
existingRoot := types.NewUserFromStorable(existingStorableRoot)
if existingRoot == nil {
return s.createOrPromoteRootUser(ctx, orgID)

View File

@@ -39,7 +39,7 @@ func (store *store) CreatePassword(ctx context.Context, password *types.FactorPa
return nil
}
func (store *store) CreateUser(ctx context.Context, user *types.StorableUser) error {
func (store *store) CreateUser(ctx context.Context, user *types.User) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
@@ -52,8 +52,8 @@ func (store *store) CreateUser(ctx context.Context, user *types.StorableUser) er
return nil
}
func (store *store) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.StorableUser, error) {
var users []*types.StorableUser
func (store *store) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.User, error) {
var users []*types.User
err := store.
sqlstore.
@@ -69,8 +69,8 @@ func (store *store) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]
return users, nil
}
func (store *store) GetUser(ctx context.Context, id valuer.UUID) (*types.StorableUser, error) {
user := new(types.StorableUser)
func (store *store) GetUser(ctx context.Context, id valuer.UUID) (*types.User, error) {
user := new(types.User)
err := store.
sqlstore.
@@ -86,8 +86,8 @@ func (store *store) GetUser(ctx context.Context, id valuer.UUID) (*types.Storabl
return user, nil
}
func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableUser, error) {
user := new(types.StorableUser)
func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.User, error) {
user := new(types.User)
err := store.
sqlstore.
@@ -104,8 +104,8 @@ func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id v
return user, nil
}
func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*types.StorableUser, error) {
var users []*types.StorableUser
func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*types.User, error) {
var users []*types.User
err := store.
sqlstore.
@@ -122,8 +122,8 @@ func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Em
return users, nil
}
func (store *store) GetActiveUsersByRoleAndOrgID(ctx context.Context, role types.Role, orgID valuer.UUID) ([]*types.StorableUser, error) {
var users []*types.StorableUser
func (store *store) GetActiveUsersByRoleAndOrgID(ctx context.Context, role types.Role, orgID valuer.UUID) ([]*types.User, error) {
var users []*types.User
err := store.
sqlstore.
@@ -141,7 +141,7 @@ func (store *store) GetActiveUsersByRoleAndOrgID(ctx context.Context, role types
return users, nil
}
func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *types.StorableUser) error {
func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *types.User) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
@@ -162,8 +162,8 @@ func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *typ
return nil
}
func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.StorableUser, error) {
users := []*types.StorableUser{}
func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.GettableUser, error) {
users := []*types.User{}
err := store.
sqlstore.
@@ -247,7 +247,7 @@ func (store *store) DeleteUser(ctx context.Context, orgID string, id string) err
// delete user
_, err = tx.NewDelete().
Model(new(types.StorableUser)).
Model(new(types.User)).
Where("org_id = ?", orgID).
Where("id = ?", id).
Exec(ctx)
@@ -332,7 +332,7 @@ func (store *store) SoftDeleteUser(ctx context.Context, orgID string, id string)
// soft delete user
now := time.Now()
_, err = tx.NewUpdate().
Model(new(types.StorableUser)).
Model(new(types.User)).
Set("status = ?", types.UserStatusDeleted).
Set("deleted_at = ?", now).
Set("updated_at = ?", now).
@@ -580,7 +580,7 @@ func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64,
}
func (store *store) CountByOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, statuses []string) (map[valuer.String]int64, error) {
user := new(types.StorableUser)
user := new(types.User)
var results []struct {
Status valuer.String `bun:"status"`
Count int64 `bun:"count"`
@@ -633,8 +633,8 @@ func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) er
})
}
func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.StorableUser, error) {
user := new(types.StorableUser)
func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, error) {
user := new(types.User)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -649,8 +649,8 @@ func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (
return user, nil
}
func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.StorableUser, error) {
users := []*types.StorableUser{}
func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) {
users := []*types.User{}
err := store.
sqlstore.
BunDB().
@@ -666,8 +666,8 @@ func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.
return users, nil
}
func (store *store) GetUserByResetPasswordToken(ctx context.Context, token string) (*types.StorableUser, error) {
user := new(types.StorableUser)
func (store *store) GetUserByResetPasswordToken(ctx context.Context, token string) (*types.User, error) {
user := new(types.User)
err := store.
sqlstore.
@@ -685,8 +685,8 @@ func (store *store) GetUserByResetPasswordToken(ctx context.Context, token strin
return user, nil
}
func (store *store) GetUsersByEmailsOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, emails []string, statuses []string) ([]*types.StorableUser, error) {
users := []*types.StorableUser{}
func (store *store) GetUsersByEmailsOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, emails []string, statuses []string) ([]*types.User, error) {
users := []*types.User{}
err := store.
sqlstore.

View File

@@ -175,6 +175,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewMigrateRulesV4ToV5Factory(sqlstore, telemetryStore),
sqlmigration.NewAddStatusUserFactory(sqlstore, sqlschema),
sqlmigration.NewDeprecateUserInviteFactory(sqlstore, sqlschema),
sqlmigration.NewFixCloudIntegrationUniqueIndexFactory(sqlstore, sqlschema),
)
}

View File

@@ -2,7 +2,6 @@ package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
@@ -17,12 +16,12 @@ type funnel struct {
types.Identifiable // funnel id
types.TimeAuditable
types.UserAuditable
Name string `json:"funnel_name" bun:"name,type:text,notnull"` // funnel name
Description string `json:"description" bun:"description,type:text"` // funnel description
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
Steps []funnelStep `json:"steps" bun:"steps,type:text,notnull"`
Tags string `json:"tags" bun:"tags,type:text"`
CreatedByUser *types.StorableUser `json:"user" bun:"rel:belongs-to,join:created_by=id"`
Name string `json:"funnel_name" bun:"name,type:text,notnull"` // funnel name
Description string `json:"description" bun:"description,type:text"` // funnel description
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
Steps []funnelStep `json:"steps" bun:"steps,type:text,notnull"`
Tags string `json:"tags" bun:"tags,type:text"`
CreatedByUser *types.User `json:"user" bun:"rel:belongs-to,join:created_by=id"`
}
type funnelStep struct {

View File

@@ -0,0 +1,264 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"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 fixCloudIntegrationUniqueIndex struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewFixCloudIntegrationUniqueIndexFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("fix_cloud_integration_index"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &fixCloudIntegrationUniqueIndex{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
},
)
}
func (migration *fixCloudIntegrationUniqueIndex) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
type cloudIntegrationRow struct {
bun.BaseModel `bun:"table:cloud_integration"`
ID string `bun:"id"`
AccountID string `bun:"account_id"`
Provider string `bun:"provider"`
OrgID string `bun:"org_id"`
Config string `bun:"config"`
UpdatedAt time.Time `bun:"updated_at"`
}
type cloudIntegrationAccountConfig struct {
Regions []string `json:"regions"`
}
// duplicateGroup holds the keeper (first element) and losers (rest) for a duplicate (account_id, provider, org_id) group.
type duplicateGroup struct {
keeper *cloudIntegrationRow
losers []*cloudIntegrationRow
}
func (migration *fixCloudIntegrationUniqueIndex) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
// Step 1: Drop the wrong index on (id, provider, org_id)
dropSqls := migration.sqlschema.Operator().DropIndex(
(&sqlschema.UniqueIndex{
TableName: "cloud_integration",
ColumnNames: []sqlschema.ColumnName{"id", "provider", "org_id"},
}).Named("unique_cloud_integration"),
)
for _, sql := range dropSqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
// Step 2: Normalize empty-string account_id to NULL
// Older table structure could store "" instead of NULL for unconnected accounts.
// Empty strings would violate the partial unique index since '' = '' (unlike NULL != NULL).
_, err = tx.NewUpdate().
TableExpr("cloud_integration").
Set("account_id = NULL").
Where("account_id = ''").
Exec(ctx)
if err != nil {
return err
}
// Step 3: Fetch all active rows with non-null account_id, ordered for grouping
var activeRows []*cloudIntegrationRow
err = tx.NewSelect().
Model(&activeRows).
Where("removed_at IS NULL").
Where("account_id IS NOT NULL").
OrderExpr("account_id, provider, org_id, updated_at DESC").
Scan(ctx)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
// Group by (account_id, provider, org_id)
groups := groupCloudIntegrationRows(activeRows)
now := time.Now()
var loserIDs []string
for _, group := range groups {
if len(group.losers) == 0 {
continue
}
// Step 4: Merge config from losers into keeper
if err = mergeCloudIntegrationConfigs(ctx, tx, group); err != nil {
return err
}
// Step 5: Reassign orphaned cloud_integration_service rows
for _, loser := range group.losers {
// Delete services from loser that would conflict with keeper's services (same type)
_, err = tx.NewDelete().
TableExpr("cloud_integration_service").
Where("cloud_integration_id = ?", loser.ID).
Where("type IN (?)",
tx.NewSelect().
TableExpr("cloud_integration_service").
Column("type").
Where("cloud_integration_id = ?", group.keeper.ID),
).
Exec(ctx)
if err != nil {
return err
}
// Reassign remaining services to the keeper
_, err = tx.ExecContext(ctx,
"UPDATE cloud_integration_service SET cloud_integration_id = ? WHERE cloud_integration_id = ?",
group.keeper.ID, loser.ID,
)
if err != nil {
return err
}
loserIDs = append(loserIDs, loser.ID)
}
}
// Step 6: Soft-delete all loser rows
if len(loserIDs) > 0 {
_, err = tx.NewUpdate().
TableExpr("cloud_integration").
Set("removed_at = ?", now).
Set("updated_at = ?", now).
Where("id IN (?)", bun.In(loserIDs)).
Exec(ctx)
if err != nil {
return err
}
}
// Step 7: Create the correct partial unique index on (account_id, provider, org_id) WHERE removed_at IS NULL
createSqls := migration.sqlschema.Operator().CreateIndex(
&sqlschema.PartialUniqueIndex{
TableName: "cloud_integration",
ColumnNames: []sqlschema.ColumnName{"account_id", "provider", "org_id"},
Where: "removed_at IS NULL",
},
)
for _, sql := range createSqls {
if _, err = tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
return tx.Commit()
}
func (migration *fixCloudIntegrationUniqueIndex) Down(ctx context.Context, db *bun.DB) error {
return nil
}
// groupCloudIntegrationRows groups rows by (account_id, provider, org_id).
// Rows must be pre-sorted by account_id, provider, org_id, updated_at DESC
// so the first row in each group is the keeper (most recently updated).
func groupCloudIntegrationRows(rows []*cloudIntegrationRow) []duplicateGroup {
if len(rows) == 0 {
return nil
}
var groups []duplicateGroup
var current duplicateGroup
current.keeper = rows[0]
for i := 1; i < len(rows); i++ {
row := rows[i]
if row.AccountID == current.keeper.AccountID &&
row.Provider == current.keeper.Provider &&
row.OrgID == current.keeper.OrgID {
current.losers = append(current.losers, row)
} else {
groups = append(groups, current)
current = duplicateGroup{keeper: row}
}
}
groups = append(groups, current)
return groups
}
// mergeCloudIntegrationConfigs unions the EnabledRegions from all rows in the group into the keeper's config and updates
func mergeCloudIntegrationConfigs(ctx context.Context, tx bun.Tx, group duplicateGroup) error {
regionSet := make(map[string]struct{})
// Parse keeper's config
parseRegions(group.keeper.Config, regionSet)
// Parse each loser's config
for _, loser := range group.losers {
parseRegions(loser.Config, regionSet)
}
// Build merged config
mergedRegions := make([]string, 0, len(regionSet))
for region := range regionSet {
mergedRegions = append(mergedRegions, region)
}
merged := cloudIntegrationAccountConfig{Regions: mergedRegions}
mergedJSON, err := json.Marshal(merged)
if err != nil {
return err
}
// Update keeper's config
_, err = tx.NewUpdate().
TableExpr("cloud_integration").
Set("config = ?", string(mergedJSON)).
Where("id = ?", group.keeper.ID).
Exec(ctx)
return err
}
// parseRegions unmarshals a config JSON string and adds its regions to the set.
func parseRegions(configJSON string, regionSet map[string]struct{}) {
if configJSON == "" {
return
}
var config cloudIntegrationAccountConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return
}
for _, region := range config.Regions {
regionSet[region] = struct{}{}
}
}

View File

@@ -34,7 +34,7 @@ func (store *store) Create(ctx context.Context, token *authtypes.StorableToken)
}
func (store *store) GetIdentityByUserID(ctx context.Context, userID valuer.UUID) (*authtypes.Identity, error) {
user := new(types.StorableUser)
user := new(types.User)
err := store.
sqlstore.

View File

@@ -128,7 +128,7 @@ func (typ *Identity) ToClaims() Claims {
type AuthNStore interface {
// Get user and factor password by email and orgID.
GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.StorableUser, *types.FactorPassword, error)
GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, error)
// Get org domain from id.
GetAuthDomainFromID(ctx context.Context, domainID valuer.UUID) (*AuthDomain, error)

View File

@@ -39,15 +39,15 @@ type OrgUserAPIKey struct {
}
type UserWithAPIKey struct {
*StorableUser `bun:",extend"`
APIKeys []*StorableAPIKeyUser `bun:"rel:has-many,join:id=user_id"`
*User `bun:",extend"`
APIKeys []*StorableAPIKeyUser `bun:"rel:has-many,join:id=user_id"`
}
type StorableAPIKeyUser struct {
StorableAPIKey `bun:",extend"`
CreatedByUser *StorableUser `json:"createdByUser" bun:"created_by_user,rel:belongs-to,join:created_by=id"`
UpdatedByUser *StorableUser `json:"updatedByUser" bun:"updated_by_user,rel:belongs-to,join:updated_by=id"`
CreatedByUser *User `json:"createdByUser" bun:"created_by_user,rel:belongs-to,join:created_by=id"`
UpdatedByUser *User `json:"updatedByUser" bun:"updated_by_user,rel:belongs-to,join:updated_by=id"`
}
type StorableAPIKey struct {
@@ -138,7 +138,7 @@ func NewGettableAPIKeyFromStorableAPIKey(storableAPIKey *StorableAPIKeyUser) *Ge
LastUsed: lastUsed,
Revoked: storableAPIKey.Revoked,
UserID: storableAPIKey.UserID.String(),
CreatedByUser: NewUserFromStorable(storableAPIKey.CreatedByUser),
UpdatedByUser: NewUserFromStorable(storableAPIKey.UpdatedByUser),
CreatedByUser: storableAPIKey.CreatedByUser,
UpdatedByUser: storableAPIKey.UpdatedByUser,
}
}

View File

@@ -18,12 +18,12 @@ type StorableFunnel struct {
types.TimeAuditable
types.UserAuditable
bun.BaseModel `bun:"table:trace_funnel"`
Name string `json:"funnel_name" bun:"name,type:text,notnull"`
Description string `json:"description" bun:"description,type:text"`
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
Steps []*FunnelStep `json:"steps" bun:"steps,type:text,notnull"`
Tags string `json:"tags" bun:"tags,type:text"`
CreatedByUser *types.StorableUser `json:"user" bun:"rel:belongs-to,join:created_by=id"`
Name string `json:"funnel_name" bun:"name,type:text,notnull"`
Description string `json:"description" bun:"description,type:text"`
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
Steps []*FunnelStep `json:"steps" bun:"steps,type:text,notnull"`
Tags string `json:"tags" bun:"tags,type:text"`
CreatedByUser *types.User `json:"user" bun:"rel:belongs-to,join:created_by=id"`
}
type FunnelStep struct {

View File

@@ -443,7 +443,7 @@ func TestConstructFunnelResponse(t *testing.T) {
},
Name: "test-funnel",
OrgID: orgID,
CreatedByUser: &types.StorableUser{
CreatedByUser: &types.User{
Identifiable: types.Identifiable{
ID: userID,
},

View File

@@ -33,25 +33,15 @@ var (
ValidUserStatus = []valuer.String{UserStatusPendingInvite, UserStatusActive, UserStatusDeleted}
)
type User struct {
Identifiable
DisplayName string `json:"displayName"`
Email valuer.Email `json:"email"`
Role Role `json:"role"` // this will be moved to roles
OrgID valuer.UUID `json:"orgId"`
IsRoot bool `json:"isRoot"`
Status valuer.String `json:"status"`
DeletedAt time.Time `json:"-"`
TimeAuditable
}
type GettableUser = User
type StorableUser struct {
type User struct {
bun.BaseModel `bun:"table:users"`
Identifiable
DisplayName string `bun:"display_name" json:"displayName"`
Email valuer.Email `bun:"email" json:"email"`
Role Role `bun:"role" json:"role"` // this will be removed as column from here
Role Role `bun:"role" json:"role"`
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
IsRoot bool `bun:"is_root" json:"isRoot"`
Status valuer.String `bun:"status" json:"status"`
@@ -67,57 +57,6 @@ type PostableRegisterOrgAndAdmin struct {
OrgName string `json:"orgName"`
}
func NewStorableUser(user *User) *StorableUser {
if user == nil {
return nil
}
return &StorableUser{
Identifiable: user.Identifiable,
DisplayName: user.DisplayName,
Email: user.Email,
Role: user.Role,
OrgID: user.OrgID,
IsRoot: user.IsRoot,
Status: user.Status,
DeletedAt: user.DeletedAt,
TimeAuditable: user.TimeAuditable,
}
}
func NewUserFromStorable(storableUser *StorableUser) *User {
if storableUser == nil {
return nil
}
return &User{
Identifiable: storableUser.Identifiable,
DisplayName: storableUser.DisplayName,
Email: storableUser.Email,
Role: storableUser.Role,
OrgID: storableUser.OrgID,
IsRoot: storableUser.IsRoot,
Status: storableUser.Status,
DeletedAt: storableUser.DeletedAt,
TimeAuditable: storableUser.TimeAuditable,
}
}
func NewUsersFromStorables(storableUsers []*StorableUser) []*User {
users := make([]*User, len(storableUsers))
for i, s := range storableUsers {
users[i] = NewUserFromStorable(s)
}
return users
}
func NewStorableUsers(users []*User) []*StorableUser {
storableUsers := make([]*StorableUser, len(users))
for i, u := range users {
storableUsers[i] = NewStorableUser(u)
}
return storableUsers
}
func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUID, status valuer.String) (*User, error) {
if email.IsZero() {
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
@@ -276,33 +215,33 @@ func (request *PostableRegisterOrgAndAdmin) UnmarshalJSON(data []byte) error {
type UserStore interface {
// Creates a user.
CreateUser(ctx context.Context, user *StorableUser) error
CreateUser(ctx context.Context, user *User) error
// Get user by id.
GetUser(context.Context, valuer.UUID) (*StorableUser, error)
GetUser(context.Context, valuer.UUID) (*User, error)
// Get user by orgID and id.
GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*StorableUser, error)
GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*User, error)
// Get user by email and orgID.
GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*StorableUser, error)
GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*User, error)
// Get users by email.
GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*StorableUser, error)
GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*User, error)
// Get users by role and org.
GetActiveUsersByRoleAndOrgID(ctx context.Context, role Role, orgID valuer.UUID) ([]*StorableUser, error)
GetActiveUsersByRoleAndOrgID(ctx context.Context, role Role, orgID valuer.UUID) ([]*User, error)
// List users by org.
ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*StorableUser, error)
ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*User, error)
// List users by email and org ids.
ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*StorableUser, error)
ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*User, error)
// Get users for an org id using emails and statuses
GetUsersByEmailsOrgIDAndStatuses(context.Context, valuer.UUID, []string, []string) ([]*StorableUser, error)
GetUsersByEmailsOrgIDAndStatuses(context.Context, valuer.UUID, []string, []string) ([]*User, error)
UpdateUser(ctx context.Context, orgID valuer.UUID, user *StorableUser) error
UpdateUser(ctx context.Context, orgID valuer.UUID, user *User) error
DeleteUser(ctx context.Context, orgID string, id string) error
SoftDeleteUser(ctx context.Context, orgID string, id string) error
@@ -328,10 +267,10 @@ type UserStore interface {
CountByOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, statuses []string) (map[valuer.String]int64, error)
// Get root user by org.
GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*StorableUser, error)
GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*User, error)
// Get user by reset password token
GetUserByResetPasswordToken(ctx context.Context, token string) (*StorableUser, error)
GetUserByResetPasswordToken(ctx context.Context, token string) (*User, error)
// Transaction
RunInTx(ctx context.Context, cb func(ctx context.Context) error) error