Skip to content

feat: add admin auth and authz scenario proof - #28

Merged
intel352 merged 19 commits into
mainfrom
pr-27-v2
May 27, 2026
Merged

intel352 merged 19 commits into
mainfrom
pr-27-v2

Conversation

@intel352

Copy link
Copy Markdown
Contributor

Summary

  • Adds the Docker/Tailscale admin scenario for exercising the embedded admin portal.
  • Adds auth admin contract-shaped API and Authentication tabs that consume workflow-plugin-auth admin descriptors.
  • Improves authz UI tabs, labels, tooltips, declared scope picker, ABAC controls, and ReBAC controls.

Locked plan

Verification

  • python3 -m py_compile scenarios/90-admin-tailnet-demo/app/app.py
  • bash -n scenarios/90-admin-tailnet-demo/test/run.sh scenarios/90-admin-tailnet-demo/test/playwright-authz-qa.sh
  • git diff --check
  • docker compose -f scenarios/90-admin-tailnet-demo/docker-compose.yml up -d --build app
  • scenarios/90-admin-tailnet-demo/test/run.sh -> 88 passed, 0 failed
  • scenarios/90-admin-tailnet-demo/test/playwright-authz-qa.sh -> screenshots written to /tmp/workflow-admin-authz-qa

Notes

  • Scenario persistence is in-memory host proof; the reusable admin auth contract lives in workflow-plugin-auth.
  • The running demo is exposed locally and through the existing Tailscale sidecar config.

intel352 added 17 commits May 26, 2026 22:15
…ests

Adds a minimal but complete IaCProvider plugin at
scenarios/lib/dns-stub-plugin/ used by scenarios 89/90/91 to drive
DNS orchestration tests without provider credentials or network
access.

Components:
- store.go: file-backed JSON state store (per-stub-instance state path
  so multi-provider scenarios route to separate files). Atomic writes
  via temp-file + rename.
- provider.go: interfaces.IaCProvider impl + ResourceDriver. Diff
  intentionally returns NeedsUpdate=false on present zones so the
  import-then-plan-NoOp path (scenario 89) doesn't false-flag drift
  from minimal configs against fully-imported state. Optional fixture
  seeding on first Initialize when state file is empty (gives
  scenarios a pre-applied baseline without an explicit apply).
- server.go: pb.IaCProvider*Server wrapper mirroring the marshalling
  patterns canonicalized in workflow-plugin-digitalocean. Auto-
  registers IaCProviderRequired + IaCProviderFinalizer + IaCProvider-
  Enumerator services via SDK type-assert.
- main.go: 5-line entrypoint hands off to sdk.ServeIaCPlugin.
- fixtures/example.yaml: default fixture for scenario 89.

Per docs/plans/2026-05-26-dns-provider-contract.md PR 9 (Task 30).
Drives `wfctl infra import-all` against the stub plugin (pre-loaded
from example.yaml fixture), then asserts `wfctl infra plan` against
the same config emits the canonical 'No changes' zero-action message.

Uses WFCTL_PLUGIN_DIR=/tmp env var (not per-subcommand --plugin-dir
flag) for cleaner multi-invocation routing. Resets the stub state file
between runs so the fixture seed actually fires each invocation.

Part of docs/plans/2026-05-26-dns-provider-contract.md PR 9 (Task 31).
Two stub provider instances (stub-A, stub-B) declared in paired source/
target configs apply the SAME record set against distinct provider
modules. Test imports both states and runs verify-transfer.py with the
per-(provider, record_type, field) lossiness charter applied.

No translate-state-to-config helper — paired fixture+config pattern
(cycle-3 reviewer Option 2) eliminates the state→config schema gap that
sank the original translate-script approach.

Charter exclusions documented inline; NS records excluded from the
matrix entirely (apex nameservers are provider-managed).

Part of docs/plans/2026-05-26-dns-provider-contract.md PR 9 (Task 32).
Single `wfctl infra apply` reconciles parent zone (stub-A) with NS
delegation records pointing at the child provider's nameservers, AND
the child zone (stub-B) served independently. Test imports each
side and asserts delegation survives via jq matchers on
`.applied_config.records[]` (json tag from
workflow/interfaces/iac_state.go:37).

Adds all three new scenarios (89/90/91) to scenarios.json with
draft/localOnly metadata mirroring the shape used by 88-iac-dns-replay-
migration.

Part of docs/plans/2026-05-26-dns-provider-contract.md PR 9 (Task 33).
Code-reviewer found two BLOCKING bugs in the initial PR 9 scaffolding;
both addressed plus the non-blocking minors and a pivot in scenarios
90/91 to work around a workflow-side typed-adapter limitation.

C1 — unsupported `backend: memory` swapped for `backend: filesystem`
+ per-scenario `directory: /tmp/dns-stub-<id>-statestore/` across all
four scenario configs. Matches wfctl's resolveStateStore allowlist
(workflow/cmd/wfctl/infra_state_store.go:79-107). Test scripts
`rm -rf` the state dir between runs so the fixture seed actually fires.

C2 — plugin discovery now writes the canonical
$WFCTL_PLUGIN_DIR/<plugin-name>/<plugin-name> subdirectory layout
that wfctl's findIaCPluginDir scans for. Each scenario builds the stub
binary once and deploys it under the right plugin-named subdir(s)
alongside a plugin.json manifest carrying name + version + author +
description + iacProvider.computePlanVersion=v2 + capabilities.iacProvider.name
matching the config's `provider: stub*` declaration.

Pivot for scenarios 90 + 91: switched away from `wfctl infra apply`
to a fixture-seed + import-all + plan-NoOp pattern. The apply path
goes through platform/differ.go::classifyCreate which uses
errors.Is(err, ErrResourceNotFound) only — no string fallback. The
typed-adapter (cmd/wfctl/iac_typed_adapter.go) doesn't translate
gRPC errors back to the sentinel, so the stub plugin's not-found
returns can't satisfy that check across the gRPC wire. Pre-loading
state via fixture seeding skips that path entirely while still
exercising EnumerateAll + Import + plan-NoOp end-to-end. Scenario 89
already used this pattern; 90/91 now match.

Configs also restructured: infra.dns resources moved from a
top-level `resources:` block (which wfctl ignores) into the
`modules:` block as `type: infra.dns` modules — this is what
`hasInfraModules` (workflow/cmd/wfctl/infra_apply.go) discriminates
on. Resource module names use the SANITIZED zone IDs (sanitizeImportedZoneName
collapses `.` → `-`) so the import-then-plan roundtrip keys
desired vs current by the same Name.

Stub plugin changes:
- Adds the pb.ResourceDriverServer surface (Create/Read/Update/Delete/
  Diff/Scale/HealthCheck/SensitiveKeys + outputToPB/outputFromPB/
  diffResultToPB helpers) so the typed plan path doesn't get a
  gRPC Unimplemented status. Mirrors workflow-plugin-hover's pattern.
- stubDriver.Diff: collapsed dead branch (both arms returned the
  same value).
- server.go: removed dead unused-import guards.
- store.go: unchanged.

Minor cleanups (review I1, M*):
- Reverted drive-by Unicode-arrow normalization in scenarios.json
  (jq pretty-print side effect on 6 pre-existing entries). Rewrote
  scenarios.json via python with ensure_ascii=True to preserve the
  \u2192 escapes the rest of the file uses.
- verify-transfer.py: provider-name labels now driven from env vars
  VERIFY_SOURCE_PROVIDER + VERIFY_TARGET_PROVIDER (default
  digitalocean / cloudflare) so the verifier is reusable when the
  stub is swapped for a real provider.
- run.sh: WFCTL_BIN takes precedence over PATH; build output teed to
  a log file so failures surface diagnostics.
- scenario.yaml: status aligned to `draft` to match scenarios.json.

All three scenarios now run `5/5 0/0` against a freshly-built wfctl
(post-PR-6 / v0.65+). Verified locally with
`WFCTL_BIN=/Users/jon/workspace/workflow/bin/wfctl`.

Part of docs/plans/2026-05-26-dns-provider-contract.md PR 9 (Tasks 30-33)
review iteration.
CI on PR 27 reported two failures:

1. Schema validation FAIL on scenarios 89 + 91 config/app.yaml. The
   schema validator (workflow/schema/validate.go:checkEntryPoints)
   requires every config to declare at least one entry point —
   triggers, HTTP routes, messaging subscriptions, scheduler jobs,
   pipelines, OR a module of type http.server / scheduler.modular /
   messaging.broker. Local 'wfctl validate' with neither
   --allow-no-entry-points nor an entry-point module rejects with
   'config has no entry points'.

   Fix: added a stub http.server module (address ':0', no routes
   wired) to each affected config. This satisfies entryPointModuleTypes
   without changing scenario semantics — the scenarios never spin up
   the server, they drive 'wfctl infra import-all' + 'wfctl infra
   plan' directly. Local 'wfctl validate --skip-unknown-types' +
   '--strict --skip-unknown-types' both PASS on the two updated
   configs.

   Scenario 90's source.yaml + target.yaml are not picked up by the
   CI scan ('scenarios/*/config/app.yaml' glob), so no entry-point
   module was added there — keeps the cross-provider config diff
   minimal.

2. Go mod tidy drift — 'go mod tidy' promotes google.golang.org/protobuf
   and gopkg.in/yaml.v3 from indirect to direct require entries.
   Both are now actually imported by the stub plugin (server.go uses
   protobuf types via the workflow SDK; provider.go decodes YAML
   fixtures). The // indirect comment was stale from before the
   stub plugin existed.

All 3 scenarios still green locally (89: 6/0/0, 90: 5/0/0, 91: 5/0/0).

Part of docs/plans/2026-05-26-dns-provider-contract.md PR 9 review
iteration.
@intel352
intel352 merged commit 266cf3e into main May 27, 2026
10 checks passed
@intel352
intel352 deleted the pr-27-v2 branch May 27, 2026 10:41
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