diff --git a/p1-task-manager/docs/assets/authorization-model.webp b/p1-task-manager/docs/assets/authorization-model.webp deleted file mode 100644 index c071854..0000000 Binary files a/p1-task-manager/docs/assets/authorization-model.webp and /dev/null differ diff --git a/p1-task-manager/docs/assets/enforcement.webp b/p1-task-manager/docs/assets/enforcement.webp deleted file mode 100644 index a4a93af..0000000 Binary files a/p1-task-manager/docs/assets/enforcement.webp and /dev/null differ diff --git a/p1-task-manager/docs/assets/expected-outcomes.webp b/p1-task-manager/docs/assets/expected-outcomes.webp deleted file mode 100644 index 8b53aa3..0000000 Binary files a/p1-task-manager/docs/assets/expected-outcomes.webp and /dev/null differ diff --git a/p1-task-manager/docs/assets/missing-authorization.webp b/p1-task-manager/docs/assets/missing-authorization.webp deleted file mode 100644 index 11bbb42..0000000 Binary files a/p1-task-manager/docs/assets/missing-authorization.webp and /dev/null differ diff --git a/p1-task-manager/docs/assets/reusable-pattern.webp b/p1-task-manager/docs/assets/reusable-pattern.webp deleted file mode 100644 index 5af80d9..0000000 Binary files a/p1-task-manager/docs/assets/reusable-pattern.webp and /dev/null differ diff --git a/p1-task-manager/docs/tutorials.md b/p1-task-manager/docs/tutorials.md index c335ac0..bf82871 100644 --- a/p1-task-manager/docs/tutorials.md +++ b/p1-task-manager/docs/tutorials.md @@ -5,7 +5,7 @@ summary: Secure a task manager with token-based server decisions and conservativ order: 20 socialImage: ./assets/social-card.webp socialImageAlt: Browser guidance and Fastify server enforcement with embedded Cedarling for task actions. -lastVerified: 2026-10-01T09:46:20Z +lastVerified: 2026-10-07T19:56:00Z --- # Protect a Node.js REST API with Cedarling @@ -29,7 +29,11 @@ the controls; direct API requests will face the same server checks. ## Build the integration or try the finished app - To build the integration, start with [Start the baseline app](#start-the-baseline-app). We'll add policies and server enforcement, then browser controls. -- To try the finished app, clone and run the [complete tagged project](https://github.com/GluuFederation/cedarling-tutorials/tree/p1-task-manager-v1.0.1/p1-task-manager) using its README, then go to [Check allowed and denied operations](#check-allowed-and-denied-operations). This version should already deny the operation we'll reproduce in the baseline. +- To try the finished app, clone and run the [finished project on main](https://github.com/GluuFederation/cedarling-tutorials/tree/main/p1-task-manager) using its README, then go to [Check allowed and denied operations](#check-allowed-and-denied-operations). This version should already deny the operation we'll reproduce in the baseline. + +If you're building from the starting project, open each **Required step** section +and complete its instructions before continuing. These sections contain the files +and changes we'll need.
What you'll need @@ -41,8 +45,10 @@ the controls; direct API requests will face the same server checks.
-Copy whole files from GitHub's raw-file view; the short examples aren't complete -replacements. Create missing parent directories. Code paths and commands are +At each copying step, open the linked file on GitHub, choose **Raw**, and copy +its full contents into the stated destination in your baseline checkout. The +short examples explain the parts we'll focus on. Create missing parent +directories first. Code paths and commands are relative to `p1-task-manager/`; repository-level `shared/` files go one directory above it. @@ -69,20 +75,6 @@ assurance standard has been met. We'll use three sample accounts: _Each decision depends on the user's facts and the action and resource involved. A role or tenant label alone doesn't settle it._ -```mermaid -flowchart TD - accTitle: Task authorization in the completed application - accDescr: The API asks its embedded Cedarling instance before accessing tasks. Browser evaluation guides controls but cannot grant server permission. - IdP["Tutorial IdP"] -->|"Signed token in server session"| API["Fastify API: current user and task facts"] - UI["React task board"] -->|"Task request"| API - API --> PDP["Embedded Cedarling instance"] - PDP --> Check["API enforces decision"] - Check -->|"ALLOW"| Data["Read or write SQLite tasks"] - Check -->|"DENY or error"| Stop["No protected effect"] - API -->|"Safe facts, ceiling and policy archive"| Browser["Browser Cedarling: unsigned evaluation"] - Browser -->|"Available controls"| UI -``` - Cedarling is the policy decision point (PDP). The API handlers are the policy enforcement points (PEPs): they act on Cedarling's decisions. The browser also evaluates rules to show the right controls, but the server checks permission @@ -90,9 +82,8 @@ again before releasing data or changing a task. ## Try the app before adding Cedarling -![In the permissive starting application, Alex creates a task successfully.](./assets/missing-authorization.webp) - -_Alex's creation request succeeds in the starting app. We'll reproduce it below._ +Let's first see what Alex can do without those permission checks. We'll send +a creation request directly to the API, then keep it for comparison after integration. ### Start the baseline app @@ -134,7 +125,8 @@ password, such as `cedarling-is-awesome`, then approve access. ### Create a task as Alex -Open developer tools on the task manager page and run this in the console: +With Alex signed in, open developer tools on the task manager page and run this +in the console: ```js const session = await fetch("/api/session").then((r) => r.json()); @@ -165,7 +157,7 @@ the first-terminal commands above before continuing. ## Where should we check permission? -Open the baseline's +Alex's task was saved, so let's find the point that should have stopped it. Open the baseline's [`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p1-task-manager/src/server/app.ts) and find `POST /api/tasks`. It authenticates the session, checks request integrity, validates the input, then calls `database.createTask()`. None of @@ -182,10 +174,8 @@ permissions for each operation. ## Decide who can do what -![Mina creates a task inside Tenant A; Alex edits an existing task assigned to him in the same tenant.](./assets/authorization-model.webp) - -_`Create` targets the existing tenant; `Edit` targets a current task.[^2] The policy -store defines the request types and rules used for both decisions._ +We know where the check belongs. Now we'll define what it should allow, starting +with creation and extending the rules to the other task operations. ### List the rules for each task action @@ -194,12 +184,12 @@ For existing tasks, "related" means the user owns the task or is its assignee. | Capability | Action | Resource | Additional conditions | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------- | -| `task.view` | [`View`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L8 "server-view-related-task") | `Task` | Related user | -| `task.create` | [`Create`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L33 "server-owner-create-task") | `Tenant` | Owner role, assurance at least 2 | -| `task.edit` | [`Edit`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L58 "server-edit-related-task") | `Task` | Related user | -| `task.assign` | [`Assign`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L83 "server-owner-assign-task") | `Task` | Task owner, owner role, assurance at least 2, assignee in the same tenant | -| `task.complete` | [`Complete`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L111 "server-owner-complete-related-task") | `Task` | Related user, owner role, assurance at least 2 | -| `task.delete` | [`Delete`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L138 "server-owner-delete-task") | `Task` | Task owner, owner role, assurance at least 2 | +| `task.view` | [`View`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/policies/server-access.cedar#L8 "server-view-related-task") | `Task` | Related user | +| `task.create` | [`Create`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/policies/server-access.cedar#L33 "server-owner-create-task") | `Tenant` | Owner role, assurance at least 2 | +| `task.edit` | [`Edit`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/policies/server-access.cedar#L58 "server-edit-related-task") | `Task` | Related user | +| `task.assign` | [`Assign`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/policies/server-access.cedar#L83 "server-owner-assign-task") | `Task` | Task owner, owner role, assurance at least 2, assignee in the same tenant | +| `task.complete` | [`Complete`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/policies/server-access.cedar#L111 "server-owner-complete-related-task") | `Task` | Related user, owner role, assurance at least 2 | +| `task.delete` | [`Delete`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/policies/server-access.cedar#L138 "server-owner-delete-task") | `Task` | Task owner, owner role, assurance at least 2 | The complete action identifier is, for example, `Task::Action::"Create"`. Creation targets `Task::Tenant`: the tenant exists before the new task does.[^2] @@ -209,7 +199,7 @@ An owner role is a tenant role; a task owner is the user in the task's ### Create the policy-store files -Create the policy-store source using Cedarling's +Let's create a `policy-store/` directory at the project root using Cedarling's [directory-based format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format): ```text @@ -223,13 +213,20 @@ policy-store/ tutorial-idp.json ``` -Create the five files above using the complete linked versions: -[`metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/metadata.json), -[`schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/schema.cedarschema), -[`tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/trusted-issuers/tutorial-idp.json), -[`server-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar), and -[`browser-shadow-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/browser-shadow-access.cedar). -The examples below explain these files; do not append a second `Create` policy. +
+Required step: Create the five policy-store files + +Create each file at the path shown above and copy its complete linked contents: + +- [`metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/metadata.json) identifies the policy store and version. +- [`schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/schema.cedarschema) defines actions, entities, and request context. +- [`trusted-issuers/tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/trusted-issuers/tutorial-idp.json) defines the trusted token mapping. +- [`policies/server-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/policies/server-access.cedar) holds the six server permission rules. +- [`policies/browser-shadow-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/policy-store/policies/browser-shadow-access.cedar) holds the matching browser guidance rules. + +
+ +We'll look at the schema, issuer mapping, and `Create` policy from those files next. `metadata.json` records the store's stable ID, name, Cedar version, and policy version `1.0.0`. The version identifies the reviewed rules; the generated @@ -298,7 +295,8 @@ operation; the policy must allow it too. ### Set the conditions for `Create` -In `policies/server-access.cedar`, the `Create` policy matches the token's subject +With the request types and trusted issuer defined, we can express the creation +rule. In `policies/server-access.cedar`, the `Create` policy matches the token's subject to the current database user and checks its audience and scope. It also requires a matching tenant, the owner role, and the required assurance level: @@ -385,10 +383,21 @@ the application. ## Add Cedarling to the app -![Browser Cedarling guides controls using a safe user projection; the Fastify API enforces a fresh Cedarling decision before a protected database effect.](./assets/enforcement.webp) +The policies now describe the permissions. Let's connect them to the API first +and prove that Alex cannot create a task. Then we'll update the browser controls. -_Cedarling checks the signed token and current database facts before the API -returns or changes task data._ +```mermaid +flowchart TD + accTitle: Server enforcement and browser guidance + accDescr: Fastify loads the session token and current database facts, asks its embedded Cedarling instance, and enforces the decision. Browser evaluation only guides controls within the server ceiling. + Request["Task request: browser or direct HTTP"] --> API["Fastify: token and current facts"] + API --> PDP["Server Cedarling"] + PDP --> Check{"API decision"} + Check -->|"ALLOW"| Data["Read or write with version checks"] + Check -->|"DENY or failure"| Stop["No protected read or write"] + API -->|"Safe facts and allowed actions"| Browser["Browser Cedarling: unsigned request"] + Browser --> Controls["Controls within server ceiling"] +``` ### Install Cedarling and build the policy archive @@ -403,9 +412,19 @@ The Cedar package checks source syntax; the Cedarling SDK makes application decisions. The examples follow the [pinned SDK README](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468). -Add the complete repository-level -[`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/shared/policy-store.mjs) builder and -[`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/shared/policy-store.d.mts) declaration. +We'll use a shared builder to validate the policy files and package them for Cedarling. + +
+Required step: Create the shared archive builder + +Create these files in the repository-level `shared/` directory and copy their +complete linked contents: + +- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/policy-store.mjs) validates and packages the policy store. +- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/policy-store.d.mts) supplies the builder's TypeScript declarations. + +
+ The builder validates source paths, JSON, schema/policy syntax, and policy IDs, then creates `.local/policy-store.cjar`, a ZIP-format archive ignored by Git: @@ -419,15 +438,25 @@ Keep `policy-store/` in Git. Rebuild and restart after policy edits. ### Create one Cedarling instance for the server -Replace these files with the complete linked versions: -[`src/server/authorization-trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/authorization-trace.ts), -[`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/app.ts), -[`src/server/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/main.ts), -[`src/server/config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/config.ts), and -[`tsconfig.server.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/tsconfig.server.json). -Add [`src/shared/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/shared/authorization.ts) -and remove the replaced `src/server/capabilities.ts`. These files wire up all six -server operations, including the imports, function calls, and response types. +With the archive built, we'll load it once and connect the decisions to all six +server operations. Copy the complete files together so their imports and types agree. + +
+Required step: Update server authorization and its shared types + +Replace these existing files with their complete linked contents: + +- [`src/server/authorization-trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/server/authorization-trace.ts) loads Cedarling, builds requests, and records decisions. +- [`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/server/app.ts) enforces decisions in routes and serves browser permission data. +- [`src/server/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/server/main.ts) starts and closes the server's Cedarling instance. +- [`src/server/config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/server/config.ts) supplies server configuration. +- [`tsconfig.server.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/tsconfig.server.json) includes the shared types in the server build. + +Create [`src/shared/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/shared/authorization.ts) +and copy its full contents for the shared actions, resources, and permission +envelope types. Remove `src/server/capabilities.ts`, which it replaces. + +
`createServerAuthorization()` loads the archive relative to `options.projectRoot`. It registers the archive and initializes Cedarling: @@ -463,7 +492,8 @@ when the app closes. With strict schema validation, an invalid model prevents st ### Check permission before saving a task -Inside `createServerAuthorization()`'s returned +Now let's follow a creation request through the code we copied. Inside +`createServerAuthorization()`'s returned `authorize(requestId, session, target)` function, `session` is trusted server state and `target` describes the action and resource. This expanded `Create` request shows what `tokenSet(session)` and `requestItem(session, target)` produce: @@ -576,12 +606,22 @@ offer operations the API now denies. We'll fix that next. ### Show the actions each user can take -Replace these browser files with the complete linked versions: -[`src/web/authorization-trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/web/authorization-trace.ts), -[`src/web/api.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/web/api.ts), -[`src/web/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/web/types.ts), -[`src/web/App.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/web/App.tsx), and -[`tsconfig.web.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/tsconfig.web.json). +The API now protects task creation. Let's make the controls reflect those +permissions too. + +
+Required step: Update browser authorization and controls + +Replace these existing files with their complete linked contents: + +- [`src/web/authorization-trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/web/authorization-trace.ts) checks the archive and evaluates browser permissions. +- [`src/web/api.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/web/api.ts) fetches tasks and their authorization envelope. +- [`src/web/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/web/types.ts) connects browser data to the shared types. +- [`src/web/App.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/web/App.tsx) updates controls as permissions or task versions change. +- [`tsconfig.web.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/tsconfig.web.json) includes shared types in browser checks. + +
+ The server returns a list of actions it currently allows: its **decision ceiling**. For Alex, that includes editing his assigned brief, but not creating a task. Browser Cedarling can remove actions from this list, but cannot add permission to create. @@ -653,15 +693,23 @@ show unsigned Cedarling decisions. Alex's direct ## Update startup and Docker builds -We still need startup and Docker builds to prepare the policy archive. Replace -these files with the complete linked versions: -[`scripts/setup.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/scripts/setup.mjs), -[`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/scripts/dev.mjs), and -[`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/Dockerfile), and update -[`shared/dev-supervisor.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/shared/dev-supervisor.mjs) -at repository root. Both startup paths will use the same policy archive. +We have checked the API and browser separately. Let's finish by making native +startup and Docker builds prepare the same policy archive automatically. + +
+Required step: Update setup, development startup, and Docker packaging + +Replace these existing files with their complete linked contents: + +- [`scripts/setup.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/scripts/setup.mjs) prepares configuration and builds the archive. +- [`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/scripts/dev.mjs) starts the IdP, browser watcher, and API together. +- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/Dockerfile) builds and includes the policy archive in the image. +- [`shared/dev-supervisor.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/dev-supervisor.mjs), at repository root, manages development processes and readiness checks. + +
+ Update only these two entries in -[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/package.json)'s existing `scripts` object; +[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/package.json)'s existing `scripts` object; retain the other scripts and installed dependencies: ```json @@ -694,15 +742,13 @@ two-terminal startup. ## Check allowed and denied operations -![Alex's creation succeeds in the starting application and is denied after Cedarling integration; his assigned-task edit and Mina's creation remain allowed.](./assets/expected-outcomes.webp) - -_Alex's creation request should fail. His assigned-task edit and Mina's creation -should succeed. We'll check responses and logs._ +Both startup paths now load the integration. We'll repeat Alex's original request +with fresh data, then check the remaining actions and their decision logs. ### Retry Alex's request, then edit his assigned task
-Reset the sample data in this checkout +If needed: Reset the sample data in this checkout Stop the exercise services first. For native execution, run `pnpm reset`, then `pnpm dev` from this checkout's `p1-task-manager/`. Reset removes `.data`, @@ -743,7 +789,8 @@ workflow checks separate from permission denials. ### Read the decision logs -Formatted JSON lets us read nested reasons in the server logs. Match the +We've checked the responses; the logs let us connect them to the policies. +Formatted JSON keeps nested reasons readable. Match the application record's `cedarlingRequestId` to Cedarling's `request_id`. One HTTP request can produce several decisions, each with its own Cedarling ID. @@ -782,13 +829,13 @@ before sharing them. ### Test stale permissions and authorization failures -Run tests from the finished checkout: the baseline's tests expect operations to -be allowed and aren't updated by copying runtime files. No new tests are needed -for this exercise. Stop P1 services to free ports 17001 and +The finished project's tests also cover permissions that change while a request +is pending. Run them in a separate checkout, which includes the integration +tests alongside the runtime code. Stop P1 services to free ports 17001 and 18001, then use a separate directory for the finished example: ```bash -git clone --branch p1-task-manager-v1.0.1 https://github.com/GluuFederation/cedarling-tutorials.git cedarling-p1-finished +git clone --branch main https://github.com/GluuFederation/cedarling-tutorials.git cedarling-p1-finished cd cedarling-p1-finished/p1-task-manager pnpm --dir ../shared/identity-provider install --frozen-lockfile pnpm install --frozen-lockfile @@ -813,22 +860,8 @@ A runtime failure needs its own error event, distinct from a policy denial, and must stop the protected operation. Stopping the IdP alone won't reliably simulate a PDP failure because signing keys and valid tokens may already be cached. -## Use the same checks in your own app - -![Authenticate, load current facts, ask Cedarling, then let the application gate the effect. ALLOW permits the effect; DENY or failure leaves it untouched.](./assets/reusable-pattern.webp) - -_Use this sequence around reads and writes that need permission._ - -Choose one operation in your own app, such as creating a task. Authenticate the -user and load current facts, then ask Cedarling whether the operation may -proceed. Put that check in the server route that returns or changes the data. - -Follow `policy-store/`, the two `authorization-trace.ts` files, and the tests -in the [completed P1 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p1-task-manager-v1.0.1/p1-task-manager) -to see where each decision controls a read or write. -
-Warning: This setup is for local practice +Warning: Before deploying this application Local HTTP and the bundled IdP's sample passwords are for learning only. Production needs real authentication and assurance, HTTPS, protected secrets, @@ -838,6 +871,10 @@ development IdP and console output are not a production identity or audit system Use a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta, rather than deploying the tutorial IdP. +For a production stack, consider Agama Lab Policy Designer for policy authoring, +Jans Auth for issuing tokens, and Lock Server for centralized decision logs. +See [Cedarling production solutions](https://cedarling.dev/solutions). + These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues). @@ -848,14 +885,24 @@ For further reference, use the official
-For a production stack, consider Agama Lab Policy Designer for policy authoring, -Jans Auth for issuing tokens, and Lock Server for centralized decision logs. -See [Cedarling production solutions](https://cedarling.dev/solutions). +## What we've learned + +We started with Alex creating tasks meant to be created by Mina. We've now +blocked that request while keeping his assigned-task edits working. The API +checks the token and current tenant, role, assurance, and task relationships +before each protected read or write. Browser decisions guide the controls +within the server's allowed actions; they cannot grant API permission. + +To use this in your own app, choose one operation and find where the server +returns or changes its data. Load trusted facts there, ask Cedarling, and let +the operation proceed only after an allowed decision. Check both a denied +request and legitimate work, including requests that bypass the interface. -Next, P2 applies the same approach to retrieval: authorize the corpus and -candidate documents before loading protected text or sending it to an AI model. +In [P2](https://cedarling.dev/learn/prevent-cross-tenant-rag-data-leaks-with-cedarling), +we'll apply that order to retrieval, checking the corpus and documents before +their text reaches an AI model. -[^1]: The bundled IdP's [`provider.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/shared/identity-provider/src/provider.ts) configures sign-in and tokens; [`accounts.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/shared/identity-provider/src/accounts.ts) supplies identity claims. P1's login callback in [`app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/app.ts) maps the verified issuer and subject to a local user. [`database.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/database.ts) seeds and loads the role, tenant, and assurance values used here. +[^1]: The bundled IdP's [`provider.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/identity-provider/src/provider.ts) configures sign-in and tokens; [`accounts.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/identity-provider/src/accounts.ts) supplies identity claims. P1's login callback in [`app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/server/app.ts) maps the verified issuer and subject to a local user. [`database.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/src/server/database.ts) seeds and loads the role, tenant, and assurance values used here. [^2]: Cedar recommends authorizing creation against an existing resource container because the new resource does not yet exist. Here, the tenant is that container. See [Cedar's resource-container guidance](https://docs.cedarpolicy.com/bestpractices/bp-resources-containers.html). @@ -867,4 +914,4 @@ candidate documents before loading protected text or sending it to an AI model. [^6]: Cedar returns an empty list of determining policies when no policy permits or forbids the request. A matched `forbid` would instead appear as a determining policy. See [How Cedar authorization works](https://docs.cedarpolicy.com/auth/authorization.html). -[^7]: Full verification examples: [`test/policy-store.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/test/policy-store.test.ts), [`test/app.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/test/app.test.ts), [`test/browser-authorization.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/test/browser-authorization.test.ts), and [`e2e/task-authorization.e2e.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/e2e/task-authorization.e2e.ts). +[^7]: Full verification examples: [`test/policy-store.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/test/policy-store.test.ts), [`test/app.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/test/app.test.ts), [`test/browser-authorization.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/test/browser-authorization.test.ts), and [`e2e/task-authorization.e2e.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p1-task-manager/e2e/task-authorization.e2e.ts). diff --git a/p2-tenantrag/docs/assets/authorization-model-v2.webp b/p2-tenantrag/docs/assets/authorization-model-v2.webp deleted file mode 100644 index 67e34fb..0000000 Binary files a/p2-tenantrag/docs/assets/authorization-model-v2.webp and /dev/null differ diff --git a/p2-tenantrag/docs/assets/enforcement-v2.webp b/p2-tenantrag/docs/assets/enforcement-v2.webp deleted file mode 100644 index 368ad28..0000000 Binary files a/p2-tenantrag/docs/assets/enforcement-v2.webp and /dev/null differ diff --git a/p2-tenantrag/docs/assets/expected-outcomes-v2.webp b/p2-tenantrag/docs/assets/expected-outcomes-v2.webp deleted file mode 100644 index 56b6fda..0000000 Binary files a/p2-tenantrag/docs/assets/expected-outcomes-v2.webp and /dev/null differ diff --git a/p2-tenantrag/docs/assets/missing-authorization.webp b/p2-tenantrag/docs/assets/missing-authorization.webp deleted file mode 100644 index 4259a80..0000000 Binary files a/p2-tenantrag/docs/assets/missing-authorization.webp and /dev/null differ diff --git a/p2-tenantrag/docs/assets/reusable-pattern-v2.webp b/p2-tenantrag/docs/assets/reusable-pattern-v2.webp deleted file mode 100644 index b84a4a7..0000000 Binary files a/p2-tenantrag/docs/assets/reusable-pattern-v2.webp and /dev/null differ diff --git a/p2-tenantrag/docs/tutorials.md b/p2-tenantrag/docs/tutorials.md index 5fb25ee..020c8c1 100644 --- a/p2-tenantrag/docs/tutorials.md +++ b/p2-tenantrag/docs/tutorials.md @@ -5,7 +5,7 @@ summary: Authorize corpus search and each candidate document before protected te order: 30 socialImage: ./assets/social-card.webp socialImageAlt: A Fastify RAG service checks corpus and documents with Cedarling before sending text to an AI model. -lastVerified: 2026-10-01T09:46:20Z +lastVerified: 2026-10-07T19:56:00Z --- # Prevent Cross-Tenant RAG Leaks with Cedarling @@ -29,7 +29,11 @@ documents remain available. The model must never decide access. ## Build the integration or try the finished app - To build the integration, start with [Run the starting application](#run-the-starting-application), then add policies and retrieval checks. -- To try the finished app, run the [complete tagged project](https://github.com/GluuFederation/cedarling-tutorials/tree/p2-tenantrag-v1.0.1/p2-tenantrag) using its README, then go to [Check which documents each user can retrieve](#check-which-documents-each-user-can-retrieve). This version already uses Cedarling. +- To try the finished app, run the [finished project on main](https://github.com/GluuFederation/cedarling-tutorials/tree/main/p2-tenantrag) using its README, then go to [Check which documents each user can retrieve](#check-which-documents-each-user-can-retrieve). This version already uses Cedarling. + +If you're building from the starting project, open each **Required step** section +and complete its instructions before continuing. These sections contain the files +and changes we'll need.
What you'll need @@ -43,9 +47,10 @@ documents remain available. The model must never decide access.
-Copy whole files from GitHub's raw-file view into your baseline checkout; don't -switch to the finished tag. The short examples aren't complete replacements. -Create missing parent directories. Paths and commands are relative to +At each copying step, open the linked file on GitHub, choose **Raw**, and copy +its full contents into the stated destination in your baseline checkout. The +short examples explain the parts we'll focus on. Create missing parent +directories first. Paths and commands are relative to `p2-tenantrag/`; repository-level `shared/` files go one directory above it. ## Meet the service and its users @@ -68,28 +73,15 @@ The bundled Node.js `oidc-provider` handles sign-in and issues access tokens. The API verifies a token, then looks up the caller's tenant in its repository; the document metadata lists who may read confidential content. -```mermaid -flowchart TD - accTitle: Permission checks in the completed retrieval service - accDescr: The API enforces a corpus decision before search and document decisions before loading text for generation. - Caller["Caller: access token and question"] --> Corpus["API enforces Cedarling SearchCorpus"] - Corpus -->|"DENY"| Stop["Stop before search"] - Corpus -->|"ALLOW"| Search["Voyage query embedding and Orama search"] - Search --> Documents["API enforces Cedarling RetrieveDocument batch"] - Documents -->|"DENY"| Filter["Exclude document text"] - Documents -->|"ALLOW"| Text["Load selected authorized text"] - Text --> Model["OpenRouter: generate answer with citations"] -``` - Cedarling is the policy decision point (PDP). The retrieval service enforces its decisions as the policy enforcement point (PEP). It checks both corpus and document access with the same embedded Cedarling instance. ## Reproduce the leak before adding Cedarling -![Before authorization, a Tenant B caller can reach Tenant A evidence through the retrieval API.](./assets/missing-authorization.webp) - -_In the baseline, authentication alone does not stop Mallory's cross-tenant retrieval._ +Let's try Mallory's cross-tenant question before adding those checks. We'll +inspect the citations as well as the answer, because model wording alone +cannot tell us which documents the service used. ### Run the starting application @@ -173,20 +165,28 @@ its volume, and install the native dependencies above for the coding steps. ## Give the server the document permissions it needs -Open the baseline's +Mallory's request shows why checking sign-in is not enough. To add the missing +permission checks, open the baseline's [`src/rag/retrieval.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p2-tenantrag/src/rag/retrieval.ts). It looks up the corpus before embedding the question and loads document metadata before reading text. Those are the two places we'll ask Cedarling for a decision. The confidential-document rule also needs a list of permitted readers. -Save these complete fact-source files before defining the rules: +We'll first make the document's reader permissions available to retrieval. -- [`src/rag/fixtures.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/fixtures.ts) -- [`src/rag/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/types.ts) -- [`src/rag/pdf.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/pdf.ts) -- [`src/rag/repository.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/repository.ts) -- [`src/rag/corpus.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/corpus.ts) -- [`src/rag/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/setup.ts) +
+Required step: Update document metadata and index preparation + +Replace these existing files with their complete linked contents: + +- [`src/rag/fixtures.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/rag/fixtures.ts) assigns users to tenants and adds confidential readers to documents. +- [`src/rag/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/rag/types.ts) defines the permission metadata types. +- [`src/rag/pdf.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/rag/pdf.ts) carries that metadata through PDF extraction. +- [`src/rag/repository.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/rag/repository.ts) resolves current document and caller facts. +- [`src/rag/corpus.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/rag/corpus.ts) binds the search index to its text and metadata. +- [`src/rag/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/rag/setup.ts) rebuilds and verifies the corpus. + +
These files add reader permissions to the index without changing the fictional PDF content. They map Ada and Leo to Tenant A and Mallory to Tenant B. Only Ada @@ -203,13 +203,13 @@ the integration files are in place so the new metadata reaches retrieval. ## Decide which evidence each caller may use -![Tenant A and Tenant B evidence are separated by corpus and document decisions defined in the policy store.](./assets/authorization-model-v2.webp) - -_The corpus rule controls search; the document rule controls which text can be loaded._ +The server can now supply tenant and reader facts. We'll use them in two rules: +one for searching a corpus, and another for loading each candidate document. ### Create the policy store -Use the [directory-based policy-store format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format): +Let's create `policy-store/` at the project root, following the +[directory-based policy-store format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format): ```text policy-store/ @@ -221,12 +221,17 @@ policy-store/ tutorial-idp.json ``` -Create the complete policy-store files from the pinned version: +
+Required step: Create the four policy-store files + +Create these files and copy their complete linked contents: -- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/metadata.json) -- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/schema.cedarschema) -- [`policy-store/policies/server-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/policies/server-access.cedar) -- [`policy-store/trusted-issuers/tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/trusted-issuers/tutorial-idp.json) +- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/policy-store/metadata.json) identifies the store and version. +- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/policy-store/schema.cedarschema) defines token, corpus, document, and context types. +- [`policy-store/policies/server-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/policy-store/policies/server-access.cedar) contains the corpus and document permission rules. +- [`policy-store/trusted-issuers/tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/policy-store/trusted-issuers/tutorial-idp.json) maps tokens from P2's IdP. + +
The metadata identifies this store and version `1.0.0`. The schema defines the request types. Both rules live in one policy file with distinct `@id` @@ -256,7 +261,8 @@ The copied `trusted-issuers/tutorial-idp.json` sets `openid_configuration_endpoi ### Check the tenant and document's readers -This complete policy from `policies/server-access.cedar` checks verified token +Let's follow the document rule, where Ada's and Leo's permissions differ. +This policy from `policies/server-access.cedar` checks verified token claims against current application facts: ```cedar @@ -306,8 +312,8 @@ handled as failures. | Capability | Identity | Action | Resource | Context | Effect waiting for ALLOW | | ------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ---------------------------------------------- | -------------------------------------- | -| `corpus.search` | Caller access token | [`SearchCorpus`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/policies/server-access.cedar#L2 "server-search-tenant-corpus") | Resolved corpus | Current user, selected corpus, server boundary | Query embedding and vector search | -| `document.retrieve` | Same token | [`RetrieveDocument`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/policies/server-access.cedar#L25 "server-retrieve-authorized-document") | Each unique candidate document | Same trusted context | Load text for generation and citations | +| `corpus.search` | Caller access token | [`SearchCorpus`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/policy-store/policies/server-access.cedar#L2 "server-search-tenant-corpus") | Resolved corpus | Current user, selected corpus, server boundary | Query embedding and vector search | +| `document.retrieve` | Same token | [`RetrieveDocument`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/policy-store/policies/server-access.cedar#L25 "server-retrieve-authorized-document") | Each unique candidate document | Same trusted context | Load text for generation and citations | The route chooses actions; the repository supplies tenants, classification, and reader permissions. Neither the question nor model output supplies trusted facts. @@ -315,9 +321,22 @@ Look up search results by document ID in the repository before building requests ## Put Cedarling in the retrieval path -![The API enforces Cedarling corpus and document decisions before loading allowed text for the model.](./assets/enforcement-v2.webp) +We have the rules. Next we'll load Cedarling and make retrieval wait for its +decisions in this order: -_A corpus DENY stops retrieval; a document DENY filters that document from the batch._ +```mermaid +flowchart TD + accTitle: Two checks before evidence reaches the model + accDescr: The API asks embedded Cedarling about the corpus before embedding or searching. It then checks each candidate document before loading text. Denied documents are filtered, and authorization failures stop retrieval. + Request["API: token and corpus facts"] --> Corpus{"Cedarling: corpus check"} + Corpus -->|"DENY or failure"| Stop["Stop retrieval"] + Corpus -->|"ALLOW"| Search["Embed question; search Orama"] + Search --> Documents{"Cedarling: document batch"} + Documents -->|"Each DENY"| Filter["Skip denied text"] + Documents -->|"Allowed set"| Text["API loads allowed chunks"] + Text -->|"Generate if chunks remain"| Result["Answer and citations"] + Documents -->|"Failed or incomplete batch"| Stop +``` ### Build the archive and load Cedarling @@ -328,10 +347,18 @@ pnpm add --save-exact @janssenproject/cedarling_wasm@0.0.468 fflate@0.8.3 pnpm add --save-dev --save-exact @cedar-policy/cedar-wasm@4.12.0 ``` -Save the repository-level archive builder and its declaration: +We'll use a shared builder to validate the policy files and package them for Cedarling. + +
+Required step: Create the shared archive builder + +Create these files in the repository-level `shared/` directory and copy their +complete linked contents: -- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/shared/policy-store.mjs) -- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/shared/policy-store.d.mts) +- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/policy-store.mjs) validates and packages the policy store. +- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/policy-store.d.mts) provides its TypeScript declarations. + +
Run the shared builder to create the archive: @@ -376,18 +403,27 @@ shutdown. `src/runtime.ts` supplies these authorization functions to retrieval. ### Check the corpus before searching -Save these complete files together to connect the permission checks: +Let's connect the initialized instance to retrieval. Copy these files together; +then we'll trace the corpus check and the document batch through them. + +
+Required step: Add authorization and update the retrieval service + +Create [`src/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/authorization.ts) +and copy its full contents to load Cedarling, build requests, and collect decisions. +Replace these existing files with their complete linked contents: + +- [`src/runtime.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/runtime.ts) connects authorization to retrieval and shutdown. +- [`src/rag/retrieval.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/rag/retrieval.ts) enforces corpus and document decisions before loading text. +- [`src/rag/trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/rag/trace.ts) records retrieval counts without document text. +- [`src/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/app.ts) handles HTTP requests and bounded errors. +- [`src/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/main.ts) starts and closes the runtime. +- [`src/config/project-config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/config/project-config.ts) supplies the policy archive location. +- [`src/openapi.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/openapi.ts) describes the retrieval responses and errors. +- [`src/auth/cli.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/src/auth/cli.ts) handles CLI sign-in and its diagnostics. +- [`tsconfig.build.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/tsconfig.build.json) keeps source-run setup scripts out of the compiled build. -- [`src/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/authorization.ts) -- [`src/runtime.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/runtime.ts) -- [`src/rag/retrieval.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/retrieval.ts) -- [`src/rag/trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/trace.ts) -- [`src/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/app.ts) -- [`src/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/main.ts) -- [`src/config/project-config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/config/project-config.ts) -- [`src/openapi.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/openapi.ts) -- [`src/auth/cli.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/auth/cli.ts) -- [`tsconfig.build.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/tsconfig.build.json) +
Inside the corpus authorization function, `principal` is the authenticated caller, `profile` holds its stored permissions, and `corpus` is the stored record. @@ -460,6 +496,7 @@ Neither case reaches `voyage.embed()` or the following `corpusSearch.search()`. ### Check each document before loading its text +An allowed corpus search can still return a document the caller may not read. The retrieval service removes duplicate documents from the search results. Its document check reuses the token set and context, submitting one item per document. In `src/authorization.ts`, the expanded batch request looks like this: @@ -522,18 +559,29 @@ also require rebuilding the index, as in the next step. ## Rebuild the index and restart the app -Save the runtime preparation and packaging files: +The request path now enforces both decisions. Before we retry Mallory's request, +we need the archive in our startup paths and the reader grants in the index. -- [`scripts/prepare.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/scripts/prepare.ts) -- [`scripts/preflight.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/scripts/preflight.ts) -- [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/scripts/setup.ts) -- [`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/scripts/dev.mjs) -- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/Dockerfile) -- [`compose.yaml`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/compose.yaml) -- [`shared/dev-supervisor.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/shared/dev-supervisor.mjs) +
+Required step: Update index preparation and startup + +Create these files and copy their complete linked contents: + +- [`scripts/prepare.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/scripts/prepare.ts) prepares identity configuration and the policy archive without provider requests. +- [`scripts/preflight.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/scripts/preflight.ts) checks the index before startup. +- [`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/scripts/dev.mjs) starts the IdP and API together. + +Replace these existing files with their complete linked contents: + +- [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/scripts/setup.ts) prepares configuration and verifies the live-provider corpus. +- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/Dockerfile) includes the archive in the API image. +- [`compose.yaml`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/compose.yaml) starts the configured services with their readiness checks. +- [`shared/dev-supervisor.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/dev-supervisor.mjs), at repository root, manages native processes and readiness checks. + +
Apply the `build` entry shown below to your existing -[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/package.json), keeping +[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p2-tenantrag/package.json), keeping the other dependencies and scripts. Also set `dev` to `node scripts/dev.mjs`. The build runs the shared archive builder before TypeScript; the copied @@ -570,9 +618,8 @@ when the PDF/index needs rebuilding. ## Check which documents each user can retrieve -![Ada may read confidential Tenant A evidence; Leo may read its public evidence but not confidential evidence; Mallory cannot search Tenant A.](./assets/expected-outcomes-v2.webp) - -_Permission to search a corpus does not grant access to every document in it._ +Let's check the two rules separately: Mallory should stop at the corpus check, +while Ada and Leo should reach document checks with different results. ### Retry Mallory's request, then compare Ada and Leo @@ -636,7 +683,8 @@ Don't rerun `pnpm run setup` to retry generation: it rebuilds the corpus using V ### Read the retrieval and decision logs -The response's `requestId` appears in `authorization.context`. Its +To explain those differences, find the response's `requestId` in +`authorization.context`. Its `cedarlingRequestId` matches Cedarling's `request_id`; document decisions also share `batch_id`. For Leo's confidential-document attempt, the main Cedarling log fields look like this: @@ -665,8 +713,9 @@ Capture only fictional data and remove tokens and secrets from recordings. ### Check denied text stays out of the result -For automated checks, use a separate checkout of the -[finished tag](https://github.com/GluuFederation/cedarling-tutorials/tree/p2-tenantrag-v1.0.1/p2-tenantrag), install its locked project and shared IdP dependencies, then run: +We've checked live responses and their decisions. To verify that denied text +never reaches generation, use the automated checks in a separate checkout of the +[finished project on main](https://github.com/GluuFederation/cedarling-tutorials/tree/main/p2-tenantrag), install its locked project and shared IdP dependencies, then run: ```bash pnpm check @@ -685,36 +734,44 @@ Approve the Ada, Leo, and Mallory sign-ins. This consumes provider quota and checks document access, not identical model answers. Record Mallory's request before and after integration, plus one successful authorized search. -## Protect retrieved evidence in your own service - -![The application enforces Cedarling corpus and per-document decisions before allowed evidence reaches the model.](./assets/reusable-pattern-v2.webp) - -_A denied corpus stops the question; a denied document is filtered while other allowed documents may continue._ +
+Warning: Before deploying this application -Check access to the collection before expensive work, then check each document before -loading its content. Keep this order wherever search results feed a model, -report, or another service. +Replace local HTTP and the bundled learning IdP with HTTPS and a configured +OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), +Gluu, Auth0, or Okta. -Follow `src/authorization.ts`, `src/rag/retrieval.ts`, `src/rag/repository.ts`, -`policy-store/`, and the tests in the -[completed P2 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p2-tenantrag-v1.0.1/p2-tenantrag). +Use trusted user and permission records, review how providers handle data, and +protect logs. Setup sends the fictional corpus for embedding to build the index; +it does not check a caller's access to those documents. Cedarling controls access +to documents, not answer accuracy or prompt injection. A public document +containing instructions is still allowed content. For a production stack, consider Agama Lab Policy Designer for policy authoring, Jans Auth for issuing tokens, and Lock Server for centralized decision logs. See [Cedarling production solutions](https://cedarling.dev/solutions). -Next, P3 puts authorization at an MCP server, where a model can request a change -to incident state as well as information. +These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI +on macOS and Windows. If a platform-specific step fails, +[open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues). -
-Warning: This setup is for local practice +
-- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta. -- Use trusted user and permission records, review how providers handle data, and protect logs. Setup sends the fictional corpus for embedding to build the index; it does not check a caller's access to those documents. -- Cedarling controls access to documents. It does not guarantee correct model answers or prevent prompt injection; a public document containing instructions is still allowed content. -- These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues). +## What we've learned -
+Mallory's cross-tenant request now stops before embedding or search. Within +Tenant A, Ada can use confidential evidence while Leo receives only documents +he may read. We've separated permission to search a collection from permission +to load each document, and used the allowed chunks for both generation and +citations. A model failure remains a different outcome from a denied document. + +For your own retrieval service, keep that order: check the collection, search, +check candidate documents against trusted metadata, then load allowed content. +The same checks apply when the destination is a report or another service +instead of an AI model. + +In [P3](https://cedarling.dev/learn/govern-mcp-capabilities), we'll protect MCP +operations that let an assistant read information and change incident state. [^1]: P2's confidential-reader rule uses the current document's `confidential_reader_subjects` set. Ada's subject is listed; Leo's is not. The application supplies this relationship for each authorization request. diff --git a/p3-mcp-capability-governance/docs/assets/authorization-model.png b/p3-mcp-capability-governance/docs/assets/authorization-model.png deleted file mode 100644 index 1424695..0000000 Binary files a/p3-mcp-capability-governance/docs/assets/authorization-model.png and /dev/null differ diff --git a/p3-mcp-capability-governance/docs/assets/enforcement.png b/p3-mcp-capability-governance/docs/assets/enforcement.png deleted file mode 100644 index 02dac10..0000000 Binary files a/p3-mcp-capability-governance/docs/assets/enforcement.png and /dev/null differ diff --git a/p3-mcp-capability-governance/docs/assets/expected-outcomes.png b/p3-mcp-capability-governance/docs/assets/expected-outcomes.png deleted file mode 100644 index 125b1ba..0000000 Binary files a/p3-mcp-capability-governance/docs/assets/expected-outcomes.png and /dev/null differ diff --git a/p3-mcp-capability-governance/docs/assets/missing-authorization.png b/p3-mcp-capability-governance/docs/assets/missing-authorization.png deleted file mode 100644 index 8d2bc7d..0000000 Binary files a/p3-mcp-capability-governance/docs/assets/missing-authorization.png and /dev/null differ diff --git a/p3-mcp-capability-governance/docs/assets/reusable-pattern.png b/p3-mcp-capability-governance/docs/assets/reusable-pattern.png deleted file mode 100644 index 21edf0a..0000000 Binary files a/p3-mcp-capability-governance/docs/assets/reusable-pattern.png and /dev/null differ diff --git a/p3-mcp-capability-governance/docs/tutorials.md b/p3-mcp-capability-governance/docs/tutorials.md index 50ad2de..72865d2 100644 --- a/p3-mcp-capability-governance/docs/tutorials.md +++ b/p3-mcp-capability-governance/docs/tutorials.md @@ -5,7 +5,7 @@ summary: Protect MCP tools, resources, and prompts with signed identity evidence order: 40 socialImage: ./assets/social-card.webp socialImageAlt: An MCP server checks incident capabilities with a private Cedarling sidecar before protected effects. -lastVerified: 2026-10-01T09:46:20Z +lastVerified: 2026-10-07T19:56:00Z --- # Govern MCP Capabilities with Cedarling @@ -28,7 +28,11 @@ work still succeeds, including calls without the chat interface. ## Build the integration or try the finished app - To build the integration, start with [Run the starting application](#run-the-starting-application), then add the policies and sidecar calls. -- To try the finished app, run the [complete tagged project](https://github.com/GluuFederation/cedarling-tutorials/tree/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance) using its README, then go to [Check assigned and unassigned incidents](#check-assigned-and-unassigned-incidents). This version already uses Cedarling. +- To try the finished app, run the [finished project on main](https://github.com/GluuFederation/cedarling-tutorials/tree/main/p3-mcp-capability-governance) using its README, then go to [Check assigned and unassigned incidents](#check-assigned-and-unassigned-incidents). This version already uses Cedarling. + +If you're building from the starting project, open each **Required step** section +and complete its instructions before continuing. These sections contain the files +and changes we'll need.
What you'll need @@ -42,9 +46,10 @@ work still succeeds, including calls without the chat interface.
-Copy whole files from GitHub's raw-file view into your baseline checkout; don't -switch to the finished tag. The short examples aren't complete replacements. -Create missing parent directories. Paths and commands are relative to +At each copying step, open the linked file on GitHub, choose **Raw**, and copy +its full contents into the stated destination in your baseline checkout. The +short examples explain the parts we'll focus on. Create missing parent +directories first. Paths and commands are relative to `p3-mcp-capability-governance/`; repository-level `shared/` files go one directory above it. ## Meet the incident assistant and its users @@ -69,29 +74,13 @@ The bundled Node.js `oidc-provider` authenticates these users and issues their access tokens. The MCP server looks up roles in its account map and assignments in the incident repository; neither comes from the model. -```mermaid -flowchart TD - accTitle: MCP enforcement with a private Cedarling sidecar - accDescr: The terminal uses OpenRouter to select work. The MCP server consults a separate Cedarling sidecar and enforces its response before accessing incidents, runbooks or prompts. - Host["Terminal host and MCP client"] <-->|"Select an operation"| Model["OpenRouter"] - Host -->|"MCP request and access token"| Server - subgraph Local["Local services"] - Server["MCP server: current caller and incident facts"] -->|"AuthZen request"| PDP["Cedarling sidecar and policy archive"] - PDP -->|"Validate signed token"| IdP["Tutorial IdP discovery and keys"] - PDP -->|"Decision"| Check["MCP server enforces result"] - Check -->|"ALLOW"| Effect["Incident, runbook or triage operation"] - Check -->|"DENY or failure"| Stop["No protected effect"] - end -``` - The host and model request work. The MCP server controls data access and changes. Cedarling supplies decisions; it does not call tools or update incidents. ## Try updating an unassigned incident -![Before authorization, Amir can confirm an MCP request and update an incident that is not assigned to him.](./assets/missing-authorization.png) - -_The baseline validates the request but does not yet enforce the assignment rule._ +Let's see why confirmation alone is not enough. We'll sign in as Amir and +confirm an update to the incident that nobody assigned to him. ### Run the starting application @@ -141,7 +130,8 @@ then stop the baseline with `docker compose down`. Restarting restores the sampl ## Where should we check permission? -Open the baseline's +The update succeeded, so the missing check belongs on the server that changed +the incident. Open the baseline's [`src/mcp/server.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p3-mcp-capability-governance/src/mcp/server.ts) and find the `update_incident_status` handler. After authentication and input validation, it logs an ALLOW without checking permission, then calls the repository: @@ -165,9 +155,8 @@ decisions before returning discovery, search results, runbook text, and triage p ## Decide which MCP operations each user may perform -![The P3 policy store combines a signed token, caller role, and current incident assignment for MCP decisions.](./assets/authorization-model.png) - -_The decision combines trusted identity with facts about the current incident._ +We'll now add Cedarling to the starting application. First, let's define which +operations each role may use and when an incident assignment is required. ### Choose an action and resource for each operation @@ -176,12 +165,12 @@ assignments come from the server. | Capability | Identity | Action | Resource | Trusted context | Protected effect | | -------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ----------------------------------- | ------------------------------------------- | -| Discover | Signed caller token | [`Discover`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L16 "operations-surface") | `Service::"incident-assistant"` | Current caller subject and role | Make MCP operations available to the caller | -| Search | Same token | [`Search`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L16 "operations-surface") | Same service | Same caller | Begin incident search | -| Read result | Same token | [`Read`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L26 "incident-assignment") | Each candidate `Incident` | Same caller; assignment on resource | Return a matching incident summary | -| Read runbook | Same token | [`ReadRunbook`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L16 "operations-surface") | `Runbook::"core"` | Same caller | Return runbook content | -| Prepare triage | Same token | [`Triage`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L26 "incident-assignment") | Current `Incident` | Same caller; assignment on resource | Return an incident-specific prompt | -| Update status | Same token | [`UpdateStatus`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L26 "incident-assignment") | Current `Incident` | Same caller; assignment on resource | Commit one valid transition | +| Discover | Signed caller token | [`Discover`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L16 "operations-surface") | `Service::"incident-assistant"` | Current caller subject and role | Make MCP operations available to the caller | +| Search | Same token | [`Search`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L16 "operations-surface") | Same service | Same caller | Begin incident search | +| Read result | Same token | [`Read`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L26 "incident-assignment") | Each candidate `Incident` | Same caller; assignment on resource | Return a matching incident summary | +| Read runbook | Same token | [`ReadRunbook`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L16 "operations-surface") | `Runbook::"core"` | Same caller | Return runbook content | +| Prepare triage | Same token | [`Triage`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L26 "incident-assignment") | Current `Incident` | Same caller; assignment on resource | Return an incident-specific prompt | +| Update status | Same token | [`UpdateStatus`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L26 "incident-assignment") | Current `Incident` | Same caller; assignment on resource | Commit one valid transition | All types and actions use namespace `P3IncidentAssistant`. The model can suggest an incident ID; the server loads its assignment and the caller's role. Neither @@ -189,7 +178,8 @@ comes from the model or caller JSON. The app still checks status changes and ret ### Create the policy store -Create the [directory-based policy store](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format): +Let's create `policy-store/` at the project root, following the +[directory-based format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format): ```text policy-store/ @@ -201,12 +191,17 @@ policy-store/ tutorial-idp.json ``` -Create the complete policy-store files from the pinned version: +
+Required step: Create the four policy-store files -- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/metadata.json) -- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/schema.cedarschema) -- [`policy-store/policies/incident-operations.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar) -- [`policy-store/trusted-issuers/tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/trusted-issuers/tutorial-idp.json) +Create these files and copy their complete linked contents: + +- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/policy-store/metadata.json) identifies the store and version. +- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/policy-store/schema.cedarschema) defines the operations, resources, and token context. +- [`policy-store/policies/incident-operations.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar) checks token identity, role, and assignment. +- [`policy-store/trusted-issuers/tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/policy-store/trusted-issuers/tutorial-idp.json) maps tokens from P3's IdP. + +
Use the integration's metadata with policy version `1.0.0`. The schema defines `Service`, `Runbook`, and `Incident`; only the incident needs an optional @@ -251,7 +246,8 @@ subject, API audience, and client ID match this caller and application. ### Check the token, then the role and assignment -In `policies/incident-operations.cedar`, a forbid protects every operation if +With the token mapping in place, let's read the rules in +`policies/incident-operations.cedar`. A forbid protects every operation if required token evidence is missing or does not match: ```cedar @@ -298,9 +294,20 @@ a matching forbid overrides any permit, and no matching permit gives DENY. ## Connect the MCP server to Cedarling -![The MCP server enforces decisions from a private Cedarling sidecar before any protected incident effect.](./assets/enforcement.png) +The policies describe what Amir, Dana, and Eve may do. Next we'll load them in +the sidecar and make the MCP server wait for a decision before each operation. -_The MCP server is the enforcement point; the sidecar returns authorization decisions._ +```mermaid +flowchart TD + accTitle: The MCP server enforces the sidecar decision + accDescr: A model-selected or direct MCP request reaches the same server. The server supplies current facts to the private Cedarling sidecar, then stops or performs the operation based on its response. + Chat["Chat request; y/N for updates"] --> Server["MCP server: caller and resource facts"] + Direct["Direct MCP request"] --> Server + Server -->|"HTTP authorization request"| PDP["Private Cedarling sidecar"] + PDP --> Check{"MCP server checks response"} + Check -->|"Valid ALLOW"| Effect["Run permitted operation"] + Check -->|"DENY or unavailable"| Stop["No protected data or change"] +``` ### Prepare the private sidecar @@ -312,10 +319,19 @@ Compose pins this Docker image: ghcr.io/janssenproject/jans/cedarling-flask-sidecar:2.4.1-1@sha256:501d5bc88e8a0b67cbab314b31c787f94b6c4ad9f16a77ec81d0e183b2645f0b ``` -Save the repository-level archive builder and its declaration: +The sidecar needs a policy archive. We'll use a shared builder to validate and +package our policy files. -- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/shared/policy-store.mjs) -- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/shared/policy-store.d.mts) +
+Required step: Create the shared archive builder + +Create these files in the repository-level `shared/` directory and copy their +complete linked contents: + +- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/policy-store.mjs) validates and packages the policy store. +- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/policy-store.d.mts) supplies its TypeScript declarations. + +
Install its build dependencies from P3: @@ -346,21 +362,30 @@ service that can reach the sidecar. This local trust boundary includes the IdP. ### Send current facts to the Cedarling sidecar -Save these complete files together to connect the permission checks: +Now we'll build the sidecar request from facts the MCP server trusts. Copy the +following files together, then follow the HTTP request below. -- [`src/mcp/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/mcp/authorization.ts) -- [`src/mcp/server.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/mcp/server.ts) -- [`src/mcp/availability.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/mcp/availability.ts) -- [`src/auth/token-verifier.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/auth/token-verifier.ts) -- [`src/incidents/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/incidents/types.ts) -- [`src/incidents/repository.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/incidents/repository.ts) -- [`src/mcp/schemas.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/mcp/schemas.ts) -- [`src/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/app.ts) -- [`src/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/main.ts) -- [`src/config/project-config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/config/project-config.ts) -- [`tsconfig.build.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/tsconfig.build.json) +
+Required step: Add the sidecar client and update MCP handlers + +Create [`src/mcp/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/mcp/authorization.ts) +and copy its full contents for the sidecar client and decision logging. +Replace these existing files with their complete linked contents: + +- [`src/mcp/server.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/mcp/server.ts) enforces discovery and operation decisions. +- [`src/mcp/availability.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/mcp/availability.ts) checks that the expected MCP server is available before chat connects. +- [`src/auth/token-verifier.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/auth/token-verifier.ts) verifies caller tokens without exposing them in diagnostics. +- [`src/incidents/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/incidents/types.ts) defines incidents, lifecycle states, and sample account names. +- [`src/incidents/repository.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/incidents/repository.ts) supplies current incidents and validates state changes. +- [`src/mcp/schemas.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/mcp/schemas.ts) validates MCP operation inputs. +- [`src/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/app.ts) connects authentication and MCP request handling. +- [`src/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/main.ts) starts the configured MCP server. +- [`src/config/project-config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/config/project-config.ts) supplies the private sidecar URL and timeout. +- [`tsconfig.build.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/tsconfig.build.json) selects the server and CLI files for compilation. -Remove `src/mcp/trace.ts`; the completed handlers no longer use it. +Remove `src/mcp/trace.ts`; authorization now records the actual decisions. + +
An AuthZEN-style decision request has a subject, action, resource, and context, and the response contains a boolean decision.[^3] In this Cedarling sidecar @@ -423,6 +448,7 @@ the optional assignment instead of sending `null` to a string field. ### Check permission before each MCP operation +With that decision response checked, we can use it in the MCP handlers. `src/mcp/server.ts` evaluates `Discover` before registering the caller's operations. A denied caller receives an empty list of operations. Registering a tool is not permission to use it against every resource. @@ -462,18 +488,29 @@ enter model messages. ## Finish setup and start the integrated stack -Save the runtime preparation and packaging files: +The handlers now wait for Cedarling. Let's finish the Compose configuration +so the archive and sidecar are ready before the MCP server starts. -- [`src/chat/host.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/chat/host.ts) -- [`scripts/setup.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/scripts/setup.mjs) -- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/Dockerfile) -- [`compose.yaml`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/compose.yaml) -- [`sidecar-bootstrap.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/sidecar-bootstrap.json) +
+Required step: Configure the sidecar and update stack startup + +Create [`sidecar-bootstrap.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/sidecar-bootstrap.json) +and copy its full contents to configure the archive, token validation, and logs. +Replace these existing files with their complete linked contents: + +- [`src/chat/host.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/src/chat/host.ts) uses discovered capabilities and handles denied or unavailable operations. +- [`scripts/setup.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/scripts/setup.mjs) prepares host chat configuration. +- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/Dockerfile) builds the app and policy archive. +- [`compose.yaml`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/compose.yaml) starts the private sidecar and dependent services. -Remove `scripts/dev.mjs`. In `package.json`, set `dev` to +Remove `scripts/dev.mjs`; Compose will manage these services. + +
+ +In `package.json`, set `dev` to `docker compose up --build` and `start` to `docker compose up`, then apply the `build` entry below. Keep the other dependencies and scripts; the -[tagged manifest](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/package.json) +[complete manifest](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p3-mcp-capability-governance/package.json) shows the completed configuration: ```json @@ -495,9 +532,8 @@ Then run `pnpm chat amir` to check assigned and unassigned incidents. ## Check assigned and unassigned incidents -![Amir can operate on his assigned incident but not an unassigned one; Eve sees an empty surface.](./assets/expected-outcomes.png) - -_An assigned incident and an unassigned incident produce different decisions for Amir._ +The integrated stack is ready. We'll first complete Amir's assigned work, +then repeat the unassigned update that succeeded before Cedarling. ### Complete Amir's assigned work @@ -538,7 +574,8 @@ supervisor condition in the policy before trying the cases below: ### Read the server and sidecar logs -The MCP server prints formatted `authorization.decision` records with +Let's connect those outcomes to the decisions. The MCP server prints formatted +`authorization.decision` records with `requestId`, `actorId`, action, resource, and ALLOW or DENY. One application request can perform both discovery and an operation, producing several decisions. Discovery repeats because the server handles each request independently. @@ -558,7 +595,8 @@ without printing tokens or raw sidecar errors. ### Try direct calls and a sidecar outage -Keep a before-and-after capture of Amir's attempt to resolve unassigned +We've checked the chat path. The same restrictions must hold without the chat +interface and when the decision service fails. Keep a before-and-after capture of Amir's attempt to resolve unassigned `INC-2001`, plus a successful update to assigned `INC-1001`. A denial matters only when the incident remains unchanged. An MCP client with a valid Amir token must not be able to bypass this by calling `update_incident_status` directly @@ -573,36 +611,47 @@ unchanged. Restore it with `docker compose start cedarling`. Restart the stack between exercises to restore sample incidents; a resolved incident cannot be resolved again. -## Protect other agent tools at the server - -![Tool, resource, and prompt requests all pass through the MCP server's authorization gate.](./assets/reusable-pattern.png) +
+Warning: Before deploying this application -_Protect MCP discovery and execution at the server boundary, not in the model._ +Replace local HTTP and the bundled learning IdP with HTTPS and a configured +OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), +Gluu, Auth0, or Okta. -The component returning data or making a change must check permission, even when an -agent selected the operation and the user confirmed it. Cover discovery, -resources, and prompts as well as tools that change data. +Production needs a database that keeps incidents after restarts, managed user +permissions, protected credentials, and stored audit logs. The lab keeps a fixed +account map and incidents in memory; free-model chat may be unavailable. -Trace `src/mcp/authorization.ts`, `src/mcp/server.ts`, -`src/incidents/repository.ts`, `policy-store/`, and `compose.yaml` in the -[completed P3 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance). +Across hosts, restrict the sidecar network and authenticate service connections +with mTLS through a service mesh[^5] or reverse proxy. JWT validation alone does +not establish which service is calling the sidecar. For a production stack, consider Agama Lab Policy Designer for policy authoring, Jans Auth for issuing tokens, and Lock Server for centralized decision logs. See [Cedarling production solutions](https://cedarling.dev/solutions). -Next, P4 asks a related question about human workflows: may a publisher still -rely on an approval after the content or reviewer's authority has changed? +These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI +on macOS and Windows. If a platform-specific step fails, +[open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues). -
-Warning: This setup is for local practice +
-- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta. -- Production needs a database that keeps incidents after restarts, managed user permissions, protected credentials, and stored audit logs. The lab keeps a fixed account map and incidents in memory; free-model chat may be unavailable. -- Across hosts, restrict the sidecar network and authenticate service connections with mTLS through a service mesh[^5] or reverse proxy. JWT validation alone does not establish which service is calling the sidecar. -- These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues). +## What we've learned -
+Amir could confirm an update to an unassigned incident in the starting app. +We've now blocked that operation at the MCP server while keeping his assigned +work available. Dana's supervisor role permits broader access, and Eve receives +no incident capabilities. Those differences come from the signed identity and +current server facts, not from the model's choice or the user's confirmation. + +For another agent application, place the check where a tool, resource, or prompt +returns data or changes state. The server must wait for a valid allowed +decision, including on direct calls. Moving Cedarling into a sidecar changes +how we request that decision; enforcement still belongs to the application. + +In [P4](https://cedarling.dev/learn/secure-editorial-publishing), we'll check +whether an editorial approval remains valid after the content or reviewer's +permission changes. [^1]: A [Cedarling sidecar](https://docs.jans.io/stable/cedarling/developer/sidecar/cedarling-sidecar-overview/) is a separate service that exposes Cedarling decisions to the application over HTTP. In P3, Docker Compose runs that service beside the MCP server. diff --git a/p4-editorial-publishing/docs/assets/authorization-model-v2.webp b/p4-editorial-publishing/docs/assets/authorization-model-v2.webp deleted file mode 100644 index a25eb92..0000000 Binary files a/p4-editorial-publishing/docs/assets/authorization-model-v2.webp and /dev/null differ diff --git a/p4-editorial-publishing/docs/assets/enforcement-v2.webp b/p4-editorial-publishing/docs/assets/enforcement-v2.webp deleted file mode 100644 index 05be96c..0000000 Binary files a/p4-editorial-publishing/docs/assets/enforcement-v2.webp and /dev/null differ diff --git a/p4-editorial-publishing/docs/assets/expected-outcomes-v2.webp b/p4-editorial-publishing/docs/assets/expected-outcomes-v2.webp deleted file mode 100644 index 089fa18..0000000 Binary files a/p4-editorial-publishing/docs/assets/expected-outcomes-v2.webp and /dev/null differ diff --git a/p4-editorial-publishing/docs/assets/missing-authorization-v2.webp b/p4-editorial-publishing/docs/assets/missing-authorization-v2.webp deleted file mode 100644 index d15378a..0000000 Binary files a/p4-editorial-publishing/docs/assets/missing-authorization-v2.webp and /dev/null differ diff --git a/p4-editorial-publishing/docs/assets/reusable-pattern-v2.webp b/p4-editorial-publishing/docs/assets/reusable-pattern-v2.webp deleted file mode 100644 index c4acf13..0000000 Binary files a/p4-editorial-publishing/docs/assets/reusable-pattern-v2.webp and /dev/null differ diff --git a/p4-editorial-publishing/docs/tutorials.md b/p4-editorial-publishing/docs/tutorials.md index 35b00f3..ece3cd8 100644 --- a/p4-editorial-publishing/docs/tutorials.md +++ b/p4-editorial-publishing/docs/tutorials.md @@ -5,7 +5,7 @@ summary: Bind editorial approval to exact content and current authority before a order: 50 socialImage: ./assets/social-card.webp socialImageAlt: Next.js Server Actions check current editorial facts with Cedarling before protected publishing effects. -lastVerified: 2026-10-01T09:46:20Z +lastVerified: 2026-10-07T19:33:56Z --- # Secure Editorial Publishing with Cedarling @@ -15,7 +15,7 @@ reviews and publishes them. We'll use Cedarling to check permissions as an article moves from draft to review and publication. What does an approval cover when the author changes the article afterward? -Our editorial app accepts the old approval. It also lets an editor approve +Our editorial app accepts the old approval. It also lets an author approve their own writing and accepts approvals from reviewers whose review permission has since been revoked. We'll start with self-approval and use Cedarling to close all three gaps: @@ -28,7 +28,11 @@ checks on article creation, reads, edits, submission, review, and publication. ## Build the integration or try the finished app - To build the integration, start with [Run the starting application](#run-the-starting-application), then add the policies and server checks. -- To try the finished app, run the [complete tagged project](https://github.com/GluuFederation/cedarling-tutorials/tree/p4-editorial-publishing-v1.0.1/p4-editorial-publishing) using its README, then go to [Check approvals and publication](#check-approvals-and-publication). This version already uses Cedarling. +- To try the finished app, run the [finished project on main](https://github.com/GluuFederation/cedarling-tutorials/tree/main/p4-editorial-publishing) using its README, then go to [Check approvals and publication](#check-approvals-and-publication). This version already uses Cedarling. + +If you're building from the starting project, open each **Required step** section +and complete its instructions before continuing. These sections contain the files +and changes we'll need.
What you'll need @@ -41,9 +45,10 @@ checks on article creation, reads, edits, submission, review, and publication.
-Copy whole files from GitHub's raw-file view into your baseline checkout; don't -switch to the finished tag. The short examples aren't complete replacements. -Create missing parent directories. Paths and commands are relative to +At each copying step, open the linked file on GitHub, choose **Raw**, and copy +its full contents into the stated destination in your baseline checkout. The +short examples explain the parts we'll focus on. Create missing parent +directories first. Paths and commands are relative to `p4-editorial-publishing/`; repository-level `shared/` files go one directory above it. ## Meet the authors and editors @@ -62,19 +67,6 @@ reviewing fixed versions of their content, and publishing approved work. All three can create articles in their tenant. Writing, reviewing, and publishing require separate permissions. Editors cannot approve their own content. -```mermaid -flowchart TD - accTitle: Server authorization for editorial changes - accDescr: A Next.js Server Action authenticates the caller and loads current facts. Cedarling evaluates them, then the service either rejects the change or rechecks the facts in a database transaction. - Form["Browser form"] --> Action["Next.js Server Action: authenticate and validate"] - Action --> Facts["Load actor, revision, approval and authority from SQLite"] - Facts --> PDP["Embedded Cedarling: unsigned evaluation"] - PDP --> Check["Editorial service enforces decision"] - Check -->|"DENY or failure"| Stop["No mutation"] - Check -->|"ALLOW"| Transaction["SQLite transaction: recheck authorized facts"] - Transaction -->|"Facts still match"| Save["Commit change"] -``` - Cedarling runs on the server using `authorizeUnsigned()`. The bundled Node.js `oidc-provider` authenticates the user first. The server then loads the current user, revision, and authority records from SQLite for Cedarling to evaluate. @@ -83,9 +75,8 @@ The browser uses the server's decisions to enable or disable controls. ## Try the workflow before adding Cedarling -![Before authorization, an earlier approval can be reused after the article changes, and self-approval is possible.](./assets/missing-authorization-v2.webp) - -_The starting app does not require a different person to approve the current revision._ +Before we add those checks, let's see what the starting app allows. We'll keep +the results so we can repeat the same requests after integration. ### Run the starting application @@ -153,7 +144,8 @@ using the commands above before the coding steps. ## Where should we check permission? -Open the baseline's +We've seen an approval succeed when it should not. To find where to enforce +the rule, open the baseline's [`src/server/service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p4-editorial-publishing/src/server/service.ts) and find `publish()`. It calls the authorization function before writing to the database. In @@ -166,19 +158,19 @@ case "publication.publish": return fact("publisherAuthorityCurrent") && fact("approvalPresent"); ``` -Nothing here checks which revision was approved or whether its reviewer still -has permission to review. We'll replace this check with Cedarling. The existing New article +We'll replace this check with a Cedarling decision that requires approval of +the current revision by someone who still has review permission. The existing New article form, authentication, CSRF checks, and database version guards stay in place. ## Decide who can review and publish -![Publication depends on the current revision digest, independent approval, and current editorial authority.](./assets/authorization-model-v2.webp) - -_The current revision needs approval from a different person who still has review permission._ +We have found the check we'll replace. Now we'll put the editorial rules in a +policy store, starting with who may review and which approval may permit publication. ### Create the policy store -Create the readable [directory-based store](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format): +Let's create a `policy-store/` directory at the P4 project root, using Cedarling's +[directory-based format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format): ```text policy-store/ @@ -188,17 +180,21 @@ policy-store/ editorial.cedar ``` -Create the complete policy-store files from the pinned version: +
+Required step: Create the three policy-store files + +Create each file below and copy its complete linked contents: -- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/metadata.json) -- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/schema.cedarschema) -- [`policy-store/policies/editorial.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar) +- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/policy-store/metadata.json): identifies the store and its version, `1.0.0`. +- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/policy-store/schema.cedarschema): defines principals, resources, actions, and the facts each request requires. +- [`policy-store/policies/editorial.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/policy-store/policies/editorial.cedar): defines the permissions for reading, writing, reviewing, and publishing. -Use the store's metadata and version `1.0.0`. Identity comes from the application's -verified OIDC session and current database user, so Cedarling needs no -trusted-issuer mapping for these unsigned requests. Principal, revision, and -permission records arrive with each request; no default entities, -templates, or custom issuers are needed. +
+ +The application verifies the OIDC session and loads the current database user +before making an unsigned request. It supplies the principal, revision, and +permission records on each call. These three files describe how Cedarling +should evaluate those facts.[^3] | Design question | P4 answer | | --------------------------------------- | ------------------------------------------------------------------------------------------------- | @@ -218,26 +214,28 @@ outdated writes; the revision version identifies the approved content. ### Choose an action and resource for each operation -Each request uses the current user loaded by the server and an action -such as `P4EditorialPublishing::Action::"CreateArticle"`. +With those types in place, we can map each operation to an action and resource. +Every request uses the current user loaded by the server and an action such as +`P4EditorialPublishing::Action::"CreateArticle"`. -| Capability | Action | Resource | Additional context | Effect waiting for ALLOW | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ | ------------------------------------------------- | ------------------------------------- | -| `article.create` | [`CreateArticle`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L51 "create-tenant-article") | Authenticated tenant | Empty | Insert article and first draft | -| `article.read` | [`ReadArticle`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L2 "read-tenant-article") | Current article | Empty | Return article and revision evidence | -| `revision.edit` | [`EditRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L10 "author-revision") | Current revision | Empty | Save owned draft or create next draft | -| `revision.submit` | [`SubmitRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L10 "author-revision") | Current revision | Empty | Submit owned content for review | -| `revision.approve` | [`ApproveRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L21 "independent-review") | Exact submitted revision | Current editor authority | Record independent approval | -| `revision.reject` | [`RejectRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L21 "independent-review") | Exact submitted revision | Current editor authority | Record independent rejection | -| `publication.publish` | [`PublishRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L34 "publish-exact-approved-revision") | Exact current revision | Current publisher authority and approval evidence | Publish that revision | +| Capability | Action | Resource | Additional context | Effect waiting for ALLOW | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ------------------------------------------------- | ------------------------------------- | +| `article.create` | [`CreateArticle`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/policy-store/policies/editorial.cedar#L51 "create-tenant-article") | Authenticated tenant | Empty | Insert article and first draft | +| `article.read` | [`ReadArticle`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/policy-store/policies/editorial.cedar#L2 "read-tenant-article") | Current article | Empty | Return article and revision evidence | +| `revision.edit` | [`EditRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/policy-store/policies/editorial.cedar#L10 "author-revision") | Current revision | Empty | Save owned draft or create next draft | +| `revision.submit` | [`SubmitRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/policy-store/policies/editorial.cedar#L10 "author-revision") | Current revision | Empty | Submit owned content for review | +| `revision.approve` | [`ApproveRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/policy-store/policies/editorial.cedar#L21 "independent-review") | Exact submitted revision | Current editor authority | Record independent approval | +| `revision.reject` | [`RejectRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/policy-store/policies/editorial.cedar#L21 "independent-review") | Exact submitted revision | Current editor authority | Record independent rejection | +| `publication.publish` | [`PublishRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/policy-store/policies/editorial.cedar#L34 "publish-exact-approved-revision") | Exact current revision | Current publisher authority and approval evidence | Publish that revision | Creating an article targets the tenant because the article does not yet exist. Its author and tenant are set by the server, not accepted from the form. ### Require an independent reviewer and approval of the current revision -In `policies/editorial.cedar`, a reviewer must have permission and must not be -the author: +Review and publication are separate actions because approval may become invalid +before we use it. In `policy-store/policies/editorial.cedar`, the review rule +requires a permitted reviewer who is not the author: ```cedar // policy-store/policies/editorial.cedar @@ -259,7 +257,7 @@ permission to review, and `principal.id` equals `resource.author_id`. Ana belong the same tenant, has permission to review, and is not the author, so she can review that submitted revision. -Publication needs another rule: +When Ana later publishes the revision, we need to check the approval again: ```cedar // policy-store/policies/editorial.cedar @@ -286,6 +284,28 @@ revision's ID, version, and digest. Revoking Omar's editor authority makes `context.approval.reviewer_authority_current` false even when the content hasn't changed. That approval can no longer permit publication. +```mermaid +flowchart TD + accTitle: Publication checks the current revision and its approval + accDescr: Publication requires the same tenant, a current publisher, and an approved revision. Approval must match the revision ID, version, and digest, and its independent reviewer must still be authorized. Any failed check denies publication. + subgraph Checks["Cedarling: permission to publish"] + State["Same tenant +Current publisher +Approved revision"] + Match["Approval matches +revision ID, version +and digest"] + Reviewer["Reviewer is not author +Review permission +is still current"] + end + State --> Result{"All checks pass?"} + Match --> Result + Reviewer --> Result + Result -->|"Yes"| Allow["ALLOW publication"] + Result -->|"No"| Deny["DENY publication"] +``` + The same file contains `read-tenant-article`, `author-revision`, and `create-tenant-article`: tenant members read their articles, authors edit/submit their own revisions, and tenant members create articles in their tenant. @@ -294,23 +314,49 @@ against conflicting writes after authorization succeeds. ## Add Cedarling to the server -![The Next.js Server Action enforces an embedded Cedarling decision and rechecks state before committing a change.](./assets/enforcement-v2.webp) +The policy now describes when an operation is allowed. We'll connect it to the +editorial service so each change waits for that decision and for a final +database check: -_The server checks permission, then checks that the facts still match before saving a change._ +```mermaid +flowchart TD + accTitle: The server checks permission before committing an editorial change + accDescr: A browser form reaches a Next.js Server Action, which authenticates the user and validates the request. The service loads current facts from SQLite, calls embedded Cedarling, and enforces its decision. It commits only after the transaction confirms that the authorized facts still match; denial, evaluation failure, or changed facts stop the write. + Form["Browser form"] --> Action["Server Action +Authenticate and validate"] + Action --> Facts["Editorial service +Load current facts"] + Facts --> PDP["Cedarling (PDP) +Evaluate request"] + PDP -->|"DENY or failure"| Stop["Service stops the write"] + PDP -->|"ALLOW"| Transaction{"Facts still match +in transaction?"} + Transaction -->|"No"| Conflict["Conflict: no write"] + Transaction -->|"Yes"| Save["Commit change"] +``` ### Build the archive and load Cedarling -From P4, install exact versions: +From the P4 directory, let's install Cedarling and the tools that package and +validate our policy store: ```bash pnpm add --save-exact @janssenproject/cedarling_wasm@0.0.468 fflate@0.8.3 pnpm add --save-dev --save-exact @cedar-policy/cedar-wasm@4.12.0 ``` -Save the repository-level archive builder and its declaration: +We'll use a shared builder to validate the policy files and package them for Cedarling. -- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/shared/policy-store.mjs) -- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/shared/policy-store.d.mts) +
+Required step: Create the shared archive-builder files + +Create these files in the repository's `shared/` directory, one level above +P4. Copy the complete contents from each link: + +- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/policy-store.mjs): validates and packages the readable policy store into a Cedar archive. +- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/policy-store.d.mts): provides TypeScript declarations for the builder. + +
Run the shared builder to create the archive: @@ -350,18 +396,30 @@ that explicitly stop the runtime. ### Pass the current revision and approval to Cedarling -Save these complete files together to connect the permission checks: +With the archive ready, we can replace the baseline's authorization check and +connect it to the service and forms. Copy the files in the list below together, +then we'll follow the publication request through them. -- [`src/server/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/authorization.ts) -- [`src/server/models.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/models.ts) -- [`src/server/errors.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/errors.ts) -- [`src/server/database.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/database.ts) -- [`src/server/service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/service.ts) -- [`src/server/runtime.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/runtime.ts) -- [`app/actions.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/actions.ts) -- [`app/action-button.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/action-button.tsx) -- [`app/articles/[articleId]/page.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/articles/[articleId]/page.tsx) -- [`next.config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/next.config.ts) +
+Required step: Copy the server integration and form files + +Replace the contents of these existing P4 files with the complete linked versions: + +- [`src/server/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/src/server/authorization.ts): loads Cedarling, maps each capability to a request, and records its decision. +- [`src/server/models.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/src/server/models.ts): defines the revision, approval, and authority evidence passed between server modules. +- [`src/server/errors.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/src/server/errors.ts): distinguishes denied, conflicting, and unavailable operations. +- [`src/server/database.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/src/server/database.ts): rechecks approval and authority evidence inside write transactions. +- [`src/server/service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/src/server/service.ts): loads trusted facts and requires permission before each protected operation. +- [`src/server/runtime.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/src/server/runtime.ts): creates the shared server runtime with Cedarling authorization. +- [`app/actions.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/app/actions.ts): verifies each form request, calls the service, and reports the outcome. +- [`app/articles/[articleId]/page.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/app/articles/[articleId]/page.tsx): uses server permission previews to show available actions and feedback. +- [`next.config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/next.config.ts): keeps the Cedarling package external to the Next.js server bundle. + +Create [`app/action-button.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/app/action-button.tsx) +and copy its full contents. It displays pending and disabled action buttons +with an explanation when the action is unavailable. + +
`src/server/service.ts` validates form input and loads the current article, revision, latest approval, and publisher permission. The authorization module @@ -431,7 +489,8 @@ editor or approval is still valid. ### Check permission before saving the publication -In `publish()`, `this.allow()` must succeed before `this.database.publish()` runs: +We have a Cedarling result; now the service must act on it. In `publish()`, +`this.allow()` must succeed before `this.database.publish()` runs: ```ts // src/server/service.ts @@ -478,16 +537,24 @@ show readable outcomes without internal request IDs. ## Finish setup and restart the app -Save the runtime preparation and packaging files: +We've connected the decisions to the workflow. Before restarting, let's make +startup and builds package the policy store, and finish the error handling. -- [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/scripts/setup.ts) -- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/Dockerfile) -- [`app/auth/callback/route.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/auth/callback/route.ts) -- [`app/error.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/error.tsx) -- [`app/styles.css`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/styles.css) +
+Required step: Replace the setup, packaging, and feedback files + +Replace these existing P4 files with their complete linked contents: + +- [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/scripts/setup.ts): packages the policy store during development setup. +- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/Dockerfile): builds and includes the archive in the runtime image. +- [`app/auth/callback/route.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/app/auth/callback/route.ts): handles login failures with bounded responses and logs. +- [`app/error.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/app/error.tsx): gives the user a recovery option when the workspace is unavailable. +- [`app/styles.css`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/app/styles.css): styles action feedback and disabled controls in the editorial workspace. + +
Apply the `build` entry shown below to your existing -[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/package.json), keeping +[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p4-editorial-publishing/package.json), keeping the other dependencies and scripts. Replace the `exclude` list in `tsconfig.json` with the following. The baseline's @@ -532,9 +599,8 @@ that Riley cannot self-approve and Ana can still review and publish his work. ## Check approvals and publication -![Self-approval, stale revision approval, and revoked reviewer authority are denied; a valid current approval is allowed.](./assets/expected-outcomes-v2.webp) - -_Changing an article's revision or its reviewer's permission can block publication._ +The app is running with Cedarling. Let's repeat our original self-approval +attempt, then check that Ana can publish only while the approval is valid. Reset the exercise database first: restarting does not undo the article changes or restore Omar's revoked permission. Reset clears editorial records, sessions, @@ -558,7 +624,7 @@ Sign in again as Riley before continuing. ### Try self-approval without the button restriction -Use a fresh article for this exercise. As Riley, create an article, submit its +We'll start with a fresh article. As Riley, create an article, submit its draft for review, and confirm that its current revision says **submitted**. The **Approve revision** button is disabled. To check the server also denies approval, open developer tools on that article's local page and run: @@ -585,6 +651,8 @@ application outcome and unchanged record, not the HTTP status alone. ### Publish reviewed work, then change its content or authority +Self-approval is now blocked at the server. Next, we'll check the valid +Riley-to-Ana flow and the two ways an earlier approval can become unusable. If Omar's authority is revoked after approval but the content stays unchanged, can Ana still publish it? Check the publication policy, then compare your answer with the revocation row below. @@ -605,7 +673,8 @@ the article Riley created during this exercise. ### Read the decision and publication logs -`authorization.context` links the application `requestId`, actor, capability, +To connect what we see in the app with the decision that produced it, find the +matching server logs. `authorization.context` links the application `requestId`, actor, capability, and `preview` or `enforcement` phase to the Cedarling request ID in `cedarlingRequestId`. Cedarling's JSON prints all policy reasons and errors. An allowed publication cites `publish-exact-approved-revision`. @@ -622,8 +691,11 @@ Logs kept in memory expire after five minutes, so they are not a lasting audit r ### Check concurrent changes and unavailable decisions -The coding steps update runtime files, not the baseline's historical tests. -For the full automated checks, use a separate checkout of the [finished tag](https://github.com/GluuFederation/cedarling-tutorials/tree/p4-editorial-publishing-v1.0.1/p4-editorial-publishing) and install its locked project and shared IdP dependencies. +We've checked the workflow and its logs. The automated suite also exercises +concurrent changes and unavailable authorization. Run it from a separate +checkout of the [finished project on main](https://github.com/GluuFederation/cedarling-tutorials/tree/main/p4-editorial-publishing), +which contains the current integration tests, and install its locked project +and shared IdP dependencies. Stop your learner stack to free ports 17004 and 18004, then run: ```bash @@ -645,36 +717,45 @@ in the baseline and is denied afterward. Also record an allowed publication of the exact approved revision. Keep tokens, cookies, and CSRF values out of recordings. -## Use the same checks in your own approval workflow - -![Use current revision, independent approval, and current authority to decide, then recheck before committing.](./assets/reusable-pattern-v2.webp) - -_Bind approval to the exact content being published, then enforce the current decision._ +
+Warning: Before deploying this application -Treat approval as a record for a specific version of the content. Before using -it, check that it still matches and that the reviewer still has permission. -Save the change only if the facts checked by Cedarling remain current. +Replace local HTTP and the bundled learning IdP with HTTPS and a configured +OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), +Gluu, Auth0, or Okta. -Follow `src/server/authorization.ts`, `src/server/service.ts`, -`src/server/database.ts`, `app/actions.ts`, `policy-store/`, and the tests in the -[completed P4 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p4-editorial-publishing-v1.0.1/p4-editorial-publishing). +Control who can change editorial permissions, protect sessions, and store audit +logs. Keep database transaction and content-version checks alongside policy +decisions. For a production stack, consider Agama Lab Policy Designer for policy authoring, Jans Auth for issuing tokens, and Lock Server for centralized decision logs. See [Cedarling production solutions](https://cedarling.dev/solutions). -In P5, we'll check access to individual fields, aggregates, and CSV exports -instead of granting it to everyone who can open a page. +These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI +on macOS and Windows. If a platform-specific step fails, +[open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues). -
-Warning: This setup is for local practice +
-- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta. -- Control who can change editorial permissions, protect sessions, and store audit logs. Keep database transaction and content-version checks alongside policy decisions. -- These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues). +## What we've learned -
+We started with an app that accepted self-approval and reused approvals after +the content or reviewer permissions changed. We've now required an independent +reviewer, bound the approval to an exact revision, and checked current authority +before publication. The valid Riley-to-Ana workflow still succeeds. We also +checked the server directly, so a disabled button is not our only protection. + +For another approval workflow, identify the content version being approved and +the permission that makes its reviewer eligible. Load those facts before the +protected action, and save the change only if they still match when it commits. + +In [P5](https://cedarling.dev/learn/protect-sensitive-data-exports), we'll check +access to individual fields, aggregates, and CSV exports +instead of granting it to everyone who can open a page. [^1]: A digest is a reproducible fingerprint of the normalized revision content. P4 compares it with the approval's digest, while current reviewer authority and revision state remain separate requirements. [^2]: The pinned [`cedarling_wasm` JavaScript API](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468) accepts a JSON-string request. Converting to JSON does not verify the values; P4 loads them from its trusted server state. + +[^3]: In this application, the server authenticates the user before calling `authorizeUnsigned()`, so the store does not need trusted-issuer mappings. Current facts arrive in each request, and these rules use neither default entities nor templates. Other applications may need those parts of the [policy-store format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/). diff --git a/p5-dataguard/docs/assets/authorization-model-v2.webp b/p5-dataguard/docs/assets/authorization-model-v2.webp deleted file mode 100644 index e70749e..0000000 Binary files a/p5-dataguard/docs/assets/authorization-model-v2.webp and /dev/null differ diff --git a/p5-dataguard/docs/assets/enforcement-v2.webp b/p5-dataguard/docs/assets/enforcement-v2.webp deleted file mode 100644 index e200a5b..0000000 Binary files a/p5-dataguard/docs/assets/enforcement-v2.webp and /dev/null differ diff --git a/p5-dataguard/docs/assets/expected-outcomes-v2.webp b/p5-dataguard/docs/assets/expected-outcomes-v2.webp deleted file mode 100644 index 62f1c76..0000000 Binary files a/p5-dataguard/docs/assets/expected-outcomes-v2.webp and /dev/null differ diff --git a/p5-dataguard/docs/assets/missing-authorization-v2.webp b/p5-dataguard/docs/assets/missing-authorization-v2.webp deleted file mode 100644 index 42c764e..0000000 Binary files a/p5-dataguard/docs/assets/missing-authorization-v2.webp and /dev/null differ diff --git a/p5-dataguard/docs/assets/reusable-pattern-v2.webp b/p5-dataguard/docs/assets/reusable-pattern-v2.webp deleted file mode 100644 index 65a6ad9..0000000 Binary files a/p5-dataguard/docs/assets/reusable-pattern-v2.webp and /dev/null differ diff --git a/p5-dataguard/docs/tutorials.md b/p5-dataguard/docs/tutorials.md index 52c6374..792874d 100644 --- a/p5-dataguard/docs/tutorials.md +++ b/p5-dataguard/docs/tutorials.md @@ -5,7 +5,7 @@ summary: Authorize fields, tenant-bound queries, aggregate disclosure, and CSV e order: 60 socialImage: ./assets/social-card.webp socialImageAlt: A Hono API checks query plans with embedded Cedarling before releasing rows, aggregates, or CSV exports. -lastVerified: 2026-10-01T09:46:20Z +lastVerified: 2026-10-07T19:56:00Z --- # Protect Sensitive Data Exports with Cedarling @@ -28,7 +28,11 @@ Leah's allowed finance reports and exports should keep working. ## Build the integration or try the finished app - To build the integration, start with [Run the starting application](#run-the-starting-application), then add the policies and server checks. -- To try the finished app, run the [complete tagged project](https://github.com/GluuFederation/cedarling-tutorials/tree/p5-dataguard-v1.0.1/p5-dataguard) using its README, then go to [Check queries and exports for each user](#check-queries-and-exports-for-each-user). This version already uses Cedarling. +- To try the finished app, run the [finished project on main](https://github.com/GluuFederation/cedarling-tutorials/tree/main/p5-dataguard) using its README, then go to [Check queries and exports for each user](#check-queries-and-exports-for-each-user). This version already uses Cedarling. + +If you're building from the starting project, open each **Required step** section +and complete its instructions before continuing. These sections contain the files +and changes we'll need.
What you'll need @@ -41,9 +45,10 @@ Leah's allowed finance reports and exports should keep working.
-Copy whole files from GitHub's raw-file view into your baseline checkout; don't -switch to the finished tag. The short examples aren't complete replacements. -Create missing parent directories. Paths and commands are relative to +At each copying step, open the linked file on GitHub, choose **Raw**, and copy +its full contents into the stated destination in your baseline checkout. The +short examples explain the parts we'll focus on. Create missing parent +directories first. Paths and commands are relative to `p5-dataguard/`; repository-level `shared/` files go one directory above it. ## Who should see which data? @@ -66,19 +71,6 @@ rows or aggregates[^1], such as a count of employees by department. An aggregate must include at least five records in every returned group. This minimum applies even when the purpose and role are allowed. -```mermaid -flowchart TD - accTitle: Query and export authorization in the completed API - accDescr: Each query or export operation loads current facts and asks server-side Cedarling. The API enforces the decision and rechecks facts before releasing data or changing an export. - Query["React: query or create export"] --> Plan["API: validate plan and load current analyst, fields and group counts"] - Export["Download or revoke request"] --> Saved["API: reload analyst and saved export"] - Plan --> PDP["Embedded Cedarling: unsigned evaluation"] - Saved --> PDP - PDP --> Check["API enforces decision"] - Check -->|"DENY or failure"| Stop["No protected result or change"] - Check -->|"ALLOW"| Effect["Recheck facts and perform the authorized operation"] -``` - Cedarling runs only on the server, using `authorizeUnsigned()`. The bundled Node.js `oidc-provider` authenticates the user first; SQLite supplies the current analyst's role and tenant. "Unsigned" describes the authorization @@ -87,9 +79,8 @@ permissions. ## Request salary data as a support analyst -![Amina sends a same-origin salary and bonus request directly to the baseline Hono API, which returns the fields after FAKE ALLOW.](./assets/missing-authorization-v2.webp) - -_The starting app lists Salary and Bonus; a direct API request checks access without using the field picker._ +Let's use Amina's session to request compensation fields directly. This lets +us check the API without relying on which fields the interface offers. ### Run the starting application @@ -156,7 +147,8 @@ the commands above before the coding steps. ## Where should we check permission? -Open the baseline's +Amina received protected values. To stop that disclosure, we need a check before +the query runs. Open the baseline's [`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p5-dataguard/src/server/app.ts) and find `evaluatePlan()`. After authentication, CSRF, and input validation, it executes the query here: @@ -177,13 +169,14 @@ wait for `ALLOW`. ## Decide which queries and exports to allow -![Field metadata, row results, aggregates, and exports enter the Hono API, which requests a Cedarling decision before releasing or withholding an effect.](./assets/authorization-model-v2.webp) - -_Queries and exports need separate decisions, even when they use the same data._ +We'll start by expressing which fields Amina may use, then apply the same plan +rules to queries, aggregates, and export creation. Saved exports will have their +own ownership rule when someone downloads or revokes them. ### Create the policy store -Use the [directory-based policy-store format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format): +Let's create `policy-store/` at the project root, following the +[directory-based policy-store format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format): ```text policy-store/ @@ -195,13 +188,18 @@ policy-store/ exports.cedar ``` -Create the complete policy-store files from the pinned version: +
+Required step: Create the five policy-store files + +Create these files and copy their complete linked contents: -- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/metadata.json) -- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/schema.cedarschema) -- [`policy-store/policies/fields.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/fields.cedar) -- [`policy-store/policies/plans.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/plans.cedar) -- [`policy-store/policies/exports.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/exports.cedar) +- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/metadata.json) identifies the store and version. +- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/schema.cedarschema) defines analysts, fields, datasets, exports, and request context. +- [`policy-store/policies/fields.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/policies/fields.cedar) controls field metadata visibility. +- [`policy-store/policies/plans.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/policies/plans.cedar) checks queries, aggregates, and export creation. +- [`policy-store/policies/exports.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/policies/exports.cedar) checks downloads and revocation of saved exports. + +
Use the store's metadata and version `1.0.0`. Namespace `P5DataGuard` contains four entity types: `Analyst`, `Dataset`, `Field`, and `Export`. OIDC and SQLite @@ -233,16 +231,17 @@ the `P5DataGuard::Action` namespace. | Capability | Action | Resource | Context | Effect waiting for ALLOW | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ----------------------------------------------------- | --------------------------------------- | -| `dataset.inspect` | [`InspectDataset`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/fields.cedar#L2 "inspect-fields") | Each candidate field | Empty | Return field metadata to React | -| `data.query` | [`Query`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/plans.cedar#L2 "authorized-plan") | Workforce dataset | Validated row-plan facts | Return rows within the plan's limit | -| `data.aggregate` | [`Aggregate`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/plans.cedar#L2 "authorized-plan") | Workforce dataset | Plan facts and current minimum group size | Return aggregate values | -| `data.export` | [`CreateExport`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/plans.cedar#L2 "authorized-plan") | Workforce dataset | Saved candidate plan facts; group size for aggregates | Create CSV within the plan's limits | -| `export.download` | [`DownloadExport`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/exports.cedar#L2 "manage-own-finance-export") | Current saved export | Empty | Prepare CSV response | -| `export.revoke` | [`RevokeExport`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/exports.cedar#L2 "manage-own-finance-export") | Current saved export | Empty | Commit revocation and clean up its file | +| `dataset.inspect` | [`InspectDataset`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/policies/fields.cedar#L2 "inspect-fields") | Each candidate field | Empty | Return field metadata to React | +| `data.query` | [`Query`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/policies/plans.cedar#L2 "authorized-plan") | Workforce dataset | Validated row-plan facts | Return rows within the plan's limit | +| `data.aggregate` | [`Aggregate`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/policies/plans.cedar#L2 "authorized-plan") | Workforce dataset | Plan facts and current minimum group size | Return aggregate values | +| `data.export` | [`CreateExport`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/policies/plans.cedar#L2 "authorized-plan") | Workforce dataset | Saved candidate plan facts; group size for aggregates | Create CSV within the plan's limits | +| `export.download` | [`DownloadExport`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/policies/exports.cedar#L2 "manage-own-finance-export") | Current saved export | Empty | Prepare CSV response | +| `export.revoke` | [`RevokeExport`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/policy-store/policies/exports.cedar#L2 "manage-own-finance-export") | Current saved export | Empty | Commit revocation and clean up its file | ### Check the plan when querying or exporting -The rule in `policies/plans.cedar` checks tenant, fields, purpose, and group size +Let's trace Amina's request through the plan rule. In `policies/plans.cedar`, +it checks tenant, fields, purpose, and group size for exports as well as queries: ```cedar @@ -311,9 +310,21 @@ integrity, and whether the export is still active. No matching permit gives DENY ## Add Cedarling to the API -![The Hono API validates current facts and uses embedded Cedarling before rechecking and releasing rows or CSV; denial releases nothing.](./assets/enforcement-v2.webp) +We have separate rules for each disclosure. Now we'll call Cedarling from the +API and keep the protected result behind its decision. -_The API enforces the decision; browser controls are guidance, not authority._ +```mermaid +flowchart TD + accTitle: Separate decisions for each data disclosure + accDescr: The Hono API loads current analyst facts for every request. Field metadata, query plans, and saved exports use distinct Cedarling actions before the API releases data or changes an export. + Fields["Inspect field metadata"] --> API["Hono: current analyst and resource facts"] + Plan["Query or create export"] --> API + Saved["Download or revoke export"] --> API + API --> PDP["Embedded Cedarling"] + PDP --> Check{"API decision"} + Check -->|"ALLOW"| Effect["Recheck facts, then perform operation"] + Check -->|"DENY or failure"| Stop["No protected result or export change"] +``` ### Build the archive and load Cedarling @@ -324,10 +335,18 @@ pnpm add --save-exact @janssenproject/cedarling_wasm@0.0.468 fflate@0.8.3 pnpm add --save-dev --save-exact @cedar-policy/cedar-wasm@4.12.0 ``` -Save the repository-level archive builder and its declaration: +We'll use a shared builder to validate the policy files and package them for Cedarling. + +
+Required step: Create the shared archive builder + +Create these files in the repository-level `shared/` directory and copy their +complete linked contents: + +- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/policy-store.mjs) validates and packages the policy store. +- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/shared/policy-store.d.mts) provides its TypeScript declarations. -- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/shared/policy-store.mjs) -- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/shared/policy-store.d.mts) +
Run the builder to create the archive: @@ -364,19 +383,30 @@ functions, passes them to `buildApp()`, and calls `shutDown()` when shutting dow ### Pass the analyst and query facts to Cedarling -Save these complete files together to connect the permission checks: +With the archive ready, let's connect Cedarling to the query and export routes. +Copy the files below together, then follow how they build a request for Amina. + +
+Required step: Add authorization and update the query and export service + +Create these files and copy their complete linked contents: + +- [`src/server/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/server/authorization.ts) builds Cedarling requests and records decisions. +- [`src/server/errors.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/server/errors.ts) defines bounded errors for denied or unavailable authorization. + +Replace these existing files with their complete linked contents: -- [`src/server/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/authorization.ts) -- [`src/server/errors.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/errors.ts) -- [`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/app.ts) -- [`src/server/database.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/database.ts) -- [`src/server/query.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/query.ts) -- [`src/server/export-service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/export-service.ts) -- [`src/server/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/main.ts) -- [`src/server/config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/config.ts) -- [`src/shared/contracts.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/shared/contracts.ts) +- [`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/server/app.ts) checks permission at each API boundary. +- [`src/server/database.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/server/database.ts) rechecks current sessions and query facts within transactions. +- [`src/server/query.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/server/query.ts) validates plans and compiles bounded SQL. +- [`src/server/export-service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/server/export-service.ts) creates CSV files and checks saved export references. +- [`src/server/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/server/main.ts) starts and closes Cedarling with the API. +- [`src/server/config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/server/config.ts) supplies the policy archive location. +- [`src/shared/contracts.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/shared/contracts.ts) defines query, export, and permission-preview types. -Remove `src/server/permissive-trace.ts`; the completed handlers no longer use it. +Remove `src/server/permissive-trace.ts`; authorization now records the actual decisions. + +
Before authorization, the server checks that the plan uses only supported forms. It collects fields used in selected columns, calculations, grouping, and filters. @@ -448,7 +478,8 @@ no unchecked fields. ### Check permission before running the query -Row queries, aggregates, and export creation use `executePlan()` in +We can now build and evaluate the request. The next step is to make the query +wait for its result. Row queries, aggregates, and export creation use `executePlan()` in `src/server/app.ts`. It requires permission before `database.execute()`: ```ts @@ -492,7 +523,7 @@ changed, it throws `409 authorization_state_changed` without running the query. `effect` uses the allowed rows to build the response or create the CSV. The SQL compiler continues to use fixed identifiers and bound values in -[`src/server/query.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/query.ts). +[`src/server/query.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/server/query.ts). Download and revoke routes reload the saved export and authorize it separately, using its stored owner. They recheck expiry, state, and current facts before @@ -501,12 +532,20 @@ CSV file then fails; failed cleanup must not restore access. ### Show which actions are available -Save the matching React controls and API client: +The API protects the results. Let's also give users controls that reflect what +the server currently permits. + +
+Required step: Update the query form and export controls + +Replace these existing files with their complete linked contents: + +- [`src/web/App.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/web/App.tsx) applies permission previews to the form and export actions. +- [`src/web/api.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/web/api.ts) requests previews and handles bounded API errors. +- [`src/web/Icon.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/web/Icon.tsx) supplies icons used by those controls. +- [`src/web/styles.css`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/src/web/styles.css) lays out controls, results, and permission messages. -- [`src/web/App.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/web/App.tsx) -- [`src/web/api.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/web/api.ts) -- [`src/web/Icon.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/web/Icon.tsx) -- [`src/web/styles.css`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/web/styles.css) +
`POST /api/authorization` previews the exact selected plan and export operations. It returns which actions are allowed, not rows or a CSV. React ignores outdated preview @@ -519,18 +558,26 @@ there is no browser Cedarling instance or separate client permission table. ## Finish setup and restart the app -Save the runtime preparation and packaging files: +The server and controls are connected. Before testing them, let's make sure +native startup and Docker both build the policy archive. + +
+Required step: Update setup, reset, and Docker packaging + +Replace these existing files with their complete linked contents: + +- [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/scripts/setup.ts) prepares configuration, builds the archive, and seeds data. +- [`scripts/reset.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/scripts/reset.ts) restores sample data and clears sessions and exports. +- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/Dockerfile) includes the archive in the runtime image. -- [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/scripts/setup.ts) -- [`scripts/reset.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/scripts/reset.ts) -- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/Dockerfile) +
Apply the `build` entry shown below to your existing -[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/package.json), keeping +[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/package.json), keeping the other dependencies and scripts. The setup script builds the archive; the production build must too. The existing -[`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/scripts/dev.mjs) +[`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/main/p5-dataguard/scripts/dev.mjs) runs both steps before starting the IdP and API, so `pnpm dev` needs no further change: @@ -561,9 +608,8 @@ We'll retry her salary request, then check the allowed queries and exports. ## Check queries and exports for each user -![Amina's compensation request is denied but support rows are allowed; Theo gets bounded counts but not rows or small groups; Leah can use finance data and her own exports.](./assets/expected-outcomes-v2.webp) - -_The allowed response depends on the requested fields, result type, and caller._ +We'll compare the same salary request before and after integration, then check +that Amina, Theo, and Leah can still do their permitted work. ### Retry Amina's salary request @@ -628,7 +674,8 @@ same-origin request. Never publish actual session or download credentials. ### Read the query and export logs -`authorization.context` connects the application request, actor, capability, +Let's use the logs to distinguish a permission decision from a completed query +or export. `authorization.context` connects the application request, actor, capability, and preview/enforcement phase with a Cedarling request ID. Cedarling's decision JSON includes full `diagnostics.reason` and `errors`. Field inspection has one decision per field; seeing both ALLOW and DENY in that batch is normal. @@ -650,8 +697,10 @@ memory expire after five minutes, so they are not a lasting audit record. ### Check changed permissions and unavailable decisions -The coding steps update runtime files, not the baseline's historical tests. -For the full automated checks, use a separate checkout of the [finished tag](https://github.com/GluuFederation/cedarling-tutorials/tree/p5-dataguard-v1.0.1/p5-dataguard) and install its locked project and shared IdP dependencies. +We've checked the visible results and logs. The automated suite also covers +changes while authorization is pending. Use a separate checkout of the +[finished project on main](https://github.com/GluuFederation/cedarling-tutorials/tree/main/p5-dataguard), +which includes the integration tests, and install its locked project and shared IdP dependencies. Stop your learner stack to free ports 17005 and 18005, then run: ```bash @@ -675,41 +724,42 @@ records, exports, and sessions; sign in again. In Docker use Do not reset a database you want to keep. Capture the identical unauthorized compensation request before and after, and Leah's allowed finance export. -## Protect data in your own application - -![The Hono API uses embedded Cedarling decisions for field metadata, rows, aggregates, and CSV exports, then rechecks saved exports at download and revoke.](./assets/reusable-pattern-v2.webp) - -_Authorization remains necessary when a generated export is downloaded later._ - -Opening a dashboard, seeing a field name, reading a row, releasing an aggregate, -and downloading a generated file are different capabilities. Check each one -where the server releases the data, using current trusted facts. +
+Warning: Before deploying this application -The same approach can protect a GraphQL API: authorize each sensitive field, -row set, aggregate, or export at the resolver or service boundary that releases -it, not just at the query entry point. P5 itself uses Hono, not GraphQL; -see [GraphQL's authorization guidance](https://graphql.org/learn/authorization/). +Replace local HTTP and the bundled learning IdP with HTTPS and a configured +OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), +Gluu, Auth0, or Okta. -Follow `src/server/authorization.ts`, `src/server/app.ts`, `src/server/query.ts`, -`src/server/export-service.ts`, `policy-store/`, and the tests in the -[completed P5 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p5-dataguard-v1.0.1/p5-dataguard). +Control permission changes, secure stored data, and store audit logs. Keep +parameterized SQL, transactions, expiry, and file cleanup alongside authorization. +A five-record minimum is a teaching example: users can still learn sensitive +facts by combining repeated queries. For a production stack, consider Agama Lab Policy Designer for policy authoring, Jans Auth for issuing tokens, and Lock Server for centralized decision logs. See [Cedarling production solutions](https://cedarling.dev/solutions). -Next, P6 moves to field-inspection workflows and authorization over submitted -work rather than query results. +These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI +on macOS and Windows. If a platform-specific step fails, +[open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues). -
-Warning: This setup is for local practice +
-- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta. -- Control permission changes, secure stored data, and store audit logs. Keep parameterized SQL, transactions, expiry, and file cleanup alongside authorization. -- A five-record minimum is a teaching example. Users can still learn sensitive facts through repeated queries; this rule alone cannot prevent that. -- These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues). +## What we've learned -
+Amina's salary request now fails, while her support queries still work. We've +given Theo bounded aggregates and kept Leah's finance reports and exports +available. Each disclosure has its own check: field metadata, query results, +aggregate values, export creation, and access to a saved export. Current tenant, +role, purpose, and ownership facts decide access; an earlier allowed query +does not grant every later operation. + +For your own reporting service, follow the data to each point where it leaves +the server. Check permission there and recheck facts that may have changed +while waiting. We can use the same approach in a GraphQL API by enforcing +field and data permissions in the resolvers or services that release them; +see [GraphQL's authorization guidance](https://graphql.org/learn/authorization/). [^1]: An aggregate reports a calculation over multiple records, such as a count by department, without returning the individual rows. Small groups and repeated queries can still reveal sensitive facts, so P5 treats aggregate release as its own authorization boundary.