mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
planes: the case plane signs with the credential too
Closing a case is the oldest governed act here and the one an obligation rests on directly (AMLR Art. 69(2)): the retained assessment is what a supervisor reads. Its decider was bound to the credential with the rest of them, and this adds what was missing — the tests that say so, and the mutation that says the tests can fail. Two properties, both already true and neither proven: the retained assessment is signed by the token's subject and not by the `by` a request body carried, and a credential that names nobody cannot close a case at all. The second refuses at the assessment, which is the record that matters, and it refuses as a REFUSAL (400) rather than as an outage — a 503 reads as "retry", and this one never clears. The console still renders a "by" field on its resolve form. It is now ignored, which is the point, but a form asking for something the API discards is a lie to the analyst filling it in: ui/ owes a one-field deletion. That repo is another track's. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/luxfi/aml/internal/instance"
|
"github.com/luxfi/aml/internal/instance"
|
||||||
"github.com/luxfi/aml/pkg/lists"
|
"github.com/luxfi/aml/pkg/lists"
|
||||||
"github.com/luxfi/aml/pkg/models"
|
"github.com/luxfi/aml/pkg/models"
|
||||||
|
"github.com/luxfi/aml/pkg/retention"
|
||||||
"github.com/luxfi/aml/pkg/suppress"
|
"github.com/luxfi/aml/pkg/suppress"
|
||||||
"github.com/luxfi/aml/pkg/types"
|
"github.com/luxfi/aml/pkg/types"
|
||||||
"github.com/luxfi/aml/pkg/watch"
|
"github.com/luxfi/aml/pkg/watch"
|
||||||
@@ -234,3 +235,63 @@ func reqWithPath(t *testing.T, key, value string) *http.Request {
|
|||||||
e.Request.SetPathValue(key, value)
|
e.Request.SetPathValue(key, value)
|
||||||
return e.Request
|
return e.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestClosingACaseNamesTheCredential.
|
||||||
|
//
|
||||||
|
// The case plane is the other governed act, and the oldest one: closing a case is
|
||||||
|
// the retained assessment a supervisor reads. The decider on it was free text too,
|
||||||
|
// and here the record that carries it is the one an obligation rests on
|
||||||
|
// (AMLR Art. 69(2)).
|
||||||
|
func TestClosingACaseNamesTheCredential(t *testing.T) {
|
||||||
|
h := plane(t)
|
||||||
|
h.Identity = func(*http.Request) (Caller, error) { return Caller{Tenant: acme, Subject: "u-7"}, nil }
|
||||||
|
c := h.Cases.Create(acme, types.SeverityHigh, []string{"alert-1"}, []string{"ivan-petrov-42"})
|
||||||
|
|
||||||
|
e, rec := send(http.MethodPost, "/v1/aml/cases/"+c.ID+"/resolve", map[string]any{
|
||||||
|
"resolution": types.ResolutionFalsePositive,
|
||||||
|
"considered": []string{"velocity over 30 days", "customer profile"},
|
||||||
|
"rationale": "salary payments consistent with the profile on file",
|
||||||
|
"by": "the head of compliance", // the lie
|
||||||
|
})
|
||||||
|
e.Request.SetPathValue("id", c.ID)
|
||||||
|
if err := h.resolveCase()(e); err != nil {
|
||||||
|
t.Fatalf("transport: %v", err)
|
||||||
|
}
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("resolve = %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
record := only(t, h, retention.ClassAssessment)
|
||||||
|
if record.Assessment.By != "u-7" {
|
||||||
|
t.Fatalf("the retained assessment is signed %q, want the token's subject u-7", record.Assessment.By)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestACaseCannotBeClosedByNobody. Fail secure at the record that matters: the
|
||||||
|
// assessment refuses a decider it does not have, and it refuses as a REFUSAL
|
||||||
|
// (400) and not as an outage (503), because 503 reads as "retry" and this one
|
||||||
|
// never clears.
|
||||||
|
func TestACaseCannotBeClosedByNobody(t *testing.T) {
|
||||||
|
h := plane(t)
|
||||||
|
h.Identity = func(*http.Request) (Caller, error) { return Caller{Tenant: acme}, nil }
|
||||||
|
c := h.Cases.Create(acme, types.SeverityHigh, []string{"alert-1"}, []string{"ivan-petrov-42"})
|
||||||
|
|
||||||
|
e, rec := send(http.MethodPost, "/v1/aml/cases/"+c.ID+"/resolve", map[string]any{
|
||||||
|
"resolution": types.ResolutionFalsePositive,
|
||||||
|
"considered": []string{"velocity over 30 days"},
|
||||||
|
"rationale": "consistent with the profile on file",
|
||||||
|
"by": "the head of compliance",
|
||||||
|
})
|
||||||
|
e.Request.SetPathValue("id", c.ID)
|
||||||
|
if err := h.resolveCase()(e); err != nil {
|
||||||
|
t.Fatalf("transport: %v", err)
|
||||||
|
}
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("resolve = %d: %s, want 400", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if got := h.Cases.Get(c.ID); got.Status == types.CaseClosed {
|
||||||
|
t.Fatal("a case was closed by nobody")
|
||||||
|
}
|
||||||
|
if held, err := h.Records.Len(); err != nil || held != 0 {
|
||||||
|
t.Fatalf("an assessment signed by nobody was retained: %d (%v)", held, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -190,6 +190,11 @@ M = [
|
|||||||
"./pkg/api/", "TestTheDeciderIsTheCredentialAndNotTheBody",
|
"./pkg/api/", "TestTheDeciderIsTheCredentialAndNotTheBody",
|
||||||
"a governed record names the verified subject, not the request body"),
|
"a governed record names the verified subject, not the request body"),
|
||||||
|
|
||||||
|
("case-decider-from-the-body", "pkg/api/routes.go",
|
||||||
|
"\t\t// The decider, from the credential and after the body, exactly as the typed\n\t\t// adapters do it for the record planes.\n\t\tdecide(&in, who.Subject)", "\t\t_ = who",
|
||||||
|
"./pkg/api/", "TestClosingACaseNamesTheCredential",
|
||||||
|
"the retained assessment is signed by the credential, not the request body"),
|
||||||
|
|
||||||
("crowding-unbounded", "pkg/suppress/shelf.go",
|
("crowding-unbounded", "pkg/suppress/shelf.go",
|
||||||
"\tif inForce >= s.maxInForce() {", "\tif false {",
|
"\tif inForce >= s.maxInForce() {", "\tif false {",
|
||||||
"./pkg/suppress/", "TestCrowdingIsRefusedWhereItCostsARequest",
|
"./pkg/suppress/", "TestCrowdingIsRefusedWhereItCostsARequest",
|
||||||
|
|||||||
Reference in New Issue
Block a user