forked from baron/baron-sso
feat: simplify RBAC roles and remove dev role switcher
- Simplified RBAC system to two roles: super_admin and user. - Removed tenant_admin and rp_admin roles across backend and frontend. - Removed Dev Role Switcher feature from adminfront. - Updated all handlers, middlewares, and navigation to reflect the new role model. - Fixed backend build errors and updated tests.
This commit is contained in:
@@ -401,7 +401,7 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
|
||||
// [New] Manageable Tenants Map for efficient lookup
|
||||
manageableSlugs := make(map[string]bool)
|
||||
if requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin {
|
||||
if requesterRole != domain.RoleSuperAdmin {
|
||||
profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if profile != nil {
|
||||
var baseTenantIDs []string
|
||||
@@ -485,7 +485,7 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id"))
|
||||
|
||||
// Tenant Admin & Member filtering
|
||||
if requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin {
|
||||
if requesterRole != domain.RoleSuperAdmin {
|
||||
hasAccess := manageableSlugs[tID]
|
||||
if !hasAccess {
|
||||
continue
|
||||
@@ -608,7 +608,7 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
|
||||
|
||||
// [New] Check access scope
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||
if requester != nil && requester.Role != domain.RoleSuperAdmin {
|
||||
allowedKeys := profileTenantAccessKeys(requester)
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowedKeys) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: access to user in another tenant denied")
|
||||
@@ -1106,7 +1106,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Role-based access check
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||
if requester != nil && requester.Role != domain.RoleSuperAdmin {
|
||||
if !profileCanAccessTenant(requester, tItem.ID, tenantSlug) {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
|
||||
continue
|
||||
@@ -1131,7 +1131,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin && !profileCanAccessTenant(requester, appointmentTenant.ID, appointmentTenant.Slug) {
|
||||
if requester != nil && requester.Role == "tenant_admin" && !profileCanAccessTenant(requester, appointmentTenant.ID, appointmentTenant.Slug) {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
|
||||
appointmentFailed = true
|
||||
break
|
||||
@@ -1495,7 +1495,6 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
var requesterRole string
|
||||
var manageableSlugs []string
|
||||
var profile *domain.UserProfileResponse
|
||||
|
||||
// [New] Manual profile resolution to support query-param role mocking
|
||||
@@ -1508,7 +1507,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
slog.Info("🔑 [AUTH] Using mock role from query for export", "role", mockRole)
|
||||
requesterRole = mockRole
|
||||
// In dev mocking, we might not have a full profile, but we need to know the manageable tenants if it's a tenant_admin
|
||||
if requesterRole == domain.RoleTenantAdmin {
|
||||
if requesterRole == "tenant_admin" {
|
||||
// Try to get actual profile if possible to get manageableTenants
|
||||
p, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if p != nil {
|
||||
@@ -1525,42 +1524,19 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
requesterRole = profile.Role
|
||||
}
|
||||
|
||||
// [New] Access Control: only admin roles can export
|
||||
if requesterRole != domain.RoleSuperAdmin && requesterRole != domain.RoleTenantAdmin {
|
||||
// [New] Access Control: only super_admin can export
|
||||
if requesterRole != domain.RoleSuperAdmin {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for export")
|
||||
}
|
||||
|
||||
if profile != nil && requesterRole == domain.RoleTenantAdmin {
|
||||
for _, t := range profile.ManageableTenants {
|
||||
manageableSlugs = append(manageableSlugs, strings.ToLower(t.Slug))
|
||||
manageableSlugs = append(manageableSlugs, strings.ToLower(t.ID))
|
||||
}
|
||||
if profile.TenantID != nil {
|
||||
manageableSlugs = append(manageableSlugs, strings.ToLower(*profile.TenantID))
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Fetch Users using Repo for efficiency
|
||||
users, _, err := h.UserRepo.List(c.Context(), 0, 10000, search, tenantSlug)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch users for export")
|
||||
}
|
||||
|
||||
// 2. Filter by manageable tenants if tenant_admin
|
||||
var filtered []domain.User
|
||||
if requesterRole == domain.RoleTenantAdmin {
|
||||
slugMap := make(map[string]bool)
|
||||
for _, s := range manageableSlugs {
|
||||
slugMap[s] = true
|
||||
}
|
||||
for _, u := range users {
|
||||
if slugMap[strings.ToLower(userTenantSlug(u))] || slugMap[strings.ToLower(userTenantID(u))] {
|
||||
filtered = append(filtered, u)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filtered = users
|
||||
}
|
||||
// 2. Data rows
|
||||
filtered := users
|
||||
|
||||
// 3. Set CSV Headers
|
||||
c.Set("Content-Type", "text/csv; charset=utf-8")
|
||||
@@ -1700,7 +1676,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
tenantCache := make(map[string]tenantCacheItem)
|
||||
|
||||
manageableSlugs := make(map[string]bool)
|
||||
if requester.Role == domain.RoleTenantAdmin {
|
||||
if requester.Role != domain.RoleSuperAdmin {
|
||||
manageableSlugs = profileTenantAccessKeys(requester)
|
||||
}
|
||||
|
||||
@@ -1724,7 +1700,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Authorization check
|
||||
if requester.Role == domain.RoleTenantAdmin {
|
||||
if requester.Role != domain.RoleSuperAdmin {
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), manageableSlugs) {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: user belongs to another tenant"})
|
||||
continue
|
||||
@@ -1858,7 +1834,7 @@ func (h *UserHandler) BulkDeleteUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
manageableSlugs := make(map[string]bool)
|
||||
if requester.Role == domain.RoleTenantAdmin {
|
||||
if requester.Role != domain.RoleSuperAdmin {
|
||||
manageableSlugs = profileTenantAccessKeys(requester)
|
||||
}
|
||||
|
||||
@@ -1882,7 +1858,7 @@ func (h *UserHandler) BulkDeleteUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Authorization check
|
||||
if requester.Role == domain.RoleTenantAdmin {
|
||||
if requester.Role != domain.RoleSuperAdmin {
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), manageableSlugs) {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden"})
|
||||
continue
|
||||
@@ -1951,7 +1927,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
|
||||
// [New] Check access scope
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin {
|
||||
allowed := map[string]bool{}
|
||||
if requester.TenantID != nil {
|
||||
allowed[strings.ToLower(*requester.TenantID)] = true
|
||||
@@ -2006,8 +1982,8 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
*req.Role = role
|
||||
}
|
||||
|
||||
// Tenant admins can only move users within tenants they can manage.
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
|
||||
// All non-superadmins can only move users within tenants they can manage.
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin {
|
||||
if !req.IsAddTenant && !req.IsRemoveTenant && req.CompanyCode != nil {
|
||||
targetSlug := strings.TrimSpace(*req.CompanyCode)
|
||||
targetAllowed := profileCanAccessTenant(requester, "", targetSlug)
|
||||
@@ -2017,13 +1993,13 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
if !targetAllowed {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: tenant admins cannot change user's tenant")
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: non-superadmins cannot change user's tenant to an unmanageable one")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [Validation] Based on Tenant Schema (Multi-tenant aware)
|
||||
isAdmin := requester != nil && (requester.Role == domain.RoleSuperAdmin || requester.Role == domain.RoleTenantAdmin)
|
||||
isAdmin := requester != nil && requester.Role == domain.RoleSuperAdmin
|
||||
|
||||
// If metadata is namespaced (key is tenant ID), validate each namespace
|
||||
// If it's flat, validate using schemaCompCode
|
||||
@@ -2360,13 +2336,13 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
var identity *service.KratosIdentity
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin || h.Worksmobile != nil {
|
||||
if (requester != nil && domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin) || h.Worksmobile != nil {
|
||||
found, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
|
||||
if err == nil {
|
||||
identity = found
|
||||
}
|
||||
}
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin {
|
||||
if identity != nil {
|
||||
allowed := map[string]bool{}
|
||||
if requester.TenantID != nil {
|
||||
@@ -2763,9 +2739,9 @@ func (h *UserHandler) syncKetoRole(ctx context.Context, userID, newRole, oldRole
|
||||
return // Nothing changed
|
||||
}
|
||||
|
||||
// 1. Handle Role Changes
|
||||
// 1. Handle Role Changes (Super Admin Only)
|
||||
if oldRole == domain.RoleSuperAdmin {
|
||||
// Only remove super_admin if the role actually changed (tenant change doesn't matter for global roles)
|
||||
// Only remove super_admin if the role actually changed
|
||||
if oldRole != newRole {
|
||||
_ = h.KetoOutboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "System",
|
||||
@@ -2775,14 +2751,6 @@ func (h *UserHandler) syncKetoRole(ctx context.Context, userID, newRole, oldRole
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
})
|
||||
}
|
||||
} else if oldRole == domain.RoleTenantAdmin && oldTenantID != "" {
|
||||
_ = h.KetoOutboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: oldTenantID,
|
||||
Relation: "admins",
|
||||
Subject: "User:" + userID,
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
})
|
||||
}
|
||||
|
||||
// Add new roles
|
||||
@@ -2796,14 +2764,6 @@ func (h *UserHandler) syncKetoRole(ctx context.Context, userID, newRole, oldRole
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
} else if newRole == domain.RoleTenantAdmin && newTID != "" {
|
||||
_ = h.KetoOutboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: newTID,
|
||||
Relation: "admins",
|
||||
Subject: "User:" + userID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
|
||||
// 2. Handle Tenant Membership (for count)
|
||||
|
||||
Reference in New Issue
Block a user