257 lines
8.7 KiB
Go
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)
|
|
}
|