Loading...
Loading...
Enterprise-grade access control patterns to prevent broken access control, IDOR, tenant data leaks, privilege escalation, and admin route exposure. Covers RBAC, ABAC, ownership checks, multi-tenant scoping, policy middleware, row-level security, and audit logging for Node.js, Cloudflare Workers, Laravel, and Python. Use when protecting routes, implementing roles, reviewing authorization, or fixing OWASP A01 findings.
npx skill4agent add afu-it/security-for-vibecoders access-control-patternsif (user.role === 'admin')app.get('/api/invoices/:id', async (req, res) => {
const invoice = await db.invoice.findUnique({ where: { id: req.params.id } });
res.json(invoice); // Any logged-in user can read any invoice by guessing ID
});app.get('/api/invoices/:id', requireAuth, async (req, res) => {
const invoice = await db.invoice.findFirst({
where: {
id: req.params.id,
tenantId: req.user.tenantId,
},
});
if (!invoice) return res.status(404).json({ error: 'Not found' });
res.json(invoice);
});404403type Role = 'owner' | 'admin' | 'member' | 'viewer';
type Permission =
| 'invoice:read'
| 'invoice:create'
| 'invoice:update'
| 'invoice:delete'
| 'user:invite'
| 'billing:manage';
const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
owner: ['invoice:read', 'invoice:create', 'invoice:update', 'invoice:delete', 'user:invite', 'billing:manage'],
admin: ['invoice:read', 'invoice:create', 'invoice:update', 'user:invite'],
member: ['invoice:read', 'invoice:create'],
viewer: ['invoice:read'],
};
function hasPermission(role: Role, permission: Permission): boolean {
return ROLE_PERMISSIONS[role]?.includes(permission) ?? false;
}interface AuthContext {
userId: string;
tenantId: string;
role: Role;
permissions: Permission[];
}
interface Invoice {
id: string;
tenantId: string;
ownerId: string;
status: 'draft' | 'sent' | 'paid';
}
function canUpdateInvoice(user: AuthContext, invoice: Invoice): boolean {
if (invoice.tenantId !== user.tenantId) return false;
if (!hasPermission(user.role, 'invoice:update')) return false;
if (invoice.status === 'paid') return false;
if (user.role === 'member' && invoice.ownerId !== user.userId) return false;
return true;
}function requireAuth(req: Request, res: Response, next: NextFunction) {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
next();
}
function requirePermission(permission: Permission) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!hasPermission(req.user.role, permission)) {
auditLog({
event: 'access.denied',
userId: req.user.id,
tenantId: req.user.tenantId,
permission,
path: req.path,
});
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.patch('/api/invoices/:id', requirePermission('invoice:update'), async (req, res) => {
const invoice = await db.invoice.findFirst({
where: { id: req.params.id, tenantId: req.user.tenantId },
});
if (!invoice) return res.status(404).json({ error: 'Not found' });
if (!canUpdateInvoice(req.user, invoice)) return res.status(403).json({ error: 'Forbidden' });
const updated = await db.invoice.update({
where: { id: invoice.id },
data: req.body,
});
res.json(updated);
});interface AuthContext {
userId: string;
tenantId: string;
role: Role;
}
async function requireAuth(request: Request, env: Env): Promise<AuthContext> {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) throw new HttpError(401, 'Authentication required');
const session = await verifySession(token, env);
if (!session) throw new HttpError(401, 'Invalid session');
return session;
}
function requirePermission(auth: AuthContext, permission: Permission): void {
if (!hasPermission(auth.role, permission)) {
throw new HttpError(403, 'Forbidden');
}
}
async function getTenantScopedInvoice(db: D1Database, auth: AuthContext, invoiceId: string) {
return db.prepare(
'SELECT * FROM invoices WHERE id = ? AND tenant_id = ?'
).bind(invoiceId, auth.tenantId).first();
}// app/Policies/InvoicePolicy.php
class InvoicePolicy
{
public function view(User $user, Invoice $invoice): bool
{
return $user->tenant_id === $invoice->tenant_id
&& $user->hasPermission('invoice:read');
}
public function update(User $user, Invoice $invoice): bool
{
return $user->tenant_id === $invoice->tenant_id
&& $user->hasPermission('invoice:update')
&& $invoice->status !== 'paid';
}
}
// Controller
public function update(Request $request, Invoice $invoice)
{
$this->authorize('update', $invoice);
$invoice->update($request->validated());
return response()->json($invoice);
}protected static function booted()
{
static::addGlobalScope('tenant', function (Builder $builder) {
if (auth()->check()) {
$builder->where('tenant_id', auth()->user()->tenant_id);
}
});
}from fastapi import Depends, HTTPException
def require_permission(permission: str):
def dependency(user: User = Depends(current_user)):
if permission not in user.permissions:
raise HTTPException(status_code=403, detail="Forbidden")
return user
return dependency
@app.patch("/invoices/{invoice_id}")
async def update_invoice(
invoice_id: str,
payload: InvoiceUpdate,
user: User = Depends(require_permission("invoice:update")),
):
invoice = await db.fetch_one(
"SELECT * FROM invoices WHERE id = :id AND tenant_id = :tenant_id",
{"id": invoice_id, "tenant_id": user.tenant_id},
)
if not invoice:
raise HTTPException(status_code=404, detail="Not found")
if invoice["status"] == "paid":
raise HTTPException(status_code=403, detail="Forbidden")
return await update_invoice_record(invoice_id, payload)ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
-- Set this per request/transaction
SELECT set_config('app.current_tenant_id', 'tenant-uuid-here', true);admin:*function requireAdminAction(action: Permission) {
return [
requireAuth,
requirePermission(action),
requireRecentMfa({ maxAgeMinutes: 15 }),
auditAdminAction(action),
];
}tenantIduserIdit('prevents user from reading another tenant invoice', async () => {
const tenantAInvoice = await createInvoice({ tenantId: 'tenant-a' });
const tenantBUser = await createUser({ tenantId: 'tenant-b' });
const response = await request(app)
.get(`/api/invoices/${tenantAInvoice.id}`)
.set('Authorization', `Bearer ${tokenFor(tenantBUser)}`);
expect(response.status).toBe(404);
});