diff --git a/README.md b/README.md index 0a1f1a6..88d14ae 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ 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_provider_catalog` - `step.auth_admin_config_describe` - `step.auth_admin_config_validate` - `step.auth_oauth_provider_config` @@ -110,12 +111,27 @@ 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. +`step.auth_provider_catalog` merges provider descriptors from auth-provider +plugins. Descriptors advertise provider categories, capabilities, required +config fields, selectable options, admin/app scopes, disabled reasons, and +secret field metadata. `step.auth_admin_config_describe` consumes these +descriptors so admin portals render provider controls dynamically instead of +hard-coding vendor-specific fields in the admin shell or auth plugin. When no +provider descriptors are supplied, the existing Google/Facebook OAuth controls +remain as a compatibility fallback. Provider capabilities are default-deny: +capabilities must set `supported: true` before auth admin or policy code treats +them as usable. + ## OAuth -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. +OAuth provider config supports Google and Facebook directly as compatibility +providers. Additional providers should be supplied by provider descriptors from +plugins such as SSO, Okta, Auth0, Entra, Ory, or another provider integration. +Policy advertising remains conservative and only marks providers login-ready +when the configured provider is supported by the current policy path and every +required descriptor field is configured. Instagram, X, and unknown providers +return disabled metadata and are not advertised as login-ready unless a provider +plugin supplies a real descriptor and implementation. `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 50caebd..6eed1cd 100644 --- a/SPEC.md +++ b/SPEC.md @@ -82,6 +82,7 @@ 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_provider_catalog` — merge provider descriptors from auth-provider plugins - `step.auth_admin_config_describe` — admin-renderable auth config controls - `step.auth_admin_config_validate` — validate and sanitize admin config patches @@ -108,6 +109,8 @@ Misc: - 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. +- V11: Provider-specific admin controls MUST be sourced from `AuthProviderDescriptor` values when descriptors are supplied; vendor-specific Google/Facebook controls are compatibility fallback only. +- V12: Provider descriptors MUST NOT advertise a capability as supported unless the owning provider plugin has a real runtime or management implementation and tests for it; missing `supported` is treated as false. ## §T — Tasks (status as of 2026-05-25) @@ -125,6 +128,7 @@ Misc: | 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 | +| T-AUTH-13 provider catalog contracts | ✅ | `step.auth_provider_catalog`, `AuthProviderDescriptor`, dynamic admin-provider controls | ## §X — References diff --git a/docs/plans/2026-05-27-auth-provider-architecture-design.md b/docs/plans/2026-05-27-auth-provider-architecture-design.md new file mode 100644 index 0000000..a3b9d74 --- /dev/null +++ b/docs/plans/2026-05-27-auth-provider-architecture-design.md @@ -0,0 +1,146 @@ +# Auth Provider Architecture Design + +## Goal +Make authentication providers plugin-first and dynamically discoverable so the admin portal can configure real auth methods, OIDC/SSO providers, identity-management providers, and enterprise SSO/SCIM providers without hard-coded vendor UI in `workflow-plugin-auth` or `workflow-plugin-admin`. + +## User Requirements +| id | requirement | design response | +|---|---|---| +| R1 | Fix the `go vet` issue first | Completed separately in `workflow-plugin-authz` v0.5.8; this design does not reopen it. | +| R2 | Remove hard-coded admin UI coupling | Completed in `workflow-plugin-admin` v1.1.6 for the shell; this design removes auth-provider hard-coding from auth admin contracts. | +| R3 | Support actual auth providers: Okta, Auth0, Entra, Ory Kratos, Ory Hydra, Ory Polis | Add strict provider descriptor contracts in auth core; update existing Okta/SSO plugins; add Auth0, Entra, Kratos, Hydra, and Polis provider plugins with SDK/API-backed implementations. | +| R4 | Distinguish auth categories clearly | Providers declare categories: `identity_management`, `authentication_method`, `oauth2_oidc`, `enterprise_sso`, `directory_sync`, `authorization_provider`, and `transport_auth`. | +| R5 | Do not support only Ory | Each category has non-Ory coverage: local passkey/passwordless auth, generic OIDC, Okta, Auth0, Entra, and Scalekit for enterprise SSO/SCIM where useful. | +| R6 | Admin forms should use lookups, not text entry, when data is known | Admin describe outputs include provider descriptors, capability descriptors, option lists, required config keys, secret flags, and validation endpoints. | +| R7 | Real plugin functionality, demo only proves it | Provider plugins own their contracts and SDK clients. `workflow-scenarios` only composes them and proves admin/runtime behavior. | +| R8 | Generate releases as updates are tested | Each PR group ends with local verification, merge, and patch release for changed plugin repos. | + +## Global Design Guidance +Source: workspace `AGENTS.md`, `README.md`, current plugin READMEs/plans. + +| guidance | response | +|---|---| +| Prefer Workflow plugin ownership boundaries | Core auth owns shared provider descriptors and auth-method policy; provider plugins own vendor SDKs/APIs; admin owns rendering/navigation only. | +| Avoid stubs, TODO-only implementations, and false functionality | A provider capability is advertised only when backed by code and tests. Unsupported SDK/API surfaces are omitted or explicitly marked unavailable with a reason. | +| Use strict proto contracts | Every cross-plugin descriptor and admin surface has protobuf messages and `plugin.contracts.json` entries. | +| Security and quality first | Secrets are write-only/redacted, provider clients use least-privilege scopes, OAuth/SSO callbacks require PKCE/state validation, and provider management actions are authz-gated by scopes. | +| Dogfood Workflow/wfctl | Scenario composes auth/admin/authz/provider plugins and is validated with `wfctl plugin validate-contract`, scenario tests, and browser QA. | +| Dirty worktrees are normal | Work happens in clean worktrees; existing root checkout dirt is not reverted. | + +## Provider Taxonomy +| category | purpose | Ory implementation | non-Ory implementation | +|---|---|---|---| +| `authentication_method` | Passkeys, passwords, TOTP, magic links, challenge delivery | Kratos for identity flows where available | `workflow-plugin-auth` local passkey/TOTP/magic-link steps | +| `identity_management` | Users, identities, credentials, recovery, profile lifecycle | Kratos via official `kratos-client-go` | Okta SDK, Auth0 Go SDK, Microsoft Graph SDK | +| `oauth2_oidc` | OAuth2/OIDC login, token validation, clients, consent/login server | Hydra via official `hydra-client-go` | `workflow-plugin-sso` using `go-oidc`/`oauth2`, Okta/Auth0/Entra OIDC descriptors | +| `enterprise_sso` | SAML/OIDC organization SSO, IdP discovery, B2B onboarding | Polis/Jackson API | Okta, Auth0 enterprise connections, Entra, Scalekit SDK | +| `directory_sync` | SCIM users/groups provisioning/deprovisioning | Polis/Jackson SCIM API | Microsoft Graph, Okta, Auth0, Scalekit SDK | +| `authorization_provider` | RBAC/ABAC/ReBAC evaluation and policy management | Keto already belongs to `workflow-plugin-authz` | Casbin, Permit.io, OpenFGA-style provider if added later | +| `transport_auth` | Protocol-level handshake identity | N/A | `workflow-plugin-ws-auth` HMAC challenge/identity | + +## Approaches Considered +| option | summary | trade-off | decision | +|---|---|---|---| +| A | Keep adding vendor fields to `workflow-plugin-auth` | Fast for one provider, but hard-codes UI/contract and grows a monolith. | Rejected; violates pluggability. | +| B | Provider plugins expose admin UI only | UI becomes pluggable, but auth core cannot validate provider choices or produce policy. | Rejected; auth policy still needs provider descriptors. | +| C | Shared provider descriptor contract + provider-owned SDK plugins | Core auth remains generic; provider repos declare capabilities; admin renders dynamic descriptors. | Chosen. | + +## Architecture +| component | responsibility | +|---|---| +| `workflow-plugin-auth` | Shared `AuthProviderDescriptor` proto, dynamic admin config describe/validate, auth method policy, built-in local auth descriptors. | +| `workflow-plugin-sso` | Generic OIDC provider runtime and descriptors for generic OIDC, Okta issuer helper, Entra issuer helper, and Auth0 issuer helper. | +| `workflow-plugin-okta` | Okta management-plane SDK client, provider descriptors, user/group/app/auth-server/admin surface contracts. | +| `workflow-plugin-auth0` | Auth0 SDK client, authentication/management descriptors, users/roles/connections/app client operations, OIDC descriptor export. | +| `workflow-plugin-entra` | Microsoft Graph SDK client using Azure/Kiota auth, users/groups/app registrations/auth-method policy descriptors, OIDC descriptor export. | +| `workflow-plugin-ory-kratos` | Kratos client for identity-management and auth-flow descriptors: identities, recovery, verification, passkeys/passwordless if SDK/API supports configured flow settings. | +| `workflow-plugin-ory-hydra` | Hydra client for OAuth2/OIDC server operations: OAuth2 clients, JWKS metadata, consent/login integration descriptors. | +| `workflow-plugin-ory-polis` | Polis/Jackson API client for enterprise SSO and SCIM where no official Go SDK exists; uses official REST/API shape and marks SDK provenance in capability metadata. | +| `workflow-plugin-scalekit` | Non-Ory enterprise SSO/SCIM provider using the official Scalekit Go SDK. | +| `workflow-plugin-admin` | Generic contribution shell only; renders provider/admin surfaces from registered contributions and descriptor JSON. | +| `workflow-scenarios` | Composes the app and admin portal, rotates provider configs, runs runtime/browser tests, and includes tailscale sidecar deployment manifests. | + +## Contract Shape +`workflow-plugin-auth` adds shared messages: + +| message | purpose | +|---|---| +| `AuthProviderDescriptor` | Provider ID, display name, categories, implementation package, version, docs URL, support level, management base path, auth base path, and capabilities. | +| `AuthProviderCapability` | Capability key, category, supported mode, required scopes, required config keys, secret keys, option lists, default authz scopes, and disabled reason. | +| `AuthProviderConfigField` | UI/admin metadata for a config key: label, description, input type, required, secret, selectable options, validation pattern, and source lookup. | +| `AuthProviderCatalogInput/Output` | Lets workflows merge descriptors from multiple provider plugin steps and pass them into auth admin describe/validate. | +| `AuthAdminConfig.providers` | Dynamic descriptor list consumed by admin describe/validate. Static vendor-specific OAuth controls become compatibility fallback only when no descriptors are supplied. | + +Provider plugins expose a `step._auth_provider_describe` step returning this descriptor shape. Provider-specific management steps remain in each provider repo and use their own strict proto messages. + +## Data Flow +1. App workflow configures provider modules: local auth, SSO OIDC, Okta/Auth0/Entra/Ory/Scalekit as needed. +2. On startup or admin page load, provider descriptor steps run and their outputs are merged by `step.auth_provider_catalog`. +3. Admin dashboard lists dynamic contributions from provider plugins and calls auth admin describe with the merged catalog. +4. Admin UI renders controls from descriptors. Known providers, capabilities, scopes, fields, and option values are selectable, not text-only. +5. Admin submits a config patch. Core auth validates generic auth-method safety; provider plugins validate provider-specific constraints. +6. Host/admin persists accepted patches into Workflow config or an approved config store. Provider plugins never echo secrets. +7. Runtime auth flows call provider runtime steps: local auth, SSO token validation, OAuth exchange/userinfo, or management actions. + +## Security Review +| risk | mitigation | +|---|---| +| Provider descriptor lies about capabilities | Runtime tests must exercise each advertised capability; capabilities include support level and disabled reason. | +| Secret disclosure | Secret fields are never returned; outputs include only `configured` and `secret_fields`. | +| Confused deputy between app/admin contexts | Capabilities declare default admin/app scopes separately; admin endpoints require admin scopes before invoking provider actions. | +| OAuth/SSO CSRF or code injection | PKCE/state/nonce validation remains required in app workflow; provider descriptors mark requirements; scenario tests callback abuse cases. | +| SSRF through endpoints | OIDC discovery and endpoint overrides require HTTPS and known-host validation unless explicit local-test flag is set. | +| Overprivileged SDK credentials | Provider configs declare least-privilege default scopes; tests assert dangerous defaults are not silently added. | +| SCIM destructive sync | Directory sync capabilities require explicit write scopes and scenario tests deprovisioning as disabled unless configured. | +| SDK/API drift | Provider SDK versions are pinned; release verification includes `go vet`, tests, contract validation, and runtime launch. | + +## Infrastructure Impact +| area | impact | +|---|---| +| Repositories | New repos may be created for Auth0, Entra, Ory Kratos, Ory Hydra, Ory Polis, and Scalekit plugins if absent. | +| Secrets | Scenario uses local/mock provider servers by default; real provider credentials remain env/secret references only. | +| Network | Runtime tests use local test servers; optional real-provider smoke tests run only when credentials are present. | +| Docker/Kubernetes | Scenario deployment includes app, admin, local provider mocks where needed, and a tailscale sidecar using existing cluster secret wiring. | +| Releases | Each changed plugin repo gets a patch release after PR merge and green validation. | + +## Multi-Component Validation +| boundary | proof | +|---|---| +| Auth descriptor proto to runtime | Core auth tests create typed descriptors and verify admin describe renders providers dynamically. | +| Provider SDK to plugin | Provider plugin tests use official SDK client types plus httptest servers or SDK mock transports. | +| Provider plugin to auth admin | Scenario runs descriptor merge and verifies provider selectors are populated from provider steps. | +| Admin UI to authz | Browser QA verifies users without admin provider-management scopes cannot see or save provider controls. | +| Runtime enforcement | Scenario rotates generic OIDC/Auth0/Entra/Okta/Ory descriptors and verifies login/token checks use the selected provider. | +| Release | `go test ./...`, `go vet ./...`, `wfctl plugin validate-contract`, scenario run, and Playwright CLI QA before each release. | + +## Assumptions +| id | assumption | challenge | fallback | +|---|---|---|---| +| A1 | Provider descriptors can be passed through Workflow step outputs today | A central runtime registry may not exist yet | Use `step.auth_provider_catalog` merge step now; later service registry can call same helpers. | +| A2 | Ory Polis has no stable official Go SDK | If a Go SDK appears, direct HTTP client is less ideal | Keep `sdk_name`/`sdk_url` metadata and switch implementation behind same contract when available. | +| A3 | Real cloud-provider integration tests cannot always run locally | Credentials may not be present | Default to SDK-backed local httptest/mocks; credentialed smoke tests are opt-in and never required for PR CI. | +| A4 | Existing `workflow-plugin-sso` is not redundant | Some OIDC flows belong in generic runtime rather than vendor management plugins | Keep it as generic OIDC runtime and add descriptors/helpers instead of folding it into auth. | +| A5 | Provider admin config persistence belongs outside provider plugins | Some providers can persist remotely | Provider plugins validate/perform remote management actions, but Workflow config persistence remains host-owned. | + +## Self-Challenge +| doubt | answer | +|---|---| +| Is this too many plugins? | The user explicitly asked for multiple provider implementations and category clarity. Plugin boundaries prevent a single auth monolith. | +| Could descriptors become another hard-coded schema? | Descriptors are generic and provider-owned; auth core only knows categories/capabilities, not vendor fields. | +| What fails first? | Descriptor/admin mismatch. Tests require every advertised capability to have a provider step, contract entry, and scenario proof before release. | + +## Rollback +| change | rollback | +|---|---| +| Core auth descriptor contract | Revert the additive PR or release a patch tag restoring previous admin fallback behavior. | +| Existing provider plugin updates | Revert provider descriptor PRs independently; auth core still supports built-in/local descriptors. | +| New provider plugin repos | Do not install in scenarios; tags can remain but registry entries can be withheld or superseded by patch releases. | +| Scenario deployment | Revert scenario config/manifests; no production deploy is performed without approval. | + +## Source Notes +- Okta Go SDK v6 is official and current per `github.com/okta/okta-sdk-golang`. +- Auth0 Go SDK v2 is official for Authentication and Management APIs per `github.com/auth0/go-auth0`. +- Microsoft Graph Go SDK is official for Graph v1.0 and pairs with Azure/Kiota auth per `github.com/microsoftgraph/msgraph-sdk-go`. +- Ory SDK guidance says Ory Network uses `client-go`; self-hosted Kratos/Hydra/Keto use component SDKs matched to deployed versions. +- Ory Polis is the open-source successor to BoxyHQ Jackson and supports SAML/OIDC enterprise SSO and SCIM, but current public materials emphasize REST/service usage rather than a stable Go SDK. + diff --git a/docs/plans/2026-05-27-auth-provider-architecture.md b/docs/plans/2026-05-27-auth-provider-architecture.md new file mode 100644 index 0000000..23119d9 --- /dev/null +++ b/docs/plans/2026-05-27-auth-provider-architecture.md @@ -0,0 +1,475 @@ +# Auth Provider Architecture Implementation Plan + +> **For the implementing agent:** REQUIRED SUB-SKILL: Use autodev:executing-plans to implement this plan task-by-task. + +**Goal:** Build SDK-backed, plugin-first auth provider integrations and make auth/admin provider UI fully dynamic. + +**Architecture:** `workflow-plugin-auth` defines strict shared provider descriptor contracts and consumes provider descriptors dynamically. Existing and new provider plugins expose descriptor steps plus provider-specific SDK-backed management/runtime steps. `workflow-scenarios` composes the app/admin stack to prove dynamic admin rendering, auth/authz gating, provider selection, and runtime enforcement. + +**Tech Stack:** Go, Workflow plugin SDK strict proto contracts, wfctl, official Okta/Auth0/Microsoft/Ory/Scalekit SDKs or documented official REST API where no Go SDK exists, Docker/Kubernetes scenario deployment, Playwright CLI. + +**Base branch:** main + +--- + +## Scope Manifest + +**PR Count:** 9 +**Tasks:** 13 +**Estimated Lines of Change:** ~9000 + +**Out of scope:** +- Production deployment to external clouds or real customer tenants. +- Storing live provider credentials in git, scenarios, or release workflows. +- Replacing `workflow-plugin-authz` authorization engines. +- Replacing `workflow-plugin-admin` shell rendering beyond consuming dynamic contributions/descriptors. +- Claiming Ory Polis has an official Go SDK unless one is verified during implementation. + +**PR Grouping:** + +| PR # | Title | Tasks | Branch | +|------|-------|-------|--------| +| 1 | Auth provider descriptor contracts | Task 1, Task 2 | feat/auth-provider-descriptors | +| 2 | Generic SSO descriptor integration | Task 3 | feat/sso-provider-descriptors | +| 3 | Okta provider descriptor hardening | Task 4 | feat/okta-provider-descriptors | +| 4 | Auth0 provider plugin | Task 5 | feat/auth0-provider-plugin | +| 5 | Entra provider plugin | Task 6 | feat/entra-provider-plugin | +| 6 | Ory Kratos and Hydra provider plugins | Task 7, Task 8 | feat/ory-auth-plugins | +| 7 | Enterprise SSO/SCIM provider plugins | Task 9, Task 10 | feat/enterprise-sso-scim-providers | +| 8 | Scenario admin/runtime proof | Task 11, Task 12 | feat/provider-admin-scenario | +| 9 | Release cascade and retrospective | Task 13 | chore/auth-provider-release-cascade | + +**Status:** Locked 2026-05-27T14:17:48Z + +### Task 1: Core Provider Descriptor Contract Tests + +**Files:** +- Modify: `workflow-plugin-auth/internal/contracts/auth.proto` +- Modify: `workflow-plugin-auth/internal/plugin.go` +- Modify: `workflow-plugin-auth/internal/plugin_contracts_test.go` +- Create: `workflow-plugin-auth/internal/step_provider_catalog_test.go` +- Modify: `workflow-plugin-auth/plugin.contracts.json` +- Modify: `workflow-plugin-auth/plugin.json` + +**Steps:** +1. Write failing tests for `AuthProviderDescriptor`, `AuthProviderCapability`, and `AuthProviderConfigField` strict proto field presence. +2. Write failing tests for `step.auth_provider_catalog` merging provider descriptors from config/current input. +3. Assert catalog merge de-duplicates by provider ID and rejects duplicate capability keys with incompatible metadata. +4. Assert secret fields are only represented by key/configured state and never echo values. +5. Assert runtime `ContractRegistry()` and `plugin.contracts.json` include `step.auth_provider_catalog`. +6. Run: `GOWORK=off go test ./internal -run 'TestAuthProvider|TestContractRegistry|TestProtoFields' -count=1`. +7. Expected: new tests fail before implementation and pass after Task 2. + +**Verification:** strict proto and contract registry tests prove descriptor contract shape before runtime use. + +**Rollback:** revert PR 1; existing auth admin fallback remains available. + +### Task 2: Dynamic Auth Admin Provider Rendering + +**Files:** +- Modify: `workflow-plugin-auth/internal/contracts/auth.proto` +- Modify: `workflow-plugin-auth/internal/contracts/auth.pb.go` +- Modify: `workflow-plugin-auth/internal/step_admin_config.go` +- Modify: `workflow-plugin-auth/internal/step_methods_policy.go` +- Modify: `workflow-plugin-auth/internal/step_oauth.go` +- Modify: `workflow-plugin-auth/internal/typed.go` +- Modify: `workflow-plugin-auth/internal/plugin.go` +- Modify: `workflow-plugin-auth/internal/step_admin_config_test.go` +- Modify: `workflow-plugin-auth/README.md` +- Modify: `workflow-plugin-auth/SPEC.md` + +**Steps:** +1. Add `AuthProviderDescriptor`, `AuthProviderCapability`, `AuthProviderConfigField`, `AuthProviderCatalogInput`, and `AuthProviderCatalogOutput` messages. +2. Generate protobuf Go code with `protoc --go_out=. --go_opt=paths=source_relative internal/contracts/auth.proto`. +3. Implement `newAuthProviderCatalogStep` and typed step registration. +4. Extend `AuthAdminConfig`/`AuthAdminDescribeInput` with repeated provider descriptors. +5. Refactor `buildOAuthAdminControls` to read descriptors and capability config fields; keep Google/Facebook compatibility fallback only when no provider descriptors are supplied. +6. Refactor OAuth policy provider support to derive advertised providers from descriptors when supplied. +7. Add built-in descriptor generation for local auth methods: passkey, password dev-only, TOTP, magic link, email code, SMS challenge. +8. Update tests so Auth0/Entra/Okta controls render from supplied descriptors without auth-core vendor constants. +9. Add tests that unsupported descriptors show disabled reason and cannot validate as enabled. +10. Run `gofmt`. +11. Run `GOWORK=off go test ./...`. +12. Run `GOWORK=off go vet ./...`. +13. Run `wfctl plugin validate-contract .`. +14. Open PR #1; after merge, release next patch tag for `workflow-plugin-auth`. + +**Verification:** plugin tests, vet, contract validation, and release CI prove auth core is dynamic and backward compatible. + +**Rollback:** revert PR 1 or publish next patch restoring prior admin controls. + +### Task 3: Generic SSO Descriptor Integration + +**Files:** +- Modify/Create in `workflow-plugin-sso`: `internal/contracts/sso.proto` +- Modify/Create: `workflow-plugin-sso/internal/step_provider_describe.go` +- Modify: `workflow-plugin-sso/internal/plugin.go` +- Modify: `workflow-plugin-sso/internal/oidc.go` +- Modify: `workflow-plugin-sso/internal/entra_provider.go` +- Modify/Create: `workflow-plugin-sso/internal/auth0_provider.go` +- Modify: `workflow-plugin-sso/plugin.contracts.json` +- Modify: `workflow-plugin-sso/README.md` + +**Steps:** +1. Add `step.sso_auth_provider_describe` returning auth-compatible provider descriptors for generic OIDC, Okta issuer helper, Entra issuer helper, and Auth0 issuer helper. +2. Keep `workflow-plugin-sso` as generic OIDC runtime; do not duplicate vendor management APIs here. +3. Add Auth0 issuer/domain helper and tests. +4. Add tests that provider descriptors include OIDC scopes, issuer URL, callback config fields, claim mappings, PKCE/state requirements, and admin/app scopes. +5. Add tests that token validation rejects unknown issuer/provider and preserves existing behavior. +6. Run `GOWORK=off go test ./...`. +7. Run `GOWORK=off go vet ./...`. +8. Run `wfctl plugin validate-contract .`. +9. Open PR #2; after merge, release next patch tag for `workflow-plugin-sso`. + +**Verification:** generic OIDC descriptor step plus existing token-validation tests prove SSO is not redundant and remains runtime-focused. + +**Rollback:** revert PR 2; generic OIDC module/steps remain unchanged. + +### Task 4: Okta Provider Descriptor Hardening + +**Files:** +- Modify in `workflow-plugin-okta`: `internal/contracts/*.proto` or create strict proto contracts if absent. +- Modify/Create: `workflow-plugin-okta/internal/step_provider_describe.go` +- Modify: `workflow-plugin-okta/internal/module.go` +- Modify: `workflow-plugin-okta/okta/provider.go` +- Modify: `workflow-plugin-okta/plugin.contracts.json` +- Modify: `workflow-plugin-okta/README.md` + +**Steps:** +1. Add `step.okta_auth_provider_describe` with categories `identity_management`, `oauth2_oidc`, `enterprise_sso`, and `directory_sync` only for already implemented Okta APIs. +2. Ensure descriptors use official Okta Go SDK v6-backed module config and least-privilege default scopes. +3. Add tests that API-token and private-key modes are mutually exclusive and descriptor scopes match auth mode. +4. Add tests for user/group/app/auth-server capability descriptors pointing at existing Okta steps. +5. Remove misleading `Experimental` wording only if integration tests prove the covered capabilities with httptest/SDK mock transport. +6. Run `GOWORK=off go test ./...`. +7. Run `GOWORK=off go vet ./...`. +8. Run `wfctl plugin validate-contract .`. +9. Open PR #3; after merge, release next patch tag for `workflow-plugin-okta`. + +**Verification:** descriptors map only to SDK-backed Okta capabilities already implemented and tested. + +**Rollback:** revert PR 3; Okta management steps remain usable without descriptors. + +### Task 5: Auth0 Provider Plugin + +**Files:** +- Create repo: `workflow-plugin-auth0` +- Create: `.github/workflows/ci.yml`, `.github/workflows/release.yml` +- Create: `cmd/workflow-plugin-auth0/main.go` +- Create: `internal/contracts/auth0.proto` +- Create: `internal/module.go` +- Create: `internal/plugin.go` +- Create: `internal/step_provider_describe.go` +- Create: `internal/step_users.go` +- Create: `internal/step_roles.go` +- Create: `internal/step_connections.go` +- Create: `auth0/provider.go` +- Create: `plugin.json`, `plugin.contracts.json`, `README.md`, `Makefile` + +**Steps:** +1. Scaffold a standard Workflow plugin repo with strict proto contract tests. +2. Use official `github.com/auth0/go-auth0/v2` Authentication and Management clients. +3. Implement module `auth0.provider` with domain plus token or client-credentials config. +4. Implement `step.auth0_auth_provider_describe`. +5. Implement SDK-backed management steps: user list/get/create/update/delete, role list/get/assign/remove, connection list/get. +6. Implement Authentication API descriptor for Auth Code + PKCE; leave app callback persistence to consuming app. +7. Add httptest-backed SDK tests for each advertised step and error mapping. +8. Add contract registry/manifest tests. +9. Add README security guidance: no password grant by default, PKCE/state required, management token least privilege. +10. Run `GOWORK=off go test ./...`. +11. Run `GOWORK=off go vet ./...`. +12. Run `wfctl plugin validate-contract .`. +13. Create GitHub repo if absent, open PR #4, merge after green CI, release `v0.1.0`. + +**Verification:** official SDK-backed local tests and contract validation prove real Auth0 provider functionality without live credentials. + +**Rollback:** do not install the plugin in scenarios/registry; publish corrective patch if tag is bad. + +### Task 6: Entra Provider Plugin + +**Files:** +- Create repo: `workflow-plugin-entra` +- Create standard plugin files mirroring Task 5. +- Create: `entra/provider.go` +- Create: `internal/step_provider_describe.go` +- Create: `internal/step_users.go` +- Create: `internal/step_groups.go` +- Create: `internal/step_apps.go` +- Create: `internal/step_auth_methods_policy.go` + +**Steps:** +1. Scaffold `workflow-plugin-entra` with strict proto contracts. +2. Use official Microsoft Graph Go SDK and Azure/Kiota auth libraries. +3. Implement module `entra.provider` with tenant ID, client ID, and secret/cert credential config. +4. Implement provider descriptors for `identity_management`, `oauth2_oidc`, `enterprise_sso`, and `directory_sync`. +5. Implement users list/get/create/update/delete where Graph SDK supports testable methods. +6. Implement groups list/get/member add/remove. +7. Implement app registration list/get/create for OIDC app management. +8. Implement auth-method policy describe/update only where Graph SDK permissions are clear; otherwise mark unavailable with reason. +9. Add httptest/request-adapter tests for advertised SDK calls. +10. Add README least-privilege Graph permission guidance. +11. Run `GOWORK=off go test ./...`. +12. Run `GOWORK=off go vet ./...`. +13. Run `wfctl plugin validate-contract .`. +14. Create GitHub repo if absent, open PR #5, merge after green CI, release `v0.1.0`. + +**Verification:** Graph SDK-backed tests prove Entra user/group/app provider operations and descriptors. + +**Rollback:** do not install plugin in scenarios/registry; publish corrective patch if tag is bad. + +### Task 7: Ory Kratos Provider Plugin + +**Files:** +- Create repo: `workflow-plugin-ory-kratos` +- Create standard plugin files. +- Create: `orykratos/provider.go` +- Create: `internal/step_provider_describe.go` +- Create: `internal/step_identities.go` +- Create: `internal/step_flows.go` + +**Steps:** +1. Verify current official Kratos Go client package and version before implementation. +2. Scaffold strict plugin contracts. +3. Implement module `ory.kratos` using the official Kratos client for self-hosted Kratos or Ory Network SDK only when configured. +4. Implement provider descriptors for `identity_management` and `authentication_method`. +5. Implement identity list/get/create/update/delete steps. +6. Implement registration/login/recovery/verification flow start or inspect steps only where SDK/API semantics are clear and testable. +7. Mark passkey/passwordless/2FA support as available only if descriptor is backed by a real flow/config API call. +8. Add httptest tests for each advertised endpoint. +9. Run `GOWORK=off go test ./...`. +10. Run `GOWORK=off go vet ./...`. +11. Run `wfctl plugin validate-contract .`. +12. Create repo/PR #6 part A, merge after green CI. + +**Verification:** Kratos SDK-backed identity/flow tests and descriptors prove real identity-management integration. + +**Rollback:** leave plugin uninstalled from scenarios/registry or publish patch. + +### Task 8: Ory Hydra Provider Plugin + +**Files:** +- Create repo: `workflow-plugin-ory-hydra` +- Create standard plugin files. +- Create: `oryhydra/provider.go` +- Create: `internal/step_provider_describe.go` +- Create: `internal/step_clients.go` +- Create: `internal/step_jwks.go` + +**Steps:** +1. Verify current official Hydra Go client package and version before implementation. +2. Implement module `ory.hydra` with admin URL and credential config. +3. Implement provider descriptors for `oauth2_oidc`. +4. Implement OAuth2 client list/get/create/update/delete steps. +5. Implement JWKS metadata/list steps if SDK supports them. +6. Do not implement consent/login UI in this plugin unless it can be backed by real Hydra APIs and scenario wiring. +7. Add httptest tests for each advertised Hydra admin API call. +8. Run `GOWORK=off go test ./...`. +9. Run `GOWORK=off go vet ./...`. +10. Run `wfctl plugin validate-contract .`. +11. Finish PR #6 with Kratos/Hydra or split if review size requires explicit amendment. +12. Release `workflow-plugin-ory-kratos v0.1.0` and `workflow-plugin-ory-hydra v0.1.0`. + +**Verification:** Hydra SDK-backed tests prove OIDC provider management capability. + +**Rollback:** leave plugin uninstalled from scenarios/registry or publish patch. + +### Task 9: Ory Polis Provider Plugin + +**Files:** +- Create repo: `workflow-plugin-ory-polis` +- Create standard plugin files. +- Create: `polis/provider.go` +- Create: `internal/step_provider_describe.go` +- Create: `internal/step_sso_connections.go` +- Create: `internal/step_directory_sync.go` + +**Steps:** +1. Verify whether a stable official Ory Polis Go SDK exists. +2. If official Go SDK exists, use it. If not, implement a small typed HTTP client over the official Polis/Jackson API and record the absence of a Go SDK in README and descriptor metadata. +3. Implement module `ory.polis` with API base URL and token config. +4. Implement provider descriptors for `enterprise_sso` and `directory_sync`. +5. Implement SSO connection list/get/create/update/delete steps. +6. Implement directory sync connection list/get and SCIM event/status steps where API supports them. +7. Add httptest tests for each advertised endpoint; do not advertise unimplemented SAML or SCIM operations. +8. Run `GOWORK=off go test ./...`. +9. Run `GOWORK=off go vet ./...`. +10. Run `wfctl plugin validate-contract .`. +11. Create repo/PR #7 part A, merge after green CI. + +**Verification:** Polis API-backed tests prove real enterprise SSO/SCIM management without claiming a nonexistent Go SDK. + +**Rollback:** leave plugin uninstalled from scenarios/registry or publish patch. + +### Task 10: Scalekit Enterprise SSO/SCIM Provider Plugin + +**Files:** +- Create repo: `workflow-plugin-scalekit` +- Create standard plugin files. +- Create: `scalekit/provider.go` +- Create: `internal/step_provider_describe.go` +- Create: `internal/step_connections.go` +- Create: `internal/step_directory.go` + +**Steps:** +1. Verify current official Scalekit Go SDK package and version. +2. Implement module `scalekit.provider` using the official SDK. +3. Implement provider descriptors for `enterprise_sso` and `directory_sync`. +4. Implement SSO connection list/get/create/update/delete steps and SCIM/directory steps supported by SDK. +5. Add httptest/SDK mock tests for each advertised call. +6. Run `GOWORK=off go test ./...`. +7. Run `GOWORK=off go vet ./...`. +8. Run `wfctl plugin validate-contract .`. +9. Finish PR #7; release `workflow-plugin-ory-polis v0.1.0` and `workflow-plugin-scalekit v0.1.0`. + +**Verification:** non-Ory enterprise SSO/SCIM provider proves Ory is not the only implementation path. + +**Rollback:** leave plugin uninstalled from scenarios/registry or publish patch. + +### Task 11: Scenario Dynamic Admin Provider UX + +**Files:** +- Modify in `workflow-scenarios`: current admin/authz scenario API files. +- Modify: scenario admin UI files. +- Modify: scenario workflow YAML and plugin version pins. +- Modify/Create: scenario tests. + +**Steps:** +1. Update scenario plugin pins to released auth/admin/authz/sso/provider plugin versions. +2. Compose provider descriptor steps for local auth, generic OIDC, Okta, Auth0, Entra, Kratos, Hydra, Polis, and Scalekit using local/mock configs. +3. Expose admin endpoint returning merged provider catalog and auth admin describe output. +4. Update admin UI provider settings tabs to render descriptors dynamically with clear labels/tooltips. +5. Ensure provider, capability, scope, role, and config options are lookup-backed where descriptor options exist. +6. Hide or disable controls when current admin user lacks required admin scope. +7. Add scenario tests: + - anonymous admin blocked. + - app user cannot see admin provider controls. + - provider-admin can view descriptors but cannot save secrets without write scope. + - auth-admin can save accepted non-secret config. + - provider choices come from descriptors, not hard-coded UI arrays. +8. Run scenario test script. +9. Run `wfctl validate` or repo-equivalent Workflow validation. + +**Verification:** scenario API/UI proves dynamic admin UX, auth gate, authz gate, and lookup-backed controls. + +**Rollback:** revert scenario PR; plugin releases remain independently usable. + +### Task 12: Scenario Runtime, Docker/Kubernetes, Tailscale QA + +**Files:** +- Modify in `workflow-scenarios`: Dockerfile/compose/Kubernetes manifests for admin provider scenario. +- Modify: tailscale sidecar manifests using existing local cluster secret names. +- Create/modify: Playwright CLI QA scripts or documented commands. + +**Steps:** +1. Build and launch the scenario locally with Docker or the local Kubernetes cluster. +2. Include tailscale sidecar using existing cluster secret wiring; do not create or print secrets. +3. Rotate provider configuration across generic OIDC, Auth0, Entra, Okta, Kratos/Hydra, and enterprise SSO descriptors using local mock servers where live credentials are absent. +4. Verify runtime enforcement: + - login/token validation succeeds for configured provider. + - invalid issuer/audience/token fails closed. + - provider-specific admin action requires provider write scope. + - role/scope assignment still enforced through authz plugin. +5. Run Playwright CLI exploratory QA: + - desktop and mobile admin views. + - provider tabs and tooltips. + - scope/role pickers. + - save/reject diagnostics. + - forbidden user flows. +6. Fix defects found by QA in plugin repos first when root cause is contract/runtime, scenario second only for composition bugs. +7. Provide local and tailnet reachable URLs when running. + +**Verification:** launched app + admin portal with tailscale sidecar; Playwright screenshots and functional tests prove realistic operation. + +**Rollback:** stop local deployment and revert scenario PR. + +### Task 13: Release Cascade and Completion + +**Files:** +- Modify: `plugin.json` versions in each changed plugin repo. +- Modify: release notes/CHANGELOG where repo has one. +- Modify: scenario plugin version pins. +- Create: retrospective doc if repo convention exists. +- Append: `/Users/jon/workspace/.autodev/state/phase-progress.jsonl`. + +**Steps:** +1. For each changed plugin repo, run: + - `GOWORK=off go test ./...` + - `GOWORK=off go vet ./...` + - `wfctl plugin validate-contract --for-publish --tag vX.Y.Z .` + - `git diff --check` +2. Create/merge PRs according to the manifest. Use `gh --version` before and after each `gh pr create`; add Copilot reviewer but do not wait on it. +3. After each merge, tag and push the next release. +4. Watch release CI for every tag and main CI for every merge. +5. Update `workflow-scenarios` pins only to released versions. +6. Re-run full scenario runtime and Playwright QA on released pins. +7. Run adversarial security review against final diffs and scenario behavior. +8. Record phase progress and, if all scope is complete, run `scope-lock-complete` with verification evidence. + +**Verification:** all plugin releases are published, main/release CI is green, scenario runs against released artifacts, and admin portal is ready for user retest. + +**Rollback:** publish patch releases from last known-good commits; scenario pins can revert to previous released versions. + +## Adversarial Plan Review + +### Report +**Phase:** plan +**Artifact:** `docs/plans/2026-05-27-auth-provider-architecture.md` +**Status:** PASS + +| sev | class | loc | issue | fix | +|---|---|---|---|---| +| Minor | Over-decomposition | Scope Manifest | Nine PRs is heavy. | Accepted because user requested multiple real providers and releases; each PR is independently revertible. | +| Minor | Infrastructure | Tasks 5-10 | New repo creation has GitHub side effects. | Only create repos if absent; no production deploy; release each after tests. | +| Minor | Assumption | Task 9 | Polis Go SDK may not exist. | Plan requires verification and avoids claiming SDK support when absent. | + +### Bug-Class Scan +| class | result | note | +|---|---|---| +| Project-guidance conflicts | Clean | Plugin boundaries follow workspace guidance. | +| Assumptions under attack | Clean | Provider SDK/API availability and no live credentials are explicit. | +| Repo-precedent conflicts | Clean | Existing strict proto/contract validation pattern is used. | +| YAGNI | Clean | Scope maps to user-requested providers/categories; no production deploy. | +| Missing failure modes | Clean | Invalid tokens, missing scopes, SDK drift, secret leakage, and provider mismatch covered. | +| Security/privacy | Clean | Least privilege, redaction, authz gates, and callback abuse cases included. | +| Infrastructure impact | Clean | New repos/releases/local deployment impacts listed. | +| Multi-component validation | Clean | Plugin-host-admin-scenario-runtime browser proof included. | +| Rollback | Clean | Each task has rollback notes. | +| Simpler alternative | Clean | Single auth monolith rejected in design due hard-coding risk. | +| User-intent drift | Clean | Directly addresses real providers, dynamic admin, authz-gated scenario, and releases. | +| Verification-class mismatch | Clean | Plugin, API, UI, runtime, and release checks match change classes. | +| Hidden serial dependencies | Clean | PR groups serialize shared contracts before provider/scenario consumers. | + +## Alignment Report + +**Status:** PASS + +**Coverage:** +| Design Requirement | Plan Task(s) | Status | +|---|---|---| +| R1 go vet fix remains first | Task 13 evidence references completed release; no code task reopens it | Covered | +| R2 remove hard-coded admin/provider UI | Task 1, Task 2, Task 11 | Covered | +| R3 actual providers Okta/Auth0/Entra/Ory | Task 3, Task 4, Task 5, Task 6, Task 7, Task 8, Task 9 | Covered | +| R4 distinguish categories | Task 1, Task 2, provider descriptor tasks | Covered | +| R5 non-Ory implementations | Task 3, Task 4, Task 5, Task 6, Task 10 | Covered | +| R6 lookup-backed UI | Task 1, Task 2, Task 11 | Covered | +| R7 plugin-first with scenario proof | Task 1-12 | Covered | +| R8 releases | Task 13 | Covered | + +**Scope Check:** +| Plan Task | Design Requirement | Status | +|---|---|---| +| Task 1 | Shared descriptor contracts | Justified | +| Task 2 | Dynamic auth admin provider rendering | Justified | +| Task 3 | Generic OIDC non-redundant SSO | Justified | +| Task 4 | Okta real provider | Justified | +| Task 5 | Auth0 real provider | Justified | +| Task 6 | Entra real provider | Justified | +| Task 7 | Ory Kratos identity provider | Justified | +| Task 8 | Ory Hydra OIDC provider | Justified | +| Task 9 | Ory Polis enterprise SSO/SCIM | Justified | +| Task 10 | Non-Ory enterprise SSO/SCIM | Justified | +| Task 11 | Dynamic admin UX scenario | Justified | +| Task 12 | Runtime/tailscale QA | Justified | +| Task 13 | Release cascade | Justified | + +**Drift Items:** none. diff --git a/docs/plans/2026-05-27-auth-provider-architecture.md.scope-lock b/docs/plans/2026-05-27-auth-provider-architecture.md.scope-lock new file mode 100644 index 0000000..71b079e --- /dev/null +++ b/docs/plans/2026-05-27-auth-provider-architecture.md.scope-lock @@ -0,0 +1 @@ +24d2b13517e2ab1c377d1850f7ebb2e6c973be4e15275bcce47315a5ab2de3db diff --git a/internal/contracts/auth.pb.go b/internal/contracts/auth.pb.go index 7929e88..8d869fb 100644 --- a/internal/contracts/auth.pb.go +++ b/internal/contracts/auth.pb.go @@ -3315,39 +3315,40 @@ func (x *AuthPolicyAuditOutput) GetError() string { } 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"` + 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"` + Providers []*AuthProviderDescriptor `protobuf:"bytes,33,rep,name=providers,proto3" json:"providers,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3606,9 +3607,17 @@ func (x *AuthAdminConfig) GetAllowInsecureTestOauthEndpoints() bool { return false } +func (x *AuthAdminConfig) GetProviders() []*AuthProviderDescriptor { + if x != nil { + return x.Providers + } + return nil +} + type AuthAdminDescribeInput struct { - state protoimpl.MessageState `protogen:"open.v1"` - Config *AuthAdminConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Config *AuthAdminConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + Providers []*AuthProviderDescriptor `protobuf:"bytes,2,rep,name=providers,proto3" json:"providers,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3650,6 +3659,13 @@ func (x *AuthAdminDescribeInput) GetConfig() *AuthAdminConfig { return nil } +func (x *AuthAdminDescribeInput) GetProviders() []*AuthProviderDescriptor { + if x != nil { + return x.Providers + } + return nil +} + type AuthAdminControlOption struct { state protoimpl.MessageState `protogen:"open.v1"` Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` @@ -4107,9 +4123,10 @@ func (x *AuthAdminValidateConfig) GetRequirePrimaryMethod() bool { } 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"` + 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"` + Providers []*AuthProviderDescriptor `protobuf:"bytes,3,rep,name=providers,proto3" json:"providers,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4158,6 +4175,13 @@ func (x *AuthAdminValidateInput) GetRequirePrimaryMethod() bool { return false } +func (x *AuthAdminValidateInput) GetProviders() []*AuthProviderDescriptor { + if x != nil { + return x.Providers + } + return nil +} + type AuthAdminValidateOutput struct { state protoimpl.MessageState `protogen:"open.v1"` Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` @@ -4945,6 +4969,562 @@ func (x *OAuthUserinfoOutput) GetError() string { return "" } +type AuthProviderConfigOption 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 *AuthProviderConfigOption) Reset() { + *x = AuthProviderConfigOption{} + mi := &file_internal_contracts_auth_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthProviderConfigOption) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthProviderConfigOption) ProtoMessage() {} + +func (x *AuthProviderConfigOption) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[59] + 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 AuthProviderConfigOption.ProtoReflect.Descriptor instead. +func (*AuthProviderConfigOption) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{59} +} + +func (x *AuthProviderConfigOption) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *AuthProviderConfigOption) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *AuthProviderConfigOption) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +type AuthProviderConfigField 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"` + HelpText string `protobuf:"bytes,4,opt,name=help_text,json=helpText,proto3" json:"help_text,omitempty"` + InputType string `protobuf:"bytes,5,opt,name=input_type,json=inputType,proto3" json:"input_type,omitempty"` + Secret bool `protobuf:"varint,6,opt,name=secret,proto3" json:"secret,omitempty"` + Required bool `protobuf:"varint,7,opt,name=required,proto3" json:"required,omitempty"` + Options []*AuthProviderConfigOption `protobuf:"bytes,8,rep,name=options,proto3" json:"options,omitempty"` + Lookup string `protobuf:"bytes,9,opt,name=lookup,proto3" json:"lookup,omitempty"` + ValidationPattern string `protobuf:"bytes,10,opt,name=validation_pattern,json=validationPattern,proto3" json:"validation_pattern,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthProviderConfigField) Reset() { + *x = AuthProviderConfigField{} + mi := &file_internal_contracts_auth_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthProviderConfigField) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthProviderConfigField) ProtoMessage() {} + +func (x *AuthProviderConfigField) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[60] + 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 AuthProviderConfigField.ProtoReflect.Descriptor instead. +func (*AuthProviderConfigField) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{60} +} + +func (x *AuthProviderConfigField) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *AuthProviderConfigField) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *AuthProviderConfigField) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AuthProviderConfigField) GetHelpText() string { + if x != nil { + return x.HelpText + } + return "" +} + +func (x *AuthProviderConfigField) GetInputType() string { + if x != nil { + return x.InputType + } + return "" +} + +func (x *AuthProviderConfigField) GetSecret() bool { + if x != nil { + return x.Secret + } + return false +} + +func (x *AuthProviderConfigField) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +func (x *AuthProviderConfigField) GetOptions() []*AuthProviderConfigOption { + if x != nil { + return x.Options + } + return nil +} + +func (x *AuthProviderConfigField) GetLookup() string { + if x != nil { + return x.Lookup + } + return "" +} + +func (x *AuthProviderConfigField) GetValidationPattern() string { + if x != nil { + return x.ValidationPattern + } + return "" +} + +type AuthProviderCapability 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"` + Category string `protobuf:"bytes,3,opt,name=category,proto3" json:"category,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + Supported bool `protobuf:"varint,5,opt,name=supported,proto3" json:"supported,omitempty"` + DisabledReason string `protobuf:"bytes,6,opt,name=disabled_reason,json=disabledReason,proto3" json:"disabled_reason,omitempty"` + AppScopes []string `protobuf:"bytes,7,rep,name=app_scopes,json=appScopes,proto3" json:"app_scopes,omitempty"` + AdminReadScopes []string `protobuf:"bytes,8,rep,name=admin_read_scopes,json=adminReadScopes,proto3" json:"admin_read_scopes,omitempty"` + AdminWriteScopes []string `protobuf:"bytes,9,rep,name=admin_write_scopes,json=adminWriteScopes,proto3" json:"admin_write_scopes,omitempty"` + ConfigFields []*AuthProviderConfigField `protobuf:"bytes,10,rep,name=config_fields,json=configFields,proto3" json:"config_fields,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthProviderCapability) Reset() { + *x = AuthProviderCapability{} + mi := &file_internal_contracts_auth_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthProviderCapability) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthProviderCapability) ProtoMessage() {} + +func (x *AuthProviderCapability) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[61] + 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 AuthProviderCapability.ProtoReflect.Descriptor instead. +func (*AuthProviderCapability) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{61} +} + +func (x *AuthProviderCapability) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *AuthProviderCapability) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *AuthProviderCapability) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *AuthProviderCapability) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AuthProviderCapability) GetSupported() bool { + if x != nil { + return x.Supported + } + return false +} + +func (x *AuthProviderCapability) GetDisabledReason() string { + if x != nil { + return x.DisabledReason + } + return "" +} + +func (x *AuthProviderCapability) GetAppScopes() []string { + if x != nil { + return x.AppScopes + } + return nil +} + +func (x *AuthProviderCapability) GetAdminReadScopes() []string { + if x != nil { + return x.AdminReadScopes + } + return nil +} + +func (x *AuthProviderCapability) GetAdminWriteScopes() []string { + if x != nil { + return x.AdminWriteScopes + } + return nil +} + +func (x *AuthProviderCapability) GetConfigFields() []*AuthProviderConfigField { + if x != nil { + return x.ConfigFields + } + return nil +} + +type AuthProviderDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,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"` + Categories []string `protobuf:"bytes,4,rep,name=categories,proto3" json:"categories,omitempty"` + Implementation string `protobuf:"bytes,5,opt,name=implementation,proto3" json:"implementation,omitempty"` + Version string `protobuf:"bytes,6,opt,name=version,proto3" json:"version,omitempty"` + DocsUrl string `protobuf:"bytes,7,opt,name=docs_url,json=docsUrl,proto3" json:"docs_url,omitempty"` + SupportLevel string `protobuf:"bytes,8,opt,name=support_level,json=supportLevel,proto3" json:"support_level,omitempty"` + DisabledReason string `protobuf:"bytes,9,opt,name=disabled_reason,json=disabledReason,proto3" json:"disabled_reason,omitempty"` + Capabilities []*AuthProviderCapability `protobuf:"bytes,10,rep,name=capabilities,proto3" json:"capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthProviderDescriptor) Reset() { + *x = AuthProviderDescriptor{} + mi := &file_internal_contracts_auth_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthProviderDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthProviderDescriptor) ProtoMessage() {} + +func (x *AuthProviderDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[62] + 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 AuthProviderDescriptor.ProtoReflect.Descriptor instead. +func (*AuthProviderDescriptor) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{62} +} + +func (x *AuthProviderDescriptor) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *AuthProviderDescriptor) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *AuthProviderDescriptor) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AuthProviderDescriptor) GetCategories() []string { + if x != nil { + return x.Categories + } + return nil +} + +func (x *AuthProviderDescriptor) GetImplementation() string { + if x != nil { + return x.Implementation + } + return "" +} + +func (x *AuthProviderDescriptor) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *AuthProviderDescriptor) GetDocsUrl() string { + if x != nil { + return x.DocsUrl + } + return "" +} + +func (x *AuthProviderDescriptor) GetSupportLevel() string { + if x != nil { + return x.SupportLevel + } + return "" +} + +func (x *AuthProviderDescriptor) GetDisabledReason() string { + if x != nil { + return x.DisabledReason + } + return "" +} + +func (x *AuthProviderDescriptor) GetCapabilities() []*AuthProviderCapability { + if x != nil { + return x.Capabilities + } + return nil +} + +type AuthProviderCatalogConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Providers []*AuthProviderDescriptor `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthProviderCatalogConfig) Reset() { + *x = AuthProviderCatalogConfig{} + mi := &file_internal_contracts_auth_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthProviderCatalogConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthProviderCatalogConfig) ProtoMessage() {} + +func (x *AuthProviderCatalogConfig) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[63] + 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 AuthProviderCatalogConfig.ProtoReflect.Descriptor instead. +func (*AuthProviderCatalogConfig) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{63} +} + +func (x *AuthProviderCatalogConfig) GetProviders() []*AuthProviderDescriptor { + if x != nil { + return x.Providers + } + return nil +} + +type AuthProviderCatalogInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Providers []*AuthProviderDescriptor `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthProviderCatalogInput) Reset() { + *x = AuthProviderCatalogInput{} + mi := &file_internal_contracts_auth_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthProviderCatalogInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthProviderCatalogInput) ProtoMessage() {} + +func (x *AuthProviderCatalogInput) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[64] + 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 AuthProviderCatalogInput.ProtoReflect.Descriptor instead. +func (*AuthProviderCatalogInput) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{64} +} + +func (x *AuthProviderCatalogInput) GetProviders() []*AuthProviderDescriptor { + if x != nil { + return x.Providers + } + return nil +} + +type AuthProviderCatalogOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Providers []*AuthProviderDescriptor `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` + Warnings []*AuthAdminDiagnostic `protobuf:"bytes,2,rep,name=warnings,proto3" json:"warnings,omitempty"` + Error string `protobuf:"bytes,100,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthProviderCatalogOutput) Reset() { + *x = AuthProviderCatalogOutput{} + mi := &file_internal_contracts_auth_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthProviderCatalogOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthProviderCatalogOutput) ProtoMessage() {} + +func (x *AuthProviderCatalogOutput) ProtoReflect() protoreflect.Message { + mi := &file_internal_contracts_auth_proto_msgTypes[65] + 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 AuthProviderCatalogOutput.ProtoReflect.Descriptor instead. +func (*AuthProviderCatalogOutput) Descriptor() ([]byte, []int) { + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{65} +} + +func (x *AuthProviderCatalogOutput) GetProviders() []*AuthProviderDescriptor { + if x != nil { + return x.Providers + } + return nil +} + +func (x *AuthProviderCatalogOutput) GetWarnings() []*AuthAdminDiagnostic { + if x != nil { + return x.Warnings + } + return nil +} + +func (x *AuthProviderCatalogOutput) GetError() string { + if x != nil { + return x.Error + } + return "" +} + type CredentialListInput struct { state protoimpl.MessageState `protogen:"open.v1"` CredentialsJson string `protobuf:"bytes,1,opt,name=credentials_json,json=credentialsJson,proto3" json:"credentials_json,omitempty"` @@ -4954,7 +5534,7 @@ type CredentialListInput struct { func (x *CredentialListInput) Reset() { *x = CredentialListInput{} - mi := &file_internal_contracts_auth_proto_msgTypes[59] + mi := &file_internal_contracts_auth_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4966,7 +5546,7 @@ func (x *CredentialListInput) String() string { func (*CredentialListInput) ProtoMessage() {} func (x *CredentialListInput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[59] + mi := &file_internal_contracts_auth_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4979,7 +5559,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{59} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{66} } func (x *CredentialListInput) GetCredentialsJson() string { @@ -5002,7 +5582,7 @@ type CredentialSummary struct { func (x *CredentialSummary) Reset() { *x = CredentialSummary{} - mi := &file_internal_contracts_auth_proto_msgTypes[60] + mi := &file_internal_contracts_auth_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5014,7 +5594,7 @@ func (x *CredentialSummary) String() string { func (*CredentialSummary) ProtoMessage() {} func (x *CredentialSummary) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[60] + mi := &file_internal_contracts_auth_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5027,7 +5607,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{60} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{67} } func (x *CredentialSummary) GetId() string { @@ -5076,7 +5656,7 @@ type CredentialListOutput struct { func (x *CredentialListOutput) Reset() { *x = CredentialListOutput{} - mi := &file_internal_contracts_auth_proto_msgTypes[61] + mi := &file_internal_contracts_auth_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5088,7 +5668,7 @@ func (x *CredentialListOutput) String() string { func (*CredentialListOutput) ProtoMessage() {} func (x *CredentialListOutput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[61] + mi := &file_internal_contracts_auth_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5101,7 +5681,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{61} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{68} } func (x *CredentialListOutput) GetCredentials() []*CredentialSummary { @@ -5136,7 +5716,7 @@ type CredentialRevokeInput struct { func (x *CredentialRevokeInput) Reset() { *x = CredentialRevokeInput{} - mi := &file_internal_contracts_auth_proto_msgTypes[62] + mi := &file_internal_contracts_auth_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5148,7 +5728,7 @@ func (x *CredentialRevokeInput) String() string { func (*CredentialRevokeInput) ProtoMessage() {} func (x *CredentialRevokeInput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[62] + mi := &file_internal_contracts_auth_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5161,7 +5741,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{62} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{69} } func (x *CredentialRevokeInput) GetCredentialId() string { @@ -5196,7 +5776,7 @@ type CredentialRevokeOutput struct { func (x *CredentialRevokeOutput) Reset() { *x = CredentialRevokeOutput{} - mi := &file_internal_contracts_auth_proto_msgTypes[63] + mi := &file_internal_contracts_auth_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5208,7 +5788,7 @@ func (x *CredentialRevokeOutput) String() string { func (*CredentialRevokeOutput) ProtoMessage() {} func (x *CredentialRevokeOutput) ProtoReflect() protoreflect.Message { - mi := &file_internal_contracts_auth_proto_msgTypes[63] + mi := &file_internal_contracts_auth_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5221,7 +5801,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{63} + return file_internal_contracts_auth_proto_rawDescGZIP(), []int{70} } func (x *CredentialRevokeOutput) GetAuthorized() bool { @@ -5556,7 +6136,7 @@ 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" + + "\x05error\x18d \x01(\tR\x05error\"\xf2\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" + @@ -5592,7 +6172,8 @@ const file_internal_contracts_auth_proto_rawDesc = "" + "\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" + + "#allow_insecure_test_oauth_endpoints\x18 \x01(\bH\tR\x1fallowInsecureTestOauthEndpoints\x88\x01\x01\x12N\n" + + "\tproviders\x18! \x03(\v20.workflow.plugins.auth.v1.AuthProviderDescriptorR\tprovidersB\x18\n" + "\x16_password_auth_enabledB\x13\n" + "\x11_password_enabledB\x14\n" + "\x12_totp_auth_enabledB\x0f\n" + @@ -5602,9 +6183,10 @@ const file_internal_contracts_auth_proto_rawDesc = "" + "\x15_oauth_routes_enabledB\x0e\n" + "\f_sms_enabledB\x13\n" + "\x11_sms_auth_enabledB&\n" + - "$_allow_insecure_test_oauth_endpoints\"[\n" + + "$_allow_insecure_test_oauth_endpoints\"\xab\x01\n" + "\x16AuthAdminDescribeInput\x12A\n" + - "\x06config\x18\x01 \x01(\v2).workflow.plugins.auth.v1.AuthAdminConfigR\x06config\"f\n" + + "\x06config\x18\x01 \x01(\v2).workflow.plugins.auth.v1.AuthAdminConfigR\x06config\x12N\n" + + "\tproviders\x18\x02 \x03(\v20.workflow.plugins.auth.v1.AuthProviderDescriptorR\tproviders\"f\n" + "\x16AuthAdminControlOption\x12\x14\n" + "\x05value\x18\x01 \x01(\tR\x05value\x12\x14\n" + "\x05label\x18\x02 \x01(\tR\x05label\x12 \n" + @@ -5646,10 +6228,11 @@ const file_internal_contracts_auth_proto_rawDesc = "" + "\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" + + "\x17_require_primary_method\"\xfe\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" + + "\x16require_primary_method\x18\x02 \x01(\bH\x00R\x14requirePrimaryMethod\x88\x01\x01\x12N\n" + + "\tproviders\x18\x03 \x03(\v20.workflow.plugins.auth.v1.AuthProviderDescriptorR\tprovidersB\x19\n" + "\x17_require_primary_method\"\xfe\x02\n" + "\x17AuthAdminValidateOutput\x12\x14\n" + "\x05valid\x18\x01 \x01(\bR\x05valid\x12@\n" + @@ -5733,6 +6316,58 @@ const file_internal_contracts_auth_proto_rawDesc = "" + "\apicture\x18\b \x01(\tR\apicture\x126\n" + "\n" + "raw_claims\x18\t \x01(\v2\x17.google.protobuf.StructR\trawClaims\x12\x14\n" + + "\x05error\x18d \x01(\tR\x05error\"h\n" + + "\x18AuthProviderConfigOption\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\"\xe8\x02\n" + + "\x17AuthProviderConfigField\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\x12\x1b\n" + + "\thelp_text\x18\x04 \x01(\tR\bhelpText\x12\x1d\n" + + "\n" + + "input_type\x18\x05 \x01(\tR\tinputType\x12\x16\n" + + "\x06secret\x18\x06 \x01(\bR\x06secret\x12\x1a\n" + + "\brequired\x18\a \x01(\bR\brequired\x12L\n" + + "\aoptions\x18\b \x03(\v22.workflow.plugins.auth.v1.AuthProviderConfigOptionR\aoptions\x12\x16\n" + + "\x06lookup\x18\t \x01(\tR\x06lookup\x12-\n" + + "\x12validation_pattern\x18\n" + + " \x01(\tR\x11validationPattern\"\x96\x03\n" + + "\x16AuthProviderCapability\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\x12\x1a\n" + + "\bcategory\x18\x03 \x01(\tR\bcategory\x12 \n" + + "\vdescription\x18\x04 \x01(\tR\vdescription\x12\x1c\n" + + "\tsupported\x18\x05 \x01(\bR\tsupported\x12'\n" + + "\x0fdisabled_reason\x18\x06 \x01(\tR\x0edisabledReason\x12\x1d\n" + + "\n" + + "app_scopes\x18\a \x03(\tR\tappScopes\x12*\n" + + "\x11admin_read_scopes\x18\b \x03(\tR\x0fadminReadScopes\x12,\n" + + "\x12admin_write_scopes\x18\t \x03(\tR\x10adminWriteScopes\x12V\n" + + "\rconfig_fields\x18\n" + + " \x03(\v21.workflow.plugins.auth.v1.AuthProviderConfigFieldR\fconfigFields\"\x81\x03\n" + + "\x16AuthProviderDescriptor\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x1e\n" + + "\n" + + "categories\x18\x04 \x03(\tR\n" + + "categories\x12&\n" + + "\x0eimplementation\x18\x05 \x01(\tR\x0eimplementation\x12\x18\n" + + "\aversion\x18\x06 \x01(\tR\aversion\x12\x19\n" + + "\bdocs_url\x18\a \x01(\tR\adocsUrl\x12#\n" + + "\rsupport_level\x18\b \x01(\tR\fsupportLevel\x12'\n" + + "\x0fdisabled_reason\x18\t \x01(\tR\x0edisabledReason\x12T\n" + + "\fcapabilities\x18\n" + + " \x03(\v20.workflow.plugins.auth.v1.AuthProviderCapabilityR\fcapabilities\"k\n" + + "\x19AuthProviderCatalogConfig\x12N\n" + + "\tproviders\x18\x01 \x03(\v20.workflow.plugins.auth.v1.AuthProviderDescriptorR\tproviders\"j\n" + + "\x18AuthProviderCatalogInput\x12N\n" + + "\tproviders\x18\x01 \x03(\v20.workflow.plugins.auth.v1.AuthProviderDescriptorR\tproviders\"\xcc\x01\n" + + "\x19AuthProviderCatalogOutput\x12N\n" + + "\tproviders\x18\x01 \x03(\v20.workflow.plugins.auth.v1.AuthProviderDescriptorR\tproviders\x12I\n" + + "\bwarnings\x18\x02 \x03(\v2-.workflow.plugins.auth.v1.AuthAdminDiagnosticR\bwarnings\x12\x14\n" + "\x05error\x18d \x01(\tR\x05error\"@\n" + "\x13CredentialListInput\x12)\n" + "\x10credentials_json\x18\x01 \x01(\tR\x0fcredentialsJson\"\x94\x01\n" + @@ -5771,7 +6406,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, 64) +var file_internal_contracts_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 71) var file_internal_contracts_auth_proto_goTypes = []any{ (*CredentialModuleConfig)(nil), // 0: workflow.plugins.auth.v1.CredentialModuleConfig (*PasskeyStepConfig)(nil), // 1: workflow.plugins.auth.v1.PasskeyStepConfig @@ -5832,34 +6467,51 @@ var file_internal_contracts_auth_proto_goTypes = []any{ (*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 + (*AuthProviderConfigOption)(nil), // 59: workflow.plugins.auth.v1.AuthProviderConfigOption + (*AuthProviderConfigField)(nil), // 60: workflow.plugins.auth.v1.AuthProviderConfigField + (*AuthProviderCapability)(nil), // 61: workflow.plugins.auth.v1.AuthProviderCapability + (*AuthProviderDescriptor)(nil), // 62: workflow.plugins.auth.v1.AuthProviderDescriptor + (*AuthProviderCatalogConfig)(nil), // 63: workflow.plugins.auth.v1.AuthProviderCatalogConfig + (*AuthProviderCatalogInput)(nil), // 64: workflow.plugins.auth.v1.AuthProviderCatalogInput + (*AuthProviderCatalogOutput)(nil), // 65: workflow.plugins.auth.v1.AuthProviderCatalogOutput + (*CredentialListInput)(nil), // 66: workflow.plugins.auth.v1.CredentialListInput + (*CredentialSummary)(nil), // 67: workflow.plugins.auth.v1.CredentialSummary + (*CredentialListOutput)(nil), // 68: workflow.plugins.auth.v1.CredentialListOutput + (*CredentialRevokeInput)(nil), // 69: workflow.plugins.auth.v1.CredentialRevokeInput + (*CredentialRevokeOutput)(nil), // 70: workflow.plugins.auth.v1.CredentialRevokeOutput + (*structpb.Struct)(nil), // 71: google.protobuf.Struct } var file_internal_contracts_auth_proto_depIdxs = []int32{ - 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 + 62, // 0: workflow.plugins.auth.v1.AuthAdminConfig.providers:type_name -> workflow.plugins.auth.v1.AuthProviderDescriptor + 43, // 1: workflow.plugins.auth.v1.AuthAdminDescribeInput.config:type_name -> workflow.plugins.auth.v1.AuthAdminConfig + 62, // 2: workflow.plugins.auth.v1.AuthAdminDescribeInput.providers:type_name -> workflow.plugins.auth.v1.AuthProviderDescriptor + 45, // 3: workflow.plugins.auth.v1.AuthAdminControl.options:type_name -> workflow.plugins.auth.v1.AuthAdminControlOption + 46, // 4: workflow.plugins.auth.v1.AuthAdminControlGroup.controls:type_name -> workflow.plugins.auth.v1.AuthAdminControl + 47, // 5: workflow.plugins.auth.v1.AuthAdminDescribeOutput.groups:type_name -> workflow.plugins.auth.v1.AuthAdminControlGroup + 71, // 6: workflow.plugins.auth.v1.AuthAdminDescribeOutput.effective_config:type_name -> google.protobuf.Struct + 71, // 7: workflow.plugins.auth.v1.AuthAdminDescribeOutput.methods_policy:type_name -> google.protobuf.Struct + 48, // 8: workflow.plugins.auth.v1.AuthAdminDescribeOutput.warnings:type_name -> workflow.plugins.auth.v1.AuthAdminDiagnostic + 71, // 9: workflow.plugins.auth.v1.AuthAdminValidateInput.desired_config:type_name -> google.protobuf.Struct + 62, // 10: workflow.plugins.auth.v1.AuthAdminValidateInput.providers:type_name -> workflow.plugins.auth.v1.AuthProviderDescriptor + 71, // 11: workflow.plugins.auth.v1.AuthAdminValidateOutput.accepted_config:type_name -> google.protobuf.Struct + 71, // 12: workflow.plugins.auth.v1.AuthAdminValidateOutput.methods_policy:type_name -> google.protobuf.Struct + 48, // 13: workflow.plugins.auth.v1.AuthAdminValidateOutput.errors:type_name -> workflow.plugins.auth.v1.AuthAdminDiagnostic + 48, // 14: workflow.plugins.auth.v1.AuthAdminValidateOutput.warnings:type_name -> workflow.plugins.auth.v1.AuthAdminDiagnostic + 71, // 15: workflow.plugins.auth.v1.OAuthExchangeOutput.raw_tokens:type_name -> google.protobuf.Struct + 71, // 16: workflow.plugins.auth.v1.OAuthUserinfoOutput.raw_claims:type_name -> google.protobuf.Struct + 59, // 17: workflow.plugins.auth.v1.AuthProviderConfigField.options:type_name -> workflow.plugins.auth.v1.AuthProviderConfigOption + 60, // 18: workflow.plugins.auth.v1.AuthProviderCapability.config_fields:type_name -> workflow.plugins.auth.v1.AuthProviderConfigField + 61, // 19: workflow.plugins.auth.v1.AuthProviderDescriptor.capabilities:type_name -> workflow.plugins.auth.v1.AuthProviderCapability + 62, // 20: workflow.plugins.auth.v1.AuthProviderCatalogConfig.providers:type_name -> workflow.plugins.auth.v1.AuthProviderDescriptor + 62, // 21: workflow.plugins.auth.v1.AuthProviderCatalogInput.providers:type_name -> workflow.plugins.auth.v1.AuthProviderDescriptor + 62, // 22: workflow.plugins.auth.v1.AuthProviderCatalogOutput.providers:type_name -> workflow.plugins.auth.v1.AuthProviderDescriptor + 48, // 23: workflow.plugins.auth.v1.AuthProviderCatalogOutput.warnings:type_name -> workflow.plugins.auth.v1.AuthAdminDiagnostic + 67, // 24: workflow.plugins.auth.v1.CredentialListOutput.credentials:type_name -> workflow.plugins.auth.v1.CredentialSummary + 25, // [25:25] is the sub-list for method output_type + 25, // [25:25] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_internal_contracts_auth_proto_init() } @@ -5880,7 +6532,7 @@ func file_internal_contracts_auth_proto_init() { 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: 64, + NumMessages: 71, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/contracts/auth.proto b/internal/contracts/auth.proto index 8741685..4802c1c 100644 --- a/internal/contracts/auth.proto +++ b/internal/contracts/auth.proto @@ -385,10 +385,12 @@ message AuthAdminConfig { string x_oauth_client_id = 30; string x_oauth_client_secret = 31; optional bool allow_insecure_test_oauth_endpoints = 32; + repeated AuthProviderDescriptor providers = 33; } message AuthAdminDescribeInput { AuthAdminConfig config = 1; + repeated AuthProviderDescriptor providers = 2; } message AuthAdminControlOption { @@ -442,6 +444,7 @@ message AuthAdminValidateConfig { message AuthAdminValidateInput { google.protobuf.Struct desired_config = 1; optional bool require_primary_method = 2; + repeated AuthProviderDescriptor providers = 3; } message AuthAdminValidateOutput { @@ -538,6 +541,65 @@ message OAuthUserinfoOutput { string error = 100; } +message AuthProviderConfigOption { + string value = 1; + string label = 2; + string description = 3; +} + +message AuthProviderConfigField { + string key = 1; + string label = 2; + string description = 3; + string help_text = 4; + string input_type = 5; + bool secret = 6; + bool required = 7; + repeated AuthProviderConfigOption options = 8; + string lookup = 9; + string validation_pattern = 10; +} + +message AuthProviderCapability { + string key = 1; + string label = 2; + string category = 3; + string description = 4; + bool supported = 5; + string disabled_reason = 6; + repeated string app_scopes = 7; + repeated string admin_read_scopes = 8; + repeated string admin_write_scopes = 9; + repeated AuthProviderConfigField config_fields = 10; +} + +message AuthProviderDescriptor { + string id = 1; + string label = 2; + string description = 3; + repeated string categories = 4; + string implementation = 5; + string version = 6; + string docs_url = 7; + string support_level = 8; + string disabled_reason = 9; + repeated AuthProviderCapability capabilities = 10; +} + +message AuthProviderCatalogConfig { + repeated AuthProviderDescriptor providers = 1; +} + +message AuthProviderCatalogInput { + repeated AuthProviderDescriptor providers = 1; +} + +message AuthProviderCatalogOutput { + repeated AuthProviderDescriptor providers = 1; + repeated AuthAdminDiagnostic warnings = 2; + string error = 100; +} + message CredentialListInput { string credentials_json = 1; } diff --git a/internal/plugin.go b/internal/plugin.go index 960dda9..a310b1c 100644 --- a/internal/plugin.go +++ b/internal/plugin.go @@ -36,6 +36,7 @@ var allStepTypes = []string{ "step.auth_policy_gate", "step.auth_methods_response", "step.auth_policy_audit", + "step.auth_provider_catalog", "step.auth_admin_config_describe", "step.auth_admin_config_validate", "step.auth_oauth_provider_config", @@ -140,6 +141,8 @@ 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_provider_catalog": + return newAuthProviderCatalogStep(name, config), nil case "step.auth_admin_config_describe": return newAuthAdminConfigDescribeStep(name, config), nil case "step.auth_admin_config_validate": @@ -235,6 +238,10 @@ 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_provider_catalog": + return sdk.NewTypedStepFactory(typeName, &contracts.AuthProviderCatalogConfig{}, &contracts.AuthProviderCatalogInput{}, typedLegacyStep[*contracts.AuthProviderCatalogConfig, *contracts.AuthProviderCatalogInput, *contracts.AuthProviderCatalogOutput](func(name string, config map[string]any) sdk.StepInstance { + return newAuthProviderCatalogStep(name, config) + }, &contracts.AuthProviderCatalogOutput{})).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) @@ -304,6 +311,7 @@ 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_provider_catalog", "AuthProviderCatalogConfig", "AuthProviderCatalogInput", "AuthProviderCatalogOutput"), 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"), diff --git a/internal/plugin_contracts_test.go b/internal/plugin_contracts_test.go index 5191a33..c22d8c0 100644 --- a/internal/plugin_contracts_test.go +++ b/internal/plugin_contracts_test.go @@ -55,6 +55,7 @@ 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_provider_catalog", "workflow.plugins.auth.v1.AuthProviderCatalogConfig", "workflow.plugins.auth.v1.AuthProviderCatalogInput", "workflow.plugins.auth.v1.AuthProviderCatalogOutput") 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") } @@ -205,6 +206,11 @@ func TestPasskeyAndTOTPContractsUseRuntimeMapKeys(t *testing.T) { requireProtoFields(t, &contracts.TOTPGenerateSecretInput{}, "email", "issuer") requireProtoFields(t, &contracts.TOTPGenerateSecretOutput{}, "secret", "provisioning_uri", "issuer", "account", "error") requireProtoFields(t, &contracts.TOTPRecoveryCodesOutput{}, "codes", "hashes", "error") + + requireProtoFields(t, &contracts.AuthProviderDescriptor{}, "id", "label", "categories", "capabilities", "disabled_reason") + requireProtoFields(t, &contracts.AuthProviderCapability{}, "key", "label", "category", "supported", "config_fields", "admin_read_scopes", "admin_write_scopes") + requireProtoFields(t, &contracts.AuthProviderConfigField{}, "key", "label", "input_type", "secret", "required", "options", "lookup") + requireProtoFields(t, &contracts.AuthProviderCatalogOutput{}, "providers", "warnings", "error") } func TestTypedPasskeyOutputsDecodeRuntimeKeys(t *testing.T) { diff --git a/internal/step_admin_config.go b/internal/step_admin_config.go index 779ab95..91b793d 100644 --- a/internal/step_admin_config.go +++ b/internal/step_admin_config.go @@ -42,7 +42,7 @@ func newAuthAdminConfigValidateStep(name string, config map[string]any) *authAdm 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) + source := mergePolicyInputs(s.config, runtimeConfig, current, desired) policy := buildAuthMethodsPolicy(source) var errors []map[string]any @@ -128,6 +128,23 @@ 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, ""), } + if providers := authProviderOAuthProviders(source); len(providers) > 0 { + for _, provider := range providers { + disabledReason := provider.DisabledReason + for _, capability := range provider.oauthCapabilities() { + if !capability.Supported && disabledReason == "" { + disabledReason = "provider is not enabled" + } + if capability.DisabledReason != "" { + disabledReason = capability.DisabledReason + } + for _, field := range capability.ConfigFields { + controls = append(controls, authAdminProviderControl(source, provider, capability, field, disabledReason)) + } + } + } + return controls + } for _, provider := range []struct { key string label string @@ -149,6 +166,36 @@ func buildOAuthAdminControls(source map[string]any) []map[string]any { return controls } +func authAdminProviderControl(source map[string]any, provider authProviderDescriptor, capability authProviderCapability, field authProviderConfigField, disabledReason string) map[string]any { + control := authAdminControl(source, field.Key, field.Label, field.InputType, field.Description, field.HelpText, field.Required, disabledReason) + control["secret"] = field.Secret || authAdminSecretKey(field.Key) + control["config_key"] = field.Key + control["configured"] = policyPresent(source, field.Key) + control["options"] = authAdminProviderControlOptions(field.Options) + if control["description"] == "" { + control["description"] = capability.Description + } + if control["description"] == "" { + control["description"] = provider.Description + } + if control["help_text"] == "" { + control["help_text"] = "Configure " + provider.Label + "." + } + return control +} + +func authAdminProviderControlOptions(options []authProviderConfigOption) []map[string]any { + out := make([]map[string]any, 0, len(options)) + for _, option := range options { + out = append(out, map[string]any{ + "value": option.Value, + "label": option.Label, + "description": option.Description, + }) + } + return out +} + 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, @@ -197,6 +244,9 @@ func validatePasskeyAdminConfig(source map[string]any) []map[string]any { } func validateOAuthAdminConfig(source map[string]any) []map[string]any { + if providers := authProviderOAuthProviders(source); len(providers) > 0 { + return validateDescriptorOAuthAdminConfig(source, providers) + } var errors []map[string]any for _, provider := range []string{"google", "facebook", "instagram", "x"} { if !authAdminOAuthProviderRequested(source, provider) { @@ -221,6 +271,52 @@ func validateOAuthAdminConfig(source map[string]any) []map[string]any { return errors } +func validateDescriptorOAuthAdminConfig(source map[string]any, providers []authProviderDescriptor) []map[string]any { + var errors []map[string]any + for _, provider := range providers { + if !authAdminDescriptorProviderRequested(source, provider) { + continue + } + disabledReason := provider.DisabledReason + for _, capability := range provider.oauthCapabilities() { + reason := disabledReason + if !capability.Supported && reason == "" { + reason = "provider is not enabled" + } + if capability.DisabledReason != "" { + reason = capability.DisabledReason + } + if reason != "" { + errors = append(errors, authAdminDiagnostic(provider.ID+"_oauth", "error", reason)) + continue + } + if !policyAnyStrictTrue(source, "auth_routes_enabled", "routes_enabled", "oauth_routes_enabled") { + errors = append(errors, authAdminDiagnostic("auth_routes_enabled", "error", provider.ID+" oauth requires auth routes to be enabled")) + } + for _, field := range capability.ConfigFields { + if field.Required && !policyPresent(source, field.Key) { + errors = append(errors, authAdminDiagnostic(field.Key, "error", provider.ID+" oauth requires "+field.Key)) + } + } + } + } + return errors +} + +func authAdminDescriptorProviderRequested(source map[string]any, provider authProviderDescriptor) bool { + if normalizeOAuthProvider(policyString(source, "oauth_provider")) == provider.ID { + return true + } + for _, capability := range provider.oauthCapabilities() { + for _, field := range capability.ConfigFields { + if policyPresent(source, field.Key) { + return true + } + } + } + return false +} + func authAdminOAuthProviderRequested(source map[string]any, provider string) bool { if normalizeOAuthProvider(policyString(source, "oauth_provider")) == provider { return true diff --git a/internal/step_methods_policy.go b/internal/step_methods_policy.go index 05b269d..36addfd 100644 --- a/internal/step_methods_policy.go +++ b/internal/step_methods_policy.go @@ -3,6 +3,7 @@ package internal import ( "context" "fmt" + "sort" "strconv" "strings" @@ -243,6 +244,9 @@ func countPrimaryPolicyMethods(output map[string]any) int { } func oauthPolicyProviders(source map[string]any) []string { + if providers := authProviderOAuthProviders(source); len(providers) > 0 { + return descriptorOAuthPolicyProviders(source, providers) + } provider := strings.ToLower(policyString(source, "oauth_provider")) if provider != "" && provider != "google" { return nil @@ -261,6 +265,46 @@ func oauthPolicyProviders(source map[string]any) []string { return []string{"google"} } +func descriptorOAuthPolicyProviders(source map[string]any, providers []authProviderDescriptor) []string { + if !policyAnyStrictTrue(source, "auth_routes_enabled", "routes_enabled", "oauth_routes_enabled") { + return nil + } + requested := normalizeOAuthProvider(policyString(source, "oauth_provider")) + enabled := make([]string, 0, len(providers)) + for _, provider := range providers { + if requested != "" && requested != provider.ID { + continue + } + if provider.DisabledReason != "" { + continue + } + if descriptorOAuthProviderReady(source, provider) { + enabled = append(enabled, provider.ID) + } + } + sort.Strings(enabled) + return enabled +} + +func descriptorOAuthProviderReady(source map[string]any, provider authProviderDescriptor) bool { + for _, capability := range provider.oauthCapabilities() { + if !capability.Supported || capability.DisabledReason != "" { + continue + } + ready := true + for _, field := range capability.ConfigFields { + if field.Required && !policyPresent(source, field.Key) { + ready = false + break + } + } + if ready { + return true + } + } + return false +} + func smsPolicyReady(source map[string]any) bool { if !policyAnyStrictTrue(source, "auth_routes_enabled", "routes_enabled") || !policyAnyStrictTrue(source, "sms_auth_enabled", "sms_enabled") { diff --git a/internal/step_provider_catalog.go b/internal/step_provider_catalog.go new file mode 100644 index 0000000..54d3793 --- /dev/null +++ b/internal/step_provider_catalog.go @@ -0,0 +1,423 @@ +package internal + +import ( + "context" + "fmt" + "sort" + "strings" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +type authProviderCatalogStep struct { + name string + config map[string]any +} + +func newAuthProviderCatalogStep(name string, config map[string]any) *authProviderCatalogStep { + return &authProviderCatalogStep{name: name, config: config} +} + +func (s *authProviderCatalogStep) Execute(_ context.Context, _ map[string]any, _ map[string]map[string]any, current, _, _ map[string]any) (*sdk.StepResult, error) { + merged := make(map[string]authProviderDescriptor) + var warnings []map[string]any + for _, source := range []map[string]any{s.config, current} { + for _, provider := range authProviderDescriptors(source) { + if provider.ID == "" { + continue + } + if existing, ok := merged[provider.ID]; ok { + if !authProviderDescriptorsCompatible(existing, provider) { + warnings = append(warnings, authAdminDiagnostic(provider.ID, "warning", "duplicate provider descriptor ignored")) + } else { + warnings = append(warnings, authAdminDiagnostic(provider.ID, "warning", "duplicate provider descriptor ignored")) + } + continue + } + merged[provider.ID] = provider + } + } + + providers := make([]authProviderDescriptor, 0, len(merged)) + for _, provider := range merged { + providers = append(providers, provider) + } + sort.Slice(providers, func(i, j int) bool { return providers[i].ID < providers[j].ID }) + + outputProviders := make([]map[string]any, 0, len(providers)) + for _, provider := range providers { + outputProviders = append(outputProviders, provider.toMap()) + } + return &sdk.StepResult{Output: map[string]any{ + "providers": outputProviders, + "warnings": warnings, + }}, nil +} + +type authProviderDescriptor struct { + ID string + Label string + Description string + Categories []string + Implementation string + Version string + DocsURL string + SupportLevel string + DisabledReason string + Capabilities []authProviderCapability +} + +type authProviderCapability struct { + Key string + Label string + Category string + Description string + Supported bool + DisabledReason string + AppScopes []string + AdminReadScopes []string + AdminWriteScopes []string + ConfigFields []authProviderConfigField +} + +type authProviderConfigField struct { + Key string + Label string + Description string + HelpText string + InputType string + Secret bool + Required bool + Options []authProviderConfigOption + Lookup string + ValidationPattern string +} + +type authProviderConfigOption struct { + Value string + Label string + Description string +} + +func authProviderDescriptors(source map[string]any) []authProviderDescriptor { + if source == nil { + return nil + } + var providers []authProviderDescriptor + for _, value := range []any{source["providers"], source["provider_descriptors"]} { + providers = append(providers, parseAuthProviderDescriptorList(value)...) + } + if catalog, ok := source["provider_catalog"].(map[string]any); ok { + providers = append(providers, parseAuthProviderDescriptorList(catalog["providers"])...) + } + return providers +} + +func parseAuthProviderDescriptorList(value any) []authProviderDescriptor { + switch typed := value.(type) { + case []map[string]any: + providers := make([]authProviderDescriptor, 0, len(typed)) + for _, item := range typed { + if provider := parseAuthProviderDescriptor(item); provider.ID != "" { + providers = append(providers, provider) + } + } + return providers + case []any: + providers := make([]authProviderDescriptor, 0, len(typed)) + for _, item := range typed { + if itemMap, ok := item.(map[string]any); ok { + if provider := parseAuthProviderDescriptor(itemMap); provider.ID != "" { + providers = append(providers, provider) + } + } + } + return providers + default: + return nil + } +} + +func parseAuthProviderDescriptor(values map[string]any) authProviderDescriptor { + provider := authProviderDescriptor{ + ID: providerString(values, "id"), + Label: providerString(values, "label"), + Description: providerString(values, "description"), + Categories: providerStringSlice(values, "categories"), + Implementation: providerString(values, "implementation"), + Version: providerString(values, "version"), + DocsURL: providerString(values, "docs_url"), + SupportLevel: providerString(values, "support_level"), + DisabledReason: providerString(values, "disabled_reason"), + } + provider.ID = strings.ToLower(strings.TrimSpace(provider.ID)) + if provider.Label == "" { + provider.Label = provider.ID + } + for _, item := range providerMapSlice(values["capabilities"]) { + capability := parseAuthProviderCapability(item) + if capability.Key != "" { + provider.Capabilities = append(provider.Capabilities, capability) + } + } + return provider +} + +func parseAuthProviderCapability(values map[string]any) authProviderCapability { + capability := authProviderCapability{ + Key: providerString(values, "key"), + Label: providerString(values, "label"), + Category: providerString(values, "category"), + Description: providerString(values, "description"), + Supported: providerBoolDefault(values, "supported", false), + DisabledReason: providerString(values, "disabled_reason"), + AppScopes: providerStringSlice(values, "app_scopes"), + AdminReadScopes: providerStringSlice(values, "admin_read_scopes"), + AdminWriteScopes: providerStringSlice(values, "admin_write_scopes"), + } + if capability.Label == "" { + capability.Label = capability.Key + } + for _, item := range providerMapSlice(values["config_fields"]) { + field := parseAuthProviderConfigField(item) + if field.Key != "" { + capability.ConfigFields = append(capability.ConfigFields, field) + } + } + return capability +} + +func parseAuthProviderConfigField(values map[string]any) authProviderConfigField { + field := authProviderConfigField{ + Key: providerString(values, "key"), + Label: providerString(values, "label"), + Description: providerString(values, "description"), + HelpText: providerString(values, "help_text"), + InputType: providerString(values, "input_type"), + Secret: providerBoolDefault(values, "secret", false), + Required: providerBoolDefault(values, "required", false), + Lookup: providerString(values, "lookup"), + ValidationPattern: providerString(values, "validation_pattern"), + } + if field.Label == "" { + field.Label = field.Key + } + if field.InputType == "" { + field.InputType = "text" + } + if field.HelpText == "" { + field.HelpText = field.Description + } + if field.HelpText == "" { + field.HelpText = fmt.Sprintf("Configure %s.", field.Label) + } + for _, item := range providerMapSlice(values["options"]) { + option := authProviderConfigOption{ + Value: providerString(item, "value"), + Label: providerString(item, "label"), + Description: providerString(item, "description"), + } + if option.Value != "" { + if option.Label == "" { + option.Label = option.Value + } + field.Options = append(field.Options, option) + } + } + return field +} + +func (p authProviderDescriptor) toMap() map[string]any { + capabilities := make([]map[string]any, 0, len(p.Capabilities)) + for _, capability := range p.Capabilities { + capabilities = append(capabilities, capability.toMap()) + } + return map[string]any{ + "id": p.ID, + "label": p.Label, + "description": p.Description, + "categories": append([]string(nil), p.Categories...), + "implementation": p.Implementation, + "version": p.Version, + "docs_url": p.DocsURL, + "support_level": p.SupportLevel, + "disabled_reason": p.DisabledReason, + "capabilities": capabilities, + } +} + +func (c authProviderCapability) toMap() map[string]any { + fields := make([]map[string]any, 0, len(c.ConfigFields)) + for _, field := range c.ConfigFields { + fields = append(fields, field.toMap()) + } + return map[string]any{ + "key": c.Key, + "label": c.Label, + "category": c.Category, + "description": c.Description, + "supported": c.Supported, + "disabled_reason": c.DisabledReason, + "app_scopes": append([]string(nil), c.AppScopes...), + "admin_read_scopes": append([]string(nil), c.AdminReadScopes...), + "admin_write_scopes": append([]string(nil), c.AdminWriteScopes...), + "config_fields": fields, + } +} + +func (f authProviderConfigField) toMap() map[string]any { + options := make([]map[string]any, 0, len(f.Options)) + for _, option := range f.Options { + options = append(options, map[string]any{ + "value": option.Value, + "label": option.Label, + "description": option.Description, + }) + } + return map[string]any{ + "key": f.Key, + "label": f.Label, + "description": f.Description, + "help_text": f.HelpText, + "input_type": f.InputType, + "secret": f.Secret, + "required": f.Required, + "options": options, + "lookup": f.Lookup, + "validation_pattern": f.ValidationPattern, + } +} + +func authProviderDescriptorsCompatible(a, b authProviderDescriptor) bool { + if a.ID != b.ID { + return false + } + if len(a.Capabilities) != len(b.Capabilities) { + return false + } + keys := make(map[string]struct{}, len(a.Capabilities)) + for _, capability := range a.Capabilities { + keys[capability.Key] = struct{}{} + } + for _, capability := range b.Capabilities { + if _, ok := keys[capability.Key]; !ok { + return false + } + } + return true +} + +func authProviderOAuthProviders(source map[string]any) []authProviderDescriptor { + providers := authProviderDescriptors(source) + oauthProviders := make([]authProviderDescriptor, 0, len(providers)) + for _, provider := range providers { + if provider.hasCategory("oauth2_oidc") || provider.hasCapabilityCategory("oauth2_oidc") { + oauthProviders = append(oauthProviders, provider) + } + } + return oauthProviders +} + +func (p authProviderDescriptor) hasCategory(category string) bool { + for _, candidate := range p.Categories { + if strings.EqualFold(strings.TrimSpace(candidate), category) { + return true + } + } + return false +} + +func (p authProviderDescriptor) hasCapabilityCategory(category string) bool { + for _, capability := range p.Capabilities { + if strings.EqualFold(strings.TrimSpace(capability.Category), category) { + return true + } + } + return false +} + +func (p authProviderDescriptor) oauthCapabilities() []authProviderCapability { + capabilities := make([]authProviderCapability, 0, len(p.Capabilities)) + for _, capability := range p.Capabilities { + if strings.EqualFold(strings.TrimSpace(capability.Category), "oauth2_oidc") { + capabilities = append(capabilities, capability) + } + } + return capabilities +} + +func providerString(values map[string]any, key string) string { + if values == nil { + return "" + } + switch typed := values[key].(type) { + case string: + return strings.TrimSpace(typed) + case fmt.Stringer: + return strings.TrimSpace(typed.String()) + default: + if typed == nil { + return "" + } + return strings.TrimSpace(fmt.Sprint(typed)) + } +} + +func providerStringSlice(values map[string]any, key string) []string { + value, ok := values[key] + if !ok { + return nil + } + switch typed := value.(type) { + case []string: + return append([]string(nil), typed...) + case []any: + out := make([]string, 0, len(typed)) + for _, item := range typed { + text := strings.TrimSpace(fmt.Sprint(item)) + if text != "" { + out = append(out, text) + } + } + return out + case string: + if strings.TrimSpace(typed) == "" { + return nil + } + return []string{strings.TrimSpace(typed)} + default: + return nil + } +} + +func providerBoolDefault(values map[string]any, key string, def bool) bool { + value, ok := values[key] + if !ok { + return def + } + switch typed := value.(type) { + case bool: + return typed + case string: + return strings.EqualFold(strings.TrimSpace(typed), "true") + default: + return def + } +} + +func providerMapSlice(value any) []map[string]any { + switch typed := value.(type) { + case []map[string]any: + return typed + case []any: + out := make([]map[string]any, 0, len(typed)) + for _, item := range typed { + if itemMap, ok := item.(map[string]any); ok { + out = append(out, itemMap) + } + } + return out + default: + return nil + } +} diff --git a/internal/step_provider_catalog_test.go b/internal/step_provider_catalog_test.go new file mode 100644 index 0000000..8317d32 --- /dev/null +++ b/internal/step_provider_catalog_test.go @@ -0,0 +1,217 @@ +package internal + +import ( + "context" + "slices" + "testing" +) + +func TestAuthProviderCatalogMergesAndDeduplicatesDescriptors(t *testing.T) { + step := newAuthProviderCatalogStep("catalog", map[string]any{ + "providers": []any{ + testOAuthProviderDescriptor("auth0", "Auth0", true, "", "auth0_oauth_client_id"), + testOAuthProviderDescriptor("auth0", "Auth0 duplicate", true, "", "auth0_oauth_client_id"), + testOAuthProviderDescriptor("entra", "Microsoft Entra ID", true, "", "entra_oauth_client_id"), + }, + }) + + result, err := step.Execute(context.Background(), nil, nil, map[string]any{ + "providers": []any{ + testOAuthProviderDescriptor("okta", "Okta", true, "", "okta_oauth_client_id"), + }, + }, nil, nil) + if err != nil { + t.Fatalf("catalog execute: %v", err) + } + + providers := authProviderDescriptors(map[string]any{"providers": result.Output["providers"]}) + if got := len(providers); got != 3 { + t.Fatalf("provider count = %d, want 3: %#v", got, result.Output["providers"]) + } + if !slices.Contains(providerIDs(providers), "auth0") || + !slices.Contains(providerIDs(providers), "entra") || + !slices.Contains(providerIDs(providers), "okta") { + t.Fatalf("provider ids = %v", providerIDs(providers)) + } + warnings, _ := result.Output["warnings"].([]map[string]any) + if len(warnings) == 0 { + t.Fatal("expected duplicate provider warning") + } +} + +func TestAuthAdminConfigDescribeUsesProviderDescriptors(t *testing.T) { + step := newAuthAdminConfigDescribeStep("admin", nil) + + result, err := step.Execute(context.Background(), nil, nil, map[string]any{ + "config": map[string]any{ + "auth_routes_enabled": true, + "auth0_oauth_client_id": "auth0-client", + "auth0_oauth_client_secret": "auth0-secret", + "auth0_oauth_redirect_url": "https://app.example.test/auth/auth0/callback", + "entra_oauth_client_id": "entra-client", + "entra_oauth_client_secret": "entra-secret", + "entra_oauth_redirect_url": "https://app.example.test/auth/entra/callback", + "google_oauth_client_id": "google-client", + "google_oauth_client_secret": "google-secret", + "google_oauth_redirect_url": "https://app.example.test/auth/google/callback", + }, + "providers": []any{ + testOAuthProviderDescriptor("auth0", "Auth0", true, "", "auth0_oauth_client_id"), + testOAuthProviderDescriptor("entra", "Microsoft Entra ID", true, "", "entra_oauth_client_id"), + }, + }, nil, nil) + if err != nil { + t.Fatalf("describe: %v", err) + } + + requireAdminControl(t, result.Output, "auth0_oauth_client_secret", adminControlWant{ + GroupKey: "oauth_providers", + Label: "Auth0 client secret", + InputType: "secret", + ConfigKey: "auth0_oauth_client_secret", + Secret: true, + Configured: true, + Enabled: true, + }) + requireAdminControl(t, result.Output, "entra_oauth_client_secret", adminControlWant{ + GroupKey: "oauth_providers", + Label: "Microsoft Entra ID client secret", + InputType: "secret", + ConfigKey: "entra_oauth_client_secret", + Secret: true, + Configured: true, + Enabled: true, + }) + if adminControlExists(t, result.Output, "google_oauth_client_secret") { + t.Fatal("google fallback control rendered despite descriptor catalog") + } +} + +func TestAuthAdminConfigValidateRejectsDisabledProviderDescriptor(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", + "auth_routes_enabled": true, + "auth0_oauth_client_id": "auth0-client", + "auth0_oauth_client_secret": "auth0-secret", + "auth0_oauth_redirect_url": "https://app.example.test/auth/auth0/callback", + "disabled_oauth_client_id": "disabled-client", + "disabled_oauth_client_secret": "disabled-secret", + "disabled_oauth_redirect_url": "https://app.example.test/auth/disabled/callback", + }, + "providers": []any{ + testOAuthProviderDescriptor("auth0", "Auth0", true, "", "auth0_oauth_client_id"), + testOAuthProviderDescriptor("disabled", "Disabled Provider", false, "provider is not enabled", "disabled_oauth_client_id"), + }, + }, nil, nil) + if err != nil { + t.Fatalf("validate: %v", err) + } + + assertBool(t, result.Output, "valid", false) + requireAdminDiagnostic(t, result.Output, "disabled_oauth", "provider is not enabled") +} + +func TestAuthAdminConfigValidateDefaultsMissingSupportedToDisabled(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", + "auth_routes_enabled": true, + "auth0_oauth_client_id": "auth0-client", + "auth0_oauth_client_secret": "auth0-secret", + "auth0_oauth_redirect_url": "https://app.example.test/auth/auth0/callback", + "missing_oauth_client_id": "missing-client", + "missing_oauth_client_secret": "missing-secret", + "missing_oauth_redirect_url": "https://app.example.test/auth/missing/callback", + }, + "providers": []any{ + testOAuthProviderDescriptor("auth0", "Auth0", true, "", "auth0_oauth_client_id"), + testOAuthProviderDescriptorWithoutSupported("missing", "Missing Supported"), + }, + }, nil, nil) + if err != nil { + t.Fatalf("validate: %v", err) + } + + assertBool(t, result.Output, "valid", false) + requireAdminDiagnostic(t, result.Output, "missing_oauth", "provider is not enabled") +} + +func testOAuthProviderDescriptor(id, label string, supported bool, disabledReason, firstField string) map[string]any { + prefix := id + "_oauth_" + return map[string]any{ + "id": id, + "label": label, + "categories": []any{"oauth2_oidc"}, + "description": label + " OIDC provider", + "capabilities": []any{ + map[string]any{ + "key": id + "_oidc_login", + "label": label + " OIDC login", + "category": "oauth2_oidc", + "supported": supported, + "disabled_reason": disabledReason, + "config_fields": []any{ + map[string]any{"key": firstField, "label": label + " client ID", "input_type": "text", "required": true}, + map[string]any{"key": prefix + "client_secret", "label": label + " client secret", "input_type": "secret", "secret": true, "required": true}, + map[string]any{"key": prefix + "redirect_url", "label": label + " redirect URL", "input_type": "url", "required": true}, + }, + }, + }, + } +} + +func testOAuthProviderDescriptorWithoutSupported(id, label string) map[string]any { + prefix := id + "_oauth_" + return map[string]any{ + "id": id, + "label": label, + "categories": []any{"oauth2_oidc"}, + "capabilities": []any{ + map[string]any{ + "key": id + "_oidc_login", + "label": label + " OIDC login", + "category": "oauth2_oidc", + "config_fields": []any{ + map[string]any{"key": prefix + "client_id", "label": label + " client ID", "input_type": "text", "required": true}, + map[string]any{"key": prefix + "client_secret", "label": label + " client secret", "input_type": "secret", "secret": true, "required": true}, + map[string]any{"key": prefix + "redirect_url", "label": label + " redirect URL", "input_type": "url", "required": true}, + }, + }, + }, + } +} + +func providerIDs(providers []authProviderDescriptor) []string { + ids := make([]string, 0, len(providers)) + for _, provider := range providers { + ids = append(ids, provider.ID) + } + return ids +} + +func adminControlExists(t *testing.T, output map[string]any, key string) bool { + 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 { + return true + } + } + } + return false +} diff --git a/plugin.contracts.json b/plugin.contracts.json index c50e14c..f01d02f 100644 --- a/plugin.contracts.json +++ b/plugin.contracts.json @@ -159,6 +159,14 @@ "input": "workflow.plugins.auth.v1.AuthMethodsPolicyInput", "output": "workflow.plugins.auth.v1.AuthPolicyAuditOutput" }, + { + "kind": "step", + "type": "step.auth_provider_catalog", + "mode": "strict", + "config": "workflow.plugins.auth.v1.AuthProviderCatalogConfig", + "input": "workflow.plugins.auth.v1.AuthProviderCatalogInput", + "output": "workflow.plugins.auth.v1.AuthProviderCatalogOutput" + }, { "kind": "step", "type": "step.auth_admin_config_describe", diff --git a/plugin.json b/plugin.json index 0a91029..a32ce77 100644 --- a/plugin.json +++ b/plugin.json @@ -63,6 +63,7 @@ "step.auth_policy_gate", "step.auth_methods_response", "step.auth_policy_audit", + "step.auth_provider_catalog", "step.auth_admin_config_describe", "step.auth_admin_config_validate", "step.auth_oauth_provider_config", @@ -97,6 +98,7 @@ "step.auth_policy_gate", "step.auth_methods_response", "step.auth_policy_audit", + "step.auth_provider_catalog", "step.auth_admin_config_describe", "step.auth_admin_config_validate", "step.auth_oauth_provider_config",