List and delete accounts with last-admin and Administrators-group guards, and expose editable network settings via the configuration UI.
195 lines
5.5 KiB
Go
195 lines
5.5 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"codeberg.org/SquidSE/ClusterCanvas/service/internal/auth"
|
|
"codeberg.org/SquidSE/ClusterCanvas/service/internal/settings"
|
|
)
|
|
|
|
type usersResponseTest struct {
|
|
Users []userPublic `json:"users"`
|
|
}
|
|
|
|
func TestUsersGetListsSanitizedFields(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cookie := seedCompletedSetup(t, configDir)
|
|
router := NewRouter(configDir)
|
|
|
|
request := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/users", nil), cookie)
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, request)
|
|
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, recorder.Code, recorder.Body.String())
|
|
}
|
|
|
|
rawBody := recorder.Body.Bytes()
|
|
var payload usersResponseTest
|
|
if err := json.Unmarshal(rawBody, &payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if len(payload.Users) != 1 {
|
|
t.Fatalf("expected 1 user, got %d", len(payload.Users))
|
|
}
|
|
|
|
user := payload.Users[0]
|
|
if user.Username != "Admin" {
|
|
t.Fatalf("expected username Admin, got %q", user.Username)
|
|
}
|
|
if !user.IsAdmin {
|
|
t.Fatal("expected is_admin true")
|
|
}
|
|
if user.CanDelete {
|
|
t.Fatal("expected can_delete false for sole admin")
|
|
}
|
|
if user.ID == "" || user.Username == "" {
|
|
t.Fatal("expected id and username")
|
|
}
|
|
|
|
raw := string(rawBody)
|
|
if strings.Contains(raw, "password_hash") || strings.Contains(raw, "totp_secret") {
|
|
t.Fatalf("response leaked secrets: %s", raw)
|
|
}
|
|
}
|
|
|
|
func TestUsersDeleteRejectsLastAdministrator(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cookie := seedCompletedSetup(t, configDir)
|
|
router := NewRouter(configDir)
|
|
|
|
request := withSession(
|
|
httptest.NewRequest(http.MethodDelete, "/api/v1/users?id=test-user-id", nil),
|
|
cookie,
|
|
)
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, request)
|
|
|
|
if recorder.Code != http.StatusConflict {
|
|
t.Fatalf("expected status %d, got %d body=%s", http.StatusConflict, recorder.Code, recorder.Body.String())
|
|
}
|
|
|
|
var payload apiErrorResponseTest
|
|
if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload.Error != "cannot delete the last administrator" {
|
|
t.Fatalf("unexpected error: %q", payload.Error)
|
|
}
|
|
}
|
|
|
|
func TestUsersDeleteAllowsNonAdminAndSecondAdmin(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cookie := seedCompletedSetup(t, configDir)
|
|
|
|
key, err := settings.KeyFromEnv()
|
|
if err != nil {
|
|
t.Fatalf("KeyFromEnv: %v", err)
|
|
}
|
|
store, err := settings.LoadPasswords(configDir, key)
|
|
if err != nil {
|
|
t.Fatalf("LoadPasswords: %v", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
passwordHash, err := auth.HashPassword("correct horse battery staple extra")
|
|
if err != nil {
|
|
t.Fatalf("HashPassword: %v", err)
|
|
}
|
|
store.Users = append(store.Users,
|
|
settings.UserCredential{
|
|
ID: "operator-id",
|
|
Username: "operator",
|
|
PasswordHash: passwordHash,
|
|
Enabled: true,
|
|
GroupNames: []string{"Operators"},
|
|
CreatedAt: now,
|
|
PasswordChangedAt: now,
|
|
},
|
|
settings.UserCredential{
|
|
ID: "admin-two-id",
|
|
Username: "AdminTwo",
|
|
PasswordHash: passwordHash,
|
|
Enabled: true,
|
|
GroupNames: []string{settings.AdministratorsGroupName},
|
|
CreatedAt: now,
|
|
PasswordChangedAt: now,
|
|
},
|
|
)
|
|
if err := settings.SavePasswords(configDir, store, key); err != nil {
|
|
t.Fatalf("SavePasswords: %v", err)
|
|
}
|
|
|
|
router := NewRouter(configDir)
|
|
|
|
deleteOperator := withSession(
|
|
httptest.NewRequest(http.MethodDelete, "/api/v1/users?id=operator-id", nil),
|
|
cookie,
|
|
)
|
|
operatorRec := httptest.NewRecorder()
|
|
router.ServeHTTP(operatorRec, deleteOperator)
|
|
if operatorRec.Code != http.StatusOK {
|
|
t.Fatalf("delete operator: expected %d, got %d body=%s", http.StatusOK, operatorRec.Code, operatorRec.Body.String())
|
|
}
|
|
|
|
deleteSecondAdmin := withSession(
|
|
httptest.NewRequest(http.MethodDelete, "/api/v1/users?id=admin-two-id", nil),
|
|
cookie,
|
|
)
|
|
adminRec := httptest.NewRecorder()
|
|
router.ServeHTTP(adminRec, deleteSecondAdmin)
|
|
if adminRec.Code != http.StatusOK {
|
|
t.Fatalf("delete second admin: expected %d, got %d body=%s", http.StatusOK, adminRec.Code, adminRec.Body.String())
|
|
}
|
|
|
|
var payload usersResponseTest
|
|
if err := json.NewDecoder(adminRec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if len(payload.Users) != 1 {
|
|
t.Fatalf("expected 1 remaining user, got %d", len(payload.Users))
|
|
}
|
|
if payload.Users[0].ID != "test-user-id" {
|
|
t.Fatalf("expected sole remaining admin id, got %q", payload.Users[0].ID)
|
|
}
|
|
if payload.Users[0].CanDelete {
|
|
t.Fatal("expected remaining sole admin can_delete false")
|
|
}
|
|
}
|
|
|
|
func TestUsersDeleteMissingIDFails(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cookie := seedCompletedSetup(t, configDir)
|
|
router := NewRouter(configDir)
|
|
|
|
request := withSession(httptest.NewRequest(http.MethodDelete, "/api/v1/users", nil), cookie)
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, request)
|
|
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, recorder.Code)
|
|
}
|
|
}
|
|
|
|
func TestUsersDeleteUnknownUserNotFound(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
cookie := seedCompletedSetup(t, configDir)
|
|
router := NewRouter(configDir)
|
|
|
|
request := withSession(
|
|
httptest.NewRequest(http.MethodDelete, "/api/v1/users?id=missing", nil),
|
|
cookie,
|
|
)
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, request)
|
|
|
|
if recorder.Code != http.StatusNotFound {
|
|
t.Fatalf("expected status %d, got %d", http.StatusNotFound, recorder.Code)
|
|
}
|
|
}
|