More webui layout, navigation, preparations of user/roles/groups/security
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
|
||||
)
|
||||
|
||||
type groupsResponse struct {
|
||||
Groups []settings.Group `json:"groups"`
|
||||
}
|
||||
|
||||
type apiErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type upsertGroupRequest struct {
|
||||
Group settings.Group `json:"group"`
|
||||
}
|
||||
|
||||
var allowedPermissions = map[string]struct{}{
|
||||
"nodes.read": {},
|
||||
"nodes.exec": {},
|
||||
"nodes.update": {},
|
||||
"jobs.read": {},
|
||||
"jobs.run": {},
|
||||
"users.manage": {},
|
||||
"secrets.manage": {},
|
||||
"roles.manage": {},
|
||||
}
|
||||
|
||||
func groupsGetHandler(configDir string) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
settingsPayload, err := loadSettingsOrDefault(configDir)
|
||||
if err != nil {
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(writer, http.StatusOK, groupsResponse{Groups: settingsPayload.Groups})
|
||||
}
|
||||
}
|
||||
|
||||
func groupsUpsertHandler(configDir string) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
if err := authorizeGroupsWrite(request); err != nil {
|
||||
writeJSON(writer, http.StatusForbidden, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var payload upsertGroupRequest
|
||||
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
|
||||
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{Error: "invalid JSON body"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateGroup(payload.Group); err != nil {
|
||||
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
settingsPayload, err := loadSettingsOrDefault(configDir)
|
||||
if err != nil {
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updated := false
|
||||
for index := range settingsPayload.Groups {
|
||||
if settingsPayload.Groups[index].Name == payload.Group.Name {
|
||||
settingsPayload.Groups[index] = payload.Group
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
settingsPayload.Groups = append(settingsPayload.Groups, payload.Group)
|
||||
}
|
||||
|
||||
if err := settings.SaveSettings(configDir, settingsPayload); err != nil {
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(writer, http.StatusOK, groupsResponse{Groups: settingsPayload.Groups})
|
||||
}
|
||||
}
|
||||
|
||||
func groupsDeleteHandler(configDir string) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
if err := authorizeGroupsWrite(request); err != nil {
|
||||
writeJSON(writer, http.StatusForbidden, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(request.URL.Query().Get("name"))
|
||||
if name == "" {
|
||||
writeJSON(writer, http.StatusBadRequest, apiErrorResponse{Error: "missing group name"})
|
||||
return
|
||||
}
|
||||
|
||||
settingsPayload, err := loadSettingsOrDefault(configDir)
|
||||
if err != nil {
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
remaining := make([]settings.Group, 0, len(settingsPayload.Groups))
|
||||
for _, group := range settingsPayload.Groups {
|
||||
if group.Name != name {
|
||||
remaining = append(remaining, group)
|
||||
}
|
||||
}
|
||||
|
||||
if len(remaining) == len(settingsPayload.Groups) {
|
||||
writeJSON(writer, http.StatusNotFound, apiErrorResponse{Error: "group not found"})
|
||||
return
|
||||
}
|
||||
|
||||
settingsPayload.Groups = remaining
|
||||
if err := settings.SaveSettings(configDir, settingsPayload); err != nil {
|
||||
writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(writer, http.StatusOK, groupsResponse{Groups: settingsPayload.Groups})
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeGroupsWrite(_ *http.Request) error {
|
||||
// TODO: integrate authz middleware.
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadSettingsOrDefault(configDir string) (settings.Settings, error) {
|
||||
payload, err := settings.LoadSettings(configDir)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return settings.Settings{Groups: []settings.Group{}}, nil
|
||||
}
|
||||
return settings.Settings{}, err
|
||||
}
|
||||
if payload.Groups == nil {
|
||||
payload.Groups = []settings.Group{}
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func validateGroup(group settings.Group) error {
|
||||
if strings.TrimSpace(group.Name) == "" {
|
||||
return errors.New("group.name is required")
|
||||
}
|
||||
|
||||
switch group.ScopeKind {
|
||||
case settings.GroupScopeNode, settings.GroupScopeGroup:
|
||||
// ok
|
||||
default:
|
||||
return errors.New("group.scope_kind must be \"node\" or \"group\"")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(group.ScopeName) == "" {
|
||||
return errors.New("group.scope_name is required")
|
||||
}
|
||||
|
||||
if len(group.Permissions) == 0 {
|
||||
return errors.New("group.permissions must be non-empty")
|
||||
}
|
||||
|
||||
for _, permission := range group.Permissions {
|
||||
if strings.TrimSpace(permission) == "" {
|
||||
return errors.New("permission strings must be non-empty")
|
||||
}
|
||||
if _, ok := allowedPermissions[permission]; !ok {
|
||||
return errors.New("permission is not in the built-in permission catalog")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user