@bonnard/cli 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/bon.mjs +25 -0
- package/dist/docs/_index.md +1 -0
- package/dist/docs/topics/dashboards.components.md +19 -0
- package/dist/docs/topics/dashboards.examples.md +74 -1
- package/dist/docs/topics/governance.md +18 -0
- package/dist/docs/topics/querying.sdk.md +35 -0
- package/dist/docs/topics/security-context.md +200 -0
- package/dist/docs/topics/syntax.context-variables.md +20 -2
- package/package.json +1 -1
package/dist/bin/bon.mjs
CHANGED
|
@@ -1908,6 +1908,30 @@ async function annotateCommand(id, options = {}) {
|
|
|
1908
1908
|
}
|
|
1909
1909
|
}
|
|
1910
1910
|
|
|
1911
|
+
//#endregion
|
|
1912
|
+
//#region src/commands/pull.ts
|
|
1913
|
+
async function pullCommand() {
|
|
1914
|
+
const paths = getProjectPaths(process.cwd());
|
|
1915
|
+
console.log(pc.dim("Downloading deployed models..."));
|
|
1916
|
+
try {
|
|
1917
|
+
const files = (await get("/api/deploy/files")).files;
|
|
1918
|
+
const fileKeys = Object.keys(files);
|
|
1919
|
+
if (fileKeys.length === 0) {
|
|
1920
|
+
console.log(pc.yellow("No deployed files found. Have you run `bon deploy` yet?"));
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
1924
|
+
const fullPath = path.join(paths.root, relativePath);
|
|
1925
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
1926
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
1927
|
+
}
|
|
1928
|
+
console.log(pc.green(`Pulled ${fileKeys.length} file(s) to bonnard/`));
|
|
1929
|
+
} catch (err) {
|
|
1930
|
+
console.log(pc.red(`Pull failed: ${err instanceof Error ? err.message : err}`));
|
|
1931
|
+
process.exit(1);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1911
1935
|
//#endregion
|
|
1912
1936
|
//#region src/commands/diff.ts
|
|
1913
1937
|
async function diffCommand(id, options = {}) {
|
|
@@ -3860,6 +3884,7 @@ datasource.command("list").description("List data sources (shows both local and
|
|
|
3860
3884
|
datasource.command("remove").description("Remove a data source from .bon/datasources.yaml (local by default)").argument("<name>", "Data source name").option("--remote", "Remove from Bonnard server instead of local (requires login)").action(datasourceRemoveCommand);
|
|
3861
3885
|
program.command("validate").description("Validate YAML syntax in bonnard/cubes/ and bonnard/views/").action(validateCommand);
|
|
3862
3886
|
program.command("deploy").description("Deploy cubes and views to Bonnard. Requires login, validates, syncs datasources").option("--ci", "Non-interactive mode").requiredOption("-m, --message <text>", "Deploy message describing your changes").action(deployCommand);
|
|
3887
|
+
program.command("pull").description("Download deployed cubes and views from Bonnard").action(pullCommand);
|
|
3863
3888
|
program.command("deployments").description("List deployment history").option("--all", "Show all deployments (default: last 10)").option("--format <format>", "Output format: table or json", "table").action(deploymentsCommand);
|
|
3864
3889
|
program.command("diff").description("Show changes in a deployment").argument("<id>", "Deployment ID").option("--format <format>", "Output format: table or json", "table").option("--breaking", "Show only breaking changes").action(diffCommand);
|
|
3865
3890
|
program.command("annotate").description("Annotate deployment changes with reasoning").argument("<id>", "Deployment ID").option("--data <json>", "Annotations JSON").action(annotateCommand);
|
package/dist/docs/_index.md
CHANGED
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
## Other
|
|
69
69
|
|
|
70
70
|
- [governance](governance) - User and group-level permissions
|
|
71
|
+
- [security-context](security-context) - B2B multi-tenancy with security context
|
|
71
72
|
- [catalog](catalog) - Browse your data model in the browser
|
|
72
73
|
- [slack-teams](slack-teams) - AI agents in team chat (coming soon)
|
|
73
74
|
|
|
@@ -172,6 +172,25 @@ Wrap components in a `<Grid>` tag to arrange them in columns:
|
|
|
172
172
|
|------|------|---------|-------------|
|
|
173
173
|
| `cols` | string | `"2"` | Number of columns in the grid |
|
|
174
174
|
|
|
175
|
+
### Layout Best Practices
|
|
176
|
+
|
|
177
|
+
**Use `##` for sections, not `#`.** The `#` heading renders very large and wastes vertical space. Use `##` for section titles and `###` for subsections. Reserve `#` for the dashboard title only (which is set in frontmatter, not in the body).
|
|
178
|
+
|
|
179
|
+
**Group related charts side by side.** Wrap pairs of charts in `<Grid cols="2">` to avoid long vertical scrolling:
|
|
180
|
+
|
|
181
|
+
```markdown
|
|
182
|
+
<Grid cols="2">
|
|
183
|
+
<BarChart data={by_channel} x="orders.channel" y="orders.total_revenue" title="By Channel" />
|
|
184
|
+
<BarChart data={by_city} x="orders.city" y="orders.total_revenue" title="By City" horizontal />
|
|
185
|
+
</Grid>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Start each section with KPIs.** Place `<BigValue>` cards at the top of a section for at-a-glance metrics, then follow with charts for detail.
|
|
189
|
+
|
|
190
|
+
**Only Grid together components of similar height.** Don't mix a `<BigValue>` with a chart in the same `<Grid>` row — the grid stretches both cells to the tallest item, leaving the BigValue card with a large empty area. Instead, place KPIs in their own row (consecutive BigValues auto-group) and pair charts with other charts of similar size.
|
|
191
|
+
|
|
192
|
+
**Keep it compact.** A good dashboard fits key information in 2-3 screens of scrolling. Use Grids, concise titles, and avoid unnecessary headings between every chart.
|
|
193
|
+
|
|
175
194
|
## Formatting
|
|
176
195
|
|
|
177
196
|
Values are auto-formatted by default — numbers get locale grouping (1,234.56), dates display as "13 Jan 2025", and nulls show as "—". Override with named presets for common currencies and percentages, or use raw Excel format codes for full control.
|
|
@@ -252,16 +252,89 @@ limit: 10
|
|
|
252
252
|
|
|
253
253
|
The `<DateRange>` automatically applies to all queries with a `timeDimension` (here: `trend`). The `<Dropdown>` filters `trend` and `by_city` by channel. The `channels` query populates the dropdown and is never filtered by it.
|
|
254
254
|
|
|
255
|
+
## Compact Multi-Section Dashboard
|
|
256
|
+
|
|
257
|
+
A dashboard with multiple sections, side-by-side charts, and compact layout. Uses `##` headings (not `#`), `<Grid>` for horizontal grouping, and keeps all queries near the components that use them.
|
|
258
|
+
|
|
259
|
+
```markdown
|
|
260
|
+
---
|
|
261
|
+
title: Operations Overview
|
|
262
|
+
description: KPIs, trends, and breakdowns across channels and cities
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
<DateRange name="period" default="last-30-days" label="Period" />
|
|
266
|
+
|
|
267
|
+
` ``query channels
|
|
268
|
+
dimensions: [orders.channel]
|
|
269
|
+
` ``
|
|
270
|
+
|
|
271
|
+
<Dropdown name="channel" dimension="orders.channel" data={channels} queries="kpis,trend,by_city" label="Channel" />
|
|
272
|
+
|
|
273
|
+
## Key Metrics
|
|
274
|
+
|
|
275
|
+
` ``query kpis
|
|
276
|
+
measures: [orders.total_revenue, orders.count, orders.avg_order_value]
|
|
277
|
+
` ``
|
|
278
|
+
|
|
279
|
+
<BigValue data={kpis} value="orders.total_revenue" title="Revenue" fmt="eur" />
|
|
280
|
+
<BigValue data={kpis} value="orders.count" title="Orders" fmt="num0" />
|
|
281
|
+
<BigValue data={kpis} value="orders.avg_order_value" title="Avg Order" fmt="eur2" />
|
|
282
|
+
|
|
283
|
+
## Trends & Breakdown
|
|
284
|
+
|
|
285
|
+
` ``query trend
|
|
286
|
+
measures: [orders.total_revenue]
|
|
287
|
+
timeDimension:
|
|
288
|
+
dimension: orders.created_at
|
|
289
|
+
granularity: week
|
|
290
|
+
` ``
|
|
291
|
+
|
|
292
|
+
` ``query by_channel
|
|
293
|
+
measures: [orders.total_revenue]
|
|
294
|
+
dimensions: [orders.channel]
|
|
295
|
+
orderBy:
|
|
296
|
+
orders.total_revenue: desc
|
|
297
|
+
` ``
|
|
298
|
+
|
|
299
|
+
<Grid cols="2">
|
|
300
|
+
<LineChart data={trend} x="orders.created_at" y="orders.total_revenue" title="Weekly Revenue" yFmt="eur" />
|
|
301
|
+
<BarChart data={by_channel} x="orders.channel" y="orders.total_revenue" title="By Channel" yFmt="eur" />
|
|
302
|
+
</Grid>
|
|
303
|
+
|
|
304
|
+
## Top Cities
|
|
305
|
+
|
|
306
|
+
` ``query by_city
|
|
307
|
+
measures: [orders.total_revenue, orders.count]
|
|
308
|
+
dimensions: [orders.city]
|
|
309
|
+
orderBy:
|
|
310
|
+
orders.total_revenue: desc
|
|
311
|
+
limit: 10
|
|
312
|
+
` ``
|
|
313
|
+
|
|
314
|
+
<Grid cols="2">
|
|
315
|
+
<BarChart data={by_city} x="orders.city" y="orders.total_revenue" title="Revenue by City" horizontal yFmt="eur" />
|
|
316
|
+
<DataTable data={by_city} fmt="orders.total_revenue:eur,orders.count:num0" />
|
|
317
|
+
</Grid>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Key patterns:
|
|
321
|
+
- **`##` headings** for sections — compact, no oversized H1s
|
|
322
|
+
- **Consecutive `<BigValue>`** auto-groups into a row (no Grid needed)
|
|
323
|
+
- **`<Grid cols="2">`** pairs a chart with a table or two charts side by side
|
|
324
|
+
- **Queries defined before their Grid** — keeps the layout clean and components grouped
|
|
325
|
+
|
|
255
326
|
## Tips
|
|
256
327
|
|
|
257
|
-
- **Start with KPIs**: Use `BigValue`
|
|
328
|
+
- **Start with KPIs**: Use `BigValue` at the top for key metrics — consecutive BigValues auto-group into a row
|
|
258
329
|
- **One query per chart**: Each component gets its own query — keep them focused
|
|
330
|
+
- **Use `##` headings**: Reserve `#` for the dashboard title (in frontmatter). Use `##` for sections
|
|
259
331
|
- **Use views**: Prefer view names over cube names when available
|
|
260
332
|
- **Name queries descriptively**: `monthly_revenue` is better than `q1`
|
|
261
333
|
- **Limit large datasets**: Add `limit` to dimension queries to avoid oversized charts
|
|
262
334
|
- **Time series**: Always use `timeDimension` with `granularity` for time-based charts
|
|
263
335
|
- **Multi-series**: Use `series="cube.column"` to split data by a dimension. For bars, default is stacked; use `type="grouped"` for side-by-side
|
|
264
336
|
- **Multiple y columns**: Use comma-separated values like `y="orders.revenue,orders.cases"` to show multiple measures on one chart
|
|
337
|
+
- **Side-by-side charts**: Wrap pairs in `<Grid cols="2">` to reduce vertical scrolling
|
|
265
338
|
|
|
266
339
|
## See Also
|
|
267
340
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
Bonnard provides admin-managed data governance — control which views, columns, and rows each group of users can access. Policies are configured in the web UI and enforced automatically across MCP queries and the API. Changes take effect within one minute.
|
|
6
6
|
|
|
7
|
+
> **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).
|
|
8
|
+
|
|
7
9
|
## How It Works
|
|
8
10
|
|
|
9
11
|
```
|
|
@@ -70,6 +72,21 @@ Policies configured in the web UI are stored in Supabase and injected into the q
|
|
|
70
72
|
|
|
71
73
|
No YAML changes are needed — governance is fully managed through the dashboard.
|
|
72
74
|
|
|
75
|
+
## Governance and Developer-Defined Policies
|
|
76
|
+
|
|
77
|
+
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:
|
|
78
|
+
|
|
79
|
+
- **Developer-defined policies** — written in YAML, typically for B2B tenant isolation using `group: "*"` (matches all users, including SDK tokens)
|
|
80
|
+
- **Governance policies** — configured in the dashboard UI for internal user access control
|
|
81
|
+
|
|
82
|
+
When governance injects policies:
|
|
83
|
+
|
|
84
|
+
1. If a view has governance policies **and** developer-defined `access_policy` entries, both are merged into a single list
|
|
85
|
+
2. If a view has developer-defined `access_policy` but **no** governance policies, the developer entries are preserved as-is
|
|
86
|
+
3. If a view has **neither**, it receives a default policy restricting access to ungoverned users
|
|
87
|
+
|
|
88
|
+
This means you can safely define tenant isolation in YAML and layer dashboard governance on top — neither overwrites the other.
|
|
89
|
+
|
|
73
90
|
## Best Practices
|
|
74
91
|
|
|
75
92
|
1. **Start with broad access, then restrict** — give groups all views first, then fine-tune as needed
|
|
@@ -81,3 +98,4 @@ No YAML changes are needed — governance is fully managed through the dashboard
|
|
|
81
98
|
|
|
82
99
|
- [querying.mcp](querying.mcp) — How AI agents query your semantic layer
|
|
83
100
|
- [views](views) — Creating curated data views
|
|
101
|
+
- [security-context](security-context) — B2B multi-tenancy with security context
|
|
@@ -39,6 +39,41 @@ const result = await bonnard.sql<OrderRow>(
|
|
|
39
39
|
// result.data is OrderRow[]
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
## Multi-tenant queries
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// Server-side: exchange secret key for a scoped token
|
|
48
|
+
const res = await fetch('https://app.bonnard.dev/api/sdk/token', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`,
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
security_context: { tenant_id: currentCustomer.id },
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
const { token } = await res.json();
|
|
59
|
+
// Pass token to the frontend
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Client-side: query with the scoped token
|
|
64
|
+
const bonnard = createClient({
|
|
65
|
+
fetchToken: async () => token, // from your server
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const result = await bonnard.query({
|
|
69
|
+
measures: ['orders.revenue'],
|
|
70
|
+
dimensions: ['orders.status'],
|
|
71
|
+
});
|
|
72
|
+
// Only returns rows where tenant_id matches — enforced server-side
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
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.
|
|
76
|
+
|
|
42
77
|
## What you can build
|
|
43
78
|
|
|
44
79
|
- **Custom dashboards** — Query your semantic layer from Next.js, React, or any frontend
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Security Context
|
|
2
|
+
|
|
3
|
+
> Implement multi-tenant data isolation for B2B apps using security context and access policies.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## When to Use What
|
|
8
|
+
|
|
9
|
+
| Use case | Mechanism | Configured in |
|
|
10
|
+
|----------|-----------|---------------|
|
|
11
|
+
| Internal users — teams, roles, field/row restrictions | [Governance](governance) | Dashboard UI |
|
|
12
|
+
| B2B apps — each customer sees only their data | Security context | YAML model + SDK |
|
|
13
|
+
| Both — internal governance + tenant isolation | Both (merged) | Dashboard + YAML |
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Your server Bonnard Database
|
|
19
|
+
│ │ │
|
|
20
|
+
├─ POST /api/sdk/token │ │
|
|
21
|
+
│ { security_context: │ │
|
|
22
|
+
│ { tenant_id: "acme" } } ─┤ │
|
|
23
|
+
│ │ │
|
|
24
|
+
│◄─ { token, expires_at } ────┤ │
|
|
25
|
+
│ │ │
|
|
26
|
+
│ (pass token to frontend) │ │
|
|
27
|
+
│ │ │
|
|
28
|
+
├─ query(measures, dims) ─────┤ │
|
|
29
|
+
│ Authorization: Bearer ... │ │
|
|
30
|
+
│ ├─ WHERE tenant_id = 'acme' ──►│
|
|
31
|
+
│ │ (injected automatically) │
|
|
32
|
+
│◄─ filtered results ─────────┤◄─────────────────────────────┤
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
1. Your server calls `exchangeToken()` with a `security_context` containing tenant attributes
|
|
36
|
+
2. Bonnard returns a short-lived scoped token (5 min TTL, refreshable via `fetchToken`)
|
|
37
|
+
3. The frontend queries using that token — the query engine injects row-level filters from the `access_policy` matching `{securityContext.attrs.X}` values
|
|
38
|
+
4. Only matching rows are returned — tenants cannot see each other's data
|
|
39
|
+
|
|
40
|
+
## Step-by-Step Setup
|
|
41
|
+
|
|
42
|
+
### 1. Define access_policy in your view YAML
|
|
43
|
+
|
|
44
|
+
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:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
views:
|
|
48
|
+
- name: orders
|
|
49
|
+
cubes:
|
|
50
|
+
- join_path: base_orders
|
|
51
|
+
includes: "*"
|
|
52
|
+
|
|
53
|
+
access_policy:
|
|
54
|
+
- group: "*"
|
|
55
|
+
row_level:
|
|
56
|
+
filters:
|
|
57
|
+
- member: tenant_id
|
|
58
|
+
operator: equals
|
|
59
|
+
values:
|
|
60
|
+
- "{securityContext.attrs.tenant_id}"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The `{securityContext.attrs.tenant_id}` placeholder is replaced at query time with the value from the token's security context.
|
|
64
|
+
|
|
65
|
+
### 2. Deploy your model
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
bon deploy
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 3. Exchange a token server-side
|
|
72
|
+
|
|
73
|
+
In your API route or server action, exchange your secret key for a scoped token by calling the `/api/sdk/token` endpoint:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// In your API route handler:
|
|
77
|
+
export async function GET(request: Request) {
|
|
78
|
+
const tenantId = await getTenantFromSession(request);
|
|
79
|
+
|
|
80
|
+
const res = await fetch('https://app.bonnard.dev/api/sdk/token', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`,
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
security_context: { tenant_id: tenantId },
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const { token } = await res.json();
|
|
92
|
+
return Response.json({ token });
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 4. Query from the frontend
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { createClient } from '@bonnard/sdk';
|
|
100
|
+
|
|
101
|
+
const bonnard = createClient({
|
|
102
|
+
fetchToken: async () => {
|
|
103
|
+
const res = await fetch('/api/bonnard-token');
|
|
104
|
+
const { token } = await res.json();
|
|
105
|
+
return token;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = await bonnard.query({
|
|
110
|
+
measures: ['orders.revenue', 'orders.count'],
|
|
111
|
+
dimensions: ['orders.status'],
|
|
112
|
+
});
|
|
113
|
+
// Only returns rows where tenant_id matches the exchanged context
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Multiple Filters
|
|
117
|
+
|
|
118
|
+
You can filter on multiple attributes. Each filter is AND'd:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
access_policy:
|
|
122
|
+
- group: "*"
|
|
123
|
+
row_level:
|
|
124
|
+
filters:
|
|
125
|
+
- member: tenant_id
|
|
126
|
+
operator: equals
|
|
127
|
+
values:
|
|
128
|
+
- "{securityContext.attrs.tenant_id}"
|
|
129
|
+
- member: region
|
|
130
|
+
operator: equals
|
|
131
|
+
values:
|
|
132
|
+
- "{securityContext.attrs.region}"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const res = await fetch('https://app.bonnard.dev/api/sdk/token', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: {
|
|
139
|
+
'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`,
|
|
140
|
+
'Content-Type': 'application/json',
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
security_context: { tenant_id: 'acme', region: 'eu' },
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
const { token } = await res.json();
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Combining with Governance
|
|
150
|
+
|
|
151
|
+
Security context policies and governance policies are **merged**, not replaced. You can safely use both:
|
|
152
|
+
|
|
153
|
+
- `group: "*"` entries in YAML handle B2B tenant isolation (matches all users including SDK tokens)
|
|
154
|
+
- Governance policies from the dashboard handle internal user access control (field visibility, row filters by group)
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
# Developer-defined in YAML — always active
|
|
160
|
+
access_policy:
|
|
161
|
+
- group: "*"
|
|
162
|
+
row_level:
|
|
163
|
+
filters:
|
|
164
|
+
- member: tenant_id
|
|
165
|
+
operator: equals
|
|
166
|
+
values:
|
|
167
|
+
- "{securityContext.attrs.tenant_id}"
|
|
168
|
+
|
|
169
|
+
# Governance adds these at runtime (configured in dashboard):
|
|
170
|
+
# - group: sales
|
|
171
|
+
# member_level:
|
|
172
|
+
# includes: [revenue, count]
|
|
173
|
+
# - group: finance
|
|
174
|
+
# member_level:
|
|
175
|
+
# includes: [margin, cost]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Token Exchange Reference
|
|
179
|
+
|
|
180
|
+
**Endpoint:** `POST /api/sdk/token`
|
|
181
|
+
|
|
182
|
+
**Headers:** `Authorization: Bearer bon_sk_...` (your secret key)
|
|
183
|
+
|
|
184
|
+
| Body parameter | Type | Description |
|
|
185
|
+
|----------------|------|-------------|
|
|
186
|
+
| `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. |
|
|
187
|
+
| `expires_in` | `number` | Token TTL in seconds. Min 60, max 3600, default 900. |
|
|
188
|
+
|
|
189
|
+
**Response:** `{ token: string, expires_at: string }`
|
|
190
|
+
|
|
191
|
+
**Token properties:**
|
|
192
|
+
- Default TTL: 15 minutes (configurable 1–60 min via `expires_in`)
|
|
193
|
+
- Renewable via `fetchToken` callback (SDK re-fetches automatically before expiry)
|
|
194
|
+
- Contains `groups: []` (empty) — matches `group: "*"` policies only
|
|
195
|
+
|
|
196
|
+
## See Also
|
|
197
|
+
|
|
198
|
+
- [governance](governance) — Dashboard-managed access control for internal users
|
|
199
|
+
- [querying.sdk](querying.sdk) — SDK query reference
|
|
200
|
+
- [syntax.context-variables](syntax.context-variables) — Context variable syntax reference
|
|
@@ -142,12 +142,30 @@ dimensions:
|
|
|
142
142
|
3. **Use COMPILE_CONTEXT** for deployment config — not for per-query logic
|
|
143
143
|
4. **Test filter pushdown** — verify FILTER_PARAMS generates expected SQL
|
|
144
144
|
|
|
145
|
-
##
|
|
145
|
+
## Row-Level Security via access_policy
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
For row-level filtering based on the current user or tenant, use `access_policy` with `{securityContext.attrs.X}` in filter values:
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
views:
|
|
151
|
+
- name: orders
|
|
152
|
+
access_policy:
|
|
153
|
+
- group: "*"
|
|
154
|
+
row_level:
|
|
155
|
+
filters:
|
|
156
|
+
- member: tenant_id
|
|
157
|
+
operator: equals
|
|
158
|
+
values:
|
|
159
|
+
- "{securityContext.attrs.tenant_id}"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
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.
|
|
163
|
+
|
|
164
|
+
> **Note:** The upstream `SECURITY_CONTEXT` SQL variable is deprecated. Use `access_policy` row-level filters with `{securityContext.attrs.X}` instead.
|
|
148
165
|
|
|
149
166
|
## See Also
|
|
150
167
|
|
|
151
168
|
- syntax
|
|
152
169
|
- syntax.references
|
|
153
170
|
- cubes.extends
|
|
171
|
+
- security-context
|