diff --git a/cmd/enterprise/Dockerfile.integration b/cmd/enterprise/Dockerfile.integration index 337d28cb115e..7e3b19e9d136 100644 --- a/cmd/enterprise/Dockerfile.integration +++ b/cmd/enterprise/Dockerfile.integration @@ -2,6 +2,7 @@ FROM node:18-bullseye AS build WORKDIR /opt/ COPY ./frontend/ ./ +ENV NODE_OPTIONS=--max-old-space-size=8192 RUN CI=1 yarn install RUN CI=1 yarn build diff --git a/ee/query-service/app/api/cloudIntegrations.go b/ee/query-service/app/api/cloudIntegrations.go index f73e488f281f..101646e4ee27 100644 --- a/ee/query-service/app/api/cloudIntegrations.go +++ b/ee/query-service/app/api/cloudIntegrations.go @@ -13,11 +13,11 @@ import ( "github.com/SigNoz/signoz/ee/query-service/constants" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/modules/user" basemodel "github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/google/uuid" "github.com/gorilla/mux" "go.uber.org/zap" ) @@ -192,14 +192,14 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser( )) } - password, err := types.NewFactorPassword(uuid.NewString()) + password := types.MustGenerateFactorPassword(newUser.ID.StringValue()) - integrationUser, err := ah.Signoz.Modules.User.CreateUserWithPassword(ctx, newUser, password) + err = ah.Signoz.Modules.User.CreateUser(ctx, newUser, user.WithFactorPassword(password)) if err != nil { return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err)) } - return integrationUser, nil + return newUser, nil } func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) ( diff --git a/pkg/http/binding/binding.go b/pkg/http/binding/binding.go new file mode 100644 index 000000000000..bd5eb1851943 --- /dev/null +++ b/pkg/http/binding/binding.go @@ -0,0 +1,38 @@ +package binding + +import ( + "io" + + "github.com/SigNoz/signoz/pkg/errors" +) + +var ( + ErrCodeInvalidRequestBody = errors.MustNewCode("invalid_request_body") +) + +var ( + JSON Binding = &jsonBinding{} +) + +type bindBodyOptions struct { + DisallowUnknownFields bool + UseNumber bool +} + +type BindBodyOption func(*bindBodyOptions) + +func WithDisallowUnknownFields(disallowUnknownFields bool) BindBodyOption { + return func(options *bindBodyOptions) { + options.DisallowUnknownFields = disallowUnknownFields + } +} + +func WithUseNumber(useNumber bool) BindBodyOption { + return func(options *bindBodyOptions) { + options.UseNumber = useNumber + } +} + +type Binding interface { + BindBody(body io.Reader, obj any, opts ...BindBodyOption) error +} diff --git a/pkg/http/binding/json.go b/pkg/http/binding/json.go new file mode 100644 index 000000000000..8299456998fe --- /dev/null +++ b/pkg/http/binding/json.go @@ -0,0 +1,41 @@ +package binding + +import ( + "encoding/json" + "io" + + "github.com/SigNoz/signoz/pkg/errors" +) + +var _ Binding = (*jsonBinding)(nil) + +type jsonBinding struct{} + +func (b *jsonBinding) BindBody(body io.Reader, obj any, opts ...BindBodyOption) error { + bindBodyOptions := &bindBodyOptions{ + DisallowUnknownFields: false, + UseNumber: false, + } + + for _, opt := range opts { + opt(bindBodyOptions) + } + + decoder := json.NewDecoder(body) + + if bindBodyOptions.DisallowUnknownFields { + decoder.DisallowUnknownFields() + } + + if bindBodyOptions.UseNumber { + decoder.UseNumber() + } + + if err := decoder.Decode(obj); err != nil { + return errors. + New(errors.TypeInvalidInput, ErrCodeInvalidRequestBody, "request body is invalid"). + WithAdditional(err.Error()) + } + + return nil +} diff --git a/pkg/modules/organization/implorganization/store.go b/pkg/modules/organization/implorganization/store.go index 6acd247dc503..fb6e2c1e0f51 100644 --- a/pkg/modules/organization/implorganization/store.go +++ b/pkg/modules/organization/implorganization/store.go @@ -20,7 +20,7 @@ func NewStore(sqlstore sqlstore.SQLStore) types.OrganizationStore { func (store *store) Create(ctx context.Context, organization *types.Organization) error { _, err := store. sqlstore. - BunDB(). + BunDBCtx(ctx). NewInsert(). Model(organization). Exec(ctx) diff --git a/pkg/modules/quickfilter/implquickfilter/store.go b/pkg/modules/quickfilter/implquickfilter/store.go index 05f7e55cf10e..4a1e0d1d8b1d 100644 --- a/pkg/modules/quickfilter/implquickfilter/store.go +++ b/pkg/modules/quickfilter/implquickfilter/store.go @@ -14,12 +14,10 @@ type store struct { store sqlstore.SQLStore } -// NewStore creates a new SQLite store for quick filters func NewStore(db sqlstore.SQLStore) quickfiltertypes.QuickFilterStore { return &store{store: db} } -// GetQuickFilters retrieves all filters for an organization func (s *store) Get(ctx context.Context, orgID valuer.UUID) ([]*quickfiltertypes.StorableQuickFilter, error) { filters := make([]*quickfiltertypes.StorableQuickFilter, 0) @@ -38,7 +36,6 @@ func (s *store) Get(ctx context.Context, orgID valuer.UUID) ([]*quickfiltertypes return filters, nil } -// GetSignalFilters retrieves filters for a specific signal in an organization func (s *store) GetBySignal(ctx context.Context, orgID valuer.UUID, signal string) (*quickfiltertypes.StorableQuickFilter, error) { filter := new(quickfiltertypes.StorableQuickFilter) @@ -60,7 +57,6 @@ func (s *store) GetBySignal(ctx context.Context, orgID valuer.UUID, signal strin return filter, nil } -// UpsertQuickFilter inserts or updates filters for an organization and signal func (s *store) Upsert(ctx context.Context, filter *quickfiltertypes.StorableQuickFilter) error { _, err := s.store. BunDB(). @@ -78,9 +74,8 @@ func (s *store) Upsert(ctx context.Context, filter *quickfiltertypes.StorableQui } func (s *store) Create(ctx context.Context, filters []*quickfiltertypes.StorableQuickFilter) error { - // Using SQLite-specific conflict resolution _, err := s.store. - BunDB(). + BunDBCtx(ctx). NewInsert(). Model(&filters). On("CONFLICT (org_id, signal) DO NOTHING"). diff --git a/pkg/modules/user/impluser/handler.go b/pkg/modules/user/impluser/handler.go index c3d7751c81da..e3b995f66e03 100644 --- a/pkg/modules/user/impluser/handler.go +++ b/pkg/modules/user/impluser/handler.go @@ -8,8 +8,9 @@ import ( "time" "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/http/binding" "github.com/SigNoz/signoz/pkg/http/render" - "github.com/SigNoz/signoz/pkg/modules/user" + root "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/valuer" @@ -18,10 +19,10 @@ import ( ) type handler struct { - module user.Module + module root.Module } -func NewHandler(module user.Module) user.Handler { +func NewHandler(module root.Module) root.Handler { return &handler{module: module} } @@ -30,8 +31,8 @@ func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) { defer cancel() req := new(types.PostableAcceptInvite) - if err := json.NewDecoder(r.Body).Decode(req); err != nil { - render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode user")) + if err := binding.JSON.BindBody(r.Body, req); err != nil { + render.Error(w, err) return } @@ -79,13 +80,13 @@ func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) { } } else { - password, err := types.NewFactorPassword(req.Password) + password, err := types.NewFactorPassword(req.Password, user.ID.StringValue()) if err != nil { render.Error(w, err) return } - _, err = h.module.CreateUserWithPassword(ctx, user, password) + err = h.module.CreateUser(ctx, user, root.WithFactorPassword(password)) if err != nil { render.Error(w, err) return @@ -335,7 +336,7 @@ func (h *handler) LoginPrecheck(w http.ResponseWriter, r *http.Request) { render.Success(w, http.StatusOK, resp) } -func (h *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Request) { +func (handler *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() @@ -348,13 +349,13 @@ func (h *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Request) } // check if the id lies in the same org as the claims - _, err = h.module.GetUserByID(ctx, claims.OrgID, id) + user, err := handler.module.GetUserByID(ctx, claims.OrgID, id) if err != nil { render.Error(w, err) return } - token, err := h.module.CreateResetPasswordToken(ctx, id) + token, err := handler.module.GetOrCreateResetPasswordToken(ctx, user.ID) if err != nil { render.Error(w, err) return @@ -363,7 +364,7 @@ func (h *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Request) render.Success(w, http.StatusOK, token) } -func (h *handler) ResetPassword(w http.ResponseWriter, r *http.Request) { +func (handler *handler) ResetPassword(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() @@ -373,22 +374,16 @@ func (h *handler) ResetPassword(w http.ResponseWriter, r *http.Request) { return } - entry, err := h.module.GetResetPassword(ctx, req.Token) + err := handler.module.UpdatePasswordByResetPasswordToken(ctx, req.Token, req.Password) if err != nil { render.Error(w, err) return } - err = h.module.UpdatePasswordAndDeleteResetPasswordEntry(ctx, entry.PasswordID, req.Password) - if err != nil { - render.Error(w, err) - return - } - - render.Success(w, http.StatusOK, nil) + render.Success(w, http.StatusNoContent, nil) } -func (h *handler) ChangePassword(w http.ResponseWriter, r *http.Request) { +func (handler *handler) ChangePassword(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() @@ -398,25 +393,13 @@ func (h *handler) ChangePassword(w http.ResponseWriter, r *http.Request) { return } - // get the current password - password, err := h.module.GetPasswordByUserID(ctx, req.UserId) + err := handler.module.UpdatePassword(ctx, req.UserID, req.OldPassword, req.NewPassword) if err != nil { render.Error(w, err) return } - if !types.ComparePassword(password.Password, req.OldPassword) { - render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "old password is incorrect")) - return - } - - err = h.module.UpdatePassword(ctx, req.UserId, req.NewPassword) - if err != nil { - render.Error(w, err) - return - } - - render.Success(w, http.StatusOK, nil) + render.Success(w, http.StatusNoContent, nil) } func (h *handler) Login(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/modules/user/impluser/module.go b/pkg/modules/user/impluser/module.go index 3551ccf1b58d..dd7a782ebcbe 100644 --- a/pkg/modules/user/impluser/module.go +++ b/pkg/modules/user/impluser/module.go @@ -13,9 +13,8 @@ import ( "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" + root "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/query-service/constants" - "github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/emailtypes" @@ -35,7 +34,7 @@ type Module struct { } // This module is a WIP, don't take inspiration from this. -func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, analytics analytics.Analytics) user.Module { +func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, analytics analytics.Analytics) root.Module { settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser") return &Module{ store: store, @@ -132,27 +131,28 @@ func (m *Module) GetInviteByEmailInOrg(ctx context.Context, orgID string, email return m.store.GetInviteByEmailInOrg(ctx, orgID, email) } -func (m *Module) CreateUserWithPassword(ctx context.Context, user *types.User, password *types.FactorPassword) (*types.User, error) { - user, err := m.store.CreateUserWithPassword(ctx, user, password) - if err != nil { - return nil, err - } +func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error { + createUserOpts := root.NewCreateUserOptions(opts...) - traitsOrProperties := types.NewTraitsFromUser(user) - m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traitsOrProperties) - m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Created", traitsOrProperties) + if err := module.store.RunInTx(ctx, func(ctx context.Context) error { + if err := module.store.CreateUser(ctx, input); err != nil { + return err + } - return user, nil -} + if createUserOpts.FactorPassword != nil { + if err := module.store.CreatePassword(ctx, createUserOpts.FactorPassword); err != nil { + return err + } + } -func (m *Module) CreateUser(ctx context.Context, user *types.User) error { - if err := m.store.CreateUser(ctx, user); err != nil { + return nil + }); err != nil { return err } - traitsOrProperties := types.NewTraitsFromUser(user) - m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traitsOrProperties) - m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Created", traitsOrProperties) + traitsOrProperties := types.NewTraitsFromUser(input) + module.analytics.IdentifyUser(ctx, input.OrgID, input.ID.String(), traitsOrProperties) + module.analytics.TrackUser(ctx, input.OrgID, input.ID.String(), "User Created", traitsOrProperties) return nil } @@ -178,7 +178,6 @@ func (m *Module) ListUsers(ctx context.Context, orgID string) ([]*types.Gettable } func (m *Module) UpdateUser(ctx context.Context, orgID string, id string, user *types.User, updatedBy string) (*types.User, error) { - existingUser, err := m.GetUserByID(ctx, orgID, id) if err != nil { return nil, err @@ -202,7 +201,7 @@ func (m *Module) UpdateUser(ctx context.Context, orgID string, id string, user * 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. + // Make sure that th e request is not demoting the last admin user. // also an admin user can only change role of their own or other user if user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin.String() { adminUsers, err := m.GetUsersByRoleInOrg(ctx, orgID, types.RoleAdmin) @@ -274,81 +273,77 @@ func (m *Module) DeleteUser(ctx context.Context, orgID string, id string, delete return nil } -func (m *Module) CreateResetPasswordToken(ctx context.Context, userID string) (*types.ResetPasswordRequest, error) { - password, err := m.store.GetPasswordByUserID(ctx, userID) +func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID valuer.UUID) (*types.ResetPasswordToken, error) { + password, err := module.store.GetPasswordByUserID(ctx, userID) if err != nil { - // if the user does not have a password, we need to create a new one - // this will happen for SSO users - if errors.Ast(err, errors.TypeNotFound) { - password, err = m.store.CreatePassword(ctx, &types.FactorPassword{ - Identifiable: types.Identifiable{ - ID: valuer.GenerateUUID(), - }, - TimeAuditable: types.TimeAuditable{ - CreatedAt: time.Now(), - }, - Password: valuer.GenerateUUID().String(), - UserID: userID, - }) - if err != nil { - return nil, err - } - } else { + if !errors.Ast(err, errors.TypeNotFound) { return nil, err } } - resetPasswordRequest, err := types.NewResetPasswordRequest(password.ID.StringValue()) + if password == nil { + // if the user does not have a password, we need to create a new one (common for SSO/SAML users) + password = types.MustGenerateFactorPassword(userID.String()) + + if err := module.store.CreatePassword(ctx, password); err != nil { + return nil, err + } + } + + resetPasswordToken, err := types.NewResetPasswordToken(password.ID) if err != nil { return nil, err } - // check if a reset password token already exists for this user - existingRequest, err := m.store.GetResetPasswordByPasswordID(ctx, resetPasswordRequest.PasswordID) - if err != nil && !errors.Ast(err, errors.TypeNotFound) { - return nil, err - } - - if existingRequest != nil { - return existingRequest, nil - } - - err = m.store.CreateResetPasswordToken(ctx, resetPasswordRequest) + err = module.store.CreateResetPasswordToken(ctx, resetPasswordToken) if err != nil { - return nil, err + if !errors.Ast(err, errors.TypeAlreadyExists) { + return nil, err + } + + // if the token already exists, we return the existing token + resetPasswordToken, err = module.store.GetResetPasswordTokenByPasswordID(ctx, password.ID) + if err != nil { + return nil, err + } } - return resetPasswordRequest, nil + return resetPasswordToken, nil } -func (m *Module) GetPasswordByUserID(ctx context.Context, id string) (*types.FactorPassword, error) { - return m.store.GetPasswordByUserID(ctx, id) -} - -func (m *Module) GetResetPassword(ctx context.Context, token string) (*types.ResetPasswordRequest, error) { - return m.store.GetResetPassword(ctx, token) -} - -func (m *Module) UpdatePasswordAndDeleteResetPasswordEntry(ctx context.Context, passwordID string, password string) error { - hashedPassword, err := types.HashPassword(password) +func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, token string, passwd string) error { + resetPasswordToken, err := module.store.GetResetPasswordToken(ctx, token) if err != nil { return err } - existingPassword, err := m.store.GetPasswordByID(ctx, passwordID) + password, err := module.store.GetPassword(ctx, resetPasswordToken.PasswordID) if err != nil { return err } - return m.store.UpdatePasswordAndDeleteResetPasswordEntry(ctx, existingPassword.UserID, hashedPassword) + if err := password.Update(passwd); err != nil { + return err + } + + return module.store.UpdatePassword(ctx, password) } -func (m *Module) UpdatePassword(ctx context.Context, userID string, password string) error { - hashedPassword, err := types.HashPassword(password) +func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, oldpasswd string, passwd string) error { + password, err := module.store.GetPasswordByUserID(ctx, userID) if err != nil { return err } - return m.store.UpdatePassword(ctx, userID, hashedPassword) + + if !password.Equals(oldpasswd) { + return errors.New(errors.TypeInvalidInput, types.ErrCodeIncorrectPassword, "old password is incorrect") + } + + if err := password.Update(passwd); err != nil { + return err + } + + return module.store.UpdatePassword(ctx, password) } func (m *Module) GetAuthenticatedUser(ctx context.Context, orgID, email, password, refreshToken string) (*types.User, error) { @@ -381,13 +376,13 @@ func (m *Module) GetAuthenticatedUser(ctx context.Context, orgID, email, passwor return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "please provide an orgID") } - existingPassword, err := m.store.GetPasswordByUserID(ctx, dbUser.ID.StringValue()) + existingPassword, err := m.store.GetPasswordByUserID(ctx, dbUser.ID) if err != nil { return nil, err } - if !types.ComparePassword(existingPassword.Password, password) { - return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid password") + if !existingPassword.Equals(password) { + return nil, errors.New(errors.TypeInvalidInput, types.ErrCodeIncorrectPassword, "password is incorrect") } return dbUser, nil @@ -631,34 +626,31 @@ func (m *Module) UpdateDomain(ctx context.Context, domain *types.GettableOrgDoma return m.store.UpdateDomain(ctx, domain) } -func (m *Module) Register(ctx context.Context, req *types.PostableRegisterOrgAndAdmin) (*types.User, error) { - if req.Email == "" { - return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "email is required") - } - - if req.Password == "" { - return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "password is required") - } - - organization := types.NewOrganization(req.OrgDisplayName) - err := m.orgSetter.Create(ctx, organization) +func (module *Module) CreateFirstUser(ctx context.Context, organization *types.Organization, name string, email string, passwd string) (*types.User, error) { + user, err := types.NewUser(name, email, types.RoleAdmin.String(), organization.ID.StringValue()) if err != nil { - return nil, model.InternalError(err) + return nil, err } - user, err := types.NewUser(req.Name, req.Email, types.RoleAdmin.String(), organization.ID.StringValue()) + password, err := types.NewFactorPassword(passwd, user.ID.StringValue()) if err != nil { - return nil, model.InternalError(err) + return nil, err } - password, err := types.NewFactorPassword(req.Password) - if err != nil { - return nil, model.InternalError(err) - } + if err = module.store.RunInTx(ctx, func(ctx context.Context) error { + err := module.orgSetter.Create(ctx, organization) + if err != nil { + return err + } - user, err = m.CreateUserWithPassword(ctx, user, password) - if err != nil { - return nil, model.InternalError(err) + err = module.CreateUser(ctx, user, root.WithFactorPassword(password)) + if err != nil { + return err + } + + return nil + }); err != nil { + return nil, err } return user, nil diff --git a/pkg/modules/user/impluser/store.go b/pkg/modules/user/impluser/store.go index 70f0001940bf..02278787ebf1 100644 --- a/pkg/modules/user/impluser/store.go +++ b/pkg/modules/user/impluser/store.go @@ -104,55 +104,29 @@ func (store *store) ListInvite(ctx context.Context, orgID string) ([]*types.Invi return *invites, nil } -func (store *store) CreatePassword(ctx context.Context, password *types.FactorPassword) (*types.FactorPassword, error) { - _, err := store.sqlstore.BunDB().NewInsert(). +func (store *store) CreatePassword(ctx context.Context, password *types.FactorPassword) error { + _, err := store. + sqlstore. + BunDBCtx(ctx). + NewInsert(). Model(password). Exec(ctx) - if err != nil { - return nil, store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrPasswordAlreadyExists, "password with user id: %s already exists", password.UserID) + return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrPasswordAlreadyExists, "password for user %s already exists", password.UserID) } - return password, nil -} - -func (store *store) CreateUserWithPassword(ctx context.Context, user *types.User, password *types.FactorPassword) (*types.User, error) { - tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil) - if err != nil { - return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to start transaction") - } - - defer func() { - _ = tx.Rollback() - }() - - if _, err := tx.NewInsert(). - Model(user). - Exec(ctx); err != nil { - return nil, store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrUserAlreadyExists, "user with email: %s already exists in org: %s", user.Email, user.OrgID) - } - - password.UserID = user.ID.StringValue() - if _, err := tx.NewInsert(). - Model(password). - Exec(ctx); err != nil { - return nil, store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrPasswordAlreadyExists, "password with email: %s already exists in org: %s", user.Email, user.OrgID) - } - - err = tx.Commit() - if err != nil { - return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to commit transaction") - } - - return user, nil + return nil } func (store *store) CreateUser(ctx context.Context, user *types.User) error { - _, err := store.sqlstore.BunDB().NewInsert(). + _, err := store. + sqlstore. + BunDBCtx(ctx). + NewInsert(). Model(user). Exec(ctx) if err != nil { - return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrUserAlreadyExists, "user with email: %s already exists in org: %s", user.Email, user.OrgID) + return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrUserAlreadyExists, "user with email %s already exists in org %s", user.Email, user.OrgID) } return nil } @@ -329,7 +303,7 @@ func (store *store) DeleteUser(ctx context.Context, orgID string, id string) err // delete reset password request _, err = tx.NewDelete(). - Model(new(types.ResetPasswordRequest)). + Model(new(types.ResetPasswordToken)). Where("password_id = ?", password.ID.String()). Exec(ctx) if err != nil { @@ -372,128 +346,123 @@ func (store *store) DeleteUser(ctx context.Context, orgID string, id string) err return nil } -func (store *store) CreateResetPasswordToken(ctx context.Context, resetPasswordRequest *types.ResetPasswordRequest) error { - _, err := store.sqlstore.BunDB().NewInsert(). - Model(resetPasswordRequest). +func (store *store) CreateResetPasswordToken(ctx context.Context, resetPasswordToken *types.ResetPasswordToken) error { + _, err := store. + sqlstore. + BunDB(). + NewInsert(). + Model(resetPasswordToken). Exec(ctx) - if err != nil { - return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrResetPasswordTokenAlreadyExists, "reset password token with password id: %s already exists", resetPasswordRequest.PasswordID) + return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrResetPasswordTokenAlreadyExists, "reset password token for password %s already exists", resetPasswordToken.PasswordID) } + return nil } -func (store *store) GetPasswordByID(ctx context.Context, id string) (*types.FactorPassword, error) { +func (store *store) GetPassword(ctx context.Context, id valuer.UUID) (*types.FactorPassword, error) { password := new(types.FactorPassword) - err := store.sqlstore.BunDB().NewSelect(). + + err := store. + sqlstore. + BunDB(). + NewSelect(). Model(password). Where("id = ?", id). Scan(ctx) if err != nil { return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password with id: %s does not exist", id) } + return password, nil } -func (store *store) GetPasswordByUserID(ctx context.Context, id string) (*types.FactorPassword, error) { +func (store *store) GetPasswordByUserID(ctx context.Context, userID valuer.UUID) (*types.FactorPassword, error) { password := new(types.FactorPassword) - err := store.sqlstore.BunDB().NewSelect(). + + err := store. + sqlstore. + BunDB(). + NewSelect(). Model(password). - Where("user_id = ?", id). + Where("user_id = ?", userID). Scan(ctx) if err != nil { - return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password with user id: %s does not exist", id) + return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password for user %s does not exist", userID) } return password, nil } -func (store *store) GetResetPasswordByPasswordID(ctx context.Context, passwordID string) (*types.ResetPasswordRequest, error) { - resetPasswordRequest := new(types.ResetPasswordRequest) - err := store.sqlstore.BunDB().NewSelect(). - Model(resetPasswordRequest). +func (store *store) GetResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) (*types.ResetPasswordToken, error) { + resetPasswordToken := new(types.ResetPasswordToken) + + err := store. + sqlstore. + BunDB(). + NewSelect(). + Model(resetPasswordToken). Where("password_id = ?", passwordID). Scan(ctx) if err != nil { - return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrResetPasswordTokenNotFound, "reset password token with password id: %s does not exist", passwordID) + return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrResetPasswordTokenNotFound, "reset password token for password %s does not exist", passwordID) } - return resetPasswordRequest, nil + + return resetPasswordToken, nil } -func (store *store) GetResetPassword(ctx context.Context, token string) (*types.ResetPasswordRequest, error) { - resetPasswordRequest := new(types.ResetPasswordRequest) - err := store.sqlstore.BunDB().NewSelect(). +func (store *store) GetResetPasswordToken(ctx context.Context, token string) (*types.ResetPasswordToken, error) { + resetPasswordRequest := new(types.ResetPasswordToken) + + err := store. + sqlstore. + BunDB(). + NewSelect(). Model(resetPasswordRequest). Where("token = ?", token). Scan(ctx) if err != nil { - return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrResetPasswordTokenNotFound, "reset password token with token: %s does not exist", token) + return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrResetPasswordTokenNotFound, "reset password token does not exist", token) } + return resetPasswordRequest, nil } -func (store *store) UpdatePasswordAndDeleteResetPasswordEntry(ctx context.Context, userID string, password string) error { +func (store *store) UpdatePassword(ctx context.Context, factorPassword *types.FactorPassword) error { tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil) if err != nil { - return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to start transaction") + return err } defer func() { _ = tx.Rollback() }() - factorPassword := &types.FactorPassword{ - UserID: userID, - Password: password, - TimeAuditable: types.TimeAuditable{ - UpdatedAt: time.Now(), - }, - } - _, err = tx.NewUpdate(). + _, err = tx. + NewUpdate(). Model(factorPassword). - Column("password"). - Column("updated_at"). - Where("user_id = ?", userID). + Where("user_id = ?", factorPassword.UserID). Exec(ctx) if err != nil { - return store.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password with user id: %s does not exist", userID) + return store.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password for user %s does not exist", factorPassword.UserID) } - _, err = tx.NewDelete(). - Model(&types.ResetPasswordRequest{}). - Where("password_id = ?", userID). + _, err = tx. + NewDelete(). + Model(&types.ResetPasswordToken{}). + Where("password_id = ?", factorPassword.ID). Exec(ctx) if err != nil { - return store.sqlstore.WrapNotFoundErrf(err, types.ErrResetPasswordTokenNotFound, "reset password token with password id: %s does not exist", userID) + return err } err = tx.Commit() if err != nil { - return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to commit transaction") + return err } return nil } -func (store *store) UpdatePassword(ctx context.Context, userID string, password string) error { - factorPassword := &types.FactorPassword{ - UserID: userID, - Password: password, - TimeAuditable: types.TimeAuditable{ - UpdatedAt: time.Now(), - }, - } - _, err := store.sqlstore.BunDB().NewUpdate(). - Model(factorPassword). - Column("password"). - Column("updated_at"). - Where("user_id = ?", userID). - Exec(ctx) - if err != nil { - return store.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password with user id: %s does not exist", userID) - } - return nil -} - func (store *store) GetDomainByName(ctx context.Context, name string) (*types.StorableOrgDomain, error) { domain := new(types.StorableOrgDomain) err := store.sqlstore.BunDB().NewSelect(). @@ -844,3 +813,9 @@ func (store *store) CountAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) ( return int64(count), nil } + +func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error { + return store.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error { + return cb(ctx) + }) +} diff --git a/pkg/modules/user/option.go b/pkg/modules/user/option.go new file mode 100644 index 000000000000..ec26694177bb --- /dev/null +++ b/pkg/modules/user/option.go @@ -0,0 +1,27 @@ +package user + +import "github.com/SigNoz/signoz/pkg/types" + +type createUserOptions struct { + FactorPassword *types.FactorPassword +} + +type CreateUserOption func(*createUserOptions) + +func WithFactorPassword(factorPassword *types.FactorPassword) CreateUserOption { + return func(o *createUserOptions) { + o.FactorPassword = factorPassword + } +} + +func NewCreateUserOptions(opts ...CreateUserOption) *createUserOptions { + o := &createUserOptions{ + FactorPassword: nil, + } + + for _, opt := range opts { + opt(o) + } + + return o +} diff --git a/pkg/modules/user/user.go b/pkg/modules/user/user.go index 3e0a5efd8f26..cc7653068c64 100644 --- a/pkg/modules/user/user.go +++ b/pkg/modules/user/user.go @@ -12,16 +12,29 @@ import ( ) type Module interface { + // Creates the organization and the first user of that organization. + CreateFirstUser(ctx context.Context, organization *types.Organization, name string, email string, password string) (*types.User, error) + + // Creates a user and sends an analytics event. + CreateUser(ctx context.Context, user *types.User, opts ...CreateUserOption) error + + // Get or Create a reset password token for a user. If the password does not exist, a new one is randomly generated and inserted. The function + // is idempotent and can be called multiple times. + GetOrCreateResetPasswordToken(ctx context.Context, userID valuer.UUID) (*types.ResetPasswordToken, error) + + // Updates password of a user using a reset password token. It also deletes all reset password tokens for the user. + // This is used to reset the password of a user when they forget their password. + UpdatePasswordByResetPasswordToken(ctx context.Context, token string, password string) error + + // Updates password of user to the new password. It also deletes all reset password tokens for the user. + UpdatePassword(ctx context.Context, userID valuer.UUID, oldPassword string, password string) error + // invite CreateBulkInvite(ctx context.Context, orgID, userID string, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error GetInviteByToken(ctx context.Context, token string) (*types.GettableInvite, error) GetInviteByEmailInOrg(ctx context.Context, orgID string, email string) (*types.Invite, error) - - // user - CreateUserWithPassword(ctx context.Context, user *types.User, password *types.FactorPassword) (*types.User, error) - CreateUser(ctx context.Context, user *types.User) error GetUserByID(ctx context.Context, orgID string, id string) (*types.GettableUser, error) GetUsersByEmail(ctx context.Context, email string) ([]*types.GettableUser, error) // public function GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*types.GettableUser, error) @@ -40,13 +53,6 @@ type Module interface { PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (string, error) CanUsePassword(ctx context.Context, email string) (bool, error) - // password - CreateResetPasswordToken(ctx context.Context, userID string) (*types.ResetPasswordRequest, error) - GetPasswordByUserID(ctx context.Context, id string) (*types.FactorPassword, error) - GetResetPassword(ctx context.Context, token string) (*types.ResetPasswordRequest, error) - UpdatePassword(ctx context.Context, userID string, password string) error - UpdatePasswordAndDeleteResetPasswordEntry(ctx context.Context, passwordID string, password string) error - // Auth Domain GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error) GetDomainFromSsoResponse(ctx context.Context, url *url.URL) (*types.GettableOrgDomain, error) @@ -63,9 +69,6 @@ type Module interface { RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error GetAPIKey(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableAPIKeyUser, error) - // Register - Register(ctx context.Context, req *types.PostableRegisterOrgAndAdmin) (*types.User, error) - statsreporter.StatsCollector } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 69ea22b345b1..478a3aa62011 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -2065,7 +2065,8 @@ func (aH *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) { return } - user, errv2 := aH.Signoz.Modules.User.Register(r.Context(), &req) + organization := types.NewOrganization(req.OrgDisplayName) + user, errv2 := aH.Signoz.Modules.User.CreateFirstUser(r.Context(), organization, req.Name, req.Email, req.Password) if errv2 != nil { render.Error(w, errv2) return diff --git a/pkg/types/factor_password.go b/pkg/types/factor_password.go new file mode 100644 index 000000000000..4a5d45652d34 --- /dev/null +++ b/pkg/types/factor_password.go @@ -0,0 +1,210 @@ +package types + +import ( + "encoding/json" + "slices" + "time" + "unicode" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/sethvargo/go-password/password" + "github.com/uptrace/bun" + "golang.org/x/crypto/bcrypt" +) + +var ( + symbols []rune = []rune("~!@#$%^&*()_+`-={}|[]\\:\"<>?,./") + minPasswordLength int = 12 + ErrInvalidPassword = errors.Newf(errors.TypeInvalidInput, errors.MustNewCode("invalid_password"), "password must be at least %d characters long, should contain at least one uppercase letter [A-Z], one lowercase letter [a-z], one number [0-9], and one symbol [%c].", minPasswordLength, symbols) + ErrCodeResetPasswordTokenAlreadyExists = errors.MustNewCode("reset_password_token_already_exists") + ErrCodePasswordNotFound = errors.MustNewCode("password_not_found") + ErrCodeResetPasswordTokenNotFound = errors.MustNewCode("reset_password_token_not_found") + ErrCodePasswordAlreadyExists = errors.MustNewCode("password_already_exists") + ErrCodeIncorrectPassword = errors.MustNewCode("incorrect_password") +) + +type PostableResetPassword struct { + Password string `json:"password"` + Token string `json:"token"` +} + +type ChangePasswordRequest struct { + UserID valuer.UUID `json:"userId"` + OldPassword string `json:"oldPassword"` + NewPassword string `json:"newPassword"` +} + +type ResetPasswordToken struct { + bun.BaseModel `bun:"table:reset_password_token"` + + Identifiable + Token string `bun:"token,type:text,notnull" json:"token"` + PasswordID valuer.UUID `bun:"password_id,type:text,notnull,unique" json:"passwordId"` +} + +type FactorPassword struct { + bun.BaseModel `bun:"table:factor_password"` + + Identifiable + Password string `bun:"password,type:text,notnull" json:"password"` + Temporary bool `bun:"temporary,type:boolean,notnull" json:"temporary"` + UserID string `bun:"user_id,type:text,notnull,unique" json:"userId"` + TimeAuditable +} + +func (request *ChangePasswordRequest) UnmarshalJSON(data []byte) error { + type Alias ChangePasswordRequest + + var temp Alias + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + if !IsPasswordValid(temp.NewPassword) { + return ErrInvalidPassword + } + + *request = ChangePasswordRequest(temp) + return nil +} + +func (request *PostableResetPassword) UnmarshalJSON(data []byte) error { + type Alias PostableResetPassword + + var temp Alias + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + if !IsPasswordValid(temp.Password) { + return ErrInvalidPassword + } + + *request = PostableResetPassword(temp) + return nil +} + +func NewFactorPassword(password string, userID string) (*FactorPassword, error) { + if !IsPasswordValid(password) { + return nil, ErrInvalidPassword + } + + hashedPassword, err := NewHashedPassword(password) + if err != nil { + return nil, err + } + + return &FactorPassword{ + Identifiable: Identifiable{ + ID: valuer.GenerateUUID(), + }, + Password: string(hashedPassword), + Temporary: false, + UserID: userID, + TimeAuditable: TimeAuditable{ + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + }, nil +} + +func GenerateFactorPassword(userID string) (*FactorPassword, error) { + password, err := password.Generate(12, 1, 1, false, false) + if err != nil { + return nil, err + } + + return NewFactorPassword(password+"Z", userID) +} + +func MustGenerateFactorPassword(userID string) *FactorPassword { + password, err := GenerateFactorPassword(userID) + if err != nil { + panic(err) + } + + return password +} + +func NewHashedPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + + return string(hashedPassword), nil +} + +func NewResetPasswordToken(passwordID valuer.UUID) (*ResetPasswordToken, error) { + return &ResetPasswordToken{ + Identifiable: Identifiable{ + ID: valuer.GenerateUUID(), + }, + Token: valuer.GenerateUUID().String(), + PasswordID: passwordID, + }, nil +} + +func IsPasswordValid(password string) bool { + if len(password) < minPasswordLength { + return false + } + + hasUpperCase := false + hasLowerCase := false + hasNumber := false + hasSymbol := false + + for _, char := range password { + if !hasLowerCase && unicode.IsLower(char) { + hasLowerCase = true + } + + if !hasUpperCase && unicode.IsUpper(char) { + hasUpperCase = true + } + + if !hasNumber && unicode.IsNumber(char) { + hasNumber = true + } + + if !hasSymbol && slices.Contains(symbols, char) { + hasSymbol = true + } + + if !unicode.IsLetter(char) && !unicode.IsNumber(char) && !slices.Contains(symbols, char) { + return false + } + } + + if !hasUpperCase || !hasLowerCase || !hasNumber || !hasSymbol { + return false + } + + return true +} + +func (f *FactorPassword) Update(password string) error { + if !IsPasswordValid(password) { + return ErrInvalidPassword + } + + hashedPassword, err := NewHashedPassword(password) + if err != nil { + return err + } + + f.Password = hashedPassword + f.UpdatedAt = time.Now() + + return nil +} + +func (f *FactorPassword) Equals(password string) bool { + return comparePassword(f.Password, password) +} + +func comparePassword(hashedPassword string, password string) bool { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil +} diff --git a/pkg/types/factor_password_test.go b/pkg/types/factor_password_test.go new file mode 100644 index 000000000000..5aad7ae6498c --- /dev/null +++ b/pkg/types/factor_password_test.go @@ -0,0 +1,14 @@ +package types + +import ( + "testing" + + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/stretchr/testify/assert" +) + +func TestMustGenerateFactorPassword(t *testing.T) { + assert.NotPanics(t, func() { + MustGenerateFactorPassword(valuer.GenerateUUID().String()) + }) +} diff --git a/pkg/types/user.go b/pkg/types/user.go index 623f45321ebf..d42941f121f8 100644 --- a/pkg/types/user.go +++ b/pkg/types/user.go @@ -2,15 +2,14 @@ package types import ( "context" + "encoding/json" "net/url" - "strings" "time" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/valuer" "github.com/google/uuid" "github.com/uptrace/bun" - "golang.org/x/crypto/bcrypt" ) var ( @@ -24,59 +23,6 @@ var ( ErrAPIKeyNotFound = errors.MustNewCode("api_key_not_found") ) -type UserStore interface { - // invite - CreateBulkInvite(ctx context.Context, invites []*Invite) error - ListInvite(ctx context.Context, orgID string) ([]*Invite, error) - DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error - GetInviteByToken(ctx context.Context, token string) (*GettableInvite, error) - GetInviteByEmailInOrg(ctx context.Context, orgID string, email string) (*Invite, error) - - // user - CreateUserWithPassword(ctx context.Context, user *User, password *FactorPassword) (*User, error) - CreateUser(ctx context.Context, user *User) error - GetUserByID(ctx context.Context, orgID string, id string) (*GettableUser, error) - GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*GettableUser, error) - GetUsersByEmail(ctx context.Context, email string) ([]*GettableUser, error) - GetUsersByRoleInOrg(ctx context.Context, orgID string, role Role) ([]*GettableUser, error) - ListUsers(ctx context.Context, orgID string) ([]*GettableUser, error) - UpdateUser(ctx context.Context, orgID string, id string, user *User) (*User, error) - DeleteUser(ctx context.Context, orgID string, id string) error - - // password - CreatePassword(ctx context.Context, password *FactorPassword) (*FactorPassword, error) - CreateResetPasswordToken(ctx context.Context, resetPasswordRequest *ResetPasswordRequest) error - GetPasswordByID(ctx context.Context, id string) (*FactorPassword, error) - GetPasswordByUserID(ctx context.Context, id string) (*FactorPassword, error) - GetResetPassword(ctx context.Context, token string) (*ResetPasswordRequest, error) - GetResetPasswordByPasswordID(ctx context.Context, passwordID string) (*ResetPasswordRequest, error) - UpdatePassword(ctx context.Context, userID string, password string) error - UpdatePasswordAndDeleteResetPasswordEntry(ctx context.Context, userID string, password string) error - - // Auth Domain - GetDomainByName(ctx context.Context, name string) (*StorableOrgDomain, error) - // org domain (auth domains) CRUD ops - GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*GettableOrgDomain, error) - ListDomains(ctx context.Context, orgId valuer.UUID) ([]*GettableOrgDomain, error) - GetDomain(ctx context.Context, id uuid.UUID) (*GettableOrgDomain, error) - CreateDomain(ctx context.Context, d *GettableOrgDomain) error - UpdateDomain(ctx context.Context, domain *GettableOrgDomain) error - DeleteDomain(ctx context.Context, id uuid.UUID) error - - // Temporary func for SSO - GetDefaultOrgID(ctx context.Context) (string, error) - - // API KEY - CreateAPIKey(ctx context.Context, apiKey *StorableAPIKey) error - UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *StorableAPIKey, updaterID valuer.UUID) error - ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*StorableAPIKeyUser, error) - RevokeAPIKey(ctx context.Context, id valuer.UUID, revokedByUserID valuer.UUID) error - GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*StorableAPIKeyUser, error) - CountAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) - - CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) -} - type GettableUser struct { User Organization string `json:"organization"` @@ -121,12 +67,12 @@ func NewUser(displayName string, email string, role string, orgID string) (*User } type PostableRegisterOrgAndAdmin struct { - PostableAcceptInvite Name string `json:"name"` OrgID string `json:"orgId"` OrgDisplayName string `json:"orgDisplayName"` OrgName string `json:"orgName"` Email string `json:"email"` + Password string `json:"password"` } type PostableAcceptInvite struct { @@ -138,95 +84,6 @@ type PostableAcceptInvite struct { SourceURL string `json:"sourceUrl"` } -func (p *PostableAcceptInvite) Validate() error { - if p.InviteToken == "" { - return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invite token is required") - } - - if p.Password == "" || len(p.Password) < 8 { - return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "password must be at least 8 characters long") - } - - return nil -} - -type FactorPassword struct { - bun.BaseModel `bun:"table:factor_password"` - - Identifiable - TimeAuditable - Password string `bun:"password,type:text,notnull" json:"password"` - Temporary bool `bun:"temporary,type:boolean,notnull" json:"temporary"` - UserID string `bun:"user_id,type:text,notnull,unique,references:user(id)" json:"userId"` -} - -func NewFactorPassword(password string) (*FactorPassword, error) { - - if password == "" && len(password) < 8 { - return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "password must be at least 8 characters long") - } - - password = strings.TrimSpace(password) - - hashedPassword, err := HashPassword(password) - if err != nil { - return nil, err - } - - return &FactorPassword{ - Identifiable: Identifiable{ - ID: valuer.GenerateUUID(), - }, - TimeAuditable: TimeAuditable{ - CreatedAt: time.Now(), - }, - Password: hashedPassword, - Temporary: false, - }, nil -} - -func HashPassword(password string) (string, error) { - // bcrypt automatically handles salting and uses a secure work factor - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return "", err - } - return string(hashedPassword), nil -} - -func ComparePassword(hashedPassword, password string) bool { - return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil -} - -type ResetPasswordRequest struct { - bun.BaseModel `bun:"table:reset_password_token"` - - Identifiable - Token string `bun:"token,type:text,notnull" json:"token"` - PasswordID string `bun:"password_id,type:text,notnull,unique" json:"passwordId"` -} - -func NewResetPasswordRequest(passwordID string) (*ResetPasswordRequest, error) { - return &ResetPasswordRequest{ - Identifiable: Identifiable{ - ID: valuer.GenerateUUID(), - }, - Token: valuer.GenerateUUID().String(), - PasswordID: passwordID, - }, nil -} - -type PostableResetPassword struct { - Password string `json:"password"` - Token string `json:"token"` -} - -type ChangePasswordRequest struct { - UserId string `json:"userId"` - OldPassword string `json:"oldPassword"` - NewPassword string `json:"newPassword"` -} - type PostableLoginRequest struct { OrgID string `json:"orgId"` Email string `json:"email"` @@ -265,3 +122,97 @@ func NewTraitsFromUser(user *User) map[string]any { "created_at": user.CreatedAt, } } + +func (request *PostableAcceptInvite) UnmarshalJSON(data []byte) error { + type Alias PostableAcceptInvite + + var temp Alias + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + if temp.InviteToken == "" { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invite token is required") + } + + if !IsPasswordValid(temp.Password) { + return ErrInvalidPassword + } + + *request = PostableAcceptInvite(temp) + return nil +} + +func (request *PostableRegisterOrgAndAdmin) UnmarshalJSON(data []byte) error { + type Alias PostableRegisterOrgAndAdmin + + var temp Alias + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + if temp.Email == "" { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required") + } + + if !IsPasswordValid(temp.Password) { + return ErrInvalidPassword + } + + *request = PostableRegisterOrgAndAdmin(temp) + return nil +} + +type UserStore interface { + // invite + CreateBulkInvite(ctx context.Context, invites []*Invite) error + ListInvite(ctx context.Context, orgID string) ([]*Invite, error) + DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error + GetInviteByToken(ctx context.Context, token string) (*GettableInvite, error) + GetInviteByEmailInOrg(ctx context.Context, orgID string, email string) (*Invite, error) + + // Creates a user. + CreateUser(ctx context.Context, user *User) error + GetUserByID(ctx context.Context, orgID string, id string) (*GettableUser, error) + GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*GettableUser, error) + GetUsersByEmail(ctx context.Context, email string) ([]*GettableUser, error) + GetUsersByRoleInOrg(ctx context.Context, orgID string, role Role) ([]*GettableUser, error) + ListUsers(ctx context.Context, orgID string) ([]*GettableUser, error) + UpdateUser(ctx context.Context, orgID string, id string, user *User) (*User, error) + DeleteUser(ctx context.Context, orgID string, id string) error + + // Creates a password. + CreatePassword(ctx context.Context, password *FactorPassword) error + CreateResetPasswordToken(ctx context.Context, resetPasswordRequest *ResetPasswordToken) error + GetPassword(ctx context.Context, id valuer.UUID) (*FactorPassword, error) + GetPasswordByUserID(ctx context.Context, userID valuer.UUID) (*FactorPassword, error) + GetResetPasswordToken(ctx context.Context, token string) (*ResetPasswordToken, error) + GetResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) (*ResetPasswordToken, error) + UpdatePassword(ctx context.Context, password *FactorPassword) error + + // Auth Domain + GetDomainByName(ctx context.Context, name string) (*StorableOrgDomain, error) + // org domain (auth domains) CRUD ops + GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*GettableOrgDomain, error) + ListDomains(ctx context.Context, orgId valuer.UUID) ([]*GettableOrgDomain, error) + GetDomain(ctx context.Context, id uuid.UUID) (*GettableOrgDomain, error) + CreateDomain(ctx context.Context, d *GettableOrgDomain) error + UpdateDomain(ctx context.Context, domain *GettableOrgDomain) error + DeleteDomain(ctx context.Context, id uuid.UUID) error + + // Temporary func for SSO + GetDefaultOrgID(ctx context.Context) (string, error) + + // API KEY + CreateAPIKey(ctx context.Context, apiKey *StorableAPIKey) error + UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *StorableAPIKey, updaterID valuer.UUID) error + ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*StorableAPIKeyUser, error) + RevokeAPIKey(ctx context.Context, id valuer.UUID, revokedByUserID valuer.UUID) error + GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*StorableAPIKeyUser, error) + CountAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) + + CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) + + // Transaction + RunInTx(ctx context.Context, cb func(ctx context.Context) error) error +} diff --git a/tests/integration/fixtures/auth.py b/tests/integration/fixtures/auth.py index ddc7e80806cd..ff0e91342336 100644 --- a/tests/integration/fixtures/auth.py +++ b/tests/integration/fixtures/auth.py @@ -8,7 +8,7 @@ from fixtures import dev, types USER_ADMIN_NAME = "admin" USER_ADMIN_EMAIL = "admin@integration.test" -USER_ADMIN_PASSWORD = "password" +USER_ADMIN_PASSWORD = "password123Z$" @pytest.fixture(name="create_user_admin", scope="package") diff --git a/tests/integration/fixtures/postgres.py b/tests/integration/fixtures/postgres.py index 5ea47399cab8..75f89a3feaba 100644 --- a/tests/integration/fixtures/postgres.py +++ b/tests/integration/fixtures/postgres.py @@ -1,7 +1,7 @@ import docker import docker.errors -import psycopg2 import pytest +from sqlalchemy import create_engine, sql from testcontainers.core.container import Network from testcontainers.postgres import PostgresContainer @@ -33,14 +33,14 @@ def postgres( ) container.start() - connection = psycopg2.connect( - dbname=container.dbname, - user=container.username, - password=container.password, - host=container.get_container_host_ip(), - port=container.get_exposed_port(5432), + engine = create_engine( + f"postgresql+psycopg2://{container.username}:{container.password}@{container.get_container_host_ip()}:{container.get_exposed_port(5432)}/{container.dbname}" ) + with engine.connect() as conn: + result = conn.execute(sql.text("SELECT 1")) + assert result.fetchone()[0] == 1 + return types.TestContainerSQL( container=types.TestContainerDocker( id=container.get_wrapped_container().id, @@ -57,7 +57,7 @@ def postgres( ) }, ), - conn=connection, + conn=engine, env={ "SIGNOZ_SQLSTORE_PROVIDER": "postgres", "SIGNOZ_SQLSTORE_POSTGRES_DSN": f"postgresql://{container.username}:{container.password}@{container.get_wrapped_container().name}:{5432}/{container.dbname}", @@ -83,17 +83,17 @@ def postgres( host_config = container.host_configs["5432"] env = cache["env"] - connection = psycopg2.connect( - dbname=env["SIGNOZ_SQLSTORE_POSTGRES_DBNAME"], - user=env["SIGNOZ_SQLSTORE_POSTGRES_USER"], - password=env["SIGNOZ_SQLSTORE_POSTGRES_PASSWORD"], - host=host_config.address, - port=host_config.port, + engine = create_engine( + f"postgresql+psycopg2://{env['SIGNOZ_SQLSTORE_POSTGRES_USER']}:{env['SIGNOZ_SQLSTORE_POSTGRES_PASSWORD']}@{host_config.address}:{host_config.port}/{env['SIGNOZ_SQLSTORE_POSTGRES_DBNAME']}" ) + with engine.connect() as conn: + result = conn.execute(sql.text("SELECT 1")) + assert result.fetchone()[0] == 1 + return types.TestContainerSQL( container=container, - conn=connection, + conn=engine, env=env, ) diff --git a/tests/integration/fixtures/sqlite.py b/tests/integration/fixtures/sqlite.py index 0467da6b3db7..bcf1b9bbd9a8 100644 --- a/tests/integration/fixtures/sqlite.py +++ b/tests/integration/fixtures/sqlite.py @@ -1,8 +1,8 @@ -import sqlite3 from collections import namedtuple from typing import Any, Generator import pytest +from sqlalchemy import create_engine, sql from fixtures import dev, types @@ -22,7 +22,11 @@ def sqlite( def create() -> types.TestContainerSQL: tmpdir = tmpfs("sqlite") path = tmpdir / "signoz.db" - connection = sqlite3.connect(path, check_same_thread=False) + + engine = create_engine(f"sqlite:///{path}") + with engine.connect() as conn: + result = conn.execute(sql.text("SELECT 1")) + assert result.fetchone()[0] == 1 return types.TestContainerSQL( container=types.TestContainerDocker( @@ -30,7 +34,7 @@ def sqlite( host_configs={}, container_configs={}, ), - conn=connection, + conn=engine, env={ "SIGNOZ_SQLSTORE_PROVIDER": "sqlite", "SIGNOZ_SQLSTORE_SQLITE_PATH": str(path), @@ -42,7 +46,12 @@ def sqlite( def restore(cache: dict) -> types.TestContainerSQL: path = cache["env"].get("SIGNOZ_SQLSTORE_SQLITE_PATH") - conn = sqlite3.connect(path, check_same_thread=False) + + engine = create_engine(f"sqlite:///{path}") + with engine.connect() as conn: + result = conn.execute(sql.text("SELECT 1")) + assert result.fetchone()[0] == 1 + return types.TestContainerSQL( container=types.TestContainerDocker( id="", diff --git a/tests/integration/fixtures/types.py b/tests/integration/fixtures/types.py index c24130f145b3..90dbb2c45c28 100644 --- a/tests/integration/fixtures/types.py +++ b/tests/integration/fixtures/types.py @@ -6,6 +6,7 @@ import clickhouse_connect import clickhouse_connect.driver import clickhouse_connect.driver.client import py +from sqlalchemy import Engine from testcontainers.core.container import Network LegacyPath = py.path.local @@ -76,7 +77,7 @@ class TestContainerDocker: class TestContainerSQL: __test__ = False container: TestContainerDocker - conn: any + conn: Engine env: Dict[str, str] def __cache__(self) -> dict: diff --git a/tests/integration/poetry.lock b/tests/integration/poetry.lock index d0cc3210633c..1c1f5c13fac7 100644 --- a/tests/integration/poetry.lock +++ b/tests/integration/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "astroid" @@ -390,7 +390,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "sys_platform == \"win32\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +markers = {main = "sys_platform == \"win32\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\""} [[package]] name = "dill" @@ -431,6 +431,75 @@ docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] ssh = ["paramiko (>=2.4.3)"] websockets = ["websocket-client (>=1.3.0)"] +[[package]] +name = "greenlet" +version = "3.2.4" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" +files = [ + {file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, + {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, + {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, + {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, + {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, + {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, + {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, + {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, + {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, + {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, + {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, + {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, + {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, + {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + [[package]] name = "idna" version = "3.10" @@ -889,6 +958,102 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "sqlalchemy" +version = "2.0.43" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "SQLAlchemy-2.0.43-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21ba7a08a4253c5825d1db389d4299f64a100ef9800e4624c8bf70d8f136e6ed"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11b9503fa6f8721bef9b8567730f664c5a5153d25e247aadc69247c4bc605227"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07097c0a1886c150ef2adba2ff7437e84d40c0f7dcb44a2c2b9c905ccfc6361c"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cdeff998cb294896a34e5b2f00e383e7c5c4ef3b4bfa375d9104723f15186443"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:bcf0724a62a5670e5718957e05c56ec2d6850267ea859f8ad2481838f889b42c"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-win32.whl", hash = "sha256:c697575d0e2b0a5f0433f679bda22f63873821d991e95a90e9e52aae517b2e32"}, + {file = "SQLAlchemy-2.0.43-cp37-cp37m-win_amd64.whl", hash = "sha256:d34c0f6dbefd2e816e8f341d0df7d4763d382e3f452423e752ffd1e213da2512"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-win32.whl", hash = "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00"}, + {file = "sqlalchemy-2.0.43-cp310-cp310-win_amd64.whl", hash = "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-win32.whl", hash = "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921"}, + {file = "sqlalchemy-2.0.43-cp311-cp311-win_amd64.whl", hash = "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d"}, + {file = "sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d"}, + {file = "sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4e6aeb2e0932f32950cf56a8b4813cb15ff792fc0c9b3752eaf067cfe298496a"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f964a05356f4bca4112e6334ed7c208174511bd56e6b8fc86dad4d024d4185"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46293c39252f93ea0910aababa8752ad628bcce3a10d3f260648dd472256983f"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:136063a68644eca9339d02e6693932116f6a8591ac013b0014479a1de664e40a"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6e2bf13d9256398d037fef09fd8bf9b0bf77876e22647d10761d35593b9ac547"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:44337823462291f17f994d64282a71c51d738fc9ef561bf265f1d0fd9116a782"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-win32.whl", hash = "sha256:13194276e69bb2af56198fef7909d48fd34820de01d9c92711a5fa45497cc7ed"}, + {file = "sqlalchemy-2.0.43-cp38-cp38-win_amd64.whl", hash = "sha256:334f41fa28de9f9be4b78445e68530da3c5fa054c907176460c81494f4ae1f5e"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ceb5c832cc30663aeaf5e39657712f4c4241ad1f638d487ef7216258f6d41fe7"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11f43c39b4b2ec755573952bbcc58d976779d482f6f832d7f33a8d869ae891bf"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:413391b2239db55be14fa4223034d7e13325a1812c8396ecd4f2c08696d5ccad"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c379e37b08c6c527181a397212346be39319fb64323741d23e46abd97a400d34"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03d73ab2a37d9e40dec4984d1813d7878e01dbdc742448d44a7341b7a9f408c7"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8cee08f15d9e238ede42e9bbc1d6e7158d0ca4f176e4eab21f88ac819ae3bd7b"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-win32.whl", hash = "sha256:b3edaec7e8b6dc5cd94523c6df4f294014df67097c8217a89929c99975811414"}, + {file = "sqlalchemy-2.0.43-cp39-cp39-win_amd64.whl", hash = "sha256:227119ce0a89e762ecd882dc661e0aa677a690c914e358f0dd8932a2e8b2765b"}, + {file = "sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc"}, + {file = "sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417"}, +] + +[package.dependencies] +greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "svix-ksuid" version = "0.6.2" @@ -1223,4 +1388,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "200a3892f48467b2639abfae99b94b6de6b75fe09f9669ea115eb2b55a2f46ea" +content-hash = "fc17158ab90e70dbd94668e3346d6126384cb17cf28c3b3ec82e5ed067058380" diff --git a/tests/integration/pyproject.toml b/tests/integration/pyproject.toml index d93f0ea32129..3068aafc8699 100644 --- a/tests/integration/pyproject.toml +++ b/tests/integration/pyproject.toml @@ -15,6 +15,7 @@ numpy = "^2.3.2" clickhouse-connect = "^0.8.18" svix-ksuid = "^0.6.2" requests = "^2.32.4" +sqlalchemy = "^2.0.43" [tool.poetry.group.dev.dependencies] diff --git a/tests/integration/src/auth/a_register.py b/tests/integration/src/auth/a_register.py index d2275824d309..0402964d363b 100644 --- a/tests/integration/src/auth/a_register.py +++ b/tests/integration/src/auth/a_register.py @@ -8,6 +8,22 @@ from fixtures.logger import setup_logger logger = setup_logger(__name__) +def test_register_with_invalid_password(signoz: types.SigNoz) -> None: + response = requests.post( + signoz.self.host_configs["8080"].get("/api/v1/register"), + json={ + "name": "admin", + "orgId": "", + "orgName": "integration.test", + "email": "admin@integration.test", + "password": "password", + }, + timeout=2, + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + def test_register(signoz: types.SigNoz, get_jwt_token) -> None: response = requests.get( signoz.self.host_configs["8080"].get("/api/v1/version"), timeout=2 @@ -23,7 +39,7 @@ def test_register(signoz: types.SigNoz, get_jwt_token) -> None: "orgId": "", "orgName": "integration.test", "email": "admin@integration.test", - "password": "password", + "password": "password123Z$", }, timeout=2, ) @@ -36,7 +52,7 @@ def test_register(signoz: types.SigNoz, get_jwt_token) -> None: assert response.status_code == HTTPStatus.OK assert response.json()["setupCompleted"] is True - admin_token = get_jwt_token("admin@integration.test", "password") + admin_token = get_jwt_token("admin@integration.test", "password123Z$") response = requests.get( signoz.self.host_configs["8080"].get("/api/v1/user"), @@ -72,7 +88,7 @@ def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None: json={"email": "editor@integration.test", "role": "EDITOR"}, timeout=2, headers={ - "Authorization": f"Bearer {get_jwt_token("admin@integration.test", "password")}" + "Authorization": f"Bearer {get_jwt_token("admin@integration.test", "password123Z$")}" }, ) @@ -82,7 +98,7 @@ def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None: signoz.self.host_configs["8080"].get("/api/v1/invite"), timeout=2, headers={ - "Authorization": f"Bearer {get_jwt_token("admin@integration.test", "password")}" + "Authorization": f"Bearer {get_jwt_token("admin@integration.test", "password123Z$")}" }, ) @@ -100,7 +116,7 @@ def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None: response = requests.post( signoz.self.host_configs["8080"].get("/api/v1/invite/accept"), json={ - "password": "password", + "password": "password123Z$", "displayName": "editor", "token": f"{found_invite['token']}", }, @@ -121,7 +137,7 @@ def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None: signoz.self.host_configs["8080"].get("/api/v1/user"), timeout=2, headers={ - "Authorization": f"Bearer {get_jwt_token("editor@integration.test", "password")}" + "Authorization": f"Bearer {get_jwt_token("editor@integration.test", "password123Z$")}" }, ) @@ -132,7 +148,7 @@ def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None: signoz.self.host_configs["8080"].get("/api/v1/user"), timeout=2, headers={ - "Authorization": f"Bearer {get_jwt_token("admin@integration.test", "password")}" + "Authorization": f"Bearer {get_jwt_token("admin@integration.test", "password123Z$")}" }, ) @@ -151,7 +167,7 @@ def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None: def test_revoke_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None: - admin_token = get_jwt_token("admin@integration.test", "password") + admin_token = get_jwt_token("admin@integration.test", "password123Z$") # Generate an invite token for the viewer user response = requests.post( signoz.self.host_configs["8080"].get("/api/v1/invite"), @@ -166,7 +182,7 @@ def test_revoke_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None signoz.self.host_configs["8080"].get("/api/v1/invite"), timeout=2, headers={ - "Authorization": f"Bearer {get_jwt_token("admin@integration.test", "password")}" + "Authorization": f"Bearer {get_jwt_token("admin@integration.test", "password123Z$")}" }, ) @@ -192,7 +208,7 @@ def test_revoke_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None response = requests.post( signoz.self.host_configs["8080"].get("/api/v1/invite/accept"), json={ - "password": "password", + "password": "password123Z$", "displayName": "viewer", "token": f"{found_invite["token"]}", }, @@ -203,7 +219,7 @@ def test_revoke_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None def test_self_access(signoz: types.SigNoz, get_jwt_token) -> None: - admin_token = get_jwt_token("admin@integration.test", "password") + admin_token = get_jwt_token("admin@integration.test", "password123Z$") response = requests.get( signoz.self.host_configs["8080"].get("/api/v1/user"), diff --git a/tests/integration/src/auth/b_license.py b/tests/integration/src/auth/b_license.py index 2a578c96ad24..ac148c006427 100644 --- a/tests/integration/src/auth/b_license.py +++ b/tests/integration/src/auth/b_license.py @@ -2,6 +2,7 @@ import http import json import requests +from sqlalchemy import sql from wiremock.client import ( HttpMethods, Mapping, @@ -52,7 +53,7 @@ def test_apply_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None: ], ) - access_token = get_jwt_token("admin@integration.test", "password") + access_token = get_jwt_token("admin@integration.test", "password123Z$") response = requests.post( url=signoz.self.host_configs["8080"].get("/api/v3/licenses"), @@ -111,7 +112,7 @@ def test_refresh_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None ], ) - access_token = get_jwt_token("admin@integration.test", "password") + access_token = get_jwt_token("admin@integration.test", "password123Z$") response = requests.put( url=signoz.self.host_configs["8080"].get("/api/v3/licenses"), @@ -121,12 +122,13 @@ def test_refresh_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None assert response.status_code == http.HTTPStatus.NO_CONTENT - cursor = signoz.sqlstore.conn.cursor() - cursor.execute( - "SELECT data FROM license WHERE id='0196360e-90cd-7a74-8313-1aa815ce2a67'" - ) - record = cursor.fetchone()[0] - assert json.loads(record)["valid_from"] == 1732146922 + with signoz.sqlstore.conn.connect() as conn: + result = conn.execute( + sql.text("SELECT data FROM license WHERE id=:id"), + {"id": "0196360e-90cd-7a74-8313-1aa815ce2a67"}, + ) + record = result.fetchone()[0] + assert json.loads(record)["valid_from"] == 1732146922 response = requests.post( url=signoz.zeus.host_configs["8080"].get("/__admin/requests/count"), @@ -163,7 +165,7 @@ def test_license_checkout(signoz: SigNoz, make_http_mocks, get_jwt_token) -> Non ], ) - access_token = get_jwt_token("admin@integration.test", "password") + access_token = get_jwt_token("admin@integration.test", "password123Z$") response = requests.post( url=signoz.self.host_configs["8080"].get("/api/v1/checkout"), @@ -210,7 +212,7 @@ def test_license_portal(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None: ], ) - access_token = get_jwt_token("admin@integration.test", "password") + access_token = get_jwt_token("admin@integration.test", "password123Z$") response = requests.post( url=signoz.self.host_configs["8080"].get("/api/v1/portal"), diff --git a/tests/integration/src/auth/c_apikey.py b/tests/integration/src/auth/c_apikey.py index 77f34a870174..35fecaa9cec4 100644 --- a/tests/integration/src/auth/c_apikey.py +++ b/tests/integration/src/auth/c_apikey.py @@ -6,7 +6,7 @@ from fixtures import types def test_api_key(signoz: types.SigNoz, get_jwt_token) -> None: - admin_token = get_jwt_token("admin@integration.test", "password") + admin_token = get_jwt_token("admin@integration.test", "password123Z$") response = requests.post( signoz.self.host_configs["8080"].get("/api/v1/pats"), diff --git a/tests/integration/src/auth/d_password.py b/tests/integration/src/auth/d_password.py new file mode 100644 index 000000000000..1c6c2e57aff7 --- /dev/null +++ b/tests/integration/src/auth/d_password.py @@ -0,0 +1,235 @@ +from http import HTTPStatus + +import requests +from sqlalchemy import sql + +from fixtures import types +from fixtures.logger import setup_logger + +logger = setup_logger(__name__) + + +def test_change_password(signoz: types.SigNoz, get_jwt_token) -> None: + admin_token = get_jwt_token("admin@integration.test", "password123Z$") + + # Create another admin user + response = requests.post( + signoz.self.host_configs["8080"].get("/api/v1/invite"), + json={"email": "admin+password@integration.test", "role": "ADMIN"}, + timeout=2, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + + assert response.status_code == HTTPStatus.CREATED + + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/invite"), + timeout=2, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + + invite_response = response.json()["data"] + found_invite = next( + ( + invite + for invite in invite_response + if invite["email"] == "admin+password@integration.test" + ), + None, + ) + + # Accept the invite with a bad password which should fail + response = requests.post( + signoz.self.host_configs["8080"].get("/api/v1/invite/accept"), + json={ + "password": "password", + "displayName": "admin password", + "token": f"{found_invite['token']}", + }, + timeout=2, + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + # Accept the invite with a good password + response = requests.post( + signoz.self.host_configs["8080"].get("/api/v1/invite/accept"), + json={ + "password": "password123Z$", + "displayName": "admin password", + "token": f"{found_invite['token']}", + }, + timeout=2, + ) + + assert response.status_code == HTTPStatus.OK + + # Get the user id + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/user"), + timeout=2, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + + assert response.status_code == HTTPStatus.OK + + user_response = response.json()["data"] + found_user = next( + ( + user + for user in user_response + if user["email"] == "admin+password@integration.test" + ), + None, + ) + + # Try logging in with the password + token = get_jwt_token("admin+password@integration.test", "password123Z$") + assert token is not None + + # Try changing the password with a bad old password which should fail + response = requests.post( + signoz.self.host_configs["8080"].get( + f"/api/v1/changePassword/{found_user['id']}" + ), + json={ + "userId": f"{found_user['id']}", + "oldPassword": "password", + "newPassword": "password123Z$", + }, + timeout=2, + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + # Try changing the password with a good old password + response = requests.post( + signoz.self.host_configs["8080"].get( + f"/api/v1/changePassword/{found_user['id']}" + ), + json={ + "userId": f"{found_user['id']}", + "oldPassword": "password123Z$", + "newPassword": "password123Znew$", + }, + timeout=2, + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == HTTPStatus.NO_CONTENT + + # Try logging in with the new password + token = get_jwt_token("admin+password@integration.test", "password123Znew$") + assert token is not None + + +def test_reset_password(signoz: types.SigNoz, get_jwt_token) -> None: + admin_token = get_jwt_token("admin@integration.test", "password123Z$") + + # Get the user id for admin+password@integration.test + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/user"), + timeout=2, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + + assert response.status_code == HTTPStatus.OK + + user_response = response.json()["data"] + found_user = next( + ( + user + for user in user_response + if user["email"] == "admin+password@integration.test" + ), + None, + ) + + response = requests.get( + signoz.self.host_configs["8080"].get( + f"/api/v1/getResetPasswordToken/{found_user['id']}" + ), + headers={"Authorization": f"Bearer {admin_token}"}, + timeout=2, + ) + + assert response.status_code == HTTPStatus.OK + + token = response.json()["data"]["token"] + + # Reset the password with a bad password which should fail + response = requests.post( + signoz.self.host_configs["8080"].get("/api/v1/resetPassword"), + json={"password": "password", "token": token}, + timeout=2, + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + # Reset the password with a good password + response = requests.post( + signoz.self.host_configs["8080"].get("/api/v1/resetPassword"), + json={"password": "password123Z$NEWNEW#!", "token": token}, + timeout=2, + ) + + assert response.status_code == HTTPStatus.NO_CONTENT + + token = get_jwt_token("admin+password@integration.test", "password123Z$NEWNEW#!") + assert token is not None + + +def test_reset_password_with_no_password(signoz: types.SigNoz, get_jwt_token) -> None: + admin_token = get_jwt_token("admin@integration.test", "password123Z$") + + # Get the user id for admin+password@integration.test + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/user"), + timeout=2, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + + assert response.status_code == HTTPStatus.OK + + user_response = response.json()["data"] + found_user = next( + ( + user + for user in user_response + if user["email"] == "admin+password@integration.test" + ), + None, + ) + + with signoz.sqlstore.conn.connect() as conn: + result = conn.execute( + sql.text("DELETE FROM factor_password WHERE user_id = :user_id"), + {"user_id": found_user["id"]}, + ) + assert result.rowcount == 1 + + # Generate a new reset password token + response = requests.get( + signoz.self.host_configs["8080"].get( + f"/api/v1/getResetPasswordToken/{found_user['id']}" + ), + headers={"Authorization": f"Bearer {admin_token}"}, + timeout=2, + ) + + assert response.status_code == HTTPStatus.OK + + token = response.json()["data"]["token"] + + # Reset the password with a good password + response = requests.post( + signoz.self.host_configs["8080"].get("/api/v1/resetPassword"), + json={"password": "FINALPASSword123!#[", "token": token}, + timeout=2, + ) + + assert response.status_code == HTTPStatus.NO_CONTENT + + token = get_jwt_token("admin+password@integration.test", "FINALPASSword123!#[") + assert token is not None