package mysql import ( "context" "encoding/json" "os" "strings" "testing" "time" "git.yuntuo.hk/k2safe/orbit/internal/fault" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" ) // TestIntegrationCredentialOperations verifies the private operation protocol: // one frozen token, stable idempotency and audit identity from MySQL itself. func TestIntegrationCredentialOperations(t *testing.T) { dsn := os.Getenv("ORBIT_TEST_MYSQL_DSN") if dsn == "" { t.Skip("set ORBIT_TEST_MYSQL_DSN to a disposable database, or run make integration") } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() db, err := Open(ctx, dsn, 8, 2) if err != nil { t.Fatal(err) } defer db.Close() if err := Migrate(ctx, db); err != nil { t.Fatal(err) } var initial BootstrapResult name := "credential-" + string(testID(t)) err = Bootstrap(ctx, db, BootstrapInput{Application: name, Namespace: "default", Environment: "test", Queue: "credential"}, func(v BootstrapResult) error { initial = v return nil }) if err != nil { t.Fatal(err) } status, err := GetCredentialStatus(ctx, db, initial.NamespaceID, initial.CredentialID) if err != nil || status.Revision != 1 || status.RevokedAt != nil { t.Fatalf("initial credential status: %+v %v", status, err) } old, err := auth.Parse(initial.Token) if err != nil { t.Fatal(err) } if err := NewTaskStore(db).AuthorizeSubmit(ctx, old); err != nil { t.Fatalf("initial credential rejected: %v", err) } op, err := PrepareCredentialOperation(ctx, db, CredentialChangeInput{ NamespaceID: initial.NamespaceID, CredentialID: initial.CredentialID, ExpectedRevision: 1, Action: credentialRotateAction, ValidFor: 2 * time.Hour, Reason: "scheduled rotation", }) if err != nil { t.Fatal(err) } if op.Token == initial.Token || op.OperationID == "" || op.ExpiresAt == nil { t.Fatal("prepare did not freeze a new token and expiry") } receipt, err := ApplyCredentialOperation(ctx, db, op) if err != nil || receipt.Revision != 2 || receipt.Deduplicated { t.Fatalf("rotation receipt: %+v %v", receipt, err) } newToken, err := auth.Parse(op.Token) if err != nil || NewTaskStore(db).AuthorizeSubmit(ctx, old) == nil || NewTaskStore(db).AuthorizeSubmit(ctx, newToken) != nil { t.Fatalf("credential authentication boundary failed: %v", err) } replayed, err := ApplyCredentialOperation(ctx, db, op) if err != nil || !replayed.Deduplicated || replayed.OperationID != receipt.OperationID || replayed.Revision != receipt.Revision { t.Fatalf("stable operation replay: %+v %v", replayed, err) } changed := op changed.Token, _, err = auth.Issue(initial.NamespaceID, initial.CredentialID) if err != nil { t.Fatal(err) } if _, err := ApplyCredentialOperation(ctx, db, changed); fault.Code(err) != "IDEMPOTENCY_CONFLICT" { t.Fatalf("changed operation file was accepted: %v", err) } // Revocation remains available for a disabled application so an incident can // close credentials even when ordinary authentication is already unavailable. if _, err := db.ExecContext(ctx, "UPDATE orbit_applications SET enabled=FALSE WHERE id=?", initial.ApplicationID); err != nil { t.Fatal(err) } revoke, err := PrepareCredentialOperation(ctx, db, CredentialChangeInput{ NamespaceID: initial.NamespaceID, CredentialID: initial.CredentialID, ExpectedRevision: 2, Action: credentialRevokeAction, Reason: "incident response", }) if err != nil { t.Fatal(err) } revoked, err := ApplyCredentialOperation(ctx, db, revoke) if err != nil || revoked.Revision != 3 || revoked.RevokedAt == nil { t.Fatalf("revoke receipt: %+v %v", revoked, err) } if _, err := ApplyCredentialOperation(ctx, db, revoke); err != nil { t.Fatal(err) } if err := NewTaskStore(db).AuthorizeSubmit(ctx, newToken); fault.Code(err) != "UNAUTHENTICATED" { t.Fatalf("revoked token remained usable: %v", err) } var audits int if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM orbit_audit_events WHERE namespace_id=? AND resource_id=? AND action IN ('credential.rotate','credential.revoke')`, initial.NamespaceID, initial.CredentialID).Scan(&audits); err != nil || audits != 2 { t.Fatalf("credential audit count=%d err=%v", audits, err) } var actor, before, after string if err := db.QueryRowContext(ctx, `SELECT actor_id,`+"`before`"+`,`+"`after`"+` FROM orbit_audit_events WHERE namespace_id=? AND operation_id=?`, initial.NamespaceID, op.OperationID).Scan(&actor, &before, &after); err != nil { t.Fatal(err) } if !strings.HasPrefix(actor, "mysql:") || actor == string(initial.CredentialID) || strings.Contains(before+after, op.Token) || strings.Contains(before+after, "secret_digest") { t.Fatalf("audit leaked or misidentified operator: actor=%q before=%s after=%s", actor, before, after) } var stored []byte if err := db.QueryRowContext(ctx, `SELECT response FROM orbit_idempotency_keys WHERE namespace_id=? AND id=?`, initial.NamespaceID, op.OperationID).Scan(&stored); err != nil { t.Fatal(err) } var saved CredentialChangeReceipt if err := json.Unmarshal(stored, &saved); err != nil || saved.OperationID != op.OperationID { t.Fatalf("stored receipt: %s %v", stored, err) } t.Log("credential operation file froze one token, replay returned the original receipt, tampering conflicted, and disabled-scope revocation preserved audit identity") } func TestIntegrationCredentialCommitFaults(t *testing.T) { dsn := os.Getenv("ORBIT_TEST_MYSQL_DSN") if dsn == "" { t.Skip("set ORBIT_TEST_MYSQL_DSN to a disposable database, or run make integration") } ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() db, err := Open(ctx, dsn, 8, 2) if err != nil { t.Fatal(err) } defer db.Close() if err := Migrate(ctx, db); err != nil { t.Fatal(err) } for _, after := range []bool{false, true} { name := "credential-commit-" + string(testID(t)) var initial BootstrapResult if err := Bootstrap(ctx, db, BootstrapInput{Application: name, Namespace: "default", Environment: "test", Queue: "credential"}, func(v BootstrapResult) error { initial = v; return nil }); err != nil { t.Fatal(err) } op, err := PrepareCredentialOperation(ctx, db, CredentialChangeInput{NamespaceID: initial.NamespaceID, CredentialID: initial.CredentialID, ExpectedRevision: 1, Action: credentialRotateAction, ValidFor: time.Hour, Reason: "commit boundary"}) if err != nil { t.Fatal(err) } proxy, proxiedDSN := newCommitFaultProxy(t, dsn) faultDB, err := Open(ctx, proxiedDSN, 2, 1) if err != nil { t.Fatal(err) } if after { proxy.mode.Store(2) } else { proxy.mode.Store(1) } _, applyErr := ApplyCredentialOperation(ctx, faultDB, op) proxy.expect(t, after, applyErr) faultDB.Close() status, err := GetCredentialStatus(ctx, db, initial.NamespaceID, initial.CredentialID) if err != nil { t.Fatal(err) } wantRevision := uint64(1) if after { wantRevision = 2 } if status.Revision != wantRevision { t.Fatalf("commit %v status revision=%d want=%d", after, status.Revision, wantRevision) } receipt, err := ApplyCredentialOperation(ctx, db, op) if err != nil || receipt.Revision != 2 || receipt.Deduplicated != after { t.Fatalf("commit %v reconciliation receipt=%+v err=%v", after, receipt, err) } } } func testID(t *testing.T) core.ID { t.Helper() id, err := core.NewID() if err != nil { t.Fatal(err) } return id }