1
0
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:
2026-06-02 18:29:18 +09:00
parent 57f05e2694
commit 802bf3e91d
32 changed files with 487 additions and 938 deletions

View File

@@ -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)