feat(password): implement strong controls for password (#8983)

## 📄 Summary

implement strong controls for password. Now the password requirement is : 

password must be at least 12 characters long, should contain at least one uppercase letter [A-Z], one lowercase letter [a-z], one number [0-9], and one symbol
This commit is contained in:
Vibhu Pandey 2025-09-04 17:22:28 +05:30 committed by GitHub
parent 27580b62ba
commit 360e8309c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1102 additions and 442 deletions

View File

@ -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

View File

@ -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) (

View File

@ -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
}

41
pkg/http/binding/json.go Normal file
View File

@ -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
}

View File

@ -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)

View File

@ -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").

View File

@ -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) {

View File

@ -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

View File

@ -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)
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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())
})
}

View File

@ -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
}

View File

@ -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")

View File

@ -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,
)

View File

@ -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="",

View File

@ -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:

View File

@ -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"

View File

@ -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]

View File

@ -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"),

View File

@ -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"),

View File

@ -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"),

View File

@ -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