diff --git a/.autodev/state/phase-progress.jsonl b/.autodev/state/phase-progress.jsonl new file mode 100644 index 0000000..ef63c7e --- /dev/null +++ b/.autodev/state/phase-progress.jsonl @@ -0,0 +1 @@ +{"ts":"2026-05-27T07:31:06Z","ev":"plan","pl":"2026-05-27-auth-admin-contracts.md","st":"complete","e":"Auth PR #36 and scenario PR #28 green in GitHub checks; no unresolved non-outdated review threads; auth release v0.2.9 green; scenario Docker app healthy; local and tailnet /admin redirect to login; scenario tests 89 passed, 0 failed; Playwright screenshots generated."} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9335e58..1ef6a36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ - README verified-status banner per workflow#714 (multi-repo QoL sweep). - CONTRIBUTING.md, examples/minimal/config.yaml, and GitHub issue/PR templates. +## v0.2.9 (2026-05-27) + +### Added + +- Added strict proto admin config contracts: + - `step.auth_admin_config_describe` + - `step.auth_admin_config_validate` +- Admin descriptors expose grouped controls, labels, help text, input types, + config keys, disabled reasons, and write-only secret configured state. +- Admin validation rejects unsafe auth config patches, including production + password enablement and zero-primary-method configs when required. + +### Security + +- Admin describe/validate outputs never echo secret values; outputs return + sanitized config plus `secret_fields` metadata. + ## v0.2.4 (2026-05-13) ### Strict-proto config-field gaps closed (BMW local smoke vs workflow v0.51.5, round 3) diff --git a/README.md b/README.md index a0c25c4..0a1f1a6 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ The plugin binary itself is distributed via public GitHub Releases — `GH_TOKEN - `step.auth_policy_gate` - `step.auth_methods_response` - `step.auth_policy_audit` +- `step.auth_admin_config_describe` +- `step.auth_admin_config_validate` - `step.auth_oauth_provider_config` - `step.auth_oauth_start` - `step.auth_oauth_exchange` @@ -93,10 +95,27 @@ issuance in the consuming app. shape. `step.auth_policy_audit` reports production password policy violations for CI or operational checks. +## Admin Configuration Contracts + +`step.auth_admin_config_describe` exposes a strict proto contract for admin +portals to render authentication settings. It returns grouped controls with +labels, help text, input types, config keys, disabled reasons, and write-only +secret state. Controls map to real plugin config keys consumed by the auth +policy, OAuth, WebAuthn, challenge, and delivery steps. + +`step.auth_admin_config_validate` accepts a desired config patch and returns a +sanitized accepted patch plus diagnostics. The plugin validates the patch; the +admin host persists accepted config into Workflow configuration or its own +config store. Secret values are never echoed in outputs. Production password +auth, incomplete passkey settings, incomplete OAuth settings, and zero-primary +method configurations are rejected when applicable. + ## OAuth -The first OAuth slice supports Google. Other providers currently return disabled -metadata and are not advertised as login-ready. +OAuth provider config supports Google and Facebook. Policy advertising remains +conservative and only marks providers login-ready when the configured provider +is supported by the current policy path. Instagram, X, and unknown providers +return disabled metadata and are not advertised as login-ready. `step.auth_oauth_start` emits `state`, optional PKCE values, `return_to`, `expires_at`, and `authorization_url`. The plugin does not store OAuth state. diff --git a/SPEC.md b/SPEC.md index eeebb14..50caebd 100644 --- a/SPEC.md +++ b/SPEC.md @@ -82,6 +82,8 @@ Policy: - `step.auth_methods_response` — frame methods in API response - `step.auth_policy_gate` — pre-handler gate - `step.auth_policy_audit` — audit trail +- `step.auth_admin_config_describe` — admin-renderable auth config controls +- `step.auth_admin_config_validate` — validate and sanitize admin config patches Misc: - `step.auth_normalize_phone` @@ -103,6 +105,9 @@ Misc: - V5: WebAuthn RP ID + origin pinned to env vars (no silent cross-origin). - V6: Disable_password_auth=true at module config → password steps return `{"disabled": true}` + endpoints return 410. - V7: Module-level disable knob, not global — multiple credentialModule instances may differ. +- V8: Admin config describe/validate outputs MUST NOT echo secret values; outputs expose configured state and `secret_fields` only. +- V9: Admin config validation MUST reject production password enablement and zero-primary-method configs when `require_primary_method` is true. +- V10: Admin config controls MUST map to real plugin config keys; no UI-only fake auth toggles. ## §T — Tasks (status as of 2026-05-25) @@ -119,6 +124,7 @@ Misc: | T-AUTH-9 test matrix | ⚠️ partial | 16 test files | | T-AUTH-10 SPEC.md backport | ✅ (this doc) | filed as #33 | | T-AUTH-11 registry manifest update | ✅ | workflow-registry#149 merged | +| T-AUTH-12 admin config contracts | ✅ | `step.auth_admin_config_describe`, `step.auth_admin_config_validate`, strict proto contracts | ## §X — References diff --git a/docs/plans/2026-05-27-auth-admin-contracts-design.md b/docs/plans/2026-05-27-auth-admin-contracts-design.md new file mode 100644 index 0000000..e142580 --- /dev/null +++ b/docs/plans/2026-05-27-auth-admin-contracts-design.md @@ -0,0 +1,130 @@ +# Auth Admin Contracts Design + +## Goal +Add real workflow-plugin-auth administrative contracts so admin portals can discover, render, validate, and persist auth configuration through Workflow-owned config, not demo-only UI state. + +## User Requirements +| id | requirement | design response | +|---|---|---| +| R1 | Admin portal configures real auth plugin behavior | Add strict proto admin describe/validate steps over existing auth config keys. | +| R2 | Passkeys, password disable, OAuth, and other auth modes must be administrable | Descriptor groups expose passkey, password, email code, SMS code, TOTP, Google OAuth, Facebook OAuth controls. | +| R3 | Toggles wired to real plugin config, always | Control `config_key` values map to keys already consumed by `auth_methods_policy`, OAuth, credential module, and send/challenge steps. | +| R4 | Plugin-first; demo is proof only | Scenario consumes contract-shaped API data and applies returned config patches; no scenario-only auth semantics. | +| R5 | Security standard | Secrets redacted, validation fail-closed, production password restrictions preserved, unsafe OAuth endpoints rejected unless explicitly configured for tests. | +| R6 | Releases generated after tested/proven | Bump plugin manifest to next version, validate contract, tag release after verification. | + +## Global Design Guidance +Source: workspace `AGENTS.md`, `SPEC.md`, `README.md`. + +| guidance | response | +|---|---| +| Prefer Workflow/plugin ownership boundaries | Auth plugin owns auth contracts and validation; admin/dashboard owns rendering and persistence orchestration. | +| Avoid stubs/TODO-only/partial wires | New admin controls bind to existing executable config keys; unsupported modes are marked unavailable rather than shown as functional. | +| Strict proto contracts | New admin steps have generated protobuf messages, runtime registry descriptors, and `plugin.contracts.json` entries. | +| Security/quality first | Secret redaction tests, unsafe-change tests, production password gating, and no secret echo in describe/validate outputs. | +| Scenario proves platform capability | `workflow-scenarios` gets light UI/API updates that consume the contract-shaped surface and preserve authz enforcement paths. | + +## Approaches Considered +| option | summary | trade-off | decision | +|---|---|---|---| +| A | Admin-specific step contracts in auth plugin | Fits existing strict step registry and Workflow DSL; host persists config patch. | Chosen. Minimal new host assumptions; real plugin contract. | +| B | New long-running auth admin service | More direct CRUD semantics; heavier lifecycle/discovery surface than current plugin precedent. | Rejected for now; no existing service pattern in this repo. | +| C | Keep admin UI-owned JSON schema | Fast UI iteration; duplicates plugin truth and can drift. | Rejected; violates plugin-first/no-fake requirement. | + +## Architecture +| component | responsibility | +|---|---| +| `step.auth_admin_config_describe` | Merge plugin config + runtime config + input, return sanitized effective settings, capabilities, control groups, warnings. | +| `step.auth_admin_config_validate` | Accept desired config patch, reject unsafe/incomplete changes, return sanitized accepted patch + effective policy + diagnostics. | +| `AuthAdmin*` proto messages | Stable contract for admin renderers: groups, controls, option lists, diagnostics, secret config state, OAuth provider state. | +| Existing `auth_methods_policy` helpers | Source of truth for which methods are actually enabled. Admin contracts call the same policy path. | +| Scenario admin UI | Renders tabs and labeled controls from contract-shaped JSON; submits patches through scenario API that applies plugin-equivalent validation. | + +## Contract Shape +| message | key fields | +|---|---| +| `AuthAdminConfig` | `environment`, passkey keys, email/SMS keys, password toggles, TOTP toggles, OAuth provider client/redirect keys, secret fields. | +| `AuthAdminDescribeInput` | Optional desired environment/context override; no secrets required. | +| `AuthAdminDescribeOutput` | `groups`, `effective_config`, `methods_policy`, `warnings`, `secret_fields`, `error`. | +| `AuthAdminValidateInput` | `desired_config`, `require_primary_method`, optional `allow_secret_placeholders`. | +| `AuthAdminValidateOutput` | `valid`, `accepted_config`, `methods_policy`, `errors`, `warnings`, `secret_fields`, `error`. | +| `AuthAdminControl` | `key`, `label`, `description`, `help_text`, `input_type`, `config_key`, `secret`, `configured`, `required`, `enabled`, `disabled_reason`, `options`. | + +Admin update persistence is intentionally not inside workflow-plugin-auth: the plugin validates and returns a config patch. The host/admin plugin writes that patch into Workflow config or its chosen config store. This avoids fake persistence and keeps declarative Workflow config as source of truth. + +## Security Review +| risk | mitigation | +|---|---| +| Secret disclosure through admin describe/validate | Secret values are never returned; outputs expose `configured=true/false` and `secret_fields`. | +| Admin enables zero primary login methods | Validation rejects when `require_primary_method=true` and policy result has `primary_method_count=0`. | +| Password auth in production | Existing production block stays authoritative; admin validate returns error/warning instead of enabling passwords. | +| OAuth SSRF / malicious endpoints | Existing OAuth endpoint validation preserved; insecure test endpoints require `allow_insecure_test_oauth_endpoints=true`. | +| Confused UI labels cause unsafe changes | Controls carry labels, descriptions, help text, disabled reasons, and group context. | +| Demo bypasses auth/authz semantics | Scenario admin endpoint remains behind login/session and admin scope; UI only proves contract consumption. | + +## Infrastructure Impact +| area | impact | +|---|---| +| Cloud resources | None. | +| Secrets | Existing auth secrets may be configured through host; plugin only validates presence/redacts values. | +| Network | No new network listeners; release workflow/tag push after verification. | +| Plugin loading | New step types require manifest/contract update; validate with `wfctl plugin validate-contract --for-publish`. | +| Runtime | Existing apps can ignore new step types; backward-compatible additive contract. | + +## Multi-Component Validation +| boundary | proof | +|---|---| +| Plugin proto ↔ runtime registry ↔ manifest | `GOWORK=off go test ./...`, contract registry test, `wfctl plugin validate-contract --for-publish --tag v0.2.9 .`. | +| Admin contract ↔ auth policy | Unit tests assert describe/validate mirrors `auth_methods_policy` for passkey/password/OAuth/TOTP/SMS/email. | +| Plugin contract ↔ scenario UI | Scenario API returns contract-shaped payload; Playwright exploratory QA verifies controls render/select/update and auth gate still blocks anonymous admin. | +| Release | Manifest version bumped, local checks pass, tag pushed to trigger release workflow. | + +## Assumptions +| id | assumption | challenge | fallback | +|---|---|---|---| +| A1 | Admin plugin/host can persist accepted config patches | Host persistence may not exist yet | Contract returns patch and diagnostics only; scenario uses in-memory/demo persistence as consumer proof, not plugin persistence. | +| A2 | Step contracts are the right current extension point | Future Workflow may prefer service contracts | Additive step types can coexist; service wrapper can call same helpers later. | +| A3 | Google/Facebook are the only real OAuth providers currently implemented | Proto has Instagram/X secret fields but runtime says disabled | Descriptor marks unsupported providers unavailable until real runtime support exists. | +| A4 | Password support remains allowed outside production | Some apps may forbid passwords everywhere | Admin config can set `password_auth_enabled=false`; future host policy can impose stricter constraints via input. | + +## Self-Challenge +| doubt | answer | +|---|---| +| Could this be just JSON schema in the admin UI? | That would drift from plugin behavior and violates plugin-first requirement. | +| Does validate imply persistence? | No; output is an accepted patch. Persistence belongs to Workflow config/admin host. | +| Is this too broad? | Scope stays to existing auth modes; no new auth providers or credential stores are added. | + +## Rollback +| change | rollback | +|---|---| +| Plugin code/contracts | Revert feature commits; old step types remain unchanged. | +| Manifest/version bump | Revert manifest/contract changes or publish a follow-up tag pointing at rollback commit. | +| Scenario UI/API proof | Revert scenario commits; plugin contract remains independently usable. | +| Release tag | If tag is pushed and bad, publish next patch tag with rollback/fix; do not delete public release tags. | + +## Adversarial Design Review +### Report +**Phase:** design +**Artifact:** `docs/plans/2026-05-27-auth-admin-contracts-design.md` +**Status:** PASS + +| sev | class | loc | issue | fix | +|---|---|---|---|---| +| Minor | Simpler alternative | Approach C | UI-only schema is simpler but rejected due user requirement. | Keep rejection explicit. | +| Minor | Infrastructure | Release | Tag push has external release effect. | Gate tag on local contract/test proof and use next patch tag rollback. | +| Minor | Multi-component validation | Scenario | Scenario cannot prove actual host persistence. | Limit claim to contract consumption; plugin validates patch, host persistence out of contract. | + +### Bug-Class Scan +| class | result | note | +|---|---|---| +| Project-guidance conflicts | Clean | Plugin-first ownership follows workspace and repo guidance. | +| Assumptions under attack | Clean | Persistence and provider support assumptions listed with fallback. | +| Repo-precedent conflicts | Clean | New surface uses existing strict step contract pattern. | +| YAGNI | Clean | No new auth provider/runtime store introduced. | +| Missing failure modes | Clean | Secret redaction, zero-primary, production password, OAuth endpoint abuse covered. | +| Security/privacy | Clean | No secret echo; fail-closed validation. | +| Infrastructure impact | Clean | Release/tag impact stated; no infra resource changes. | +| Multi-component validation | Clean | Plugin registry/manifest and scenario UI proof included. | +| Rollback | Clean | Runtime/release rollback path stated. | +| User-intent drift | Clean | Directly targets real plugin contracts and scenario proof. | + diff --git a/docs/plans/2026-05-27-auth-admin-contracts.md b/docs/plans/2026-05-27-auth-admin-contracts.md new file mode 100644 index 0000000..acaea1b --- /dev/null +++ b/docs/plans/2026-05-27-auth-admin-contracts.md @@ -0,0 +1,296 @@ +# Auth Admin Contracts Implementation Plan + +> **For the implementing agent:** REQUIRED SUB-SKILL: Use autodev:executing-plans to implement this plan task-by-task. + +**Goal:** Add strict auth admin contracts to workflow-plugin-auth and prove them in workflow-scenarios without demo-only auth state. + +**Architecture:** workflow-plugin-auth adds two strict proto step contracts: describe and validate auth admin config. The admin UI receives renderable control descriptors and submits config patches; the plugin validates and returns accepted patches while the host/scenario owns persistence. + +**Tech Stack:** Go, protobuf, Workflow external plugin SDK, wfctl contract validation, workflow-scenarios Python/JS/Playwright/Docker. + +**Base branch:** main + +--- + +## Scope Manifest + +**PR Count:** 2 +**Tasks:** 7 +**Estimated Lines of Change:** ~900 + +**Out of scope:** +- Adding new auth providers beyond currently implemented runtime support. +- Persisting Workflow config inside workflow-plugin-auth. +- Replacing the admin plugin renderer architecture outside the scenario proof. + +**PR Grouping:** + +| PR # | Title | Tasks | Branch | +|------|-------|-------|--------| +| 1 | Auth plugin admin contracts | Task 1, Task 2, Task 3, Task 7 | feat/auth-admin-contracts | +| 2 | Scenario auth admin proof | Task 4, Task 5, Task 6 | pr-27-v2 | + +**Status:** Complete 2026-05-27T07:31:06Z + +### Task 1: Auth Admin Contract Tests + +**Files:** +- Create: `internal/step_admin_config_test.go` +- Modify: `internal/plugin_contracts_test.go` + +**Steps:** +1. Write failing tests for `newAuthAdminConfigDescribeStep`: + - passkey controls show `webauthn_rp_id`/`webauthn_origin`. + - password control maps to `password_auth_enabled`. + - Google/Facebook OAuth groups expose provider labels and secret configured flags. + - Instagram/X are either absent or `enabled=false` with `disabled_reason`. +2. Write failing tests for `newAuthAdminConfigValidateStep`: + - rejects `environment=production` + `password_auth_enabled=true`. + - rejects zero primary methods when `require_primary_method=true`. + - accepts passkey config with RP ID + HTTPS origin. + - redacts all configured secrets from output. +3. Add registry test requirements for: + - `step.auth_admin_config_describe` + - `step.auth_admin_config_validate` +4. Run: `GOWORK=off go test ./internal -run 'TestAuthAdmin|TestContractRegistry' -count=1` +5. Expected: FAIL with missing constructor/step/contract symbols. + +**Verification:** failing tests prove contract and security expectations before implementation. + +**Rollback:** remove test file and registry expectations. + +### Task 2: Auth Admin Proto and Runtime Implementation + +**Files:** +- Modify: `internal/contracts/auth.proto` +- Modify: `internal/contracts/auth.pb.go` +- Create: `internal/step_admin_config.go` +- Modify: `internal/plugin.go` + +**Steps:** +1. Add protobuf messages: + - `AuthAdminConfig` + - `AuthAdminDescribeInput` + - `AuthAdminControlOption` + - `AuthAdminControl` + - `AuthAdminControlGroup` + - `AuthAdminDiagnostic` + - `AuthAdminDescribeOutput` + - `AuthAdminValidateConfig` + - `AuthAdminValidateInput` + - `AuthAdminValidateOutput` +2. Regenerate Go protobuf bindings: + - `protoc --go_out=. --go_opt=paths=source_relative internal/contracts/auth.proto` +3. Implement shared helpers in `step_admin_config.go`: + - merge config/runtime/current/input with existing `mergePolicyInputs`. + - build groups for Primary methods, Second factors, OAuth providers, Delivery/Secrets. + - classify `input_type` as `toggle`, `text`, `url`, `secret`, `select`. + - return labels/descriptions/help text for each control. + - redact secret values; expose only `configured`. +4. Implement describe step using `buildAuthMethodsPolicy`. +5. Implement validate step: + - merge desired config patch. + - reject production password enable. + - reject zero primary method when required. + - reject passkey enable without RP ID + HTTPS origin. + - reject OAuth enable without routes + provider client ID/secret/redirect. + - reject non-HTTPS OAuth URLs unless `allow_insecure_test_oauth_endpoints=true`. +6. Register legacy and typed step factories in `internal/plugin.go`. +7. Run: `gofmt -w internal/step_admin_config.go internal/plugin.go` +8. Run: `GOWORK=off go test ./internal -run 'TestAuthAdmin|TestContractRegistry' -count=1` +9. Expected: PASS. + +**Verification:** focused Go tests pass and generated proto compiles. + +**Rollback:** revert Task 2 files; Task 1 tests fail again until reverted. + +### Task 3: Manifest, Docs, and Contract Validation + +**Files:** +- Modify: `plugin.json` +- Modify: `plugin.contracts.json` +- Modify: `README.md` +- Modify: `SPEC.md` +- Modify: `CHANGELOG.md` + +**Steps:** +1. Add both admin step types to `plugin.json`. +2. Add strict proto descriptors to `plugin.contracts.json`. +3. Bump manifest/download references to next patch version after latest tag (`v0.2.9` unless tags changed before release). +4. Document admin contract semantics: + - plugin validates config patches. + - host/admin persists accepted patches. + - secrets are write-only/redacted. +5. Update SPEC with admin-contract constraints/invariants. +6. Update CHANGELOG with unreleased/new version entry. +7. Run: + - `GOWORK=off go test ./...` + - `wfctl plugin validate-contract --for-publish --tag v0.2.9 .` + - `PLUGIN_MANIFEST_EXPECT_VERSION=0.2.9 GOWORK=off go test ./internal -run TestIntegration_PluginManifestAndStepTypes -count=1` +8. Expected: + - Go tests report `ok`/`?`. + - wfctl validation exits 0. + - manifest integration test exits 0. +9. Commit Tasks 1-3. + +**Verification:** plugin contract validates through wfctl and tests. + +**Rollback:** revert manifest/docs/version commit; do not publish tag. + +### Task 4: Scenario API Uses Contract-Shaped Auth Admin Surface + +**Files:** +- Modify: `/Users/jon/workspace/workflow-scenarios/scenarios/99-*/...` or current admin scenario app files after locating the running admin demo. +- Modify/Create: matching scenario tests under `/Users/jon/workspace/workflow-scenarios/e2e` or scenario `test/`. + +**Steps:** +1. Locate current admin/authz scenario implementation with `rg -n "authz|admin|roles|scopes" scenarios`. +2. Add `/api/admin/auth/config` or existing-route equivalent returning the same shape as `AuthAdminDescribeOutput`. +3. Add `/api/admin/auth/config/validate` or existing-route equivalent accepting desired config patch and returning `AuthAdminValidateOutput` shape. +4. Keep persistence scenario-local but explicit: scenario stores accepted config patch in its demo state; plugin contract remains source of shape/validation semantics. +5. Keep endpoint behind the existing admin login/session/scope guard. +6. Add tests: + - anonymous request returns 401/redirect. + - non-admin user returns 403. + - admin can fetch descriptors. + - unsafe password-in-production patch returns validation error. + - valid passkey patch returns accepted config without secrets. +7. Run the scenario API test command discovered in the repo. +8. Expected: tests pass and unauthenticated admin access is blocked. + +**Verification:** scenario API proves admin consumer boundary and auth guard. + +**Rollback:** revert scenario API/test commit. + +### Task 5: Scenario Admin UX for Auth and Authz Usability + +**Files:** +- Modify: `/Users/jon/workspace/workflow-scenarios/` +- Modify/Create: Playwright specs under `/Users/jon/workspace/workflow-scenarios/e2e/tests/` + +**Steps:** +1. Add an Authentication tab to the admin portal navigation. +2. Render auth groups/controls from contract-shaped API payload. +3. Use controls from payload: + - toggles for booleans. + - select/picker for known options. + - secret inputs that never display existing values. + - URL/text fields with labels and helper tooltip text. +4. Improve authz form labels/tooltips: + - distinguish subject/user/role/resource/action/context. + - keep scope fields picker-backed by declared scopes. + - clarify admin vs frontend scope context. +5. Submit config patches through validation endpoint; show errors/warnings from diagnostics. +6. Add Playwright checks: + - tabs group auth/authz forms. + - auth controls render labels and tooltip/help text. + - scope picker has known scopes and no free-text requirement. + - auth validation error renders for production password attempt. +7. Run: scenario Playwright command discovered in repo. +8. Expected: Playwright passes and screenshots show no overlapping/ambiguous labels. + +**Verification:** browser UI consumes descriptors and improves admin usability. + +**Rollback:** revert scenario UI/test commit. + +### Task 6: Runtime, Docker/Tailnet, and Exploratory QA + +**Files:** +- Modify only scenario launch/config files if runtime validation exposes a required wiring defect. + +**Steps:** +1. Rebuild/relaunch the scenario Docker or k8s app using the repo's existing scripts. +2. Preserve existing Tailscale sidecar/host serve path. +3. Verify local app: + - `curl -i http://127.0.0.1:18080/admin` anonymous returns login/401, not admin data. + - login as admin. + - fetch auth/admin descriptors. + - submit safe and unsafe auth config patches. +4. Verify tailnet reachability using the existing tailnet URL if available. +5. Run exploratory QA with `npx playwright`/Playwright CLI: + - admin login. + - Authentication tab. + - Authz tab. + - scopes picker. + - role-scope assignment enforcement smoke. +6. Capture any UI/security defects, fix within Tasks 4/5 scope, and rerun checks. + +**Verification:** running app is reachable locally and over tailnet; admin remains auth-gated; UX paths work in browser. + +**Rollback:** stop scenario containers and revert scenario commits. + +### Task 7: Release Auth Plugin + +**Files:** +- No code files unless release verification finds a bug in Tasks 1-3. + +**Steps:** +1. Re-run final auth plugin checks: + - `GOWORK=off go test ./...` + - `wfctl plugin validate-contract --for-publish --tag v0.2.9 .` + - `git diff --check` +2. Confirm `plugin.json` version and download URLs match release tag. +3. Tag release: + - `git tag v0.2.9` + - `git push origin feat/auth-admin-contracts` + - `git push origin v0.2.9` +4. Monitor release workflow with `gh run list --workflow release.yml --limit 5` and `gh run watch `. +5. Expected: release workflow succeeds; if it fails, fix forward and publish next patch tag. + +**Verification:** GitHub release workflow green for the new auth plugin tag. + +**Rollback:** public tags are not deleted; fix forward with next patch tag or revert commit and publish rollback tag. + +## Plan Adversarial Review +### Report +**Phase:** plan +**Artifact:** `docs/plans/2026-05-27-auth-admin-contracts.md` +**Status:** PASS + +| sev | class | loc | issue | fix | +|---|---|---|---|---| +| Minor | Hidden dependency | Task 4 | Scenario cannot execute actual Go plugin step unless host wiring exists. | Keep scenario claim to contract-shaped consumer proof; plugin validation is proven in Go/wfctl. | +| Minor | Release risk | Task 7 | Tagging from feature branch may publish before PR merge. | User explicitly requested releases; gate on local validation and fix-forward on failure. | +| Minor | Task granularity | Task 2 | Proto + implementation in one task is larger than 2-5 minutes. | Kept together because generated bindings and implementation must compile atomically. | + +### Bug-Class Scan +| class | result | note | +|---|---|---| +| Project-guidance conflicts | Clean | Plugin-first, strict contract, scenario proof preserved. | +| Assumptions under attack | Clean | Host persistence assumption isolated to accepted patch output. | +| Repo-precedent conflicts | Clean | Uses existing strict step factory/contract registry pattern. | +| YAGNI | Clean | No new auth providers or service framework added. | +| Missing failure modes | Clean | Unsafe auth changes, secret redaction, anonymous admin access included. | +| Security/privacy | Clean | Tests include auth gate, redaction, zero-primary, production password. | +| Infrastructure impact | Clean | Release tag impact and runtime Docker/tailnet validation included. | +| Multi-component validation | Clean | Plugin/wfctl boundary + scenario/UI boundary both tested. | +| Rollback wiring | Clean | Each runtime/release task includes rollback. | +| Verification-class mismatch | Clean | Plugin contract uses wfctl; UI uses Playwright; runtime uses launched app/curl. | +| Hidden serial dependencies | Clean | PR grouping separates plugin and scenario; scenario depends on plugin contract shape only. | +| User-intent drift | Clean | Addresses real auth plugin contracts and updated demo. | + +## Alignment Report +**Status:** PASS + +| Design Requirement | Plan Task(s) | Status | +|---|---|---| +| R1 real plugin admin config | Task 1, Task 2, Task 3 | Covered | +| R2 administrable auth modes | Task 1, Task 2, Task 5 | Covered | +| R3 toggles wired to plugin config | Task 1, Task 2, Task 4, Task 5 | Covered | +| R4 plugin-first scenario proof | Task 4, Task 5, Task 6 | Covered | +| R5 security standard | Task 1, Task 2, Task 4, Task 6 | Covered | +| R6 releases | Task 3, Task 7 | Covered | +| Strict proto contracts | Task 2, Task 3 | Covered | +| Multi-component validation | Task 3, Task 4, Task 6 | Covered | + +| Plan Task | Design Requirement | Status | +|---|---|---| +| Task 1 | R1/R2/R5 | Justified | +| Task 2 | R1/R2/R3/R5 | Justified | +| Task 3 | R1/R6/strict proto | Justified | +| Task 4 | R4/R5 | Justified | +| Task 5 | R2/R3/R4 | Justified | +| Task 6 | R4/R5/multi-component validation | Justified | +| Task 7 | R6 | Justified | + +**Drift Items:** none. diff --git a/internal/contracts/auth.pb.go b/internal/contracts/auth.pb.go index 2536fe9..7929e88 100644 --- a/internal/contracts/auth.pb.go +++ b/internal/contracts/auth.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.0 // source: internal/contracts/auth.proto package contracts @@ -3314,6 +3314,942 @@ func (x *AuthPolicyAuditOutput) GetError() string { return "" } +type AuthAdminConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Environment string `protobuf:"bytes,1,opt,name=environment,proto3" json:"environment,omitempty"` + PasswordAuthEnabled *bool `protobuf:"varint,2,opt,name=password_auth_enabled,json=passwordAuthEnabled,proto3,oneof" json:"password_auth_enabled,omitempty"` + PasswordEnabled *bool `protobuf:"varint,3,opt,name=password_enabled,json=passwordEnabled,proto3,oneof" json:"password_enabled,omitempty"` + TotpAuthEnabled *bool `protobuf:"varint,4,opt,name=totp_auth_enabled,json=totpAuthEnabled,proto3,oneof" json:"totp_auth_enabled,omitempty"` + TotpEnabled *bool `protobuf:"varint,5,opt,name=totp_enabled,json=totpEnabled,proto3,oneof" json:"totp_enabled,omitempty"` + WebauthnRpId string `protobuf:"bytes,6,opt,name=webauthn_rp_id,json=webauthnRpId,proto3" json:"webauthn_rp_id,omitempty"` + WebauthnOrigin string `protobuf:"bytes,7,opt,name=webauthn_origin,json=webauthnOrigin,proto3" json:"webauthn_origin,omitempty"` + SmtpHost string `protobuf:"bytes,8,opt,name=smtp_host,json=smtpHost,proto3" json:"smtp_host,omitempty"` + SmtpFrom string `protobuf:"bytes,9,opt,name=smtp_from,json=smtpFrom,proto3" json:"smtp_from,omitempty"` + AuthRoutesEnabled *bool `protobuf:"varint,10,opt,name=auth_routes_enabled,json=authRoutesEnabled,proto3,oneof" json:"auth_routes_enabled,omitempty"` + RoutesEnabled *bool `protobuf:"varint,11,opt,name=routes_enabled,json=routesEnabled,proto3,oneof" json:"routes_enabled,omitempty"` + OauthRoutesEnabled *bool `protobuf:"varint,12,opt,name=oauth_routes_enabled,json=oauthRoutesEnabled,proto3,oneof" json:"oauth_routes_enabled,omitempty"` + GoogleOauthClientId string `protobuf:"bytes,13,opt,name=google_oauth_client_id,json=googleOauthClientId,proto3" json:"google_oauth_client_id,omitempty"` + GoogleOauthClientSecret string `protobuf:"bytes,14,opt,name=google_oauth_client_secret,json=googleOauthClientSecret,proto3" json:"google_oauth_client_secret,omitempty"` + GoogleOauthRedirectUrl string `protobuf:"bytes,15,opt,name=google_oauth_redirect_url,json=googleOauthRedirectUrl,proto3" json:"google_oauth_redirect_url,omitempty"` + OauthProvider string `protobuf:"bytes,16,opt,name=oauth_provider,json=oauthProvider,proto3" json:"oauth_provider,omitempty"` + SmsEnabled *bool `protobuf:"varint,17,opt,name=sms_enabled,json=smsEnabled,proto3,oneof" json:"sms_enabled,omitempty"` + TwilioVerifyServiceSid string `protobuf:"bytes,18,opt,name=twilio_verify_service_sid,json=twilioVerifyServiceSid,proto3" json:"twilio_verify_service_sid,omitempty"` + TwilioAccountSid string `protobuf:"bytes,19,opt,name=twilio_account_sid,json=twilioAccountSid,proto3" json:"twilio_account_sid,omitempty"` + TwilioAuthToken string `protobuf:"bytes,20,opt,name=twilio_auth_token,json=twilioAuthToken,proto3" json:"twilio_auth_token,omitempty"` + TwilioApiKeySid string `protobuf:"bytes,21,opt,name=twilio_api_key_sid,json=twilioApiKeySid,proto3" json:"twilio_api_key_sid,omitempty"` + TwilioApiKeySecret string `protobuf:"bytes,22,opt,name=twilio_api_key_secret,json=twilioApiKeySecret,proto3" json:"twilio_api_key_secret,omitempty"` + JwtSecret string `protobuf:"bytes,23,opt,name=jwt_secret,json=jwtSecret,proto3" json:"jwt_secret,omitempty"` + SmsAuthEnabled *bool `protobuf:"varint,24,opt,name=sms_auth_enabled,json=smsAuthEnabled,proto3,oneof" json:"sms_auth_enabled,omitempty"` + FacebookOauthClientId string `protobuf:"bytes,25,opt,name=facebook_oauth_client_id,json=facebookOauthClientId,proto3" json:"facebook_oauth_client_id,omitempty"` + FacebookOauthClientSecret string `protobuf:"bytes,26,opt,name=facebook_oauth_client_secret,json=facebookOauthClientSecret,proto3" json:"facebook_oauth_client_secret,omitempty"` + FacebookOauthRedirectUrl string `protobuf:"bytes,27,opt,name=facebook_oauth_redirect_url,json=facebookOauthRedirectUrl,proto3" json:"facebook_oauth_redirect_url,omitempty"` + InstagramOauthClientId string `protobuf:"bytes,28,opt,name=instagram_oauth_client_id,json=instagramOauthClientId,proto3" json:"instagram_oauth_client_id,omitempty"` + InstagramOauthClientSecret string `protobuf:"bytes,29,opt,name=instagram_oauth_client_secret,json=instagramOauthClientSecret,proto3" json:"instagram_oauth_client_secret,omitempty"` + XOauthClientId string `protobuf:"bytes,30,opt,name=x_oauth_client_id,json=xOauthClientId,proto3" json:"x_oauth_client_id,omitempty"` + XOauthClientSecret string `protobuf:"bytes,31,opt,name=x_oauth_client_secret,json=xOauthClientSecret,proto3" json:"x_oauth_client_secret,omitempty"` + AllowInsecureTestOauthEndpoints *bool `protobuf:"varint,32,opt,name=allow_insecure_test_oauth_endpoints,json=allowInsecureTestOauthEndpoints,proto3,oneof" json:"allow_insecure_test_oauth_endpoints,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthAdminConfig) Reset() { + *x = AuthAdminConfig{} + mi := &file_internal_contracts_auth_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthAdminConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAdminConfig) ProtoMessage() {} + +func (x *AuthAdminConfig) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAdminConfig.ProtoReflect.Descriptor instead. +func (*AuthAdminConfig) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{43} +} + +func (x *AuthAdminConfig) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +func (x *AuthAdminConfig) GetPasswordAuthEnabled() bool { + if x != nil && x.PasswordAuthEnabled != nil { + return *x.PasswordAuthEnabled + } + return false +} + +func (x *AuthAdminConfig) GetPasswordEnabled() bool { + if x != nil && x.PasswordEnabled != nil { + return *x.PasswordEnabled + } + return false +} + +func (x *AuthAdminConfig) GetTotpAuthEnabled() bool { + if x != nil && x.TotpAuthEnabled != nil { + return *x.TotpAuthEnabled + } + return false +} + +func (x *AuthAdminConfig) GetTotpEnabled() bool { + if x != nil && x.TotpEnabled != nil { + return *x.TotpEnabled + } + return false +} + +func (x *AuthAdminConfig) GetWebauthnRpId() string { + if x != nil { + return x.WebauthnRpId + } + return "" +} + +func (x *AuthAdminConfig) GetWebauthnOrigin() string { + if x != nil { + return x.WebauthnOrigin + } + return "" +} + +func (x *AuthAdminConfig) GetSmtpHost() string { + if x != nil { + return x.SmtpHost + } + return "" +} + +func (x *AuthAdminConfig) GetSmtpFrom() string { + if x != nil { + return x.SmtpFrom + } + return "" +} + +func (x *AuthAdminConfig) GetAuthRoutesEnabled() bool { + if x != nil && x.AuthRoutesEnabled != nil { + return *x.AuthRoutesEnabled + } + return false +} + +func (x *AuthAdminConfig) GetRoutesEnabled() bool { + if x != nil && x.RoutesEnabled != nil { + return *x.RoutesEnabled + } + return false +} + +func (x *AuthAdminConfig) GetOauthRoutesEnabled() bool { + if x != nil && x.OauthRoutesEnabled != nil { + return *x.OauthRoutesEnabled + } + return false +} + +func (x *AuthAdminConfig) GetGoogleOauthClientId() string { + if x != nil { + return x.GoogleOauthClientId + } + return "" +} + +func (x *AuthAdminConfig) GetGoogleOauthClientSecret() string { + if x != nil { + return x.GoogleOauthClientSecret + } + return "" +} + +func (x *AuthAdminConfig) GetGoogleOauthRedirectUrl() string { + if x != nil { + return x.GoogleOauthRedirectUrl + } + return "" +} + +func (x *AuthAdminConfig) GetOauthProvider() string { + if x != nil { + return x.OauthProvider + } + return "" +} + +func (x *AuthAdminConfig) GetSmsEnabled() bool { + if x != nil && x.SmsEnabled != nil { + return *x.SmsEnabled + } + return false +} + +func (x *AuthAdminConfig) GetTwilioVerifyServiceSid() string { + if x != nil { + return x.TwilioVerifyServiceSid + } + return "" +} + +func (x *AuthAdminConfig) GetTwilioAccountSid() string { + if x != nil { + return x.TwilioAccountSid + } + return "" +} + +func (x *AuthAdminConfig) GetTwilioAuthToken() string { + if x != nil { + return x.TwilioAuthToken + } + return "" +} + +func (x *AuthAdminConfig) GetTwilioApiKeySid() string { + if x != nil { + return x.TwilioApiKeySid + } + return "" +} + +func (x *AuthAdminConfig) GetTwilioApiKeySecret() string { + if x != nil { + return x.TwilioApiKeySecret + } + return "" +} + +func (x *AuthAdminConfig) GetJwtSecret() string { + if x != nil { + return x.JwtSecret + } + return "" +} + +func (x *AuthAdminConfig) GetSmsAuthEnabled() bool { + if x != nil && x.SmsAuthEnabled != nil { + return *x.SmsAuthEnabled + } + return false +} + +func (x *AuthAdminConfig) GetFacebookOauthClientId() string { + if x != nil { + return x.FacebookOauthClientId + } + return "" +} + +func (x *AuthAdminConfig) GetFacebookOauthClientSecret() string { + if x != nil { + return x.FacebookOauthClientSecret + } + return "" +} + +func (x *AuthAdminConfig) GetFacebookOauthRedirectUrl() string { + if x != nil { + return x.FacebookOauthRedirectUrl + } + return "" +} + +func (x *AuthAdminConfig) GetInstagramOauthClientId() string { + if x != nil { + return x.InstagramOauthClientId + } + return "" +} + +func (x *AuthAdminConfig) GetInstagramOauthClientSecret() string { + if x != nil { + return x.InstagramOauthClientSecret + } + return "" +} + +func (x *AuthAdminConfig) GetXOauthClientId() string { + if x != nil { + return x.XOauthClientId + } + return "" +} + +func (x *AuthAdminConfig) GetXOauthClientSecret() string { + if x != nil { + return x.XOauthClientSecret + } + return "" +} + +func (x *AuthAdminConfig) GetAllowInsecureTestOauthEndpoints() bool { + if x != nil && x.AllowInsecureTestOauthEndpoints != nil { + return *x.AllowInsecureTestOauthEndpoints + } + return false +} + +type AuthAdminDescribeInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *AuthAdminConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthAdminDescribeInput) Reset() { + *x = AuthAdminDescribeInput{} + mi := &file_internal_contracts_auth_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthAdminDescribeInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAdminDescribeInput) ProtoMessage() {} + +func (x *AuthAdminDescribeInput) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAdminDescribeInput.ProtoReflect.Descriptor instead. +func (*AuthAdminDescribeInput) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{44} +} + +func (x *AuthAdminDescribeInput) GetConfig() *AuthAdminConfig { + if x != nil { + return x.Config + } + return nil +} + +type AuthAdminControlOption struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthAdminControlOption) Reset() { + *x = AuthAdminControlOption{} + mi := &file_internal_contracts_auth_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthAdminControlOption) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAdminControlOption) ProtoMessage() {} + +func (x *AuthAdminControlOption) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAdminControlOption.ProtoReflect.Descriptor instead. +func (*AuthAdminControlOption) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{45} +} + +func (x *AuthAdminControlOption) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *AuthAdminControlOption) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *AuthAdminControlOption) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +type AuthAdminControl struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + GroupKey string `protobuf:"bytes,2,opt,name=group_key,json=groupKey,proto3" json:"group_key,omitempty"` + Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + HelpText string `protobuf:"bytes,5,opt,name=help_text,json=helpText,proto3" json:"help_text,omitempty"` + InputType string `protobuf:"bytes,6,opt,name=input_type,json=inputType,proto3" json:"input_type,omitempty"` + ConfigKey string `protobuf:"bytes,7,opt,name=config_key,json=configKey,proto3" json:"config_key,omitempty"` + Secret bool `protobuf:"varint,8,opt,name=secret,proto3" json:"secret,omitempty"` + Configured bool `protobuf:"varint,9,opt,name=configured,proto3" json:"configured,omitempty"` + Required bool `protobuf:"varint,10,opt,name=required,proto3" json:"required,omitempty"` + Enabled bool `protobuf:"varint,11,opt,name=enabled,proto3" json:"enabled,omitempty"` + DisabledReason string `protobuf:"bytes,12,opt,name=disabled_reason,json=disabledReason,proto3" json:"disabled_reason,omitempty"` + Options []*AuthAdminControlOption `protobuf:"bytes,13,rep,name=options,proto3" json:"options,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthAdminControl) Reset() { + *x = AuthAdminControl{} + mi := &file_internal_contracts_auth_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthAdminControl) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAdminControl) ProtoMessage() {} + +func (x *AuthAdminControl) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAdminControl.ProtoReflect.Descriptor instead. +func (*AuthAdminControl) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{46} +} + +func (x *AuthAdminControl) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *AuthAdminControl) GetGroupKey() string { + if x != nil { + return x.GroupKey + } + return "" +} + +func (x *AuthAdminControl) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *AuthAdminControl) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AuthAdminControl) GetHelpText() string { + if x != nil { + return x.HelpText + } + return "" +} + +func (x *AuthAdminControl) GetInputType() string { + if x != nil { + return x.InputType + } + return "" +} + +func (x *AuthAdminControl) GetConfigKey() string { + if x != nil { + return x.ConfigKey + } + return "" +} + +func (x *AuthAdminControl) GetSecret() bool { + if x != nil { + return x.Secret + } + return false +} + +func (x *AuthAdminControl) GetConfigured() bool { + if x != nil { + return x.Configured + } + return false +} + +func (x *AuthAdminControl) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +func (x *AuthAdminControl) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *AuthAdminControl) GetDisabledReason() string { + if x != nil { + return x.DisabledReason + } + return "" +} + +func (x *AuthAdminControl) GetOptions() []*AuthAdminControlOption { + if x != nil { + return x.Options + } + return nil +} + +type AuthAdminControlGroup struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Controls []*AuthAdminControl `protobuf:"bytes,4,rep,name=controls,proto3" json:"controls,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthAdminControlGroup) Reset() { + *x = AuthAdminControlGroup{} + mi := &file_internal_contracts_auth_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthAdminControlGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAdminControlGroup) ProtoMessage() {} + +func (x *AuthAdminControlGroup) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAdminControlGroup.ProtoReflect.Descriptor instead. +func (*AuthAdminControlGroup) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{47} +} + +func (x *AuthAdminControlGroup) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *AuthAdminControlGroup) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *AuthAdminControlGroup) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AuthAdminControlGroup) GetControls() []*AuthAdminControl { + if x != nil { + return x.Controls + } + return nil +} + +type AuthAdminDiagnostic struct { + state protoimpl.MessageState `protogen:"open.v1"` + Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"` + Severity string `protobuf:"bytes,2,opt,name=severity,proto3" json:"severity,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthAdminDiagnostic) Reset() { + *x = AuthAdminDiagnostic{} + mi := &file_internal_contracts_auth_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthAdminDiagnostic) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAdminDiagnostic) ProtoMessage() {} + +func (x *AuthAdminDiagnostic) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAdminDiagnostic.ProtoReflect.Descriptor instead. +func (*AuthAdminDiagnostic) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{48} +} + +func (x *AuthAdminDiagnostic) GetField() string { + if x != nil { + return x.Field + } + return "" +} + +func (x *AuthAdminDiagnostic) GetSeverity() string { + if x != nil { + return x.Severity + } + return "" +} + +func (x *AuthAdminDiagnostic) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type AuthAdminDescribeOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Groups []*AuthAdminControlGroup `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty"` + EffectiveConfig *structpb.Struct `protobuf:"bytes,2,opt,name=effective_config,json=effectiveConfig,proto3" json:"effective_config,omitempty"` + MethodsPolicy *structpb.Struct `protobuf:"bytes,3,opt,name=methods_policy,json=methodsPolicy,proto3" json:"methods_policy,omitempty"` + Warnings []*AuthAdminDiagnostic `protobuf:"bytes,4,rep,name=warnings,proto3" json:"warnings,omitempty"` + SecretFields []string `protobuf:"bytes,5,rep,name=secret_fields,json=secretFields,proto3" json:"secret_fields,omitempty"` + Error string `protobuf:"bytes,100,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthAdminDescribeOutput) Reset() { + *x = AuthAdminDescribeOutput{} + mi := &file_internal_contracts_auth_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthAdminDescribeOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAdminDescribeOutput) ProtoMessage() {} + +func (x *AuthAdminDescribeOutput) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAdminDescribeOutput.ProtoReflect.Descriptor instead. +func (*AuthAdminDescribeOutput) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{49} +} + +func (x *AuthAdminDescribeOutput) GetGroups() []*AuthAdminControlGroup { + if x != nil { + return x.Groups + } + return nil +} + +func (x *AuthAdminDescribeOutput) GetEffectiveConfig() *structpb.Struct { + if x != nil { + return x.EffectiveConfig + } + return nil +} + +func (x *AuthAdminDescribeOutput) GetMethodsPolicy() *structpb.Struct { + if x != nil { + return x.MethodsPolicy + } + return nil +} + +func (x *AuthAdminDescribeOutput) GetWarnings() []*AuthAdminDiagnostic { + if x != nil { + return x.Warnings + } + return nil +} + +func (x *AuthAdminDescribeOutput) GetSecretFields() []string { + if x != nil { + return x.SecretFields + } + return nil +} + +func (x *AuthAdminDescribeOutput) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type AuthAdminValidateConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequirePrimaryMethod *bool `protobuf:"varint,1,opt,name=require_primary_method,json=requirePrimaryMethod,proto3,oneof" json:"require_primary_method,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthAdminValidateConfig) Reset() { + *x = AuthAdminValidateConfig{} + mi := &file_internal_contracts_auth_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthAdminValidateConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAdminValidateConfig) ProtoMessage() {} + +func (x *AuthAdminValidateConfig) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAdminValidateConfig.ProtoReflect.Descriptor instead. +func (*AuthAdminValidateConfig) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{50} +} + +func (x *AuthAdminValidateConfig) GetRequirePrimaryMethod() bool { + if x != nil && x.RequirePrimaryMethod != nil { + return *x.RequirePrimaryMethod + } + return false +} + +type AuthAdminValidateInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + DesiredConfig *structpb.Struct `protobuf:"bytes,1,opt,name=desired_config,json=desiredConfig,proto3" json:"desired_config,omitempty"` + RequirePrimaryMethod *bool `protobuf:"varint,2,opt,name=require_primary_method,json=requirePrimaryMethod,proto3,oneof" json:"require_primary_method,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthAdminValidateInput) Reset() { + *x = AuthAdminValidateInput{} + mi := &file_internal_contracts_auth_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthAdminValidateInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAdminValidateInput) ProtoMessage() {} + +func (x *AuthAdminValidateInput) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAdminValidateInput.ProtoReflect.Descriptor instead. +func (*AuthAdminValidateInput) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{51} +} + +func (x *AuthAdminValidateInput) GetDesiredConfig() *structpb.Struct { + if x != nil { + return x.DesiredConfig + } + return nil +} + +func (x *AuthAdminValidateInput) GetRequirePrimaryMethod() bool { + if x != nil && x.RequirePrimaryMethod != nil { + return *x.RequirePrimaryMethod + } + return false +} + +type AuthAdminValidateOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` + AcceptedConfig *structpb.Struct `protobuf:"bytes,2,opt,name=accepted_config,json=acceptedConfig,proto3" json:"accepted_config,omitempty"` + MethodsPolicy *structpb.Struct `protobuf:"bytes,3,opt,name=methods_policy,json=methodsPolicy,proto3" json:"methods_policy,omitempty"` + Errors []*AuthAdminDiagnostic `protobuf:"bytes,4,rep,name=errors,proto3" json:"errors,omitempty"` + Warnings []*AuthAdminDiagnostic `protobuf:"bytes,5,rep,name=warnings,proto3" json:"warnings,omitempty"` + SecretFields []string `protobuf:"bytes,6,rep,name=secret_fields,json=secretFields,proto3" json:"secret_fields,omitempty"` + Error string `protobuf:"bytes,100,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthAdminValidateOutput) Reset() { + *x = AuthAdminValidateOutput{} + mi := &file_internal_contracts_auth_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthAdminValidateOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthAdminValidateOutput) ProtoMessage() {} + +func (x *AuthAdminValidateOutput) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthAdminValidateOutput.ProtoReflect.Descriptor instead. +func (*AuthAdminValidateOutput) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{52} +} + +func (x *AuthAdminValidateOutput) GetValid() bool { + if x != nil { + return x.Valid + } + return false +} + +func (x *AuthAdminValidateOutput) GetAcceptedConfig() *structpb.Struct { + if x != nil { + return x.AcceptedConfig + } + return nil +} + +func (x *AuthAdminValidateOutput) GetMethodsPolicy() *structpb.Struct { + if x != nil { + return x.MethodsPolicy + } + return nil +} + +func (x *AuthAdminValidateOutput) GetErrors() []*AuthAdminDiagnostic { + if x != nil { + return x.Errors + } + return nil +} + +func (x *AuthAdminValidateOutput) GetWarnings() []*AuthAdminDiagnostic { + if x != nil { + return x.Warnings + } + return nil +} + +func (x *AuthAdminValidateOutput) GetSecretFields() []string { + if x != nil { + return x.SecretFields + } + return nil +} + +func (x *AuthAdminValidateOutput) GetError() string { + if x != nil { + return x.Error + } + return "" +} + type OAuthProviderConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` @@ -3341,7 +4277,7 @@ type OAuthProviderConfig struct { func (x *OAuthProviderConfig) Reset() { *x = OAuthProviderConfig{} - mi := &file_internal_contracts_auth_proto_msgTypes[43] + mi := &file_internal_contracts_auth_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3353,7 +4289,7 @@ func (x *OAuthProviderConfig) String() string { func (*OAuthProviderConfig) ProtoMessage() {} func (x *OAuthProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[43] + mi := &file_internal_contracts_auth_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3366,7 +4302,7 @@ func (x *OAuthProviderConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use OAuthProviderConfig.ProtoReflect.Descriptor instead. func (*OAuthProviderConfig) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{43} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{53} } func (x *OAuthProviderConfig) GetProvider() string { @@ -3467,7 +4403,7 @@ type OAuthProviderInput struct { func (x *OAuthProviderInput) Reset() { *x = OAuthProviderInput{} - mi := &file_internal_contracts_auth_proto_msgTypes[44] + mi := &file_internal_contracts_auth_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3479,7 +4415,7 @@ func (x *OAuthProviderInput) String() string { func (*OAuthProviderInput) ProtoMessage() {} func (x *OAuthProviderInput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[44] + mi := &file_internal_contracts_auth_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3492,7 +4428,7 @@ func (x *OAuthProviderInput) ProtoReflect() protoreflect.Message { // Deprecated: Use OAuthProviderInput.ProtoReflect.Descriptor instead. func (*OAuthProviderInput) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{44} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{54} } func (x *OAuthProviderInput) GetProvider() string { @@ -3555,7 +4491,7 @@ type OAuthProviderConfigOutput struct { func (x *OAuthProviderConfigOutput) Reset() { *x = OAuthProviderConfigOutput{} - mi := &file_internal_contracts_auth_proto_msgTypes[45] + mi := &file_internal_contracts_auth_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3567,7 +4503,7 @@ func (x *OAuthProviderConfigOutput) String() string { func (*OAuthProviderConfigOutput) ProtoMessage() {} func (x *OAuthProviderConfigOutput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[45] + mi := &file_internal_contracts_auth_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3580,7 +4516,7 @@ func (x *OAuthProviderConfigOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use OAuthProviderConfigOutput.ProtoReflect.Descriptor instead. func (*OAuthProviderConfigOutput) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{45} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{55} } func (x *OAuthProviderConfigOutput) GetProvider() string { @@ -3672,7 +4608,7 @@ type OAuthStartOutput struct { func (x *OAuthStartOutput) Reset() { *x = OAuthStartOutput{} - mi := &file_internal_contracts_auth_proto_msgTypes[46] + mi := &file_internal_contracts_auth_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3684,7 +4620,7 @@ func (x *OAuthStartOutput) String() string { func (*OAuthStartOutput) ProtoMessage() {} func (x *OAuthStartOutput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[46] + mi := &file_internal_contracts_auth_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3697,7 +4633,7 @@ func (x *OAuthStartOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use OAuthStartOutput.ProtoReflect.Descriptor instead. func (*OAuthStartOutput) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{46} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{56} } func (x *OAuthStartOutput) GetStarted() bool { @@ -3795,7 +4731,7 @@ type OAuthExchangeOutput struct { func (x *OAuthExchangeOutput) Reset() { *x = OAuthExchangeOutput{} - mi := &file_internal_contracts_auth_proto_msgTypes[47] + mi := &file_internal_contracts_auth_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3807,7 +4743,7 @@ func (x *OAuthExchangeOutput) String() string { func (*OAuthExchangeOutput) ProtoMessage() {} func (x *OAuthExchangeOutput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[47] + mi := &file_internal_contracts_auth_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3820,7 +4756,7 @@ func (x *OAuthExchangeOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use OAuthExchangeOutput.ProtoReflect.Descriptor instead. func (*OAuthExchangeOutput) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{47} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{57} } func (x *OAuthExchangeOutput) GetExchanged() bool { @@ -3911,7 +4847,7 @@ type OAuthUserinfoOutput struct { func (x *OAuthUserinfoOutput) Reset() { *x = OAuthUserinfoOutput{} - mi := &file_internal_contracts_auth_proto_msgTypes[48] + mi := &file_internal_contracts_auth_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3923,7 +4859,7 @@ func (x *OAuthUserinfoOutput) String() string { func (*OAuthUserinfoOutput) ProtoMessage() {} func (x *OAuthUserinfoOutput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[48] + mi := &file_internal_contracts_auth_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3936,7 +4872,7 @@ func (x *OAuthUserinfoOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use OAuthUserinfoOutput.ProtoReflect.Descriptor instead. func (*OAuthUserinfoOutput) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{48} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{58} } func (x *OAuthUserinfoOutput) GetFetched() bool { @@ -4018,7 +4954,7 @@ type CredentialListInput struct { func (x *CredentialListInput) Reset() { *x = CredentialListInput{} - mi := &file_internal_contracts_auth_proto_msgTypes[49] + mi := &file_internal_contracts_auth_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4030,7 +4966,7 @@ func (x *CredentialListInput) String() string { func (*CredentialListInput) ProtoMessage() {} func (x *CredentialListInput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[49] + mi := &file_internal_contracts_auth_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4043,7 +4979,7 @@ func (x *CredentialListInput) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialListInput.ProtoReflect.Descriptor instead. func (*CredentialListInput) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{49} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{59} } func (x *CredentialListInput) GetCredentialsJson() string { @@ -4066,7 +5002,7 @@ type CredentialSummary struct { func (x *CredentialSummary) Reset() { *x = CredentialSummary{} - mi := &file_internal_contracts_auth_proto_msgTypes[50] + mi := &file_internal_contracts_auth_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4078,7 +5014,7 @@ func (x *CredentialSummary) String() string { func (*CredentialSummary) ProtoMessage() {} func (x *CredentialSummary) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[50] + mi := &file_internal_contracts_auth_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4091,7 +5027,7 @@ func (x *CredentialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialSummary.ProtoReflect.Descriptor instead. func (*CredentialSummary) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{50} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{60} } func (x *CredentialSummary) GetId() string { @@ -4140,7 +5076,7 @@ type CredentialListOutput struct { func (x *CredentialListOutput) Reset() { *x = CredentialListOutput{} - mi := &file_internal_contracts_auth_proto_msgTypes[51] + mi := &file_internal_contracts_auth_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4152,7 +5088,7 @@ func (x *CredentialListOutput) String() string { func (*CredentialListOutput) ProtoMessage() {} func (x *CredentialListOutput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[51] + mi := &file_internal_contracts_auth_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4165,7 +5101,7 @@ func (x *CredentialListOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialListOutput.ProtoReflect.Descriptor instead. func (*CredentialListOutput) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{51} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{61} } func (x *CredentialListOutput) GetCredentials() []*CredentialSummary { @@ -4200,7 +5136,7 @@ type CredentialRevokeInput struct { func (x *CredentialRevokeInput) Reset() { *x = CredentialRevokeInput{} - mi := &file_internal_contracts_auth_proto_msgTypes[52] + mi := &file_internal_contracts_auth_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4212,7 +5148,7 @@ func (x *CredentialRevokeInput) String() string { func (*CredentialRevokeInput) ProtoMessage() {} func (x *CredentialRevokeInput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[52] + mi := &file_internal_contracts_auth_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4225,7 +5161,7 @@ func (x *CredentialRevokeInput) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialRevokeInput.ProtoReflect.Descriptor instead. func (*CredentialRevokeInput) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{52} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{62} } func (x *CredentialRevokeInput) GetCredentialId() string { @@ -4260,7 +5196,7 @@ type CredentialRevokeOutput struct { func (x *CredentialRevokeOutput) Reset() { *x = CredentialRevokeOutput{} - mi := &file_internal_contracts_auth_proto_msgTypes[53] + mi := &file_internal_contracts_auth_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4272,7 +5208,7 @@ func (x *CredentialRevokeOutput) String() string { func (*CredentialRevokeOutput) ProtoMessage() {} func (x *CredentialRevokeOutput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[53] + mi := &file_internal_contracts_auth_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4285,7 +5221,7 @@ func (x *CredentialRevokeOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialRevokeOutput.ProtoReflect.Descriptor instead. func (*CredentialRevokeOutput) Descriptor() ([]byte, []int) { - return file_internal_contracts_auth_proto_rawDescGZIP(), []int{53} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{63} } func (x *CredentialRevokeOutput) GetAuthorized() bool { @@ -4620,6 +5556,108 @@ const file_internal_contracts_auth_proto_rawDesc = "" + "\n" + "violations\x18\x02 \x03(\tR\n" + "violations\x12\x14\n" + + "\x05error\x18d \x01(\tR\x05error\"\xa2\x0e\n" + + "\x0fAuthAdminConfig\x12 \n" + + "\venvironment\x18\x01 \x01(\tR\venvironment\x127\n" + + "\x15password_auth_enabled\x18\x02 \x01(\bH\x00R\x13passwordAuthEnabled\x88\x01\x01\x12.\n" + + "\x10password_enabled\x18\x03 \x01(\bH\x01R\x0fpasswordEnabled\x88\x01\x01\x12/\n" + + "\x11totp_auth_enabled\x18\x04 \x01(\bH\x02R\x0ftotpAuthEnabled\x88\x01\x01\x12&\n" + + "\ftotp_enabled\x18\x05 \x01(\bH\x03R\vtotpEnabled\x88\x01\x01\x12$\n" + + "\x0ewebauthn_rp_id\x18\x06 \x01(\tR\fwebauthnRpId\x12'\n" + + "\x0fwebauthn_origin\x18\a \x01(\tR\x0ewebauthnOrigin\x12\x1b\n" + + "\tsmtp_host\x18\b \x01(\tR\bsmtpHost\x12\x1b\n" + + "\tsmtp_from\x18\t \x01(\tR\bsmtpFrom\x123\n" + + "\x13auth_routes_enabled\x18\n" + + " \x01(\bH\x04R\x11authRoutesEnabled\x88\x01\x01\x12*\n" + + "\x0eroutes_enabled\x18\v \x01(\bH\x05R\rroutesEnabled\x88\x01\x01\x125\n" + + "\x14oauth_routes_enabled\x18\f \x01(\bH\x06R\x12oauthRoutesEnabled\x88\x01\x01\x123\n" + + "\x16google_oauth_client_id\x18\r \x01(\tR\x13googleOauthClientId\x12;\n" + + "\x1agoogle_oauth_client_secret\x18\x0e \x01(\tR\x17googleOauthClientSecret\x129\n" + + "\x19google_oauth_redirect_url\x18\x0f \x01(\tR\x16googleOauthRedirectUrl\x12%\n" + + "\x0eoauth_provider\x18\x10 \x01(\tR\roauthProvider\x12$\n" + + "\vsms_enabled\x18\x11 \x01(\bH\aR\n" + + "smsEnabled\x88\x01\x01\x129\n" + + "\x19twilio_verify_service_sid\x18\x12 \x01(\tR\x16twilioVerifyServiceSid\x12,\n" + + "\x12twilio_account_sid\x18\x13 \x01(\tR\x10twilioAccountSid\x12*\n" + + "\x11twilio_auth_token\x18\x14 \x01(\tR\x0ftwilioAuthToken\x12+\n" + + "\x12twilio_api_key_sid\x18\x15 \x01(\tR\x0ftwilioApiKeySid\x121\n" + + "\x15twilio_api_key_secret\x18\x16 \x01(\tR\x12twilioApiKeySecret\x12\x1d\n" + + "\n" + + "jwt_secret\x18\x17 \x01(\tR\tjwtSecret\x12-\n" + + "\x10sms_auth_enabled\x18\x18 \x01(\bH\bR\x0esmsAuthEnabled\x88\x01\x01\x127\n" + + "\x18facebook_oauth_client_id\x18\x19 \x01(\tR\x15facebookOauthClientId\x12?\n" + + "\x1cfacebook_oauth_client_secret\x18\x1a \x01(\tR\x19facebookOauthClientSecret\x12=\n" + + "\x1bfacebook_oauth_redirect_url\x18\x1b \x01(\tR\x18facebookOauthRedirectUrl\x129\n" + + "\x19instagram_oauth_client_id\x18\x1c \x01(\tR\x16instagramOauthClientId\x12A\n" + + "\x1dinstagram_oauth_client_secret\x18\x1d \x01(\tR\x1ainstagramOauthClientSecret\x12)\n" + + "\x11x_oauth_client_id\x18\x1e \x01(\tR\x0exOauthClientId\x121\n" + + "\x15x_oauth_client_secret\x18\x1f \x01(\tR\x12xOauthClientSecret\x12Q\n" + + "#allow_insecure_test_oauth_endpoints\x18 \x01(\bH\tR\x1fallowInsecureTestOauthEndpoints\x88\x01\x01B\x18\n" + + "\x16_password_auth_enabledB\x13\n" + + "\x11_password_enabledB\x14\n" + + "\x12_totp_auth_enabledB\x0f\n" + + "\r_totp_enabledB\x16\n" + + "\x14_auth_routes_enabledB\x11\n" + + "\x0f_routes_enabledB\x17\n" + + "\x15_oauth_routes_enabledB\x0e\n" + + "\f_sms_enabledB\x13\n" + + "\x11_sms_auth_enabledB&\n" + + "$_allow_insecure_test_oauth_endpoints\"[\n" + + "\x16AuthAdminDescribeInput\x12A\n" + + "\x06config\x18\x01 \x01(\v2).workflow.plugins.auth.v1.AuthAdminConfigR\x06config\"f\n" + + "\x16AuthAdminControlOption\x12\x14\n" + + "\x05value\x18\x01 \x01(\tR\x05value\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\"\xb7\x03\n" + + "\x10AuthAdminControl\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x1b\n" + + "\tgroup_key\x18\x02 \x01(\tR\bgroupKey\x12\x14\n" + + "\x05label\x18\x03 \x01(\tR\x05label\x12 \n" + + "\vdescription\x18\x04 \x01(\tR\vdescription\x12\x1b\n" + + "\thelp_text\x18\x05 \x01(\tR\bhelpText\x12\x1d\n" + + "\n" + + "input_type\x18\x06 \x01(\tR\tinputType\x12\x1d\n" + + "\n" + + "config_key\x18\a \x01(\tR\tconfigKey\x12\x16\n" + + "\x06secret\x18\b \x01(\bR\x06secret\x12\x1e\n" + + "\n" + + "configured\x18\t \x01(\bR\n" + + "configured\x12\x1a\n" + + "\brequired\x18\n" + + " \x01(\bR\brequired\x12\x18\n" + + "\aenabled\x18\v \x01(\bR\aenabled\x12'\n" + + "\x0fdisabled_reason\x18\f \x01(\tR\x0edisabledReason\x12J\n" + + "\aoptions\x18\r \x03(\v20.workflow.plugins.auth.v1.AuthAdminControlOptionR\aoptions\"\xa9\x01\n" + + "\x15AuthAdminControlGroup\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12F\n" + + "\bcontrols\x18\x04 \x03(\v2*.workflow.plugins.auth.v1.AuthAdminControlR\bcontrols\"a\n" + + "\x13AuthAdminDiagnostic\x12\x14\n" + + "\x05field\x18\x01 \x01(\tR\x05field\x12\x1a\n" + + "\bseverity\x18\x02 \x01(\tR\bseverity\x12\x18\n" + + "\amessage\x18\x03 \x01(\tR\amessage\"\xec\x02\n" + + "\x17AuthAdminDescribeOutput\x12G\n" + + "\x06groups\x18\x01 \x03(\v2/.workflow.plugins.auth.v1.AuthAdminControlGroupR\x06groups\x12B\n" + + "\x10effective_config\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x0feffectiveConfig\x12>\n" + + "\x0emethods_policy\x18\x03 \x01(\v2\x17.google.protobuf.StructR\rmethodsPolicy\x12I\n" + + "\bwarnings\x18\x04 \x03(\v2-.workflow.plugins.auth.v1.AuthAdminDiagnosticR\bwarnings\x12#\n" + + "\rsecret_fields\x18\x05 \x03(\tR\fsecretFields\x12\x14\n" + + "\x05error\x18d \x01(\tR\x05error\"o\n" + + "\x17AuthAdminValidateConfig\x129\n" + + "\x16require_primary_method\x18\x01 \x01(\bH\x00R\x14requirePrimaryMethod\x88\x01\x01B\x19\n" + + "\x17_require_primary_method\"\xae\x01\n" + + "\x16AuthAdminValidateInput\x12>\n" + + "\x0edesired_config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\rdesiredConfig\x129\n" + + "\x16require_primary_method\x18\x02 \x01(\bH\x00R\x14requirePrimaryMethod\x88\x01\x01B\x19\n" + + "\x17_require_primary_method\"\xfe\x02\n" + + "\x17AuthAdminValidateOutput\x12\x14\n" + + "\x05valid\x18\x01 \x01(\bR\x05valid\x12@\n" + + "\x0faccepted_config\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x0eacceptedConfig\x12>\n" + + "\x0emethods_policy\x18\x03 \x01(\v2\x17.google.protobuf.StructR\rmethodsPolicy\x12E\n" + + "\x06errors\x18\x04 \x03(\v2-.workflow.plugins.auth.v1.AuthAdminDiagnosticR\x06errors\x12I\n" + + "\bwarnings\x18\x05 \x03(\v2-.workflow.plugins.auth.v1.AuthAdminDiagnosticR\bwarnings\x12#\n" + + "\rsecret_fields\x18\x06 \x03(\tR\fsecretFields\x12\x14\n" + "\x05error\x18d \x01(\tR\x05error\"\xba\x05\n" + "\x13OAuthProviderConfig\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x123\n" + @@ -4733,7 +5771,7 @@ func file_internal_contracts_auth_proto_rawDescGZIP() []byte { return file_internal_contracts_auth_proto_rawDescData } -var file_internal_contracts_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 54) +var file_internal_contracts_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 64) var file_internal_contracts_auth_proto_goTypes = []any{ (*CredentialModuleConfig)(nil), // 0: workflow.plugins.auth.v1.CredentialModuleConfig (*PasskeyStepConfig)(nil), // 1: workflow.plugins.auth.v1.PasskeyStepConfig @@ -4778,28 +5816,50 @@ var file_internal_contracts_auth_proto_goTypes = []any{ (*AuthPolicyGateInput)(nil), // 40: workflow.plugins.auth.v1.AuthPolicyGateInput (*AuthMethodsResponseOutput)(nil), // 41: workflow.plugins.auth.v1.AuthMethodsResponseOutput (*AuthPolicyAuditOutput)(nil), // 42: workflow.plugins.auth.v1.AuthPolicyAuditOutput - (*OAuthProviderConfig)(nil), // 43: workflow.plugins.auth.v1.OAuthProviderConfig - (*OAuthProviderInput)(nil), // 44: workflow.plugins.auth.v1.OAuthProviderInput - (*OAuthProviderConfigOutput)(nil), // 45: workflow.plugins.auth.v1.OAuthProviderConfigOutput - (*OAuthStartOutput)(nil), // 46: workflow.plugins.auth.v1.OAuthStartOutput - (*OAuthExchangeOutput)(nil), // 47: workflow.plugins.auth.v1.OAuthExchangeOutput - (*OAuthUserinfoOutput)(nil), // 48: workflow.plugins.auth.v1.OAuthUserinfoOutput - (*CredentialListInput)(nil), // 49: workflow.plugins.auth.v1.CredentialListInput - (*CredentialSummary)(nil), // 50: workflow.plugins.auth.v1.CredentialSummary - (*CredentialListOutput)(nil), // 51: workflow.plugins.auth.v1.CredentialListOutput - (*CredentialRevokeInput)(nil), // 52: workflow.plugins.auth.v1.CredentialRevokeInput - (*CredentialRevokeOutput)(nil), // 53: workflow.plugins.auth.v1.CredentialRevokeOutput - (*structpb.Struct)(nil), // 54: google.protobuf.Struct + (*AuthAdminConfig)(nil), // 43: workflow.plugins.auth.v1.AuthAdminConfig + (*AuthAdminDescribeInput)(nil), // 44: workflow.plugins.auth.v1.AuthAdminDescribeInput + (*AuthAdminControlOption)(nil), // 45: workflow.plugins.auth.v1.AuthAdminControlOption + (*AuthAdminControl)(nil), // 46: workflow.plugins.auth.v1.AuthAdminControl + (*AuthAdminControlGroup)(nil), // 47: workflow.plugins.auth.v1.AuthAdminControlGroup + (*AuthAdminDiagnostic)(nil), // 48: workflow.plugins.auth.v1.AuthAdminDiagnostic + (*AuthAdminDescribeOutput)(nil), // 49: workflow.plugins.auth.v1.AuthAdminDescribeOutput + (*AuthAdminValidateConfig)(nil), // 50: workflow.plugins.auth.v1.AuthAdminValidateConfig + (*AuthAdminValidateInput)(nil), // 51: workflow.plugins.auth.v1.AuthAdminValidateInput + (*AuthAdminValidateOutput)(nil), // 52: workflow.plugins.auth.v1.AuthAdminValidateOutput + (*OAuthProviderConfig)(nil), // 53: workflow.plugins.auth.v1.OAuthProviderConfig + (*OAuthProviderInput)(nil), // 54: workflow.plugins.auth.v1.OAuthProviderInput + (*OAuthProviderConfigOutput)(nil), // 55: workflow.plugins.auth.v1.OAuthProviderConfigOutput + (*OAuthStartOutput)(nil), // 56: workflow.plugins.auth.v1.OAuthStartOutput + (*OAuthExchangeOutput)(nil), // 57: workflow.plugins.auth.v1.OAuthExchangeOutput + (*OAuthUserinfoOutput)(nil), // 58: workflow.plugins.auth.v1.OAuthUserinfoOutput + (*CredentialListInput)(nil), // 59: workflow.plugins.auth.v1.CredentialListInput + (*CredentialSummary)(nil), // 60: workflow.plugins.auth.v1.CredentialSummary + (*CredentialListOutput)(nil), // 61: workflow.plugins.auth.v1.CredentialListOutput + (*CredentialRevokeInput)(nil), // 62: workflow.plugins.auth.v1.CredentialRevokeInput + (*CredentialRevokeOutput)(nil), // 63: workflow.plugins.auth.v1.CredentialRevokeOutput + (*structpb.Struct)(nil), // 64: google.protobuf.Struct } var file_internal_contracts_auth_proto_depIdxs = []int32{ - 54, // 0: workflow.plugins.auth.v1.OAuthExchangeOutput.raw_tokens:type_name -> google.protobuf.Struct - 54, // 1: workflow.plugins.auth.v1.OAuthUserinfoOutput.raw_claims:type_name -> google.protobuf.Struct - 50, // 2: workflow.plugins.auth.v1.CredentialListOutput.credentials:type_name -> workflow.plugins.auth.v1.CredentialSummary - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 43, // 0: workflow.plugins.auth.v1.AuthAdminDescribeInput.config:type_name -> workflow.plugins.auth.v1.AuthAdminConfig + 45, // 1: workflow.plugins.auth.v1.AuthAdminControl.options:type_name -> workflow.plugins.auth.v1.AuthAdminControlOption + 46, // 2: workflow.plugins.auth.v1.AuthAdminControlGroup.controls:type_name -> workflow.plugins.auth.v1.AuthAdminControl + 47, // 3: workflow.plugins.auth.v1.AuthAdminDescribeOutput.groups:type_name -> workflow.plugins.auth.v1.AuthAdminControlGroup + 64, // 4: workflow.plugins.auth.v1.AuthAdminDescribeOutput.effective_config:type_name -> google.protobuf.Struct + 64, // 5: workflow.plugins.auth.v1.AuthAdminDescribeOutput.methods_policy:type_name -> google.protobuf.Struct + 48, // 6: workflow.plugins.auth.v1.AuthAdminDescribeOutput.warnings:type_name -> workflow.plugins.auth.v1.AuthAdminDiagnostic + 64, // 7: workflow.plugins.auth.v1.AuthAdminValidateInput.desired_config:type_name -> google.protobuf.Struct + 64, // 8: workflow.plugins.auth.v1.AuthAdminValidateOutput.accepted_config:type_name -> google.protobuf.Struct + 64, // 9: workflow.plugins.auth.v1.AuthAdminValidateOutput.methods_policy:type_name -> google.protobuf.Struct + 48, // 10: workflow.plugins.auth.v1.AuthAdminValidateOutput.errors:type_name -> workflow.plugins.auth.v1.AuthAdminDiagnostic + 48, // 11: workflow.plugins.auth.v1.AuthAdminValidateOutput.warnings:type_name -> workflow.plugins.auth.v1.AuthAdminDiagnostic + 64, // 12: workflow.plugins.auth.v1.OAuthExchangeOutput.raw_tokens:type_name -> google.protobuf.Struct + 64, // 13: workflow.plugins.auth.v1.OAuthUserinfoOutput.raw_claims:type_name -> google.protobuf.Struct + 60, // 14: workflow.plugins.auth.v1.CredentialListOutput.credentials:type_name -> workflow.plugins.auth.v1.CredentialSummary + 15, // [15:15] is the sub-list for method output_type + 15, // [15:15] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_internal_contracts_auth_proto_init() } @@ -4810,14 +5870,17 @@ func file_internal_contracts_auth_proto_init() { file_internal_contracts_auth_proto_msgTypes[34].OneofWrappers = []any{} file_internal_contracts_auth_proto_msgTypes[35].OneofWrappers = []any{} file_internal_contracts_auth_proto_msgTypes[43].OneofWrappers = []any{} - file_internal_contracts_auth_proto_msgTypes[44].OneofWrappers = []any{} + file_internal_contracts_auth_proto_msgTypes[50].OneofWrappers = []any{} + file_internal_contracts_auth_proto_msgTypes[51].OneofWrappers = []any{} + file_internal_contracts_auth_proto_msgTypes[53].OneofWrappers = []any{} + file_internal_contracts_auth_proto_msgTypes[54].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_contracts_auth_proto_rawDesc), len(file_internal_contracts_auth_proto_rawDesc)), NumEnums: 0, - NumMessages: 54, + NumMessages: 64, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/contracts/auth.proto b/internal/contracts/auth.proto index 2ee1814..8741685 100644 --- a/internal/contracts/auth.proto +++ b/internal/contracts/auth.proto @@ -352,6 +352,108 @@ message AuthPolicyAuditOutput { string error = 100; } +message AuthAdminConfig { + string environment = 1; + optional bool password_auth_enabled = 2; + optional bool password_enabled = 3; + optional bool totp_auth_enabled = 4; + optional bool totp_enabled = 5; + string webauthn_rp_id = 6; + string webauthn_origin = 7; + string smtp_host = 8; + string smtp_from = 9; + optional bool auth_routes_enabled = 10; + optional bool routes_enabled = 11; + optional bool oauth_routes_enabled = 12; + string google_oauth_client_id = 13; + string google_oauth_client_secret = 14; + string google_oauth_redirect_url = 15; + string oauth_provider = 16; + optional bool sms_enabled = 17; + string twilio_verify_service_sid = 18; + string twilio_account_sid = 19; + string twilio_auth_token = 20; + string twilio_api_key_sid = 21; + string twilio_api_key_secret = 22; + string jwt_secret = 23; + optional bool sms_auth_enabled = 24; + string facebook_oauth_client_id = 25; + string facebook_oauth_client_secret = 26; + string facebook_oauth_redirect_url = 27; + string instagram_oauth_client_id = 28; + string instagram_oauth_client_secret = 29; + string x_oauth_client_id = 30; + string x_oauth_client_secret = 31; + optional bool allow_insecure_test_oauth_endpoints = 32; +} + +message AuthAdminDescribeInput { + AuthAdminConfig config = 1; +} + +message AuthAdminControlOption { + string value = 1; + string label = 2; + string description = 3; +} + +message AuthAdminControl { + string key = 1; + string group_key = 2; + string label = 3; + string description = 4; + string help_text = 5; + string input_type = 6; + string config_key = 7; + bool secret = 8; + bool configured = 9; + bool required = 10; + bool enabled = 11; + string disabled_reason = 12; + repeated AuthAdminControlOption options = 13; +} + +message AuthAdminControlGroup { + string key = 1; + string label = 2; + string description = 3; + repeated AuthAdminControl controls = 4; +} + +message AuthAdminDiagnostic { + string field = 1; + string severity = 2; + string message = 3; +} + +message AuthAdminDescribeOutput { + repeated AuthAdminControlGroup groups = 1; + google.protobuf.Struct effective_config = 2; + google.protobuf.Struct methods_policy = 3; + repeated AuthAdminDiagnostic warnings = 4; + repeated string secret_fields = 5; + string error = 100; +} + +message AuthAdminValidateConfig { + optional bool require_primary_method = 1; +} + +message AuthAdminValidateInput { + google.protobuf.Struct desired_config = 1; + optional bool require_primary_method = 2; +} + +message AuthAdminValidateOutput { + bool valid = 1; + google.protobuf.Struct accepted_config = 2; + google.protobuf.Struct methods_policy = 3; + repeated AuthAdminDiagnostic errors = 4; + repeated AuthAdminDiagnostic warnings = 5; + repeated string secret_fields = 6; + string error = 100; +} + message OAuthProviderConfig { string provider = 1; string google_oauth_client_id = 2; diff --git a/internal/plugin.go b/internal/plugin.go index 68b76fb..960dda9 100644 --- a/internal/plugin.go +++ b/internal/plugin.go @@ -36,6 +36,8 @@ var allStepTypes = []string{ "step.auth_policy_gate", "step.auth_methods_response", "step.auth_policy_audit", + "step.auth_admin_config_describe", + "step.auth_admin_config_validate", "step.auth_oauth_provider_config", "step.auth_oauth_start", "step.auth_oauth_exchange", @@ -138,6 +140,10 @@ func (p *authPlugin) CreateStep(typeName, name string, config map[string]any) (s return newAuthMethodsResponseStep(name, config), nil case "step.auth_policy_audit": return newAuthPolicyAuditStep(name, config), nil + case "step.auth_admin_config_describe": + return newAuthAdminConfigDescribeStep(name, config), nil + case "step.auth_admin_config_validate": + return newAuthAdminConfigValidateStep(name, config), nil case "step.auth_oauth_provider_config": return newOAuthProviderConfigStep(name, config), nil case "step.auth_oauth_start": @@ -229,6 +235,14 @@ func (p *authPlugin) CreateTypedStep(typeName, name string, config *anypb.Any) ( return sdk.NewTypedStepFactory(typeName, &contracts.AuthMethodsPolicyConfig{}, &contracts.AuthMethodsPolicyInput{}, typedLegacyStep[*contracts.AuthMethodsPolicyConfig, *contracts.AuthMethodsPolicyInput, *contracts.AuthPolicyAuditOutput](func(name string, config map[string]any) sdk.StepInstance { return newAuthPolicyAuditStep(name, config) }, &contracts.AuthPolicyAuditOutput{})).CreateTypedStep(typeName, name, config) + case "step.auth_admin_config_describe": + return sdk.NewTypedStepFactory(typeName, &contracts.EmptyConfig{}, &contracts.AuthAdminDescribeInput{}, typedLegacyStep[*contracts.EmptyConfig, *contracts.AuthAdminDescribeInput, *contracts.AuthAdminDescribeOutput](func(name string, config map[string]any) sdk.StepInstance { + return newAuthAdminConfigDescribeStep(name, config) + }, &contracts.AuthAdminDescribeOutput{})).CreateTypedStep(typeName, name, config) + case "step.auth_admin_config_validate": + return sdk.NewTypedStepFactory(typeName, &contracts.AuthAdminValidateConfig{}, &contracts.AuthAdminValidateInput{}, typedLegacyStep[*contracts.AuthAdminValidateConfig, *contracts.AuthAdminValidateInput, *contracts.AuthAdminValidateOutput](func(name string, config map[string]any) sdk.StepInstance { + return newAuthAdminConfigValidateStep(name, config) + }, &contracts.AuthAdminValidateOutput{})).CreateTypedStep(typeName, name, config) case "step.auth_oauth_provider_config": return sdk.NewTypedStepFactory(typeName, &contracts.OAuthProviderConfig{}, &contracts.OAuthProviderInput{}, typedLegacyStep[*contracts.OAuthProviderConfig, *contracts.OAuthProviderInput, *contracts.OAuthProviderConfigOutput](func(name string, config map[string]any) sdk.StepInstance { return newOAuthProviderConfigStep(name, config) @@ -290,6 +304,8 @@ var authContractRegistry = &pb.ContractRegistry{ stepContract("step.auth_policy_gate", "AuthPolicyGateConfig", "AuthPolicyGateInput", "AuthMethodsPolicyOutput"), stepContract("step.auth_methods_response", "EmptyConfig", "AuthMethodsPolicyOutput", "AuthMethodsResponseOutput"), stepContract("step.auth_policy_audit", "AuthMethodsPolicyConfig", "AuthMethodsPolicyInput", "AuthPolicyAuditOutput"), + stepContract("step.auth_admin_config_describe", "EmptyConfig", "AuthAdminDescribeInput", "AuthAdminDescribeOutput"), + stepContract("step.auth_admin_config_validate", "AuthAdminValidateConfig", "AuthAdminValidateInput", "AuthAdminValidateOutput"), stepContract("step.auth_oauth_provider_config", "OAuthProviderConfig", "OAuthProviderInput", "OAuthProviderConfigOutput"), stepContract("step.auth_oauth_start", "OAuthProviderConfig", "OAuthProviderInput", "OAuthStartOutput"), stepContract("step.auth_oauth_exchange", "OAuthProviderConfig", "OAuthProviderInput", "OAuthExchangeOutput"), diff --git a/internal/plugin_contracts_test.go b/internal/plugin_contracts_test.go index e4ca323..5191a33 100644 --- a/internal/plugin_contracts_test.go +++ b/internal/plugin_contracts_test.go @@ -55,6 +55,8 @@ func TestContractRegistryDeclaresStrictContracts(t *testing.T) { requireContract(t, runtimeContracts, "module:auth.credential", "workflow.plugins.auth.v1.CredentialModuleConfig", "", "") requireContract(t, runtimeContracts, "step:step.auth_challenge_generate", "workflow.plugins.auth.v1.AuthChallengeGenerateConfig", "workflow.plugins.auth.v1.ChallengeGenerateInput", "workflow.plugins.auth.v1.ChallengeGenerateOutput") requireContract(t, runtimeContracts, "step:step.auth_methods_policy", "workflow.plugins.auth.v1.AuthMethodsPolicyConfig", "workflow.plugins.auth.v1.AuthMethodsPolicyInput", "workflow.plugins.auth.v1.AuthMethodsPolicyOutput") + requireContract(t, runtimeContracts, "step:step.auth_admin_config_describe", "workflow.plugins.auth.v1.EmptyConfig", "workflow.plugins.auth.v1.AuthAdminDescribeInput", "workflow.plugins.auth.v1.AuthAdminDescribeOutput") + requireContract(t, runtimeContracts, "step:step.auth_admin_config_validate", "workflow.plugins.auth.v1.AuthAdminValidateConfig", "workflow.plugins.auth.v1.AuthAdminValidateInput", "workflow.plugins.auth.v1.AuthAdminValidateOutput") } func TestCreateTypedModuleRejectsWrongConfigType(t *testing.T) { diff --git a/internal/step_admin_config.go b/internal/step_admin_config.go new file mode 100644 index 0000000..779ab95 --- /dev/null +++ b/internal/step_admin_config.go @@ -0,0 +1,335 @@ +package internal + +import ( + "context" + "net/url" + "sort" + "strings" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +type authAdminConfigDescribeStep struct { + name string + config map[string]any +} + +func newAuthAdminConfigDescribeStep(name string, config map[string]any) *authAdminConfigDescribeStep { + return &authAdminConfigDescribeStep{name: name, config: config} +} + +func (s *authAdminConfigDescribeStep) Execute(_ context.Context, _ map[string]any, _ map[string]map[string]any, current, _, runtimeConfig map[string]any) (*sdk.StepResult, error) { + source := mergePolicyInputs(s.config, runtimeConfig, authAdminNestedConfig(current, "config"), current) + policy := buildAuthMethodsPolicy(source) + + return &sdk.StepResult{Output: map[string]any{ + "groups": buildAuthAdminGroups(source), + "effective_config": sanitizeAuthAdminConfig(source), + "methods_policy": policy, + "warnings": authAdminWarnings(source, policy), + "secret_fields": authAdminSecretFields(source), + }}, nil +} + +type authAdminConfigValidateStep struct { + name string + config map[string]any +} + +func newAuthAdminConfigValidateStep(name string, config map[string]any) *authAdminConfigValidateStep { + return &authAdminConfigValidateStep{name: name, config: config} +} + +func (s *authAdminConfigValidateStep) Execute(_ context.Context, _ map[string]any, _ map[string]map[string]any, current, _, runtimeConfig map[string]any) (*sdk.StepResult, error) { + desired := authAdminNestedConfig(current, "desired_config") + source := mergePolicyInputs(s.config, runtimeConfig, desired) + policy := buildAuthMethodsPolicy(source) + + var errors []map[string]any + var warnings []map[string]any + + if isProduction(policyString(source, "environment")) && + (policyStrictTrue(source, "password_auth_enabled") || policyStrictTrue(source, "password_enabled")) { + errors = append(errors, authAdminDiagnostic("password_auth_enabled", "error", "password auth cannot be enabled in production")) + } + + if requirePrimaryMethod(s.config, current) && policyInt(policy, "primary_method_count") == 0 { + errors = append(errors, authAdminDiagnostic("primary_methods", "error", "at least one primary authentication method must be configured")) + } + + errors = append(errors, validatePasskeyAdminConfig(source)...) + errors = append(errors, validateOAuthAdminConfig(source)...) + warnings = append(warnings, authAdminWarnings(source, policy)...) + + return &sdk.StepResult{Output: map[string]any{ + "valid": len(errors) == 0, + "accepted_config": sanitizeAuthAdminConfig(desired), + "methods_policy": policy, + "errors": errors, + "warnings": warnings, + "secret_fields": authAdminSecretFields(desired), + }}, nil +} + +func buildAuthAdminGroups(source map[string]any) []map[string]any { + groups := []map[string]any{ + { + "key": "primary_methods", + "label": "Primary methods", + "description": "Login methods that can establish a user session.", + "controls": []map[string]any{ + authAdminControl(source, "webauthn_rp_id", "Passkey relying party ID", "text", "Domain used by browsers to scope passkey credentials.", "Use the effective application host, for example app.example.com.", true, ""), + authAdminControl(source, "webauthn_origin", "Passkey origin", "url", "Origin that WebAuthn challenges must be created for.", "Use the full HTTPS origin, for example https://app.example.com.", true, ""), + authAdminControl(source, "password_auth_enabled", "Password login", "toggle", "Allows users to sign in with a password outside production.", "Production policy blocks password login even when this is enabled.", false, passwordAdminDisabledReason(source)), + }, + }, + { + "key": "delivery_methods", + "label": "Delivery methods", + "description": "Email and SMS configuration used by passwordless login challenges.", + "controls": []map[string]any{ + authAdminControl(source, "smtp_host", "SMTP host", "text", "SMTP server used for email codes and magic links.", "Set with smtp_from to enable email-code login.", false, ""), + authAdminControl(source, "smtp_from", "SMTP sender", "text", "From address used for auth emails.", "Use a verified sender address from the configured mail provider.", false, ""), + authAdminControl(source, "sms_auth_enabled", "SMS login", "toggle", "Allows SMS verification challenges when Twilio is configured.", "Requires auth routes, Twilio Verify service SID, and Twilio credentials.", false, ""), + authAdminControl(source, "twilio_verify_service_sid", "Twilio Verify service SID", "text", "Verify service that sends SMS challenges.", "Use a VA-prefixed Twilio Verify service SID.", false, ""), + authAdminControl(source, "twilio_account_sid", "Twilio account SID", "text", "Twilio account used with an auth token.", "Required when using twilio_auth_token instead of API key credentials.", false, ""), + authAdminControl(source, "twilio_auth_token", "Twilio auth token", "secret", "Secret token for Twilio account authentication.", "Write-only. Leave blank to keep an existing configured value.", false, ""), + authAdminControl(source, "twilio_api_key_sid", "Twilio API key SID", "text", "Twilio API key identifier.", "Use with twilio_api_key_secret as the preferred SMS credential pair.", false, ""), + authAdminControl(source, "twilio_api_key_secret", "Twilio API key secret", "secret", "Secret paired with the Twilio API key SID.", "Write-only. Leave blank to keep an existing configured value.", false, ""), + authAdminControl(source, "jwt_secret", "Challenge signing secret", "secret", "Secret used to sign email and challenge tokens.", "Write-only. Required by policy gates for email-code login.", false, ""), + }, + }, + { + "key": "second_factors", + "label": "Second factors", + "description": "Additional verification methods used after primary login.", + "controls": []map[string]any{ + authAdminControl(source, "totp_auth_enabled", "Authenticator app codes", "toggle", "Enables TOTP enrollment and verification steps.", "Use recovery codes alongside TOTP for account recovery.", false, ""), + }, + }, + { + "key": "oauth_providers", + "label": "OAuth providers", + "description": "External identity providers available to auth routes.", + "controls": buildOAuthAdminControls(source), + }, + } + for _, group := range groups { + groupKey, _ := group["key"].(string) + controls, _ := group["controls"].([]map[string]any) + for _, control := range controls { + control["group_key"] = groupKey + } + } + return groups +} + +func buildOAuthAdminControls(source map[string]any) []map[string]any { + controls := []map[string]any{ + authAdminControl(source, "auth_routes_enabled", "Auth routes", "toggle", "Enables HTTP auth routes used by OAuth callback flows.", "OAuth login requires auth routes before any provider can become login-ready.", false, ""), + } + for _, provider := range []struct { + key string + label string + }{ + {"google", "Google"}, + {"facebook", "Facebook"}, + {"instagram", "Instagram"}, + {"x", "X"}, + } { + disabledReason := providerDisabledReason(source, provider.key) + controls = append(controls, + authAdminControl(source, provider.key+"_oauth_client_id", provider.label+" client ID", "text", "OAuth client identifier issued by "+provider.label+".", "Pair with the matching client secret and redirect URL.", false, disabledReason), + authAdminControl(source, provider.key+"_oauth_client_secret", provider.label+" client secret", "secret", "OAuth client secret issued by "+provider.label+".", "Write-only. Leave blank to keep an existing configured value.", false, disabledReason), + ) + if provider.key == "google" || provider.key == "facebook" { + controls = append(controls, authAdminControl(source, provider.key+"_oauth_redirect_url", provider.label+" redirect URL", "url", "Callback URL registered with "+provider.label+".", "Must be HTTPS and match the provider application settings.", false, disabledReason)) + } + } + return controls +} + +func authAdminControl(source map[string]any, key, label, inputType, description, helpText string, required bool, disabledReason string) map[string]any { + return map[string]any{ + "key": key, + "group_key": "", + "label": label, + "description": description, + "help_text": helpText, + "input_type": inputType, + "config_key": key, + "secret": authAdminSecretKey(key), + "configured": policyPresent(source, key), + "required": required, + "enabled": disabledReason == "", + "disabled_reason": disabledReason, + "options": []map[string]any{}, + } +} + +func authAdminWarnings(source, policy map[string]any) []map[string]any { + var warnings []map[string]any + if isProduction(policyString(source, "environment")) && policyInt(policy, "primary_method_count") == 0 { + warnings = append(warnings, authAdminDiagnostic("primary_methods", "warning", "production has no enabled primary authentication methods")) + } + if policyStrictTrue(source, "password_auth_enabled") && !policyBool(policy, "password_enabled") { + warnings = append(warnings, authAdminDiagnostic("password_auth_enabled", "warning", "password login was requested but is not available in this environment")) + } + return warnings +} + +func validatePasskeyAdminConfig(source map[string]any) []map[string]any { + var errors []map[string]any + rpID := policyString(source, "webauthn_rp_id") + origin := policyString(source, "webauthn_origin") + if rpID == "" && origin == "" { + return errors + } + if rpID == "" { + errors = append(errors, authAdminDiagnostic("webauthn_rp_id", "error", "passkey login requires a relying party ID")) + } + if origin == "" { + errors = append(errors, authAdminDiagnostic("webauthn_origin", "error", "passkey login requires an origin")) + } else if !authAdminSecureOrigin(origin) { + errors = append(errors, authAdminDiagnostic("webauthn_origin", "error", "passkey origin must use https except for localhost development")) + } + return errors +} + +func validateOAuthAdminConfig(source map[string]any) []map[string]any { + var errors []map[string]any + for _, provider := range []string{"google", "facebook", "instagram", "x"} { + if !authAdminOAuthProviderRequested(source, provider) { + continue + } + if disabled := providerDisabledReason(source, provider); disabled != "" { + errors = append(errors, authAdminDiagnostic(provider+"_oauth", "error", disabled)) + continue + } + if !policyAnyStrictTrue(source, "auth_routes_enabled", "routes_enabled", "oauth_routes_enabled") { + errors = append(errors, authAdminDiagnostic("auth_routes_enabled", "error", provider+" oauth requires auth routes to be enabled")) + } + for _, key := range []string{provider + "_oauth_client_id", provider + "_oauth_client_secret", provider + "_oauth_redirect_url"} { + if !policyPresent(source, key) { + errors = append(errors, authAdminDiagnostic(key, "error", provider+" oauth requires "+key)) + } + } + if _, disabled := oauthProviderConfig(source, provider); disabled != "" { + errors = append(errors, authAdminDiagnostic(provider+"_oauth", "error", disabled)) + } + } + return errors +} + +func authAdminOAuthProviderRequested(source map[string]any, provider string) bool { + if normalizeOAuthProvider(policyString(source, "oauth_provider")) == provider { + return true + } + clientID := policyPresent(source, provider+"_oauth_client_id") + clientSecret := policyPresent(source, provider+"_oauth_client_secret") + redirect := policyPresent(source, provider+"_oauth_redirect_url") + return clientID || (clientSecret && redirect) || (clientID && clientSecret) +} + +func providerDisabledReason(source map[string]any, provider string) string { + if provider == "instagram" || provider == "x" { + _, reason := oauthProviderConfig(source, provider) + return reason + } + return "" +} + +func passwordAdminDisabledReason(source map[string]any) string { + if isProduction(policyString(source, "environment")) { + return "password auth cannot be enabled in production" + } + return "" +} + +func authAdminNestedConfig(source map[string]any, key string) map[string]any { + if source == nil { + return nil + } + value, ok := source[key] + if !ok || value == nil { + return nil + } + switch typed := value.(type) { + case map[string]any: + return typed + case map[string]string: + converted := make(map[string]any, len(typed)) + for key, value := range typed { + converted[key] = value + } + return converted + default: + return nil + } +} + +func sanitizeAuthAdminConfig(source map[string]any) map[string]any { + sanitized := make(map[string]any, len(source)) + for key, value := range source { + if authAdminSecretKey(key) { + continue + } + if key == "desired_config" || key == "config" || key == "require_primary_method" { + continue + } + sanitized[key] = value + } + return sanitized +} + +func authAdminSecretFields(source map[string]any) []string { + fields := make([]string, 0) + for key := range source { + if authAdminSecretKey(key) && policyPresent(source, key) { + fields = append(fields, key) + } + } + sort.Strings(fields) + return fields +} + +func authAdminSecretKey(key string) bool { + key = strings.ToLower(strings.TrimSpace(key)) + return strings.Contains(key, "secret") || + key == "twilio_auth_token" || + key == "smtp_pass" || + key == "jwt_secret" +} + +func requirePrimaryMethod(config, current map[string]any) bool { + if policyStrictTrue(current, "require_primary_method") { + return true + } + if value, ok := current["require_primary_method"]; ok && value == false { + return false + } + return policyStrictTrue(config, "require_primary_method") +} + +func authAdminDiagnostic(field, severity, message string) map[string]any { + return map[string]any{ + "field": field, + "severity": severity, + "message": message, + } +} + +func authAdminSecureOrigin(origin string) bool { + parsed, err := url.Parse(strings.TrimSpace(origin)) + if err != nil || parsed.Host == "" { + return false + } + if parsed.Scheme == "https" { + return true + } + if parsed.Scheme != "http" { + return false + } + host := parsed.Hostname() + return host == "localhost" || host == "127.0.0.1" || host == "::1" +} diff --git a/internal/step_admin_config_test.go b/internal/step_admin_config_test.go new file mode 100644 index 0000000..67e5207 --- /dev/null +++ b/internal/step_admin_config_test.go @@ -0,0 +1,286 @@ +package internal + +import ( + "context" + "slices" + "strings" + "testing" +) + +func TestAuthAdminConfigDescribeExposesRealConfigControls(t *testing.T) { + step := newAuthAdminConfigDescribeStep("admin", map[string]any{ + "environment": "development", + "password_auth_enabled": true, + "webauthn_rp_id": "app.example.test", + "webauthn_origin": "https://app.example.test", + "auth_routes_enabled": true, + "google_oauth_client_id": "google-client", + "google_oauth_client_secret": "google-secret", + "google_oauth_redirect_url": "https://app.example.test/auth/google/callback", + "facebook_oauth_client_id": "facebook-client", + "facebook_oauth_client_secret": "facebook-secret", + "facebook_oauth_redirect_url": "https://app.example.test/auth/facebook/callback", + }) + + result, err := step.Execute(context.Background(), nil, nil, nil, nil, nil) + if err != nil { + t.Fatalf("describe admin config: %v", err) + } + + requireAdminControl(t, result.Output, "webauthn_rp_id", adminControlWant{ + GroupKey: "primary_methods", + Label: "Passkey relying party ID", + InputType: "text", + ConfigKey: "webauthn_rp_id", + Secret: false, + Configured: true, + Enabled: true, + }) + requireAdminControl(t, result.Output, "webauthn_origin", adminControlWant{ + GroupKey: "primary_methods", + Label: "Passkey origin", + InputType: "url", + ConfigKey: "webauthn_origin", + Secret: false, + Configured: true, + Enabled: true, + }) + requireAdminControl(t, result.Output, "password_auth_enabled", adminControlWant{ + GroupKey: "primary_methods", + Label: "Password login", + InputType: "toggle", + ConfigKey: "password_auth_enabled", + Secret: false, + Configured: true, + Enabled: true, + }) + requireAdminControl(t, result.Output, "google_oauth_client_secret", adminControlWant{ + GroupKey: "oauth_providers", + Label: "Google client secret", + InputType: "secret", + ConfigKey: "google_oauth_client_secret", + Secret: true, + Configured: true, + Enabled: true, + }) + requireAdminControl(t, result.Output, "facebook_oauth_client_secret", adminControlWant{ + GroupKey: "oauth_providers", + Label: "Facebook client secret", + InputType: "secret", + ConfigKey: "facebook_oauth_client_secret", + Secret: true, + Configured: true, + Enabled: true, + }) + requireAdminControl(t, result.Output, "instagram_oauth_client_secret", adminControlWant{ + GroupKey: "oauth_providers", + Label: "Instagram client secret", + InputType: "secret", + ConfigKey: "instagram_oauth_client_secret", + Secret: true, + Enabled: false, + DisabledReason: "instagram oauth provider is disabled in this release", + AllowUnconfigured: true, + }) + requireAdminControl(t, result.Output, "x_oauth_client_secret", adminControlWant{ + GroupKey: "oauth_providers", + Label: "X client secret", + InputType: "secret", + ConfigKey: "x_oauth_client_secret", + Secret: true, + Enabled: false, + DisabledReason: "x oauth provider is disabled in this release", + AllowUnconfigured: true, + }) + + policy, ok := result.Output["methods_policy"].(map[string]any) + if !ok { + t.Fatalf("methods_policy has type %T, want map[string]any", result.Output["methods_policy"]) + } + assertBool(t, policy, "passkey_enabled", true) + assertBool(t, policy, "password_enabled", true) + + secretFields, ok := result.Output["secret_fields"].([]string) + if !ok { + t.Fatalf("secret_fields has type %T, want []string", result.Output["secret_fields"]) + } + for _, secret := range []string{"google_oauth_client_secret", "facebook_oauth_client_secret"} { + if !slices.Contains(secretFields, secret) { + t.Fatalf("secret_fields missing %q: %v", secret, secretFields) + } + } +} + +func TestAuthAdminConfigValidateRejectsUnsafePasswordProduction(t *testing.T) { + step := newAuthAdminConfigValidateStep("admin", nil) + + result, err := step.Execute(context.Background(), nil, nil, map[string]any{ + "require_primary_method": true, + "desired_config": map[string]any{ + "environment": "production", + "password_auth_enabled": true, + }, + }, nil, nil) + if err != nil { + t.Fatalf("validate admin config: %v", err) + } + + assertBool(t, result.Output, "valid", false) + requireAdminDiagnostic(t, result.Output, "password_auth_enabled", "password auth cannot be enabled in production") +} + +func TestAuthAdminConfigValidateRejectsZeroPrimaryMethods(t *testing.T) { + step := newAuthAdminConfigValidateStep("admin", nil) + + result, err := step.Execute(context.Background(), nil, nil, map[string]any{ + "require_primary_method": true, + "desired_config": map[string]any{ + "environment": "development", + "password_auth_enabled": false, + "webauthn_rp_id": "", + "webauthn_origin": "", + }, + }, nil, nil) + if err != nil { + t.Fatalf("validate admin config: %v", err) + } + + assertBool(t, result.Output, "valid", false) + requireAdminDiagnostic(t, result.Output, "primary_methods", "at least one primary authentication method must be configured") +} + +func TestAuthAdminConfigValidateAcceptsPasskeyPatchAndRedactsSecrets(t *testing.T) { + step := newAuthAdminConfigValidateStep("admin", nil) + + result, err := step.Execute(context.Background(), nil, nil, map[string]any{ + "require_primary_method": true, + "desired_config": map[string]any{ + "environment": "development", + "webauthn_rp_id": "app.example.test", + "webauthn_origin": "https://app.example.test", + "auth_routes_enabled": true, + "google_oauth_client_id": "google-client", + "google_oauth_client_secret": "google-secret", + "google_oauth_redirect_url": "https://app.example.test/auth/google/callback", + "facebook_oauth_client_secret": "facebook-secret", + }, + }, nil, nil) + if err != nil { + t.Fatalf("validate admin config: %v", err) + } + + assertBool(t, result.Output, "valid", true) + accepted, ok := result.Output["accepted_config"].(map[string]any) + if !ok { + t.Fatalf("accepted_config has type %T, want map[string]any", result.Output["accepted_config"]) + } + if accepted["webauthn_rp_id"] != "app.example.test" { + t.Fatalf("accepted webauthn_rp_id = %v", accepted["webauthn_rp_id"]) + } + if _, exists := accepted["google_oauth_client_secret"]; exists { + t.Fatal("accepted_config echoed google_oauth_client_secret") + } + if _, exists := accepted["facebook_oauth_client_secret"]; exists { + t.Fatal("accepted_config echoed facebook_oauth_client_secret") + } + secretFields, ok := result.Output["secret_fields"].([]string) + if !ok { + t.Fatalf("secret_fields has type %T, want []string", result.Output["secret_fields"]) + } + for _, secret := range []string{"google_oauth_client_secret", "facebook_oauth_client_secret"} { + if !slices.Contains(secretFields, secret) { + t.Fatalf("secret_fields missing %q: %v", secret, secretFields) + } + } + + policy, ok := result.Output["methods_policy"].(map[string]any) + if !ok { + t.Fatalf("methods_policy has type %T, want map[string]any", result.Output["methods_policy"]) + } + assertBool(t, policy, "passkey_enabled", true) +} + +type adminControlWant struct { + GroupKey string + Label string + InputType string + ConfigKey string + Secret bool + Configured bool + Enabled bool + DisabledReason string + AllowUnconfigured bool +} + +func requireAdminControl(t *testing.T, output map[string]any, key string, want adminControlWant) { + t.Helper() + control := findAdminControl(t, output, key) + if control["group_key"] != want.GroupKey { + t.Fatalf("%s group_key = %v, want %s", key, control["group_key"], want.GroupKey) + } + if control["label"] != want.Label { + t.Fatalf("%s label = %v, want %s", key, control["label"], want.Label) + } + if control["input_type"] != want.InputType { + t.Fatalf("%s input_type = %v, want %s", key, control["input_type"], want.InputType) + } + if control["config_key"] != want.ConfigKey { + t.Fatalf("%s config_key = %v, want %s", key, control["config_key"], want.ConfigKey) + } + if control["secret"] != want.Secret { + t.Fatalf("%s secret = %v, want %v", key, control["secret"], want.Secret) + } + if !want.AllowUnconfigured && control["configured"] != want.Configured { + t.Fatalf("%s configured = %v, want %v", key, control["configured"], want.Configured) + } + if control["enabled"] != want.Enabled { + t.Fatalf("%s enabled = %v, want %v", key, control["enabled"], want.Enabled) + } + if want.DisabledReason != "" { + got := control["disabled_reason"] + if got == nil || !strings.Contains(got.(string), want.DisabledReason) { + t.Fatalf("%s disabled_reason = %v, want contains %q", key, got, want.DisabledReason) + } + } + if strings.TrimSpace(control["help_text"].(string)) == "" { + t.Fatalf("%s help_text is empty", key) + } +} + +func findAdminControl(t *testing.T, output map[string]any, key string) map[string]any { + t.Helper() + groups, ok := output["groups"].([]map[string]any) + if !ok { + t.Fatalf("groups has type %T, want []map[string]any", output["groups"]) + } + for _, group := range groups { + controls, ok := group["controls"].([]map[string]any) + if !ok { + t.Fatalf("%s controls has type %T, want []map[string]any", group["key"], group["controls"]) + } + for _, control := range controls { + if control["key"] == key { + if control["group_key"] == nil { + control["group_key"] = group["key"] + } + return control + } + } + } + t.Fatalf("missing admin control %q", key) + return nil +} + +func requireAdminDiagnostic(t *testing.T, output map[string]any, field, contains string) { + t.Helper() + diagnostics, ok := output["errors"].([]map[string]any) + if !ok { + t.Fatalf("errors has type %T, want []map[string]any", output["errors"]) + } + for _, diagnostic := range diagnostics { + if diagnostic["field"] == field && strings.Contains(diagnostic["message"].(string), contains) { + return + } + } + t.Fatalf("missing diagnostic field=%q contains=%q in %v", field, contains, diagnostics) +} diff --git a/plugin.contracts.json b/plugin.contracts.json index be6f404..c50e14c 100644 --- a/plugin.contracts.json +++ b/plugin.contracts.json @@ -159,6 +159,22 @@ "input": "workflow.plugins.auth.v1.AuthMethodsPolicyInput", "output": "workflow.plugins.auth.v1.AuthPolicyAuditOutput" }, + { + "kind": "step", + "type": "step.auth_admin_config_describe", + "mode": "strict", + "config": "workflow.plugins.auth.v1.EmptyConfig", + "input": "workflow.plugins.auth.v1.AuthAdminDescribeInput", + "output": "workflow.plugins.auth.v1.AuthAdminDescribeOutput" + }, + { + "kind": "step", + "type": "step.auth_admin_config_validate", + "mode": "strict", + "config": "workflow.plugins.auth.v1.AuthAdminValidateConfig", + "input": "workflow.plugins.auth.v1.AuthAdminValidateInput", + "output": "workflow.plugins.auth.v1.AuthAdminValidateOutput" + }, { "kind": "step", "type": "step.auth_oauth_provider_config", diff --git a/plugin.json b/plugin.json index 395307d..283287a 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "workflow-plugin-auth", - "version": "0.2.7", + "version": "0.2.9", "description": "Passwordless authentication plugin: WebAuthn/passkeys, TOTP, email magic links", "author": "GoCodeAlone", "license": "MIT", @@ -22,22 +22,22 @@ { "os": "linux", "arch": "amd64", - "url": "https://github.com/GoCodeAlone/workflow-plugin-auth/releases/download/v0.2.7/workflow-plugin-auth_0.2.7_linux_amd64.tar.gz" + "url": "https://github.com/GoCodeAlone/workflow-plugin-auth/releases/download/v0.2.9/workflow-plugin-auth_0.2.9_linux_amd64.tar.gz" }, { "os": "linux", "arch": "arm64", - "url": "https://github.com/GoCodeAlone/workflow-plugin-auth/releases/download/v0.2.7/workflow-plugin-auth_0.2.7_linux_arm64.tar.gz" + "url": "https://github.com/GoCodeAlone/workflow-plugin-auth/releases/download/v0.2.9/workflow-plugin-auth_0.2.9_linux_arm64.tar.gz" }, { "os": "darwin", "arch": "amd64", - "url": "https://github.com/GoCodeAlone/workflow-plugin-auth/releases/download/v0.2.7/workflow-plugin-auth_0.2.7_darwin_amd64.tar.gz" + "url": "https://github.com/GoCodeAlone/workflow-plugin-auth/releases/download/v0.2.9/workflow-plugin-auth_0.2.9_darwin_amd64.tar.gz" }, { "os": "darwin", "arch": "arm64", - "url": "https://github.com/GoCodeAlone/workflow-plugin-auth/releases/download/v0.2.7/workflow-plugin-auth_0.2.7_darwin_arm64.tar.gz" + "url": "https://github.com/GoCodeAlone/workflow-plugin-auth/releases/download/v0.2.9/workflow-plugin-auth_0.2.9_darwin_arm64.tar.gz" } ], "moduleTypes": [ @@ -63,6 +63,8 @@ "step.auth_policy_gate", "step.auth_methods_response", "step.auth_policy_audit", + "step.auth_admin_config_describe", + "step.auth_admin_config_validate", "step.auth_oauth_provider_config", "step.auth_oauth_start", "step.auth_oauth_exchange", @@ -95,6 +97,8 @@ "step.auth_policy_gate", "step.auth_methods_response", "step.auth_policy_audit", + "step.auth_admin_config_describe", + "step.auth_admin_config_validate", "step.auth_oauth_provider_config", "step.auth_oauth_start", "step.auth_oauth_exchange",