mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-10 23:42:08 +00:00
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: deprecate user invite table * fix: handle soft deleted users flow * fix: handle edge cases for authentication and reset password flow * feat: integration tests with fixes for new flow * fix: array for grants * fix: edge cases for reset token and context api * chore: remove all code related to old invite flow * fix: openapi specs * fix: integration tests and minor naming change * fix: integration tests fmtlint * feat: improve invitation email template * fix: role tests * fix: context api * fix: openapi frontend * chore: rename countbyorgid to activecountbyorgid * fix: a deleted user cannot recycled, creating a new one * feat: migrate existing invites to user as pending invite status * fix: error from GetUsersByEmailAndOrgID * feat: add backward compatibility to existing apis using new invite flow * chore: change ordering of apis in server * chore: change ordering of apis in server * fix: filter active users in role and org id check * fix: check deleted user in reset password flow * chore: address some review comments, add back countbyorgid method * chore: move to bulk inserts for migrating existing invites * fix: wrap funcs to transactions, and fix openapi specs * fix: move reset link method to types, also move authz grants outside transation * fix: transaction issues * feat: helper method ErrIfDeleted for user * fix: error code for errifdeleted in user * fix: soft delete store method * fix: password authn tests also add old invite flow test * fix: callbackauthn tests * fix: remove extra oidc tests * fix: callback authn tests oidc * chore: address review comments and optimise bulk invite api * fix: use db ctx in various places * fix: fix duplicate email invite issue and add partial invite * fix: openapi specs * fix: errifpending * fix: user status persistence * fix: edge cases * chore: add tests for partial index too * feat: use composite unique index on users table instead of partial one * chore: move duplicate email check to unmarshaljson and query user again in accept invite * fix: make 068 migratin idempotent * chore: remove unused emails var * chore: add a temp filter to show only active users in frontend until next frontend fix * chore: remove one check from register flow testing until temp code is removed * chore: remove commented code from tests * chore: address frontend review comments * chore: address frontend review comments
225 lines
6.0 KiB
Go
225 lines
6.0 KiB
Go
package impluser
|
|
|
|
import (
|
|
"context"
|
|
"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/organization"
|
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
|
"github.com/SigNoz/signoz/pkg/types"
|
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
"github.com/SigNoz/signoz/pkg/types/roletypes"
|
|
"github.com/SigNoz/signoz/pkg/valuer"
|
|
)
|
|
|
|
type service struct {
|
|
settings factory.ScopedProviderSettings
|
|
store types.UserStore
|
|
module user.Module
|
|
orgGetter organization.Getter
|
|
authz authz.AuthZ
|
|
config user.RootConfig
|
|
stopC chan struct{}
|
|
}
|
|
|
|
func NewService(
|
|
providerSettings factory.ProviderSettings,
|
|
store types.UserStore,
|
|
module user.Module,
|
|
orgGetter organization.Getter,
|
|
authz authz.AuthZ,
|
|
config user.RootConfig,
|
|
) user.Service {
|
|
return &service{
|
|
settings: factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/pkg/modules/user"),
|
|
store: store,
|
|
module: module,
|
|
orgGetter: orgGetter,
|
|
authz: authz,
|
|
config: config,
|
|
stopC: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (s *service) Start(ctx context.Context) error {
|
|
if !s.config.Enabled {
|
|
<-s.stopC
|
|
return nil
|
|
}
|
|
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
err := s.reconcile(ctx)
|
|
if err == nil {
|
|
s.settings.Logger().InfoContext(ctx, "root user reconciliation completed successfully")
|
|
<-s.stopC
|
|
return nil
|
|
}
|
|
|
|
s.settings.Logger().WarnContext(ctx, "root user reconciliation failed, retrying", "error", err)
|
|
|
|
select {
|
|
case <-s.stopC:
|
|
return nil
|
|
case <-ticker.C:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *service) Stop(ctx context.Context) error {
|
|
close(s.stopC)
|
|
return nil
|
|
}
|
|
|
|
func (s *service) reconcile(ctx context.Context) error {
|
|
if !s.config.Org.ID.IsZero() {
|
|
return s.reconcileWithOrgID(ctx)
|
|
}
|
|
|
|
return s.reconcileByName(ctx)
|
|
}
|
|
|
|
func (s *service) reconcileWithOrgID(ctx context.Context) error {
|
|
org, err := s.orgGetter.Get(ctx, s.config.Org.ID)
|
|
if err != nil {
|
|
if !errors.Ast(err, errors.TypeNotFound) {
|
|
return err // something really went wrong
|
|
}
|
|
|
|
// org was not found using id check if we can find an org using name
|
|
|
|
existingOrgByName, nameErr := s.orgGetter.GetByName(ctx, s.config.Org.Name)
|
|
if nameErr != nil && !errors.Ast(nameErr, errors.TypeNotFound) {
|
|
return nameErr // something really went wrong
|
|
}
|
|
|
|
// we found an org using name
|
|
if existingOrgByName != nil {
|
|
// the existing org has the same name as config but org id is different inform user with actionable message
|
|
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "organization with name %q already exists with a different ID %s (expected %s)", s.config.Org.Name, existingOrgByName.ID.StringValue(), s.config.Org.ID.StringValue())
|
|
}
|
|
|
|
// default - we did not found any org using id and name both - create a new org
|
|
newOrg := types.NewOrganizationWithID(s.config.Org.ID, s.config.Org.Name, s.config.Org.Name)
|
|
_, err = s.module.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password)
|
|
return err
|
|
}
|
|
|
|
return s.reconcileRootUser(ctx, org.ID)
|
|
}
|
|
|
|
func (s *service) reconcileByName(ctx context.Context) error {
|
|
org, err := s.orgGetter.GetByName(ctx, s.config.Org.Name)
|
|
if err != nil {
|
|
if errors.Ast(err, errors.TypeNotFound) {
|
|
newOrg := types.NewOrganization(s.config.Org.Name, s.config.Org.Name)
|
|
_, err := s.module.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password)
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return s.reconcileRootUser(ctx, org.ID)
|
|
}
|
|
|
|
func (s *service) reconcileRootUser(ctx context.Context, orgID valuer.UUID) error {
|
|
existingRoot, err := s.store.GetRootUserByOrgID(ctx, orgID)
|
|
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
|
return err
|
|
}
|
|
|
|
if existingRoot == nil {
|
|
return s.createOrPromoteRootUser(ctx, orgID)
|
|
}
|
|
|
|
return s.updateExistingRootUser(ctx, orgID, existingRoot)
|
|
}
|
|
|
|
func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID) error {
|
|
existingUser, err := s.module.GetNonDeletedUserByEmailAndOrgID(ctx, s.config.Email, orgID)
|
|
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
|
return err
|
|
}
|
|
|
|
if existingUser != nil {
|
|
oldRole := existingUser.Role
|
|
|
|
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,
|
|
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(oldRole)},
|
|
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin)},
|
|
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return s.setPassword(ctx, existingUser.ID)
|
|
}
|
|
|
|
// Create new root user
|
|
newUser, err := types.NewRootUser(s.config.Email.String(), s.config.Email, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
factorPassword, err := types.NewFactorPassword(s.config.Password, newUser.ID.StringValue())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.module.CreateUser(ctx, newUser, user.WithFactorPassword(factorPassword))
|
|
}
|
|
|
|
func (s *service) updateExistingRootUser(ctx context.Context, orgID valuer.UUID, existingRoot *types.User) error {
|
|
existingRoot.PromoteToRoot()
|
|
|
|
if existingRoot.Email != s.config.Email {
|
|
existingRoot.UpdateEmail(s.config.Email)
|
|
if err := s.module.UpdateAnyUser(ctx, orgID, existingRoot); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return s.setPassword(ctx, existingRoot.ID)
|
|
}
|
|
|
|
func (s *service) setPassword(ctx context.Context, userID valuer.UUID) error {
|
|
password, err := s.store.GetPasswordByUserID(ctx, userID)
|
|
if err != nil {
|
|
if !errors.Ast(err, errors.TypeNotFound) {
|
|
return err
|
|
}
|
|
|
|
factorPassword, err := types.NewFactorPassword(s.config.Password, userID.StringValue())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.store.CreatePassword(ctx, factorPassword)
|
|
}
|
|
|
|
if !password.Equals(s.config.Password) {
|
|
if err := password.Update(s.config.Password); err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.store.UpdatePassword(ctx, password)
|
|
}
|
|
|
|
return nil
|
|
}
|