mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
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:
parent
27580b62ba
commit
360e8309c8
@ -2,6 +2,7 @@ FROM node:18-bullseye AS build
|
|||||||
|
|
||||||
WORKDIR /opt/
|
WORKDIR /opt/
|
||||||
COPY ./frontend/ ./
|
COPY ./frontend/ ./
|
||||||
|
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||||
RUN CI=1 yarn install
|
RUN CI=1 yarn install
|
||||||
RUN CI=1 yarn build
|
RUN CI=1 yarn build
|
||||||
|
|
||||||
|
|||||||
@ -13,11 +13,11 @@ import (
|
|||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"go.uber.org/zap"
|
"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 {
|
if err != nil {
|
||||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
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) (
|
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
||||||
|
|||||||
38
pkg/http/binding/binding.go
Normal file
38
pkg/http/binding/binding.go
Normal 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
41
pkg/http/binding/json.go
Normal 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
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ func NewStore(sqlstore sqlstore.SQLStore) types.OrganizationStore {
|
|||||||
func (store *store) Create(ctx context.Context, organization *types.Organization) error {
|
func (store *store) Create(ctx context.Context, organization *types.Organization) error {
|
||||||
_, err := store.
|
_, err := store.
|
||||||
sqlstore.
|
sqlstore.
|
||||||
BunDB().
|
BunDBCtx(ctx).
|
||||||
NewInsert().
|
NewInsert().
|
||||||
Model(organization).
|
Model(organization).
|
||||||
Exec(ctx)
|
Exec(ctx)
|
||||||
|
|||||||
@ -14,12 +14,10 @@ type store struct {
|
|||||||
store sqlstore.SQLStore
|
store sqlstore.SQLStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStore creates a new SQLite store for quick filters
|
|
||||||
func NewStore(db sqlstore.SQLStore) quickfiltertypes.QuickFilterStore {
|
func NewStore(db sqlstore.SQLStore) quickfiltertypes.QuickFilterStore {
|
||||||
return &store{store: db}
|
return &store{store: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetQuickFilters retrieves all filters for an organization
|
|
||||||
func (s *store) Get(ctx context.Context, orgID valuer.UUID) ([]*quickfiltertypes.StorableQuickFilter, error) {
|
func (s *store) Get(ctx context.Context, orgID valuer.UUID) ([]*quickfiltertypes.StorableQuickFilter, error) {
|
||||||
filters := make([]*quickfiltertypes.StorableQuickFilter, 0)
|
filters := make([]*quickfiltertypes.StorableQuickFilter, 0)
|
||||||
|
|
||||||
@ -38,7 +36,6 @@ func (s *store) Get(ctx context.Context, orgID valuer.UUID) ([]*quickfiltertypes
|
|||||||
return filters, nil
|
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) {
|
func (s *store) GetBySignal(ctx context.Context, orgID valuer.UUID, signal string) (*quickfiltertypes.StorableQuickFilter, error) {
|
||||||
filter := new(quickfiltertypes.StorableQuickFilter)
|
filter := new(quickfiltertypes.StorableQuickFilter)
|
||||||
|
|
||||||
@ -60,7 +57,6 @@ func (s *store) GetBySignal(ctx context.Context, orgID valuer.UUID, signal strin
|
|||||||
return filter, nil
|
return filter, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpsertQuickFilter inserts or updates filters for an organization and signal
|
|
||||||
func (s *store) Upsert(ctx context.Context, filter *quickfiltertypes.StorableQuickFilter) error {
|
func (s *store) Upsert(ctx context.Context, filter *quickfiltertypes.StorableQuickFilter) error {
|
||||||
_, err := s.store.
|
_, err := s.store.
|
||||||
BunDB().
|
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 {
|
func (s *store) Create(ctx context.Context, filters []*quickfiltertypes.StorableQuickFilter) error {
|
||||||
// Using SQLite-specific conflict resolution
|
|
||||||
_, err := s.store.
|
_, err := s.store.
|
||||||
BunDB().
|
BunDBCtx(ctx).
|
||||||
NewInsert().
|
NewInsert().
|
||||||
Model(&filters).
|
Model(&filters).
|
||||||
On("CONFLICT (org_id, signal) DO NOTHING").
|
On("CONFLICT (org_id, signal) DO NOTHING").
|
||||||
|
|||||||
@ -8,8 +8,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"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/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"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
@ -18,10 +19,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type handler struct {
|
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}
|
return &handler{module: module}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,8 +31,8 @@ func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req := new(types.PostableAcceptInvite)
|
req := new(types.PostableAcceptInvite)
|
||||||
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||||
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode user"))
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,13 +80,13 @@ func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
password, err := types.NewFactorPassword(req.Password)
|
password, err := types.NewFactorPassword(req.Password, user.ID.StringValue())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = h.module.CreateUserWithPassword(ctx, user, password)
|
err = h.module.CreateUser(ctx, user, root.WithFactorPassword(password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
@ -335,7 +336,7 @@ func (h *handler) LoginPrecheck(w http.ResponseWriter, r *http.Request) {
|
|||||||
render.Success(w, http.StatusOK, resp)
|
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)
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
defer cancel()
|
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
|
// 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 {
|
if err != nil {
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := h.module.CreateResetPasswordToken(ctx, id)
|
token, err := handler.module.GetOrCreateResetPasswordToken(ctx, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
@ -363,7 +364,7 @@ func (h *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Request)
|
|||||||
render.Success(w, http.StatusOK, token)
|
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)
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -373,22 +374,16 @@ func (h *handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := h.module.GetResetPassword(ctx, req.Token)
|
err := handler.module.UpdatePasswordByResetPasswordToken(ctx, req.Token, req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.module.UpdatePasswordAndDeleteResetPasswordEntry(ctx, entry.PasswordID, req.Password)
|
render.Success(w, http.StatusNoContent, nil)
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusOK, 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)
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -398,25 +393,13 @@ func (h *handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the current password
|
err := handler.module.UpdatePassword(ctx, req.UserID, req.OldPassword, req.NewPassword)
|
||||||
password, err := h.module.GetPasswordByUserID(ctx, req.UserId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !types.ComparePassword(password.Password, req.OldPassword) {
|
render.Success(w, http.StatusNoContent, nil)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) Login(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@ -13,9 +13,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"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/constants"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
"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.
|
// 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")
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
|
||||||
return &Module{
|
return &Module{
|
||||||
store: store,
|
store: store,
|
||||||
@ -132,27 +131,28 @@ func (m *Module) GetInviteByEmailInOrg(ctx context.Context, orgID string, email
|
|||||||
return m.store.GetInviteByEmailInOrg(ctx, orgID, email)
|
return m.store.GetInviteByEmailInOrg(ctx, orgID, email)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) CreateUserWithPassword(ctx context.Context, user *types.User, password *types.FactorPassword) (*types.User, error) {
|
func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
|
||||||
user, err := m.store.CreateUserWithPassword(ctx, user, password)
|
createUserOpts := root.NewCreateUserOptions(opts...)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
traitsOrProperties := types.NewTraitsFromUser(user)
|
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||||
m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traitsOrProperties)
|
if err := module.store.CreateUser(ctx, input); err != nil {
|
||||||
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Created", traitsOrProperties)
|
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 {
|
return nil
|
||||||
if err := m.store.CreateUser(ctx, user); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
traitsOrProperties := types.NewTraitsFromUser(user)
|
traitsOrProperties := types.NewTraitsFromUser(input)
|
||||||
m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traitsOrProperties)
|
module.analytics.IdentifyUser(ctx, input.OrgID, input.ID.String(), traitsOrProperties)
|
||||||
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Created", traitsOrProperties)
|
module.analytics.TrackUser(ctx, input.OrgID, input.ID.String(), "User Created", traitsOrProperties)
|
||||||
|
|
||||||
return nil
|
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) {
|
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)
|
existingUser, err := m.GetUserByID(ctx, orgID, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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")
|
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
|
// also an admin user can only change role of their own or other user
|
||||||
if user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin.String() {
|
if user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin.String() {
|
||||||
adminUsers, err := m.GetUsersByRoleInOrg(ctx, orgID, types.RoleAdmin)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) CreateResetPasswordToken(ctx context.Context, userID string) (*types.ResetPasswordRequest, error) {
|
func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID valuer.UUID) (*types.ResetPasswordToken, error) {
|
||||||
password, err := m.store.GetPasswordByUserID(ctx, userID)
|
password, err := module.store.GetPasswordByUserID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// if the user does not have a password, we need to create a new one
|
if !errors.Ast(err, errors.TypeNotFound) {
|
||||||
// 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 {
|
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if a reset password token already exists for this user
|
err = module.store.CreateResetPasswordToken(ctx, resetPasswordToken)
|
||||||
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)
|
|
||||||
if err != nil {
|
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) {
|
func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, token string, passwd string) error {
|
||||||
return m.store.GetPasswordByUserID(ctx, id)
|
resetPasswordToken, err := module.store.GetResetPasswordToken(ctx, token)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
existingPassword, err := m.store.GetPasswordByID(ctx, passwordID)
|
password, err := module.store.GetPassword(ctx, resetPasswordToken.PasswordID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, oldpasswd string, passwd string) error {
|
||||||
hashedPassword, err := types.HashPassword(password)
|
password, err := module.store.GetPasswordByUserID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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) {
|
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")
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !types.ComparePassword(existingPassword.Password, password) {
|
if !existingPassword.Equals(password) {
|
||||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid password")
|
return nil, errors.New(errors.TypeInvalidInput, types.ErrCodeIncorrectPassword, "password is incorrect")
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbUser, nil
|
return dbUser, nil
|
||||||
@ -631,34 +626,31 @@ func (m *Module) UpdateDomain(ctx context.Context, domain *types.GettableOrgDoma
|
|||||||
return m.store.UpdateDomain(ctx, domain)
|
return m.store.UpdateDomain(ctx, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) Register(ctx context.Context, req *types.PostableRegisterOrgAndAdmin) (*types.User, error) {
|
func (module *Module) CreateFirstUser(ctx context.Context, organization *types.Organization, name string, email string, passwd string) (*types.User, error) {
|
||||||
if req.Email == "" {
|
user, err := types.NewUser(name, email, types.RoleAdmin.String(), organization.ID.StringValue())
|
||||||
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)
|
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
return nil, model.InternalError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
password, err := types.NewFactorPassword(req.Password)
|
if err = module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||||
if err != nil {
|
err := module.orgSetter.Create(ctx, organization)
|
||||||
return nil, model.InternalError(err)
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
user, err = m.CreateUserWithPassword(ctx, user, password)
|
err = module.CreateUser(ctx, user, root.WithFactorPassword(password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, model.InternalError(err)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
|
|||||||
@ -104,55 +104,29 @@ func (store *store) ListInvite(ctx context.Context, orgID string) ([]*types.Invi
|
|||||||
return *invites, nil
|
return *invites, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *store) CreatePassword(ctx context.Context, password *types.FactorPassword) (*types.FactorPassword, error) {
|
func (store *store) CreatePassword(ctx context.Context, password *types.FactorPassword) error {
|
||||||
_, err := store.sqlstore.BunDB().NewInsert().
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDBCtx(ctx).
|
||||||
|
NewInsert().
|
||||||
Model(password).
|
Model(password).
|
||||||
Exec(ctx)
|
Exec(ctx)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *store) CreateUser(ctx context.Context, user *types.User) error {
|
func (store *store) CreateUser(ctx context.Context, user *types.User) error {
|
||||||
_, err := store.sqlstore.BunDB().NewInsert().
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDBCtx(ctx).
|
||||||
|
NewInsert().
|
||||||
Model(user).
|
Model(user).
|
||||||
Exec(ctx)
|
Exec(ctx)
|
||||||
if err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@ -329,7 +303,7 @@ func (store *store) DeleteUser(ctx context.Context, orgID string, id string) err
|
|||||||
|
|
||||||
// delete reset password request
|
// delete reset password request
|
||||||
_, err = tx.NewDelete().
|
_, err = tx.NewDelete().
|
||||||
Model(new(types.ResetPasswordRequest)).
|
Model(new(types.ResetPasswordToken)).
|
||||||
Where("password_id = ?", password.ID.String()).
|
Where("password_id = ?", password.ID.String()).
|
||||||
Exec(ctx)
|
Exec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -372,128 +346,123 @@ func (store *store) DeleteUser(ctx context.Context, orgID string, id string) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *store) CreateResetPasswordToken(ctx context.Context, resetPasswordRequest *types.ResetPasswordRequest) error {
|
func (store *store) CreateResetPasswordToken(ctx context.Context, resetPasswordToken *types.ResetPasswordToken) error {
|
||||||
_, err := store.sqlstore.BunDB().NewInsert().
|
_, err := store.
|
||||||
Model(resetPasswordRequest).
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewInsert().
|
||||||
|
Model(resetPasswordToken).
|
||||||
Exec(ctx)
|
Exec(ctx)
|
||||||
|
|
||||||
if err != nil {
|
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
|
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)
|
password := new(types.FactorPassword)
|
||||||
err := store.sqlstore.BunDB().NewSelect().
|
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
Model(password).
|
Model(password).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password with id: %s does not exist", id)
|
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password with id: %s does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return password, nil
|
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)
|
password := new(types.FactorPassword)
|
||||||
err := store.sqlstore.BunDB().NewSelect().
|
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
Model(password).
|
Model(password).
|
||||||
Where("user_id = ?", id).
|
Where("user_id = ?", userID).
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
if err != nil {
|
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
|
return password, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *store) GetResetPasswordByPasswordID(ctx context.Context, passwordID string) (*types.ResetPasswordRequest, error) {
|
func (store *store) GetResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) (*types.ResetPasswordToken, error) {
|
||||||
resetPasswordRequest := new(types.ResetPasswordRequest)
|
resetPasswordToken := new(types.ResetPasswordToken)
|
||||||
err := store.sqlstore.BunDB().NewSelect().
|
|
||||||
Model(resetPasswordRequest).
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(resetPasswordToken).
|
||||||
Where("password_id = ?", passwordID).
|
Where("password_id = ?", passwordID).
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
if err != nil {
|
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) {
|
func (store *store) GetResetPasswordToken(ctx context.Context, token string) (*types.ResetPasswordToken, error) {
|
||||||
resetPasswordRequest := new(types.ResetPasswordRequest)
|
resetPasswordRequest := new(types.ResetPasswordToken)
|
||||||
err := store.sqlstore.BunDB().NewSelect().
|
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
Model(resetPasswordRequest).
|
Model(resetPasswordRequest).
|
||||||
Where("token = ?", token).
|
Where("token = ?", token).
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
if err != nil {
|
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
|
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)
|
tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to start transaction")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
factorPassword := &types.FactorPassword{
|
_, err = tx.
|
||||||
UserID: userID,
|
NewUpdate().
|
||||||
Password: password,
|
|
||||||
TimeAuditable: types.TimeAuditable{
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err = tx.NewUpdate().
|
|
||||||
Model(factorPassword).
|
Model(factorPassword).
|
||||||
Column("password").
|
Where("user_id = ?", factorPassword.UserID).
|
||||||
Column("updated_at").
|
|
||||||
Where("user_id = ?", userID).
|
|
||||||
Exec(ctx)
|
Exec(ctx)
|
||||||
if err != nil {
|
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().
|
_, err = tx.
|
||||||
Model(&types.ResetPasswordRequest{}).
|
NewDelete().
|
||||||
Where("password_id = ?", userID).
|
Model(&types.ResetPasswordToken{}).
|
||||||
|
Where("password_id = ?", factorPassword.ID).
|
||||||
Exec(ctx)
|
Exec(ctx)
|
||||||
if err != nil {
|
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()
|
err = tx.Commit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to commit transaction")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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) {
|
func (store *store) GetDomainByName(ctx context.Context, name string) (*types.StorableOrgDomain, error) {
|
||||||
domain := new(types.StorableOrgDomain)
|
domain := new(types.StorableOrgDomain)
|
||||||
err := store.sqlstore.BunDB().NewSelect().
|
err := store.sqlstore.BunDB().NewSelect().
|
||||||
@ -844,3 +813,9 @@ func (store *store) CountAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) (
|
|||||||
|
|
||||||
return int64(count), nil
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
27
pkg/modules/user/option.go
Normal file
27
pkg/modules/user/option.go
Normal 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
|
||||||
|
}
|
||||||
@ -12,16 +12,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Module interface {
|
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
|
// invite
|
||||||
CreateBulkInvite(ctx context.Context, orgID, userID string, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error)
|
CreateBulkInvite(ctx context.Context, orgID, userID string, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error)
|
||||||
ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error)
|
ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error)
|
||||||
DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
|
DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
|
||||||
GetInviteByToken(ctx context.Context, token string) (*types.GettableInvite, error)
|
GetInviteByToken(ctx context.Context, token string) (*types.GettableInvite, error)
|
||||||
GetInviteByEmailInOrg(ctx context.Context, orgID string, email string) (*types.Invite, 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)
|
GetUserByID(ctx context.Context, orgID string, id string) (*types.GettableUser, error)
|
||||||
GetUsersByEmail(ctx context.Context, email string) ([]*types.GettableUser, error) // public function
|
GetUsersByEmail(ctx context.Context, email string) ([]*types.GettableUser, error) // public function
|
||||||
GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*types.GettableUser, error)
|
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)
|
PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (string, error)
|
||||||
CanUsePassword(ctx context.Context, email string) (bool, 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
|
// Auth Domain
|
||||||
GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error)
|
GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error)
|
||||||
GetDomainFromSsoResponse(ctx context.Context, url *url.URL) (*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
|
RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error
|
||||||
GetAPIKey(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableAPIKeyUser, 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
|
statsreporter.StatsCollector
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2065,7 +2065,8 @@ func (aH *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 {
|
if errv2 != nil {
|
||||||
render.Error(w, errv2)
|
render.Error(w, errv2)
|
||||||
return
|
return
|
||||||
|
|||||||
210
pkg/types/factor_password.go
Normal file
210
pkg/types/factor_password.go
Normal 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
|
||||||
|
}
|
||||||
14
pkg/types/factor_password_test.go
Normal file
14
pkg/types/factor_password_test.go
Normal 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())
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -2,15 +2,14 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -24,59 +23,6 @@ var (
|
|||||||
ErrAPIKeyNotFound = errors.MustNewCode("api_key_not_found")
|
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 {
|
type GettableUser struct {
|
||||||
User
|
User
|
||||||
Organization string `json:"organization"`
|
Organization string `json:"organization"`
|
||||||
@ -121,12 +67,12 @@ func NewUser(displayName string, email string, role string, orgID string) (*User
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PostableRegisterOrgAndAdmin struct {
|
type PostableRegisterOrgAndAdmin struct {
|
||||||
PostableAcceptInvite
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
OrgID string `json:"orgId"`
|
OrgID string `json:"orgId"`
|
||||||
OrgDisplayName string `json:"orgDisplayName"`
|
OrgDisplayName string `json:"orgDisplayName"`
|
||||||
OrgName string `json:"orgName"`
|
OrgName string `json:"orgName"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostableAcceptInvite struct {
|
type PostableAcceptInvite struct {
|
||||||
@ -138,95 +84,6 @@ type PostableAcceptInvite struct {
|
|||||||
SourceURL string `json:"sourceUrl"`
|
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 {
|
type PostableLoginRequest struct {
|
||||||
OrgID string `json:"orgId"`
|
OrgID string `json:"orgId"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@ -265,3 +122,97 @@ func NewTraitsFromUser(user *User) map[string]any {
|
|||||||
"created_at": user.CreatedAt,
|
"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
|
||||||
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from fixtures import dev, types
|
|||||||
|
|
||||||
USER_ADMIN_NAME = "admin"
|
USER_ADMIN_NAME = "admin"
|
||||||
USER_ADMIN_EMAIL = "admin@integration.test"
|
USER_ADMIN_EMAIL = "admin@integration.test"
|
||||||
USER_ADMIN_PASSWORD = "password"
|
USER_ADMIN_PASSWORD = "password123Z$"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="create_user_admin", scope="package")
|
@pytest.fixture(name="create_user_admin", scope="package")
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import docker
|
import docker
|
||||||
import docker.errors
|
import docker.errors
|
||||||
import psycopg2
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from sqlalchemy import create_engine, sql
|
||||||
from testcontainers.core.container import Network
|
from testcontainers.core.container import Network
|
||||||
from testcontainers.postgres import PostgresContainer
|
from testcontainers.postgres import PostgresContainer
|
||||||
|
|
||||||
@ -33,14 +33,14 @@ def postgres(
|
|||||||
)
|
)
|
||||||
container.start()
|
container.start()
|
||||||
|
|
||||||
connection = psycopg2.connect(
|
engine = create_engine(
|
||||||
dbname=container.dbname,
|
f"postgresql+psycopg2://{container.username}:{container.password}@{container.get_container_host_ip()}:{container.get_exposed_port(5432)}/{container.dbname}"
|
||||||
user=container.username,
|
|
||||||
password=container.password,
|
|
||||||
host=container.get_container_host_ip(),
|
|
||||||
port=container.get_exposed_port(5432),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with engine.connect() as conn:
|
||||||
|
result = conn.execute(sql.text("SELECT 1"))
|
||||||
|
assert result.fetchone()[0] == 1
|
||||||
|
|
||||||
return types.TestContainerSQL(
|
return types.TestContainerSQL(
|
||||||
container=types.TestContainerDocker(
|
container=types.TestContainerDocker(
|
||||||
id=container.get_wrapped_container().id,
|
id=container.get_wrapped_container().id,
|
||||||
@ -57,7 +57,7 @@ def postgres(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
conn=connection,
|
conn=engine,
|
||||||
env={
|
env={
|
||||||
"SIGNOZ_SQLSTORE_PROVIDER": "postgres",
|
"SIGNOZ_SQLSTORE_PROVIDER": "postgres",
|
||||||
"SIGNOZ_SQLSTORE_POSTGRES_DSN": f"postgresql://{container.username}:{container.password}@{container.get_wrapped_container().name}:{5432}/{container.dbname}",
|
"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"]
|
host_config = container.host_configs["5432"]
|
||||||
env = cache["env"]
|
env = cache["env"]
|
||||||
|
|
||||||
connection = psycopg2.connect(
|
engine = create_engine(
|
||||||
dbname=env["SIGNOZ_SQLSTORE_POSTGRES_DBNAME"],
|
f"postgresql+psycopg2://{env['SIGNOZ_SQLSTORE_POSTGRES_USER']}:{env['SIGNOZ_SQLSTORE_POSTGRES_PASSWORD']}@{host_config.address}:{host_config.port}/{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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with engine.connect() as conn:
|
||||||
|
result = conn.execute(sql.text("SELECT 1"))
|
||||||
|
assert result.fetchone()[0] == 1
|
||||||
|
|
||||||
return types.TestContainerSQL(
|
return types.TestContainerSQL(
|
||||||
container=container,
|
container=container,
|
||||||
conn=connection,
|
conn=engine,
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import sqlite3
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import Any, Generator
|
from typing import Any, Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from sqlalchemy import create_engine, sql
|
||||||
|
|
||||||
from fixtures import dev, types
|
from fixtures import dev, types
|
||||||
|
|
||||||
@ -22,7 +22,11 @@ def sqlite(
|
|||||||
def create() -> types.TestContainerSQL:
|
def create() -> types.TestContainerSQL:
|
||||||
tmpdir = tmpfs("sqlite")
|
tmpdir = tmpfs("sqlite")
|
||||||
path = tmpdir / "signoz.db"
|
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(
|
return types.TestContainerSQL(
|
||||||
container=types.TestContainerDocker(
|
container=types.TestContainerDocker(
|
||||||
@ -30,7 +34,7 @@ def sqlite(
|
|||||||
host_configs={},
|
host_configs={},
|
||||||
container_configs={},
|
container_configs={},
|
||||||
),
|
),
|
||||||
conn=connection,
|
conn=engine,
|
||||||
env={
|
env={
|
||||||
"SIGNOZ_SQLSTORE_PROVIDER": "sqlite",
|
"SIGNOZ_SQLSTORE_PROVIDER": "sqlite",
|
||||||
"SIGNOZ_SQLSTORE_SQLITE_PATH": str(path),
|
"SIGNOZ_SQLSTORE_SQLITE_PATH": str(path),
|
||||||
@ -42,7 +46,12 @@ def sqlite(
|
|||||||
|
|
||||||
def restore(cache: dict) -> types.TestContainerSQL:
|
def restore(cache: dict) -> types.TestContainerSQL:
|
||||||
path = cache["env"].get("SIGNOZ_SQLSTORE_SQLITE_PATH")
|
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(
|
return types.TestContainerSQL(
|
||||||
container=types.TestContainerDocker(
|
container=types.TestContainerDocker(
|
||||||
id="",
|
id="",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import clickhouse_connect
|
|||||||
import clickhouse_connect.driver
|
import clickhouse_connect.driver
|
||||||
import clickhouse_connect.driver.client
|
import clickhouse_connect.driver.client
|
||||||
import py
|
import py
|
||||||
|
from sqlalchemy import Engine
|
||||||
from testcontainers.core.container import Network
|
from testcontainers.core.container import Network
|
||||||
|
|
||||||
LegacyPath = py.path.local
|
LegacyPath = py.path.local
|
||||||
@ -76,7 +77,7 @@ class TestContainerDocker:
|
|||||||
class TestContainerSQL:
|
class TestContainerSQL:
|
||||||
__test__ = False
|
__test__ = False
|
||||||
container: TestContainerDocker
|
container: TestContainerDocker
|
||||||
conn: any
|
conn: Engine
|
||||||
env: Dict[str, str]
|
env: Dict[str, str]
|
||||||
|
|
||||||
def __cache__(self) -> dict:
|
def __cache__(self) -> dict:
|
||||||
|
|||||||
171
tests/integration/poetry.lock
generated
171
tests/integration/poetry.lock
generated
@ -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]]
|
[[package]]
|
||||||
name = "astroid"
|
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-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{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]]
|
[[package]]
|
||||||
name = "dill"
|
name = "dill"
|
||||||
@ -431,6 +431,75 @@ docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"]
|
|||||||
ssh = ["paramiko (>=2.4.3)"]
|
ssh = ["paramiko (>=2.4.3)"]
|
||||||
websockets = ["websocket-client (>=1.3.0)"]
|
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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.10"
|
version = "3.10"
|
||||||
@ -889,6 +958,102 @@ urllib3 = ">=1.21.1,<3"
|
|||||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
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]]
|
[[package]]
|
||||||
name = "svix-ksuid"
|
name = "svix-ksuid"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@ -1223,4 +1388,4 @@ cffi = ["cffi (>=1.11)"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.13"
|
python-versions = "^3.13"
|
||||||
content-hash = "200a3892f48467b2639abfae99b94b6de6b75fe09f9669ea115eb2b55a2f46ea"
|
content-hash = "fc17158ab90e70dbd94668e3346d6126384cb17cf28c3b3ec82e5ed067058380"
|
||||||
|
|||||||
@ -15,6 +15,7 @@ numpy = "^2.3.2"
|
|||||||
clickhouse-connect = "^0.8.18"
|
clickhouse-connect = "^0.8.18"
|
||||||
svix-ksuid = "^0.6.2"
|
svix-ksuid = "^0.6.2"
|
||||||
requests = "^2.32.4"
|
requests = "^2.32.4"
|
||||||
|
sqlalchemy = "^2.0.43"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|||||||
@ -8,6 +8,22 @@ from fixtures.logger import setup_logger
|
|||||||
logger = setup_logger(__name__)
|
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:
|
def test_register(signoz: types.SigNoz, get_jwt_token) -> None:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/version"), timeout=2
|
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": "",
|
"orgId": "",
|
||||||
"orgName": "integration.test",
|
"orgName": "integration.test",
|
||||||
"email": "admin@integration.test",
|
"email": "admin@integration.test",
|
||||||
"password": "password",
|
"password": "password123Z$",
|
||||||
},
|
},
|
||||||
timeout=2,
|
timeout=2,
|
||||||
)
|
)
|
||||||
@ -36,7 +52,7 @@ def test_register(signoz: types.SigNoz, get_jwt_token) -> None:
|
|||||||
assert response.status_code == HTTPStatus.OK
|
assert response.status_code == HTTPStatus.OK
|
||||||
assert response.json()["setupCompleted"] is True
|
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(
|
response = requests.get(
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
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"},
|
json={"email": "editor@integration.test", "role": "EDITOR"},
|
||||||
timeout=2,
|
timeout=2,
|
||||||
headers={
|
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"),
|
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||||
timeout=2,
|
timeout=2,
|
||||||
headers={
|
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(
|
response = requests.post(
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
|
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
|
||||||
json={
|
json={
|
||||||
"password": "password",
|
"password": "password123Z$",
|
||||||
"displayName": "editor",
|
"displayName": "editor",
|
||||||
"token": f"{found_invite['token']}",
|
"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"),
|
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||||
timeout=2,
|
timeout=2,
|
||||||
headers={
|
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"),
|
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||||
timeout=2,
|
timeout=2,
|
||||||
headers={
|
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:
|
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
|
# Generate an invite token for the viewer user
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
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"),
|
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||||
timeout=2,
|
timeout=2,
|
||||||
headers={
|
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(
|
response = requests.post(
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
|
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
|
||||||
json={
|
json={
|
||||||
"password": "password",
|
"password": "password123Z$",
|
||||||
"displayName": "viewer",
|
"displayName": "viewer",
|
||||||
"token": f"{found_invite["token"]}",
|
"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:
|
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(
|
response = requests.get(
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import http
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from sqlalchemy import sql
|
||||||
from wiremock.client import (
|
from wiremock.client import (
|
||||||
HttpMethods,
|
HttpMethods,
|
||||||
Mapping,
|
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(
|
response = requests.post(
|
||||||
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
|
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(
|
response = requests.put(
|
||||||
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
|
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
|
assert response.status_code == http.HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
cursor = signoz.sqlstore.conn.cursor()
|
with signoz.sqlstore.conn.connect() as conn:
|
||||||
cursor.execute(
|
result = conn.execute(
|
||||||
"SELECT data FROM license WHERE id='0196360e-90cd-7a74-8313-1aa815ce2a67'"
|
sql.text("SELECT data FROM license WHERE id=:id"),
|
||||||
)
|
{"id": "0196360e-90cd-7a74-8313-1aa815ce2a67"},
|
||||||
record = cursor.fetchone()[0]
|
)
|
||||||
assert json.loads(record)["valid_from"] == 1732146922
|
record = result.fetchone()[0]
|
||||||
|
assert json.loads(record)["valid_from"] == 1732146922
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url=signoz.zeus.host_configs["8080"].get("/__admin/requests/count"),
|
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(
|
response = requests.post(
|
||||||
url=signoz.self.host_configs["8080"].get("/api/v1/checkout"),
|
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(
|
response = requests.post(
|
||||||
url=signoz.self.host_configs["8080"].get("/api/v1/portal"),
|
url=signoz.self.host_configs["8080"].get("/api/v1/portal"),
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from fixtures import types
|
|||||||
|
|
||||||
|
|
||||||
def test_api_key(signoz: types.SigNoz, get_jwt_token) -> None:
|
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(
|
response = requests.post(
|
||||||
signoz.self.host_configs["8080"].get("/api/v1/pats"),
|
signoz.self.host_configs["8080"].get("/api/v1/pats"),
|
||||||
|
|||||||
235
tests/integration/src/auth/d_password.py
Normal file
235
tests/integration/src/auth/d_password.py
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user