multi-tenant-safety-checker
Ensures tenant isolation at query and policy level using Row Level Security, automated testing, and security audits. Prevents data leakage between tenants. Use for "multi-tenancy", "tenant isolation", "RLS", or "data security".
What this skill does
# Multi-tenant Safety Checker
Ensure complete tenant isolation and prevent data leakage.
## Row Level Security (RLS)
### PostgreSQL RLS Setup
```sql
-- Enable RLS on tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
-- Create policy for users table
CREATE POLICY tenant_isolation_policy ON users
USING (tenant_id = current_setting('app.tenant_id')::INTEGER);
-- Create policy for orders table
CREATE POLICY tenant_isolation_policy ON orders
USING (tenant_id = current_setting('app.tenant_id')::INTEGER);
-- Create policy for products table
CREATE POLICY tenant_isolation_policy ON products
USING (tenant_id = current_setting('app.tenant_id')::INTEGER);
-- Force RLS even for table owners
ALTER TABLE users FORCE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
ALTER TABLE products FORCE ROW LEVEL SECURITY;
```
### Application-Level Tenant Context
```typescript
// middleware/tenant-context.ts
import { PrismaClient } from "@prisma/client";
export class TenantContext {
constructor(private prisma: PrismaClient) {}
async setTenant(tenantId: number): Promise<void> {
await this.prisma.$executeRaw`
SET LOCAL app.tenant_id = ${tenantId}
`;
}
async withTenant<T>(
tenantId: number,
callback: () => Promise<T>
): Promise<T> {
return this.prisma.$transaction(async (tx) => {
// Set tenant context for this transaction
await tx.$executeRaw`SET LOCAL app.tenant_id = ${tenantId}`;
// Execute queries within tenant context
return callback();
});
}
}
// Usage in API route
app.get("/api/orders", async (req, res) => {
const tenantId = req.user.tenantId;
const orders = await tenantContext.withTenant(tenantId, async () => {
return prisma.order.findMany(); // Automatically filtered by RLS
});
res.json(orders);
});
```
## Tenant Isolation Checklist
```markdown
# Multi-tenant Security Checklist
## Database Level
- [ ] All tables have `tenant_id` column
- [ ] `tenant_id` is NOT NULL on all tables
- [ ] Foreign keys include tenant_id checks
- [ ] Row Level Security enabled on all tables
- [ ] RLS policies created for all tables
- [ ] RLS enforced even for table owners
- [ ] Composite indexes include tenant_id
## Application Level
- [ ] Tenant context set on every request
- [ ] Tenant ID validated from JWT/session
- [ ] No raw SQL without tenant filter
- [ ] All queries include tenant_id (if no RLS)
- [ ] API endpoints validate tenant access
- [ ] File uploads scoped to tenant
- [ ] Background jobs include tenant context
## Testing
- [ ] Cross-tenant query tests
- [ ] RLS bypass attempt tests
- [ ] SQL injection with tenant bypass tests
- [ ] Automated regression tests
- [ ] Regular security audits
```
## Automated Security Tests
```typescript
// tests/tenant-isolation.test.ts
import { PrismaClient } from "@prisma/client";
describe("Tenant Isolation", () => {
let prisma: PrismaClient;
let tenant1Id: number;
let tenant2Id: number;
beforeAll(async () => {
prisma = new PrismaClient();
// Create test tenants
const tenant1 = await prisma.tenant.create({
data: { name: "Tenant 1" },
});
const tenant2 = await prisma.tenant.create({
data: { name: "Tenant 2" },
});
tenant1Id = tenant1.id;
tenant2Id = tenant2.id;
// Create test data
await prisma.user.create({
data: {
email: "[email protected]",
tenantId: tenant1Id,
},
});
await prisma.user.create({
data: {
email: "[email protected]",
tenantId: tenant2Id,
},
});
});
it("should not access data from other tenants", async () => {
// Set tenant context to Tenant 1
await prisma.$executeRaw`SET app.tenant_id = ${tenant1Id}`;
// Query users
const users = await prisma.user.findMany();
// Should only see Tenant 1 users
expect(users.length).toBe(1);
expect(users[0].email).toBe("[email protected]");
// Should NOT see Tenant 2 users
expect(users.find((u) => u.email === "[email protected]")).toBeUndefined();
});
it("should prevent cross-tenant updates", async () => {
await prisma.$executeRaw`SET app.tenant_id = ${tenant1Id}`;
// Try to update Tenant 2 user (should fail silently with RLS)
const tenant2User = await prisma.user.findFirst({
where: { email: "[email protected]" },
});
// Should not find user from other tenant
expect(tenant2User).toBeNull();
});
it("should prevent cross-tenant deletes", async () => {
await prisma.$executeRaw`SET app.tenant_id = ${tenant1Id}`;
// Try to delete Tenant 2 user
const result = await prisma.user.deleteMany({
where: { tenantId: tenant2Id },
});
// Should delete 0 rows (RLS prevents access)
expect(result.count).toBe(0);
// Verify user still exists
await prisma.$executeRaw`SET app.tenant_id = ${tenant2Id}`;
const user = await prisma.user.findFirst({
where: { email: "[email protected]" },
});
expect(user).not.toBeNull();
});
it("should handle transaction rollback correctly", async () => {
try {
await prisma.$transaction(async (tx) => {
await tx.$executeRaw`SET LOCAL app.tenant_id = ${tenant1Id}`;
// Create user
await tx.user.create({
data: {
email: "[email protected]",
tenantId: tenant1Id,
},
});
// Force error
throw new Error("Rollback test");
});
} catch (error) {
// Transaction rolled back
}
// User should not exist
await prisma.$executeRaw`SET app.tenant_id = ${tenant1Id}`;
const user = await prisma.user.findFirst({
where: { email: "[email protected]" },
});
expect(user).toBeNull();
});
});
```
## RLS Audit Script
```typescript
// scripts/audit-rls.ts
async function auditRLS() {
const tables = await prisma.$queryRaw<any[]>`
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename != '_prisma_migrations'
`;
console.log("๐ Auditing Row Level Security...\n");
for (const { tablename } of tables) {
// Check if table has tenant_id
const columns = await prisma.$queryRaw<any[]>`
SELECT column_name
FROM information_schema.columns
WHERE table_name = ${tablename}
AND column_name = 'tenant_id'
`;
if (columns.length === 0) {
console.log(`โ ${tablename}: Missing tenant_id column`);
continue;
}
// Check if RLS is enabled
const rlsStatus = await prisma.$queryRaw<any[]>`
SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class
WHERE relname = ${tablename}
`;
if (!rlsStatus[0]?.relrowsecurity) {
console.log(`โ ${tablename}: RLS not enabled`);
continue;
}
if (!rlsStatus[0]?.relforcerowsecurity) {
console.log(`โ ๏ธ ${tablename}: RLS not forced (owners can bypass)`);
}
// Check if policy exists
const policies = await prisma.$queryRaw<any[]>`
SELECT policyname, qual
FROM pg_policies
WHERE tablename = ${tablename}
`;
if (policies.length === 0) {
console.log(`โ ${tablename}: No RLS policies defined`);
} else {
console.log(
`โ
${tablename}: RLS configured (${policies.length} policies)`
);
}
}
}
```
## Composite Indexes for Performance
```sql
-- Composite indexes with tenant_id first
CREATE INDEX idx_orders_tenant_user ON orders(tenant_id, user_id);
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_products_tenant_category ON products(tenant_id, category);
-- This ensures queries filtered by tenant_id are fast
-- SELECT * FROM orders WHERE tenant_id = 1 AND user_id = 123; -- Uses index
```
## Middleware for Automatic Tenant Injection
```typescript
// prisma/middleware.ts
import { Prisma } from "@prisma/client";
eRelated 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.