@@ -0,0 +1,256 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user