Files
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

257 lines
8.7 KiB
Go

package api
import (
"errors"
"net/http"
"regexp"
"strconv"
"strings"
"gardomatic.kleiax.de/internal/storage"
"github.com/julienschmidt/httprouter"
)
var roleNameRX = regexp.MustCompile(`^[a-z][a-z0-9:_-]{1,63}$`)
func validRolePermissions(scope storage.RoleScope, permissions []string) bool {
for _, permission := range permissions {
if scope == storage.RoleScopeApplication && !storage.ValidApplicationPermission(permission) ||
scope == storage.RoleScopeGarden && !storage.ValidGardenPermission(permission) {
return false
}
}
return scope == storage.RoleScopeApplication || scope == storage.RoleScopeGarden
}
func (app *application) listAdminRolesHandler(w http.ResponseWriter, r *http.Request) {
applicationRoles, err := app.models.Roles.List(storage.RoleScopeApplication)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
gardenRoles, err := app.models.Roles.List(storage.RoleScopeGarden)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
app.writeJSON(w, http.StatusOK, envelope{"application_roles": applicationRoles, "garden_roles": gardenRoles}, nil)
}
func (app *application) createAdminRoleHandler(w http.ResponseWriter, r *http.Request) {
var input struct {
Name string `json:"name"`
Scope storage.RoleScope `json:"scope"`
Label string `json:"label"`
Permissions []string `json:"permissions"`
}
if err := app.readJSON(w, r, &input); err != nil {
app.badRequestResponse(w, r, err)
return
}
input.Name, input.Label = strings.TrimSpace(input.Name), strings.TrimSpace(input.Label)
if input.Scope == storage.RoleScopeApplication && !strings.HasPrefix(input.Name, "application:") {
input.Name = "application:" + input.Name
}
if !roleNameRX.MatchString(input.Name) || input.Label == "" || !validRolePermissions(input.Scope, input.Permissions) {
app.failedValidationResponse(w, r, map[string]string{"role": "name, scope, label, or permissions are invalid"})
return
}
role, err := app.models.Roles.Create(storage.Role{Name: input.Name, Scope: input.Scope, Label: input.Label, Permissions: input.Permissions})
if err != nil {
if errors.Is(err, storage.ErrConflict) {
app.conflictResponse(w, r)
} else {
app.serverErrorResponse(w, r, err)
}
return
}
app.writeJSON(w, http.StatusCreated, envelope{"role": role}, nil)
}
func (app *application) updateAdminRoleHandler(w http.ResponseWriter, r *http.Request) {
name := httprouter.ParamsFromContext(r.Context()).ByName("roleName")
role, err := app.models.Roles.Get(name)
if err != nil || role.GardenID != nil {
app.notFoundResponse(w, r)
return
}
var input struct {
Label string `json:"label"`
Permissions []string `json:"permissions"`
}
if err = app.readJSON(w, r, &input); err != nil {
app.badRequestResponse(w, r, err)
return
}
input.Label = strings.TrimSpace(input.Label)
if input.Label == "" || !validRolePermissions(role.Scope, input.Permissions) {
app.failedValidationResponse(w, r, map[string]string{"role": "label or permissions are invalid"})
return
}
if role.Name == string(storage.GardenRoleOwner) {
app.permissionDeniedResponse(w, r)
return
}
if role.Name == string(storage.ApplicationRoleAdmin) {
hasRoleManagement := false
for _, permission := range input.Permissions {
if permission == string(storage.ApplicationPermissionRolesManage) || permission == "*" {
hasRoleManagement = true
}
}
if !hasRoleManagement {
app.failedValidationResponse(w, r, map[string]string{"permissions": "the administrator role must retain roles:manage"})
return
}
}
role.Label, role.Permissions = input.Label, input.Permissions
role, err = app.models.Roles.Update(role)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
app.writeJSON(w, http.StatusOK, envelope{"role": role}, nil)
}
func (app *application) deleteAdminRoleHandler(w http.ResponseWriter, r *http.Request) {
name := httprouter.ParamsFromContext(r.Context()).ByName("roleName")
role, err := app.models.Roles.Get(name)
if err != nil || role.GardenID != nil {
app.notFoundResponse(w, r)
return
}
if err := app.models.Roles.Delete(name); err != nil {
if errors.Is(err, storage.ErrConflict) {
app.conflictResponse(w, r)
} else {
app.serverErrorResponse(w, r, err)
}
return
}
w.WriteHeader(http.StatusNoContent)
}
type gardenRoleSetting struct {
Role storage.Role `json:"role"`
EffectivePermissions []storage.GardenPermission `json:"effective_permissions"`
}
func (app *application) listGardenRoleSettingsHandler(w http.ResponseWriter, r *http.Request) {
gardenID, _ := app.readGardenIDParam(r)
roles, err := app.models.Roles.ListForGarden(gardenID)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
overrides, err := app.models.Roles.ListGardenOverrides(gardenID)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
settings := make([]gardenRoleSetting, 0, len(roles))
for _, role := range roles {
selected := []storage.GardenRolePermissionOverride{}
for _, override := range overrides {
if override.RoleName == role.Name {
selected = append(selected, override)
}
}
settings = append(settings, gardenRoleSetting{Role: role, EffectivePermissions: storage.ResolveGardenPermissions(role.Permissions, selected)})
}
app.writeJSON(w, http.StatusOK, envelope{"roles": settings}, nil)
}
func (app *application) updateGardenRoleSettingsHandler(w http.ResponseWriter, r *http.Request) {
gardenID, _ := app.readGardenIDParam(r)
name := httprouter.ParamsFromContext(r.Context()).ByName("roleName")
role, err := app.models.Roles.GetForGarden(gardenID, name)
if err != nil {
app.notFoundResponse(w, r)
return
}
if name == string(storage.GardenRoleOwner) {
app.permissionDeniedResponse(w, r)
return
}
var input struct {
Permissions []string `json:"permissions"`
}
if err = app.readJSON(w, r, &input); err != nil {
app.badRequestResponse(w, r, err)
return
}
if !validRolePermissions(storage.RoleScopeGarden, input.Permissions) {
app.failedValidationResponse(w, r, map[string]string{"permissions": "contains an invalid garden permission"})
return
}
desired := map[string]bool{}
for _, permission := range storage.ResolveGardenPermissions(input.Permissions, nil) {
desired[string(permission)] = true
}
base := storage.ResolveGardenPermissions(role.Permissions, nil)
baseSet := map[string]bool{}
for _, permission := range base {
baseSet[string(permission)] = true
}
overrides := []storage.GardenRolePermissionOverride{}
for _, permission := range storage.AllGardenPermissions() {
want, has := desired[string(permission)], baseSet[string(permission)]
if want != has {
overrides = append(overrides, storage.GardenRolePermissionOverride{GardenID: gardenID, RoleName: name, Permission: string(permission), Granted: want})
}
}
if err = app.models.Roles.ReplaceGardenOverrides(gardenID, name, overrides); err != nil {
app.serverErrorResponse(w, r, err)
return
}
app.writeJSON(w, http.StatusOK, envelope{"effective_permissions": storage.ResolveGardenPermissions(role.Permissions, overrides)}, nil)
}
func (app *application) createGardenRoleHandler(w http.ResponseWriter, r *http.Request) {
gardenID, _ := app.readGardenIDParam(r)
var input struct {
Name string `json:"name"`
Label string `json:"label"`
Permissions []string `json:"permissions"`
}
if err := app.readJSON(w, r, &input); err != nil {
app.badRequestResponse(w, r, err)
return
}
slug, label := strings.TrimSpace(input.Name), strings.TrimSpace(input.Label)
name := "garden:" + strconv.Itoa(gardenID) + ":" + slug
if !roleNameRX.MatchString(name) || label == "" || !validRolePermissions(storage.RoleScopeGarden, input.Permissions) {
app.failedValidationResponse(w, r, map[string]string{"role": "name, label, or permissions are invalid"})
return
}
role, err := app.models.Roles.Create(storage.Role{Name: name, Scope: storage.RoleScopeGarden, GardenID: &gardenID, Label: label, Permissions: input.Permissions})
if err != nil {
if errors.Is(err, storage.ErrConflict) {
app.conflictResponse(w, r)
} else {
app.serverErrorResponse(w, r, err)
}
return
}
app.writeJSON(w, http.StatusCreated, envelope{"role": role}, nil)
}
func (app *application) deleteGardenRoleHandler(w http.ResponseWriter, r *http.Request) {
gardenID, _ := app.readGardenIDParam(r)
name := httprouter.ParamsFromContext(r.Context()).ByName("roleName")
role, err := app.models.Roles.GetForGarden(gardenID, name)
if err != nil || role.GardenID == nil {
app.notFoundResponse(w, r)
return
}
if err = app.models.Roles.Delete(name); err != nil {
if errors.Is(err, storage.ErrConflict) {
app.conflictResponse(w, r)
} else {
app.serverErrorResponse(w, r, err)
}
return
}
w.WriteHeader(http.StatusNoContent)
}