Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
459
ai-compliance-sdk/internal/rbac/middleware.go
Normal file
459
ai-compliance-sdk/internal/rbac/middleware.go
Normal file
@@ -0,0 +1,459 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Context keys for RBAC data
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
ContextKeyUserID contextKey = "user_id"
|
||||
ContextKeyTenantID contextKey = "tenant_id"
|
||||
ContextKeyNamespaceID contextKey = "namespace_id"
|
||||
ContextKeyPermissions contextKey = "permissions"
|
||||
ContextKeyRoles contextKey = "roles"
|
||||
ContextKeyUserContext contextKey = "user_context"
|
||||
)
|
||||
|
||||
// Middleware provides RBAC middleware for Gin
|
||||
type Middleware struct {
|
||||
service *Service
|
||||
policyEngine *PolicyEngine
|
||||
}
|
||||
|
||||
// NewMiddleware creates a new RBAC middleware
|
||||
func NewMiddleware(service *Service, policyEngine *PolicyEngine) *Middleware {
|
||||
return &Middleware{
|
||||
service: service,
|
||||
policyEngine: policyEngine,
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractUserContext extracts user context from headers/JWT and stores in context
|
||||
// This middleware should run after authentication
|
||||
func (m *Middleware) ExtractUserContext() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Extract user ID from header (set by auth middleware)
|
||||
userIDStr := c.GetHeader("X-User-ID")
|
||||
if userIDStr == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract tenant ID (from header or default)
|
||||
tenantIDStr := c.GetHeader("X-Tenant-ID")
|
||||
if tenantIDStr == "" {
|
||||
// Try to get from query param
|
||||
tenantIDStr = c.Query("tenant_id")
|
||||
}
|
||||
if tenantIDStr == "" {
|
||||
// Use default tenant slug
|
||||
tenantIDStr = c.GetHeader("X-Tenant-Slug")
|
||||
if tenantIDStr != "" {
|
||||
tenant, err := m.service.store.GetTenantBySlug(c.Request.Context(), tenantIDStr)
|
||||
if err == nil {
|
||||
tenantIDStr = tenant.ID.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tenantID uuid.UUID
|
||||
if tenantIDStr != "" {
|
||||
tenantID, _ = uuid.Parse(tenantIDStr)
|
||||
}
|
||||
|
||||
// Extract namespace ID (optional)
|
||||
var namespaceID *uuid.UUID
|
||||
namespaceIDStr := c.GetHeader("X-Namespace-ID")
|
||||
if namespaceIDStr == "" {
|
||||
namespaceIDStr = c.Query("namespace_id")
|
||||
}
|
||||
if namespaceIDStr != "" {
|
||||
if nsID, err := uuid.Parse(namespaceIDStr); err == nil {
|
||||
namespaceID = &nsID
|
||||
}
|
||||
}
|
||||
|
||||
// Store in context
|
||||
c.Set(string(ContextKeyUserID), userID)
|
||||
c.Set(string(ContextKeyTenantID), tenantID)
|
||||
if namespaceID != nil {
|
||||
c.Set(string(ContextKeyNamespaceID), *namespaceID)
|
||||
}
|
||||
|
||||
// Get effective permissions
|
||||
if tenantID != uuid.Nil {
|
||||
perms, err := m.service.GetEffectivePermissions(c.Request.Context(), userID, tenantID, namespaceID)
|
||||
if err == nil {
|
||||
c.Set(string(ContextKeyPermissions), perms.Permissions)
|
||||
c.Set(string(ContextKeyRoles), perms.Roles)
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequirePermission requires the user to have a specific permission
|
||||
func (m *Middleware) RequirePermission(permission string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, tenantID, namespaceID := m.extractIDs(c)
|
||||
|
||||
if userID == uuid.Nil || tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission, err := m.service.HasPermission(c.Request.Context(), userID, tenantID, namespaceID, permission)
|
||||
if err != nil || !hasPermission {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Insufficient permissions",
|
||||
"required": permission,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAnyPermission requires the user to have any of the specified permissions
|
||||
func (m *Middleware) RequireAnyPermission(permissions ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, tenantID, namespaceID := m.extractIDs(c)
|
||||
|
||||
if userID == uuid.Nil || tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission, err := m.service.HasAnyPermission(c.Request.Context(), userID, tenantID, namespaceID, permissions)
|
||||
if err != nil || !hasPermission {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Insufficient permissions",
|
||||
"required": permissions,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAllPermissions requires the user to have all specified permissions
|
||||
func (m *Middleware) RequireAllPermissions(permissions ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, tenantID, namespaceID := m.extractIDs(c)
|
||||
|
||||
if userID == uuid.Nil || tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission, err := m.service.HasAllPermissions(c.Request.Context(), userID, tenantID, namespaceID, permissions)
|
||||
if err != nil || !hasPermission {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Insufficient permissions - all required",
|
||||
"required": permissions,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireNamespaceAccess requires access to the specified namespace
|
||||
func (m *Middleware) RequireNamespaceAccess(operation string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, tenantID, namespaceID := m.extractIDs(c)
|
||||
|
||||
if userID == uuid.Nil || tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Get namespace ID from URL param if not in context
|
||||
if namespaceID == nil {
|
||||
nsIDStr := c.Param("namespace_id")
|
||||
if nsIDStr == "" {
|
||||
nsIDStr = c.Param("namespaceId")
|
||||
}
|
||||
if nsIDStr != "" {
|
||||
if nsID, err := uuid.Parse(nsIDStr); err == nil {
|
||||
namespaceID = &nsID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if namespaceID == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "bad_request",
|
||||
"message": "Namespace ID required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
result, err := m.policyEngine.EvaluateNamespaceAccess(c.Request.Context(), &NamespaceAccessRequest{
|
||||
UserID: userID,
|
||||
TenantID: tenantID,
|
||||
NamespaceID: *namespaceID,
|
||||
Operation: operation,
|
||||
})
|
||||
|
||||
if err != nil || !result.Allowed {
|
||||
reason := "access denied"
|
||||
if result != nil {
|
||||
reason = result.Reason
|
||||
}
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Namespace access denied",
|
||||
"reason": reason,
|
||||
"namespace": namespaceID.String(),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Store namespace access result in context
|
||||
c.Set("namespace_access", result)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireLLMAccess validates LLM access based on policy
|
||||
func (m *Middleware) RequireLLMAccess() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, tenantID, namespaceID := m.extractIDs(c)
|
||||
|
||||
if userID == uuid.Nil || tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Basic LLM permission check
|
||||
hasPermission, err := m.service.HasAnyPermission(c.Request.Context(), userID, tenantID, namespaceID, []string{
|
||||
PermissionLLMAll,
|
||||
PermissionLLMQuery,
|
||||
PermissionLLMOwnQuery,
|
||||
})
|
||||
|
||||
if err != nil || !hasPermission {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "LLM access denied",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRole requires the user to have a specific role
|
||||
func (m *Middleware) RequireRole(role string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
roles, exists := c.Get(string(ContextKeyRoles))
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
roleSlice, ok := roles.([]string)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "internal_error",
|
||||
"message": "Invalid role data",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
hasRole := false
|
||||
for _, r := range roleSlice {
|
||||
if r == role {
|
||||
hasRole = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasRole {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Required role missing",
|
||||
"required": role,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// extractIDs extracts user, tenant, and namespace IDs from context
|
||||
func (m *Middleware) extractIDs(c *gin.Context) (uuid.UUID, uuid.UUID, *uuid.UUID) {
|
||||
var userID, tenantID uuid.UUID
|
||||
var namespaceID *uuid.UUID
|
||||
|
||||
if id, exists := c.Get(string(ContextKeyUserID)); exists {
|
||||
userID = id.(uuid.UUID)
|
||||
}
|
||||
if id, exists := c.Get(string(ContextKeyTenantID)); exists {
|
||||
tenantID = id.(uuid.UUID)
|
||||
}
|
||||
if id, exists := c.Get(string(ContextKeyNamespaceID)); exists {
|
||||
nsID := id.(uuid.UUID)
|
||||
namespaceID = &nsID
|
||||
}
|
||||
|
||||
return userID, tenantID, namespaceID
|
||||
}
|
||||
|
||||
// GetUserID retrieves user ID from Gin context
|
||||
func GetUserID(c *gin.Context) uuid.UUID {
|
||||
if id, exists := c.Get(string(ContextKeyUserID)); exists {
|
||||
return id.(uuid.UUID)
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
// GetTenantID retrieves tenant ID from Gin context
|
||||
func GetTenantID(c *gin.Context) uuid.UUID {
|
||||
if id, exists := c.Get(string(ContextKeyTenantID)); exists {
|
||||
return id.(uuid.UUID)
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
// GetNamespaceID retrieves namespace ID from Gin context
|
||||
func GetNamespaceID(c *gin.Context) *uuid.UUID {
|
||||
if id, exists := c.Get(string(ContextKeyNamespaceID)); exists {
|
||||
nsID := id.(uuid.UUID)
|
||||
return &nsID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPermissions retrieves permissions from Gin context
|
||||
func GetPermissions(c *gin.Context) []string {
|
||||
if perms, exists := c.Get(string(ContextKeyPermissions)); exists {
|
||||
return perms.([]string)
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// GetRoles retrieves roles from Gin context
|
||||
func GetRoles(c *gin.Context) []string {
|
||||
if roles, exists := c.Get(string(ContextKeyRoles)); exists {
|
||||
return roles.([]string)
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// ContextWithUserID adds user ID to context
|
||||
func ContextWithUserID(ctx context.Context, userID uuid.UUID) context.Context {
|
||||
return context.WithValue(ctx, ContextKeyUserID, userID)
|
||||
}
|
||||
|
||||
// ContextWithTenantID adds tenant ID to context
|
||||
func ContextWithTenantID(ctx context.Context, tenantID uuid.UUID) context.Context {
|
||||
return context.WithValue(ctx, ContextKeyTenantID, tenantID)
|
||||
}
|
||||
|
||||
// ContextWithNamespaceID adds namespace ID to context
|
||||
func ContextWithNamespaceID(ctx context.Context, namespaceID uuid.UUID) context.Context {
|
||||
return context.WithValue(ctx, ContextKeyNamespaceID, namespaceID)
|
||||
}
|
||||
|
||||
// UserIDFromContext retrieves user ID from standard context
|
||||
func UserIDFromContext(ctx context.Context) uuid.UUID {
|
||||
if id, ok := ctx.Value(ContextKeyUserID).(uuid.UUID); ok {
|
||||
return id
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
// TenantIDFromContext retrieves tenant ID from standard context
|
||||
func TenantIDFromContext(ctx context.Context) uuid.UUID {
|
||||
if id, ok := ctx.Value(ContextKeyTenantID).(uuid.UUID); ok {
|
||||
return id
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
// NamespaceIDFromContext retrieves namespace ID from standard context
|
||||
func NamespaceIDFromContext(ctx context.Context) *uuid.UUID {
|
||||
if id, ok := ctx.Value(ContextKeyNamespaceID).(uuid.UUID); ok {
|
||||
return &id
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasPermissionFromHeader checks permission from header-based context (for API keys)
|
||||
func (m *Middleware) HasPermissionFromHeader(c *gin.Context, permission string) bool {
|
||||
// Check X-API-Permissions header (set by API key auth)
|
||||
permsHeader := c.GetHeader("X-API-Permissions")
|
||||
if permsHeader != "" {
|
||||
perms := strings.Split(permsHeader, ",")
|
||||
for _, p := range perms {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == permission {
|
||||
return true
|
||||
}
|
||||
// Wildcard check
|
||||
if strings.HasSuffix(p, ":*") {
|
||||
prefix := strings.TrimSuffix(p, "*")
|
||||
if strings.HasPrefix(permission, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user