forked from baron/baron-sso
Merge remote-tracking branch 'origin/main'
This commit is contained in:
140
backend/internal/middleware/rbac.go
Normal file
140
backend/internal/middleware/rbac.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/handler"
|
||||
"baron-sso-backend/internal/service"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// RBACConfig defines the configuration for RBAC middleware
|
||||
type RBACConfig struct {
|
||||
AllowedRoles []string
|
||||
AuthHandler *handler.AuthHandler
|
||||
KetoService service.KetoService
|
||||
}
|
||||
|
||||
// RequireKetoPermission enforces permissions using Ory Keto (ReBAC)
|
||||
func RequireKetoPermission(config RBACConfig, namespace, relation string) 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"})
|
||||
}
|
||||
|
||||
// Super Admin bypass
|
||||
if profile.Role == domain.RoleSuperAdmin {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// Get object ID from path (e.g., tenant ID)
|
||||
objectID := c.Params("id")
|
||||
if objectID == "" {
|
||||
objectID = c.Params("tenantId")
|
||||
}
|
||||
|
||||
if objectID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "missing object id for permission check"})
|
||||
}
|
||||
|
||||
// Check with Keto
|
||||
allowed, err := config.KetoService.CheckPermission(c.Context(), profile.ID, namespace, objectID, relation)
|
||||
if err != nil || !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"})
|
||||
}
|
||||
|
||||
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: " + err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// 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"})
|
||||
}
|
||||
|
||||
// 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 profile.TenantID == nil || *profile.TenantID != 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"})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user