cb9752b1a2
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
512 lines
19 KiB
Go
512 lines
19 KiB
Go
package plugins
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/plugins/manifest"
|
|
"github.com/kandev/kandev/internal/plugins/store"
|
|
)
|
|
|
|
func TestApprovalLedgerGrantRevokeAndTombstone(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
|
|
grantAt := time.Date(2026, time.August, 31, 12, 1, 0, 0, time.UTC)
|
|
approval, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", grantAt)
|
|
if err != nil {
|
|
t.Fatalf("grant: %v", err)
|
|
}
|
|
if approval.State != ApprovalStateActive || approval.Revision != 1 {
|
|
t.Fatalf("grant approval = %#v", approval)
|
|
}
|
|
|
|
fetched, ok, err := ledger.get("inst-1", "ws-1")
|
|
if err != nil || !ok {
|
|
t.Fatalf("get: ok=%v err=%v", ok, err)
|
|
}
|
|
if fetched.ManifestDigest != "digest-a" || len(fetched.CapabilityIDs) != 1 {
|
|
t.Fatalf("fetched = %#v", fetched)
|
|
}
|
|
|
|
revoked, err := ledger.revoke("inst-1", "ws-1", "human", "revoke", "audit-2", grantAt.Add(time.Minute))
|
|
if err != nil {
|
|
t.Fatalf("revoke: %v", err)
|
|
}
|
|
if revoked.State != ApprovalStateRevoked || revoked.Revision != 2 {
|
|
t.Fatalf("revoked = %#v", revoked)
|
|
}
|
|
|
|
if err := ledger.tombstoneInstallation("inst-1", grantAt.Add(2*time.Minute)); err != nil {
|
|
t.Fatalf("tombstone: %v", err)
|
|
}
|
|
after, ok, err := ledger.get("inst-1", "ws-1")
|
|
if err != nil || !ok {
|
|
t.Fatalf("get after tombstone: ok=%v err=%v", ok, err)
|
|
}
|
|
if after.Revision != 3 || after.State != ApprovalStateRevoked {
|
|
t.Fatalf("after tombstone = %#v", after)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizePluginCapabilityStableDenyReasons(t *testing.T) {
|
|
dir := t.TempDir()
|
|
svc := &Service{}
|
|
if err := svc.SetPluginsDir(dir); err != nil {
|
|
t.Fatalf("SetPluginsDir: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = svc.Close() })
|
|
|
|
decision := svc.authorizePluginCapability("inst-1", "ws-1", "host.v2.read:tasks", 1, "req", "method")
|
|
if decision.Allowed {
|
|
t.Fatal("decision unexpectedly allowed without approval")
|
|
}
|
|
if decision.Reason != ApprovalDenyMissingApproval {
|
|
t.Fatalf("reason = %q, want missing approval", decision.Reason)
|
|
}
|
|
}
|
|
|
|
func TestApprovalLedgerGrantRejectsRevisionRegression(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", time.Now().UTC()); err != nil {
|
|
t.Fatalf("first grant: %v", err)
|
|
}
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-b", []string{"host.v2.read:tasks"}, "human", "grant", "audit-2", time.Now().UTC()); err == nil {
|
|
t.Fatal("grant accepted a non-incrementing revision")
|
|
}
|
|
}
|
|
|
|
func TestApprovalLedgerGrantRecordsHumanNarrowing(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
at := time.Now().UTC()
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:messages", "host.v2.read:tasks"}, "human", "grant", "audit-1", at); err != nil {
|
|
t.Fatalf("initial grant: %v", err)
|
|
}
|
|
if _, err := ledger.grant("inst-1", "ws-1", 2, "digest-a", []string{"host.v2.read:tasks"}, "human", "remove messages access", "audit-2", at.Add(time.Second)); err != nil {
|
|
t.Fatalf("narrow approval: %v", err)
|
|
}
|
|
|
|
file, err := ledger.load()
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if got := file.Events[1].Type; got != CapabilityApprovalEventNarrow {
|
|
t.Fatalf("narrowing event type = %q, want %q", got, CapabilityApprovalEventNarrow)
|
|
}
|
|
}
|
|
|
|
func TestApprovalLedgerGrantRejectsZeroRevision(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
|
|
if _, err := ledger.grant("inst-1", "ws-1", 0, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", time.Now().UTC()); err == nil {
|
|
t.Fatal("grant accepted revision zero")
|
|
}
|
|
if _, ok, err := ledger.get("inst-1", "ws-1"); err != nil || ok {
|
|
t.Fatalf("grant persisted revision zero approval: ok=%v err=%v", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestApprovalLedgerGrantRejectsNonInitialRevision(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
|
|
if _, err := ledger.grant("inst-1", "ws-1", 2, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", time.Now().UTC()); err == nil {
|
|
t.Fatal("grant accepted non-initial revision")
|
|
}
|
|
}
|
|
|
|
func TestApprovalLedgerTombstonePersistsAcrossReload(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", time.Now().UTC()); err != nil {
|
|
t.Fatalf("grant: %v", err)
|
|
}
|
|
if err := ledger.tombstoneInstallation("inst-1", time.Now().UTC()); err != nil {
|
|
t.Fatalf("tombstone: %v", err)
|
|
}
|
|
reloaded := newApprovalLedger(dir)
|
|
approval, ok, err := reloaded.get("inst-1", "ws-1")
|
|
if err != nil || !ok {
|
|
t.Fatalf("reloaded get: ok=%v err=%v", ok, err)
|
|
}
|
|
if approval.State != ApprovalStateRevoked || approval.Revision != 2 {
|
|
t.Fatalf("approval after reload = %#v", approval)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizePluginCapabilityAllowsExactCurrentRevision(t *testing.T) {
|
|
dir := t.TempDir()
|
|
svc := &Service{}
|
|
if err := svc.SetPluginsDir(dir); err != nil {
|
|
t.Fatalf("SetPluginsDir: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = svc.Close() })
|
|
if _, err := svc.approvalGrant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1"); err != nil {
|
|
t.Fatalf("grant: %v", err)
|
|
}
|
|
|
|
decision := svc.authorizePluginCapability("inst-1", "ws-1", "host.v2.read:tasks", 1, "req", "method")
|
|
if !decision.Allowed {
|
|
t.Fatalf("decision = %#v, want allowed for exact current revision", decision)
|
|
}
|
|
if decision.Receipt.Result != "allowed" {
|
|
t.Fatalf("receipt result = %q, want allowed", decision.Receipt.Result)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizePluginCapabilityDeniesStaleRevision(t *testing.T) {
|
|
dir := t.TempDir()
|
|
svc := &Service{}
|
|
if err := svc.SetPluginsDir(dir); err != nil {
|
|
t.Fatalf("SetPluginsDir: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = svc.Close() })
|
|
if _, err := svc.approvalGrant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1"); err != nil {
|
|
t.Fatalf("initial grant: %v", err)
|
|
}
|
|
if _, err := svc.approvalGrant("inst-1", "ws-1", 2, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-2"); err != nil {
|
|
t.Fatalf("second grant: %v", err)
|
|
}
|
|
|
|
decision := svc.authorizePluginCapability("inst-1", "ws-1", "host.v2.read:tasks", 1, "req", "method")
|
|
if decision.Allowed {
|
|
t.Fatalf("decision = %#v, want stale revision denied", decision)
|
|
}
|
|
if decision.Reason != ApprovalDenyStaleRevision {
|
|
t.Fatalf("reason = %q, want stale_capability_revision", decision.Reason)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizePluginCapabilityDeniesHumanReservedCapability(t *testing.T) {
|
|
dir := t.TempDir()
|
|
svc := &Service{}
|
|
if err := svc.SetPluginsDir(dir); err != nil {
|
|
t.Fatalf("SetPluginsDir: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = svc.Close() })
|
|
if _, err := svc.approvalGrant("inst-1", "ws-1", 1, "digest-a", []string{"merge"}, "human", "grant", "audit-1"); err == nil {
|
|
t.Fatal("approvalGrant persisted a Human-reserved capability")
|
|
}
|
|
|
|
decision := svc.authorizePluginCapability("inst-1", "ws-1", "merge", 1, "req", "method")
|
|
if decision.Allowed {
|
|
t.Fatalf("decision = %#v, want human-reserved deny", decision)
|
|
}
|
|
if decision.Reason != ApprovalDenyHumanReserved {
|
|
t.Fatalf("reason = %q, want human_reserved", decision.Reason)
|
|
}
|
|
}
|
|
|
|
func TestAuthorizePluginCapabilityDeniesMalformedRequest(t *testing.T) {
|
|
dir := t.TempDir()
|
|
svc := &Service{}
|
|
if err := svc.SetPluginsDir(dir); err != nil {
|
|
t.Fatalf("SetPluginsDir: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = svc.Close() })
|
|
|
|
cases := []struct {
|
|
name string
|
|
installationID string
|
|
workspaceID string
|
|
requestDigest string
|
|
methodDigest string
|
|
}{
|
|
{"missing installation", "", "ws-1", "req", "method"},
|
|
{"missing workspace", "inst-1", "", "req", "method"},
|
|
{"missing request digest", "inst-1", "ws-1", "", "method"},
|
|
{"missing method digest", "inst-1", "ws-1", "req", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
decision := svc.authorizePluginCapability(tc.installationID, tc.workspaceID, "host.v2.read:tasks", 1, tc.requestDigest, tc.methodDigest)
|
|
if decision.Allowed {
|
|
t.Fatalf("decision unexpectedly allowed: %#v", decision)
|
|
}
|
|
if decision.Reason != ApprovalDenyMalformedRequest {
|
|
t.Fatalf("reason = %q, want malformed_request", decision.Reason)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthorizePluginCapabilityDeniesUnsupportedCapabilityID(t *testing.T) {
|
|
dir := t.TempDir()
|
|
svc := &Service{}
|
|
if err := svc.SetPluginsDir(dir); err != nil {
|
|
t.Fatalf("SetPluginsDir: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = svc.Close() })
|
|
if _, err := svc.approvalGrant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1"); err != nil {
|
|
t.Fatalf("grant: %v", err)
|
|
}
|
|
|
|
for _, capabilityID := range []string{"", "host.v2.read:*", "host.v2.read:tas?s", " host.v2.read:tasks"} {
|
|
decision := svc.authorizePluginCapability("inst-1", "ws-1", capabilityID, 1, "req", "method")
|
|
if decision.Allowed {
|
|
t.Fatalf("capability %q unexpectedly allowed", capabilityID)
|
|
}
|
|
if decision.Reason != ApprovalDenyUnsupportedCapability {
|
|
t.Fatalf("capability %q reason = %q, want unsupported_capability", capabilityID, decision.Reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApprovalGrantRetryIsIdempotent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
at := time.Now().UTC()
|
|
first, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", at)
|
|
if err != nil {
|
|
t.Fatalf("first grant: %v", err)
|
|
}
|
|
second, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", at.Add(time.Second))
|
|
if err != nil {
|
|
t.Fatalf("idempotent retry: %v", err)
|
|
}
|
|
if second.Revision != first.Revision || !second.UpdatedAt.Equal(first.UpdatedAt) {
|
|
t.Fatalf("retry changed approval: first=%#v second=%#v", first, second)
|
|
}
|
|
file, err := ledger.load()
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(file.Events) != 1 {
|
|
t.Fatalf("event count = %d, want one idempotent event", len(file.Events))
|
|
}
|
|
}
|
|
|
|
func TestApprovalGrantRetryRejectsChangedAuditPayload(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
at := time.Now().UTC()
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", at); err != nil {
|
|
t.Fatalf("first grant: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
actor string
|
|
reason string
|
|
}{
|
|
{name: "actor", actor: "other-human", reason: "grant"},
|
|
{name: "reason", actor: "human", reason: "different reason"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, tc.actor, tc.reason, "audit-1", at.Add(time.Second)); !errors.Is(err, ErrApprovalIdempotencyConflict) {
|
|
t.Fatalf("changed grant payload error = %v, want idempotency conflict", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApprovalRevokeRetryRejectsChangedAuditPayload(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
at := time.Now().UTC()
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "grant-1", at); err != nil {
|
|
t.Fatalf("grant: %v", err)
|
|
}
|
|
if _, err := ledger.revokeIfRevision("inst-1", "ws-1", 1, "human", "revoke", "revoke-1", at.Add(time.Second), false); err != nil {
|
|
t.Fatalf("first revoke: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
expectedRevision uint64
|
|
actor string
|
|
reason string
|
|
}{
|
|
{name: "revision", expectedRevision: 99, actor: "human", reason: "revoke"},
|
|
{name: "actor", expectedRevision: 1, actor: "other-human", reason: "revoke"},
|
|
{name: "reason", expectedRevision: 1, actor: "human", reason: "different reason"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := ledger.revokeIfRevision("inst-1", "ws-1", tc.expectedRevision, tc.actor, tc.reason, "revoke-1", at.Add(2*time.Second), false); !errors.Is(err, ErrApprovalIdempotencyConflict) {
|
|
t.Fatalf("changed revoke payload error = %v, want idempotency conflict", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApprovalRevokeRetryReplaysExactPayloadAfterRevisionAdvances(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
at := time.Now().UTC()
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "grant-1", at); err != nil {
|
|
t.Fatalf("grant: %v", err)
|
|
}
|
|
first, err := ledger.revokeIfRevision("inst-1", "ws-1", 1, "human", "revoke", "revoke-1", at.Add(time.Second), false)
|
|
if err != nil {
|
|
t.Fatalf("first revoke: %v", err)
|
|
}
|
|
file, err := ledger.load()
|
|
if err != nil {
|
|
t.Fatalf("load after revoke: %v", err)
|
|
}
|
|
current := file.Approvals[approvalKey("inst-1", "ws-1")]
|
|
if current.Revision != first.Revision {
|
|
t.Fatalf("loaded approval revision = %d, want %d", current.Revision, first.Revision)
|
|
}
|
|
|
|
replayed, err := ledger.revokeIfRevision("inst-1", "ws-1", 1, "human", "revoke", "revoke-1", at.Add(2*time.Second), false)
|
|
if err != nil {
|
|
t.Fatalf("exact replay revoke: %v", err)
|
|
}
|
|
if replayed.Revision != first.Revision || replayed.State != first.State || !replayed.UpdatedAt.Equal(first.UpdatedAt) {
|
|
t.Fatalf("retry did not replay original revoke: first=%#v replayed=%#v", first, replayed)
|
|
}
|
|
|
|
file, err = ledger.load()
|
|
if err != nil {
|
|
t.Fatalf("reload after replay: %v", err)
|
|
}
|
|
if len(file.Events) != 2 {
|
|
t.Fatalf("event count = %d, want grant and first revoke only", len(file.Events))
|
|
}
|
|
}
|
|
|
|
func TestApprovalGrantRetryReplaysAfterLaterRevision(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
at := time.Now().UTC()
|
|
first, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", at)
|
|
if err != nil {
|
|
t.Fatalf("first grant: %v", err)
|
|
}
|
|
if _, err := ledger.grant("inst-1", "ws-1", 2, "digest-b", []string{"host.v2.read:tasks"}, "human", "upgrade", "audit-2", at.Add(time.Second)); err != nil {
|
|
t.Fatalf("second grant: %v", err)
|
|
}
|
|
|
|
replayed, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", at.Add(2*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("retry original grant after later revision: %v", err)
|
|
}
|
|
if replayed.Revision != first.Revision || replayed.ManifestDigest != first.ManifestDigest || !replayed.UpdatedAt.Equal(first.UpdatedAt) {
|
|
t.Fatalf("retry did not replay original result: first=%#v replayed=%#v", first, replayed)
|
|
}
|
|
file, err := ledger.load()
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(file.Events) != 2 {
|
|
t.Fatalf("event count = %d, want original two events only", len(file.Events))
|
|
}
|
|
}
|
|
|
|
func TestApprovalTombstoneAppendsWorkspaceEventAndMarksRow(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
at := time.Now().UTC()
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", at); err != nil {
|
|
t.Fatalf("grant: %v", err)
|
|
}
|
|
if err := ledger.tombstoneInstallation("inst-1", at.Add(time.Second)); err != nil {
|
|
t.Fatalf("tombstone: %v", err)
|
|
}
|
|
row, ok, err := ledger.get("inst-1", "ws-1")
|
|
if err != nil || !ok {
|
|
t.Fatalf("get: ok=%v err=%v", ok, err)
|
|
}
|
|
if row.TombstonedAt == nil {
|
|
t.Fatal("tombstone did not mark current approval")
|
|
}
|
|
file, err := ledger.load()
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(file.Events) != 2 || file.Events[1].Type != CapabilityApprovalEventRevoke || file.Events[1].AuditID == "" {
|
|
t.Fatalf("tombstone events = %#v", file.Events)
|
|
}
|
|
}
|
|
|
|
func TestApprovalTombstoneRetryDoesNotAdvanceRevisionOrDuplicateEvents(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
at := time.Now().UTC()
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", at); err != nil {
|
|
t.Fatalf("grant: %v", err)
|
|
}
|
|
if err := ledger.tombstoneInstallation("inst-1", at.Add(time.Second)); err != nil {
|
|
t.Fatalf("first tombstone: %v", err)
|
|
}
|
|
first, ok, err := ledger.get("inst-1", "ws-1")
|
|
if err != nil || !ok {
|
|
t.Fatalf("get after first tombstone: ok=%v err=%v", ok, err)
|
|
}
|
|
if err := ledger.tombstoneInstallation("inst-1", at.Add(2*time.Second)); err != nil {
|
|
t.Fatalf("second tombstone: %v", err)
|
|
}
|
|
second, ok, err := ledger.get("inst-1", "ws-1")
|
|
if err != nil || !ok {
|
|
t.Fatalf("get after second tombstone: ok=%v err=%v", ok, err)
|
|
}
|
|
if second.Revision != first.Revision || !second.UpdatedAt.Equal(first.UpdatedAt) {
|
|
t.Fatalf("tombstone retry advanced row: first=%#v second=%#v", first, second)
|
|
}
|
|
file, err := ledger.load()
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if len(file.Events) != 2 {
|
|
t.Fatalf("event count = %d, want grant plus one tombstone revoke", len(file.Events))
|
|
}
|
|
}
|
|
|
|
func TestApprovalLedgerRejectsGrantForTombstonedInstallation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", time.Now().UTC()); err != nil {
|
|
t.Fatalf("grant: %v", err)
|
|
}
|
|
if err := ledger.tombstoneInstallation("inst-1", time.Now().UTC()); err != nil {
|
|
t.Fatalf("tombstone: %v", err)
|
|
}
|
|
if _, err := ledger.grant("inst-1", "ws-1", 3, "digest-b", []string{"host.v2.read:tasks"}, "human", "regrant", "audit-2", time.Now().UTC()); err == nil {
|
|
t.Fatal("grant accepted tombstoned installation")
|
|
}
|
|
}
|
|
|
|
func TestApprovalLedgerRejectsMissingAuditID(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ledger := newApprovalLedger(dir)
|
|
if _, err := ledger.grant("inst-1", "ws-1", 1, "digest-a", []string{"host.v2.read:tasks"}, "human", "grant", "", time.Now().UTC()); err == nil {
|
|
t.Fatal("grant accepted empty audit id")
|
|
}
|
|
}
|
|
|
|
func TestAuthorizePluginCapabilityRequiresCurrentInstalledManifest(t *testing.T) {
|
|
dir := t.TempDir()
|
|
svc := &Service{registry: NewRegistry()}
|
|
if err := svc.SetPluginsDir(dir); err != nil {
|
|
t.Fatalf("SetPluginsDir: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = svc.Close() })
|
|
svc.registry.Add(&store.Record{
|
|
Manifest: manifest.Manifest{ID: "plugin-a", Capabilities: manifest.Capabilities{APIRead: []string{"tasks"}}},
|
|
InstallationID: "inst-1",
|
|
})
|
|
if _, err := svc.approvalGrant("inst-1", "ws-1", 1, ManifestCapabilityDigest(svc.registry.List()[0].Manifest), []string{"host.v2.read:tasks"}, "human", "grant", "audit-1"); err != nil {
|
|
t.Fatalf("grant: %v", err)
|
|
}
|
|
decision := svc.authorizePluginCapability("inst-1", "ws-1", "host.v2.write:tasks", 1, "req", "method")
|
|
if decision.Allowed || decision.Reason != ApprovalDenyUndeclaredCapability {
|
|
t.Fatalf("decision = %#v, want manifest intersection denial", decision)
|
|
}
|
|
}
|
|
|
|
func TestApprovalLedgerRejectsNULIdentifiers(t *testing.T) {
|
|
ledger := newApprovalLedger(t.TempDir())
|
|
if _, err := ledger.grant("inst\x00-1", "ws-1", 1, "digest", []string{"host.v2.read:tasks"}, "human", "grant", "audit-1", time.Now().UTC()); !errors.Is(err, ErrApprovalInvalidIdentifier) {
|
|
t.Fatalf("grant error = %v, want invalid identifier", err)
|
|
}
|
|
}
|