mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-06 17:52:18 +00:00
Compare commits
11 Commits
main
...
feat/root-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
172e866a23 | ||
|
|
56b6f1d32c | ||
|
|
cdf8bfd155 | ||
|
|
7f5462f4a0 | ||
|
|
57200c6a9c | ||
|
|
613b2a9b8f | ||
|
|
db2ff8f639 | ||
|
|
d86de3d59a | ||
|
|
18a40f341e | ||
|
|
190b1d6d39 | ||
|
|
700a50f5ee |
4
Makefile
4
Makefile
@@ -108,6 +108,8 @@ go-run-community: ## Runs the community go backend server
|
||||
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
|
||||
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
|
||||
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER=cluster \
|
||||
SIGNOZ_USER_ROOT_EMAIL=root@example.com \
|
||||
SIGNOZ_USER_ROOT_PASSWORD=Str0ngP@ssw0rd! \
|
||||
go run -race \
|
||||
$(GO_BUILD_CONTEXT_COMMUNITY)/*.go server
|
||||
|
||||
@@ -238,4 +240,4 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
|
||||
.PHONY: gen-mocks
|
||||
gen-mocks:
|
||||
@echo ">> Generating mocks"
|
||||
@mockery --config .mockery.yml
|
||||
@mockery --config .mockery.yml
|
||||
|
||||
@@ -300,3 +300,8 @@ user:
|
||||
allow_self: true
|
||||
# The duration within which a user can reset their password.
|
||||
max_token_lifetime: 6h
|
||||
root:
|
||||
# The email of the root user.
|
||||
email: root@example.com
|
||||
# The password of the root user.
|
||||
password: Str0ngP@ssw0rd!
|
||||
|
||||
72
pkg/modules/rootuser/implrootuser/module.go
Normal file
72
pkg/modules/rootuser/implrootuser/module.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package implrootuser
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/role"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rootuser"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/roletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store types.RootUserStore
|
||||
settings factory.ScopedProviderSettings
|
||||
config user.RootUserConfig
|
||||
granter role.Granter
|
||||
}
|
||||
|
||||
func NewModule(store types.RootUserStore, providerSettings factory.ProviderSettings, config user.RootUserConfig, granter role.Granter) rootuser.Module {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/rootuser/implrootuser")
|
||||
return &module{
|
||||
store: store,
|
||||
settings: settings,
|
||||
config: config,
|
||||
granter: granter,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *module) Authenticate(ctx context.Context, orgID valuer.UUID, email valuer.Email, password string) (*authtypes.Identity, error) {
|
||||
// get the root user by email and org id
|
||||
rootUser, err := m.store.GetByEmailAndOrgID(ctx, orgID, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// verify the password
|
||||
if !rootUser.VerifyPassword(password) {
|
||||
return nil, errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "invalid email or password")
|
||||
}
|
||||
|
||||
// create a root user identity
|
||||
identity := authtypes.NewRootIdentity(rootUser.ID, orgID, rootUser.Email)
|
||||
|
||||
// make sure the returning identity has admin role
|
||||
err = m.granter.Grant(ctx, orgID, roletypes.SigNozAdminRoleName, authtypes.MustNewSubject(authtypes.TypeableUser, rootUser.ID.StringValue(), rootUser.OrgID, nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
func (m *module) ExistsByOrgID(ctx context.Context, orgID valuer.UUID) (bool, error) {
|
||||
return m.store.ExistsByOrgID(ctx, orgID)
|
||||
}
|
||||
|
||||
func (m *module) GetByEmailAndOrgID(ctx context.Context, orgID valuer.UUID, email valuer.Email) (*types.RootUser, error) {
|
||||
return m.store.GetByEmailAndOrgID(ctx, orgID, email)
|
||||
}
|
||||
|
||||
func (m *module) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.RootUser, error) {
|
||||
return m.store.GetByOrgIDAndID(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (m *module) GetByEmailAndOrgIDs(ctx context.Context, orgIDs []valuer.UUID, email valuer.Email) ([]*types.RootUser, error) {
|
||||
return m.store.GetByEmailAndOrgIDs(ctx, orgIDs, email)
|
||||
}
|
||||
135
pkg/modules/rootuser/implrootuser/reconciler.go
Normal file
135
pkg/modules/rootuser/implrootuser/reconciler.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package implrootuser
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rootuser"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type reconciler struct {
|
||||
store types.RootUserStore
|
||||
settings factory.ScopedProviderSettings
|
||||
orgGetter organization.Getter
|
||||
config user.RootUserConfig
|
||||
}
|
||||
|
||||
func NewReconciler(store types.RootUserStore, settings factory.ProviderSettings, orgGetter organization.Getter, config user.RootUserConfig) rootuser.Reconciler {
|
||||
scopedSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/rootuser/implrootuser/reconciler")
|
||||
|
||||
return &reconciler{
|
||||
store: store,
|
||||
settings: scopedSettings,
|
||||
orgGetter: orgGetter,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *reconciler) Reconcile(ctx context.Context) error {
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: reconciling root user(s)")
|
||||
if !r.config.IsConfigured() {
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: root user is not configured, skipping reconciliation")
|
||||
return nil
|
||||
}
|
||||
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: reconciling root user(s)")
|
||||
|
||||
// get the organizations that are owned by this instance of signoz
|
||||
orgs, err := r.orgGetter.ListByOwnedKeyRange(ctx)
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "failed to get list of organizations owned by this instance of signoz")
|
||||
}
|
||||
|
||||
if len(orgs) == 0 {
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: no organizations owned by this instance of signoz, skipping reconciliation")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, org := range orgs {
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: reconciling root user for organization", "organization_id", org.ID, "organization_name", org.Name)
|
||||
|
||||
err := r.reconcileRootUserForOrg(ctx, org)
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "reconciler: failed to reconcile root user for organization %s (%s)", org.Name, org.ID)
|
||||
}
|
||||
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: root user reconciled for organization", "organization_id", org.ID, "organization_name", org.Name)
|
||||
}
|
||||
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: reconciliation complete")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *reconciler) reconcileRootUserForOrg(ctx context.Context, org *types.Organization) error {
|
||||
// check if the root user already exists for the org
|
||||
existingRootUser, err := r.store.GetByOrgID(ctx, org.ID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if existingRootUser != nil {
|
||||
// make updates to the existing root user if needed
|
||||
return r.updateRootUserForOrg(ctx, org.ID, existingRootUser)
|
||||
}
|
||||
|
||||
// create a new root user
|
||||
return r.createRootUserForOrg(ctx, org.ID)
|
||||
}
|
||||
|
||||
func (r *reconciler) createRootUserForOrg(ctx context.Context, orgID valuer.UUID) error {
|
||||
rootUser, err := types.NewRootUser(
|
||||
valuer.MustNewEmail(r.config.Email),
|
||||
r.config.Password,
|
||||
orgID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: creating new root user for organization", "organization_id", orgID, "email", r.config.Email)
|
||||
|
||||
err = r.store.Create(ctx, rootUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: root user created for organization", "organization_id", orgID, "email", r.config.Email)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *reconciler) updateRootUserForOrg(ctx context.Context, orgID valuer.UUID, rootUser *types.RootUser) error {
|
||||
needsUpdate := false
|
||||
|
||||
if rootUser.Email != valuer.MustNewEmail(r.config.Email) {
|
||||
rootUser.Email = valuer.MustNewEmail(r.config.Email)
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if !rootUser.VerifyPassword(r.config.Password) {
|
||||
passwordHash, err := types.NewHashedPassword(r.config.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootUser.PasswordHash = passwordHash
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: updating root user for organization", "organization_id", orgID, "email", r.config.Email)
|
||||
err := r.store.Update(ctx, orgID, rootUser.ID, rootUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.settings.Logger().InfoContext(ctx, "reconciler: root user updated for organization", "organization_id", orgID, "email", r.config.Email)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
126
pkg/modules/rootuser/implrootuser/store.go
Normal file
126
pkg/modules/rootuser/implrootuser/store.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package implrootuser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type store struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
settings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewStore(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings) types.RootUserStore {
|
||||
return &store{
|
||||
sqlstore: sqlstore,
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
func (store *store) Create(ctx context.Context, rootUser *types.RootUser) error {
|
||||
_, err := store.sqlstore.BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(rootUser).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrCodeRootUserAlreadyExists, "root user with email %s already exists in org %s", rootUser.Email, rootUser.OrgID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) GetByOrgID(ctx context.Context, orgID valuer.UUID) (*types.RootUser, error) {
|
||||
rootUser := new(types.RootUser)
|
||||
|
||||
err := store.sqlstore.BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(rootUser).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeRootUserNotFound, "root user with org_id %s does not exist", orgID)
|
||||
}
|
||||
return rootUser, nil
|
||||
}
|
||||
|
||||
func (store *store) GetByEmailAndOrgID(ctx context.Context, orgID valuer.UUID, email valuer.Email) (*types.RootUser, error) {
|
||||
rootUser := new(types.RootUser)
|
||||
|
||||
err := store.sqlstore.BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(rootUser).
|
||||
Where("email = ?", email).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeRootUserNotFound, "root user with email %s does not exist in org %s", email, orgID)
|
||||
}
|
||||
return rootUser, nil
|
||||
}
|
||||
|
||||
func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.RootUser, error) {
|
||||
rootUser := new(types.RootUser)
|
||||
|
||||
err := store.sqlstore.BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(rootUser).
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeRootUserNotFound, "root user with id %s does not exist in org %s", id, orgID)
|
||||
}
|
||||
return rootUser, nil
|
||||
}
|
||||
|
||||
func (store *store) GetByEmailAndOrgIDs(ctx context.Context, orgIDs []valuer.UUID, email valuer.Email) ([]*types.RootUser, error) {
|
||||
rootUsers := []*types.RootUser{}
|
||||
|
||||
err := store.sqlstore.BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(&rootUsers).
|
||||
Where("email = ?", email).
|
||||
Where("org_id IN (?)", bun.In(orgIDs)).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeRootUserNotFound, "root user with email %s does not exist in orgs %s", email, orgIDs)
|
||||
}
|
||||
|
||||
return rootUsers, nil
|
||||
}
|
||||
|
||||
func (store *store) ExistsByOrgID(ctx context.Context, orgID valuer.UUID) (bool, error) {
|
||||
exists, err := store.sqlstore.BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(new(types.RootUser)).
|
||||
Where("org_id = ?", orgID).
|
||||
Exists(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (store *store) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, rootUser *types.RootUser) error {
|
||||
rootUser.UpdatedAt = time.Now()
|
||||
_, err := store.sqlstore.BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
Model(rootUser).
|
||||
Column("email").
|
||||
Column("password_hash").
|
||||
Column("updated_at").
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeRootUserNotFound, "root user with id %s does not exist", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
31
pkg/modules/rootuser/rootuser.go
Normal file
31
pkg/modules/rootuser/rootuser.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package rootuser
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
// Authenticate a root user by email and password
|
||||
Authenticate(ctx context.Context, orgID valuer.UUID, email valuer.Email, password string) (*authtypes.Identity, error)
|
||||
|
||||
// Get the root user by email and orgID.
|
||||
GetByEmailAndOrgID(ctx context.Context, orgID valuer.UUID, email valuer.Email) (*types.RootUser, error)
|
||||
|
||||
// Get the root user by orgID and ID.
|
||||
GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.RootUser, error)
|
||||
|
||||
// Get the root users by email and org IDs.
|
||||
GetByEmailAndOrgIDs(ctx context.Context, orgIDs []valuer.UUID, email valuer.Email) ([]*types.RootUser, error)
|
||||
|
||||
// Checks if a root user exists for an organization
|
||||
ExistsByOrgID(ctx context.Context, orgID valuer.UUID) (bool, error)
|
||||
}
|
||||
|
||||
type Reconciler interface {
|
||||
// Reconcile the root users.
|
||||
Reconcile(ctx context.Context) error
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rootuser"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/tokenizer"
|
||||
@@ -21,24 +22,26 @@ import (
|
||||
)
|
||||
|
||||
type module struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
authNs map[authtypes.AuthNProvider]authn.AuthN
|
||||
user user.Module
|
||||
userGetter user.Getter
|
||||
authDomain authdomain.Module
|
||||
tokenizer tokenizer.Tokenizer
|
||||
orgGetter organization.Getter
|
||||
settings factory.ScopedProviderSettings
|
||||
authNs map[authtypes.AuthNProvider]authn.AuthN
|
||||
user user.Module
|
||||
userGetter user.Getter
|
||||
authDomain authdomain.Module
|
||||
tokenizer tokenizer.Tokenizer
|
||||
orgGetter organization.Getter
|
||||
rootUserModule rootuser.Module
|
||||
}
|
||||
|
||||
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, user user.Module, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter) session.Module {
|
||||
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, user user.Module, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter, rootUserModule rootuser.Module) session.Module {
|
||||
return &module{
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
|
||||
authNs: authNs,
|
||||
user: user,
|
||||
userGetter: userGetter,
|
||||
authDomain: authDomain,
|
||||
tokenizer: tokenizer,
|
||||
orgGetter: orgGetter,
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
|
||||
authNs: authNs,
|
||||
user: user,
|
||||
userGetter: userGetter,
|
||||
authDomain: authDomain,
|
||||
tokenizer: tokenizer,
|
||||
orgGetter: orgGetter,
|
||||
rootUserModule: rootUserModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +63,19 @@ func (module *module) GetSessionContext(ctx context.Context, email valuer.Email,
|
||||
orgIDs = append(orgIDs, org.ID)
|
||||
}
|
||||
|
||||
// ROOT USER
|
||||
// if this email is a root user email, we will only allow password authentication
|
||||
if module.rootUserModule != nil {
|
||||
rootUserContexts, err := module.getRootUserSessionContext(ctx, orgs, orgIDs, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rootUserContexts.Exists {
|
||||
return rootUserContexts, nil
|
||||
}
|
||||
}
|
||||
|
||||
// REGULAR USER
|
||||
users, err := module.userGetter.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -108,6 +124,22 @@ func (module *module) GetSessionContext(ctx context.Context, email valuer.Email,
|
||||
}
|
||||
|
||||
func (module *module) CreatePasswordAuthNSession(ctx context.Context, authNProvider authtypes.AuthNProvider, email valuer.Email, password string, orgID valuer.UUID) (*authtypes.Token, error) {
|
||||
// Root User Authentication
|
||||
if module.rootUserModule != nil {
|
||||
// Ignore root user authentication errors and continue with regular user authentication.
|
||||
// This error can be either not found or incorrect password, in both cases we continue with regular user authentication.
|
||||
identity, err := module.rootUserModule.Authenticate(ctx, orgID, email, password)
|
||||
if err != nil && !errors.Asc(err, types.ErrCodeRootUserNotFound) && !errors.Ast(err, errors.TypeUnauthenticated) {
|
||||
// something else went wrong, we should report back to the caller
|
||||
return nil, err
|
||||
}
|
||||
if identity != nil {
|
||||
// root user authentication successful
|
||||
return module.tokenizer.CreateToken(ctx, identity, map[string]string{})
|
||||
}
|
||||
}
|
||||
|
||||
// Regular User Authentication
|
||||
passwordAuthN, err := getProvider[authn.PasswordAuthN](authNProvider, module.authNs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -215,3 +247,32 @@ func getProvider[T authn.AuthN](authNProvider authtypes.AuthNProvider, authNs ma
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (module *module) getRootUserSessionContext(ctx context.Context, orgs []*types.Organization, orgIDs []valuer.UUID, email valuer.Email) (*authtypes.SessionContext, error) {
|
||||
context := authtypes.NewSessionContext()
|
||||
|
||||
rootUsers, err := module.rootUserModule.GetByEmailAndOrgIDs(ctx, orgIDs, email)
|
||||
if err != nil && !errors.Asc(err, types.ErrCodeRootUserNotFound) {
|
||||
// something else went wrong, report back to the caller
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rootUser := range rootUsers {
|
||||
idx := slices.IndexFunc(orgs, func(org *types.Organization) bool {
|
||||
return org.ID == rootUser.OrgID
|
||||
})
|
||||
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
org := orgs[idx]
|
||||
if rootUser != nil {
|
||||
context.Exists = true
|
||||
orgContext := authtypes.NewOrgSessionContext(org.ID, org.Name).AddPasswordAuthNSupport(authtypes.AuthNProviderEmailPassword)
|
||||
context = context.AddOrgContext(orgContext)
|
||||
}
|
||||
}
|
||||
|
||||
return context, nil
|
||||
}
|
||||
|
||||
@@ -5,15 +5,26 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var (
|
||||
minRootUserPasswordLength = 12
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Password PasswordConfig `mapstructure:"password"`
|
||||
Password PasswordConfig `mapstructure:"password"`
|
||||
RootUserConfig RootUserConfig `mapstructure:"root"`
|
||||
}
|
||||
type PasswordConfig struct {
|
||||
Reset ResetConfig `mapstructure:"reset"`
|
||||
}
|
||||
|
||||
type RootUserConfig struct {
|
||||
Email string `mapstructure:"email"`
|
||||
Password string `mapstructure:"password"`
|
||||
}
|
||||
|
||||
type ResetConfig struct {
|
||||
AllowSelf bool `mapstructure:"allow_self"`
|
||||
MaxTokenLifetime time.Duration `mapstructure:"max_token_lifetime"`
|
||||
@@ -31,6 +42,10 @@ func newConfig() factory.Config {
|
||||
MaxTokenLifetime: 6 * time.Hour,
|
||||
},
|
||||
},
|
||||
RootUserConfig: RootUserConfig{
|
||||
Email: "",
|
||||
Password: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,5 +54,22 @@ func (c Config) Validate() error {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::password::reset::max_token_lifetime must be positive")
|
||||
}
|
||||
|
||||
if c.RootUserConfig.Email != "" {
|
||||
_, err := valuer.NewEmail(c.RootUserConfig.Email)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to validate user::root::email %s", c.RootUserConfig.Email)
|
||||
}
|
||||
}
|
||||
|
||||
if c.RootUserConfig.Password != "" {
|
||||
if len(c.RootUserConfig.Password) < minRootUserPasswordLength {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::root::password must be at least %d characters long", minRootUserPasswordLength)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r RootUserConfig) IsConfigured() bool {
|
||||
return r.Email != "" && r.Password != ""
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rootuser"
|
||||
root "github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
@@ -18,12 +19,13 @@ import (
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
module root.Module
|
||||
getter root.Getter
|
||||
module root.Module
|
||||
getter root.Getter
|
||||
rootUserModule rootuser.Module
|
||||
}
|
||||
|
||||
func NewHandler(module root.Module, getter root.Getter) root.Handler {
|
||||
return &handler{module: module, getter: getter}
|
||||
func NewHandler(module root.Module, getter root.Getter, rootUserModule rootuser.Module) root.Handler {
|
||||
return &handler{module: module, getter: getter, rootUserModule: rootUserModule}
|
||||
}
|
||||
|
||||
func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -61,6 +63,23 @@ func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// ROOT USER CHECK - START
|
||||
// if the to-be-invited email is one of the root users, we forbid this operation
|
||||
if h.rootUserModule != nil {
|
||||
rootUser, err := h.rootUserModule.GetByEmailAndOrgID(ctx, valuer.MustNewUUID(claims.OrgID), req.Email)
|
||||
if err != nil && !errors.Asc(err, types.ErrCodeRootUserNotFound) {
|
||||
// something else went wrong, report back to UI
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if rootUser != nil {
|
||||
render.Error(rw, errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot invite this email id"))
|
||||
return
|
||||
}
|
||||
}
|
||||
// ROOT USER CHECK - END
|
||||
|
||||
invites, err := h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &types.PostableBulkInviteRequest{
|
||||
Invites: []types.PostableInvite{req},
|
||||
})
|
||||
@@ -94,6 +113,25 @@ func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// ROOT USER CHECK - START
|
||||
// if the to-be-invited email is one of the root users, we forbid this operation
|
||||
if h.rootUserModule != nil {
|
||||
for _, invite := range req.Invites {
|
||||
rootUser, err := h.rootUserModule.GetByEmailAndOrgID(ctx, valuer.MustNewUUID(claims.OrgID), invite.Email)
|
||||
if err != nil && !errors.Asc(err, types.ErrCodeRootUserNotFound) {
|
||||
// something else went wrong, report back to UI
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if rootUser != nil {
|
||||
render.Error(rw, errors.New(errors.TypeForbidden, errors.CodeForbidden, "reserved email(s) found, failed to invite users"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// ROOT USER CHECK - END
|
||||
|
||||
_, err = h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
@@ -192,6 +230,37 @@ func (h *handler) GetMyUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// ROOT USER
|
||||
if h.rootUserModule != nil {
|
||||
rootUser, err := h.rootUserModule.GetByEmailAndOrgID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewEmail(claims.Email))
|
||||
if err != nil && !errors.Asc(err, types.ErrCodeRootUserNotFound) {
|
||||
// something else is wrong report back in UI
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if rootUser != nil {
|
||||
// root user detected
|
||||
rUser := types.User{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: rootUser.ID,
|
||||
},
|
||||
DisplayName: "Root User",
|
||||
Email: rootUser.Email,
|
||||
Role: types.RoleAdmin,
|
||||
OrgID: rootUser.OrgID,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: rootUser.CreatedAt,
|
||||
UpdatedAt: rootUser.UpdatedAt,
|
||||
},
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, rUser)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// NORMAL USER
|
||||
user, err := h.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID))
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
@@ -259,6 +328,11 @@ func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if claims.UserID == id {
|
||||
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "huh! seriously? why are you trying to delete yourself?"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.module.DeleteUser(ctx, valuer.MustNewUUID(claims.OrgID), id, claims.UserID); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
|
||||
"github.com/SigNoz/signoz/pkg/modules/role"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rootuser"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rootuser/implrootuser"
|
||||
"github.com/SigNoz/signoz/pkg/modules/savedview"
|
||||
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
|
||||
"github.com/SigNoz/signoz/pkg/modules/services"
|
||||
@@ -70,6 +72,7 @@ type Modules struct {
|
||||
RoleSetter role.Setter
|
||||
RoleGetter role.Getter
|
||||
Granter role.Granter
|
||||
RootUser rootuser.Module
|
||||
}
|
||||
|
||||
func NewModules(
|
||||
@@ -99,6 +102,8 @@ func NewModules(
|
||||
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
|
||||
rootUser := implrootuser.NewModule(implrootuser.NewStore(sqlstore, providerSettings), providerSettings, config.User.RootUserConfig, granter)
|
||||
|
||||
return Modules{
|
||||
OrgGetter: orgGetter,
|
||||
OrgSetter: orgSetter,
|
||||
@@ -112,7 +117,7 @@ func NewModules(
|
||||
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
||||
RawDataExport: implrawdataexport.NewModule(querier),
|
||||
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
|
||||
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
|
||||
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter, rootUser),
|
||||
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
||||
Services: implservices.NewModule(querier, telemetryStore),
|
||||
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
|
||||
@@ -120,5 +125,6 @@ func NewModules(
|
||||
RoleSetter: roleSetter,
|
||||
RoleGetter: roleGetter,
|
||||
Granter: granter,
|
||||
RootUser: rootUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddAuthzIndexFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateRbacToAuthzFactory(sqlstore),
|
||||
sqlmigration.NewMigratePublicDashboardsFactory(sqlstore),
|
||||
sqlmigration.NewAddRootUserFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -236,7 +237,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
orgGetter,
|
||||
authz,
|
||||
implorganization.NewHandler(modules.OrgGetter, modules.OrgSetter),
|
||||
impluser.NewHandler(modules.User, modules.UserGetter),
|
||||
impluser.NewHandler(modules.User, modules.UserGetter, modules.RootUser),
|
||||
implsession.NewHandler(modules.Session),
|
||||
implauthdomain.NewHandler(modules.AuthDomain),
|
||||
implpreference.NewHandler(modules.Preference),
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/role"
|
||||
"github.com/SigNoz/signoz/pkg/modules/role/implrole"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rootuser/implrootuser"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
@@ -449,6 +450,15 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize and run the root user reconciler
|
||||
rootUserStore := implrootuser.NewStore(sqlstore, providerSettings)
|
||||
rootUserReconciler := implrootuser.NewReconciler(rootUserStore, providerSettings, orgGetter, config.User.RootUserConfig)
|
||||
err = rootUserReconciler.Reconcile(ctx)
|
||||
if err != nil {
|
||||
// Question: Should we fail the startup if the root user reconciliation fails?
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SigNoz{
|
||||
Registry: registry,
|
||||
Analytics: analytics,
|
||||
|
||||
98
pkg/sqlmigration/063_add_root_user.go
Normal file
98
pkg/sqlmigration/063_add_root_user.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addRootUser struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAddRootUserFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("add_root_user"),
|
||||
func(ctx context.Context, settings factory.ProviderSettings, config Config) (SQLMigration, error) {
|
||||
return newAddRootUser(ctx, settings, config, sqlstore)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func newAddRootUser(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore) (SQLMigration, error) {
|
||||
return &addRootUser{sqlstore: sqlstore}, nil
|
||||
}
|
||||
|
||||
func (migration *addRootUser) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addRootUser) Up(ctx context.Context, db *bun.DB) error {
|
||||
// create root_use table
|
||||
if _, err := db.NewCreateTable().
|
||||
Model(
|
||||
&struct {
|
||||
bun.BaseModel `bun:"table:root_users"`
|
||||
ID string `bun:"id,pk,type:text"`
|
||||
Email string `bun:"email,type:text,notnull"`
|
||||
PasswordHash string `bun:"password_hash,type:text,notnull"`
|
||||
OrgID string `bun:"org_id,type:text,notnull"`
|
||||
CreatedAt int `bun:"created_at,type:bigint,notnull"`
|
||||
UpdatedAt int `bun:"updated_at,type:bigint,notnull"`
|
||||
}{}).
|
||||
ForeignKey(`("org_id") REFERENCES "organizations" ("id") ON DELETE CASCADE`).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create unique index on org_id to make sure one root user per org
|
||||
if _, err := db.NewCreateIndex().
|
||||
Model(
|
||||
&struct {
|
||||
bun.BaseModel `bun:"table:root_users"`
|
||||
}{}).
|
||||
Index("idx_root_user_org_id_unique").
|
||||
Column("org_id").
|
||||
Unique().
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create index on email for login lookups
|
||||
if _, err := db.NewCreateIndex().
|
||||
Model(
|
||||
&struct {
|
||||
bun.BaseModel `bun:"table:root_users"`
|
||||
}{}).
|
||||
Index("idx_root_user_email").
|
||||
Column("email").
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addRootUser) Down(ctx context.Context, db *bun.DB) error {
|
||||
// drop root_users table
|
||||
if _, err := db.NewDropTable().
|
||||
Model(
|
||||
&struct {
|
||||
bun.BaseModel `bun:"table:root_users"`
|
||||
}{}).
|
||||
IfExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package sqltokenizerstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -34,6 +35,7 @@ func (store *store) Create(ctx context.Context, token *authtypes.StorableToken)
|
||||
}
|
||||
|
||||
func (store *store) GetIdentityByUserID(ctx context.Context, userID valuer.UUID) (*authtypes.Identity, error) {
|
||||
// try to get the user from the user table - this will be most common case
|
||||
user := new(types.User)
|
||||
|
||||
err := store.
|
||||
@@ -43,11 +45,36 @@ func (store *store) GetIdentityByUserID(ctx context.Context, userID valuer.UUID)
|
||||
Model(user).
|
||||
Where("id = ?", userID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with id: %s does not exist", userID)
|
||||
// if err != nil {
|
||||
// return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with id: %s does not exist", userID)
|
||||
// }
|
||||
|
||||
if err == nil {
|
||||
// we found the user, return the identity
|
||||
return authtypes.NewIdentity(userID, user.OrgID, user.Email, types.Role(user.Role)), nil
|
||||
}
|
||||
|
||||
return authtypes.NewIdentity(userID, user.OrgID, user.Email, types.Role(user.Role)), nil
|
||||
if err != sql.ErrNoRows {
|
||||
// this is not a not found error, return the error, something else went wrong
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if the user not found, try to find that in root_user table
|
||||
rootUser := new(types.RootUser)
|
||||
|
||||
err = store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(rootUser).
|
||||
Where("id = ?", userID).
|
||||
Scan(ctx)
|
||||
|
||||
if err == nil {
|
||||
return authtypes.NewRootIdentity(userID, rootUser.OrgID, rootUser.Email), nil
|
||||
}
|
||||
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with id: %s does not exist", userID)
|
||||
}
|
||||
|
||||
func (store *store) GetByAccessToken(ctx context.Context, accessToken string) (*authtypes.StorableToken, error) {
|
||||
|
||||
@@ -29,6 +29,7 @@ type Identity struct {
|
||||
OrgID valuer.UUID `json:"orgId"`
|
||||
Email valuer.Email `json:"email"`
|
||||
Role types.Role `json:"role"`
|
||||
IsRoot bool `json:"isRoot"`
|
||||
}
|
||||
|
||||
type CallbackIdentity struct {
|
||||
@@ -84,6 +85,17 @@ func NewIdentity(userID valuer.UUID, orgID valuer.UUID, email valuer.Email, role
|
||||
OrgID: orgID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
IsRoot: false,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRootIdentity(userID valuer.UUID, orgID valuer.UUID, email valuer.Email) *Identity {
|
||||
return &Identity{
|
||||
UserID: userID,
|
||||
OrgID: orgID,
|
||||
Email: email,
|
||||
Role: types.RoleAdmin,
|
||||
IsRoot: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
73
pkg/types/rootuser.go
Normal file
73
pkg/types/rootuser.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeRootUserAlreadyExists = errors.MustNewCode("root_user_already_exists")
|
||||
ErrCodeRootUserNotFound = errors.MustNewCode("root_user_not_found")
|
||||
)
|
||||
|
||||
type RootUser struct {
|
||||
bun.BaseModel `bun:"table:root_users"`
|
||||
|
||||
Identifiable // gives ID field
|
||||
Email valuer.Email `bun:"email,type:text" json:"email"`
|
||||
PasswordHash string `bun:"password_hash,type:text" json:"-"`
|
||||
OrgID valuer.UUID `bun:"org_id,type:text" json:"orgId"`
|
||||
TimeAuditable // gives CreatedAt and UpdatedAt fields
|
||||
}
|
||||
|
||||
func NewRootUser(email valuer.Email, password string, orgID valuer.UUID) (*RootUser, error) {
|
||||
passwordHash, err := NewHashedPassword(password)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to generate password hash")
|
||||
}
|
||||
|
||||
return &RootUser{
|
||||
Identifiable: Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
Email: email,
|
||||
PasswordHash: string(passwordHash),
|
||||
OrgID: orgID,
|
||||
TimeAuditable: TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RootUser) VerifyPassword(password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(r.PasswordHash), []byte(password)) == nil
|
||||
}
|
||||
|
||||
type RootUserStore interface {
|
||||
// Creates a new root user. Returns ErrCodeRootUserAlreadyExists if a root user already exists for the organization.
|
||||
Create(ctx context.Context, rootUser *RootUser) error
|
||||
|
||||
// Gets the root user by organization ID. Returns ErrCodeRootUserNotFound if a root user does not exist for the organization.
|
||||
GetByOrgID(ctx context.Context, orgID valuer.UUID) (*RootUser, error)
|
||||
|
||||
// Gets a root user by email and organization ID. Returns ErrCodeRootUserNotFound if a root user does not exist for the organization.
|
||||
GetByEmailAndOrgID(ctx context.Context, orgID valuer.UUID, email valuer.Email) (*RootUser, error)
|
||||
|
||||
// Gets a root user by organization ID and ID. Returns ErrCodeRootUserNotFound if a root user does not exist for the organization.
|
||||
GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*RootUser, error)
|
||||
|
||||
// Gets all root users by email and organization IDs. Returns ErrCodeRootUserNotFound if a root user does not exist for the organization.
|
||||
GetByEmailAndOrgIDs(ctx context.Context, orgIDs []valuer.UUID, email valuer.Email) ([]*RootUser, error)
|
||||
|
||||
// Updates the password of a root user. Returns ErrCodeRootUserNotFound if a root user does not exist.
|
||||
Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, rootUser *RootUser) error
|
||||
|
||||
// Checks if a root user exists for an organization. Returns true if a root user exists for the organization, false otherwise.
|
||||
ExistsByOrgID(ctx context.Context, orgID valuer.UUID) (bool, error)
|
||||
}
|
||||
Reference in New Issue
Block a user