Files
Vishal Dalwadi 132aee0bb3 NM-341-v6: AMB Multi-tenancy Management (#4101)
* wip(go): return auth token with user on create org owner;

* wip(go): prevent use of default tenant slug ;

* wip(go): upsert settings for tenant in ctx;

* wip(go): use passed ctx when updating user; remove password validation from ValidateUser (already validated in UpdateUser);

* wip(go): qualify all tenant id usages with table name;

* wip(go): use passed ctx

* wip(go): return old enrollment key type on create;

* wip(go): prevent null networks return;

* wip(go): wire ctx in dns/nameserver functions; wire ctx in host registration functions;

* wip(go): wire ctx in node functions;

* wip(go): wire ctx in node functions;

* wip(go): wire ctx in extclient functions;

* wip(go): remove not needed field;

* wip(go): use passed ctx;

* wip(go): use unique index instead of primary key;

* wip(go): wire ctx in extclient apis;

* wip(go): wire ctx in extclient apis;

* wip(go): remove org id check from global scoped get tenant api;

* wip(go): add scope slug to discover response;

* wip(go): wire ctx in get users;

* wip(go): set tenant id in posture check violations;

* wip(go): upsert membership instead of user;

* wip(go): pass correct len;

* wip(go): optimize (single loop);

* wip(go): unify todos;

* wip(go): add todo;

* wip(go): wire context for network get;

* wip(go): wire context;

* wip(go): wire context;

* wip(go): wire ctx when getting tenant superadmin;

* wip(go): add network creator to network admin group;

* wip(go): check for user membership on sync;

* wip(go): add methods with strict membership check;

* wip(go): remove unwanted feature flags;

* wip(go): report per-tenant usage during license validation;

* wip(go): move email setting from tenant to org;

* wip(go): pass org scope to email init;

* wip(go): remove old tenant email settings usages; use org settings to get email settings

* wip(go): use get email settings from org settings;

* wip(go): remove email settings migration;

* wip(go): migrate email settings from server to org settings;

* wip(go): assign global network admin group membership only for super-admin and admins;

* wip(go): assign global network admin group membership only for super-admin and admins;

* wip(go): add membership alias to filter;

* wip(go): skip clearing local license validation response cache;

* wip(go): extend license validation response for multi-tenancy;

* wip(go): add functions to get sole tenant and org;

* wip(go): use tenant-specific feature flags;

* wip(go): add tenant status type;

* wip(go): remove old hook comment;

* wip(go): expect ctx during license validation;

* wip(go): always create default org and tenant for ce;

* wip(go): add tenant-specific check for license validation error;

* wip(go): cache license validation response in memory;

* wip(go): add todo to handle multiple-tenant for integration pull;

* wip(go): pass ctx during license validation;

* wip(go): add todo for making changes later; run hook separately for each tenant;

* wip(go): return specific errors when license validation response not in cache;

* wip(go): use retryable.httpclient for calling license validation api;

* wip(go): process license validation;

* wip(go): license validation function;

* wip(go): migration on license validation;

* wip(go): org settings not expected;

* wip(go): remove expiry from license validation response;

* wip(go): add limits and limits enforcement to license validation response;

* wip(go): remove expiry usages;

* wip(go): add arg to clear cache;

* wip(go): add arg to clear cache;

* wip(go): skip license validation if cached resp;

* wip(go): allow org_id and tenant_id query params;

* wip(go): upsert tenant membership on sync;

* wip(go): pass ctx to delete network roles;

* wip(go): create tenant membership on org owner creation on ce;

* wip(go): add tenant orchestrator;

* wip(go): use tenant orchestrator;

* wip(go): add default settings for tenant;

* wip(go): make org owner superadmin on all tenants;

* wip(go): simplify SyncOrgAndTenants;

* wip(go): retry license validation only 3 times

* wip(go): store invalid err;

* wip(go): add function to check if limits are to be enforced on a tenant;

* wip(go): pass tenant scope if possible when checking for license validation error;

* wip(go): remove merge errors;

* wip(go): add functions for limit checks;

* wip(go): wire host limit check;

* wip(go): wire ingress limit check;

* wip(go): wire egress limit check;

* wip(go): wire client limit check;

* wip(go): wire network limit check;

* wip(go): wire user limit check;

* wip(go): use correct errors;

* wip(go): skip telemetry for msps;

* wip(go): disable telemetry on msp tenants;

* wip(go): err check;

* wip(go): uncomment log event;

* wip(go): get sso state to get scope for non-headless oauth login;

* wip(go): inline delete pending user;

* wip(go): allow both org and tenant scope for pending users table;

* wip(go): add migration for pending user table during set tenant id and rekey org/tenant id;

* wip(go): update queries to use new scope fields in pending users table;

* wip(go): allow both org and tenant scope for user invites table;

* wip(go): update queries to use new scope fields;

* wip(go): add migration for rekey-ing org/tenant id and setting tenant id;

* wip(go): inline delete user invite;

* wip(go): set scope when creating invite;

* wip(go): get user count with membership is ctx is tenant scoped;

* wip(go): add methods to upsert and delete org memberships;

* wip(go): add global valid tenant and org roles map;

* wip(go): add scope query in user invite get by email;

* wip(go): add support for upsert and delete of org and tenant membership both;

* wip(go): infer scope for user invite apis;

* wip(go): wire ctx in get user invite by email;

* wip(go): allow no scope headers to be passed in infer scope apis;

* wip(go): add validation and checks for org-scoped invite api handlers;

* wip(go): wire ctx to is pending user check;

* wip(go): infer scope for pending user apis;

* wip(go): set default role based on ctx scope;

* wip(go): move mfa to tenant membership table; move account status to membership tables;

* wip(go): migrate account status and mfa settings;

* wip(go): update account status on sync;

* wip(go): use update mfa to update mfa setting;

* wip(go): set new membership settings on user creation;

* wip(go): use membership where required;

* wip(go): wire ctx on key unlink refs;

* wip(go): use r.Context directly without reinitializing db;

* wip(go): wire ctx in acls;

* wip(go): add tenant scoped primary key constructor;

* wip(go): use tenant scoped primary key for acls, dns, extclients and tags;

* wip(go): wire ctx for get groups in network;

* wip(go): wire ctx for get groups;

* wip(go): scope non-uuid group ids by tenant id;

* wip(go): migrate non-uuid group ids to scoped ids;

* wip(go): don't expect scope id in invite validation and signup apis;

* wip(go): wire ctx for get matching user; allow org scope when querying by external idp id;

* wip(go): add mfa to org;

* wip(go): scope preauth token;

* wip(go): add fallback for older auth tokens;

* wip(go): wire ctx for preauth token creation;

* wip(go): migrate mfa settings;

* wip(go): resolve mfa settings for inherited auth;

* wip(go): update mfa for org;

* wip(go): infer scope for totp apis;

* wip(go): use ctx when passed;

* wip(go): infer scope for list user apis;

* wip(go): ensure set primary keys don't cause errors during fetch;

* wip(go): make primary key for integrations uuid and store provider in separate key;

* wip(go): migrate integration ids;

* wip(go): use provider instead of id everywhere;

* wip(go): remove gorm column;

* wip(go): wire ctx for igw egress creation;

* wip(go): wire ctx for igw egress update;

* wip(go): wire ctx for host registration;

* wip(go): wire ctx for host registration;

* wip(go): merge errors;

* wip(go): merge errors;

* wip(go): wire ctx; remove default scope usages;

* wip(go): use get with membership;

* wip(go): org scope license validation;

* wip(go): remove debug log;

* wip(go): remove scoping on list roles;

* wip(go): ignore invite creation if already exists;

* wip(go): add is msp func to check if deployment is msp deployment;

* wip(go): ensure username is email for msp users;

* wip(go): add email validated check on user table;

* wip(go): mark email validated when oauth user;

* wip(go): handle gorm error;

* wip(go): add tenant id to siem grpc schema;

* wip(go): read tenant from scoped ctx;

* wip(go): pass scoped ctx to siem grpc client;

* wip(go): remove unused publisher;

* wip(go): publish all tenant exporter updates on pull;

* wip(go): fix wg usage;

* wip(go): tenant scope the ctx when getting server info;

* wip(go): add tenant id to enrollment key token;

* wip(go): make host register tenant scope;

* wip(go): add default settings;

* wip(go): use correct checks for is msp tenant;

* wip(go): add email validation extension;

* wip(go): add pages for validation check;

* wip(go): use extension for validating user;

* wip(go): add invite type;

* wip(go): create only one validation invite per user email;

* wip(go): add validate email handler;

* wip(go): add email builder;

* wip(go): add user invite type migration logic;

* wip(go): only log email send failures;

* wip(go): add tenant id to flows table;

* wip(go): add func to check if connected to clickhouse;

* wip(go): migrate clickhouse;

* wip(go): add tenant id to mdm and edr state;

* wip(go): add delete methods for tables;

* wip(go): add tenant teardown method;

* wip(go): handle tenant deletion;

* wip(go): clean up flow logs on tenant deletion;

* wip(go): add update org membership role id method;

* wip(go): add transfer org ownership api;

* wip(go): move audit logs retention period to org settings;

* wip(go):get retention period from org settings;

* wip(go): remove old audit logs usage;

* wip(go): migrate audit logs retention period;

* wip(go): unmarshal decodes nums as float64;

* wip(go): run flow logs cleanup for all tenants in one go;

* wip(go): add audit logs retention period and smtp settings to tenant settings table for non-msp tenants;

* wip(go): resolve retention period and smtp settings correctly;

* wip(go): handle retention period and smtp settings updates and fetches correctly;

* wip(go): overlay retention period and smtp settings on server settings;;

* wip(go): upsert overlayed settings;

* wip(go): add azure tenant id to org settings;

* wip(go): tenant scope list roles;

* wip(go): add noop creds;

* wip(go): use first instead of find;

* wip(go): use correct roles when transfering ownership;

* wip(go): delete access tokens only if tenant scoped;

* wip(go): make delete user work for both org and tenant scope;

* wip(go): delete extclients only if tenant scoped;

* wip(go): check if caller is allowed to delete user;

* wip(go): infer scope for update user;

* wip(go): prevent org-owner elevation;

* wip(go): permissions check for update;

* wip(go): delete user access tokens only if tenant scope;

* wip(go): delete extclients only if tenant scope;

* wip(go): move upsert membership in upsert user;

* wip(go): set api host when constructing server config;

* wip(go): fix static errors;

* wip(go): fix test cases;

* wip(go): use correct get ns order;

* wip(go): use correct get pending host order;

* wip(go): use correct get access token order;

* wip(go): use correct get event order;

* wip(go): return error response on emqx user creation error;

* wip(go): store correct value in cache;

* wip(go): return better error response;

* wip(go): return better error response;

* wip(go): use cache when listing egress acls;

* wip(go): wire ctx in UserHasNetworkGroupAccess;

* wip(go): remove double scope wrapping;

* wip(go): add security check for list and get org;

* wip(go): ratelimit discover api;

* wip(go): expect sole org id in get and create org owner;

* wip(go): wrap create org owner in tx;
2026-08-05 21:25:02 +08:00

554 lines
18 KiB
Go

package schema
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"github.com/gravitl/netmaker/db"
dbtypes "github.com/gravitl/netmaker/db/types"
"github.com/gravitl/netmaker/scope"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type AuthType string
var (
BasicAuth AuthType = "basic_auth"
OAuth AuthType = "oauth"
Inherited AuthType = "inherited"
)
type Theme string
const (
Dark Theme = "dark"
Light Theme = "light"
System Theme = "system"
)
var (
ErrUserIdentifiersNotProvided = errors.New("user identifiers not provided")
ErrTenantIDNotProvided = errors.New("tenant ID not provided")
ErrScopeNotProvided = errors.New("scope not provided")
)
type User struct {
ID string `gorm:"primaryKey" json:"id"`
Username string `gorm:"unique" json:"username"`
DisplayName string `json:"display_name"`
PlatformRoleID UserRoleID `gorm:"-" json:"platform_role_id,omitempty"`
ExternalIdentityProviderID string `gorm:"-" json:"external_identity_provider_id"`
AccountDisabled bool `gorm:"-" json:"account_disabled"`
AuthType AuthType `gorm:"-" json:"auth_type"`
Password string `gorm:"-" json:"password"`
IsMFAEnabled bool `gorm:"-" json:"is_mfa_enabled"`
TOTPSecret string `gorm:"-" json:"totp_secret"`
EmailValidated bool `json:"email_validated"`
// NOTE: json tag is different from field name to ensure compatibility with the older model.
LastLoginAt time.Time `json:"last_login_time"`
// NOTE: json tag is different from field name to ensure compatibility with the older model.
UserGroups datatypes.JSONType[map[UserGroupID]struct{}] `gorm:"-" json:"user_group_ids,omitempty"`
Theme Theme `json:"theme"`
TextSize string `json:"text_size"`
ReducedMotion bool `json:"reduced_motion"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (u *User) TableName() string {
return "users_v1"
}
// userWithOrgMembership is a flattened scan target for queries that JOIN org_memberships_v1.
type userWithOrgMembership struct {
User
MemberRoleID UserRoleID `gorm:"column:member_role_id"`
MemberAuthType AuthType `gorm:"column:member_auth_type"`
MemberExternalIdentityProviderID string `gorm:"column:member_external_identity_provider_id"`
MemberPassword string `gorm:"column:member_password"`
MemberAccountDisabled bool `gorm:"column:member_account_disabled"`
MemberIsMFAEnabled bool `gorm:"column:member_is_mfa_enabled"`
MemberTOTPSecret string `gorm:"column:member_totp_secret"`
}
// userWithMembership is a flattened scan target for queries that JOIN tenant_memberships_v1.
type userWithMembership struct {
User
MemberRoleID UserRoleID `gorm:"column:member_role_id"`
MemberGroups datatypes.JSONType[map[UserGroupID]struct{}] `gorm:"column:member_groups"`
MemberAuthType AuthType `gorm:"column:member_auth_type"`
MemberExternalIdentityProviderID string `gorm:"column:member_external_identity_provider_id"`
MemberPassword string `gorm:"column:member_password"`
MemberAccountDisabled bool `gorm:"column:member_account_disabled"`
MemberIsMFAEnabled bool `gorm:"column:member_is_mfa_enabled"`
MemberTOTPSecret string `gorm:"column:member_totp_secret"`
}
func (u *User) SuperAdminExists(ctx context.Context) (bool, error) {
tenantID := scope.ID(ctx)
if tenantID == "" {
return false, ErrTenantIDNotProvided
}
var exists bool
err := db.FromContext(ctx).Raw(
"SELECT EXISTS (SELECT 1 FROM tenant_memberships_v1 WHERE tenant_id = ? AND role_id = ?)",
tenantID,
SuperAdminRole,
).Scan(&exists).Error
return exists, err
}
func (u *User) Create(ctx context.Context) error {
if u.ID == "" {
u.ID = uuid.NewString()
}
return db.FromContext(ctx).Model(&User{}).Create(u).Error
}
func (u *User) Get(ctx context.Context) error {
return u.get(ctx, false)
}
func (u *User) GetWithMembership(ctx context.Context) error {
return u.get(ctx, true)
}
func (u *User) get(ctx context.Context, requireMembership bool) error {
if u.ID == "" && u.Username == "" {
return ErrUserIdentifiersNotProvided
}
joinType := "LEFT"
if requireMembership {
joinType = "INNER"
}
if scope.Level(ctx) == scope.OrgScope {
orgID := scope.ID(ctx)
var row userWithOrgMembership
err := db.FromContext(ctx).
Table("users_v1").
Select("users_v1.*, om.role_id AS member_role_id, om.auth_type AS member_auth_type, om.external_identity_provider_id AS member_external_identity_provider_id, om.password AS member_password, om.account_disabled AS member_account_disabled, om.is_mfa_enabled AS member_is_mfa_enabled, om.totp_secret AS member_totp_secret").
Joins(joinType+" JOIN org_memberships_v1 om ON om.user_id = users_v1.id AND om.organization_id = ?", orgID).
Where("users_v1.id = ? OR users_v1.username = ?", u.ID, u.Username).
First(&row).
Error
if err != nil {
return err
}
*u = row.User
u.PlatformRoleID = row.MemberRoleID
u.AuthType = row.MemberAuthType
u.ExternalIdentityProviderID = row.MemberExternalIdentityProviderID
u.Password = row.MemberPassword
u.AccountDisabled = row.MemberAccountDisabled
u.IsMFAEnabled = row.MemberIsMFAEnabled
u.TOTPSecret = row.MemberTOTPSecret
return nil
}
if scope.Level(ctx) == scope.TenantScope {
tenantID := scope.ID(ctx)
var row userWithMembership
err := db.FromContext(ctx).
Table("users_v1").
Select("users_v1.*, tm.role_id AS member_role_id, tm.groups AS member_groups, tm.auth_type AS member_auth_type, tm.external_identity_provider_id AS member_external_identity_provider_id, tm.password AS member_password, tm.account_disabled AS member_account_disabled, tm.is_mfa_enabled AS member_is_mfa_enabled, tm.totp_secret AS member_totp_secret").
Joins(joinType+" JOIN tenant_memberships_v1 tm ON tm.user_id = users_v1.id AND tm.tenant_id = ?", tenantID).
Where("users_v1.id = ? OR users_v1.username = ?", u.ID, u.Username).
First(&row).
Error
if err != nil {
return err
}
*u = row.User
u.PlatformRoleID = row.MemberRoleID
u.UserGroups = row.MemberGroups
u.AuthType = row.MemberAuthType
u.ExternalIdentityProviderID = row.MemberExternalIdentityProviderID
u.Password = row.MemberPassword
u.AccountDisabled = row.MemberAccountDisabled
u.IsMFAEnabled = row.MemberIsMFAEnabled
u.TOTPSecret = row.MemberTOTPSecret
return nil
}
if requireMembership {
return ErrScopeNotProvided
}
return db.FromContext(ctx).Model(&User{}).
Where("id = ? OR username = ?", u.ID, u.Username).
First(u).
Error
}
func (u *User) GetByExternalID(ctx context.Context) error {
if scope.ID(ctx) == "" {
return ErrScopeNotProvided
}
if u.ExternalIdentityProviderID == "" {
return ErrUserIdentifiersNotProvided
}
if scope.Level(ctx) == scope.OrgScope {
var row userWithOrgMembership
orgID := scope.ID(ctx)
err := db.FromContext(ctx).
Table("users_v1").
Select("users_v1.*, om.role_id AS member_role_id, om.auth_type AS member_auth_type, om.external_identity_provider_id AS member_external_identity_provider_id, om.password AS member_password, om.account_disabled AS member_account_disabled").
Joins("LEFT JOIN org_memberships_v1 om ON om.user_id = users_v1.id AND om.organization_id = ?", orgID).
Where("om.external_identity_provider_id = ?", u.ExternalIdentityProviderID).
First(&row).
Error
if err != nil {
return err
}
*u = row.User
u.PlatformRoleID = row.MemberRoleID
u.AuthType = row.MemberAuthType
u.ExternalIdentityProviderID = row.MemberExternalIdentityProviderID
u.Password = row.MemberPassword
u.AccountDisabled = row.MemberAccountDisabled
return nil
}
var row userWithMembership
tenantID := scope.ID(ctx)
err := db.FromContext(ctx).
Table("users_v1").
Select("users_v1.*, tm.role_id AS member_role_id, tm.groups AS member_groups, tm.auth_type AS member_auth_type, tm.external_identity_provider_id AS member_external_identity_provider_id, tm.password AS member_password, tm.account_disabled AS member_account_disabled, tm.is_mfa_enabled AS member_is_mfa_enabled, tm.totp_secret AS member_totp_secret").
Joins("LEFT JOIN tenant_memberships_v1 tm ON tm.user_id = users_v1.id AND tm.tenant_id = ?", tenantID).
Where("tm.external_identity_provider_id = ?", u.ExternalIdentityProviderID).
First(&row).
Error
if err != nil {
return err
}
*u = row.User
u.PlatformRoleID = row.MemberRoleID
u.UserGroups = row.MemberGroups
u.AuthType = row.MemberAuthType
u.ExternalIdentityProviderID = row.MemberExternalIdentityProviderID
u.Password = row.MemberPassword
u.AccountDisabled = row.MemberAccountDisabled
u.IsMFAEnabled = row.MemberIsMFAEnabled
u.TOTPSecret = row.MemberTOTPSecret
return nil
}
func (u *User) GetSuperAdmin(ctx context.Context) error {
tenantID := scope.ID(ctx)
if tenantID == "" {
return ErrTenantIDNotProvided
}
var row userWithMembership
err := db.FromContext(ctx).
Table("users_v1").
Select("users_v1.*, tm.role_id AS member_role_id, tm.groups AS member_groups, tm.auth_type AS member_auth_type, tm.external_identity_provider_id AS member_external_identity_provider_id, tm.password AS member_password, tm.account_disabled AS member_account_disabled, tm.is_mfa_enabled AS member_is_mfa_enabled, tm.totp_secret AS member_totp_secret").
Joins("INNER JOIN tenant_memberships_v1 tm ON tm.user_id = users_v1.id AND tm.tenant_id = ?", tenantID).
Where("tm.role_id = ?", SuperAdminRole).
First(&row).
Error
if err != nil {
return err
}
*u = row.User
u.PlatformRoleID = row.MemberRoleID
u.UserGroups = row.MemberGroups
u.AuthType = row.MemberAuthType
u.ExternalIdentityProviderID = row.MemberExternalIdentityProviderID
u.Password = row.MemberPassword
u.AccountDisabled = row.MemberAccountDisabled
u.IsMFAEnabled = row.MemberIsMFAEnabled
u.TOTPSecret = row.MemberTOTPSecret
return nil
}
func (u *User) Count(ctx context.Context, options ...dbtypes.Option) (int, error) {
return u.count(ctx, false, options...)
}
func (u *User) CountWithMembership(ctx context.Context, options ...dbtypes.Option) (int, error) {
return u.count(ctx, true, options...)
}
func (u *User) count(ctx context.Context, requireMembership bool, options ...dbtypes.Option) (int, error) {
joinType := "LEFT"
if requireMembership {
joinType = "INNER"
}
var count int64
var query *gorm.DB
switch scope.Level(ctx) {
case scope.OrgScope:
orgID := scope.ID(ctx)
query = db.FromContext(ctx).
Table("users_v1").
Joins(joinType+" JOIN org_memberships_v1 om ON om.user_id = users_v1.id AND om.organization_id = ?", orgID)
case scope.TenantScope:
tenantID := scope.ID(ctx)
query = db.FromContext(ctx).
Table("users_v1").
Joins(joinType+" JOIN tenant_memberships_v1 tm ON tm.user_id = users_v1.id AND tm.tenant_id = ?", tenantID)
default:
if requireMembership {
return 0, ErrScopeNotProvided
}
query = db.FromContext(ctx).Model(&User{})
}
for _, option := range options {
query = option(query)
}
err := query.Count(&count).Error
return int(count), err
}
func (u *User) ListAll(ctx context.Context, options ...dbtypes.Option) ([]User, error) {
return u.listAll(ctx, false, options...)
}
func (u *User) ListAllWithMembership(ctx context.Context, options ...dbtypes.Option) ([]User, error) {
return u.listAll(ctx, true, options...)
}
func (u *User) listAll(ctx context.Context, requireMembership bool, options ...dbtypes.Option) ([]User, error) {
joinType := "LEFT"
if requireMembership {
joinType = "INNER"
}
if scope.Level(ctx) == scope.OrgScope {
orgID := scope.ID(ctx)
query := db.FromContext(ctx).
Table("users_v1").
Select("users_v1.*, om.role_id AS member_role_id, om.auth_type AS member_auth_type, om.external_identity_provider_id AS member_external_identity_provider_id, om.password AS member_password, om.account_disabled AS member_account_disabled, om.is_mfa_enabled AS member_is_mfa_enabled, om.totp_secret AS member_totp_secret").
Joins(joinType+" JOIN org_memberships_v1 om ON om.user_id = users_v1.id AND om.organization_id = ?", orgID)
for _, option := range options {
query = option(query)
}
var rows []userWithOrgMembership
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
users := make([]User, len(rows))
for i, row := range rows {
users[i] = row.User
users[i].PlatformRoleID = row.MemberRoleID
users[i].AuthType = row.MemberAuthType
users[i].ExternalIdentityProviderID = row.MemberExternalIdentityProviderID
users[i].Password = row.MemberPassword
users[i].AccountDisabled = row.MemberAccountDisabled
users[i].IsMFAEnabled = row.MemberIsMFAEnabled
users[i].TOTPSecret = row.MemberTOTPSecret
}
return users, nil
}
if scope.Level(ctx) == scope.TenantScope {
tenantID := scope.ID(ctx)
query := db.FromContext(ctx).
Table("users_v1").
Select("users_v1.*, tm.role_id AS member_role_id, tm.groups AS member_groups, tm.auth_type AS member_auth_type, tm.external_identity_provider_id AS member_external_identity_provider_id, tm.password AS member_password, tm.account_disabled AS member_account_disabled, tm.is_mfa_enabled AS member_is_mfa_enabled, tm.totp_secret AS member_totp_secret").
Joins(joinType+" JOIN tenant_memberships_v1 tm ON tm.user_id = users_v1.id AND tm.tenant_id = ?", tenantID)
for _, option := range options {
query = option(query)
}
var rows []userWithMembership
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
users := make([]User, len(rows))
for i, row := range rows {
users[i] = row.User
users[i].PlatformRoleID = row.MemberRoleID
users[i].UserGroups = row.MemberGroups
users[i].AuthType = row.MemberAuthType
users[i].ExternalIdentityProviderID = row.MemberExternalIdentityProviderID
users[i].Password = row.MemberPassword
users[i].AccountDisabled = row.MemberAccountDisabled
users[i].IsMFAEnabled = row.MemberIsMFAEnabled
users[i].TOTPSecret = row.MemberTOTPSecret
}
return users, nil
}
if requireMembership {
return nil, ErrScopeNotProvided
}
var users []User
query := db.FromContext(ctx).Model(&User{})
for _, option := range options {
query = option(query)
}
err := query.Find(&users).Error
return users, err
}
func (u *User) Update(ctx context.Context) error {
if u.ID == "" && u.Username == "" {
return ErrUserIdentifiersNotProvided
}
return db.FromContext(ctx).Model(&User{}).
Where("id = ? OR username = ?", u.ID, u.Username).
Updates(u).
Error
}
func (u *User) UpdateAccountStatus(ctx context.Context) error {
scopeID := scope.ID(ctx)
if scopeID == "" {
return ErrScopeNotProvided
}
if scope.Level(ctx) == scope.OrgScope {
return (&OrgMembership{
OrganizationID: scopeID,
UserID: u.ID,
AccountDisabled: u.AccountDisabled,
}).UpdateAccountStatus(ctx)
}
return (&TenantMembership{
TenantID: scopeID,
UserID: u.ID,
AccountDisabled: u.AccountDisabled,
}).UpdateAccountStatus(ctx)
}
func (u *User) UpdateMFA(ctx context.Context) error {
scopeID := scope.ID(ctx)
if scopeID == "" {
return ErrScopeNotProvided
}
if scope.Level(ctx) == scope.OrgScope {
return (&OrgMembership{
OrganizationID: scopeID,
UserID: u.ID,
IsMFAEnabled: u.IsMFAEnabled,
TOTPSecret: u.TOTPSecret,
}).UpdateMFA(ctx)
}
tm := &TenantMembership{TenantID: scopeID, UserID: u.ID}
err := tm.Get(ctx)
if err != nil {
return err
}
if tm.AuthType == Inherited {
tenant := &Tenant{ID: scopeID}
err = tenant.Get(ctx)
if err != nil {
return err
}
return (&OrgMembership{
OrganizationID: tenant.OrganizationID,
UserID: u.ID,
IsMFAEnabled: u.IsMFAEnabled,
TOTPSecret: u.TOTPSecret,
}).UpdateMFA(ctx)
}
return (&TenantMembership{
TenantID: scopeID,
UserID: u.ID,
IsMFAEnabled: u.IsMFAEnabled,
TOTPSecret: u.TOTPSecret,
}).UpdateMFA(ctx)
}
func (u *User) UpdateUserSettings(ctx context.Context) error {
if u.ID == "" && u.Username == "" {
return ErrUserIdentifiersNotProvided
}
return db.FromContext(ctx).Model(&User{}).
Where("id = ? OR username = ?", u.ID, u.Username).
Updates(map[string]any{
"theme": u.Theme,
"text_size": u.TextSize,
"reduced_motion": u.ReducedMotion,
}).
Error
}
func (u *User) Delete(ctx context.Context) error {
if u.ID == "" && u.Username == "" {
return ErrUserIdentifiersNotProvided
}
return db.FromContext(ctx).Model(&User{}).
Where("id = ? OR username = ?", u.ID, u.Username).
Delete(u).
Error
}
func (u *User) UpsertMembership(ctx context.Context) error {
scopeID := scope.ID(ctx)
if scopeID == "" {
return ErrScopeNotProvided
}
if scope.Level(ctx) == scope.OrgScope {
return (&OrgMembership{
OrganizationID: scopeID,
UserID: u.ID,
RoleID: u.PlatformRoleID,
AuthType: u.AuthType,
ExternalIdentityProviderID: u.ExternalIdentityProviderID,
Password: u.Password,
AccountDisabled: u.AccountDisabled,
IsMFAEnabled: u.IsMFAEnabled,
TOTPSecret: u.TOTPSecret,
}).Upsert(ctx)
}
return (&TenantMembership{
TenantID: scopeID,
UserID: u.ID,
RoleID: u.PlatformRoleID,
Groups: u.UserGroups,
AuthType: u.AuthType,
ExternalIdentityProviderID: u.ExternalIdentityProviderID,
Password: u.Password,
AccountDisabled: u.AccountDisabled,
IsMFAEnabled: u.IsMFAEnabled,
TOTPSecret: u.TOTPSecret,
}).Upsert(ctx)
}
func (u *User) DeleteMembership(ctx context.Context) error {
scopeID := scope.ID(ctx)
if scopeID == "" {
return ErrScopeNotProvided
}
if scope.Level(ctx) == scope.OrgScope {
return (&OrgMembership{OrganizationID: scopeID, UserID: u.ID}).Delete(ctx)
}
return (&TenantMembership{TenantID: scopeID, UserID: u.ID}).Delete(ctx)
}