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>
243 lines
6.0 KiB
Go
243 lines
6.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"tyto/internal/database"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// RolesAPI provides HTTP handlers for role management.
|
|
type RolesAPI struct {
|
|
db database.Database
|
|
auth *Authorizer
|
|
}
|
|
|
|
// NewRolesAPI creates a new roles API handler.
|
|
func NewRolesAPI(db database.Database, auth *Authorizer) *RolesAPI {
|
|
return &RolesAPI{
|
|
db: db,
|
|
auth: auth,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers role management routes.
|
|
func (a *RolesAPI) RegisterRoutes(group *gin.RouterGroup, authMiddleware *Middleware) {
|
|
roles := group.Group("/roles")
|
|
roles.Use(authMiddleware.Required())
|
|
{
|
|
// View roles (requires roles:view)
|
|
roles.GET("", a.auth.RequirePermission(PermRolesView), a.handleListRoles)
|
|
roles.GET("/:id", a.auth.RequirePermission(PermRolesView), a.handleGetRole)
|
|
|
|
// Manage roles (requires roles:manage)
|
|
roles.POST("", a.auth.RequirePermission(PermRolesManage), a.handleCreateRole)
|
|
roles.PUT("/:id", a.auth.RequirePermission(PermRolesManage), a.handleUpdateRole)
|
|
roles.DELETE("/:id", a.auth.RequirePermission(PermRolesManage), a.handleDeleteRole)
|
|
}
|
|
}
|
|
|
|
// RoleResponse represents a role in API responses.
|
|
type RoleResponse struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Permissions []string `json:"permissions"`
|
|
IsSystem bool `json:"isSystem"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
func roleToResponse(r *database.Role) RoleResponse {
|
|
return RoleResponse{
|
|
ID: r.ID,
|
|
Name: r.Name,
|
|
Description: r.Description,
|
|
Permissions: r.Permissions,
|
|
IsSystem: r.IsSystem,
|
|
CreatedAt: r.CreatedAt.Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func (a *RolesAPI) handleListRoles(c *gin.Context) {
|
|
roles, err := a.db.ListRoles(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list roles"})
|
|
return
|
|
}
|
|
|
|
response := make([]RoleResponse, len(roles))
|
|
for i, r := range roles {
|
|
response[i] = roleToResponse(r)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
func (a *RolesAPI) handleGetRole(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
role, err := a.db.GetRole(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get role"})
|
|
return
|
|
}
|
|
|
|
if role == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "role not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, roleToResponse(role))
|
|
}
|
|
|
|
// CreateRoleRequest represents a role creation request.
|
|
type CreateRoleRequest struct {
|
|
Name string `json:"name" binding:"required,min=2,max=64"`
|
|
Description string `json:"description"`
|
|
Permissions []string `json:"permissions" binding:"required,min=1"`
|
|
}
|
|
|
|
func (a *RolesAPI) handleCreateRole(c *gin.Context) {
|
|
var req CreateRoleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
|
return
|
|
}
|
|
|
|
// Validate permissions
|
|
if !a.validatePermissions(req.Permissions) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid permissions"})
|
|
return
|
|
}
|
|
|
|
role := &database.Role{
|
|
ID: generateID(),
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
Permissions: req.Permissions,
|
|
IsSystem: false,
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
|
|
if err := a.db.CreateRole(c.Request.Context(), role); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create role"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, roleToResponse(role))
|
|
}
|
|
|
|
// UpdateRoleRequest represents a role update request.
|
|
type UpdateRoleRequest struct {
|
|
Name string `json:"name" binding:"required,min=2,max=64"`
|
|
Description string `json:"description"`
|
|
Permissions []string `json:"permissions" binding:"required,min=1"`
|
|
}
|
|
|
|
func (a *RolesAPI) handleUpdateRole(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
role, err := a.db.GetRole(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get role"})
|
|
return
|
|
}
|
|
|
|
if role == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "role not found"})
|
|
return
|
|
}
|
|
|
|
if role.IsSystem {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "cannot modify system role"})
|
|
return
|
|
}
|
|
|
|
var req UpdateRoleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
|
return
|
|
}
|
|
|
|
// Validate permissions
|
|
if !a.validatePermissions(req.Permissions) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid permissions"})
|
|
return
|
|
}
|
|
|
|
role.Name = req.Name
|
|
role.Description = req.Description
|
|
role.Permissions = req.Permissions
|
|
|
|
if err := a.db.UpdateRole(c.Request.Context(), role); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update role"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, roleToResponse(role))
|
|
}
|
|
|
|
func (a *RolesAPI) handleDeleteRole(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
role, err := a.db.GetRole(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get role"})
|
|
return
|
|
}
|
|
|
|
if role == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "role not found"})
|
|
return
|
|
}
|
|
|
|
if role.IsSystem {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete system role"})
|
|
return
|
|
}
|
|
|
|
if err := a.db.DeleteRole(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete role"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "role deleted"})
|
|
}
|
|
|
|
// validatePermissions checks if all permissions are valid.
|
|
func (a *RolesAPI) validatePermissions(perms []string) bool {
|
|
validPerms := make(map[string]bool)
|
|
for _, p := range AllPermissions() {
|
|
validPerms[string(p)] = true
|
|
}
|
|
validPerms["*"] = true // Wildcard is valid
|
|
|
|
for _, p := range perms {
|
|
// Check exact match
|
|
if validPerms[p] {
|
|
continue
|
|
}
|
|
|
|
// Check category wildcard (e.g., "agents:*")
|
|
if len(p) > 2 && p[len(p)-2:] == ":*" {
|
|
prefix := p[:len(p)-1]
|
|
found := false
|
|
for valid := range validPerms {
|
|
if len(valid) > len(prefix) && valid[:len(prefix)] == prefix {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
continue
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|