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 }