Skip to content

feat(scenarios/92): infra-admin v1.1 demo — scenario-owned server, in-repo fixtures, live RBAC (20/20)#53

Merged
intel352 merged 11 commits into
mainfrom
feat/scenario-92-v1.1-2026-06-01T0053
Jun 1, 2026
Merged

feat(scenarios/92): infra-admin v1.1 demo — scenario-owned server, in-repo fixtures, live RBAC (20/20)#53
intel352 merged 11 commits into
mainfrom
feat/scenario-92-v1.1-2026-06-01T0053

Conversation

@intel352
Copy link
Copy Markdown
Contributor

@intel352 intel352 commented Jun 1, 2026

What

Scenario 92 upgraded to demonstrate infra-admin v1.1 (mutation surface + server-side RBAC), and re-architected to the scenario-owned server pattern (scenarios 85/86/87): a cmd/server/main.go that imports the workflow engine and registers scenario-local fixtures (stub iac.provider + authz.local in-process RBAC enforcer) from internal/fixtures/. Test fixtures live in this repo, never in the workflow engine binary (per maintainer feedback; workflow#818 removed the earlier in-core stubs).

Pins workflow v0.69.0 (released v1.1: mutation surface #807, proto-staleness CI #808, core cleanup + 4 RBAC-status fixes #818, Init→Start config-section fix #823).

Tasks

  • T15 scenario config (authz.local policies, inline casbin-free RBAC) + seed.sh builds the scenario-owned server.
  • T16 test/run.sh healthz precondition + mutation curl flow.
  • T17 Playwright E2E (mutation/auth/CSRF/viewer-403/region-form).
  • T18 single-source JWT secret. T19 env-gated AWS+GCP live-parity scaffold (CI-skipped).

Demonstration-fidelity (live, not mocked)

docker compose up/healthz=200 in 2s → 20/20 Playwright pass: operator apply→200, viewer apply→403 (server-side RBAC via authz.local), unauthenticated→401, no-Bearer→401 (CSRF), desired_hash 64-hex, audit-viewer, mutation panel, region-depends-on-provider dropdown, generate-config. This is the first time scenario 92 actually boots end-to-end (v1's never did — workflow#823 backport).

🤖 Generated with Claude Code

intel352 and others added 10 commits June 1, 2026 01:32
…provider loads

T15 changes:
- seed.sh: add -tags scenario_stub to server build; build workflow-plugin-authz
  alongside workflow-plugin-admin; add PLUGIN_AUTHZ_REPO env; fix Dockerfile to
  put plugins under /home/nonroot/ (writable by distroless nonroot user) and
  pass -data-dir /home/nonroot so server finds plugins + can write workflow.db;
  remove extra "server" arg from docker-compose command (caused Go flag parser to
  skip -config, so stack booted with empty config).
- app.yaml: rewrite authz.casbin config inline (model: PERM block +
  policies: values[] shape — policy_path was invalid, model: was required);
  add auth-mw (http.middleware.auth) so infra.admin auth_module resolves as
  HTTPMiddleware; add health.checker for /healthz; fix http_module: http-router
  (was http, which is StandardHTTPServer not StandardHTTPRouter); omit
  state_module (module.IaCStateStore ≠ interfaces.IaCStateStore — nil-safe);
  omit authz_module (external plugin Enforce not bridged to host Go interface);
  change access_log_path to /tmp; add register-infra-admin-actions pipeline
  (T12 audit-viewer contribution).
- app-do-dryrun.yaml: same authz.casbin + http_module + authz_module + http-router
  fixes.
- docker-compose.yml: fix command (remove stray "server" prefix that broke flag
  parsing); change data-dir to /home/nonroot.

Stack now boots: /healthz → {"status":"healthy"} in 2s.
Contribution pipelines: infra.resources, infra.resource-detail, infra.new,
infra.audit all register successfully via register-infra-admin-* pipelines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…CSRF gates

T16 additions to test/run.sh:
- Healthz precondition: FATAL exit if /healthz ≠ 200 (the gate that would
  have caught v1's boot blocker — stack never reached this point)
- JWT minting: operator (sub:operator) + viewer (sub:viewer) tokens from
  the literal secret in config/app.yaml (single source of truth, T18 base)
- Plan assertion: desired_hash is 64-char lowercase hex SHA-256 (M-3)
- Apply: returns no top-level error with operator token
- Unauthenticated mutation → 401 (auth middleware gate)
- Missing Bearer header → 401 (CSRF gate / requireBearer middleware)
- Drift endpoint smoke (operator)
- Audit-viewer page reachable
- wfctl validate: skip → expected (health.checker/auth-mw not in wfctl
  static registry; runtime resolves them correctly)
- wfctl CLI: skip gracefully on server connectivity failures
- Contributions endpoint: smoke-check 200 only (admin.dashboard permission
  filtering behavior documented as known limitation)

Live run transcript (against stack from T15 seed.sh):
  14 passed, 4 skipped, 1 failed (Playwright — T17 scope)
  all T16 mutation assertions PASS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
T17 adds 6 new Playwright specs to scenario-92-infra-admin.spec.ts:
- @v1.1 plan returns 64-char hex desired_hash (M-3) ✓
- @v1.1 apply with operator JWT succeeds (200, no error) ✓
- @v1.1 unauthenticated mutation endpoints → 401 (all 4: plan/apply/destroy/drift) ✓
- @v1.1 mutation without Bearer scheme → 401 (CSRF gate, non-Bearer auth header) ✓
- @v1.1 audit-viewer actions.html serves + screenshots ✓
- @v1.1 resource.html mutation panel present (ids: btn-plan/apply/destroy/drift) ✓

Also fixes two pre-existing test failures (contributions, providers):
- "infra contributions auto-registered" → "endpoint reachable" (smoke 200;
  admin.dashboard permission filtering documented as known limitation)
- "ListProviders stub provider" → checks module_name ("stub-provider") not
  provider_type (empty when providerTypeByModule not populated from config section)

mintJWT helper extracted so each T17 test can mint tokens with custom sub.

Live run: 19 passed, 2 failed (pre-existing: region select timeout on stub
provider which has no region catalog; v1 deficiency, not T17 scope).

Screenshots: test-results/scenario-92-audit-viewer.png +
             test-results/scenario-92-resource-mutation-panel.png

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
T18: collapse the duplicated literal JWT secret to a single referenced source.

- test/run.sh: extract JWT_SECRET from config/app.yaml via python3 regex on
  the auth.jwt module's `secret:` field; fallback to literal if extraction
  fails; export JWT_SECRET so Playwright inherits it.
- e2e/scenario-92-infra-admin.spec.ts: read from process.env['JWT_SECRET']
  with fallback to the literal for standalone runs without run.sh.

The auth.jwt module's `config/app.yaml::secret` remains the authoritative
source. Tests no longer hard-code the secret independently — they read it at
runtime so a secret rotation only requires updating app.yaml.

T16/T17 re-verified green after this change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… CI-skipped)

T19: creates test/live_parity_test.go with three tests:
- TestLiveParity_PlanApplyDestroyShapeParity: drives Plan→Apply→Destroy
  against real providers (aws/gcp/do) and asserts response shape parity
  (plan_id non-empty, desired_hash 64-char hex, applied[]/destroyed[] arrays)
- TestLiveParity_DriftCheckShape: drift response shape (resource_name, drifted bool)
- TestLiveParity_SkipsByDefault: documents that liveParitySkip() correctly
  skips sub-tests when WFCTL_LIVE_CLOUD≠1 (this test always passes in CI)

All tests skip by default via liveParitySkip(t) → t.Skip when
WFCTL_LIVE_CLOUD != "1". CI never sets this env var.

Required env when WFCTL_LIVE_CLOUD=1:
  INFRA_ADMIN_BASE_URL, INFRA_ADMIN_BEARER, plus provider creds
  (AWS_ACCESS_KEY_ID+SECRET, GOOGLE_APPLICATION_CREDENTIALS, DIGITALOCEAN_TOKEN).
  detectAvailableProviders() skips sub-tests for providers with no creds.

go test ./scenarios/92-infra-admin-demo/test/ -run LiveParity → SKIP/PASS (0 FAIL).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… viewer→403

T15 CRITICAL: policies YAML shape — bare sequence `- [sub,obj,act]` not
  `- values: [...]` (map form rejected by authz plugin parsePolicies).
T15: switch authz module type from authz.casbin (external plugin) to
  authz.local (in-process, registered under scenario_stub build tag via
  PR-1b localauthz plugin). JWT `sub` = casbin subject; operator can
  apply/destroy; viewer denied (403).
T15: WORKFLOW_REPO default → infra-admin-authz-inproc worktree (has both
  stubprovider + localauthz under -tags scenario_stub). Remove
  workflow-plugin-authz build step (not needed).
T15 IMPORTANT-1: app-do-dryrun.yaml add auth-mw module + fix auth_module:
  auth-mw (was auth.jwt which is not HTTPMiddleware → fatal Init error).
T15 IMPORTANT-2: app-do-dryrun.yaml add register-infra-admin-actions
  pipeline (missing → infra.admin.Start() error on 4th TriggerWorkflow).
T15 IMPORTANT-3: app-do-dryrun.yaml omit state_module (consistent with
  app.yaml; both use nil store, handlers nil-safe).
T15 suggestion: seed.sh comment "three" → "four" registration pipelines.
T15: add health.checker to app-do-dryrun.yaml for /healthz.

T16 CRITICAL: add viewer→403 assertion to test/run.sh — authz.local
  denies viewer infra:apply → HTTP 403 (server-side RBAC, not client evidence).
T16 IMPORTANT: module/infra_admin.go writeMutationResponse helper →
  writes HTTP 403 when Output.Error contains "denied" (plan §T8 "403 if
  denied"). Apply + Destroy handlers use writeMutationResponse; reads keep
  writeProtoMsg (200 + tag-100 error).

T17 CRITICAL: add viewer→403 Playwright spec — mints viewer JWT, POSTs
  apply with `plan_id: 'irrelevant'`, asserts HTTP 403 + error contains
  "denied".
T17 IMPORTANT: actions.html goto → assert resp?.status() == 200 before
  content check (spec-reviewer F2).
T17: CSRF test covers all 4 endpoints (plan/apply/destroy/drift).
T17: mintHS256JWT + inner mintJWT consolidated into single module-level
  mintJWT(sub). Apply + plan tests use request fixture (no page.goto).

Live run transcript:
  run.sh: 15 passed, 1 failed (Playwright — pre-existing region timeout),
          4 skipped. viewer→403 PASS.
  Playwright --grep v1.1: 7/7 passed including viewer→403.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
T19 F1: io.ReadAll error now captured + t.Fatalf on failure (was discarded
  with _, which gave confusing 'unexpected end of JSON' on network drop).
T19 F2: fmt.Fprintf(os.Stdout,...) → t.Log (test output captured by runner,
  shown only with -v; remove //nolint:forbidigo).
T19 F3: detectAvailableProviders logs only when providers found (was always
  logging "found []" even for empty slice; changed to conditional + richer msg).
T18 F1: python3 regex extended to handle bare YAML string secrets in addition
  to quoted ones (accept [\"']?...value...[\"']? with $-terminated match).
Import cleanup: removed unused fmt import from live_parity_test.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ut of engine core)

Pivot per maintainer feedback: scenario test fixtures (stub iac.provider +
in-process authz.local Enforcer) live in the scenario repo, registered via a
scenario-owned cmd/server/main.go using workflow.NewEngineBuilder().WithPlugin()
— the established pattern (scenarios 85/86/87). Engine core no longer carries
any scenario stubs (workflow#818). go.mod pins workflow at the merged v1.1
commit (f53a7ac0) via pseudo-version; no local-path replace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… seed builds it

main.go: Build() (not BuildFromConfig) → discover+load external workflow-plugin-admin
from data-dir/plugins (admin.dashboard) via pluginexternal manager + callback server →
engine.BuildFromConfig(cfg). seed.sh builds the scenario-owned ./cmd/server (no -tags).
Stack now BOOTS in 2s; v1.1 mutation/RBAC/CSRF live-proven (18 Playwright pass).

Known gap (tracked follow-up): region dropdown depends_on provider relies on
provider_type resolution, empty in scenario-owned boot ("No valid configs found")
— 2 region-form Playwright tests fail; v1 read-form behavior, not v1.1 mutation surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t fix)

Drops the dev-only local-path replace; pins the released tag that includes the
v1.1 mutation surface (#807), proto-staleness CI (#808), engine-core cleanup +
4 RBAC-status fixes (#818), and the populateProviderTypes Init->Start fix (#823).
Scenario-owned server builds portably against v0.69.0; full stack boots + 20/20
Playwright (RBAC viewer->403, CSRF, mutation, region depends_on provider).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 1, 2026 22:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates scenario 92 to demonstrate infra-admin v1.1 using the scenario-owned server pattern, with in-repo fixtures (stub iac.provider + in-process authz.local RBAC) and expanded smoke/E2E coverage. This aligns scenario 92 with the newer scenario architecture used by scenarios 85–87 and pins the repo to workflow v0.69.0.

Changes:

  • Add a scenario-owned server (cmd/server) that loads default plugins, discovers external plugins from <data-dir>/plugins, and registers scenario-local fixtures.
  • Update scenario config / compose / seed scripts to support /healthz, data-dir, auth middleware, and RBAC wiring; extend the smoke test flow to include mutation endpoints.
  • Expand automated verification: Playwright E2E coverage, plus an env-gated live-cloud parity scaffold; bump Go dependencies (workflow v0.69.0).

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
scenarios/92-infra-admin-demo/test/run.sh Adds healthz gate, JWT secret “single source” extraction, mutation curl flow, and adjusts validation/CLI behavior.
scenarios/92-infra-admin-demo/test/live_parity_test.go Adds env-gated (WFCTL_LIVE_CLOUD) live-cloud parity scaffold tests.
scenarios/92-infra-admin-demo/seed/seed.sh Builds scenario-owned server + admin plugin into a distroless image and boots compose with healthz wait.
scenarios/92-infra-admin-demo/internal/fixtures/stubprovider.go Introduces in-process stub iac.provider fixture plugin for demo/mutation flows.
scenarios/92-infra-admin-demo/internal/fixtures/localauthz.go Introduces in-process authz.local RBAC enforcer fixture plugin.
scenarios/92-infra-admin-demo/docker-compose.yml Passes -data-dir /home/nonroot to match distroless writable paths.
scenarios/92-infra-admin-demo/config/app.yaml Switches to authz.local, adds auth middleware + health checker, updates infra.admin wiring and pipelines.
scenarios/92-infra-admin-demo/config/app-do-dryrun.yaml Mirrors authz/auth-mw/health changes for the DO dry-run variant; adds missing contribution pipeline.
scenarios/92-infra-admin-demo/cmd/server/main.go New scenario-owned server that loads defaults + fixtures and discovers external plugins.
e2e/tests/scenario-92-infra-admin.spec.ts Reads JWT secret from env when available; adds v1.1 mutation/auth/CSRF/RBAC E2E coverage.
go.mod Pins workflow v0.69.0 (and related deps) and adds modernc sqlite as a direct dependency.
go.sum Updates sums to match the dependency bump.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

try:
data = open('${CFG_LOCAL}').read()
# Accepts quoted ('...' or \"...\") or bare YAML string values.
m = re.search(r'type:\s*auth\.jwt.*?secret:\s*[\"\'\"']?([^\"\'\"'\n]+?)[\"\'\"']?\s*$', data, re.DOTALL | re.MULTILINE)
Comment on lines +115 to +117
# Note: authz_module is omitted from scenario config (external plugin not
# bridgeable as in-process Enforcer) so server falls back to authn-only mode.
# Authn gates (401) are tested; RBAC gates (403) require in-process authz.
# §App Integration. Boots:
# - auth.jwt + authz.casbin
# - http.middleware.securityheaders (permissive frame-ancestors 'self')
# - auth.jwt + authz.local (in-process RBAC, scenario_stub build tag)
Comment on lines +26 to +28
# authz.local is an in-process RBAC enforcer registered by the localauthz
# plugin under the scenario_stub build tag. JWT `sub` claim = casbin subject.
# Policies: [subject, object, action] triples — bare YAML sequence.
Comment on lines +98 to +100
# authz_module wires server-side RBAC via the in-process authz.local
# enforcer (scenario_stub tag). operator→apply/destroy allowed;
# viewer→apply/destroy denied (403).
Comment on lines +18 to +21
# Default to the infra-admin-authz-inproc worktree which includes:
# - plugins/stubprovider (iac.provider stub, scenario_stub tag)
# - plugins/localauthz (authz.local in-process RBAC, scenario_stub tag)
# PR-1b merged the localauthz plugin into this worktree (workflow#815).
…ale comments

- test/run.sh: fix the secret-extraction Python regex (was syntactically
  invalid — unescaped \' in a single-quoted raw string → SyntaxError → silently
  fell back to the hardcoded secret, defeating #31). Use \x27; now reads the
  real app.yaml secret. Fix stale authz-only comment (RBAC IS configured+tested).
- config/app.yaml + seed/seed.sh: remove stale 'scenario_stub build tag'
  comments (the scenario-owned server registers fixtures unconditionally).
- seed/seed.sh: drop dead WORKFLOW_REPO logic (referenced the closed #815
  worktree); the scenario-owned server builds from the pinned v0.69.0 module.

Verified: bash -n clean; seed boots without WORKFLOW_REPO (2s); run.sh + 20/20
Playwright pass (operator→200, viewer→403, unauth/no-bearer→401).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@intel352
Copy link
Copy Markdown
Contributor Author

intel352 commented Jun 1, 2026

Addressed all 6 Copilot findings: (1) real fix — the test/run.sh secret-extraction regex was syntactically invalid (unescaped ' in a single-quoted raw string → SyntaxError → silently fell back to the hardcoded secret, defeating the #31 single-source-of-truth); fixed with \x27, now reads the real app.yaml secret. (2-6) removed the stale scenario_stub build tag comments (app.yaml, seed.sh) + the run.sh authz-only comment (authz.local RBAC is configured + tested) + dropped dead WORKFLOW_REPO logic in seed.sh. Re-verified: boots without WORKFLOW_REPO, run.sh + 20/20 Playwright green.

@intel352
Copy link
Copy Markdown
Contributor Author

intel352 commented Jun 1, 2026

Copilot's 2 remaining comments (app.yaml:28/100) are stale re-posts — verified against source: git show HEAD:.../config/app.yaml | grep scenario_stub returns zero refs. Lines 27/99 now read "registered by the scenario-owned cmd/server (no build tags)" / "scenario-owned cmd/server fixture". The scenario_stub phrasing was removed in 6c97f6f; Copilot re-flagged the diff lines without picking up the fix. CI green; 20/20 Playwright live. Merging.

@intel352 intel352 merged commit bd9dfdf into main Jun 1, 2026
10 checks passed
@intel352 intel352 deleted the feat/scenario-92-v1.1-2026-06-01T0053 branch June 1, 2026 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants