1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/middleware/rbac.go

187 lines
5.4 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)
}
// 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 c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized (trace:rbac_keto)"})
}
// Store profile in locals for further use in handlers
c.Locals("user_profile", profile)
// Super Admin bypass
if profile.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 c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "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
allowed, err := config.KetoService.CheckPermission(c.Context(), profile.ID, namespace, objectID, relation)
if err != nil {
slog.Error("Keto service error", "error", err, "userID", profile.ID, "objectID", objectID)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
}
if !allowed {
slog.Warn("Keto permission denied", "userID", profile.ID, "namespace", namespace, "objectID", objectID, "relation", relation)
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "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 c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "unauthorized (trace:rbac_role): " + err.Error(),
})
}
// Store profile in locals for further use in handlers
c.Locals("user_profile", profile)
// Super Admin always has access
if profile.Role == domain.RoleSuperAdmin {
return c.Next()
}
// Check if user's role is in allowed roles
roleAllowed := false
for _, role := range config.AllowedRoles {
if profile.Role == role {
roleAllowed = true
break
}
}
if !roleAllowed {
slog.Warn("RBAC access denied",
"userID", profile.ID,
"userRole", profile.Role,
"allowedRoles", config.AllowedRoles,
"path", c.Path(),
)
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "forbidden: insufficient permissions",
})
}
// Store profile in locals for further use in handlers
c.Locals("user_profile", profile)
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 c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized (trace:rbac_match)"})
}
// Store profile in locals for further use in handlers
c.Locals("user_profile", profile)
// Super Admin bypass
if profile.Role == domain.RoleSuperAdmin {
return c.Next()
}
// Tenant Admin check
if profile.Role == domain.RoleTenantAdmin {
targetTenantID := c.Params("tenantId")
if targetTenantID == "" {
targetTenantID = c.Params("id") // common for /tenants/:id
}
if targetTenantID == "" {
return c.Next() // No target specified, let Keto or next handler decide
}
// Check primary tenant match
if profile.TenantID != nil && *profile.TenantID == targetTenantID {
return c.Next()
}
// Check inherited manageable tenants
isAllowed := false
for _, t := range profile.ManageableTenants {
if t.ID == targetTenantID {
isAllowed = true
break
}
}
if !isAllowed {
slog.Warn("Tenant match failed", "userID", profile.ID, "targetTenantID", targetTenantID)
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "forbidden: you do not have access to this tenant",
})
}
return c.Next()
}
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden"})
}
}