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
16 changes: 11 additions & 5 deletions .github/workflows/devportal-integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@ permissions:

jobs:
rest-api-test:
name: REST API tests (${{ matrix.db }})
name: REST API tests (${{ matrix.db }}, ${{ matrix.mode }} mode)
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
db: [sqlite, postgres]
# auth.authorization.mode — the whole suite runs in each. "role" is the
# shipped default; "scope" is what an issuer minting dp:* scopes directly
# uses. A matrix dimension rather than the both-modes `make test-rest-api`
# target so the four combinations run in parallel and CI wall-clock stays
# where it was. See portals/api-portal/it/README.md "Authorization modes".
mode: [scope, role]
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -39,21 +45,21 @@ jobs:
- name: Build API Portal image
run: make -C portals/api-portal build

- name: Run REST API integration tests (${{ matrix.db }})
- name: Run REST API integration tests (${{ matrix.db }}, ${{ matrix.mode }} mode)
env:
PLATFORM_API_IMAGE: platform-api:it-api-portal
run: |
if [ "${{ matrix.db }}" = "postgres" ]; then
make -C portals/api-portal/it test-rest-api-postgres
make -C portals/api-portal/it test-rest-api-postgres-${{ matrix.mode }}
else
make -C portals/api-portal/it test-rest-api
make -C portals/api-portal/it test-rest-api-${{ matrix.mode }}
fi

- name: Upload test reports
uses: actions/upload-artifact@v4
if: always()
with:
name: rest-api-test-reports-${{ matrix.db }}
name: rest-api-test-reports-${{ matrix.db }}-${{ matrix.mode }}
path: portals/api-portal/it/reports/
retention-days: 7

Expand Down
6 changes: 0 additions & 6 deletions portals/api-portal/configs/config-template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,6 @@ pool_request_timeout_ms = 30000 # MSSQL only - per-query execution timeo
encryption_key = "" # 64-char hex — AES-256-GCM key for encrypting secrets at rest
session_secret = "" # 64-char hex — express-session signing secret

# Static shared-secret header for calling the API Portal's own REST API.
[api_portal.security.service_api_key]
enabled = true
header_name = "x-wso2-api-key"
value = ""

# =============================================================================
# AUTHENTICATION
# =============================================================================
Expand Down
6 changes: 3 additions & 3 deletions portals/api-portal/docs/api-portal-openapi-spec-v0.9.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5359,7 +5359,6 @@ components:
type: object
required:
- id
- labels
properties:
id:
type: string
Expand All @@ -5371,8 +5370,9 @@ components:
example: Partner APIs
labels:
type: array
minItems: 1
description: Label names to attach to the view.
description: >-
Label names to attach to the view. Optional — omit or pass an empty array to create a view with no
labels, which surfaces no APIs until labels are attached later via the update endpoint.
items:
type: string
example:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ curl -X GET "https://api.example.com/orders/v1/orders" \

## Revoke a Client ID

To remove a linked client ID, go to **Manage Keys** and click **Revoke keys** for that key manager. This only removes the local reference in the portal — it does not deregister or delete the OAuth application in the key manager, and any tokens already issued remain valid until they expire. To invalidate the OAuth application itself or revoke a specific token, use the key manager's own console or revoke endpoint.
To remove a linked client ID, go to **Manage Keys** and click **Remove keys** for that key manager. This only removes the local reference in the portal — it does not deregister or delete the OAuth application in the key manager, and any tokens already issued remain valid until they expire. To invalidate the OAuth application itself or revoke a specific token, use the key manager's own console or revoke endpoint.

---

Expand Down
49 changes: 40 additions & 9 deletions portals/api-portal/it/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
# under the License.
# --------------------------------------------------------------------

.PHONY: all test test-postgres test-rest-api test-rest-api-postgres open clean deps ensure-test-tag ensure-certs
.PHONY: all test test-postgres test-rest-api test-rest-api-scope test-rest-api-role \
test-rest-api-postgres test-rest-api-postgres-scope test-rest-api-postgres-role \
open clean deps ensure-test-tag ensure-certs

VERSION ?= $(shell cat ../VERSION 2>/dev/null | tr -d '[:space:]' || echo "0.0.1-SNAPSHOT")
DOCKER_REGISTRY ?= ghcr.io/wso2/api-platform
Expand Down Expand Up @@ -107,19 +109,48 @@ test-postgres: ensure-test-tag ensure-certs
docker compose -p $(IT_PROJECT_POSTGRES) -f docker-compose.test.postgres.yaml down -v --remove-orphans; \
exit $$EXIT

# Run the REST API integration test suite (Jest + Supertest) against SQLite.
# --- REST API suite (Jest + Supertest) -------------------------------------
#
# The whole suite runs twice, once per authorization mode, because
# auth.authorization.mode changes where a request's effective scopes come from:
#
# scope — the portal reads the token's own scope claim (what platform-api mints).
# role — the portal IGNORES that claim and expands the token's roles claim
# through its own grant table (configs/portal-roles-it.yaml). This is the
# SHIPPED DEFAULT in configs/config.toml.
#
# Both are real deployment configurations, so both must pass the same specs. The
# portal-side grant table mirrors platform-api's, which is what lets one set of
# expectations hold in either mode; auth/grant-table-parity.spec.js guards the mirror
# and auth/authorization-mode.spec.js covers the deliberate difference between them.
#
# `test-rest-api` runs both, sequentially. Use the -scope / -role targets to run one.
# Requires the API Portal image to be built first: make build (from portals/api-portal/).
test-rest-api: ensure-test-tag ensure-certs
@DOCKER_REGISTRY=$(DOCKER_REGISTRY) docker compose -p $(IT_PROJECT) -f docker-compose.test.yaml up api-portal rest-api-tests --abort-on-container-exit --exit-code-from rest-api-tests; \
test-rest-api:
@$(MAKE) test-rest-api-scope
@$(MAKE) test-rest-api-role

test-rest-api-scope: AUTH_MODE = scope
test-rest-api-role: AUTH_MODE = role
test-rest-api-scope test-rest-api-role: ensure-test-tag ensure-certs
@echo "==> REST API suite (SQLite, authorization mode = $(AUTH_MODE))"
@AUTH_MODE=$(AUTH_MODE) DOCKER_REGISTRY=$(DOCKER_REGISTRY) docker compose -p $(IT_PROJECT) -f docker-compose.test.yaml up api-portal rest-api-tests --abort-on-container-exit --exit-code-from rest-api-tests; \
EXIT=$$?; \
docker compose -p $(IT_PROJECT) -f docker-compose.test.yaml down -v --remove-orphans; \
AUTH_MODE=$(AUTH_MODE) docker compose -p $(IT_PROJECT) -f docker-compose.test.yaml down -v --remove-orphans; \
exit $$EXIT

# Run the REST API integration test suite (Jest + Supertest) against PostgreSQL.
test-rest-api-postgres: ensure-test-tag ensure-certs
@DOCKER_REGISTRY=$(DOCKER_REGISTRY) docker compose -p $(IT_PROJECT_POSTGRES) -f docker-compose.test.postgres.yaml up postgres api-portal rest-api-tests --abort-on-container-exit --exit-code-from rest-api-tests; \
# Same, against PostgreSQL.
test-rest-api-postgres:
@$(MAKE) test-rest-api-postgres-scope
@$(MAKE) test-rest-api-postgres-role

test-rest-api-postgres-scope: AUTH_MODE = scope
test-rest-api-postgres-role: AUTH_MODE = role
test-rest-api-postgres-scope test-rest-api-postgres-role: ensure-test-tag ensure-certs
@echo "==> REST API suite (PostgreSQL, authorization mode = $(AUTH_MODE))"
@AUTH_MODE=$(AUTH_MODE) DOCKER_REGISTRY=$(DOCKER_REGISTRY) docker compose -p $(IT_PROJECT_POSTGRES) -f docker-compose.test.postgres.yaml up postgres api-portal rest-api-tests --abort-on-container-exit --exit-code-from rest-api-tests; \
EXIT=$$?; \
docker compose -p $(IT_PROJECT_POSTGRES) -f docker-compose.test.postgres.yaml down -v --remove-orphans; \
AUTH_MODE=$(AUTH_MODE) docker compose -p $(IT_PROJECT_POSTGRES) -f docker-compose.test.postgres.yaml down -v --remove-orphans; \
exit $$EXIT

# Open Cypress interactive UI — runs against a LOCALLY running portal (not in Docker).
Expand Down
50 changes: 43 additions & 7 deletions portals/api-portal/it/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ Each suite can run against either **SQLite** (default, no external DB) or **Post
- **Cypress** — UI E2E test framework (headless Electron).
- **SQLite / PostgreSQL** — SQLite by default; the `-postgres` targets swap in a Postgres service.

## Authorization modes

`auth.authorization.mode` decides where a request's effective scopes come from, and the
REST suite runs **in full, once per mode**:

| Mode | Effective scopes come from | Grant table |
|---|---|---|
| `scope` | the token's own `scope` claim, as minted by platform-api | `configs/roles-platform-api-it.yaml` |
| `role` (shipped default) | expanding the token's `roles` claim — the scope claim is **ignored** | `configs/portal-roles-it.yaml` |

Both are real deployment configurations, so both must pass the same specs. That works
because the portal-side table mirrors platform-api's exactly, giving each IT account the
same grant either way. Two things keep that honest:

- **`rest-api/auth/grant-table-parity.spec.js`** fails if the two tables drift apart,
naming the role and the missing scopes — instead of surfacing as a puzzling 403 in
some unrelated spec. Regenerate the portal table after editing platform-api's.
- **`rest-api/auth/authorization-mode.spec.js`** covers the one deliberate divergence.
The `narrow` account's roles claim (`dp_narrow_it`) is granted the full developer scope
set by platform-api but read-only by the portal, so the *same token* creating an
application succeeds in scope mode and is refused in role mode. Each assertion runs in
exactly one mode; together they prove the scope claim really is ignored under role mode
rather than merged — i.e. a caller cannot widen a role's grant by getting extra scopes
from their issuer. No other spec uses that account.

Mode is selected by `AUTH_MODE`, which the compose fixture feeds to both the portal
(`APIP_AP_AUTH_AUTHORIZATION_MODE`) and the test process (`API_PORTAL_AUTH_MODE`) so they
cannot disagree. Cypress always runs in the default `scope` mode.

## Prerequisites

- Docker and Docker Compose
Expand Down Expand Up @@ -96,8 +125,12 @@ portals/api-portal/it/
|---------|-------------|
| `make test` | Run the Cypress UI suite headlessly (SQLite, CI-friendly) |
| `make test-postgres` | Run the Cypress UI suite headlessly (PostgreSQL) |
| `make test-rest-api` | Run the Jest REST API suite (SQLite) |
| `make test-rest-api-postgres` | Run the Jest REST API suite (PostgreSQL) |
| `make test-rest-api` | Run the Jest REST API suite (SQLite) — **both** authorization modes, sequentially |
| `make test-rest-api-scope` | Same, scope mode only |
| `make test-rest-api-role` | Same, role mode only (the shipped default) |
| `make test-rest-api-postgres` | Run the Jest REST API suite (PostgreSQL) — both modes |
| `make test-rest-api-postgres-scope` | Same, scope mode only |
| `make test-rest-api-postgres-role` | Same, role mode only |
| `make open` | Open the Cypress interactive UI against a locally running portal |
| `make deps` | Install Node dependencies (only needed for `make open`) |
| `make clean` | Remove test containers, volumes, and report artifacts |
Expand All @@ -115,8 +148,9 @@ You can also run both UI suites from the portal root: `make -C portals/api-porta
Both suites run automatically on pull requests that touch `portals/api-portal/**`,
via [`.github/workflows/devportal-integration-test.yml`](../../../.github/workflows/devportal-integration-test.yml):

- **`rest-api-test`** — builds the image and runs `make test-rest-api` /
`make test-rest-api-postgres` in an `sqlite` × `postgres` matrix.
- **`rest-api-test`** — builds the image and runs the suite in an
`sqlite` × `postgres` × `scope` × `role` matrix (four parallel jobs, via the
per-mode targets, so covering both authorization modes doesn't double wall-clock).
- **`ui-test`** — builds the image and runs `make test` (Cypress, SQLite).

Test reports (`it/reports/`) are uploaded as workflow artifacts on every run. The workflow
Expand All @@ -134,7 +168,7 @@ Defined in `ui/cypress/support/`:
|---------|-------------|
| `cy.visitPortal(path)` | Navigate to a path inside the default portal view |
| `cy.portalUrl(path)` | Build a URL under the default view without visiting it |
| `cy.apiRequest(method, path, options)` | `cy.request` wrapper that injects the IT API key header for admin-protected endpoints |
| `cy.apiRequest(method, path, options)` | `cy.request` wrapper that authenticates with the current session cookie plus the `X-CSRF-Token` header (call `cy.login()` first) |
| `cy.login(username, password)` | Perform a real login flow (see `support/commands/auth.js`) |
| `cy.logout()` | Log the current user out |
| `cy.createApplication(name)` / `cy.deleteApplication(name)` | Create/delete an application (see `support/commands/applications.js`) |
Expand Down Expand Up @@ -195,8 +229,10 @@ After a run, artifacts are available under `reports/`:
- **REST API suite** performs **real session logins** against `platform-api` using the
file-based users defined in `configs/config-platform-api-it.toml`
(`admin`/`admin`, `publisher`/`publisher`, `developer`/`developer`).
- **UI suite** uses both real login flows (`auth/` specs) and, for admin-protected REST
calls, an IT API key injected via the `x-wso2-api-key` header.
- **UI suite** uses real login flows throughout. Seeding and cleanup hooks call
`cy.login()` and then `cy.apiRequest`, which authenticates with the resulting session
cookie plus the `X-CSRF-Token` double-submit header. `testIsolation` clears cookies
between tests, so each `before`/`after` hook needs its own `cy.login()`.

## Adding New Tests

Expand Down
14 changes: 14 additions & 0 deletions portals/api-portal/it/configs/config-platform-api-it.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,17 @@ roles = ["dp_publisher_it"]
username = "developer"
password_hash = "$2y$10$jX3o2E5jF4i3EOgoyJ0k.uegbDYmsmFNDfIxnvcZgTNJifAPjgKKK"
roles = ["dp_developer_it"]

# Exists only to make the difference between authorization modes observable.
#
# Its roles claim is dp_narrow_it, which platform-api's grant table
# (roles-platform-api-it.yaml) gives the FULL developer scope set — so the token it
# receives carries dp:application:create in its scope claim. The portal's own table
# (portal-roles-it.yaml) grants dp_narrow_it read-only.
#
# So this one account can create an application in scope mode and is refused in role
# mode, which is what auth/authorization-mode.spec.js asserts. No other spec uses it.
[[platform_api.auth.file.users]]
username = "narrow"
password_hash = "$2b$10$87Aj.eQ6JU4WZ2SohdyZEOKRXfVVt7QPGwE9bwaYxk1WqYbF8uTR6"
roles = ["dp_narrow_it"]
Loading
Loading