veylant/internal/router/rbac.go
2026-02-23 13:35:04 +01:00

68 lines
2.1 KiB
Go

// Package router implements model-based provider routing and RBAC for the Veylant proxy.
package router
import (
"strings"
"github.com/veylant/ia-gateway/internal/apierror"
"github.com/veylant/ia-gateway/internal/config"
)
// Canonical role names as defined in Keycloak.
const (
roleAdmin = "admin"
roleManager = "manager"
roleUser = "user"
roleAuditor = "auditor"
)
// HasAccess returns nil if a user holding roles may use model, or a 403 error.
//
// Rules (evaluated top to bottom):
// - admin / manager → unrestricted access to all models
// - auditor → 403 unless cfg.AuditorCanComplete is true
// - user → allowed only if model matches cfg.UserAllowedModels
// (exact or prefix, e.g. "gpt-4o-mini" matches "gpt-4o-mini-2024-07-18")
// - unknown role → treated as user (fail-safe)
func HasAccess(roles []string, model string, cfg *config.RBACConfig) error {
if hasRole(roles, roleAdmin) || hasRole(roles, roleManager) {
return nil
}
if hasRole(roles, roleAuditor) {
if cfg.AuditorCanComplete {
return nil
}
return apierror.NewForbiddenError("auditors are not permitted to make completion requests")
}
// User role (or any unknown role treated as user for fail-safe behaviour).
if modelMatchesAny(model, cfg.UserAllowedModels) {
return nil
}
allowed := strings.Join(cfg.UserAllowedModels, ", ")
return apierror.NewForbiddenError(
"model \"" + model + "\" is not available for your role — allowed models for your role: [" + allowed + "]. Contact your administrator to request access.",
)
}
// hasRole reports whether role appears in roles (case-insensitive).
func hasRole(roles []string, role string) bool {
for _, r := range roles {
if strings.EqualFold(r, role) {
return true
}
}
return false
}
// modelMatchesAny returns true if model equals any pattern or starts with any pattern.
// This allows exact matches ("gpt-4o-mini") and prefix matches
// ("gpt-4o-mini" matches "gpt-4o-mini-2024-07-18").
func modelMatchesAny(model string, patterns []string) bool {
for _, p := range patterns {
if model == p || strings.HasPrefix(model, p) {
return true
}
}
return false
}