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) }