@bonnard/cli
Advanced tools
| # Security Context | ||
| > Implement multi-tenant data isolation for B2B apps using security context and access policies. | ||
| Security context lets you build customer-facing applications where each tenant only sees their own data. It works through the SDK's token exchange mechanism — your server sets the context, and row-level filters are enforced automatically on every query. | ||
| ## When to Use What | ||
| | Use case | Mechanism | Configured in | | ||
| |----------|-----------|---------------| | ||
| | Internal users — teams, roles, field/row restrictions | [Governance](governance) | Dashboard UI | | ||
| | B2B apps — each customer sees only their data | Security context | YAML model + SDK | | ||
| | Both — internal governance + tenant isolation | Both (merged) | Dashboard + YAML | | ||
| ## How It Works | ||
| ``` | ||
| Your server Bonnard Database | ||
| │ │ │ | ||
| ├─ POST /api/sdk/token │ │ | ||
| │ { security_context: │ │ | ||
| │ { tenant_id: "acme" } } ─┤ │ | ||
| │ │ │ | ||
| │◄─ { token, expires_at } ────┤ │ | ||
| │ │ │ | ||
| │ (pass token to frontend) │ │ | ||
| │ │ │ | ||
| ├─ query(measures, dims) ─────┤ │ | ||
| │ Authorization: Bearer ... │ │ | ||
| │ ├─ WHERE tenant_id = 'acme' ──►│ | ||
| │ │ (injected automatically) │ | ||
| │◄─ filtered results ─────────┤◄─────────────────────────────┤ | ||
| ``` | ||
| 1. Your server calls `exchangeToken()` with a `security_context` containing tenant attributes | ||
| 2. Bonnard returns a short-lived scoped token (5 min TTL, refreshable via `fetchToken`) | ||
| 3. The frontend queries using that token — the query engine injects row-level filters from the `access_policy` matching `{securityContext.attrs.X}` values | ||
| 4. Only matching rows are returned — tenants cannot see each other's data | ||
| ## Step-by-Step Setup | ||
| ### 1. Define access_policy in your view YAML | ||
| Add an `access_policy` entry with `group: "*"` (matches all users, including SDK tokens with empty groups) and a row-level filter referencing security context attributes: | ||
| ```yaml | ||
| views: | ||
| - name: orders | ||
| cubes: | ||
| - join_path: base_orders | ||
| includes: "*" | ||
| access_policy: | ||
| - group: "*" | ||
| row_level: | ||
| filters: | ||
| - member: tenant_id | ||
| operator: equals | ||
| values: | ||
| - "{securityContext.attrs.tenant_id}" | ||
| ``` | ||
| The `{securityContext.attrs.tenant_id}` placeholder is replaced at query time with the value from the token's security context. | ||
| ### 2. Deploy your model | ||
| ```bash | ||
| bon deploy | ||
| ``` | ||
| ### 3. Exchange a token server-side | ||
| In your API route or server action, exchange your secret key for a scoped token by calling the `/api/sdk/token` endpoint: | ||
| ```typescript | ||
| // In your API route handler: | ||
| export async function GET(request: Request) { | ||
| const tenantId = await getTenantFromSession(request); | ||
| const res = await fetch('https://app.bonnard.dev/api/sdk/token', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify({ | ||
| security_context: { tenant_id: tenantId }, | ||
| }), | ||
| }); | ||
| const { token } = await res.json(); | ||
| return Response.json({ token }); | ||
| } | ||
| ``` | ||
| ### 4. Query from the frontend | ||
| ```typescript | ||
| import { createClient } from '@bonnard/sdk'; | ||
| const bonnard = createClient({ | ||
| fetchToken: async () => { | ||
| const res = await fetch('/api/bonnard-token'); | ||
| const { token } = await res.json(); | ||
| return token; | ||
| }, | ||
| }); | ||
| const result = await bonnard.query({ | ||
| measures: ['orders.revenue', 'orders.count'], | ||
| dimensions: ['orders.status'], | ||
| }); | ||
| // Only returns rows where tenant_id matches the exchanged context | ||
| ``` | ||
| ## Multiple Filters | ||
| You can filter on multiple attributes. Each filter is AND'd: | ||
| ```yaml | ||
| access_policy: | ||
| - group: "*" | ||
| row_level: | ||
| filters: | ||
| - member: tenant_id | ||
| operator: equals | ||
| values: | ||
| - "{securityContext.attrs.tenant_id}" | ||
| - member: region | ||
| operator: equals | ||
| values: | ||
| - "{securityContext.attrs.region}" | ||
| ``` | ||
| ```typescript | ||
| const res = await fetch('https://app.bonnard.dev/api/sdk/token', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify({ | ||
| security_context: { tenant_id: 'acme', region: 'eu' }, | ||
| }), | ||
| }); | ||
| const { token } = await res.json(); | ||
| ``` | ||
| ## Combining with Governance | ||
| Security context policies and governance policies are **merged**, not replaced. You can safely use both: | ||
| - `group: "*"` entries in YAML handle B2B tenant isolation (matches all users including SDK tokens) | ||
| - Governance policies from the dashboard handle internal user access control (field visibility, row filters by group) | ||
| When both are active on the same view, the final `access_policy` contains all entries. Cube evaluates them based on the user's group membership — SDK tokens have `groups: []`, so they match `group: "*"` but not named groups. | ||
| ```yaml | ||
| # Developer-defined in YAML — always active | ||
| access_policy: | ||
| - group: "*" | ||
| row_level: | ||
| filters: | ||
| - member: tenant_id | ||
| operator: equals | ||
| values: | ||
| - "{securityContext.attrs.tenant_id}" | ||
| # Governance adds these at runtime (configured in dashboard): | ||
| # - group: sales | ||
| # member_level: | ||
| # includes: [revenue, count] | ||
| # - group: finance | ||
| # member_level: | ||
| # includes: [margin, cost] | ||
| ``` | ||
| ## Token Exchange Reference | ||
| **Endpoint:** `POST /api/sdk/token` | ||
| **Headers:** `Authorization: Bearer bon_sk_...` (your secret key) | ||
| | Body parameter | Type | Description | | ||
| |----------------|------|-------------| | ||
| | `security_context` | `Record<string, string>` | Key-value pairs. Keys must match `{securityContext.attrs.X}` placeholders in your access_policy. Max 20 keys, key max 64 chars, value max 256 chars. | | ||
| | `expires_in` | `number` | Token TTL in seconds. Min 60, max 3600, default 900. | | ||
| **Response:** `{ token: string, expires_at: string }` | ||
| **Token properties:** | ||
| - Default TTL: 15 minutes (configurable 1–60 min via `expires_in`) | ||
| - Renewable via `fetchToken` callback (SDK re-fetches automatically before expiry) | ||
| - Contains `groups: []` (empty) — matches `group: "*"` policies only | ||
| ## See Also | ||
| - [governance](governance) — Dashboard-managed access control for internal users | ||
| - [querying.sdk](querying.sdk) — SDK query reference | ||
| - [syntax.context-variables](syntax.context-variables) — Context variable syntax reference |
@@ -71,2 +71,3 @@ # Bonnard Documentation | ||
| - [governance](governance) - User and group-level permissions | ||
| - [security-context](security-context) - B2B multi-tenancy with security context | ||
| - [catalog](catalog) - Browse your data model in the browser | ||
@@ -73,0 +74,0 @@ - [slack-teams](slack-teams) - AI agents in team chat (coming soon) |
@@ -7,2 +7,4 @@ # Governance | ||
| > **Building a B2B product?** Governance is for managing _internal_ user access via the dashboard. For tenant isolation in customer-facing apps (where each customer sees only their data), see [security-context](security-context). | ||
| ## How It Works | ||
@@ -74,2 +76,17 @@ | ||
| ## Governance and Developer-Defined Policies | ||
| Governance policies from the dashboard are **merged** with any `access_policy` entries you define in your YAML model files. This lets you combine both approaches: | ||
| - **Developer-defined policies** — written in YAML, typically for B2B tenant isolation using `group: "*"` (matches all users, including SDK tokens) | ||
| - **Governance policies** — configured in the dashboard UI for internal user access control | ||
| When governance injects policies: | ||
| 1. If a view has governance policies **and** developer-defined `access_policy` entries, both are merged into a single list | ||
| 2. If a view has developer-defined `access_policy` but **no** governance policies, the developer entries are preserved as-is | ||
| 3. If a view has **neither**, it receives a default policy restricting access to ungoverned users | ||
| This means you can safely define tenant isolation in YAML and layer dashboard governance on top — neither overwrites the other. | ||
| ## Best Practices | ||
@@ -86,1 +103,2 @@ | ||
| - [views](views) — Creating curated data views | ||
| - [security-context](security-context) — B2B multi-tenancy with security context |
@@ -42,2 +42,37 @@ # SDK | ||
| ## Multi-tenant queries | ||
| When building B2B apps where each customer should only see their own data, use **security context** with token exchange. Your server exchanges a secret key for a scoped token, then your frontend queries with that token — row-level filters are enforced automatically. | ||
| ```typescript | ||
| // Server-side: exchange secret key for a scoped token | ||
| const res = await fetch('https://app.bonnard.dev/api/sdk/token', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify({ | ||
| security_context: { tenant_id: currentCustomer.id }, | ||
| }), | ||
| }); | ||
| const { token } = await res.json(); | ||
| // Pass token to the frontend | ||
| ``` | ||
| ```typescript | ||
| // Client-side: query with the scoped token | ||
| const bonnard = createClient({ | ||
| fetchToken: async () => token, // from your server | ||
| }); | ||
| const result = await bonnard.query({ | ||
| measures: ['orders.revenue'], | ||
| dimensions: ['orders.status'], | ||
| }); | ||
| // Only returns rows where tenant_id matches — enforced server-side | ||
| ``` | ||
| This requires an `access_policy` on your view with a `{securityContext.attrs.tenant_id}` filter. See [security-context](security-context) for the full setup guide. | ||
| ## What you can build | ||
@@ -44,0 +79,0 @@ |
@@ -145,6 +145,23 @@ # Context Variables | ||
| ## Deprecated: SECURITY_CONTEXT | ||
| ## Row-Level Security via access_policy | ||
| `SECURITY_CONTEXT` is deprecated. Use `query_rewrite` for security filtering instead. | ||
| For row-level filtering based on the current user or tenant, use `access_policy` with `{securityContext.attrs.X}` in filter values: | ||
| ```yaml | ||
| views: | ||
| - name: orders | ||
| access_policy: | ||
| - group: "*" | ||
| row_level: | ||
| filters: | ||
| - member: tenant_id | ||
| operator: equals | ||
| values: | ||
| - "{securityContext.attrs.tenant_id}" | ||
| ``` | ||
| Security context attributes are set during token exchange (SDK) or via governance user attributes (dashboard). See [security-context](security-context) for the full B2B multi-tenancy guide. | ||
| > **Note:** The upstream `SECURITY_CONTEXT` SQL variable is deprecated. Use `access_policy` row-level filters with `{securityContext.attrs.X}` instead. | ||
| ## See Also | ||
@@ -155,1 +172,2 @@ | ||
| - cubes.extends | ||
| - security-context |
+1
-1
| { | ||
| "name": "@bonnard/cli", | ||
| "version": "0.2.13", | ||
| "version": "0.2.14", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "bin": { |
409355
2.61%71
1.43%