notion-enterprise-rbac
Configure Notion enterprise access control with OAuth, workspace permissions, and audit logging. Use when implementing OAuth public integrations, managing multi-workspace access, or building permission-aware Notion applications. Trigger with phrases like "notion SSO", "notion RBAC", "notion enterprise", "notion OAuth", "notion permissions", "notion multi-workspace".
What this skill does
# Notion Enterprise RBAC
## Overview
Implement enterprise-grade access control for Notion integrations. This covers the full OAuth 2.0 authorization flow for public integrations (multi-tenant), per-workspace token storage with encryption at rest, Notion's page-level permission model and how to handle `ObjectNotFound` vs `RestrictedResource`, an application-level role system (admin/editor/viewer) layered on top of Notion's permissions, comprehensive audit logging to a Notion database, and workspace deauthorization cleanup.
## Prerequisites
- Notion public integration created at https://www.notion.so/my-integrations (for OAuth)
- `@notionhq/client` v2+ installed (`npm install @notionhq/client`)
- Python alternative: `notion-client` (`pip install notion-client`)
- Database for storing per-workspace tokens (PostgreSQL, DynamoDB, etc.)
- HTTPS endpoint for OAuth callback (required by Notion)
## Instructions
### Step 1: OAuth 2.0 Authorization Flow
Notion uses OAuth 2.0 for public integrations to access external workspaces:
```typescript
import { Client } from '@notionhq/client';
import crypto from 'crypto';
// Step 1: Build the authorization URL
function getAuthorizationUrl(state: string): string {
const params = new URLSearchParams({
client_id: process.env.NOTION_OAUTH_CLIENT_ID!,
response_type: 'code',
owner: 'user', // 'user' = user-level token, 'workspace' = workspace-level
redirect_uri: process.env.NOTION_REDIRECT_URI!,
state, // CSRF protection — must verify on callback
});
return `https://api.notion.com/v1/oauth/authorize?${params}`;
}
// Step 2: Exchange authorization code for access token
async function exchangeCodeForToken(code: string): Promise<{
access_token: string;
bot_id: string;
workspace_id: string;
workspace_name: string;
workspace_icon: string | null;
owner: { type: string; user?: { id: string; name: string } };
duplicated_template_id: string | null;
}> {
const credentials = Buffer.from(
`${process.env.NOTION_OAUTH_CLIENT_ID}:${process.env.NOTION_OAUTH_CLIENT_SECRET}`
).toString('base64');
const response = await fetch('https://api.notion.com/v1/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${credentials}`,
},
body: JSON.stringify({
grant_type: 'authorization_code',
code,
redirect_uri: process.env.NOTION_REDIRECT_URI,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`OAuth token exchange failed: ${error.error}`);
}
return response.json();
}
// Step 3: Create a Client for a specific workspace
function createWorkspaceClient(accessToken: string): Client {
return new Client({ auth: accessToken, timeoutMs: 30_000 });
}
// Express route handlers
app.get('/auth/notion', (req, res) => {
const state = crypto.randomUUID();
req.session.oauthState = state;
res.redirect(getAuthorizationUrl(state));
});
app.get('/auth/notion/callback', async (req, res) => {
// Verify CSRF state
if (req.query.state !== req.session.oauthState) {
return res.status(403).send('Invalid state — possible CSRF attack');
}
if (req.query.error) {
return res.status(400).send(`Authorization denied: ${req.query.error}`);
}
const tokenData = await exchangeCodeForToken(req.query.code as string);
await storeWorkspaceToken(tokenData);
res.redirect(`/dashboard?workspace=${encodeURIComponent(tokenData.workspace_name)}`);
});
```
**Python — OAuth flow:**
```python
import base64
import requests
from notion_client import Client
def exchange_code_for_token(code: str) -> dict:
credentials = base64.b64encode(
f"{os.environ['NOTION_OAUTH_CLIENT_ID']}:{os.environ['NOTION_OAUTH_CLIENT_SECRET']}".encode()
).decode()
response = requests.post(
"https://api.notion.com/v1/oauth/token",
headers={
"Content-Type": "application/json",
"Authorization": f"Basic {credentials}",
},
json={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": os.environ["NOTION_REDIRECT_URI"],
},
)
response.raise_for_status()
return response.json()
def create_workspace_client(access_token: str) -> Client:
return Client(auth=access_token, timeout_ms=30_000)
```
### Step 2: Token Storage and Permission-Aware API Calls
**Per-workspace token management:**
```typescript
import { isNotionClientError, APIErrorCode } from '@notionhq/client';
interface WorkspaceToken {
botId: string; // Primary key — unique per installation
workspaceId: string;
workspaceName: string;
accessToken: string; // MUST be encrypted at rest
ownerUserId: string;
authorizedAt: Date;
lastUsedAt: Date;
}
// In production, use a database with encryption (e.g., AWS KMS, column-level encryption)
class TokenStore {
private tokens = new Map<string, WorkspaceToken>();
async store(tokenData: any): Promise<void> {
const entry: WorkspaceToken = {
botId: tokenData.bot_id,
workspaceId: tokenData.workspace_id,
workspaceName: tokenData.workspace_name,
accessToken: tokenData.access_token, // Encrypt before storing!
ownerUserId: tokenData.owner?.user?.id ?? '',
authorizedAt: new Date(),
lastUsedAt: new Date(),
};
this.tokens.set(entry.botId, entry);
}
async getClient(botId: string): Promise<Client> {
const token = this.tokens.get(botId);
if (!token) {
throw new Error(`No token found for bot ${botId}. User needs to re-authorize.`);
}
token.lastUsedAt = new Date();
return new Client({ auth: token.accessToken, timeoutMs: 30_000 });
}
async revoke(botId: string): Promise<void> {
this.tokens.delete(botId);
}
async listWorkspaces(): Promise<{ botId: string; name: string; authorizedAt: Date }[]> {
return Array.from(this.tokens.values()).map(t => ({
botId: t.botId,
name: t.workspaceName,
authorizedAt: t.authorizedAt,
}));
}
}
const tokenStore = new TokenStore();
```
**Permission-aware API calls — handle Notion's page-level permissions:**
```typescript
// Notion returns ObjectNotFound for pages not shared with the integration
// This is NOT the same as the page being deleted
async function safePageAccess(notion: Client, pageId: string) {
try {
return await notion.pages.retrieve({ page_id: pageId });
} catch (error) {
if (!isNotionClientError(error)) throw error;
switch (error.code) {
case APIErrorCode.ObjectNotFound:
// Page exists but is NOT shared with this integration
// User needs to share it via the "..." menu > Connections
console.log(`Page ${pageId} not accessible. Ask user to share via Connections.`);
return null;
case APIErrorCode.RestrictedResource:
// Integration lacks the required capability (read/update/insert/delete)
console.log(`Integration lacks capability for ${pageId}. Check integration settings.`);
return null;
case APIErrorCode.Unauthorized:
// Token was revoked — user needs to re-authorize
console.log(`Token revoked. User needs to re-authorize.`);
return null;
default:
throw error;
}
}
}
// List all pages accessible to the integration (discovers shared content)
async function discoverAccessiblePages(notion: Client): Promise<string[]> {
const pageIds: string[] = [];
let cursor: string | undefined;
do {
const response = await notion.search({
filter: { property: 'object', value: 'page' },
page_size: 100,
start_cursor: cursor,
});
pageIds.push(...response.results.map(r => r.id));
cursor = response.has_more ? response.next_cursor ?? undefined : undefined;
} while (cursor);
return pageIds;
}
```
### Step 3: Application-Level Roles and Audit Logging
**Role-based access control layered on top of Notion permRelated in Security
mac-ops
IncludedComprehensive macOS workstation operations — diagnose kernel panics, identify failing drives, audit launchd startup items, decode wake reasons, triage TCC permission denials, manage APFS snapshots, recover from no-boot. Use for: Mac is slow, slow bootup, won't boot, kernel panic, kernel_task hot, mds_stores CPU, photoanalysisd, cloudd, login loop, gray screen, sleep wake failure, drive failing, IO errors, APFS snapshots eating space, Time Machine local snapshots, Spotlight indexing, launchd, LaunchAgent, LaunchDaemon, login items, TCC permissions, Full Disk Access, Screen Recording denied, Gatekeeper, quarantine, com.apple.quarantine, app is damaged, helper tool, /Library/PrivilegedHelperTools, pmset, wake reasons, dark wake, sysdiagnose, panic.ips, DiagnosticReports, configuration profile, MDM profile, remote diagnostics over SSH.
a11y-audit
IncludedRun accessibility audits on web projects combining automated scanning (axe-core, Lighthouse) with WCAG 2.1 AA compliance mapping, manual check guidance, and structured reporting. Output is configurable: markdown report only, markdown plus machine-readable JSON, or markdown plus issue tracker integration. Use this skill whenever the user mentions "accessibility audit", "a11y audit", "WCAG audit", "accessibility check", "compliance scan", or asks to check a web project for accessibility issues. Also trigger when the user wants to verify WCAG conformance or map findings to a specific standard (CAN-ASC-6.2, EN 301 549, ADA/AODA).
erpclaw
IncludedAI-native ERP system with self-extending OS. Full accounting, invoicing, inventory, purchasing, tax, billing, HR, payroll, advanced accounting (ASC 606/842, intercompany, consolidation), and financial reporting. 413 actions across 14 domains, 43 expansion modules. Constitutional guardrails, adversarial audit, schema migration. Double-entry GL, immutable audit trail, US GAAP.
assess
IncludedAssesses and rates quality 0-10 across multiple dimensions (correctness, maintainability, security, performance, testability, simplicity) with pros/cons analysis. Compares against project conventions and prior decisions from memory. Produces structured evaluation reports with actionable improvement suggestions. Use when evaluating code, designs, architectures, or comparing alternative approaches.
spring-boot-security-jwt
IncludedProvides JWT authentication and authorization patterns for Spring Boot 3.5.x covering token generation with JJWT, Bearer/cookie authentication, database/OAuth2 integration, and RBAC/permission-based access control using Spring Security 6.x. Use when implementing authentication or authorization in Spring Boot applications.
code-hardcode-audit
IncludedDetect hardcoded values, magic numbers, and leaked secrets. TRIGGERS - hardcode audit, magic numbers, PLR2004, secret scanning.