diff --git a/docs/auth.md b/docs/auth.md index 0c4b9480..59f5f09f 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -10,6 +10,7 @@ interface AuthProvider { logout: (params?: Record) => Promise; check: (params?: Record) => Promise; getIdentity: () => Promise; + // UI-only hints; API, RLS, and action handlers must authorize independently. getPermissions?: (params?: Record) => Promise; register?: (params: Record) => Promise; forgotPassword?: (params: Record) => Promise; @@ -83,11 +84,19 @@ mutate(error); // Calls authProvider.onError → may redirect or logout ### `usePermissions()` +`usePermissions()` is a client-side rendering helper. It can hide or disable UI, but APIs, data providers, and database policies must enforce authorization independently. + ```typescript -const { data, isLoading, error } = usePermissions(); -// data: whatever authProvider.getPermissions() returns +const { raw, has, can, isLoading, error } = usePermissions(); +// raw: whatever trusted authProvider.getPermissions() resolver returns (UI hints only) +// has/can: exact client-side UI checks ``` +The built-in Supabase and SSO providers expose `getPermissions()`, but it returns `null` +until the application configures a trusted resolver. Resolver values may change labels, +navigation, or disabled controls only. API, RLS, and action handlers must independently +authenticate and authorize every request. + ## Auth Pages Built-in glassmorphism auth pages: diff --git a/docs/src/content/docs/hooks/auth.md b/docs/src/content/docs/hooks/auth.md index 1c3a9c92..b5aace5a 100644 --- a/docs/src/content/docs/hooks/auth.md +++ b/docs/src/content/docs/hooks/auth.md @@ -36,19 +36,25 @@ const { isAuthenticated, isLoading } = useIsAuthenticated(); ### `usePermissions()` +`usePermissions()` is a client-side rendering helper. It can hide or disable UI, but APIs, data providers, and database policies must enforce authorization independently. + ```typescript -const { raw, has, can, isLoading, refetch } = usePermissions(); +const permissionHints = usePermissions(); -// Check specific permission -if (has('admin')) { /* ... */ } +// Change navigation or a disabled control from a UI hint. +if (permissionHints.has('admin')) { /* ... */ } -// Check resource:action permission -if (can('posts', 'edit')) { /* ... */ } +// Read a UI hint using the resource:action naming convention. +if (permissionHints.can('posts', 'edit')) { /* ... */ } -// Session-level refresh (e.g. after role upgrade) -await refetch(); +await permissionHints.refetch(); ``` +The built-in Supabase and SSO providers expose `getPermissions()`, but it returns `null` until +the application configures a trusted resolver. Resolver values are UI hints only; do not use +these browser-visible values as an API, RLS, or action authorization decision. The backend must +authenticate and authorize every request. + ### `useOnError()` ```typescript diff --git a/docs/src/content/docs/providers/auth.md b/docs/src/content/docs/providers/auth.md index ccd412d5..4ba3ad6e 100644 --- a/docs/src/content/docs/providers/auth.md +++ b/docs/src/content/docs/providers/auth.md @@ -13,6 +13,7 @@ interface AuthProvider { logout: (params?: Record) => Promise; check: (params?: Record) => Promise; getIdentity: () => Promise; + // UI-only hints; API, RLS, and action handlers must authorize independently. getPermissions?: (params?: Record) => Promise; register?: (params: Record) => Promise; forgotPassword?: (params: Record) => Promise; @@ -21,6 +22,8 @@ interface AuthProvider { } ``` +`getPermissions` is intentionally a UI hint hook. It should return permissions only when the application provides a trusted resolver; it must not replace API, backend, or database authorization. + ## Auth Hooks | Hook | Purpose | @@ -33,7 +36,7 @@ interface AuthProvider { | `useGetIdentity()` | Get current user info | | `useIsAuthenticated()` | Check auth status | | `useOnError()` | Handle API errors (401→logout) | -| `usePermissions()` | Get user permissions | +| `usePermissions()` | Get UI-only permission hints | ### Usage @@ -42,6 +45,11 @@ const { mutate: login, isPending } = useLogin(); await login({ email: 'user@example.com', password: 'secret' }); ``` +The built-in Supabase and SSO providers expose `getPermissions()`, but it returns `null` until +the application configures a trusted resolver. Resolver values may change labels, navigation, +or disabled controls, but browser-visible values never authorize API, RLS, or action requests; +the backend must enforce those separately. + ## Auth Pages Built-in glassmorphism auth pages included: @@ -91,10 +99,19 @@ export const mockAuthProvider: AuthProvider = { ```typescript import { createSupabaseAuthProvider } from '@svadmin/supabase'; -const authProvider = createSupabaseAuthProvider(supabaseClient); +const authProvider = createSupabaseAuthProvider(supabaseClient, { + getPermissions: async ({ client }) => { + const { data, error } = await client + .from('effective_permission_grants') + .select('permission'); + if (error) throw error; + return data.map((grant) => grant.permission); + }, +}); ``` `@supacloud/js` does not change the auth flow. Keep using the official Supabase client with `createSupabaseAuthProvider()`, and layer any task APIs separately through [`@svadmin/supabase/supacloud`](/providers/supacloud). +Do not use user-editable metadata as an authorization fact. ### Appwrite diff --git a/docs/src/content/docs/providers/sso.md b/docs/src/content/docs/providers/sso.md index df796a61..024b6f87 100644 --- a/docs/src/content/docs/providers/sso.md +++ b/docs/src/content/docs/providers/sso.md @@ -73,6 +73,8 @@ interface SSOConfig { refreshLock?: RefreshLock; /** Injectable fetch implementation for tests and SSR runtimes. */ fetcher?: typeof fetch; + /** Trusted permission resolver for UI hints. */ + getPermissions?: (context: SSOPermissionResolverContext) => Promise | unknown; } ``` @@ -114,13 +116,21 @@ const authProvider = createSSOAuthProvider({ }); ``` -### Permissions from ID Token +### Trusted Permissions Resolver -The provider automatically extracts `roles`, `groups`, or `permissions` claims from the ID token via `getPermissions()`. +`getPermissions()` returns `null` unless you configure a trusted resolver. Use the resolver to call your application authorization endpoint or another backend-controlled source. ID Token claims are not turned into browser permissions; this is a UI hint only. API endpoints must independently validate the token and enforce authorization. ```typescript -const permissions = await authProvider.getPermissions(); -// → ['admin', 'editor'] (from ID token claims) +const authProvider = createSSOAuthProvider({ + issuer: 'https://your-tenant.okta.com', + clientId: 'abc', + redirectUri: '/callback', + getPermissions: async ({ createAuthenticatedFetch }) => { + const response = await createAuthenticatedFetch()(new URL('/api/me/permissions', window.location.origin)); + if (!response.ok) throw new Error('Failed to load permissions'); + return response.json(); + }, +}); ``` ### Calling Protected APIs diff --git a/docs/src/content/docs/zh-cn/hooks/auth.md b/docs/src/content/docs/zh-cn/hooks/auth.md index 4cc7a75b..3986b9b3 100644 --- a/docs/src/content/docs/zh-cn/hooks/auth.md +++ b/docs/src/content/docs/zh-cn/hooks/auth.md @@ -36,19 +36,24 @@ const { isAuthenticated, isLoading } = useIsAuthenticated(); ### `usePermissions()` +`usePermissions()` 只是客户端渲染辅助。它可以隐藏或禁用 UI,但 API、DataProvider 和数据库策略必须独立执行授权。 + ```typescript -const { raw, has, can, isLoading, refetch } = usePermissions(); +const permissionHints = usePermissions(); -// 检查特定权限 -if (has('admin')) { /* ... */ } +// 使用 UI 提示调整导航或禁用控件。 +if (permissionHints.has('admin')) { /* ... */ } -// 检查资源:操作权限 -if (can('posts', 'edit')) { /* ... */ } +// 使用 resource:action 命名读取 UI 提示。 +if (permissionHints.can('posts', 'edit')) { /* ... */ } -// 重新获取权限(比如角色升级后) -await refetch(); +await permissionHints.refetch(); ``` +内置 Supabase 与 SSO Provider 会提供 `getPermissions()`,但在应用配置可信 resolver 前返回 +`null`。resolver 的返回值只能作为 UI 提示,绝不能将浏览器中的值作为 API、RLS 或动作授权 +决定;后端必须认证并授权每一个请求。 + ### `useOnError()` ```typescript diff --git a/docs/src/content/docs/zh-cn/providers/auth.md b/docs/src/content/docs/zh-cn/providers/auth.md index 853f8e0c..9037cc95 100644 --- a/docs/src/content/docs/zh-cn/providers/auth.md +++ b/docs/src/content/docs/zh-cn/providers/auth.md @@ -13,6 +13,7 @@ interface AuthProvider { logout: (params?: Record) => Promise; check: (params?: Record) => Promise; getIdentity: () => Promise; + // 仅限 UI 提示;API、RLS 和动作处理器必须独立授权。 getPermissions?: (params?: Record) => Promise; register?: (params: Record) => Promise; forgotPassword?: (params: Record) => Promise; @@ -21,6 +22,8 @@ interface AuthProvider { } ``` +`getPermissions` 只作为 UI hint。只有应用提供可信 resolver 时才应返回权限;它不能替代 API、后端或数据库授权。 + ## 认证 Hook | Hook | 用途 | @@ -33,7 +36,7 @@ interface AuthProvider { | `useGetIdentity()` | 获取当前用户信息 | | `useIsAuthenticated()` | 检查认证状态 | | `useOnError()` | 处理 API 错误(401→登出) | -| `usePermissions()` | 获取用户权限 | +| `usePermissions()` | 获取仅限 UI 的权限提示 | ### 用法 @@ -42,6 +45,10 @@ const { mutate: login, isPending } = useLogin(); await login({ email: 'user@example.com', password: 'secret' }); ``` +内置 Supabase 与 SSO Provider 会提供 `getPermissions()`,但在应用配置可信 resolver 前返回 +`null`。resolver 的返回值可用于调整标签、导航或禁用控件,但浏览器中的值绝不能授权 API、 +RLS 或动作请求;后端必须独立强制授权。 + ## 认证页面 内置毛玻璃风格认证页面: @@ -91,10 +98,19 @@ export const mockAuthProvider: AuthProvider = { ```typescript import { createSupabaseAuthProvider } from '@svadmin/supabase'; -const authProvider = createSupabaseAuthProvider(supabaseClient); +const authProvider = createSupabaseAuthProvider(supabaseClient, { + getPermissions: async ({ client }) => { + const { data, error } = await client + .from('effective_permission_grants') + .select('permission'); + if (error) throw error; + return data.map((grant) => grant.permission); + }, +}); ``` `@supacloud/js` 不会改变认证流程。认证部分仍然建议继续使用官方 Supabase 客户端配合 `createSupabaseAuthProvider()`,任务相关 API 再通过 [`@svadmin/supabase/supacloud`](/zh-cn/providers/supacloud) 单独组合接入。 +不要把用户可修改的 metadata 当成授权事实。 ### Appwrite diff --git a/docs/src/content/docs/zh-cn/providers/sso.md b/docs/src/content/docs/zh-cn/providers/sso.md index 6c1ab5e1..e015a25f 100644 --- a/docs/src/content/docs/zh-cn/providers/sso.md +++ b/docs/src/content/docs/zh-cn/providers/sso.md @@ -65,6 +65,8 @@ interface SSOConfig { refreshBuffer?: number; /** 额外授权请求参数,例如 audience 或 prompt */ authorizationParams?: Record; + /** 可信权限 resolver,仅用于 UI hint */ + getPermissions?: (context: SSOPermissionResolverContext) => Promise | unknown; } ``` @@ -98,13 +100,21 @@ const authProvider = createSSOAuthProvider({ }); ``` -### 从 ID Token 提取权限 +### 可信权限 Resolver -自动从 ID Token 的 `roles`、`groups`、`permissions` claim 中提取权限信息。 +未配置可信 resolver 时,`getPermissions()` 返回 `null`。ID Token claims 不会自动转化为浏览器权限。请在 resolver 中调用应用自己的授权接口或后端控制的数据源。结果仅用于 UI 提示;API 端点必须独立验证令牌并强制授权。 ```typescript -const permissions = await authProvider.getPermissions(); -// → ['admin', 'editor'](来自 ID Token claims) +const authProvider = createSSOAuthProvider({ + issuer: 'https://your-tenant.okta.com', + clientId: 'abc', + redirectUri: '/callback', + getPermissions: async ({ createAuthenticatedFetch }) => { + const response = await createAuthenticatedFetch()(new URL('/api/me/permissions', window.location.origin)); + if (!response.ok) throw new Error('Failed to load permissions'); + return response.json(); + }, +}); ``` ### 调用受保护 API diff --git a/packages/core/src/auth-hooks.svelte.ts b/packages/core/src/auth-hooks.svelte.ts index febdaedd..07118f3f 100644 --- a/packages/core/src/auth-hooks.svelte.ts +++ b/packages/core/src/auth-hooks.svelte.ts @@ -269,11 +269,14 @@ export function useOnError() { // ─── usePermissions ────────────────────────────────────────── /** - * Fetches permissions from authProvider.getPermissions(). - * Returns a reactive object with convenience methods for permission checks. - * + * Fetches UI-only hints from authProvider.getPermissions(). + * Returns a reactive UI helper with convenience methods for permission checks. + * The result can change labels, navigation, and disabled controls, but it runs + * in the browser and never authorizes API, RLS, or action requests. Those + * requests must be independently authenticated and authorized by the backend. + * * Supports `refetch()` for session-level permission refresh (e.g., after role change). - * + * * @example * ```svelte *