convex-authz について
@djpanda/convex-authzは、Convexアプリケーション向けのTypeScript認可コンポーネントです。ロール・属性・関係に基づくアクセス制御をまとめ、インデックス化された権限確認とReactのリアクティブな連携を提供します。
主な機能
- 01RBAC・ABAC・ReBACに対応
- 02インデックス化された有効権限チェック
- 03スコープ付きロールとリソース権限
- 04権限監査ログと期限付き付与
- 05関係のトラバーサルと権限連携
- 06リアクティブなReact HooksとPermissionGate
活用シーン
- 01ドキュメントやリソースへのアクセス制御
- 02チームや組織の関係のモデル化
- 03属性ベースのアクセス方針の適用
- 04スキーマ変更後の権限再構築
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dbjpanda/ convex-tenants --skill convex-authzインストール後、Claude Code で/convex-authzを入力して読み込まれたか確認します。
詳細
@djpanda/convex-authz
RBAC、ABAC、ReBACに対応し、O(1)のインデックス検索を特徴とする、Convex向けの包括的で本番環境対応の認可コンポーネントです。Google Zanzibarに着想を得ています。
特長
| 特長 | 説明 |
|---|---|
| RBAC | スコープ付きロールによるロールベースのアクセス制御 |
| ABAC | カスタムポリシーによる属性ベースのアクセス制御 |
| ReBAC | グラフ走査による関係ベースのアクセス制御 |
| O(1)検索 | 即時チェックを可能にする事前計算済みの権限 |
| 型安全性 | 型安全な権限を備えたTypeScriptの完全サポート |
| 監査ログ | すべての権限変更とチェックを追跡 |
| スコープ付き権限 | リソースレベルのアクセス制御 |
| 有効期限付き付与 | 期間限定のロール割り当てと権限 |
| Convexネイティブ | Convex専用に構築され、リアルタイム更新に対応 |
用語
| 用語 | 定義 |
|---|---|
| RBAC | ロールベースのアクセス制御。ロール(admin、editor、viewer)を通じて権限を割り当てます |
| ABAC | 属性ベースのアクセス制御。ユーザーやリソースの属性(department=engineering)に基づいて権限を決定します |
| ReBAC | 関係ベースのアクセス制御。関係(リソースを所有するチームのメンバーなど)から権限を導き出します |
| Zanzibar | Googleのグローバル認可システム。OpenFGAとこのコンポーネントの着想元です |
| タプル | 関係を表す3要素:(subject, relation, object)。例:(user:alice, member, team:sales) |
| スコープ | リソースレベルの権限コンテキスト。例:「team:123のadmin」とグローバルな「admin」の違い |
| 走査 | 継承されたアクセスを判定するために関係の連鎖をたどること |
| O(1)検索 | 事前計算済みのインデックスを使用した、定数時間の権限チェック |
| 権限の上書き | ロールベースの権限を迂回する、直接的な許可または拒否 |
インストール
npm install @djpanda/convex-authz
クイックスタート
1. コンポーネントを登録する
// convex/convex.config.ts
import { defineApp } from "convex/server";
import authz from "@djpanda/convex-authz/convex.config";
const app = defineApp();
app.use(authz);
export default app;
2. 権限とロールを定義する
// convex/authz.ts
import { Authz, definePermissions, defineRoles } from "@djpanda/convex-authz";
import { components } from "./_generated/api";
// Step 1: Define permissions
const permissions = definePermissions({
documents: {
create: true,
read: true,
update: true,
delete: true,
},
settings: {
view: true,
manage: true,
},
});
// Step 2: Define roles
const roles = defineRoles(permissions, {
admin: {
documents: ["create", "read", "update", "delete"],
settings: ["view", "manage"],
},
editor: {
documents: ["create", "read", "update"],
settings: ["view"],
},
viewer: {
documents: ["read"],
},
});
// Step 3: Create the authz client
export const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
ロールの継承と合成
権限リストの重複を避けるため、他のロールを基にロールを定義できます。
**inherits**– 親ロールを1つ指定します。有効な権限は、親ロールの権限 ∪ このロールの直接の権限です。**includes**– 複数のロールを指定します。有効な権限は、すべての包含ロールの権限の和集合 ∪ このロールの直接の権限です。
継承の例(admin > editor > viewer):
const roles = defineRoles(permissions, {
viewer: { documents: ["read"] },
editor: { inherits: "viewer", documents: ["create", "update"] },
admin: { inherits: "editor", documents: ["delete"], settings: ["manage"] },
});
合成の例(ロールの組み合わせ):
const roles = defineRoles(permissions, {
editor: { documents: ["create", "read", "update"] },
billing_admin: { billing: ["view", "manage"] },
billing_manager: { includes: ["editor", "billing_admin"], settings: ["view"] },
});
注: inheritsとincludesはロール定義で予約されているキーです。権限リソース名として使用しないでください。
3. 関数で使用する
// convex/documents.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { authz } from "./authz";
import { getAuthUserId } from "@convex-dev/auth/server";
export const updateDocument = mutation({
args: { docId: v.id("documents"), content: v.string() },
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
// Check permission (throws if denied)
await authz.require(ctx, userId, "documents:update");
// Or with scope
await authz.require(ctx, userId, "documents:update", {
type: "document",
id: args.docId,
});
// Proceed with update...
},
});
Unified Authz v2
v2 ではすべてが単一の Authz クラスに統合されます。以前 IndexedAuthz を使っていた場合は、名前を変更するだけです。コンストラクターのシグネチャは同一です。
変更点
- クラスの統合:
Authzが従来のAuthzとIndexedAuthzの両方に置き換わります。事前計算された有効なテーブルによる O(1) の読み取りが、デフォルトで利用できるようになりました。 Authzでの ReBAC:hasRelation、addRelation、removeRelationは、すべてのAuthzインスタンスで直接利用できます。- ABAC ポリシーの種類:ポリシーでは
typeフィールド("static"または"deferred")を指定できます。現在の実装では、can()の呼び出し時に、どちらの種類も読み取り時に評価されます。Convex のミューテーションからクエリを呼び出すことはできないため、書き込み時の評価はできません。typeフィールドは将来の最適化のために予約されていますが、現時点で動作に違いはありません。 canWithContext():実行時コンテキスト(IP アドレスや時刻など)が必要な、遅延 ABAC ポリシーを確認します。recomputeUser():ユーザーの有効な権限テーブルをオンデマンドで再構築します。スキーマ変更後やデプロイ後の移行時に便利です。withTenant():クロステナントの管理操作用に、別のテナントにバインドされたスコープ付きのクライアントのコピーを取得します。
ReBAC の例
// Add a relationship
await authz.addRelation(ctx, { type: "user", id: userId }, "member", { type: "team", id: teamId });
// Check a relationship
const isMember = await authz.hasRelation(ctx, { type: "user", id: userId }, "member", { type: "team", id: teamId });
// Remove a relationship
await authz.removeRelation(ctx, { type: "user", id: userId }, "member", { type: "team", id: teamId });
ReBAC → 権限ブリッジ
defineRelationPermissions を使うと、リレーションシップの作成時に権限を自動的に付与できます。
import { defineRelationPermissions } from "@djpanda/convex-authz";
const authz = new Authz(components.authz, {
permissions, roles, tenantId: "my-app",
relationPermissions: defineRelationPermissions({
"document:viewer": ["documents:read"],
"document:editor": ["documents:read", "documents:update"],
"document:owner": ["documents:read", "documents:update", "documents:delete"],
}),
});
// Adding a relation automatically grants scoped permissions
await authz.addRelation(ctx, { type: "user", id: userId }, "editor", { type: "document", id: docId });
// can() now checks relation-derived permissions — no separate hasRelation() needed
const canUpdate = await authz.can(ctx, userId, "documents:update", { type: "document", id: docId }); // true
// Removing the relation revokes the permissions
await authz.removeRelation(ctx, { type: "user", id: userId }, "editor", { type: "document", id: docId });
ABAC の例
// Policies are always evaluated at read-time when can() is called.
// The optional type field ("static" | "deferred") is reserved for future use
// and currently has no behavioral effect.
const policies = definePolicies({
"documents:read": {
condition: (ctx) => ctx.getAttribute("verified") === true,
message: "Only verified users can read documents",
},
"billing:export": {
condition: (ctx) => {
const hour = new Date().getUTCHours();
return hour >= 9 && hour <= 17;
},
message: "Billing exports only during business hours",
},
});
// Use canWithContext() when request context is available
const allowed = await authz.canWithContext(ctx, userId, "documents:read", undefined, {
ipAllowlisted: true,
});
デプロイ後の再構築
// Rebuild a single user's effective permissions after upgrading
await authz.recomputeUser(ctx, userId);
クロステナント操作
const otherTenantAuthz = authz.withTenant("other-tenant-id");
const allowed = await otherTenantAuthz.can(ctx, userId, "documents:read");
移行ガイド:IndexedAuthz → Authz
注:
IndexedAuthzは v2 ではエクスポートされなくなりました。以下のインポートは失敗するため、Authzに置き換えてください。
// Before (v1) — this import no longer works in v2
// import { IndexedAuthz } from "@djpanda/convex-authz";
// const authz = new IndexedAuthz(components.authz, { permissions, roles, tenantId: "my-app" });
// After (v2) — same constructor, just rename the class
import { Authz } from "@djpanda/convex-authz";
const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
アップグレード後、既存ユーザーごとに recomputeUser() を実行して、有効な権限テーブルをバックフィルしてください。
// one-time migration mutation
export const backfillEffectivePermissions = mutation({
args: {},
handler: async (ctx) => {
const users = await ctx.db.query("users").collect();
for (const user of users) {
await authz.recomputeUser(ctx, String(user._id));
}
},
});
React 連携
このパッケージには、UI で権限やロールをリアクティブに確認するための React フックと PermissionGate コンポーネントが用意されています。アプリでは、Authz コンポーネントをラップする Convex クエリ(checkPermission、getUserRoles など)を公開する必要があります。フックは Convex の useQuery を介してこれらのクエリを呼び出すため、ポーリングしなくても権限やロールの変更が最新の状態に保たれます。
1. Convex クエリを公開する
たとえば、Authz クライアントに処理を委譲するクエリを定義します。
// convex/app.ts (or similar)
import { query } from "./_generated/server";
import { v } from "convex/values";
import { authz } from "./authz";
export const checkPermission = query({
args: {
userId: v.string(),
permission: v.string(),
scope: v.optional(v.object({ type: v.string(), id: v.string() })),
},
handler: async (ctx, args) => {
return authz.can(ctx, args.userId, args.permission, args.scope);
},
});
export const getUserRoles = query({
args: {
userId: v.string(),
scope: v.optional(v.object({ type: v.string(), id: v.string() })),
},
handler: async (ctx, args) => {
return authz.getUserRoles(ctx, args.userId, args.scope);
},
});
2. AuthzProvider でアプリをラップする
Convex クエリの参照(必要に応じてデフォルトのユーザー ID も)をプロバイダーに渡します。
import { AuthzProvider } from "@djpanda/convex-authz/react";
import { api } from "./convex/_generated/api";
<AuthzProvider
queryRefs={{
checkPermission: api.app.checkPermission,
getUserRoles: api.app.getUserRoles,
}}
defaultUserId={currentUserId} // optional; hooks can pass userId in options
>
<App />
</AuthzProvider>
3. フックと PermissionGate を使う
- useCanUser(permission, options?) —
{ allowed, isLoading, error }を返します。オプション:{ userId?, scope? }。userIdを省略すると、プロバイダーのdefaultUserIdを使います。 - useUserRoles(options?) —
{ roles, isLoading, error }を返します。オプション:{ userId?, scope? }。 - useRequirePermission(permission, options?) — ユーザーに許可がない場合に例外をスローします(拒否状態を表示するには、エラーバウンダリーを使用してください)。
- PermissionGate — 許可されている場合は
children、拒否されている場合はfallback、読み込み中は(任意で)loadingFallbackをレンダリングします。
import {
useCanUser,
useUserRoles,
useRequirePermission,
PermissionGate,
} from "@djpanda/convex-authz/react";
function DocumentList() {
const { allowed, isLoading } = useCanUser("documents:read");
if (isLoading) return <Spinner />;
if (!allowed) return <p>You cannot view documents.</p>;
return <div>{/* list */}</div>;
}
function AdminPanel() {
useRequirePermission("settings:manage"); // throws if denied; wrap in error boundary
return <div>Admin content</div>;
}
function EditButton({ docId }: { docId: string }) {
return (
<PermissionGate
permission="documents:update"
scope={{ type: "document", id: docId }}
fallback={<span>No access</span>}
loadingFallback={<span>Checking…</span>}
>
<button>Edit</button>
</PermissionGate>
);
}
Convex のリアクティビティにより、バックエンドで権限やロールが変更されると、フックと PermissionGate が再実行され、UI が自動的に更新されます。
アーキテクチャ
┌─────────────────────────────────────────────────────────────────────────────┐
│ @djpanda/convex-authz │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ RBAC │ │ ABAC │ │ ReBAC │ │
│ │ Role-Based │ │ Attribute-Based │ │ Relationship-Based │ │
│ │ Access Control │ │ Access Control │ │ Access Control │ │
│ │ │ │ │ │ │ │
│ │ • Roles │ │ • User attrs │ │ • Tuples (S, R, O) │ │
│ │ • Permissions │ │ • Policies │ │ • Graph traversal │ │
│ │ • Scopes │ │ • Conditions │ │ • Inheritance │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ O(1) Indexed Permission Cache │ │
│ │ │ │
│ │ effectivePermissions │ effectiveRoles │ effectiveRelationships │ │
│ │ [user, perm, scope] │ [user, role] │ [subject, rel, object] │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
RBAC(ロールベースアクセス制御)
ロールの割り当て
// Global role
await authz.assignRole(ctx, userId, "admin");
// Scoped role (e.g., admin of a specific team)
await authz.assignRole(ctx, userId, "admin", {
type: "team",
id: "team_123",
});
// With expiration (24 hours)
await authz.assignRole(ctx, userId, "admin", undefined, Date.now() + 86400000);
ロールの取り消し
await authz.revokeRole(ctx, userId, "admin");
// Scoped
await authz.revokeRole(ctx, userId, "admin", { type: "team", id: "team_123" });
権限の確認
// Boolean check
const canEdit = await authz.can(ctx, userId, "documents:update");
// Throws if denied
await authz.require(ctx, userId, "documents:update");
// With scope
const canEditTeamDocs = await authz.can(ctx, userId, "documents:update", {
type: "team",
id: "team_123",
});
ロールの確認
const isAdmin = await authz.hasRole(ctx, userId, "admin");
// Scoped
const isTeamAdmin = await authz.hasRole(ctx, userId, "admin", {
type: "team",
id: "team_123",
});
ワイルドカードとパターンマッチングによる権限
権限チェックと上書きでは、ワイルドカードパターンを使用できるため、複数の権限をまとめて許可または拒否できます。
パターン形式: resource:action。resource または action(あるいは両方)に * を指定できます。
| パターン | 意味 | 一致する例 |
|---|---|---|
* | すべての権限 | documents:read、settings:manage |
documents:* | documents のすべてのアクション | documents:read、documents:update |
*:read | 任意のリソースに対する読み取り | documents:read、settings:read |
*:* | すべての権限(* と同じ) | 任意の resource:action |
チェック: can(ctx, userId, "documents:read") または require(ctx, userId, "documents:read") を呼び出すと、バックエンドは、その権限に 一致する 保存済みのロールまたは上書きを、権限を付与するものとして扱います。そのため、ユーザーが documents:* を持つロール、または *:read の上書きを持っている場合、documents:read が許可されます。
割り当て: grantPermission と denyPermission にパターンを渡すことができます。
// Grant all document actions
await authz.grantPermission(ctx, userId, "documents:*", undefined, "Full document access");
// Deny read on any resource
await authz.denyPermission(ctx, userId, "*:read", undefined, "Read access revoked");
ロール定義: コンポーネントが権限を評価する際、同じパターンルールを使用して、要求された権限と各ロールの権限リストを照合します。そのため、ロールの権限に "documents:*" が含まれている場合(フラット化されたロールと権限のマップ内)、can(ctx, userId, "documents:read") は許可されます。defineRoles では通常、リソースごとに具体的なアクションを指定します(例:documents: ["read", "update"])。ロールでパターンを使用するには、そのロールのパターン文字列を含むロールと権限のマップを指定します。
クライアント側ヘルパー: バックエンドを呼び出さずにパターンが権限に一致するかどうかを確認するには、エクスポートされているヘルパーを使用します。
import { matchesPermissionPattern } from "@djpanda/convex-authz";
matchesPermissionPattern("documents:read", "documents:*"); // true
matchesPermissionPattern("documents:read", "*:read"); // true
matchesPermissionPattern("settings:read", "documents:*"); // false
同じワイルドカードの動作が、すべての権限チェックに適用されます。
ユーザーロールの取得
const roles = await authz.getUserRoles(ctx, userId);
// Returns: [{ role: "admin", scopeKey: "global" }, { role: "editor", scopeKey: "team:123", scope: { type: "team", id: "123" } }]
一括操作とオフボーディング
大規模またはエンタープライズのワークフロー向けに、API では一括権限チェック(最大 100 件)とロール更新(最大 20 件)を 1 回の呼び出しで実行できます。
権限の一括チェック(canAny)
1 回の往復で、指定した権限の いずれか をユーザーが持っているかどうかを確認します。
const allowed = await authz.canAny(ctx, userId, [
"documents:read",
"documents:update",
"documents:delete",
], scope);
// true if the user has at least one of these permissions
ロールの一括割り当てと取り消し
1 人のユーザーに対して、複数のロールを 1 つのトランザクション で割り当てるか、取り消します。
// Assign multiple roles at once (max 20 per call)
const { assigned, assignmentIds } = await authz.assignRoles(ctx, userId, [
{ role: "admin" },
{ role: "editor", scope: { type: "team", id: "team_1" } },
{ role: "viewer", scope: { type: "org", id: "org_1" }, expiresAt: Date.now() + 86400000 },
], actorId);
// Revoke multiple roles at once (max 20 per call)
const { revoked } = await authz.revokeRoles(ctx, userId, [
{ role: "editor", scope: { type: "team", id: "team_1" } },
{ role: "viewer" },
], actorId);
すべてのロールの取り消し
ユーザーのすべてのロールを取り消します(オプションで指定したスコープ内のみを対象にできます)。
const count = await authz.revokeAllRoles(ctx, userId);
const countScoped = await authz.revokeAllRoles(ctx, userId, { type: "team", id: "team_1" }, actorId);
ユーザーの完全なオフボーディング
1 回の呼び出しで、ユーザーのすべてのロール、権限の上書き、属性、および必要に応じて ReBAC 関係を削除します(スコープを指定することもできます)。インデックス付きの effectiveRoles、effectivePermissions、effectiveRelationships が存在する場合、それらもクリアされます。
const result = await authz.offboardUser(ctx, userId, {
scope: { type: "org", id: "org_1" }, // optional: only remove data in this scope
actorId: "system",
removeAttributes: true, // default true
removeOverrides: true, // default true
removeRelationships: true, // default true when no scope (full offboard)
});
// result: { rolesRevoked, overridesRemoved, attributesRemoved, relationshipsRemoved, effectiveRolesRemoved, effectivePermissionsRemoved, effectiveRelationshipsRemoved }
スコープを省略した場合、この呼び出しは完全なプロビジョニング解除を実行します。すべてのロール、上書き、属性、およびユーザーが対象となっているすべての ReBAC 関係が削除されます。スコープを指定した場合は、そのスコープ内のデータのみが削除され、関係は変更されません。
ユーザーのプロビジョニング解除(完全消去)
セキュリティインシデントへの対応、エンタープライズのオフボーディング、またはワンクリックでの無効化には、deprovisionUser を使用して、ユーザーのすべてのロール、属性、関係、および権限の上書きをアトミックに消去します(スコープおよびオプションは指定しません)。
const result = await authz.deprovisionUser(ctx, userId, {
actorId: "security-team",
enableAudit: true,
});
// result: { rolesRevoked, overridesRemoved, attributesRemoved, relationshipsRemoved, effectiveRolesRemoved, effectivePermissionsRemoved, effectiveRelationshipsRemoved }
一括配列には、呼び出しごとの上限があります。canAny の権限は最大 100 項目、assignRoles / revokeRoles のロールは最大 20 項目です。クライアントとコンポーネントは値を検証し、上限を超えた場合は明確なエラーをスローします。
ABAC(属性ベースのアクセス制御)
ユーザー属性の設定
await authz.setAttribute(ctx, userId, "department", "engineering");
await authz.setAttribute(ctx, userId, "clearanceLevel", 5);
await authz.setAttribute(ctx, userId, "location", { country: "US", state: "CA" });
属性の取得
const attributes = await authz.getUserAttributes(ctx, userId);
// Returns: [{ key: "department", value: "engineering" }, { key: "clearanceLevel", value: 5 }]
ポリシーの定義
condition 関数は boolean または Promise<boolean> を返すため、非同期ロジック(データベースへの問い合わせや外部 API の呼び出しなど)を使用できます。
import { definePolicies, evaluatePolicyCondition } from "@djpanda/convex-authz";
const policies = definePolicies({
"documents:update": {
// User can update if they own the document (sync)
condition: (ctx) => ctx.resource?.ownerId === ctx.subject.userId,
message: "Only document owners can update",
},
"reports:view": {
// Only engineering department with clearance >= 3 (sync)
condition: (ctx) =>
ctx.subject.attributes.department === "engineering" &&
(ctx.subject.attributes.clearanceLevel as number) >= 3,
message: "Requires engineering department with clearance level 3+",
},
"documents:delete": {
// Async: e.g. check external service or fetch extra data
condition: async (ctx) => {
const doc = await getDocument(ctx.resource?.id);
return doc != null && doc.ownerId === ctx.subject.userId;
},
message: "Only document owners can delete",
},
});
const authz = new Authz(components.authz, { permissions, roles, policies, tenantId: "my-app" });
ポリシーを評価するとき(たとえば、RBAC で許可された後)は、同期ポリシーと非同期ポリシーの両方が動作するよう、必ず条件を await してください。evaluatePolicyCondition を使用すると、戻り値を Promise に正規化できます。
const policy = policies["documents:update"];
if (policy) {
const allowed = await evaluatePolicyCondition(policy.condition, policyCtx);
if (!allowed) throw new Error(policy.message ?? "Permission denied");
}
ポリシーコンテキスト
ポリシーには、次のコンテキストオブジェクトが渡されます。
interface PolicyContext {
subject: {
userId: string;
roles: string[];
attributes: Record<string, unknown>;
};
resource?: {
type: string;
id: string;
[key: string]: unknown; // Resource data
};
action: string; // The permission being checked
environment?: {
timestamp: number;
ip?: string;
};
hasRole: (role: string) => boolean;
hasAttribute: (key: string) => boolean;
getAttribute: <T = unknown>(key: string, defaultValue?: T) => T | undefined;
}
API に関する注意: 既存の同期条件は引き続き有効です。破壊的変更はありません。戻り値の型が拡張され、非同期ポリシーで Promise<boolean> を返せるようになっただけです。
ReBAC(関係ベースのアクセス制御)
ReBAC はエンティティ間の関係に基づくアクセス制御を実現します。CRM、ドキュメント共有、組織構造など、階層型のシステムに最適です。
関係モデル
関係はタプル (subject, relation, object) として保存されます。
user:alice ──member──► team:sales
team:sales ──owner──► account:acme
account:acme ──parent──► deal:big_deal
関係の追加
// Use the Authz client — NOT direct component calls
// User is member of team
await authz.addRelation(ctx, { type: "user", id: "alice" }, "member", { type: "team", id: "sales" });
// Team owns account
await authz.addRelation(ctx, { type: "team", id: "sales" }, "owner", { type: "account", id: "acme" });
直接の関係の確認
// Use the Authz client — NOT direct component calls
const isMember = await authz.hasRelation(ctx, { type: "user", id: "alice" }, "member", { type: "team", id: "sales" });
// Returns: true
// To remove a relationship
await authz.removeRelation(ctx, { type: "user", id: "alice" }, "member", { type: "team", id: "sales" });
関係のたどり方(継承されるアクセス権)
ReBAC の真価は、関係の連鎖を通じてアクセス権を確認できることです。
// Define how permissions flow through relationships
const traversalRules = {
// A deal viewer is anyone who can view the parent account
"deal:viewer": [
{ through: "account", via: "parent", inherit: "viewer" }
],
// An account viewer is any member of the owning team
"account:viewer": [
{ through: "team", via: "owner", inherit: "member" }
],
};
// Graph traversal is available via the component query directly
// Check: Can alice view big_deal?
const result = await ctx.runQuery(components.authz.rebac.checkRelationWithTraversal, {
subjectType: "user",
subjectId: "alice",
relation: "viewer",
objectType: "deal",
objectId: "big_deal",
traversalRules,
maxDepth: 5,
});
// Returns:
// {
// allowed: true,
// path: [
// "account:acme -[parent]-> deal:big_deal",
// "team:sales -[owner]-> account:acme",
// "user:alice -[member]-> team:sales"
// ],
// reason: "Access via relationship chain"
// }
たどる際には maxDepth の上限(デフォルトは 5)が使用され、訪問済みの (objectType, objectId, relation) ノードが記録されるため、循環関係による無限ループを防止できます。
CRM の例
// Setup CRM hierarchy using the Authz client
const setupCRM = async (ctx) => {
// Sales rep Alice is on sales team
await authz.addRelation(ctx, { type: "user", id: "alice" }, "member", { type: "team", id: "sales" });
// Sales team owns Acme Corp account
await authz.addRelation(ctx, { type: "team", id: "sales" }, "owner", { type: "account", id: "acme_corp" });
// Acme Corp has a big deal
await authz.addRelation(ctx, { type: "account", id: "acme_corp" }, "parent", { type: "deal", id: "big_deal" });
// Now Alice can access the deal through the relationship chain!
};
O(1) のインデックス付き検索
本番環境で高いパフォーマンスを実現するため、インデックス付きシステムでは権限を事前計算し、即座に検索できるようにします。
インデックス付き API の使用
import { Authz } from "@djpanda/convex-authz";
import { components } from "./_generated/api";
const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
// O(1) permission check - single index lookup
const canEdit = await authz.can(ctx, userId, "documents:update");
// O(1) role check
const isAdmin = await authz.hasRole(ctx, userId, "admin");
// O(1) relationship check
const isMember = await authz.hasRelation(ctx, { type: "user", id: userId }, "member", { type: "team", id: "sales" });
仕組み
Traditional (O(n)): Indexed (O(1)):
┌──────┐ ┌──────┐
│ User │ │ User │
└──┬───┘ └──┬───┘
│ │
▼ ▼
┌──────────┐ ┌─────────────────────────────┐
│ Get Roles│ ◄── Query │ Index Lookup: │
└──┬───────┘ │ effectivePermissions │
│ │ [userId, permission, scope] │
▼ └─────────────────────────────┘
┌───────────────┐ │
│ Expand Perms │ ◄── Loop ▼
└──┬────────────┘ true/false
│
▼
┌──────────────┐
│ Check Each │ ◄── Loop
│ Permission │
└──┬───────────┘
│
▼
true/false
トレードオフ
| 操作 | 従来方式 | インデックス付き |
|---|---|---|
| 権限チェック | O(roles × perms) | O(1) |
| ロールの割り当て | O(1) | O(permissions) |
| 権限の付与 | O(1) | O(1) |
| メモリ使用量 | 少ない | 多い(非正規化) |
権限チェックが多い本番環境のワークロードでは、インデックス付きの方式を使用してください。
監査ログ
コンプライアンス対応とデバッグのため、すべての認可の変更が記録されます。
自動ログ記録
次の操作は自動的に記録されます。
role_assigned- ロールが割り当てられたときrole_revoked- ロールが取り消されたときpermission_granted- 直接の権限が付与されたときpermission_denied- 権限が明示的に拒否されたときattribute_set- ユーザー属性が設定されたときattribute_removed- ユーザー属性が削除されたときpermission_check- (任意)権限がチェックされたとき
監査ログの検索
ページネーションオプションを指定しない場合、getAuditLog は単純な配列を返します(任意の limit。デフォルトは 100)。
// Get all logs for a user
const logs = await authz.getAuditLog(ctx, {
userId: "user_123",
limit: 50,
});
// Get logs by action type
const roleChanges = await authz.getAuditLog(ctx, {
action: "role_assigned",
limit: 100,
});
大量のログを扱う場合は、numItems を渡してカーソルベースのページネーションを使用してください(次のページの取得には、任意で cursor を指定します)。戻り値は { page, isDone, continueCursor } になります。
// First page
const result = await authz.getAuditLog(ctx, { numItems: 50 });
if (!Array.isArray(result)) {
console.log(result.page);
if (!result.isDone) {
// Next page
const next = await authz.getAuditLog(ctx, {
numItems: 50,
cursor: result.continueCursor,
});
}
}
ログエントリーの構造
{
_id: "...",
timestamp: 1704672000000,
actorId: "admin_user", // Who made the change
action: "role_assigned",
userId: "target_user", // Who was affected
details: {
role: "editor",
scope: { type: "team", id: "team_123" },
},
}
権限のオーバーライド
ロールベースの割り当てを上書きする、特定の権限の付与または拒否を設定します。
権限の付与
// Grant a permission directly (bypasses role checks)
await authz.grantPermission(ctx, userId, "documents:delete", undefined, "Temporary access for migration");
// With scope
await authz.grantPermission(ctx, userId, "documents:delete", { type: "team", id: "team_123" });
// With expiration
await authz.grantPermission(ctx, userId, "documents:delete", undefined, "Temporary", Date.now() + 3600000);
権限の拒否
// Deny a permission (even if user has it via role)
await authz.denyPermission(ctx, userId, "documents:delete", undefined, "Access restricted");
スキーマリファレンス
テーブル
| テーブル | 用途 |
|---|---|
roleAssignments | ユーザーのロール割り当て |
userAttributes | ABAC 用のユーザー属性 |
permissionOverrides | 直接の権限の付与/拒否 |
relationships | ReBAC の関係タプル |
effectivePermissions | 事前計算された権限(O(1)) |
effectiveRoles | 事前計算されたロール(O(1)) |
effectiveRelationships | 事前計算された関係(O(1)) |
auditLog | 認可の監査証跡 |
インデックス
すべてのテーブルには、一般的なクエリパターンに合わせて最適化されたインデックスがあります。
// roleAssignments
.index("by_user", ["userId"])
.index("by_role", ["role"])
.index("by_user_and_role", ["userId", "role"])
// effectivePermissions (O(1) lookup)
.index("by_user_permission_scope", ["userId", "permission", "scopeKey"])
// relationships
.index("by_subject_relation_object", ["subjectType", "subjectId", "relation", "objectType", "objectId"])
API リファレンス
Authz クライアント
class Authz<P, R, Policy> {
// Permission checks
can(ctx, userId, permission, scope?): Promise<boolean>
canAny(ctx, userId, permissions: string[], scope?): Promise<boolean> // bulk: any of N permissions (max 100)
require(ctx, userId, permission, scope?): Promise<void>
// Role management
hasRole(ctx, userId, role, scope?): Promise<boolean>
assignRole(ctx, userId, role, scope?, expiresAt?, actorId?): Promise<string>
assignRoles(ctx, userId, roles: RoleAssignItem[], actorId?): Promise<{ assigned: number; assignmentIds: string[] }> // bulk, max 20
revokeRole(ctx, userId, role, scope?, actorId?): Promise<boolean>
revokeRoles(ctx, userId, roles: RoleScopeItem[], actorId?): Promise<{ revoked: number }> // bulk, max 20
revokeAllRoles(ctx, userId, scope?, actorId?): Promise<number>
getUserRoles(ctx, userId, scope?): Promise<Role[]>
getUserPermissions(ctx, userId, scope?): Promise<PermissionResult>
// Offboarding
offboardUser(ctx, userId, options?: { scope?, actorId?, removeAttributes?, removeOverrides?, removeRelationships? }): Promise<OffboardResult>
deprovisionUser(ctx, userId, options?: { actorId?, enableAudit? }): Promise<OffboardResult> // full wipe: roles, overrides, attributes, relationships
// Attribute management
setAttribute(ctx, userId, key, value, actorId?): Promise<string>
removeAttribute(ctx, userId, key, actorId?): Promise<boolean>
getUserAttributes(ctx, userId): Promise<Attribute[]>
// Permission overrides
grantPermission(ctx, userId, permission, scope?, reason?, expiresAt?, actorId?): Promise<string>
denyPermission(ctx, userId, permission, scope?, reason?, expiresAt?, actorId?): Promise<string>
// Audit
getAuditLog(ctx, options?): Promise<AuditEntry[] | { page: AuditEntry[]; isDone: boolean; continueCursor: string }>
}
引数の検証
Authz のすべてのパブリックメソッドは、コンポーネントを呼び出す前に引数を検証します。入力が無効な場合は、明確なメッセージを含む Error がスローされるため、早期に失敗させて呼び出し箇所を修正できます。
| 引数 | ルール | エラーの例 |
|---|---|---|
userId | 空でない文字列、最大512文字 | "userId must be a non-empty string" |
permission | resource:action 形式である必要があります(例:documents:read) | "Invalid permission format: \"read\". Expected \"resource:action\"" |
scope | 指定する場合、type と id は空でない文字列である必要があります | "scope must have non-empty type when provided" |
role | 空でない文字列で、構築時に渡されたロールのいずれかである必要があります | "Unknown role: \"superadmin\"" |
expiresAt | 指定する場合、有限の数値(タイムスタンプ)である必要があります | "expiresAt must be a finite number" |
属性の key | 空でない文字列 | "Attribute key must be a non-empty string" |
getAuditLog の limit | 指定する場合、1~1000の正の整数 | "limit must be a positive integer when provided" |
getAuditLog の numItems | 指定する場合(ページネーション)、1~1000の正の整数 | limit と同じ |
| リレーションの引数 | subjectType、subjectId、relation、objectType、objectId は空でない文字列である必要があります | "subjectType must be a non-empty string" |
canAny の permissions | 空でない配列で、各要素が有効な resource:action 形式であり、長さは100以下 | "permissions must not exceed 100 items" |
assignRoles/revokeRoles の roles | 空でない配列で、各ロールが有効であり、長さは20以下 | "roles must not exceed 20 items" |
オプションのパラメーターは、指定された場合にのみ検証されます(例:scope を省略するのは有効ですが、scope: { type: "", id: "x" } を渡すとエラーがスローされます)。
Google Zanzibar に着想を得ています
このコンポーネントは、Google Drive、YouTube、Cloud などを支える Google のグローバル認可システムである Google Zanzibar の概念を実装しています。
実装されているZanzibarの概念
| Zanzibarの概念 | 実装 | 説明 |
|---|---|---|
| Relation Tuples | relationshipsテーブル | (user:alice, member, team:sales) |
| Usersets | トラバーサルルール | 関係によって定義されるグループ |
| Check API | checkPermissionFast | O(1)「ユーザーXはZに対してYを実行できますか?」 |
| Expand API | checkRelationWithTraversal | アクセスを許可するすべてのパスを検索 |
| Read API | getSubjectRelations | すべての関係を一覧表示 |
| Watch API | Convexのリアクティビティ | 権限をリアルタイムで更新 |
| Computed Relations | effectivePermissions | O(1)で参照できるよう事前計算 |
Zanzibarの仕組み
┌─────────────────────────────────────────────────────────────────────────────┐
│ Google Zanzibar Model │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Relation Tuples (stored): │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ (user:alice, member, team:sales) │ │
│ │ (team:sales, owner, account:acme) │ │
│ │ (account:acme, parent, deal:big_deal) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ Authorization Model (defines inheritance): │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ type deal │ │
│ │ relations │ │
│ │ define parent: [account] │ │
│ │ define viewer: viewer from parent ← Computed relation │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ Check: "Can alice view deal:big_deal?" │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ 1. deal:big_deal.viewer = viewer from parent │ │
│ │ 2. parent = account:acme │ │
│ │ 3. account:acme.viewer = member from owner │ │
│ │ 4. owner = team:sales │ │
│ │ 5. team:sales.member includes user:alice ✓ │ │
│ │ → ALLOWED │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
提供するZanzibarの主な利点
- 大規模環境での一貫性
- 権限を事前計算することで、迅速かつ一貫性のあるチェックを実現します
- 読み取り間で権限にずれが生じません
- 柔軟な権限モデル
- 必要に応じてRBAC、ABAC、ReBACを組み合わせられます
- 複雑な階層(組織 → チーム → プロジェクト → リソース)に対応します
- 監査可能性
- すべての権限変更の完全な監査ログを記録します
- パスの追跡により、アクセスが許可された理由を確認できます
- リアルタイム更新
- Convexのリアクティビティにより、権限が変更されるとUIが即座に更新されます
- ポーリングは不要です(Zanzibarより優れています!)
他のソリューションとの比較
| 機能 | @djpanda/convex-authz | OpenFGA | Oso | Cerbos |
|---|---|---|---|---|
| RBAC | ✅ | ✅ | ✅ | ✅ |
| ABAC | ✅ | ⚠️ 制限あり | ✅ | ✅ |
| ReBAC | ✅ | ✅ ネイティブ | ✅ | ⚠️ |
| O(1)検索 | ✅ | ✅ | ✅ | ✅ |
| Convexネイティブ | ✅ | ❌ | ❌ | ❌ |
| 型安全性 | ✅ TypeScript | DSL | Polar | YAML |
| リアルタイム | ✅ Convexクエリ | ポーリング | ポーリング | ポーリング |
| セルフホスト可能 | ✅ | ✅ | ✅ | ✅ |
テスト
パッケージのテストを実行する
cd packages/authz
npm test
convex-testでの使用
import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import schema from "./component/schema.js";
import { api } from "./component/_generated/api.js";
describe("authorization", () => {
it("should assign and check roles", async () => {
const t = convexTest(schema, modules);
await t.mutation(api.mutations.assignRole, {
userId: "user_123",
role: "admin",
});
const hasRole = await t.query(api.queries.hasRole, {
userId: "user_123",
role: "admin",
});
expect(hasRole).toBe(true);
});
});
マルチテナントのデータ分離
authzコンポーネント内のすべてのテーブルには必須のtenantIdフィールドがあり、すべてのデータベースインデックスの先頭の列として使用されます。これにより、テナント間のデータ分離をデータベースレベルで実現します。あるテナントに対するクエリが、別のテナントのデータを返すことはありません。
tenantIdとscope
tenantId | scope | |
|---|---|---|
| 目的 | データ分離境界 | リソースレベルのグループ化 |
| 適用レベル | データベースレベル(インデックスのプレフィックス) | アプリケーションレベル(クエリフィルター) |
| 必須 | 常に | 任意 |
| 例 | "acme-corp" | { type: "project", id: "proj-123" } |
- tenantIdが答えるのは「このデータは誰のものか?」です。つまり、組織/顧客の境界です
- scopeが答えるのは「このテナント内で、どのリソースに適用されるか?」です。たとえば、特定のプロジェクトの管理者などです
設定
// Single-tenant apps — pass any constant string
const authz = new Authz(components.authz, {
permissions, roles,
tenantId: "my-app",
});
// Multi-tenant apps — pass the current organization/tenant ID
const authz = new Authz(components.authz, {
permissions, roles,
tenantId: currentOrgId,
});
テナントをまたぐ操作
別のテナントのデータにアクセスする必要があるまれな管理操作では、withTenant()メソッドを使用してください。
// Returns a new Authz instance scoped to a different tenant
const otherTenant = authz.withTenant("other-org-id");
await otherTenant.getUserRoles(ctx, userId);
コンプライアンス
tenantIdを先頭に配置するインデックス設計は、SOC2およびHIPAAのデータ分離要件を満たします。
- すべてのクエリは、データベースのインデックスレベルでテナントごとに分割されます
- 標準APIを通じてテナント間のデータにアクセスすることは、構造上不可能です
- コンストラクターで
tenantIdが必須であるため、誤って省略することはできません
ベストプラクティス
1. マルチテナントではスコープ付きロールを使用する
// Don't: Global admin
await authz.assignRole(ctx, userId, "admin");
// Do: Scoped admin
await authz.assignRole(ctx, userId, "admin", { type: "org", id: orgId });
2. 本番環境では Authz クライアントを使用する
// Authz uses O(1) indexed lookups by default — no separate class needed
const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
3. 複雑な階層には ReBAC を使用する
// CRM, document sharing, org charts → ReBAC
// Simple role assignments → RBAC
4. 一時的なアクセスには有効期限を設定する
await authz.assignRole(ctx, userId, "contractor", undefined,
Date.now() + 30 * 24 * 60 * 60 * 1000 // 30 days
);
5. 監査ログを必ず使用する
監査ログは、次の用途で非常に役立ちます。
- コンプライアンス(SOC2、GDPR)
- アクセスに関する問題のデバッグ
- セキュリティインシデントの調査
6. 期限切れデータのクリーンアップ(コンポーネント経由でスケジュール)
期限切れのロール割り当てと権限の上書き(およびそれらのインデックス行)は、スケジュール済みのクリーンアップジョブによって削除されます。このジョブは一度有効にすればよく、convex/crons.ts を自分で追加する必要はありません。このコンポーネントには @convex-dev/crons が組み込まれています。コンポーネントのインストール後、次のコマンドを一度だけ実行して、毎日のジョブを登録してください。
npx convex run authz/cronSetup:ensureCleanupCronRegistered
または、デプロイ時に実行する初期化スクリプト(例:convex dev --run init 経由で呼び出す convex/init.ts)から実行します。
await ctx.runMutation(components.authz.cronSetup.ensureCleanupCronRegistered, {});
このジョブは24時間ごとに実行され、roleAssignments、permissionOverrides、effectiveRoles、effectivePermissions をクリーンアップします。任意の方法として、アプリの convex/crons.ts でクリーンアップを定義するか、components.authz.mutations.runScheduledCleanup を手動で実行することもできます。
6.1. 監査ログの保持期間
監査ログが無制限に増加するのを防ぐため(コンプライアンスとコストの観点から)、同じ cron 登録によって毎日の監査ログ保持ジョブもスケジュールされます。Convex の環境変数で設定してください(ダッシュボードまたは CLI を使用)。
| 変数 | 説明 |
|---|---|
AUDIT_RETENTION_DAYS | 指定した日数より古いエントリを削除します(例:90)。省略または 0 の場合、経過日数による削除は行いません。 |
AUDIT_RETENTION_MAX_ENTRIES | 件数がこの値以下になるまで古いエントリを削除し、合計件数を制限します(例:100000)。省略または 0 の場合、件数による削除は行いません。 |
保持を有効にするには、少なくとも一方を設定してください。このジョブは24時間ごとに実行されます(同じ ensureCleanupCronRegistered の手順を使用)。また、必要に応じて引数 { maxAgeDays?, maxEntries? } を指定して components.authz.mutations.runAuditRetentionCleanup を手動で実行し、その実行時の環境変数設定を上書きすることもできます。
7. Authz をグローバルなシングルトンとして使用する
Authz はグローバルコンポーネントです。一度インストールし、アプリ全体で単一のクライアントインスタンスを共有してください。アプリごとに複数の Authz インスタンスを作成しないでください。
convex/
convex.config.ts ← app.use(authz) — registered once
authz.ts ← definePermissions, defineRoles, export authz client
documents.ts ← import { authz } from "./authz"
billing.ts ← import { authz } from "./authz"
settings.ts ← import { authz } from "./authz"
// convex/authz.ts — single source of truth
import { Authz, definePermissions, defineRoles } from "@djpanda/convex-authz";
import { components } from "./_generated/api";
const permissions = definePermissions({
documents: { create: true, read: true, update: true, delete: true },
billing: { view: true, manage: true },
settings: { view: true, manage: true },
});
const roles = defineRoles(permissions, {
admin: {
documents: ["create", "read", "update", "delete"],
billing: ["view", "manage"],
settings: ["view", "manage"],
},
viewer: {
documents: ["read"],
settings: ["view"],
},
});
// Export the single authz client — import this everywhere
export const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
// convex/documents.ts — uses the shared client
import { mutation } from "./_generated/server";
import { authz } from "./authz";
export const deleteDocument = mutation({
args: { docId: v.id("documents") },
handler: async (ctx, args) => {
await authz.require(ctx, userId, "documents:delete");
// ...
},
});
7. 権限をドメインごとに整理する
大規模なアプリでは、権限とロールの定義をドメインごとに分割し、definePermissions と defineRoles を使って単一の authz クライアントに統合してください。
// convex/permissions/documents.ts
export const documentPermissions = {
documents: { create: true, read: true, update: true, delete: true },
};
export const documentRoles = {
editor: { documents: ["create", "read", "update"] as const },
viewer: { documents: ["read"] as const },
};
// convex/permissions/billing.ts
export const billingPermissions = {
billing: { view: true, manage: true },
};
export const billingRoles = {
billing_admin: { billing: ["view", "manage"] as const },
};
// convex/authz.ts — merge all domains
import { Authz, definePermissions, defineRoles } from "@djpanda/convex-authz";
import { components } from "./_generated/api";
import { documentPermissions, documentRoles } from "./permissions/documents";
import { billingPermissions, billingRoles } from "./permissions/billing";
const permissions = definePermissions(documentPermissions, billingPermissions);
const roles = defineRoles(permissions, documentRoles, billingRoles);
export const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
これにより、各ドメインを自己完結させながら、型安全な単一の authz クライアントを作成できます。
8. 他の Convex コンポーネントとの統合
他の Convex コンポーネント(例:@djpanda/convex-tenants)で認可が必要な場合は、同じグローバル authz インスタンスを共有します。手順は次のとおりです。
convex.config.tsで両方のコンポーネントを個別に登録する- もう一方のコンポーネントから、必要な権限とロールをエクスポートする
- それらをアプリ独自の定義と統合する
- authz クライアントをもう一方のコンポーネントの API ファクトリーに渡す
graph LR
subgraph app ["Your App (convex.config.ts)"]
AuthzComp["authz component"]
TenantsComp["tenants component"]
end
subgraph authzSetup ["convex/authz.ts"]
AppPerms["App permissions"]
TenantPerms["Tenant permissions"]
Merge["definePermissions + defineRoles"]
Client["authz client (singleton)"]
AppPerms --> Merge
TenantPerms --> Merge
Merge --> Client
end
Client -->|"import { authz }"| Docs["convex/documents.ts"]
Client -->|"import { authz }"| Billing["convex/billing.ts"]
Client -->|"passed to makeTenantsAPI"| TenantsAPI["convex/tenants.ts"]
// convex/convex.config.ts — register both components
import { defineApp } from "convex/server";
import authz from "@djpanda/convex-authz/convex.config";
import tenants from "@djpanda/convex-tenants/convex.config";
const app = defineApp();
app.use(authz);
app.use(tenants);
export default app;
// convex/authz.ts — merge app + component permissions
import { Authz, definePermissions, defineRoles } from "@djpanda/convex-authz";
import { TENANTS_PERMISSIONS, TENANTS_ROLES } from "@djpanda/convex-tenants";
import { components } from "./_generated/api";
// Your app's own permissions
const appPermissions = {
documents: { create: true, read: true, update: true, delete: true },
};
const appRoles = {
editor: { documents: ["create", "read", "update"] as const },
};
// Merge with tenant component's permissions
const permissions = definePermissions(appPermissions, TENANTS_PERMISSIONS);
const roles = defineRoles(permissions, appRoles, TENANTS_ROLES);
export const authz = new Authz(components.authz, { permissions, roles, tenantId: "my-app" });
// convex/tenants.ts — pass the shared authz client
import { makeTenantsAPI } from "@djpanda/convex-tenants";
import { components } from "./_generated/api";
import { authz } from "./authz";
export const {
createOrg,
inviteMember,
removeMember,
// ...
} = makeTenantsAPI(components.tenants, {
authz,
creatorRole: "owner",
auth: async (ctx) => {
// return the current user ID
},
});
これにより、アプリのあらゆる部分(独自の関数もサードパーティ製コンポーネントも)が、単一で一貫性のある認可レイヤーを共有できます。
開発
# Install dependencies
npm install
# Run development mode
npm run dev
# Run tests
npm test
# Build for production
npm run build
# Type check
npm run typecheck
ファイル構成
packages/authz/
├── package.json # Package configuration
├── README.md # This documentation
├── src/
│ ├── client/
│ │ ├── index.ts # Main exports (Authz, helpers)
│ │ └── index.test.ts # Client tests
│ ├── component/
│ │ ├── convex.config.ts # Component registration
│ │ ├── schema.ts # Database tables and indexes
│ │ ├── helpers.ts # Shared utilities
│ │ ├── queries.ts # Query functions
│ │ ├── mutations.ts # Mutation functions
│ │ ├── rebac.ts # ReBAC relationship functions
│ │ ├── indexed.ts # O(1) indexed functions
│ │ ├── authz.test.ts # RBAC/ABAC tests
│ │ ├── rebac.test.ts # ReBAC tests
│ │ ├── indexed.test.ts # O(1) indexed tests
│ │ └── _generated/ # Auto-generated types
│ └── test.ts # Test helpers
└── example/ # Example app
ライセンス
MIT
コントリビューション
コントリビューションを歓迎します!PR を送信する前に、CONTRIBUTING.md をお読みください。