Files
tyto/backend/internal/auth/rbac.go
vikingowl 7b746643c7 feat: add RBAC with permission model and management APIs
RBAC System (internal/auth/):
- Permission constants for all resources (dashboard, agents, alerts, etc.)
- Wildcard permission support ("*" for admin, "category:*" for groups)
- Authorizer service with role-based permission checking
- RequirePermission middleware for route protection

Role Management API:
- GET /roles - List all roles
- GET /roles/:id - Get role details
- POST /roles - Create custom role
- PUT /roles/:id - Update role (custom only)
- DELETE /roles/:id - Delete role (custom only)

User Management API (admin):
- GET /users - List all users
- GET /users/:id - Get user details
- GET /users/:id/roles - Get user's roles
- POST /users - Create new user
- PUT /users/:id - Update user profile
- DELETE /users/:id - Disable user account
- POST /users/:id/enable - Re-enable user
- POST /users/:id/reset-password - Reset password
- PUT /users/:id/roles - Assign roles to user

Built-in Roles (via database migrations):
- admin: Full access (*)
- operator: Agent and alert management
- viewer: Read-only dashboard access

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 08:28:22 +01:00

244 lines
5.7 KiB
Go

package auth
import (
"context"
"net/http"
"strings"
"tyto/internal/database"
"github.com/gin-gonic/gin"
)
// Permission represents a single permission.
type Permission string
// Permission constants for Tyto resources.
const (
// Dashboard permissions
PermDashboardView Permission = "dashboard:view"
// Agent permissions
PermAgentsView Permission = "agents:view"
PermAgentsManage Permission = "agents:manage" // Add, remove, approve
// Alert permissions
PermAlertsView Permission = "alerts:view"
PermAlertsAcknowledge Permission = "alerts:acknowledge"
PermAlertsConfigure Permission = "alerts:configure"
// Metrics permissions
PermMetricsQuery Permission = "metrics:query"
PermMetricsExport Permission = "metrics:export"
// User permissions
PermUsersView Permission = "users:view"
PermUsersManage Permission = "users:manage" // Create, update, disable
// Role permissions
PermRolesView Permission = "roles:view"
PermRolesManage Permission = "roles:manage"
// Settings permissions
PermSettingsView Permission = "settings:view"
PermSettingsManage Permission = "settings:manage"
// PKI permissions
PermPKIManage Permission = "pki:manage"
// Audit log permissions
PermAuditView Permission = "audit:view"
)
// AllPermissions returns all defined permissions.
func AllPermissions() []Permission {
return []Permission{
PermDashboardView,
PermAgentsView,
PermAgentsManage,
PermAlertsView,
PermAlertsAcknowledge,
PermAlertsConfigure,
PermMetricsQuery,
PermMetricsExport,
PermUsersView,
PermUsersManage,
PermRolesView,
PermRolesManage,
PermSettingsView,
PermSettingsManage,
PermPKIManage,
PermAuditView,
}
}
// Authorizer provides authorization checking.
type Authorizer struct {
db database.Database
}
// NewAuthorizer creates a new authorizer.
func NewAuthorizer(db database.Database) *Authorizer {
return &Authorizer{db: db}
}
// HasPermission checks if a user has a specific permission.
func (a *Authorizer) HasPermission(ctx context.Context, userID string, perm Permission) bool {
roles, err := a.db.GetUserRoles(ctx, userID)
if err != nil {
return false
}
return a.checkRolesPermission(roles, perm)
}
// HasAnyPermission checks if a user has any of the specified permissions.
func (a *Authorizer) HasAnyPermission(ctx context.Context, userID string, perms ...Permission) bool {
roles, err := a.db.GetUserRoles(ctx, userID)
if err != nil {
return false
}
for _, perm := range perms {
if a.checkRolesPermission(roles, perm) {
return true
}
}
return false
}
// HasAllPermissions checks if a user has all specified permissions.
func (a *Authorizer) HasAllPermissions(ctx context.Context, userID string, perms ...Permission) bool {
roles, err := a.db.GetUserRoles(ctx, userID)
if err != nil {
return false
}
for _, perm := range perms {
if !a.checkRolesPermission(roles, perm) {
return false
}
}
return true
}
// GetUserPermissions returns all permissions for a user.
func (a *Authorizer) GetUserPermissions(ctx context.Context, userID string) []Permission {
roles, err := a.db.GetUserRoles(ctx, userID)
if err != nil {
return nil
}
permSet := make(map[Permission]bool)
for _, role := range roles {
for _, p := range role.Permissions {
if p == "*" {
// Wildcard - return all permissions
return AllPermissions()
}
permSet[Permission(p)] = true
}
}
perms := make([]Permission, 0, len(permSet))
for p := range permSet {
perms = append(perms, p)
}
return perms
}
// checkRolesPermission checks if any of the roles grant the permission.
func (a *Authorizer) checkRolesPermission(roles []*database.Role, perm Permission) bool {
permStr := string(perm)
for _, role := range roles {
for _, p := range role.Permissions {
// Exact match
if p == permStr {
return true
}
// Wildcard match: "*" matches everything
if p == "*" {
return true
}
// Category wildcard: "agents:*" matches "agents:view", "agents:manage"
if strings.HasSuffix(p, ":*") {
prefix := strings.TrimSuffix(p, "*")
if strings.HasPrefix(permStr, prefix) {
return true
}
}
}
}
return false
}
// RequirePermission returns middleware that requires a specific permission.
func (a *Authorizer) RequirePermission(perm Permission) gin.HandlerFunc {
return func(c *gin.Context) {
user := GetUser(c)
if user == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "authentication required",
})
return
}
if !a.HasPermission(c.Request.Context(), user.ID, perm) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "insufficient permissions",
})
return
}
c.Next()
}
}
// RequireAnyPermission returns middleware that requires any of the specified permissions.
func (a *Authorizer) RequireAnyPermission(perms ...Permission) gin.HandlerFunc {
return func(c *gin.Context) {
user := GetUser(c)
if user == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "authentication required",
})
return
}
if !a.HasAnyPermission(c.Request.Context(), user.ID, perms...) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "insufficient permissions",
})
return
}
c.Next()
}
}
// RequireAllPermissions returns middleware that requires all specified permissions.
func (a *Authorizer) RequireAllPermissions(perms ...Permission) gin.HandlerFunc {
return func(c *gin.Context) {
user := GetUser(c)
if user == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "authentication required",
})
return
}
if !a.HasAllPermissions(c.Request.Context(), user.ID, perms...) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "insufficient permissions",
})
return
}
c.Next()
}
}