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 }