forked from baron/baron-sso
- 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.
167 lines
4.9 KiB
Go
167 lines
4.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"baron-sso-backend/internal/service"
|
|
"log/slog"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
// RBACConfig defines the configuration for RBAC middleware
|
|
type RBACConfig struct {
|
|
AllowedRoles []string
|
|
AuthHandler AuthProfileProvider
|
|
KetoService service.KetoService
|
|
}
|
|
|
|
// AuthProfileProvider는 미들웨어에서 사용자 정보를 조회하기 위한 최소 인터페이스입니다.
|
|
type AuthProfileProvider interface {
|
|
GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
|
|
}
|
|
|
|
func setAuditUserContext(c *fiber.Ctx, profile *domain.UserProfileResponse) {
|
|
if profile == nil || profile.ID == "" {
|
|
return
|
|
}
|
|
if existingUserID, _ := c.Locals("user_id").(string); existingUserID != "" {
|
|
return
|
|
}
|
|
c.Locals("user_id", profile.ID)
|
|
}
|
|
|
|
// RequireKetoPermission enforces permissions using Ory Keto (ReBAC)
|
|
func RequireKetoPermission(config RBACConfig, namespace, relation string) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
profile, err := config.AuthHandler.GetEnrichedProfile(c)
|
|
if err != nil {
|
|
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized (trace:rbac_keto)")
|
|
}
|
|
|
|
// Store profile in locals for further use in handlers
|
|
c.Locals("user_profile", profile)
|
|
setAuditUserContext(c, profile)
|
|
|
|
role := domain.NormalizeRole(profile.Role)
|
|
|
|
// Super Admin bypass
|
|
if role == domain.RoleSuperAdmin {
|
|
return c.Next()
|
|
}
|
|
|
|
// Get object ID from path (e.g., tenant ID)
|
|
// Fix: For Tenant namespace, prioritize tenantId param if available
|
|
objectID := ""
|
|
if namespace == "Tenant" {
|
|
objectID = c.Params("tenantId")
|
|
}
|
|
|
|
if objectID == "" {
|
|
objectID = c.Params("id")
|
|
}
|
|
|
|
if objectID == "" {
|
|
slog.Error("RBAC Keto check failed: missing object id", "path", c.Path())
|
|
return errorJSON(c, fiber.StatusBadRequest, "missing object id for permission check")
|
|
}
|
|
|
|
slog.Info("Performing Keto permission check", "userID", profile.ID, "namespace", namespace, "objectID", objectID, "relation", relation)
|
|
|
|
// Set tenant_id for audit logging if namespace is Tenant
|
|
if namespace == "Tenant" {
|
|
c.Locals("tenant_id", objectID)
|
|
}
|
|
|
|
// Check with Keto - add User: prefix to subject
|
|
allowed, err := config.KetoService.CheckPermission(c.Context(), "User:"+profile.ID, namespace, objectID, relation)
|
|
if err != nil {
|
|
slog.Error("Keto service error", "error", err, "userID", profile.ID, "objectID", objectID)
|
|
return errorJSON(c, fiber.StatusInternalServerError, "permission check error")
|
|
}
|
|
|
|
if !allowed {
|
|
slog.Warn("Keto permission denied", "userID", profile.ID, "userRole", role, "namespace", namespace, "objectID", objectID, "relation", relation, "X-Test-Role", c.Get("X-Test-Role"))
|
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: keto permission denied for "+namespace+":"+objectID)
|
|
}
|
|
|
|
return c.Next()
|
|
}
|
|
}
|
|
|
|
// RequireRole enforces that the user has one of the allowed roles
|
|
func RequireRole(config RBACConfig) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
// Bypass if already authenticated via API Key
|
|
if c.Locals("apiKeyName") != nil {
|
|
return c.Next()
|
|
}
|
|
|
|
profile, err := config.AuthHandler.GetEnrichedProfile(c)
|
|
if err != nil {
|
|
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized (trace:rbac_role): "+err.Error())
|
|
}
|
|
|
|
// Store profile in locals for further use in handlers
|
|
c.Locals("user_profile", profile)
|
|
setAuditUserContext(c, profile)
|
|
|
|
userRole := domain.NormalizeRole(profile.Role)
|
|
|
|
// Super Admin always has access
|
|
if userRole == domain.RoleSuperAdmin {
|
|
return c.Next()
|
|
}
|
|
|
|
// Check if user's role is in allowed roles
|
|
roleAllowed := false
|
|
for _, role := range config.AllowedRoles {
|
|
if userRole == domain.NormalizeRole(role) {
|
|
roleAllowed = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !roleAllowed {
|
|
slog.Warn("RBAC access denied",
|
|
"userID", profile.ID,
|
|
"userRole", userRole,
|
|
"allowedRoles", config.AllowedRoles,
|
|
"path", c.Path(),
|
|
"X-Test-Role", c.Get("X-Test-Role"),
|
|
)
|
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions")
|
|
}
|
|
|
|
return c.Next()
|
|
}
|
|
}
|
|
|
|
// RequireTenantMatch enforces that a Tenant Admin can only access their own tenant's data
|
|
func RequireTenantMatch(config RBACConfig) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
// Bypass if already authenticated via API Key (System-wide for now)
|
|
if c.Locals("apiKeyName") != nil {
|
|
return c.Next()
|
|
}
|
|
|
|
profile, err := config.AuthHandler.GetEnrichedProfile(c)
|
|
if err != nil {
|
|
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized (trace:rbac_match)")
|
|
}
|
|
|
|
// Store profile in locals for further use in handlers
|
|
c.Locals("user_profile", profile)
|
|
setAuditUserContext(c, profile)
|
|
|
|
userRole := domain.NormalizeRole(profile.Role)
|
|
|
|
// Super Admin bypass
|
|
if userRole == domain.RoleSuperAdmin {
|
|
return c.Next()
|
|
}
|
|
|
|
// Since only Super Admin is maintained for tenant management, others are rejected here
|
|
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
|
}
|
|
}
|