185 lines
4.9 KiB
Go
185 lines
4.9 KiB
Go
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
|
|
}
|
|
|