Skip to content

fix(platform-api): enforce OpenAPI scopes - #2942

Merged
Thushani-Jayasekera merged 6 commits into
wso2:mainfrom
Thushani-Jayasekera:fix-scopeval
Jul 30, 2026
Merged

fix(platform-api): enforce OpenAPI scopes#2942
Thushani-Jayasekera merged 6 commits into
wso2:mainfrom
Thushani-Jayasekera:fix-scopeval

Conversation

@Thushani-Jayasekera

@Thushani-Jayasekera Thushani-Jayasekera commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Root cause

1. The route pattern was never available (the reported bug)

// internal/middleware/authorization.go — before
pattern := r.Pattern          // always "" — the mux has not matched yet
requiredScopes, found := registry.Lookup(r.Method, path)
if !found || len(requiredScopes) == 0 {
    next.ServeHTTP(w, r)      // every scoped route lands here
    return
}

r.Pattern is populated by ServeMux during its own ServeHTTP. The enforcer sits
outside it (gohttpkit.Chain(chain...)(mux)), so it can never observe a pattern that
way.

This is a regression with a known origin. Commit a232853ac ("removing gin go
library from source", 26 Jun 2026) migrated this middleware off gin
. The gin version
read c.FullPath(), and gin's router.Use middleware runs inside the engine after
route matching, so it returned the matched template correctly:

// before a232853ac — worked, because gin middleware runs after routing
requiredScopes, found := registry.Lookup(c.Request.Method, c.FullPath())

The port substituted r.Pattern — the correct net/http equivalent — but the chain
now wraps the mux from outside, where that value is never set. Scope enforcement
worked before that commit and has been ineffective since.

Changes

Resolve the route pattern from the router

ScopeEnforcerConfig gains a Routes RouteMatcher field, satisfied by
*http.ServeMux. The enforcer asks the router which pattern the request will match,
without serving it. r.Pattern is still preferred when non-empty, so the middleware
keeps working if it is ever moved inside the router.

Fail closed on an unenforceable configuration

ScopeEnforcer now returns an error when scope validation is enabled but no registry
or no route matcher was supplied. The server refuses to start rather than degrading
to a pass-through (GO-AUTH-011).

Normalize path-parameter names out of the registry key

/gateways/{gatewayId} and /gateways/{apiId} now produce the same key. The wildcard
{p...} form stays distinct, since it matches a different set of paths.

Parse only real operations from a path item

Path-item parsing decodes only keys that are HTTP methods, ignoring parameters,
summary, $ref and friends.

Drop the legacy /api/v1/secrets alias

SecretHandler.RegisterRoutes now registers on constants.APIBasePath only, matching
every other handler. The integration tests and two stale DTO doc comments are migrated
to /api/v0.9. With the alias gone, deny-by-default has no exceptions on the public
base path.

Validate the spec↔route mapping at startup

New ValidateScopeRegistryRoutes probes every declared operation against the router.
If a declared operation resolves to a structurally different route, the real route
has no entry of its own and would be denied at runtime — so startup fails loudly
instead. A declared operation matching nothing is not an error: it is a dead registry
entry protecting a route that does not exist, and such requests already get a 404.


Testing

The hardening breaks nothing

internal/server/scope_route_coverage_test.go registers every real handler — core
plus event-gateway plugin — onto a mux and validates it against the shipped
resources/openapi.yaml and the plugin's embedded spec.

All 114 core operations and all 34 plugin operations map exactly, so no live
endpoint regresses to a 403 under deny-by-default. The same check runs at startup, so
future drift is caught at boot as well as at build time.

The plugin defects, proven end to end

internal/server/plugin_scope_enforcement_test.go covers defects 2 and 3 with three
tests, since neither was observable at runtime — both presented exactly like the
reported bug, as "no scope requirement found":

  • TestPluginSpecLoadsWithItsScopes — the plugin spec now yields 34 operations;
    before the parser fix it yielded an error, and therefore zero. The merged registry
    goes 114 → 148.

  • TestEveryPluginRouteResolvesToDeclaredScopes — all 32 registered plugin routes,
    probed as live requests through the real mux, resolve to a declared scope despite the
    spec saying {apiId} where the handlers register {webSubApiId}/{webBrokerApiId}.

  • TestScopeEnforcementOnRealPluginRoutes — drives the real enforcer over the real
    router and real merged registry
    on DELETE /api/v0.9/websub-apis/{...}, the exact
    route where spec and handler disagree on the parameter name:

    Scope presented Result
    (none) 403
    ap:websub_api:read 403
    ap:webbroker_api:delete 403
    ap:websub_api:delete admitted
    ap:websub_api:manage admitted
    ap:websub_api:* admitted

    next is a sentinel rather than the mux — handlers built with nil services would
    panic if a request reached one — but the mux is still the route matcher, so pattern
    resolution follows the production path.

The secrets alias removal

TestSecretsRoutesAreRegisteredOnTheBasePath asserts the five real routes are still
registered on /api/v0.9 and that each resolves to a pattern the registry has a
scope for, so removing the alias cannot have silently taken the real API with it.

The ~25 call sites in secret_integration_test.go now exercise /api/v0.9/secrets,
and the whole internal/handler suite passes against the new path.

Results

go build ./...     ok
go vet ./...       ok
go test ./...      ok   (all packages)

gofmt reports no issues on any file touched here.


Notes for reviewers

  1. Per-handler organization scoping was not audited here. Handlers derive the
    tenant from token claims independently of this middleware, so that control is
    untouched either way — but this PR makes no claim about which operations were
    reachable cross-tenant while enforcement was broken. Worth a separate pass before
    the advisory states a blast radius.

  2. go build -tags experimental is broken on a clean tree
    (api.OrganizationSubscription / api.OrganizationQuota undefined in
    plugins/eventgateway/plugin.go). Pre-existing and unrelated to this change, so
    left alone. The plugin path above is therefore compile-verified by the tests but not
    runnable end-to-end until that is fixed.

  3. The event-gateway plugin still serves its HMAC-secret routes from /api/v1
    (/api/v1/websub-apis/{id}/secrets). These are declared in its own spec and are
    properly enforced, so they are left as-is — but unifying the plugin onto
    /api/v0.9 is a reasonable follow-up.

  4. Nothing fails if the /api/v1/secrets alias is re-added. It would be a dead
    403 route rather than an unprotected one, since deny-by-default neutralizes any
    route with no declared scope. The security property holds; there is simply no early
    warning.

Behavioural changes to be aware of

  • /api/v1/secrets now returns 404. Callers must use /api/v0.9/secrets. No
    in-repo consumer was on the old path, but any external caller still pointing at it
    will break — worth calling out in release notes.
  • A route registered without an OpenAPI security block now returns 403 instead of
    being served unauthenticated. This is the intended hardening, but it is a behaviour
    change for any such route outside SkipPaths.
  • Startup now fails if scope validation is enabled with no registry/route matcher,
    or if a declared operation resolves to a structurally different registered route.

…and tests

- Changed endpoint paths in CreateSecretRequest and UpdateSecretRequest from /api/v1/secrets to /api/v0.9/secrets.
- Updated integration tests to reflect the new API version for all secret-related operations.
- Refactored SecretHandler to use a constant for the API base path, ensuring consistency across route registrations.
- Added validation to ensure scope registry routes match the registered routes, enhancing security checks.
@Thushani-Jayasekera Thushani-Jayasekera changed the title Added validation to ensure scope registry routes match the registered routes, enhancing security checks [AIWS + PlatformAPI] Added validation to ensure scope registry routes match the registered routes, enhancing security checks Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Secret endpoints now register only under the shared API base path. OpenAPI scope loading normalizes path parameters and validates route coverage. Scope enforcement resolves router patterns, bypasses validated paths, fails closed for undeclared routes, and validates configuration during startup.

Changes

Scope routing and authorization

Layer / File(s) Summary
Secret routes use the shared API base path
platform-api/internal/dto/secret.go, platform-api/internal/handler/secret.go, platform-api/internal/handler/secret_integration_test.go
Secret route documentation, registration, and integration requests now use /api/v0.9; the legacy /api/v1 alias is removed.
OpenAPI operations and route validation
platform-api/internal/middleware/openapi_scope_registry.go, platform-api/internal/middleware/openapi_scope_registry_test.go, platform-api/internal/middleware/scope_route_validation.go, platform-api/internal/middleware/scope_route_validation_test.go
Scope registry loading filters path-item metadata, normalizes parameter segments, exposes operations, and validates registry routes against router patterns.
Fail-closed authorization and server wiring
platform-api/internal/middleware/authorization.go, platform-api/internal/middleware/auth.go, platform-api/internal/middleware/scope_enforcer_test.go, platform-api/internal/server/server.go, platform-api/internal/server/*scope*test.go
ScopeEnforcer resolves routes through the mux, supports boundary-safe skip prefixes, rejects undeclared routes, validates enabled configuration, and is covered across core and plugin routes.
Product-defined authentication skip paths
platform-api/config/*, platform-api/internal/server/plugins.go, platform-api/README.md, kubernetes/helm/platform-api-helm-chart/*, common/authenticators/*
Operator configuration of auth.skip_paths is removed; built-in and plugin paths use centralized boundary-safe matching and validation, with startup checks and updated documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Server
  participant ScopeRegistry
  participant RouteMatcher
  participant ScopeEnforcer
  participant Handler
  Server->>ScopeRegistry: load merged OpenAPI scopes
  Server->>RouteMatcher: validate registered route patterns
  Server->>ScopeEnforcer: configure Routes and SkipPaths
  ScopeEnforcer->>RouteMatcher: resolve request route pattern
  ScopeEnforcer->>ScopeRegistry: look up normalized method and path
  ScopeEnforcer->>Handler: allow request or return 403
Loading

Possibly related PRs

Suggested reviewers: krishanx92, lasanthas, pubudu538, renuka-fernando, virajsalaka

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR has useful detail, but it does not follow the required template and omits several required sections. Add the template sections: Purpose, Goals, Approach, User stories, Documentation, Automation tests, Security checks, Samples, Related PRs, and Test environment.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 90.63% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: enforcing OpenAPI scopes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Thushani-Jayasekera Thushani-Jayasekera changed the title [AIWS + PlatformAPI] Added validation to ensure scope registry routes match the registered routes, enhancing security checks fix(platform-api): enforce OpenAPI scopes — ScopeEnforcer never matched a route Jul 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
platform-api/internal/middleware/openapi_scope_registry.go (1)

37-53: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Normalize ServeMux exact anchors separately from path parameters.

Since this module targets Go 1.26, ServeMux can use {$} as an exact-path anchor. The current regex also matches {$} and normalizes it to {}, so a route like /gateways/{$} would collide with ordinary /gateways/{gatewayId} entries in the scope registry. Update the normalization comment/exclusion to keep exact anchors distinct from named parameters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/middleware/openapi_scope_registry.go` around lines 37 -
53, Update normalizePathParams and its pathParamName pattern so the ServeMux
exact-path anchor `{$}` is excluded from parameter normalization and remains
distinct from named placeholders such as `{gatewayId}`. Revise the adjacent
comment to document this exclusion while preserving the existing wildcard
distinction and normalization behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform-api/internal/middleware/authorization.go`:
- Around line 109-113: Update hasPathPrefix, used by the authorization
middleware’s SkipPaths bypass, to normalize the request path and require either
an exact match or a segment-boundary match against each trimmed skip prefix;
preserve valid nested paths while preventing similarly prefixed routes such as
secrets-admin or health-probe-fake from bypassing authorization.

---

Nitpick comments:
In `@platform-api/internal/middleware/openapi_scope_registry.go`:
- Around line 37-53: Update normalizePathParams and its pathParamName pattern so
the ServeMux exact-path anchor `{$}` is excluded from parameter normalization
and remains distinct from named placeholders such as `{gatewayId}`. Revise the
adjacent comment to document this exclusion while preserving the existing
wildcard distinction and normalization behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0bb69b0f-e4c7-48bb-a45c-ce68ab796895

📥 Commits

Reviewing files that changed from the base of the PR and between f98e75a and fce28d2.

📒 Files selected for processing (11)
  • platform-api/internal/dto/secret.go
  • platform-api/internal/handler/secret.go
  • platform-api/internal/handler/secret_integration_test.go
  • platform-api/internal/middleware/authorization.go
  • platform-api/internal/middleware/openapi_scope_registry.go
  • platform-api/internal/middleware/openapi_scope_registry_test.go
  • platform-api/internal/middleware/scope_enforcer_test.go
  • platform-api/internal/middleware/scope_route_validation.go
  • platform-api/internal/middleware/scope_route_validation_test.go
  • platform-api/internal/server/scope_route_coverage_test.go
  • platform-api/internal/server/server.go

Comment thread platform-api/internal/middleware/authorization.go
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 28, 2026
Comment on lines +171 to +175
if p == "/" {
// A root prefix skips everything — preserved from the previous
// behaviour rather than silently narrowed here.
return true
}

@malinthaprasan malinthaprasan Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This means a prefix inside prefixes list is empty? The prefix list is the auth skip paths right? Does this mean if a segment in the skip paths is empty, we skip the whole validation? Not sure we allow empty paths in the skip list, but we ideally shouldn't do that if thats the case. We should fail that at the startup itself.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bdw, shall we also check if we really need to expose skip paths in the config toml? Ideally we don't need to do that, unless if there's a specific use case. Because this is the product's implementation and nobody who's configuring shouldn't need to change it. Having the explicit scope validation on/off config should be sufficient IMHO.

@Thushani-Jayasekera Thushani-Jayasekera Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because this is the product's implementation and nobody who's configuring shouldn't need to change it. Having the explicit scope validation on/off config should be sufficient IMHO.

  • 1 will remove allowing to configure.

Ideally we don't need to do that, unless if there's a specific use case.
Yes, we can remove.

@malinthaprasan malinthaprasan Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just to be sure, I mean, ONLY from config toml. External plugins may inject paths which they expose as skip auth. They are via plugins, not via the config toml.

@Thushani-Jayasekera Thushani-Jayasekera Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

changes commited

Make the auth skip-path list non-configurable

platform-api/config/config.go

  • Auth.SkipPaths retagged koanf:"-" — no longer bindable from config. The list now comes solely from DefaultConfig plus prefixes plugins declare via AuthSkipPaths().
  • LoadConfig fails startup if a config file still carries platform_api.auth.skip_paths (koanf ignores unknown keys, so a stale entry would otherwise look effective while doing nothing).
  • New exported ValidateAuthSkipPath — rejects "", /, non-/-prefixed paths, and ... Moved here from internal/server/plugins.go so config-sourced and plugin-sourced prefixes share one check.
  • validateAuthConfig now runs it over every SkipPaths entry.

platform-api/internal/middleware/authorization.go

  • Removed the p == "/" early-return in hasPathPrefix that let a root prefix bypass every route — now unreachable since / fails validation at startup.

Config surface / docs

  • config-template.toml: removed the 20-entry skip_paths block.
  • Helm values.yaml: removed auth.skipPaths; configmap.yaml: removed the with $auth.skipPaths template block.
  • README.md: dropped skip_paths from the [platform_api.auth] row; replaced the "setting it replaces the default list" note with an explanation that the list is built-in (a wrong entry is an auth bypass), that plugins declare their own prefixes, that scope_validation is the enforcement knob, and that a stale key fails startup.

Tests

  • TestLoadConfig_RemovedSkipPathsKey_Errors — stale key errors.
  • TestLoadConfig_SkipPathsDefaultsSurvive — built-in defaults (/health, /api/internal/v1/secrets) still populate.
  • Four new validateAuthConfig cases: /, empty, relative, traversal — all rejected.
  • TestLoadConfig_MultiFile_ArrayReplaceNotAppend retargeted from skip_paths to server.cors.allowed_origins, since the old key is no longer bindable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just to be sure, I mean, ONLY from config toml. External plugins may inject paths which they expose as skip auth. They are via plugins, not via the config toml.

Config TOML: removed. SkipPaths []string \koanf:"-" (config/config.go:171) — koanf can't populate it, and auth.skip_pathsin a config file aborts startup viaremovedConfigKeys.

Plugins: still supported. plugin.AuthSkipPathProvider (internal/plugin/plugin.go:111) is an optional interface a plugin implements. initPlugins collects each declared path, runs it through config.ValidateAuthSkipPath — startup aborts on an invalid/over-broad one (empty, no leading /, bare /, or containing ..) — and server.go:462 appends them to cfg.Auth.SkipPaths before the auth middleware is built, so the list is complete when the chain is assembled.

So the effective list is: built-in defaults (default_config.go:55) + plugin-declared prefixes. Operators get auth.authorization.enabled for on/off, not path editing.

One nuance worth knowing, since you said "external plugins": there are two plugin tiers, and only the internal tier can declare skip paths. The external tier deliberately cannot — TestInitPlugins_ExternalTierCannotDeclareAuthSkipPaths asserts that an external plugin with an AuthSkipPaths() method returning / contributes zero skip paths, because externalPlugin doesn't forward that interface. Middleware is forwarded for the external tier; skip paths are not. The asymmetry is intentional: external BeforePlatformChain middleware runs before auth and can't mark a request authenticated, whereas a skip path removes the auth check outright (GO-AUTH-004).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform-api/config/config.go`:
- Around line 692-704: Replace ValidateAuthSkipPath in
platform-api/config/config.go:692-704 with structural validation of exact router
patterns, including methods where applicable, rather than accepting broad path
prefixes. In platform-api/internal/server/plugins.go:119, validate each plugin
exemption against the mux’s registered route patterns before adding it to the
auth chain, and ensure auth exemptions match resolved route identities rather
than raw r.URL.Path prefixes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d354a490-274c-4c3f-9115-355b8423b26a

📥 Commits

Reviewing files that changed from the base of the PR and between fce28d2 and bae9ca5.

📒 Files selected for processing (14)
  • kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml
  • kubernetes/helm/platform-api-helm-chart/values.yaml
  • platform-api/README.md
  • platform-api/config/config-template.toml
  • platform-api/config/config.go
  • platform-api/config/config_multifile_test.go
  • platform-api/config/config_test.go
  • platform-api/internal/middleware/auth.go
  • platform-api/internal/middleware/authorization.go
  • platform-api/internal/middleware/openapi_scope_registry.go
  • platform-api/internal/middleware/scope_enforcer_test.go
  • platform-api/internal/server/plugin_scope_enforcement_test.go
  • platform-api/internal/server/plugins.go
  • platform-api/internal/server/scope_route_coverage_test.go
💤 Files with no reviewable changes (3)
  • kubernetes/helm/platform-api-helm-chart/templates/configmap.yaml
  • platform-api/config/config-template.toml
  • kubernetes/helm/platform-api-helm-chart/values.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • platform-api/internal/middleware/openapi_scope_registry.go
  • platform-api/internal/middleware/authorization.go
  • platform-api/internal/middleware/scope_enforcer_test.go

Comment thread platform-api/config/config.go
…tication middleware and add corresponding tests.
@Thushani-Jayasekera

Copy link
Copy Markdown
Contributor Author

Authorization settings were scattered across two places and one of them was the
wrong place. scope_validation sat on [platform_api.auth], while the
authorization mode and its role-to-scope mapping file sat under
[platform_api.auth.idp] — a section that otherwise describes only how a token
is verified against an identity provider's JWKS.

The server never read those two keys as IDP-specific, but the startup path did
gate them on auth.mode == "idp", so role-based authorization was unreachable
in external_token mode
purely because of where its keys lived — even though
a token minted by an enterprise IDP carries the same roles claim whether the
platform verifies it via JWKS or with a local public key.

This PR groups all authorization settings into one [platform_api.auth.authorization]
block that applies in every auth mode, mirroring the separation Kubernetes draws
between its authentication and authorization configs and Envoy draws between JWT
providers and rules.

Before / After

Config

# BEFORE
[platform_api.auth]
mode             = "idp"
scope_validation = true

[platform_api.auth.idp]
jwks_url        = "https://accounts.example.com/oauth2/jwks"
issuer          = ["https://accounts.example.com"]
validation_mode = "role"
role_mappings   = "/etc/platform-api/roles.yaml"
# AFTER
[platform_api.auth]
mode = "idp"

[platform_api.auth.authorization]
enabled       = true
mode          = "role"
role_mappings = "/etc/platform-api/roles.yaml"

[platform_api.auth.idp]
jwks_url = "https://accounts.example.com/oauth2/jwks"
issuer   = ["https://accounts.example.com"]

Key mapping

Before After
auth.scope_validation auth.authorization.enabled
auth.idp.validation_mode auth.authorization.mode
auth.idp.role_mappings auth.authorization.role_mappings

Helm values.yaml mirrors the same move: config.auth.scopeValidation and
config.auth.idp.validationMode / config.auth.idp.roleMappings become
config.auth.authorization.{enabled,mode,roleMappings}.

Go types

IDP now carries provider concerns only (name, jwks_url, issuer,
audience). A new config.Authorization struct holds Enabled, Mode, and
RoleMappings, exposed as cfg.Auth.Authorization, with config.AuthzModeScope
/ config.AuthzModeRole constants replacing the bare "scope" / "role"
string literals.

Behaviour changes

Old spellings fail startup, naming their replacement. koanf ignores unknown
keys, so a config left on an old spelling would silently lose the setting — for
validation_mode that means falling back to scope mode while the operator
believes role checks are active. All three removed keys (plus the previously
removed auth.skip_paths) now abort startup with an error naming the key that
replaced them. This is a breaking config change: existing config files and
Helm values must be updated.

Role mode now works in external_token mode. The role-scope map is loaded
based on the authorization mode alone, no longer gated on auth.mode == "idp".

Role mode is rejected with auth.mode = "file". File-based users carry
explicit scopes in [[platform_api.auth.file.users]], so there are no IDP roles
to expand — previously a silent no-op, now a startup error.

Role mode requires role_mappings. Previously, role mode with no mapping
file fell through to passthrough: raw IDP role names were used directly as scope
values, so a role happening to be named ap:secret:manage would grant that
scope — making the IDP's role naming an authorization decision. Now:

  • config validation refuses to start when mode = "role" and role_mappings is
    empty (or claim_mappings.roles is unset), and
  • resolvePlatformRoles returns no scopes for a nil map instead of the raw role
    names, so the passthrough path cannot be reached at runtime either.

Validation runs in every auth mode. The authorization block was previously
validated only inside validateIDPConfig, so an invalid validation_mode went
unchecked in the other two modes while the server still read it (GO-AUTH-011:
validate the effective outcome, not the field in isolation).

Defaults are unchanged. Authorization is enabled and scope-based by default;
mode = "" is accepted as scope mode.

@malinthaprasan @Krishanx92 shall I add this change in this PR. I think this improves clarity in authorization part. WDYT?

@Thushani-Jayasekera Thushani-Jayasekera changed the title fix(platform-api): enforce OpenAPI scopes — ScopeEnforcer never matched a route fix(platform-api): enforce OpenAPI scopes Jul 29, 2026
@Thushani-Jayasekera
Thushani-Jayasekera merged commit 8222d73 into wso2:main Jul 30, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants