mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-26 02:12:34 +00:00
Compare commits
20 Commits
fix/remove
...
feat/root-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cf1384892 | ||
|
|
5aa655f875 | ||
|
|
ffde89d905 | ||
|
|
ff732c6d6c | ||
|
|
30d902f92d | ||
|
|
6a9a8b14b3 | ||
|
|
4e7d2f8fb8 | ||
|
|
bbc7f7bb3d | ||
|
|
c3294cf704 | ||
|
|
e94b2a66d7 | ||
|
|
ceca9fbd42 | ||
|
|
14cec7b465 | ||
|
|
04753e2d57 | ||
|
|
85ba9a6840 | ||
|
|
230b5ab7b7 | ||
|
|
93c7c7fc93 | ||
|
|
3011c24662 | ||
|
|
559631f217 | ||
|
|
80bcc8971f | ||
|
|
d8e3134729 |
2
Makefile
2
Makefile
@@ -238,4 +238,4 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
|
|||||||
.PHONY: gen-mocks
|
.PHONY: gen-mocks
|
||||||
gen-mocks:
|
gen-mocks:
|
||||||
@echo ">> Generating mocks"
|
@echo ">> Generating mocks"
|
||||||
@mockery --config .mockery.yml
|
@mockery --config .mockery.yml
|
||||||
|
|||||||
@@ -300,3 +300,8 @@ user:
|
|||||||
allow_self: true
|
allow_self: true
|
||||||
# The duration within which a user can reset their password.
|
# The duration within which a user can reset their password.
|
||||||
max_token_lifetime: 6h
|
max_token_lifetime: 6h
|
||||||
|
root:
|
||||||
|
# The email of the root user.
|
||||||
|
email: root@example.com
|
||||||
|
# The password of the root user.
|
||||||
|
password: Str0ngP@ssw0rd!
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func Error(rw http.ResponseWriter, cause error) {
|
|||||||
case errors.TypeUnauthenticated:
|
case errors.TypeUnauthenticated:
|
||||||
httpCode = http.StatusUnauthorized
|
httpCode = http.StatusUnauthorized
|
||||||
case errors.TypeUnsupported:
|
case errors.TypeUnsupported:
|
||||||
httpCode = http.StatusNotImplemented
|
httpCode = http.StatusUnprocessableEntity
|
||||||
case errors.TypeForbidden:
|
case errors.TypeForbidden:
|
||||||
httpCode = http.StatusForbidden
|
httpCode = http.StatusForbidden
|
||||||
case errors.TypeCanceled:
|
case errors.TypeCanceled:
|
||||||
|
|||||||
@@ -6,18 +6,20 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/rootuser"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type setter struct {
|
type setter struct {
|
||||||
store types.OrganizationStore
|
store types.OrganizationStore
|
||||||
alertmanager alertmanager.Alertmanager
|
alertmanager alertmanager.Alertmanager
|
||||||
quickfilter quickfilter.Module
|
quickfilter quickfilter.Module
|
||||||
|
rootUserReconciler rootuser.Reconciler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module) organization.Setter {
|
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module, rootUserReconciler rootuser.Reconciler) organization.Setter {
|
||||||
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter}
|
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter, rootUserReconciler: rootUserReconciler}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (module *setter) Create(ctx context.Context, organization *types.Organization, createManagedRoles func(context.Context, valuer.UUID) error) error {
|
func (module *setter) Create(ctx context.Context, organization *types.Organization, createManagedRoles func(context.Context, valuer.UUID) error) error {
|
||||||
@@ -37,6 +39,10 @@ func (module *setter) Create(ctx context.Context, organization *types.Organizati
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := module.rootUserReconciler.ReconcileForOrg(ctx, organization); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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/authz"
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"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
|
||||||
|
authz authz.AuthZ
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModule(store types.RootUserStore, providerSettings factory.ProviderSettings, config user.RootUserConfig, authz authz.AuthZ) rootuser.Module {
|
||||||
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/rootuser/implrootuser")
|
||||||
|
return &module{
|
||||||
|
store: store,
|
||||||
|
settings: settings,
|
||||||
|
config: config,
|
||||||
|
authz: authz,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.authz.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)
|
||||||
|
}
|
||||||
165
pkg/modules/rootuser/implrootuser/reconciler.go
Normal file
165
pkg/modules/rootuser/implrootuser/reconciler.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
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 {
|
||||||
|
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) ReconcileForOrg(ctx context.Context, org *types.Organization) error {
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reconciler) reconcileRootUserForOrg(ctx context.Context, org *types.Organization) error {
|
||||||
|
|
||||||
|
// try creating the user optimisitically
|
||||||
|
err := r.createRootUserForOrg(ctx, org.ID)
|
||||||
|
if err == nil {
|
||||||
|
// success - yay
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if error is not "alredy exists", something really went wrong
|
||||||
|
if !errors.Asc(err, types.ErrCodeRootUserAlreadyExists) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// here means the root user already exists - just make sure it is configured correctly
|
||||||
|
// this could be the case where the root user was created by some other means
|
||||||
|
// either previously or by some other instance of signoz
|
||||||
|
r.settings.Logger().InfoContext(ctx, "reconciler: root user already exists for organization", "organization_id", org.ID, "organization_name", org.Name)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// make updates to the existing root user if needed
|
||||||
|
return r.updateRootUserForOrg(ctx, org.ID, existingRootUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
34
pkg/modules/rootuser/rootuser.go
Normal file
34
pkg/modules/rootuser/rootuser.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
// Reconcile the root user for the given org.
|
||||||
|
ReconcileForOrg(ctx context.Context, org *types.Organization) error
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"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/session"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
"github.com/SigNoz/signoz/pkg/tokenizer"
|
"github.com/SigNoz/signoz/pkg/tokenizer"
|
||||||
@@ -21,24 +22,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type module struct {
|
type module struct {
|
||||||
settings factory.ScopedProviderSettings
|
settings factory.ScopedProviderSettings
|
||||||
authNs map[authtypes.AuthNProvider]authn.AuthN
|
authNs map[authtypes.AuthNProvider]authn.AuthN
|
||||||
user user.Module
|
user user.Module
|
||||||
userGetter user.Getter
|
userGetter user.Getter
|
||||||
authDomain authdomain.Module
|
authDomain authdomain.Module
|
||||||
tokenizer tokenizer.Tokenizer
|
tokenizer tokenizer.Tokenizer
|
||||||
orgGetter organization.Getter
|
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{
|
return &module{
|
||||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
|
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
|
||||||
authNs: authNs,
|
authNs: authNs,
|
||||||
user: user,
|
user: user,
|
||||||
userGetter: userGetter,
|
userGetter: userGetter,
|
||||||
authDomain: authDomain,
|
authDomain: authDomain,
|
||||||
tokenizer: tokenizer,
|
tokenizer: tokenizer,
|
||||||
orgGetter: orgGetter,
|
orgGetter: orgGetter,
|
||||||
|
rootUserModule: rootUserModule,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +63,19 @@ func (module *module) GetSessionContext(ctx context.Context, email valuer.Email,
|
|||||||
orgIDs = append(orgIDs, org.ID)
|
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)
|
users, err := module.userGetter.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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)
|
passwordAuthN, err := getProvider[authn.PasswordAuthN](authNProvider, module.authNs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -215,3 +247,31 @@ func getProvider[T authn.AuthN](authNProvider authtypes.AuthNProvider, authNs ma
|
|||||||
|
|
||||||
return provider, nil
|
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]
|
||||||
|
|
||||||
|
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/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
minRootUserPasswordLength = 12
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Password PasswordConfig `mapstructure:"password"`
|
Password PasswordConfig `mapstructure:"password"`
|
||||||
|
RootUserConfig RootUserConfig `mapstructure:"root"`
|
||||||
}
|
}
|
||||||
type PasswordConfig struct {
|
type PasswordConfig struct {
|
||||||
Reset ResetConfig `mapstructure:"reset"`
|
Reset ResetConfig `mapstructure:"reset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RootUserConfig struct {
|
||||||
|
Email string `mapstructure:"email"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
type ResetConfig struct {
|
type ResetConfig struct {
|
||||||
AllowSelf bool `mapstructure:"allow_self"`
|
AllowSelf bool `mapstructure:"allow_self"`
|
||||||
MaxTokenLifetime time.Duration `mapstructure:"max_token_lifetime"`
|
MaxTokenLifetime time.Duration `mapstructure:"max_token_lifetime"`
|
||||||
@@ -31,6 +42,10 @@ func newConfig() factory.Config {
|
|||||||
MaxTokenLifetime: 6 * time.Hour,
|
MaxTokenLifetime: 6 * time.Hour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
RootUserConfig: RootUserConfig{
|
||||||
|
Email: "",
|
||||||
|
Password: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,5 +54,40 @@ func (c Config) Validate() error {
|
|||||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::password::reset::max_token_lifetime must be positive")
|
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::password::reset::max_token_lifetime must be positive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := c.RootUserConfig.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r RootUserConfig) Validate() error {
|
||||||
|
if (r.Email == "") != (r.Password == "") {
|
||||||
|
// all or nothing case
|
||||||
|
return errors.Newf(
|
||||||
|
errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"user::root requires both email and password to be set, or neither",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing case
|
||||||
|
if !r.IsConfigured() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := valuer.NewEmail(r.Email)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid user::root::email %s", r.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.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/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
"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/rootuser"
|
||||||
root "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"
|
||||||
@@ -18,12 +19,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type handler struct {
|
type handler struct {
|
||||||
module root.Module
|
module root.Module
|
||||||
getter root.Getter
|
getter root.Getter
|
||||||
|
rootUserModule rootuser.Module
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(module root.Module, getter root.Getter) root.Handler {
|
func NewHandler(module root.Module, getter root.Getter, rootUserModule rootuser.Module) root.Handler {
|
||||||
return &handler{module: module, getter: getter}
|
return &handler{module: module, getter: getter, rootUserModule: rootUserModule}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
|
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
|
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, err := h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &types.PostableBulkInviteRequest{
|
||||||
Invites: []types.PostableInvite{req},
|
Invites: []types.PostableInvite{req},
|
||||||
})
|
})
|
||||||
@@ -94,6 +113,25 @@ func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
_, err = h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
@@ -192,6 +230,37 @@ func (h *handler) GetMyUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ROOT USER
|
||||||
|
if h.rootUserModule != nil {
|
||||||
|
rootUser, err := h.rootUserModule.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID))
|
||||||
|
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))
|
user, err := h.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
@@ -259,6 +328,11 @@ func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if claims.UserID == id {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "users cannot delete themselves"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.module.DeleteUser(ctx, valuer.MustNewUUID(claims.OrgID), id, claims.UserID); err != nil {
|
if err := h.module.DeleteUser(ctx, valuer.MustNewUUID(claims.OrgID), id, claims.UserID); err != nil {
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/rootuser/implrootuser"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||||
"github.com/SigNoz/signoz/pkg/sharder"
|
"github.com/SigNoz/signoz/pkg/sharder"
|
||||||
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
||||||
@@ -40,7 +42,8 @@ func TestNewHandlers(t *testing.T) {
|
|||||||
queryParser := queryparser.New(providerSettings)
|
queryParser := queryparser.New(providerSettings)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
|
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
|
||||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule)
|
rootUserReconciler := implrootuser.NewReconciler(implrootuser.NewStore(sqlstore, providerSettings), providerSettings, orgGetter, user.RootUserConfig{})
|
||||||
|
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, rootUserReconciler)
|
||||||
|
|
||||||
handlers := NewHandlers(modules, providerSettings, nil, nil, nil, nil, nil, nil, nil)
|
handlers := NewHandlers(modules, providerSettings, nil, nil, nil, nil, nil, nil, nil)
|
||||||
reflectVal := reflect.ValueOf(handlers)
|
reflectVal := reflect.ValueOf(handlers)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter"
|
"github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
|
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
|
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
|
||||||
|
"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"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
|
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/services"
|
"github.com/SigNoz/signoz/pkg/modules/services"
|
||||||
@@ -66,6 +68,7 @@ type Modules struct {
|
|||||||
SpanPercentile spanpercentile.Module
|
SpanPercentile spanpercentile.Module
|
||||||
MetricsExplorer metricsexplorer.Module
|
MetricsExplorer metricsexplorer.Module
|
||||||
Promote promote.Module
|
Promote promote.Module
|
||||||
|
RootUser rootuser.Module
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModules(
|
func NewModules(
|
||||||
@@ -85,13 +88,16 @@ func NewModules(
|
|||||||
queryParser queryparser.QueryParser,
|
queryParser queryparser.QueryParser,
|
||||||
config Config,
|
config Config,
|
||||||
dashboard dashboard.Module,
|
dashboard dashboard.Module,
|
||||||
|
rootUserReconciler rootuser.Reconciler,
|
||||||
) Modules {
|
) Modules {
|
||||||
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
|
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
|
||||||
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
|
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter, rootUserReconciler)
|
||||||
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User)
|
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User)
|
||||||
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
|
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
|
||||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||||
|
|
||||||
|
rootUser := implrootuser.NewModule(implrootuser.NewStore(sqlstore, providerSettings), providerSettings, config.User.RootUserConfig, authz)
|
||||||
|
|
||||||
return Modules{
|
return Modules{
|
||||||
OrgGetter: orgGetter,
|
OrgGetter: orgGetter,
|
||||||
OrgSetter: orgSetter,
|
OrgSetter: orgSetter,
|
||||||
@@ -105,10 +111,11 @@ func NewModules(
|
|||||||
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
||||||
RawDataExport: implrawdataexport.NewModule(querier),
|
RawDataExport: implrawdataexport.NewModule(querier),
|
||||||
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
|
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),
|
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
||||||
Services: implservices.NewModule(querier, telemetryStore),
|
Services: implservices.NewModule(querier, telemetryStore),
|
||||||
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
|
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
|
||||||
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
|
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
|
||||||
|
RootUser: rootUser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/rootuser/implrootuser"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||||
"github.com/SigNoz/signoz/pkg/sharder"
|
"github.com/SigNoz/signoz/pkg/sharder"
|
||||||
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
||||||
@@ -40,7 +42,8 @@ func TestNewModules(t *testing.T) {
|
|||||||
queryParser := queryparser.New(providerSettings)
|
queryParser := queryparser.New(providerSettings)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
|
dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser)
|
||||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule)
|
rootUserReconciler := implrootuser.NewReconciler(implrootuser.NewStore(sqlstore, providerSettings), providerSettings, orgGetter, user.RootUserConfig{})
|
||||||
|
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, rootUserReconciler)
|
||||||
|
|
||||||
reflectVal := reflect.ValueOf(modules)
|
reflectVal := reflect.ValueOf(modules)
|
||||||
for i := 0; i < reflectVal.NumField(); i++ {
|
for i := 0; i < reflectVal.NumField(); i++ {
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ func NewSQLMigrationProviderFactories(
|
|||||||
sqlmigration.NewMigrateRbacToAuthzFactory(sqlstore),
|
sqlmigration.NewMigrateRbacToAuthzFactory(sqlstore),
|
||||||
sqlmigration.NewMigratePublicDashboardsFactory(sqlstore),
|
sqlmigration.NewMigratePublicDashboardsFactory(sqlstore),
|
||||||
sqlmigration.NewAddAnonymousPublicDashboardTransactionFactory(sqlstore),
|
sqlmigration.NewAddAnonymousPublicDashboardTransactionFactory(sqlstore),
|
||||||
|
sqlmigration.NewAddRootUserFactory(sqlstore, sqlschema),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +238,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
|||||||
orgGetter,
|
orgGetter,
|
||||||
authz,
|
authz,
|
||||||
implorganization.NewHandler(modules.OrgGetter, modules.OrgSetter),
|
implorganization.NewHandler(modules.OrgGetter, modules.OrgSetter),
|
||||||
impluser.NewHandler(modules.User, modules.UserGetter),
|
impluser.NewHandler(modules.User, modules.UserGetter, modules.RootUser),
|
||||||
implsession.NewHandler(modules.Session),
|
implsession.NewHandler(modules.Session),
|
||||||
implauthdomain.NewHandler(modules.AuthDomain),
|
implauthdomain.NewHandler(modules.AuthDomain),
|
||||||
implpreference.NewHandler(modules.Preference),
|
implpreference.NewHandler(modules.Preference),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/rootuser/implrootuser"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||||
"github.com/SigNoz/signoz/pkg/querier"
|
"github.com/SigNoz/signoz/pkg/querier"
|
||||||
@@ -266,6 +267,10 @@ func New(
|
|||||||
// Initialize organization getter
|
// Initialize organization getter
|
||||||
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstore), sharder)
|
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstore), sharder)
|
||||||
|
|
||||||
|
// Initialize and run the root user reconciler
|
||||||
|
rootUserStore := implrootuser.NewStore(sqlstore, providerSettings)
|
||||||
|
rootUserReconciler := implrootuser.NewReconciler(rootUserStore, providerSettings, orgGetter, config.User.RootUserConfig)
|
||||||
|
|
||||||
// Initialize tokenizer from the available tokenizer provider factories
|
// Initialize tokenizer from the available tokenizer provider factories
|
||||||
tokenizer, err := factory.NewProviderFromNamedMap(
|
tokenizer, err := factory.NewProviderFromNamedMap(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -387,7 +392,7 @@ func New(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize all modules
|
// Initialize all modules
|
||||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard)
|
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, rootUserReconciler)
|
||||||
|
|
||||||
// Initialize all handlers for the modules
|
// Initialize all handlers for the modules
|
||||||
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway, telemetryMetadataStore, authz)
|
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway, telemetryMetadataStore, authz)
|
||||||
@@ -443,6 +448,12 @@ func New(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = rootUserReconciler.Reconcile(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Question: Should we fail the startup if the root user reconciliation fails?
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &SigNoz{
|
return &SigNoz{
|
||||||
Registry: registry,
|
Registry: registry,
|
||||||
Analytics: analytics,
|
Analytics: analytics,
|
||||||
|
|||||||
103
pkg/sqlmigration/064_add_root_user.go
Normal file
103
pkg/sqlmigration/064_add_root_user.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package sqlmigration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type addRootUser struct {
|
||||||
|
sqlStore sqlstore.SQLStore
|
||||||
|
sqlSchema sqlschema.SQLSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAddRootUserFactory(sqlStore sqlstore.SQLStore, sqlSchema sqlschema.SQLSchema) 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, sqlSchema)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAddRootUser(_ context.Context, _ factory.ProviderSettings, _ Config, sqlStore sqlstore.SQLStore, sqlSchema sqlschema.SQLSchema) (SQLMigration, error) {
|
||||||
|
return &addRootUser{sqlStore: sqlStore, sqlSchema: sqlSchema}, 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 {
|
||||||
|
|
||||||
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
sqls := [][]byte{}
|
||||||
|
|
||||||
|
// create root_users table sqls
|
||||||
|
tableSQLs := migration.sqlSchema.Operator().CreateTable(
|
||||||
|
&sqlschema.Table{
|
||||||
|
Name: "root_users",
|
||||||
|
Columns: []*sqlschema.Column{
|
||||||
|
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "email", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "password_hash", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||||
|
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||||
|
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []sqlschema.ColumnName{"id"},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||||
|
{
|
||||||
|
ReferencingColumnName: sqlschema.ColumnName("org_id"),
|
||||||
|
ReferencedTableName: sqlschema.TableName("organizations"),
|
||||||
|
ReferencedColumnName: sqlschema.ColumnName("id"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
sqls = append(sqls, tableSQLs...)
|
||||||
|
|
||||||
|
// create index sqls
|
||||||
|
indexSQLs := migration.sqlSchema.Operator().CreateIndex(
|
||||||
|
&sqlschema.UniqueIndex{
|
||||||
|
TableName: sqlschema.TableName("root_users"),
|
||||||
|
ColumnNames: []sqlschema.ColumnName{"org_id"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
sqls = append(sqls, indexSQLs...)
|
||||||
|
|
||||||
|
for _, sqlStmt := range sqls {
|
||||||
|
if _, err := tx.ExecContext(ctx, string(sqlStmt)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *addRootUser) Down(ctx context.Context, db *bun.DB) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package sqltokenizerstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"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) {
|
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)
|
user := new(types.User)
|
||||||
|
|
||||||
err := store.
|
err := store.
|
||||||
@@ -43,11 +45,36 @@ func (store *store) GetIdentityByUserID(ctx context.Context, userID valuer.UUID)
|
|||||||
Model(user).
|
Model(user).
|
||||||
Where("id = ?", userID).
|
Where("id = ?", userID).
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with id: %s does not exist", userID)
|
// 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) {
|
func (store *store) GetByAccessToken(ctx context.Context, accessToken string) (*authtypes.StorableToken, error) {
|
||||||
@@ -130,6 +157,24 @@ func (store *store) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*auth
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ROOT USER TOKENS
|
||||||
|
rootUserTokens := make([]*authtypes.StorableToken, 0)
|
||||||
|
|
||||||
|
err = store.
|
||||||
|
sqlstore.
|
||||||
|
BunDBCtx(ctx).
|
||||||
|
NewSelect().
|
||||||
|
Model(&rootUserTokens).
|
||||||
|
Join("JOIN root_users").
|
||||||
|
JoinOn("root_users.id = auth_token.user_id").
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens = append(tokens, rootUserTokens...)
|
||||||
|
|
||||||
return tokens, nil
|
return tokens, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +196,26 @@ func (store *store) ListByOrgIDs(ctx context.Context, orgIDs []valuer.UUID) ([]*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ROOT USER TOKENS
|
||||||
|
rootUserTokens := make([]*authtypes.StorableToken, 0)
|
||||||
|
|
||||||
|
err = store.
|
||||||
|
sqlstore.
|
||||||
|
BunDBCtx(ctx).
|
||||||
|
NewSelect().
|
||||||
|
Model(&rootUserTokens).
|
||||||
|
Join("JOIN root_users").
|
||||||
|
JoinOn("root_users.id = auth_token.user_id").
|
||||||
|
Join("JOIN organizations").
|
||||||
|
JoinOn("organizations.id = root_users.org_id").
|
||||||
|
Where("organizations.id IN (?)", bun.In(orgIDs)).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens = append(tokens, rootUserTokens...)
|
||||||
|
|
||||||
return tokens, nil
|
return tokens, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Identity struct {
|
|||||||
OrgID valuer.UUID `json:"orgId"`
|
OrgID valuer.UUID `json:"orgId"`
|
||||||
Email valuer.Email `json:"email"`
|
Email valuer.Email `json:"email"`
|
||||||
Role types.Role `json:"role"`
|
Role types.Role `json:"role"`
|
||||||
|
IsRoot bool `json:"isRoot"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CallbackIdentity struct {
|
type CallbackIdentity struct {
|
||||||
@@ -84,6 +85,17 @@ func NewIdentity(userID valuer.UUID, orgID valuer.UUID, email valuer.Email, role
|
|||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
Email: email,
|
Email: email,
|
||||||
Role: role,
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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, false otherwise.
|
||||||
|
ExistsByOrgID(ctx context.Context, orgID valuer.UUID) (bool, error)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user