Completes run-if/set_variable and requires_sudo editing, sudoers path resolve helpers, and execute permission checks so node action groups can gate upgrades and elevate safely.
616 lines
18 KiB
Go
616 lines
18 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
|
|
)
|
|
|
|
func seedNodeForActions(t *testing.T, configDir string) settings.Node {
|
|
t.Helper()
|
|
node := settings.Node{
|
|
ID: "11111111-1111-4111-8111-111111111111",
|
|
Kind: settings.NodeKindContainer,
|
|
Name: "box-1",
|
|
HostIP: "127.0.0.1",
|
|
Username: "root",
|
|
GroupName: settings.AdministratorsGroupName,
|
|
PublicKey: "ssh-ed25519 AAAA",
|
|
KeyAlgo: "ed25519",
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
if err := settings.SaveNodes(configDir, settings.NodeStore{Nodes: []settings.Node{node}}); err != nil {
|
|
t.Fatalf("SaveNodes: %v", err)
|
|
}
|
|
return node
|
|
}
|
|
|
|
func TestActionGroupsCRUDCloneReorderAndAuthz(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cookie := seedCompletedSetup(t, configDir)
|
|
node := seedNodeForActions(t, configDir)
|
|
if _, err := settings.LoadActionsOrSeed(configDir); err != nil {
|
|
t.Fatalf("LoadActionsOrSeed: %v", err)
|
|
}
|
|
|
|
router := NewRouter(configDir)
|
|
|
|
createBody, _ := json.Marshal(map[string]any{
|
|
"name": "Nightly",
|
|
"schedule": map[string]any{
|
|
"enabled": false,
|
|
"kind": "interval",
|
|
"every_seconds": 3600,
|
|
"times": []any{},
|
|
},
|
|
})
|
|
createReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups",
|
|
bytes.NewReader(createBody),
|
|
),
|
|
cookie,
|
|
)
|
|
createRec := httptest.NewRecorder()
|
|
router.ServeHTTP(createRec, createReq)
|
|
if createRec.Code != http.StatusOK {
|
|
t.Fatalf("create group status=%d body=%s", createRec.Code, createRec.Body.String())
|
|
}
|
|
var created actionGroupResponse
|
|
if err := json.Unmarshal(createRec.Body.Bytes(), &created); err != nil {
|
|
t.Fatalf("decode create: %v", err)
|
|
}
|
|
groupID := created.Group.ID
|
|
|
|
addLibBody, _ := json.Marshal(map[string]any{
|
|
"source": "library",
|
|
"library_action_id": settings.BuiltinActionUptimeID,
|
|
})
|
|
addReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items",
|
|
bytes.NewReader(addLibBody),
|
|
),
|
|
cookie,
|
|
)
|
|
addRec := httptest.NewRecorder()
|
|
router.ServeHTTP(addRec, addReq)
|
|
if addRec.Code != http.StatusOK {
|
|
t.Fatalf("add item status=%d body=%s", addRec.Code, addRec.Body.String())
|
|
}
|
|
var withItem actionGroupResponse
|
|
if err := json.Unmarshal(addRec.Body.Bytes(), &withItem); err != nil {
|
|
t.Fatalf("decode add: %v", err)
|
|
}
|
|
if len(withItem.Group.Items) != 1 || withItem.Group.Items[0].Source != settings.ActionItemSourceLibrary {
|
|
t.Fatalf("items = %#v", withItem.Group.Items)
|
|
}
|
|
itemID := withItem.Group.Items[0].ID
|
|
|
|
addCustomBody, _ := json.Marshal(map[string]any{
|
|
"source": "local",
|
|
"name": "Echo",
|
|
"kind": "shell",
|
|
"body": "echo hi",
|
|
"env": []any{},
|
|
})
|
|
addCustomReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items",
|
|
bytes.NewReader(addCustomBody),
|
|
),
|
|
cookie,
|
|
)
|
|
addCustomRec := httptest.NewRecorder()
|
|
router.ServeHTTP(addCustomRec, addCustomReq)
|
|
if addCustomRec.Code != http.StatusOK {
|
|
t.Fatalf("add custom status=%d body=%s", addCustomRec.Code, addCustomRec.Body.String())
|
|
}
|
|
var withTwo actionGroupResponse
|
|
_ = json.Unmarshal(addCustomRec.Body.Bytes(), &withTwo)
|
|
if len(withTwo.Group.Items) != 2 {
|
|
t.Fatalf("expected 2 items, got %d", len(withTwo.Group.Items))
|
|
}
|
|
customID := withTwo.Group.Items[1].ID
|
|
|
|
orderBody, _ := json.Marshal(map[string]any{
|
|
"item_ids": []string{customID, itemID},
|
|
})
|
|
orderReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPut,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items/order",
|
|
bytes.NewReader(orderBody),
|
|
),
|
|
cookie,
|
|
)
|
|
orderRec := httptest.NewRecorder()
|
|
router.ServeHTTP(orderRec, orderReq)
|
|
if orderRec.Code != http.StatusOK {
|
|
t.Fatalf("reorder status=%d body=%s", orderRec.Code, orderRec.Body.String())
|
|
}
|
|
var reordered actionGroupResponse
|
|
_ = json.Unmarshal(orderRec.Body.Bytes(), &reordered)
|
|
if reordered.Group.Items[0].ID != customID || reordered.Group.Items[1].ID != itemID {
|
|
t.Fatalf("order = %#v", reordered.Group.Items)
|
|
}
|
|
|
|
cloneReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items/"+itemID+"/clone",
|
|
nil,
|
|
),
|
|
cookie,
|
|
)
|
|
cloneRec := httptest.NewRecorder()
|
|
router.ServeHTTP(cloneRec, cloneReq)
|
|
if cloneRec.Code != http.StatusOK {
|
|
t.Fatalf("clone status=%d body=%s", cloneRec.Code, cloneRec.Body.String())
|
|
}
|
|
var cloned actionGroupResponse
|
|
_ = json.Unmarshal(cloneRec.Body.Bytes(), &cloned)
|
|
var clonedItem settings.NodeActionItem
|
|
for _, item := range cloned.Group.Items {
|
|
if item.ID == itemID {
|
|
clonedItem = item
|
|
break
|
|
}
|
|
}
|
|
if clonedItem.Source != settings.ActionItemSourceLocal || clonedItem.Name != "Uptime" {
|
|
t.Fatalf("cloned item = %#v", clonedItem)
|
|
}
|
|
if clonedItem.RequiresSudo {
|
|
t.Fatalf("builtin uptime clone should not require sudo: %#v", clonedItem)
|
|
}
|
|
|
|
// Forbidden without actions.create for a limited user session is covered below via groups.
|
|
listReq := withSession(
|
|
httptest.NewRequest(http.MethodGet, "/api/v1/nodes/"+node.ID+"/action-groups", nil),
|
|
cookie,
|
|
)
|
|
listRec := httptest.NewRecorder()
|
|
router.ServeHTTP(listRec, listReq)
|
|
if listRec.Code != http.StatusOK {
|
|
t.Fatalf("list status=%d", listRec.Code)
|
|
}
|
|
|
|
logsReq := withSession(
|
|
httptest.NewRequest(http.MethodGet, "/api/v1/nodes/"+node.ID+"/action-logs", nil),
|
|
cookie,
|
|
)
|
|
logsRec := httptest.NewRecorder()
|
|
router.ServeHTTP(logsRec, logsReq)
|
|
if logsRec.Code != http.StatusOK {
|
|
t.Fatalf("logs list status=%d body=%s", logsRec.Code, logsRec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestActionGroupItemVariablesCreateAndPatch(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cookie := seedCompletedSetup(t, configDir)
|
|
node := seedNodeForActions(t, configDir)
|
|
if _, err := settings.LoadActionsOrSeed(configDir); err != nil {
|
|
t.Fatalf("LoadActionsOrSeed: %v", err)
|
|
}
|
|
router := NewRouter(configDir)
|
|
|
|
createBody, _ := json.Marshal(map[string]any{
|
|
"name": "Vars",
|
|
"schedule": map[string]any{
|
|
"enabled": false,
|
|
"kind": "interval",
|
|
"times": []any{},
|
|
},
|
|
})
|
|
createReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups",
|
|
bytes.NewReader(createBody),
|
|
),
|
|
cookie,
|
|
)
|
|
createRec := httptest.NewRecorder()
|
|
router.ServeHTTP(createRec, createReq)
|
|
if createRec.Code != http.StatusOK {
|
|
t.Fatalf("create group status=%d body=%s", createRec.Code, createRec.Body.String())
|
|
}
|
|
var created actionGroupResponse
|
|
if err := json.Unmarshal(createRec.Body.Bytes(), &created); err != nil {
|
|
t.Fatalf("decode create: %v", err)
|
|
}
|
|
groupID := created.Group.ID
|
|
|
|
addLocalBody, _ := json.Marshal(map[string]any{
|
|
"source": "local",
|
|
"name": "Count",
|
|
"kind": "shell",
|
|
"body": "echo 3",
|
|
"env": []any{},
|
|
"set_variable": "UpdateCount",
|
|
"run_if": "",
|
|
})
|
|
addLocalReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items",
|
|
bytes.NewReader(addLocalBody),
|
|
),
|
|
cookie,
|
|
)
|
|
addLocalRec := httptest.NewRecorder()
|
|
router.ServeHTTP(addLocalRec, addLocalReq)
|
|
if addLocalRec.Code != http.StatusOK {
|
|
t.Fatalf("add local status=%d body=%s", addLocalRec.Code, addLocalRec.Body.String())
|
|
}
|
|
var withLocal actionGroupResponse
|
|
if err := json.Unmarshal(addLocalRec.Body.Bytes(), &withLocal); err != nil {
|
|
t.Fatalf("decode local: %v", err)
|
|
}
|
|
if withLocal.Group.Items[0].SetVariable != "UpdateCount" {
|
|
t.Fatalf("set_variable = %#v", withLocal.Group.Items[0])
|
|
}
|
|
localID := withLocal.Group.Items[0].ID
|
|
|
|
addLibBody, _ := json.Marshal(map[string]any{
|
|
"source": "library",
|
|
"library_action_id": settings.BuiltinActionUptimeID,
|
|
"set_variable": "$Bad-Name",
|
|
})
|
|
badLibReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items",
|
|
bytes.NewReader(addLibBody),
|
|
),
|
|
cookie,
|
|
)
|
|
badLibRec := httptest.NewRecorder()
|
|
router.ServeHTTP(badLibRec, badLibReq)
|
|
if badLibRec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected bad variable name, got %d body=%s", badLibRec.Code, badLibRec.Body.String())
|
|
}
|
|
|
|
addLibOKBody, _ := json.Marshal(map[string]any{
|
|
"source": "library",
|
|
"library_action_id": settings.BuiltinActionUptimeID,
|
|
})
|
|
addLibReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items",
|
|
bytes.NewReader(addLibOKBody),
|
|
),
|
|
cookie,
|
|
)
|
|
addLibRec := httptest.NewRecorder()
|
|
router.ServeHTTP(addLibRec, addLibReq)
|
|
if addLibRec.Code != http.StatusOK {
|
|
t.Fatalf("add library status=%d body=%s", addLibRec.Code, addLibRec.Body.String())
|
|
}
|
|
var withLib actionGroupResponse
|
|
_ = json.Unmarshal(addLibRec.Body.Bytes(), &withLib)
|
|
var libID string
|
|
for _, item := range withLib.Group.Items {
|
|
if item.Source == settings.ActionItemSourceLibrary {
|
|
libID = item.ID
|
|
break
|
|
}
|
|
}
|
|
if libID == "" {
|
|
t.Fatal("library item missing")
|
|
}
|
|
|
|
patchLibBody, _ := json.Marshal(map[string]any{
|
|
"run_if": "if $UpdateCount >= 1",
|
|
"set_variable": "UptimeOut",
|
|
})
|
|
patchLibReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPatch,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items/"+libID,
|
|
bytes.NewReader(patchLibBody),
|
|
),
|
|
cookie,
|
|
)
|
|
patchLibRec := httptest.NewRecorder()
|
|
router.ServeHTTP(patchLibRec, patchLibReq)
|
|
if patchLibRec.Code != http.StatusOK {
|
|
t.Fatalf("patch library vars status=%d body=%s", patchLibRec.Code, patchLibRec.Body.String())
|
|
}
|
|
var patchedLib actionGroupResponse
|
|
_ = json.Unmarshal(patchLibRec.Body.Bytes(), &patchedLib)
|
|
var libItem settings.NodeActionItem
|
|
for _, item := range patchedLib.Group.Items {
|
|
if item.ID == libID {
|
|
libItem = item
|
|
break
|
|
}
|
|
}
|
|
if libItem.RunIf != "if $UpdateCount >= 1" || libItem.SetVariable != "UptimeOut" {
|
|
t.Fatalf("library vars = %#v", libItem)
|
|
}
|
|
|
|
badBodyPatch, _ := json.Marshal(map[string]any{
|
|
"body": "echo no",
|
|
})
|
|
badBodyReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPatch,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items/"+libID,
|
|
bytes.NewReader(badBodyPatch),
|
|
),
|
|
cookie,
|
|
)
|
|
badBodyRec := httptest.NewRecorder()
|
|
router.ServeHTTP(badBodyRec, badBodyReq)
|
|
if badBodyRec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected clone-required for body edit, got %d", badBodyRec.Code)
|
|
}
|
|
|
|
badCondBody, _ := json.Marshal(map[string]any{
|
|
"run_if": "not valid",
|
|
})
|
|
badCondReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPatch,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items/"+localID,
|
|
bytes.NewReader(badCondBody),
|
|
),
|
|
cookie,
|
|
)
|
|
badCondRec := httptest.NewRecorder()
|
|
router.ServeHTTP(badCondRec, badCondReq)
|
|
if badCondRec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected bad run_if, got %d body=%s", badCondRec.Code, badCondRec.Body.String())
|
|
}
|
|
|
|
sudoLocalBody, _ := json.Marshal(map[string]any{
|
|
"requires_sudo": true,
|
|
})
|
|
sudoLocalReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPatch,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+groupID+"/items/"+localID,
|
|
bytes.NewReader(sudoLocalBody),
|
|
),
|
|
cookie,
|
|
)
|
|
sudoLocalRec := httptest.NewRecorder()
|
|
router.ServeHTTP(sudoLocalRec, sudoLocalReq)
|
|
if sudoLocalRec.Code != http.StatusOK {
|
|
t.Fatalf("patch requires_sudo status=%d body=%s", sudoLocalRec.Code, sudoLocalRec.Body.String())
|
|
}
|
|
var withSudo actionGroupResponse
|
|
_ = json.Unmarshal(sudoLocalRec.Body.Bytes(), &withSudo)
|
|
foundSudo := false
|
|
for _, item := range withSudo.Group.Items {
|
|
if item.ID == localID {
|
|
foundSudo = true
|
|
if !item.RequiresSudo {
|
|
t.Fatalf("expected requires_sudo on local item, got %#v", item)
|
|
}
|
|
}
|
|
}
|
|
if !foundSudo {
|
|
t.Fatal("local item missing after sudo patch")
|
|
}
|
|
}
|
|
|
|
func TestActionGroupsForbiddenWithoutPermission(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cookie := seedCompletedSetup(t, configDir)
|
|
node := seedNodeForActions(t, configDir)
|
|
|
|
// Downgrade Administrators permissions to nodes.read only.
|
|
payload, err := settings.LoadSettings(configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadSettings: %v", err)
|
|
}
|
|
payload.Groups[0].Permissions = []string{"nodes.read"}
|
|
if err := settings.SaveSettings(configDir, payload); err != nil {
|
|
t.Fatalf("SaveSettings: %v", err)
|
|
}
|
|
|
|
router := NewRouter(configDir)
|
|
body, _ := json.Marshal(map[string]any{"name": "Nope"})
|
|
req := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups",
|
|
bytes.NewReader(body),
|
|
),
|
|
cookie,
|
|
)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected forbidden, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestActionGroupRunRequiresActionsExecute(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cookie := seedCompletedSetup(t, configDir)
|
|
node := seedNodeForActions(t, configDir)
|
|
if _, err := settings.LoadActionsOrSeed(configDir); err != nil {
|
|
t.Fatalf("LoadActionsOrSeed: %v", err)
|
|
}
|
|
|
|
// Create a group while still admin, then strip execute (+ jobs.run so
|
|
// migration does not re-grant actions.execute).
|
|
router := NewRouter(configDir)
|
|
createBody, _ := json.Marshal(map[string]any{
|
|
"name": "RunAuth",
|
|
"schedule": map[string]any{
|
|
"enabled": false,
|
|
"kind": "interval",
|
|
"times": []any{},
|
|
},
|
|
})
|
|
createReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups",
|
|
bytes.NewReader(createBody),
|
|
),
|
|
cookie,
|
|
)
|
|
createRec := httptest.NewRecorder()
|
|
router.ServeHTTP(createRec, createReq)
|
|
if createRec.Code != http.StatusOK {
|
|
t.Fatalf("create group status=%d body=%s", createRec.Code, createRec.Body.String())
|
|
}
|
|
var created actionGroupResponse
|
|
_ = json.Unmarshal(createRec.Body.Bytes(), &created)
|
|
|
|
payload, err := settings.LoadSettings(configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadSettings: %v", err)
|
|
}
|
|
payload.Groups[0].Permissions = []string{"nodes.read", "actions.create", "actions.update"}
|
|
if err := settings.SaveSettings(configDir, payload); err != nil {
|
|
t.Fatalf("SaveSettings: %v", err)
|
|
}
|
|
|
|
runReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+created.Group.ID+"/run",
|
|
nil,
|
|
),
|
|
cookie,
|
|
)
|
|
runRec := httptest.NewRecorder()
|
|
router.ServeHTTP(runRec, runReq)
|
|
if runRec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected run forbidden without actions.execute, got %d body=%s", runRec.Code, runRec.Body.String())
|
|
}
|
|
if !strings.Contains(runRec.Body.String(), "actions.execute") {
|
|
t.Fatalf("expected actions.execute in error, got %s", runRec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestActionGroupCloneCopiesRequiresSudo(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cookie := seedCompletedSetup(t, configDir)
|
|
node := seedNodeForActions(t, configDir)
|
|
if _, err := settings.LoadActionsOrSeed(configDir); err != nil {
|
|
t.Fatalf("LoadActionsOrSeed: %v", err)
|
|
}
|
|
router := NewRouter(configDir)
|
|
|
|
createActionBody, _ := json.Marshal(map[string]any{
|
|
"name": "Restart svc",
|
|
"description": "",
|
|
"kind": "shell",
|
|
"body": "/usr/bin/systemctl restart myservice",
|
|
"env": []any{},
|
|
"requires_sudo": true,
|
|
})
|
|
createActionReq := withSession(
|
|
httptest.NewRequest(http.MethodPost, "/api/v1/actions", bytes.NewReader(createActionBody)),
|
|
cookie,
|
|
)
|
|
createActionRec := httptest.NewRecorder()
|
|
router.ServeHTTP(createActionRec, createActionReq)
|
|
if createActionRec.Code != http.StatusOK {
|
|
t.Fatalf("create action status=%d body=%s", createActionRec.Code, createActionRec.Body.String())
|
|
}
|
|
var actionsPayload actionsResponseTest
|
|
if err := json.Unmarshal(createActionRec.Body.Bytes(), &actionsPayload); err != nil {
|
|
t.Fatalf("decode actions: %v", err)
|
|
}
|
|
var libraryID string
|
|
for _, action := range actionsPayload.Actions {
|
|
if action.Name == "Restart svc" {
|
|
libraryID = action.ID
|
|
if !action.RequiresSudo {
|
|
t.Fatalf("library action missing requires_sudo: %#v", action)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if libraryID == "" {
|
|
t.Fatal("library action not found")
|
|
}
|
|
|
|
createGroupBody, _ := json.Marshal(map[string]any{
|
|
"name": "SudoGrp",
|
|
"schedule": map[string]any{
|
|
"enabled": false,
|
|
"kind": "interval",
|
|
"times": []any{},
|
|
},
|
|
})
|
|
createGroupReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups",
|
|
bytes.NewReader(createGroupBody),
|
|
),
|
|
cookie,
|
|
)
|
|
createGroupRec := httptest.NewRecorder()
|
|
router.ServeHTTP(createGroupRec, createGroupReq)
|
|
if createGroupRec.Code != http.StatusOK {
|
|
t.Fatalf("create group status=%d", createGroupRec.Code)
|
|
}
|
|
var createdGroup actionGroupResponse
|
|
_ = json.Unmarshal(createGroupRec.Body.Bytes(), &createdGroup)
|
|
|
|
addItemBody, _ := json.Marshal(map[string]any{
|
|
"source": "library",
|
|
"library_action_id": libraryID,
|
|
})
|
|
addItemReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+createdGroup.Group.ID+"/items",
|
|
bytes.NewReader(addItemBody),
|
|
),
|
|
cookie,
|
|
)
|
|
addItemRec := httptest.NewRecorder()
|
|
router.ServeHTTP(addItemRec, addItemReq)
|
|
if addItemRec.Code != http.StatusOK {
|
|
t.Fatalf("add item status=%d body=%s", addItemRec.Code, addItemRec.Body.String())
|
|
}
|
|
var withItem actionGroupResponse
|
|
_ = json.Unmarshal(addItemRec.Body.Bytes(), &withItem)
|
|
itemID := withItem.Group.Items[0].ID
|
|
|
|
cloneReq := withSession(
|
|
httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/nodes/"+node.ID+"/action-groups/"+createdGroup.Group.ID+"/items/"+itemID+"/clone",
|
|
nil,
|
|
),
|
|
cookie,
|
|
)
|
|
cloneRec := httptest.NewRecorder()
|
|
router.ServeHTTP(cloneRec, cloneReq)
|
|
if cloneRec.Code != http.StatusOK {
|
|
t.Fatalf("clone status=%d body=%s", cloneRec.Code, cloneRec.Body.String())
|
|
}
|
|
var cloned actionGroupResponse
|
|
_ = json.Unmarshal(cloneRec.Body.Bytes(), &cloned)
|
|
clonedItem := cloned.Group.Items[0]
|
|
if clonedItem.Source != settings.ActionItemSourceLocal || !clonedItem.RequiresSudo {
|
|
t.Fatalf("cloned item should be local with requires_sudo: %#v", clonedItem)
|
|
}
|
|
if clonedItem.Body != "/usr/bin/systemctl restart myservice" {
|
|
t.Fatalf("cloned body = %q", clonedItem.Body)
|
|
}
|
|
}
|