- internal/ui/ui.go: add PGCred, Tags to AccountDetailData; register
PUT /accounts/{id}/pgcreds and PUT /accounts/{id}/tags routes; add
pgcreds_form.html and tags_editor.html to shared template set; remove
unused AccountTagsData; fix fieldalignment on PolicyRuleView, PoliciesData
- internal/ui/handlers_accounts.go: add handleSetPGCreds — encrypts
password via crypto.SealAESGCM, writes audit EventPGCredUpdated, renders
pgcreds_form fragment; password never echoed; load PG creds and tags in
handleAccountDetail
- internal/ui/handlers_policy.go: fix handleSetAccountTags to render with
AccountDetailData instead of removed AccountTagsData
- internal/ui/ui_test.go: add 5 PG credential UI tests
- web/templates/fragments/pgcreds_form.html: new fragment — metadata display
+ set/replace form; system accounts only; password write-only
- web/templates/fragments/tags_editor.html: new fragment — textarea editor
with HTMX PUT for atomic tag replacement
- web/templates/fragments/policy_form.html: rewrite to use structured fields
matching handleCreatePolicyRule (roles/account_types/actions multi-select,
resource_type, subject_uuid, service_names, required_tags, checkbox)
- web/templates/policies.html: new policies management page
- web/templates/fragments/policy_row.html: new HTMX table row with toggle
and delete
- web/templates/account_detail.html: add Tags card and PG Credentials card
- web/templates/base.html: add Policies nav link
- internal/server/server.go: remove ~220 lines of duplicate tag/policy
handler code (real implementations are in handlers_policy.go)
- internal/policy/engine_wrapper.go: fix corrupted source; use errors.New
- internal/db/policy_test.go: use model.AccountTypeHuman constant
- cmd/mciasctl/main.go: add nolint:gosec to int(os.Stdin.Fd()) calls
- gofmt/goimports: db/policy_test.go, policy/defaults.go,
policy/engine_test.go, ui/ui.go, cmd/mciasctl/main.go
- fieldalignment: model.PolicyRuleRecord, policy.Engine, policy.Rule,
policy.RuleBody, ui.PolicyRuleView
Security: PG password encrypted AES-256-GCM with fresh random nonce before
storage; plaintext never logged or returned in any response; audit event
written on every credential write.
213 lines
5.8 KiB
Go
213 lines
5.8 KiB
Go
package db
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"git.wntrmute.dev/kyle/mcias/internal/model"
|
|
)
|
|
|
|
func TestCreateAndGetPolicyRule(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
ruleJSON := `{"actions":["pgcreds:read"],"resource_type":"pgcreds","effect":"allow"}`
|
|
rec, err := db.CreatePolicyRule("test rule", 50, ruleJSON, nil)
|
|
if err != nil {
|
|
t.Fatalf("CreatePolicyRule: %v", err)
|
|
}
|
|
if rec.ID == 0 {
|
|
t.Error("expected non-zero ID after create")
|
|
}
|
|
if rec.Priority != 50 {
|
|
t.Errorf("expected priority 50, got %d", rec.Priority)
|
|
}
|
|
if !rec.Enabled {
|
|
t.Error("new rule should be enabled by default")
|
|
}
|
|
|
|
got, err := db.GetPolicyRule(rec.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPolicyRule: %v", err)
|
|
}
|
|
if got.Description != "test rule" {
|
|
t.Errorf("expected description %q, got %q", "test rule", got.Description)
|
|
}
|
|
if got.RuleJSON != ruleJSON {
|
|
t.Errorf("rule_json mismatch: got %q", got.RuleJSON)
|
|
}
|
|
}
|
|
|
|
func TestGetPolicyRule_NotFound(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
_, err := db.GetPolicyRule(99999)
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("expected ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListPolicyRules(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
_, _ = db.CreatePolicyRule("rule A", 100, `{"effect":"allow"}`, nil)
|
|
_, _ = db.CreatePolicyRule("rule B", 50, `{"effect":"deny"}`, nil)
|
|
_, _ = db.CreatePolicyRule("rule C", 200, `{"effect":"allow"}`, nil)
|
|
|
|
rules, err := db.ListPolicyRules(false)
|
|
if err != nil {
|
|
t.Fatalf("ListPolicyRules: %v", err)
|
|
}
|
|
if len(rules) != 3 {
|
|
t.Fatalf("expected 3 rules, got %d", len(rules))
|
|
}
|
|
// Should be ordered by priority ascending.
|
|
if rules[0].Priority > rules[1].Priority || rules[1].Priority > rules[2].Priority {
|
|
t.Errorf("rules not sorted by priority: %v %v %v",
|
|
rules[0].Priority, rules[1].Priority, rules[2].Priority)
|
|
}
|
|
}
|
|
|
|
func TestListPolicyRules_EnabledOnly(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
r1, _ := db.CreatePolicyRule("enabled rule", 100, `{"effect":"allow"}`, nil)
|
|
r2, _ := db.CreatePolicyRule("disabled rule", 100, `{"effect":"deny"}`, nil)
|
|
|
|
if err := db.SetPolicyRuleEnabled(r2.ID, false); err != nil {
|
|
t.Fatalf("SetPolicyRuleEnabled: %v", err)
|
|
}
|
|
|
|
all, err := db.ListPolicyRules(false)
|
|
if err != nil {
|
|
t.Fatalf("ListPolicyRules(all): %v", err)
|
|
}
|
|
if len(all) != 2 {
|
|
t.Errorf("expected 2 total rules, got %d", len(all))
|
|
}
|
|
|
|
enabled, err := db.ListPolicyRules(true)
|
|
if err != nil {
|
|
t.Fatalf("ListPolicyRules(enabledOnly): %v", err)
|
|
}
|
|
if len(enabled) != 1 {
|
|
t.Fatalf("expected 1 enabled rule, got %d", len(enabled))
|
|
}
|
|
if enabled[0].ID != r1.ID {
|
|
t.Errorf("wrong rule returned: got ID %d, want %d", enabled[0].ID, r1.ID)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePolicyRule(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
rec, _ := db.CreatePolicyRule("original", 100, `{"effect":"allow"}`, nil)
|
|
|
|
newDesc := "updated description"
|
|
newPriority := 25
|
|
if err := db.UpdatePolicyRule(rec.ID, &newDesc, &newPriority, nil); err != nil {
|
|
t.Fatalf("UpdatePolicyRule: %v", err)
|
|
}
|
|
|
|
got, err := db.GetPolicyRule(rec.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPolicyRule after update: %v", err)
|
|
}
|
|
if got.Description != newDesc {
|
|
t.Errorf("expected description %q, got %q", newDesc, got.Description)
|
|
}
|
|
if got.Priority != newPriority {
|
|
t.Errorf("expected priority %d, got %d", newPriority, got.Priority)
|
|
}
|
|
// RuleJSON should be unchanged.
|
|
if got.RuleJSON != `{"effect":"allow"}` {
|
|
t.Errorf("rule_json should not change when not provided: %q", got.RuleJSON)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePolicyRule_RuleJSON(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
rec, _ := db.CreatePolicyRule("rule", 100, `{"effect":"allow"}`, nil)
|
|
|
|
newJSON := `{"effect":"deny","roles":["auditor"]}`
|
|
if err := db.UpdatePolicyRule(rec.ID, nil, nil, &newJSON); err != nil {
|
|
t.Fatalf("UpdatePolicyRule (json only): %v", err)
|
|
}
|
|
|
|
got, err := db.GetPolicyRule(rec.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPolicyRule: %v", err)
|
|
}
|
|
if got.RuleJSON != newJSON {
|
|
t.Errorf("expected updated rule_json, got %q", got.RuleJSON)
|
|
}
|
|
// Description and priority unchanged.
|
|
if got.Description != "rule" {
|
|
t.Errorf("description should be unchanged, got %q", got.Description)
|
|
}
|
|
}
|
|
|
|
func TestSetPolicyRuleEnabled(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
rec, _ := db.CreatePolicyRule("toggle rule", 100, `{"effect":"allow"}`, nil)
|
|
if !rec.Enabled {
|
|
t.Fatal("new rule should be enabled")
|
|
}
|
|
|
|
if err := db.SetPolicyRuleEnabled(rec.ID, false); err != nil {
|
|
t.Fatalf("SetPolicyRuleEnabled(false): %v", err)
|
|
}
|
|
got, _ := db.GetPolicyRule(rec.ID)
|
|
if got.Enabled {
|
|
t.Error("rule should be disabled after SetPolicyRuleEnabled(false)")
|
|
}
|
|
|
|
if err := db.SetPolicyRuleEnabled(rec.ID, true); err != nil {
|
|
t.Fatalf("SetPolicyRuleEnabled(true): %v", err)
|
|
}
|
|
got, _ = db.GetPolicyRule(rec.ID)
|
|
if !got.Enabled {
|
|
t.Error("rule should be enabled after SetPolicyRuleEnabled(true)")
|
|
}
|
|
}
|
|
|
|
func TestDeletePolicyRule(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
rec, _ := db.CreatePolicyRule("to delete", 100, `{"effect":"allow"}`, nil)
|
|
|
|
if err := db.DeletePolicyRule(rec.ID); err != nil {
|
|
t.Fatalf("DeletePolicyRule: %v", err)
|
|
}
|
|
|
|
_, err := db.GetPolicyRule(rec.ID)
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("expected ErrNotFound after delete, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeletePolicyRule_NonExistent(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
// Deleting a non-existent rule should be a no-op, not an error.
|
|
if err := db.DeletePolicyRule(99999); err != nil {
|
|
t.Errorf("DeletePolicyRule on nonexistent ID should not error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCreatePolicyRule_WithCreatedBy(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
acct, _ := db.CreateAccount("policy-creator", model.AccountTypeHuman, "hash")
|
|
rec, err := db.CreatePolicyRule("by user", 100, `{"effect":"allow"}`, &acct.ID)
|
|
if err != nil {
|
|
t.Fatalf("CreatePolicyRule with createdBy: %v", err)
|
|
}
|
|
|
|
got, _ := db.GetPolicyRule(rec.ID)
|
|
if got.CreatedBy == nil || *got.CreatedBy != acct.ID {
|
|
t.Errorf("expected CreatedBy=%d, got %v", acct.ID, got.CreatedBy)
|
|
}
|
|
}
|