Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@bonnard/cli

Package Overview
Dependencies
Maintainers
1
Versions
49
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bonnard/cli - npm Package Compare versions

Comparing version
0.2.13
to
0.2.14
+200
dist/docs/topics/security-context.md
# 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
+1
-0

@@ -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": {