Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Authorization Rbac

ASecurity

RBAC and ABAC in the application layer: defining roles and claims, mapping the authenticated identity to roles, enforcing roles in middleware and UI. The critical seam: row-level data scoping hands off to data-platform RLS via auth.uid(). Authentication proves identity; authorization controls access.

7 stars
0 votes
0 copies
0 views
Added 9/23/2026
ai-agentsgosqlreactnextjsexpressapidatabasesecurity

Works with

cliapi

Security Analysis

A100/100

Scanned 9/23/2026

$npx -y skills add mcorbett51090/RavenClaude --skill authorization-rbac --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Authorization Rbac?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Authorization Rbac
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mcorbett51090-authorization-rbac/badge)](https://www.skillsdirectory.com/skills/mcorbett51090-authorization-rbac)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
name: authorization-rbac
description: "RBAC and ABAC in the application layer: defining roles and claims, mapping the authenticated identity to roles, enforcing roles in middleware and UI. The critical seam: row-level data scoping hands off to data-platform RLS via auth.uid(). Authentication proves identity; authorization controls access."
---

# Skill: authorization-rbac

> **Invoked by:** any agent designing access-control rules for a web app or API; `ravenclaude-core/security-reviewer` for any role-enforcement change.
>
> **When to invoke:** defining who can access what (roles, permissions); mapping Google-authenticated users to application roles; implementing admin-only sections; designing the handoff between this plugin's identity layer and data-platform's RLS layer.
>
> **Output:** role model documented + roles stored/retrieved + enforcement in middleware + UI conditional rendering + data-row seam to data-platform RLS documented.

---

## Boundary: authentication ≠ authorization ≠ data row access

Three distinct layers — keep them separate:

| Layer | What it answers | Owned by |
|---|---|---|
| **Authentication** | "Who is this person?" — verifies Google identity, establishes `auth.uid()` | This plugin (`auth-identity`) |
| **Application authorization** | "What is this person allowed to do?" — roles, permissions, feature flags | This skill |
| **Data authorization** | "Which rows can this person read?" — row-level scoping by tenant/org | `data-platform` plugin (`rls-policy-authoring` skill) |

A logged-in user is not automatically entitled to every row in the database. A user with the `admin` role in the app is not automatically entitled to another tenant's data. These are separate controls enforced at separate layers.

The best-practice `authenticate-the-person-authorize-the-data-separately.md` is an absolute rule in this plugin.

---

## RBAC vs ABAC

| Model | Structure | When to use |
|---|---|---|
| **RBAC** (Role-Based Access Control) | User → Role → Permissions. Roles are pre-defined (admin, editor, viewer). | Most web apps. Simple, auditable, easy to reason about. |
| **ABAC** (Attribute-Based Access Control) | Access based on attributes of the user, resource, and environment. More flexible, more complex. | When roles are insufficient — e.g., "can edit documents they own" or "can access based on department + resource tag". |
| **ReBAC** (Relationship-Based) | Access based on graph relationships (user owns resource, is member of group). | Very fine-grained access (Google Zanzibar pattern). Complex; usually overkill for SMB apps. |

**Default to RBAC.** Add ABAC rules only when a concrete access requirement cannot be expressed as a role.

---

## Storing roles

### Option A — Supabase custom claims / user metadata (recommended)

Store roles in Supabase Auth's `user_metadata` or `app_metadata`. `app_metadata` is server-editable only (not alterable by the user):

```sql
-- Set role server-side (admin action only — never let the user set their own app_metadata)
-- Supabase Admin API or a server-side function:
UPDATE auth.users
SET raw_app_meta_data = raw_app_meta_data || '{"role": "admin"}'::jsonb
WHERE id = '<user_id>';
```

Read in a Server Component or API route:

```ts
const { data: { user } } = await supabase.auth.getUser();
const role = user?.app_metadata?.role ?? "viewer"; // default to lowest privilege
```

[unverified — confirm `app_metadata` write behavior in current Supabase Auth API]

### Option B — Roles table in the application database

For complex role models (multiple roles per user, org-scoped roles):

```sql
-- Roles table — created once per environment
CREATE TABLE user_roles (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  role        text NOT NULL CHECK (role IN ('admin', 'editor', 'viewer')),
  created_at  timestamptz DEFAULT now(),
  UNIQUE(user_id, role)
);

-- RLS: users can read their own roles; only admins can write
ALTER TABLE user_roles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "users can read own roles" ON user_roles
  FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "admins can manage roles" ON user_roles
  USING (EXISTS (
    SELECT 1 FROM user_roles r2
    WHERE r2.user_id = auth.uid() AND r2.role = 'admin'
  ));
```

### Option C — Roles in the JWT (custom claims)

Embed roles in the Supabase JWT via a custom claims function [unverified — Supabase custom JWT claims feature; verify in current docs]:

```sql
-- Supabase SQL editor: custom claim function
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb AS $$
DECLARE
  claims jsonb;
  user_role text;
BEGIN
  SELECT role INTO user_role FROM public.user_roles WHERE user_id = (event->>'userId')::uuid LIMIT 1;
  claims := event->'claims';
  claims := jsonb_set(claims, '{app_role}', to_jsonb(coalesce(user_role, 'viewer')));
  RETURN jsonb_set(event, '{claims}', claims);
END;
$$ LANGUAGE plpgsql;
```

This embeds the role in the JWT so API routes can read it without a database round-trip. Route to `ravenclaude-core/security-reviewer` — custom claim functions are security-critical.

---

## Enforcing roles in Next.js middleware

```ts
// middleware.ts — role-based route protection
// Assumes role is in user.app_metadata.role (Option A)
import { createServerClient } from "@supabase/ssr";
import { NextResponse } from "next/server";

const ADMIN_PATHS = ["/admin", "/admin/:path*"];
const EDITOR_PATHS = ["/edit/:path*"];

export async function middleware(request) {
  // ... (session setup from protect-spa-and-api skill)
  const { data: { user } } = await supabase.auth.getUser();
  const role = user?.app_metadata?.role ?? "viewer";

  const path = request.nextUrl.pathname;

  if (ADMIN_PATHS.some((p) => path.startsWith(p.replace("/:path*", ""))) && role !== "admin") {
    return NextResponse.redirect(new URL("/unauthorized", request.url));
  }

  if (EDITOR_PATHS.some((p) => path.startsWith(p.replace("/:path*", ""))) && !["admin", "editor"].includes(role)) {
    return NextResponse.redirect(new URL("/unauthorized", request.url));
  }

  return NextResponse.next();
}
```

---

## Enforcing roles in API routes

Never rely on UI-only gating — API routes must check roles independently:

```ts
// app/api/admin/route.ts
export async function DELETE(request: Request) {
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return new Response("Unauthorized", { status: 401 });

  const role = user.app_metadata?.role;
  if (role !== "admin") return new Response("Forbidden", { status: 403 });

  // proceed with admin action
}
```

---

## Conditional rendering in the UI

```tsx
// components/AdminPanel.tsx
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";

export function AdminPanel() {
  const [isAdmin, setIsAdmin] = useState(false);
  const supabase = createClient();

  useEffect(() => {
    supabase.auth.getUser().then(({ data: { user } }) => {
      setIsAdmin(user?.app_metadata?.role === "admin");
    });
  }, [supabase]);

  if (!isAdmin) return null; // UI gating only — API must enforce independently
  return <div>Admin controls</div>;
}
```

**UI gating alone is not a security control.** API routes must enforce roles regardless of what the UI renders.

---

## The identity → RLS seam (data authorization handoff)

Once the user's identity and application role are established, data-row scoping is the `data-platform` plugin's responsibility:

1. `auth.uid()` from the Supabase JWT is available in Postgres RLS policies automatically — the Supabase client passes the JWT in every query.
2. The `data-platform/rls-policy-authoring` skill defines policies like:
   ```sql
   -- Users can only read rows belonging to their tenant
   CREATE POLICY "tenant_isolation" ON orders
     FOR SELECT USING (tenant_id = (
       SELECT tenant_id FROM tenant_memberships WHERE user_id = auth.uid()
     ));
   ```
3. The `auth-identity` plugin's job ends at establishing `auth.uid()`. The RLS policy determines which rows that user can read.

**Never duplicate RLS logic in application code** as the load-bearing control. App-code row filters are acceptable as redundant defense-in-depth layers, but the authoritative control is the database RLS policy. See `data-platform/best-practices/enforce-tenant-isolation-closest-to-data.md`.

---

## Anti-patterns this skill flags

- Checking roles only in UI components and not in API routes — API routes must enforce independently
- Storing roles in `user_metadata` (user-editable) instead of `app_metadata` (server-only) [unverified — confirm Supabase metadata editability]
- Admin-role check that accepts any truthy `role` value instead of explicitly checking `role === 'admin'`
- Using UI-rendered role gating as the sole data-access control — RLS must be the load-bearing layer
- No default role — always assign the least-privilege role (`viewer`) if none is set
- Roles stored in localStorage or client-side state — read from server-side session only
- Hard-coding `user.id` in a URL parameter to determine which data to show — derive tenant from the authenticated session, not the URL

---

## See also

- Skill: [`../protect-spa-and-api/SKILL.md`](../protect-spa-and-api/SKILL.md) — route guards that this skill extends with role checks
- Skill: [`../gate-the-dashboard/SKILL.md`](../gate-the-dashboard/SKILL.md) — dashboard-specific role gate + RLS handoff
- Best-practice: [`../../best-practices/authenticate-the-person-authorize-the-data-separately.md`](../../best-practices/authenticate-the-person-authorize-the-data-separately.md)
- data-platform: [`../../../data-platform/skills/rls-policy-authoring/SKILL.md`](../../../data-platform/skills/rls-policy-authoring/SKILL.md) — the data-row authorization layer that follows this skill
- Security escalation: [`../../../ravenclaude-core/agents/security-reviewer.md`](../../../ravenclaude-core/agents/security-reviewer.md)

Attribution

mcorbett51090mcorbett51090
View sourceSee grades on GitHubMore from mcorbett51090 →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Terse caveman voice: answer first, fluff gone, every technical fact kept. Use for /caveman, "caveman mode", "talk like caveman", "be brief", "less tokens". Stays on until "stop caveman" or "normal mode".

1100021 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

698461 votes

Writing Skills

Create and manage Claude Code skills in HASH repository following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns), UserPromptSubmit hook, and the 500-line rule. Includes validation and debugging with SKILL_DEBUG. Examples include rust-error-stack, cargo-dep...

3931 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3421 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Amp, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Grok Build, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

741 votes
View all in ai-agents →