Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ developer-portal-ui:
level: debug
platformApi:
insecure: true
security:
roleValidation: false
auth:
authorization:
# Relax page role-tier gating for local dev. REST scope enforcement
# (authorization.enabled) stays on — it is a separate switch.
pageRoleValidation: false
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,33 @@ data:

[api_portal.auth]
mode = {{ ternary "idp" "local" (ne $auth.idp.clientId "") | quote }}
role_validation = {{ $cfg.security.roleValidation }}

[api_portal.auth.claim_mappings]
organization = {{ $auth.claimMappings.organization | quote }}
roles = {{ $auth.claimMappings.roles | quote }}
groups = {{ $auth.claimMappings.groups | quote }}

{{- $authz := $auth.authorization }}
{{- if and (ne $authz.mode "scope") (ne $authz.mode "role") }}
{{- fail (printf "config.auth.authorization.mode must be \"scope\" or \"role\", got %q" $authz.mode) }}
{{- end }}
{{- if and (eq $authz.mode "role") (not $authz.roleToScopeMapping) }}
{{- fail "config.auth.authorization.mode is \"role\" but config.auth.authorization.roleToScopeMapping is empty — the portal would refuse to start" }}
{{- end }}
# Authorization — what a verified token may do. Rendered outside the idp block
# below because it applies in every auth mode: a token carries the same roles
# claim whether the portal verified it against a JWKS endpoint or against the
# Platform API's public key.
[api_portal.auth.authorization]
enabled = {{ $authz.enabled }}
mode = {{ $authz.mode | quote }}
role_to_scope_mapping = {{ $authz.roleToScopeMapping | quote }}
page_role_validation = {{ $authz.pageRoleValidation }}

[api_portal.auth.authorization.portal_roles]
admin = {{ $authz.portalRoles.admin | quote }}
subscriber = {{ $authz.portalRoles.subscriber | quote }}

[api_portal.auth.local]
platform_api_url = {{ $platformApiUrl | quote }}
public_key_path = {{ $auth.publicKeyPath | quote }}
Expand All @@ -108,11 +128,6 @@ data:
org_callback = {{ $auth.idp.orgCallback }}
silent_sso = {{ $auth.idp.silentSso }}
token_refresh_timeout_ms = {{ $auth.idp.tokenRefreshTimeoutMs | int }}

[api_portal.auth.idp.roles]
admin = {{ $auth.idp.roles.admin | quote }}
subscriber = {{ $auth.idp.roles.subscriber | quote }}
super_admin = {{ $auth.idp.roles.superAdmin | quote }}
{{- end }}

{{- $org := $cfg.organization }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ config:
level: debug
platformApi:
insecure: true
security:
# Relax role checks for local dev only (secure default is true).
roleValidation: false
auth:
authorization:
# Relax page role-tier gating for local dev. REST scope enforcement
# (authorization.enabled) stays on — it is a separate switch.
pageRoleValidation: false
35 changes: 28 additions & 7 deletions kubernetes/helm/developer-portal-ui-helm-chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,6 @@ config:

# --- Security ---
security:
# Enforce per-operation role checks on incoming tokens. Secure by default;
# override to false locally (values-local.yaml) only if a deployment
# genuinely cannot supply role claims yet.
roleValidation: true
# A shared API key some server-to-server callers present in a header.
serviceApiKey:
enabled: true
Expand Down Expand Up @@ -144,11 +140,36 @@ config:
orgCallback: false # redirect to the org's own landing page after login
silentSso: true
tokenRefreshTimeoutMs: 10000
# Map IDP role names to the portal's internal roles (idp mode).
roles:

# --- Authorization: what a verified token may do ---
# Outside auth.local/auth.idp on purpose — a token carries the same roles claim
# whichever way the portal verified it, so these apply in every auth mode.
authorization:
# Enforce each REST operation's declared dp:* scopes. Secure by default;
# false lets any authenticated caller through (development only).
enabled: true
# "role" (the default) expands the token's roles claim through
# roleToScopeMapping. "scope" reads the token's own scope claim instead — use
# it when the issuer mints dp:* scopes directly (an Asgardeo tenant registered
# via production/scripts/register_asgardeo_scopes.sh, or the Platform API in
# local auth mode).
mode: role
# Path to the role-to-scope grant table inside the container. REQUIRED in role
# mode — the chart refuses to render without it, since the portal would refuse
# to start. Defaults to the table baked into the image (WORKDIR /app). To change
# what a role grants, mount your own copy from a ConfigMap and point this at it.
roleToScopeMapping: ./resources/role-to-scope-mapping.yaml
# Gate portal pages on the caller's role tier (portalRoles below). Distinct
# from `enabled`, which governs the REST API — one switch for both would mean
# turning page gating off also silently turned REST enforcement off.
# This replaced config.security.roleValidation.
pageRoleValidation: false
# Which role name in the token's roles claim grants each page tier. Was
# auth.idp.roles, despite being read in local auth mode too. There were three
# tiers; the superAdmin one gated pages this portal does not serve and is gone.
portalRoles:
admin: admin
subscriber: Internal/subscriber
superAdmin: superAdmin

organization:
handle: default # URL slug: /{handle}/views/{viewName}; required
Expand Down
11 changes: 8 additions & 3 deletions portals/api-portal/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ dist: clean-dist ## Build standalone API Portal distribution zip
@mkdir -p $(DIST_DIR)/configs
@mkdir -p $(DIST_DIR)/resources/api-portal/db-scripts
@cp -R database/* $(DIST_DIR)/resources/api-portal/db-scripts
# Both components ship a role-to-scope-mapping.yaml, so each goes under its own
# resources/<component>/ dir (matching db-scripts) rather than colliding on one
# resources/role-to-scope-mapping.yaml. The in-container mount paths are unchanged.
@cp resources/role-to-scope-mapping.yaml $(DIST_DIR)/resources/api-portal/role-to-scope-mapping.yaml
@mkdir -p $(DIST_DIR)/resources/platform-api/db-scripts
@mkdir -p $(DIST_DIR)/resources/samples
@cp -R samples/apis $(DIST_DIR)/resources/samples/
Expand All @@ -285,12 +289,12 @@ ifeq ($(PLATFORM_API_FROM_TAG),true)
@git -C ../.. show "$(PLATFORM_API_TAG):platform-api/config/config-template.toml" \
> $(DIST_DIR)/configs/.pa-config-template.toml
@git -C ../.. show "$(PLATFORM_API_TAG):platform-api/resources/role-to-scope-mapping.yaml" \
> $(DIST_DIR)/resources/role-to-scope-mapping.yaml
> $(DIST_DIR)/resources/platform-api/role-to-scope-mapping.yaml
else
@cp ../../platform-api/internal/database/schema.*.sql $(DIST_DIR)/resources/platform-api/db-scripts/
@cp ../../platform-api/config/config.toml $(DIST_DIR)/configs/.pa-config.toml
@cp ../../platform-api/config/config-template.toml $(DIST_DIR)/configs/.pa-config-template.toml
@cp ../../platform-api/resources/role-to-scope-mapping.yaml $(DIST_DIR)/resources/role-to-scope-mapping.yaml
@cp ../../platform-api/resources/role-to-scope-mapping.yaml $(DIST_DIR)/resources/platform-api/role-to-scope-mapping.yaml
endif
# Require a [platform_api] root table — pre-unified configs would merge into a broken file.
@if ! grep -q '^\[platform_api' $(DIST_DIR)/configs/.pa-config.toml; then \
Expand All @@ -311,7 +315,8 @@ endif
@rm -f $(DIST_DIR)/configs/.pa-config.toml $(DIST_DIR)/configs/.pa-config-template.toml $(DIST_DIR)/configs/.aiw-config-template.toml
# Point the platform-api mount at the merged config so both containers share one file.
@sed -e 's#\.\./\.\./platform-api/config/config\.toml:#./configs/config.toml:#' \
-e 's#\.\./\.\./platform-api/resources/role-to-scope-mapping\.yaml:#./resources/role-to-scope-mapping.yaml:#' \
-e 's#\.\./\.\./platform-api/resources/role-to-scope-mapping\.yaml:#./resources/platform-api/role-to-scope-mapping.yaml:#' \
-e 's#\./resources/role-to-scope-mapping\.yaml:#./resources/api-portal/role-to-scope-mapping.yaml:#' \
docker-compose.yaml > $(DIST_DIR)/docker-compose.yaml
@cp distribution/README.md $(DIST_DIR)/README.md
@mkdir -p $(DIST_DIR)/scripts
Expand Down
58 changes: 41 additions & 17 deletions portals/api-portal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,19 @@ For quick exploration without an IdP, the portal delegates credential validation
[[platform_api.auth.file.users]]
username = "admin"
password_hash = "$2y$10$..." # bcrypt hash — generate with: htpasswd -bnBC 12 "" <pw> | tr -d ':\n'
roles = ["ap_admin"] # grants dp:organization:manage, dp:api:manage, … — see role-to-scope-mapping.yaml
roles = ["ap_admin"] # grants dp:organization:manage, dp:api:manage, … — see platform-api's role-to-scope-mapping.yaml
```

To change what a portal user may do, edit that role's entry in `role-to-scope-mapping.yaml` — or name a second role alongside it — rather than listing scopes on the user block.
To change what a portal user may do, edit that role's entry in [platform-api's `role-to-scope-mapping.yaml`](../../platform-api/resources/role-to-scope-mapping.yaml) — or name a second role alongside it — rather than listing scopes on the user block.

Note there are two files with this name, read by different components in different modes:

| File | Read by | When |
|---|---|---|
| [`platform-api/resources/role-to-scope-mapping.yaml`](../../platform-api/resources/role-to-scope-mapping.yaml) | Platform API | Local auth — expands a file user's `roles` into the `scope` claim of the token it issues (roles named `ap_*`) |
| [`resources/role-to-scope-mapping.yaml`](resources/role-to-scope-mapping.yaml) | This portal | `auth.authorization.mode = "role"` — expands an incoming token's roles claim on every request (roles named `dp_admin`, `dp_subscriber`) |

Local auth uses the first; an external IDP in role mode uses the second. See [Authorization](docs/administer/authentication.md#authorization).
Comment thread
Piumal1999 marked this conversation as resolved.

The portal config (or `APIP_AP_AUTH_LOCAL_*` env vars) must point to the Platform API. `config.toml`'s own defaults assume Docker Compose, where `platform-api` is a resolvable hostname on the compose network — `npm run start:local` already overrides `platform_api_url` to `https://localhost:9243` (the sidecar's port published to the host) and `tls_skip_verify = true` (self-signed cert), so no manual edit is needed for that flow:

Expand All @@ -277,30 +286,45 @@ organization is refused.

### Environment variable overrides

Every config key can be overridden with an `APIP_AP_*` environment variable. You can place these in a `.env` file at the project root.
There is **no** automatic `APIP_AP_*` override layer. A variable takes effect only where
`configs/config.toml` explicitly references it with a `{{ env "NAME" "fallback" }}` token
— the same design platform-api uses (see `src/config/configLoader.js`). Setting a
variable no key references does nothing, silently.
Comment thread
Piumal1999 marked this conversation as resolved.

**Convention:**
- Prefix: `APIP_AP_`
- `_` separates nesting levels (one token = one config object level)
- `__` represents a literal underscore within a key name
- Tokens are matched case-insensitively against config keys (matched against the camelCase struct produced from the TOML's snake_case keys)
Following `platform-api/config/config.toml`, tokens are used sparingly: a key gets one
only where something actually drives it — the Compose database overrides,
`npm run start:local`, `docker-entrypoint.sh`, or a secret. Everything else is a plain
literal, so the file states its own effective configuration.

These are the variables the shipped `configs/config.toml` honours:

| Env var | Config path |
|---------|-------------|
| `APIP_AP_SERVER_PORT` | `config.server.port` |
| `APIP_AP_SERVER_HTTPS_ENABLED` | `config.server.https.enabled` |
| `APIP_AP_LOGGING_LEVEL` | `config.logging.level` |
| `APIP_AP_DATABASE_DRIVER` | `config.database.driver` |
| `APIP_AP_DATABASE_PATH` | `config.database.path` |
| `APIP_AP_DATABASE_HOST` | `config.database.host` |
| `APIP_AP_DATABASE_PORT` | `config.database.port` |
| `APIP_AP_SERVER_HTTPS_ENABLED` | `config.server.https.enabled` |
| `APIP_AP_IDP_CLIENTID` | `config.auth.idp.clientId` |
| `APIP_AP_IDP_ISSUER` | `config.auth.idp.issuer` |
| `APIP_AP_SERVER_PORT` | `config.server.port` |
| `APIP_AP_SERVER_BASE_URL` | `config.server.baseUrl` |
| `APIP_AP_DATABASE_SSL_MODE` | `config.database.sslMode` |

`.env` example:
| `APIP_AP_DATABASE_NAME` | `config.database.name` |
| `APIP_AP_DATABASE_USER` | `config.database.user` |
| `APIP_AP_DATABASE_PASSWORD` | `config.database.password` |
| `APIP_AP_AUTH_LOCAL_PLATFORM_API_URL` | `config.auth.local.platformApiUrl` |
| `APIP_AP_AUTH_LOCAL_PUBLIC_KEY_PATH` | `config.auth.local.publicKeyPath` |
| `APIP_AP_AUTH_LOCAL_TLS_SKIP_VERIFY` | `config.auth.local.tlsSkipVerify` |
| `APIP_AP_ORGANIZATION_HANDLE` | `config.organization.handle` |
| `APIP_AP_ORGANIZATION_DISPLAY_NAME` | `config.organization.displayName` |

To make any other key settable from the environment, add the token to `config.toml`
yourself. To change something without an environment variable — including the
`[api_portal.auth.authorization]` block and the IDP settings — edit `config.toml`, or
layer a thin overlay with a second `--config` flag.

`.env` example (loaded from `api-platform.env` at the project root):
```dotenv
APIP_AP_DATABASE_HOST=my-postgres-host
APIP_AP_DATABASE_PASSWORD=my-secret-password
APIP_AP_IDP_CLIENTID=my-client-id
```

---
Expand Down
70 changes: 62 additions & 8 deletions portals/api-portal/configs/config-template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,15 @@ value = ""
# =============================================================================
# AUTHENTICATION
# =============================================================================
# A mode gate plus the two backends it selects between: local (the default) and
# idp. Configure the block matching your chosen mode; the other is ignored.
# HOW a token is verified: a mode gate plus the two backends it selects between,
# local (the default) and idp. Configure the block matching your chosen mode; the
# other is ignored. What a verified token may DO is authorization, configured in
# its own mode-independent section further below.

[api_portal.auth]
# "local" — username/password validated against the Platform API control plane
# ([api_portal.auth.local] below). "idp" — external OIDC IDP (auth.idp below).
mode = "local" # local | idp
role_validation = false # Enforce per-operation role validation

# JWT claim name mappings — which token claim carries each field.
# Dot-notation supported for nested claims (e.g. "realm_access.roles").
Expand Down Expand Up @@ -145,11 +146,64 @@ token_refresh_timeout_ms = 10000
silent_sso = true # Enable silent SSO
org_callback = false # Redirect to the org's own landing page after login

# Maps IDP role names to API Portal's internal roles.
[api_portal.auth.idp.roles]
admin = "admin"
subscriber = "Internal/subscriber"
super_admin = "superAdmin"
# =============================================================================
# AUTHORIZATION
# =============================================================================
# What a VERIFIED token may do. Deliberately its own section, outside both
# auth.local and auth.idp: a token carries the same roles claim whether the portal
# verified it against a JWKS endpoint or against the Platform API's public key, so
# these settings apply in every auth mode.
#
# Retired keys (startup fails if either is still present, because an ignored key
# would silently apply the default instead of what the file says):
# auth.role_validation -> auth.authorization.page_role_validation
# auth.idp.roles -> auth.authorization.portal_roles

[api_portal.auth.authorization]
# Master switch for REST API (/api/v0.9) authorization. false lets any
# authenticated caller satisfy every operation's declared scope list — an explicit
# development opt-out that logs a warning at startup.
enabled = true
# How a REST request's effective scopes are derived:
# "role" — (the DEFAULT) by expanding the token's roles claim
# (auth.claim_mappings.roles) through role_to_scope_mapping below. Works
# for every issuer: an external IDP emits the roles its estate is
# organized around and has no reason to mint dp:* scopes, and the
# Platform API mints its own ap_* role names, which the shipped table
# aliases. The scope claim is ignored entirely in this mode, so a caller
# cannot widen a role's grant by requesting extra scopes.
# "scope" — from the token's own scope claim. Use this when the issuer mints dp:*
# scopes directly: an Asgardeo tenant set up with
# production/scripts/register_asgardeo_scopes.sh (which registers all
# dp:* scopes and lets you attach them to an Asgardeo role), or the
# Platform API in local auth mode.
# Validated even when enabled = false, so a typo surfaces when it is written rather
# than when enforcement is switched back on.
mode = "role" # scope | role
# Path to the YAML role-to-scope grant table. REQUIRED when mode = "role", which is
# the default — hence a real default rather than an empty string. The shipped table is
# baked into the image at ./resources (and resolves from the project root for
# `npm start`); docker-compose.yaml overrides this to the mounted /etc/api-portal copy
# so operators can edit what a role grants without rebuilding.
# Loaded and validated at startup whenever it is set (regardless of mode) against the
# portal's OpenAPI spec: an undeclared dp:* scope fails startup rather than surfacing
# later as a role that logs in fine and is denied every request.
role_to_scope_mapping = "./resources/role-to-scope-mapping.yaml"
# Per-page role-tier gating: requires the caller's roles claim to name the tier a
# page demands (portal_roles below). Separate from `enabled` above, which governs
# REST scopes — one switch for both would mean turning page gating off also
# silently turned REST scope enforcement off.
page_role_validation = false

# Which role name, as it appears in the token's roles claim, grants each of the
# portal's two page-access tiers. Was [api_portal.auth.idp.roles], despite being
# read in local auth mode too. (There was a third tier, super_admin; it gated pages
# this portal does not serve, so it guarded nothing and was removed.) Point these at your IDP's role names — or at the role
# names in role_to_scope_mapping (e.g. admin = "dp_admin") to drive page gating and
# REST authorization from the same roles.
[api_portal.auth.authorization.portal_roles]
admin = "ap_admin"
subscriber = "ap_subscriber"

# =============================================================================
# PAGE ACCESS RULES
Expand Down
Loading
Loading