Add users and network settings tabs with admin protections.

List and delete accounts with last-admin and Administrators-group guards, and expose editable network settings via the configuration UI.
This commit is contained in:
2026-07-18 15:18:01 +02:00
parent f049f766d9
commit 2605c3b346
49 changed files with 5440 additions and 209 deletions
+64 -20
View File
@@ -18,13 +18,12 @@ type apiErrorResponseTest struct {
Error string `json:"error"`
}
func TestGroupsGetEmptyWhenSettingsMissing(t *testing.T) {
t.Parallel()
func TestGroupsGetEmptyWhenNoGroups(t *testing.T) {
configDir := t.TempDir()
cookie := seedCompletedSetup(t, configDir)
router := NewRouter(configDir)
request := httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil)
request := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil), cookie)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
@@ -43,9 +42,8 @@ func TestGroupsGetEmptyWhenSettingsMissing(t *testing.T) {
}
func TestGroupsUpsertRejectsInvalidPermissions(t *testing.T) {
t.Parallel()
configDir := t.TempDir()
cookie := seedCompletedSetup(t, configDir)
router := NewRouter(configDir)
body := []byte(`{
@@ -56,7 +54,7 @@ func TestGroupsUpsertRejectsInvalidPermissions(t *testing.T) {
"permissions": ["nodes.nope"]
}
}`)
request := httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(body))
request := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(body)), cookie)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
@@ -75,9 +73,8 @@ func TestGroupsUpsertRejectsInvalidPermissions(t *testing.T) {
}
func TestGroupsUpsertUpsertsByName(t *testing.T) {
t.Parallel()
configDir := t.TempDir()
cookie := seedCompletedSetup(t, configDir)
router := NewRouter(configDir)
createBody := []byte(`{
@@ -88,7 +85,7 @@ func TestGroupsUpsertUpsertsByName(t *testing.T) {
"permissions": ["nodes.read"]
}
}`)
createReq := httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(createBody))
createReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(createBody)), cookie)
createRec := httptest.NewRecorder()
router.ServeHTTP(createRec, createReq)
if createRec.Code != http.StatusOK {
@@ -103,14 +100,14 @@ func TestGroupsUpsertUpsertsByName(t *testing.T) {
"permissions": ["nodes.exec","nodes.update"]
}
}`)
updateReq := httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(updateBody))
updateReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(updateBody)), cookie)
updateRec := httptest.NewRecorder()
router.ServeHTTP(updateRec, updateReq)
if updateRec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, updateRec.Code)
}
getReq := httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil)
getReq := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil), cookie)
getRec := httptest.NewRecorder()
router.ServeHTTP(getRec, getReq)
if getRec.Code != http.StatusOK {
@@ -133,9 +130,8 @@ func TestGroupsUpsertUpsertsByName(t *testing.T) {
}
func TestGroupsDeleteRemovesByName(t *testing.T) {
t.Parallel()
configDir := t.TempDir()
cookie := seedCompletedSetup(t, configDir)
router := NewRouter(configDir)
createBody := []byte(`{
@@ -146,21 +142,21 @@ func TestGroupsDeleteRemovesByName(t *testing.T) {
"permissions": ["nodes.read"]
}
}`)
createReq := httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(createBody))
createReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(createBody)), cookie)
createRec := httptest.NewRecorder()
router.ServeHTTP(createRec, createReq)
if createRec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, createRec.Code)
}
deleteReq := httptest.NewRequest(http.MethodDelete, "/api/v1/groups?name=Admins", nil)
deleteReq := withSession(httptest.NewRequest(http.MethodDelete, "/api/v1/groups?name=Admins", nil), cookie)
deleteRec := httptest.NewRecorder()
router.ServeHTTP(deleteRec, deleteReq)
if deleteRec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, deleteRec.Code)
}
getReq := httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil)
getReq := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil), cookie)
getRec := httptest.NewRecorder()
router.ServeHTTP(getRec, getReq)
if getRec.Code != http.StatusOK {
@@ -177,12 +173,11 @@ func TestGroupsDeleteRemovesByName(t *testing.T) {
}
func TestGroupsDeleteMissingNameFails(t *testing.T) {
t.Parallel()
configDir := t.TempDir()
cookie := seedCompletedSetup(t, configDir)
router := NewRouter(configDir)
deleteReq := httptest.NewRequest(http.MethodDelete, "/api/v1/groups", nil)
deleteReq := withSession(httptest.NewRequest(http.MethodDelete, "/api/v1/groups", nil), cookie)
deleteRec := httptest.NewRecorder()
router.ServeHTTP(deleteRec, deleteReq)
@@ -199,3 +194,52 @@ func TestGroupsDeleteMissingNameFails(t *testing.T) {
}
}
func TestGroupsDeleteRejectsAdministrators(t *testing.T) {
configDir := t.TempDir()
cookie := seedCompletedSetup(t, configDir)
router := NewRouter(configDir)
createBody := []byte(`{
"group": {
"name": "Administrators",
"scope_kind": "group",
"scope_name": "Admin Group",
"permissions": ["nodes.read","users.manage"]
}
}`)
createReq := withSession(httptest.NewRequest(http.MethodPost, "/api/v1/groups", bytes.NewReader(createBody)), cookie)
createRec := httptest.NewRecorder()
router.ServeHTTP(createRec, createReq)
if createRec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, createRec.Code, createRec.Body.String())
}
deleteReq := withSession(
httptest.NewRequest(http.MethodDelete, "/api/v1/groups?name=Administrators", nil),
cookie,
)
deleteRec := httptest.NewRecorder()
router.ServeHTTP(deleteRec, deleteReq)
if deleteRec.Code != http.StatusConflict {
t.Fatalf("expected status %d, got %d body=%s", http.StatusConflict, deleteRec.Code, deleteRec.Body.String())
}
var payload apiErrorResponseTest
if err := json.NewDecoder(deleteRec.Body).Decode(&payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.Error != "Administrators group cannot be deleted" {
t.Fatalf("unexpected error: %q", payload.Error)
}
getReq := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/groups", nil), cookie)
getRec := httptest.NewRecorder()
router.ServeHTTP(getRec, getReq)
var getPayload groupsResponseTest
if err := json.NewDecoder(getRec.Body).Decode(&getPayload); err != nil {
t.Fatalf("decode response: %v", err)
}
if len(getPayload.Groups) != 1 || getPayload.Groups[0].Name != settings.AdministratorsGroupName {
t.Fatalf("Administrators group should still exist, got %#v", getPayload.Groups)
}
}