Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes. Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending. Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete. Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
118 lines
4.0 KiB
Go
118 lines
4.0 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"agent-system-backend/internal/settings"
|
|
"agent-system-backend/internal/store"
|
|
)
|
|
|
|
func TestInprocHostRechecksEffectiveAccountOnEveryCapability(t *testing.T) {
|
|
st, err := store.OpenSQLite(t.TempDir() + "/host.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
if err := st.CreateAccount("alice", "", "", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
check := accountEffectiveAuthorization(st, true)
|
|
h := inprocHost{caps: hostcaps{st: st, accountGuard: func(owner string) error {
|
|
valid, err := check(context.Background(), owner)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !valid {
|
|
return store.ErrAccountForbidden
|
|
}
|
|
return nil
|
|
}}, owner: "alice", actor: "alice", pluginID: "rdm"}
|
|
if err := h.KVPut("before", "allowed"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.DB().Exec(`UPDATE users SET enabled=FALSE WHERE owner=?`, "alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.KVPut("after", "denied"); !errors.Is(err, store.ErrAccountForbidden) {
|
|
t.Fatalf("disabled host wrote KV: %v", err)
|
|
}
|
|
if _, _, err := h.KVGet("before"); !errors.Is(err, store.ErrAccountForbidden) {
|
|
t.Fatalf("disabled host read KV: %v", err)
|
|
}
|
|
if _, err := h.TaskCreate("test", "denied", ""); !errors.Is(err, store.ErrAccountForbidden) {
|
|
t.Fatalf("disabled host created task: %v", err)
|
|
}
|
|
if _, err := h.ProjectNameAvailable("denied", ""); !errors.Is(err, store.ErrAccountForbidden) {
|
|
t.Fatalf("disabled host used project capability: %v", err)
|
|
}
|
|
if err := h.Audit("action", "target", ""); !errors.Is(err, store.ErrAccountForbidden) {
|
|
t.Fatalf("disabled host wrote audit: %v", err)
|
|
}
|
|
if _, err := h.Complete(context.Background(), "denied"); !errors.Is(err, store.ErrAccountForbidden) {
|
|
t.Fatalf("disabled host reached model completion: %v", err)
|
|
}
|
|
if _, err := h.KnowledgeSearch(context.Background(), "", "denied", 1); !errors.Is(err, store.ErrAccountForbidden) {
|
|
t.Fatalf("disabled host read knowledge: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHostCompletionChecksAgainAfterModelSettingsRead(t *testing.T) {
|
|
st, err := store.OpenSQLite(t.TempDir() + "/host-complete.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
requests := 0
|
|
model := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
requests++
|
|
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"late"}}]}`))
|
|
}))
|
|
defer model.Close()
|
|
set := settings.NewService(st)
|
|
set.SetSystemOverride(map[string]any{"models": map[string]any{"chat": map[string]any{"model": "test", "baseUrl": model.URL}}})
|
|
checks := 0
|
|
h := inprocHost{caps: hostcaps{st: st, set: set, accountGuard: func(string) error {
|
|
checks++
|
|
if checks >= 3 {
|
|
return store.ErrAccountForbidden
|
|
}
|
|
return nil
|
|
}}, owner: "alice", actor: "alice", pluginID: "rdm"}
|
|
if _, err := h.Complete(context.Background(), "late"); !errors.Is(err, store.ErrAccountForbidden) {
|
|
t.Fatalf("completion after disable: %v", err)
|
|
}
|
|
if checks != 3 || requests != 0 {
|
|
t.Fatalf("late completion reached model: checks=%d requests=%d", checks, requests)
|
|
}
|
|
}
|
|
|
|
func TestImplicitLocalAccountExceptionIsAuthOffOnly(t *testing.T) {
|
|
st, err := store.OpenSQLite(t.TempDir() + "/local.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
allowed, err := accountEffectiveAuthorization(st, false)(context.Background(), store.OwnerDefault)
|
|
if err != nil || !allowed {
|
|
t.Fatalf("implicit local denied: %v %v", allowed, err)
|
|
}
|
|
allowed, err = accountEffectiveAuthorization(st, true)(context.Background(), store.OwnerDefault)
|
|
if !errors.Is(err, store.ErrNotFound) || allowed {
|
|
t.Fatalf("auth-on missing account allowed: %v %v", allowed, err)
|
|
}
|
|
if err := st.EnsureUser(store.OwnerDefault, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.DB().Exec(`UPDATE users SET enabled=FALSE WHERE owner=?`, store.OwnerDefault); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
allowed, err = accountEffectiveAuthorization(st, false)(context.Background(), store.OwnerDefault)
|
|
if err != nil || allowed {
|
|
t.Fatalf("disabled existing local account allowed: %v %v", allowed, err)
|
|
}
|
|
}
|