mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 12:27:43 +00:00
feat(organization): support multiple permissions check (#2227)
* feat: remove the artificial resource limit so that code can check Also change `permission` to `permissions` (clearer for end user). `permission` is left for backwards compatibility. * docs: add examples for multiple perms checking * refactor: check `permissions` first, then legacy one * feat: use union types for `permission` & `permissions` * fix: properly use union types * fix: remove accidental `@deprecated` comment * chore: lint * fix test * chore: add oneTimeToken plugin to client barrel exports (#2224) * docs(expo): add id token usage * feat(oauth2): override user info on provider sign-in (#2148) * feat(oauth2): override user info on provider sign-in * improve email verification handling * resolve mrge * fix(sso): update overrideUserInfo handling to use provider configuration * fix param * chore: change plugin interface middleware type (#2195) * fix: delete from session table when stopImpersonate called (#2230) * chore: fix active organization inferred type * chore: fix admin test --------- Co-authored-by: Bereket Engida <bekacru@gmail.com> Co-authored-by: Wade Fletcher <3798059+wadefletch@users.noreply.github.com> Co-authored-by: Bereket Engida <86073083+Bekacru@users.noreply.github.com> Co-authored-by: KinfeMichael Tariku <65047246+Kinfe123@users.noreply.github.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import { MysqlDialect } from "kysely";
|
||||
import { createPool } from "mysql2/promise";
|
||||
import { nextCookies } from "better-auth/next-js";
|
||||
import { passkey } from "better-auth/plugins/passkey";
|
||||
import { expo } from "@better-auth/expo";
|
||||
import { stripe } from "@better-auth/stripe";
|
||||
import { Stripe } from "stripe";
|
||||
|
||||
@@ -197,5 +198,7 @@ export const auth = betterAuth({
|
||||
],
|
||||
},
|
||||
}),
|
||||
expo(),
|
||||
],
|
||||
trustedOrigins: ["exp://"],
|
||||
});
|
||||
|
||||
@@ -416,10 +416,18 @@ To check a user's permissions, you can use the `hasPermission` function provided
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
const canCreateProject = await authClient.admin.hasPermission({
|
||||
permission: {
|
||||
permissions: {
|
||||
project: ["create"],
|
||||
},
|
||||
});
|
||||
|
||||
// You can also check multiple resource permissions at the same time
|
||||
const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({
|
||||
permissions: {
|
||||
project: ["create"],
|
||||
sale: ["create"]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you want to check a user's permissions server-side, you can use the `userHasPermission` action provided by the `api` to check the user's permissions.
|
||||
@@ -429,21 +437,32 @@ import { auth } from "@/auth";
|
||||
auth.api.userHasPermission({
|
||||
body: {
|
||||
userId: 'id', //the user id
|
||||
permission: {
|
||||
permissions: {
|
||||
project: ["create"], // This must match the structure in your access control
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
//you can also just pass the role directly
|
||||
// You can also just pass the role directly
|
||||
auth.api.userHasPermission({
|
||||
body: {
|
||||
role: "admin",
|
||||
permission: {
|
||||
permissions: {
|
||||
project: ["create"], // This must match the structure in your access control
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// You can also check multiple resource permissions at the same time
|
||||
auth.api.userHasPermission({
|
||||
body: {
|
||||
role: "admin",
|
||||
permissions: {
|
||||
project: ["create"], // This must match the structure in your access control
|
||||
sale: ["create"]
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -453,11 +472,20 @@ Once you have defined the roles and permissions to avoid checking the permission
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
const canCreateProject = client.admin.checkRolePermission({
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["delete"],
|
||||
},
|
||||
role: "admin",
|
||||
});
|
||||
|
||||
// You can also check multiple resource permissions at the same time
|
||||
const canCreateProjectAndRevokeSession = client.admin.checkRolePermission({
|
||||
permissions: {
|
||||
user: ["delete"],
|
||||
session: ["revoke"]
|
||||
},
|
||||
role: "admin",
|
||||
});
|
||||
```
|
||||
|
||||
## Schema
|
||||
@@ -584,4 +612,3 @@ admin({
|
||||
bannedUserMessage: "Custom banned user message",
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -800,24 +800,43 @@ You can use the `hasPermission` action provided by the `api` to check the permis
|
||||
|
||||
```ts title="api.ts"
|
||||
import { auth } from "@/auth";
|
||||
auth.api.hasPermission({
|
||||
auth.api.hasPermission({
|
||||
headers: await headers(),
|
||||
body: {
|
||||
permission: {
|
||||
permissions: {
|
||||
project: ["create"] // This must match the structure in your access control
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// You can also check multiple resource permissions at the same time
|
||||
auth.api.hasPermission({
|
||||
headers: await headers(),
|
||||
body: {
|
||||
permissions: {
|
||||
project: ["create"], // This must match the structure in your access control
|
||||
sale: ["create"]
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
If you want to check the permission of the user on the client from the server you can use the `hasPermission` function provided by the client.
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
const canCreateProject = await authClient.organization.hasPermission({
|
||||
permission: {
|
||||
permissions: {
|
||||
project: ["create"]
|
||||
}
|
||||
})
|
||||
|
||||
// You can also check multiple resource permissions at the same time
|
||||
const canCreateProjectAndCreateSale = await authClient.organization.hasPermission({
|
||||
permissions: {
|
||||
project: ["create"],
|
||||
sale: ["create"]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Check Role Permission**:
|
||||
@@ -826,11 +845,20 @@ Once you have defined the roles and permissions to avoid checking the permission
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
const canCreateProject = client.organization.checkRolePermission({
|
||||
permission: {
|
||||
permissions: {
|
||||
organization: ["delete"],
|
||||
},
|
||||
role: "admin",
|
||||
});
|
||||
|
||||
// You can also check multiple resource permissions at the same time
|
||||
const canCreateProjectAndCreateSale = client.organization.checkRolePermission({
|
||||
permissions: {
|
||||
organization: ["delete"],
|
||||
member: ["delete"]
|
||||
},
|
||||
role: "admin",
|
||||
});
|
||||
```
|
||||
|
||||
## Teams
|
||||
|
||||
@@ -624,7 +624,7 @@ describe("Admin plugin", async () => {
|
||||
|
||||
describe("access control", async (it) => {
|
||||
const ac = createAccessControl({
|
||||
user: ["create", "read", "update", "delete", "list"],
|
||||
user: ["create", "read", "update", "delete", "list", "bulk-delete"],
|
||||
order: ["create", "read", "update", "delete", "update-many"],
|
||||
});
|
||||
|
||||
@@ -699,61 +699,136 @@ describe("access control", async (it) => {
|
||||
it("should validate on the client", async () => {
|
||||
const canCreateOrder = client.admin.checkRolePermission({
|
||||
role: "admin",
|
||||
permission: {
|
||||
permissions: {
|
||||
order: ["create"],
|
||||
},
|
||||
});
|
||||
expect(canCreateOrder).toBe(true);
|
||||
|
||||
// To be removed when `permission` will be removed entirely
|
||||
const canCreateOrderLegacy = client.admin.checkRolePermission({
|
||||
role: "admin",
|
||||
permission: {
|
||||
order: ["create"],
|
||||
user: ["read"],
|
||||
},
|
||||
});
|
||||
expect(canCreateOrderLegacy).toBe(true);
|
||||
|
||||
const canCreateOrderAndReadUser = client.admin.checkRolePermission({
|
||||
role: "admin",
|
||||
permissions: {
|
||||
order: ["create"],
|
||||
user: ["read"],
|
||||
},
|
||||
});
|
||||
expect(canCreateOrderAndReadUser).toBe(true);
|
||||
|
||||
const canCreateUser = client.admin.checkRolePermission({
|
||||
role: "user",
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["create"],
|
||||
},
|
||||
});
|
||||
expect(canCreateUser).toBe(false);
|
||||
|
||||
const canCreateOrderAndCreateUser = client.admin.checkRolePermission({
|
||||
role: "user",
|
||||
permissions: {
|
||||
order: ["create"],
|
||||
user: ["create"],
|
||||
},
|
||||
});
|
||||
expect(canCreateOrderAndCreateUser).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate using userId", async () => {
|
||||
const canCreateUser = await auth.api.userHasPermission({
|
||||
body: {
|
||||
userId: user.id,
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["create"],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(canCreateUser.success).toBe(true);
|
||||
|
||||
const canCreateUserAndCreateOrder = await auth.api.userHasPermission({
|
||||
body: {
|
||||
userId: user.id,
|
||||
permissions: {
|
||||
user: ["create"],
|
||||
order: ["create"],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(canCreateUserAndCreateOrder.success).toBe(true);
|
||||
|
||||
const canUpdateManyOrder = await auth.api.userHasPermission({
|
||||
body: {
|
||||
userId: user.id,
|
||||
permission: {
|
||||
permissions: {
|
||||
order: ["update-many"],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(canUpdateManyOrder.success).toBe(false);
|
||||
|
||||
const canUpdateManyOrderAndBulkDeleteUser =
|
||||
await auth.api.userHasPermission({
|
||||
body: {
|
||||
userId: user.id,
|
||||
permissions: {
|
||||
user: ["bulk-delete"],
|
||||
order: ["update-many"],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(canUpdateManyOrderAndBulkDeleteUser.success).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate using role", async () => {
|
||||
const canCreateUser = await auth.api.userHasPermission({
|
||||
body: {
|
||||
role: "admin",
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["create"],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(canCreateUser.success).toBe(true);
|
||||
|
||||
const canCreateUserAndCreateOrder = await auth.api.userHasPermission({
|
||||
body: {
|
||||
role: "admin",
|
||||
permissions: {
|
||||
user: ["create"],
|
||||
order: ["create"],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(canCreateUserAndCreateOrder.success).toBe(true);
|
||||
|
||||
const canUpdateOrder = await auth.api.userHasPermission({
|
||||
body: {
|
||||
role: "user",
|
||||
permission: {
|
||||
permissions: {
|
||||
order: ["update"],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(canUpdateOrder.success).toBe(false);
|
||||
|
||||
const canUpdateOrderAndUpdateUser = await auth.api.userHasPermission({
|
||||
body: {
|
||||
role: "user",
|
||||
permissions: {
|
||||
order: ["update"],
|
||||
user: ["update"],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(canUpdateOrderAndUpdateUser.success).toBe(false);
|
||||
});
|
||||
|
||||
it("shouldn't allow to list users", async () => {
|
||||
|
||||
@@ -119,6 +119,26 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
? S
|
||||
: DefaultStatements;
|
||||
|
||||
type PermissionType = {
|
||||
[key in keyof Statements]?: Array<
|
||||
Statements[key] extends readonly unknown[]
|
||||
? Statements[key][number]
|
||||
: never
|
||||
>;
|
||||
};
|
||||
type PermissionExclusive =
|
||||
| {
|
||||
/**
|
||||
* @deprecated Use `permissions` instead
|
||||
*/
|
||||
permission: PermissionType;
|
||||
permissions?: never;
|
||||
}
|
||||
| {
|
||||
permissions: PermissionType;
|
||||
permission?: never;
|
||||
};
|
||||
|
||||
const adminMiddleware = createAuthMiddleware(async (ctx) => {
|
||||
const session = await getSessionFromCtx(ctx);
|
||||
if (!session) {
|
||||
@@ -265,7 +285,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: ctx.context.session.user.id,
|
||||
role: ctx.context.session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["set-role"],
|
||||
},
|
||||
});
|
||||
@@ -370,7 +390,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: session.user.id,
|
||||
role: session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["create"],
|
||||
},
|
||||
});
|
||||
@@ -528,7 +548,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: ctx.context.session.user.id,
|
||||
role: session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["list"],
|
||||
},
|
||||
});
|
||||
@@ -629,7 +649,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: ctx.context.session.user.id,
|
||||
role: session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
session: ["list"],
|
||||
},
|
||||
});
|
||||
@@ -689,7 +709,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: ctx.context.session.user.id,
|
||||
role: session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["ban"],
|
||||
},
|
||||
});
|
||||
@@ -769,7 +789,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: ctx.context.session.user.id,
|
||||
role: session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["ban"],
|
||||
},
|
||||
});
|
||||
@@ -848,7 +868,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: ctx.context.session.user.id,
|
||||
role: ctx.context.session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["impersonate"],
|
||||
},
|
||||
});
|
||||
@@ -1001,7 +1021,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: ctx.context.session.user.id,
|
||||
role: session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
session: ["revoke"],
|
||||
},
|
||||
});
|
||||
@@ -1061,7 +1081,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: ctx.context.session.user.id,
|
||||
role: session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
session: ["revoke"],
|
||||
},
|
||||
});
|
||||
@@ -1120,7 +1140,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: ctx.context.session.user.id,
|
||||
role: session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["delete"],
|
||||
},
|
||||
});
|
||||
@@ -1178,7 +1198,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: ctx.context.session.user.id,
|
||||
role: ctx.context.session.user.role,
|
||||
options: opts,
|
||||
permission: {
|
||||
permissions: {
|
||||
user: ["set-password"],
|
||||
},
|
||||
});
|
||||
@@ -1204,11 +1224,23 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
"/admin/has-permission",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
permission: z.record(z.string(), z.array(z.string())),
|
||||
body: z
|
||||
.object({
|
||||
userId: z.coerce.string().optional(),
|
||||
role: z.string().optional(),
|
||||
})
|
||||
.and(
|
||||
z.union([
|
||||
z.object({
|
||||
permission: z.record(z.string(), z.array(z.string())),
|
||||
permissions: z.undefined(),
|
||||
}),
|
||||
z.object({
|
||||
permission: z.undefined(),
|
||||
permissions: z.record(z.string(), z.array(z.string())),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
metadata: {
|
||||
openapi: {
|
||||
description: "Check if the user has permission",
|
||||
@@ -1221,9 +1253,14 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
permission: {
|
||||
type: "object",
|
||||
description: "The permission to check",
|
||||
deprecated: true,
|
||||
},
|
||||
permissions: {
|
||||
type: "object",
|
||||
description: "The permission to check",
|
||||
},
|
||||
},
|
||||
required: ["permission"],
|
||||
required: ["permissions"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1251,11 +1288,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
},
|
||||
},
|
||||
$Infer: {
|
||||
body: {} as {
|
||||
permission: {
|
||||
//@ts-expect-error
|
||||
[key in keyof Statements]?: Array<Statements[key][number]>;
|
||||
};
|
||||
body: {} as PermissionExclusive & {
|
||||
userId?: string;
|
||||
role?: InferAdminRolesFromOption<O>;
|
||||
},
|
||||
@@ -1263,13 +1296,10 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
},
|
||||
},
|
||||
async (ctx) => {
|
||||
if (
|
||||
!ctx.body.permission ||
|
||||
Object.keys(ctx.body.permission).length > 1
|
||||
) {
|
||||
if (!ctx.body?.permission && !ctx.body?.permissions) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message:
|
||||
"invalid permission check. you can only check one resource permission at a time.",
|
||||
"invalid permission check. no permission(s) were passed.",
|
||||
});
|
||||
}
|
||||
const session = await getSessionFromCtx(ctx);
|
||||
@@ -1297,7 +1327,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
|
||||
userId: user.id,
|
||||
role: user.role,
|
||||
options: options as AdminOptions,
|
||||
permission: ctx.body.permission as any,
|
||||
permissions: (ctx.body.permissions ?? ctx.body.permission) as any,
|
||||
});
|
||||
return ctx.json({
|
||||
error: null,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { BetterAuthError } from "../../error";
|
||||
import type { BetterAuthClientPlugin } from "../../types";
|
||||
import { type AccessControl, type Role } from "../access";
|
||||
import { adminAc, defaultStatements, userAc } from "./access";
|
||||
@@ -17,6 +16,26 @@ export const adminClient = <O extends AdminClientOptions>(options?: O) => {
|
||||
type Statements = O["ac"] extends AccessControl<infer S>
|
||||
? S
|
||||
: DefaultStatements;
|
||||
type PermissionType = {
|
||||
[key in keyof Statements]?: Array<
|
||||
Statements[key] extends readonly unknown[]
|
||||
? Statements[key][number]
|
||||
: never
|
||||
>;
|
||||
};
|
||||
type PermissionExclusive =
|
||||
| {
|
||||
/**
|
||||
* @deprecated Use `permissions` instead
|
||||
*/
|
||||
permission: PermissionType;
|
||||
permissions?: never;
|
||||
}
|
||||
| {
|
||||
permissions: PermissionType;
|
||||
permission?: never;
|
||||
};
|
||||
|
||||
const roles = {
|
||||
admin: adminAc,
|
||||
user: userAc,
|
||||
@@ -44,25 +63,18 @@ export const adminClient = <O extends AdminClientOptions>(options?: O) => {
|
||||
R extends O extends { roles: any }
|
||||
? keyof O["roles"]
|
||||
: "admin" | "user",
|
||||
>(data: {
|
||||
>(
|
||||
data: PermissionExclusive & {
|
||||
role: R;
|
||||
permission: {
|
||||
//@ts-expect-error fix this later
|
||||
[key in keyof Statements]?: Statements[key][number][];
|
||||
};
|
||||
}) => {
|
||||
if (Object.keys(data.permission).length > 1) {
|
||||
throw new BetterAuthError(
|
||||
"you can only check one resource permission at a time.",
|
||||
);
|
||||
}
|
||||
},
|
||||
) => {
|
||||
const isAuthorized = hasPermission({
|
||||
role: data.role as string,
|
||||
options: {
|
||||
ac: options?.ac,
|
||||
roles: roles,
|
||||
},
|
||||
permission: data.permission as any,
|
||||
permissions: (data.permissions ?? data.permission) as any,
|
||||
});
|
||||
return isAuthorized;
|
||||
},
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
import { defaultRoles } from "./access";
|
||||
import type { AdminOptions } from "./admin";
|
||||
|
||||
export const hasPermission = (input: {
|
||||
type PermissionExclusive =
|
||||
| {
|
||||
/**
|
||||
* @deprecated Use `permissions` instead
|
||||
*/
|
||||
permission: { [key: string]: string[] };
|
||||
permissions?: never;
|
||||
}
|
||||
| {
|
||||
permissions: { [key: string]: string[] };
|
||||
permission?: never;
|
||||
};
|
||||
|
||||
export const hasPermission = (
|
||||
input: {
|
||||
userId?: string;
|
||||
role?: string;
|
||||
options?: AdminOptions;
|
||||
permission: {
|
||||
[key: string]: string[];
|
||||
};
|
||||
}) => {
|
||||
} & PermissionExclusive,
|
||||
) => {
|
||||
if (input.userId && input.options?.adminUserIds?.includes(input.userId)) {
|
||||
return true;
|
||||
}
|
||||
if (!input.permissions && !input.permission) {
|
||||
return false;
|
||||
}
|
||||
const roles = (input.role || input.options?.defaultRole || "user").split(",");
|
||||
const acRoles = input.options?.roles || defaultRoles;
|
||||
for (const role of roles) {
|
||||
const _role = acRoles[role as keyof typeof acRoles];
|
||||
const result = _role?.authorize(input.permission);
|
||||
const result = _role?.authorize(input.permission ?? input.permissions);
|
||||
if (result?.success) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { type AccessControl, type Role } from "../access";
|
||||
import type { BetterAuthClientPlugin } from "../../client/types";
|
||||
import type { organization } from "./organization";
|
||||
import { useAuthQuery } from "../../client";
|
||||
import { BetterAuthError } from "../../error";
|
||||
import { defaultStatements, adminAc, memberAc, ownerAc } from "./access";
|
||||
import { hasPermission } from "./has-permission";
|
||||
|
||||
@@ -37,6 +36,26 @@ export const organizationClient = <O extends OrganizationClientOptions>(
|
||||
type Statements = O["ac"] extends AccessControl<infer S>
|
||||
? S
|
||||
: DefaultStatements;
|
||||
type PermissionType = {
|
||||
[key in keyof Statements]?: Array<
|
||||
Statements[key] extends readonly unknown[]
|
||||
? Statements[key][number]
|
||||
: never
|
||||
>;
|
||||
};
|
||||
type PermissionExclusive =
|
||||
| {
|
||||
/**
|
||||
* @deprecated Use `permissions` instead
|
||||
*/
|
||||
permission: PermissionType;
|
||||
permissions?: never;
|
||||
}
|
||||
| {
|
||||
permissions: PermissionType;
|
||||
permission?: never;
|
||||
};
|
||||
|
||||
const roles = {
|
||||
admin: adminAc,
|
||||
member: memberAc,
|
||||
@@ -86,25 +105,18 @@ export const organizationClient = <O extends OrganizationClientOptions>(
|
||||
R extends O extends { roles: any }
|
||||
? keyof O["roles"]
|
||||
: "admin" | "member" | "owner",
|
||||
>(data: {
|
||||
>(
|
||||
data: PermissionExclusive & {
|
||||
role: R;
|
||||
permission: {
|
||||
//@ts-expect-error fix this later
|
||||
[key in keyof Statements]?: Statements[key][number][];
|
||||
};
|
||||
}) => {
|
||||
if (Object.keys(data.permission).length > 1) {
|
||||
throw new BetterAuthError(
|
||||
"you can only check one resource permission at a time.",
|
||||
);
|
||||
}
|
||||
},
|
||||
) => {
|
||||
const isAuthorized = hasPermission({
|
||||
role: data.role as string,
|
||||
options: {
|
||||
ac: options?.ac,
|
||||
roles: roles,
|
||||
},
|
||||
permission: data.permission as any,
|
||||
permissions: (data.permissions ?? data.permission) as any,
|
||||
});
|
||||
return isAuthorized;
|
||||
},
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
import { defaultRoles } from "./access";
|
||||
import type { OrganizationOptions } from "./organization";
|
||||
|
||||
export const hasPermission = (input: {
|
||||
type PermissionExclusive =
|
||||
| {
|
||||
/**
|
||||
* @deprecated Use `permissions` instead
|
||||
*/
|
||||
permission: { [key: string]: string[] };
|
||||
permissions?: never;
|
||||
}
|
||||
| {
|
||||
permissions: { [key: string]: string[] };
|
||||
permission?: never;
|
||||
};
|
||||
|
||||
export const hasPermission = (
|
||||
input: {
|
||||
role: string;
|
||||
options: OrganizationOptions;
|
||||
permission: {
|
||||
[key: string]: string[];
|
||||
};
|
||||
}) => {
|
||||
} & PermissionExclusive,
|
||||
) => {
|
||||
if (!input.permissions && !input.permission) {
|
||||
return false;
|
||||
}
|
||||
const roles = input.role.split(",");
|
||||
const acRoles = input.options.roles || defaultRoles;
|
||||
for (const role of roles) {
|
||||
const _role = acRoles[role as keyof typeof acRoles];
|
||||
const result = _role?.authorize(input.permission);
|
||||
const result = _role?.authorize(input.permissions ?? input.permission);
|
||||
if (result?.success) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { createAuthClient } from "../../client";
|
||||
import { organizationClient } from "./client";
|
||||
import { createAccessControl } from "../access";
|
||||
import { ORGANIZATION_ERROR_CODES } from "./error-codes";
|
||||
import { BetterAuthError } from "../../error";
|
||||
import { APIError } from "better-call";
|
||||
|
||||
describe("organization", async (it) => {
|
||||
@@ -500,7 +499,7 @@ describe("organization", async (it) => {
|
||||
},
|
||||
});
|
||||
const hasPermission = await client.organization.hasPermission({
|
||||
permission: {
|
||||
permissions: {
|
||||
member: ["update"],
|
||||
},
|
||||
fetchOptions: {
|
||||
@@ -508,6 +507,17 @@ describe("organization", async (it) => {
|
||||
},
|
||||
});
|
||||
expect(hasPermission.data?.success).toBe(true);
|
||||
|
||||
const hasMultiplePermissions = await client.organization.hasPermission({
|
||||
permissions: {
|
||||
member: ["update"],
|
||||
invitation: ["create"],
|
||||
},
|
||||
fetchOptions: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
expect(hasMultiplePermissions.data?.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow deleting organization", async () => {
|
||||
@@ -795,15 +805,25 @@ describe("access control", async (it) => {
|
||||
it("should return success", async () => {
|
||||
const canCreateProject = checkRolePermission({
|
||||
role: "admin",
|
||||
permission: {
|
||||
permissions: {
|
||||
project: ["create"],
|
||||
},
|
||||
});
|
||||
expect(canCreateProject).toBe(true);
|
||||
const canCreateProjectServer = await hasPermission({
|
||||
|
||||
// To be removed when `permission` will be removed entirely
|
||||
const canCreateProjectLegacy = checkRolePermission({
|
||||
role: "admin",
|
||||
permission: {
|
||||
project: ["create"],
|
||||
},
|
||||
});
|
||||
expect(canCreateProjectLegacy).toBe(true);
|
||||
|
||||
const canCreateProjectServer = await hasPermission({
|
||||
permissions: {
|
||||
project: ["create"],
|
||||
},
|
||||
fetchOptions: {
|
||||
headers,
|
||||
},
|
||||
@@ -814,7 +834,7 @@ describe("access control", async (it) => {
|
||||
it("should return not success", async () => {
|
||||
const canCreateProject = checkRolePermission({
|
||||
role: "admin",
|
||||
permission: {
|
||||
permissions: {
|
||||
project: ["delete"],
|
||||
},
|
||||
});
|
||||
@@ -822,21 +842,14 @@ describe("access control", async (it) => {
|
||||
});
|
||||
|
||||
it("should return not success", async () => {
|
||||
let error: BetterAuthError | null = null;
|
||||
try {
|
||||
checkRolePermission({
|
||||
const res = checkRolePermission({
|
||||
role: "admin",
|
||||
permission: {
|
||||
permissions: {
|
||||
project: ["read"],
|
||||
sales: ["delete"],
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof BetterAuthError) {
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
expect(error).toBeInstanceOf(BetterAuthError);
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -443,6 +443,26 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
||||
type Statements = O["ac"] extends AccessControl<infer S>
|
||||
? S
|
||||
: DefaultStatements;
|
||||
type PermissionType = {
|
||||
[key in keyof Statements]?: Array<
|
||||
Statements[key] extends readonly unknown[]
|
||||
? Statements[key][number]
|
||||
: never
|
||||
>;
|
||||
};
|
||||
type PermissionExclusive =
|
||||
| {
|
||||
/**
|
||||
* @deprecated Use `permissions` instead
|
||||
*/
|
||||
permission: PermissionType;
|
||||
permissions?: never;
|
||||
}
|
||||
| {
|
||||
permissions: PermissionType;
|
||||
permission?: never;
|
||||
};
|
||||
|
||||
return {
|
||||
id: "organization",
|
||||
endpoints: {
|
||||
@@ -454,18 +474,26 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
||||
{
|
||||
method: "POST",
|
||||
requireHeaders: true,
|
||||
body: z.object({
|
||||
body: z
|
||||
.object({
|
||||
organizationId: z.string().optional(),
|
||||
})
|
||||
.and(
|
||||
z.union([
|
||||
z.object({
|
||||
permission: z.record(z.string(), z.array(z.string())),
|
||||
permissions: z.undefined(),
|
||||
}),
|
||||
z.object({
|
||||
permission: z.undefined(),
|
||||
permissions: z.record(z.string(), z.array(z.string())),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
use: [orgSessionMiddleware],
|
||||
metadata: {
|
||||
$Infer: {
|
||||
body: {} as {
|
||||
permission: {
|
||||
//@ts-expect-error
|
||||
[key in keyof Statements]?: Array<Statements[key][number]>;
|
||||
};
|
||||
body: {} as PermissionExclusive & {
|
||||
organizationId?: string;
|
||||
},
|
||||
},
|
||||
@@ -480,9 +508,14 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
||||
permission: {
|
||||
type: "object",
|
||||
description: "The permission to check",
|
||||
deprecated: true,
|
||||
},
|
||||
permissions: {
|
||||
type: "object",
|
||||
description: "The permission to check",
|
||||
},
|
||||
},
|
||||
required: ["permission"],
|
||||
required: ["permissions"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -534,7 +567,7 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
||||
const result = hasPermission({
|
||||
role: member.role,
|
||||
options: options as OrganizationOptions,
|
||||
permission: ctx.body.permission as any,
|
||||
permissions: (ctx.body.permissions ?? ctx.body.permission) as any,
|
||||
});
|
||||
return ctx.json({
|
||||
error: null,
|
||||
|
||||
@@ -153,7 +153,7 @@ export const createInvitation = <O extends OrganizationOptions | undefined>(
|
||||
const canInvite = hasPermission({
|
||||
role: member.role,
|
||||
options: ctx.context.orgOptions,
|
||||
permission: {
|
||||
permissions: {
|
||||
invitation: ["create"],
|
||||
},
|
||||
});
|
||||
@@ -489,7 +489,7 @@ export const cancelInvitation = createAuthEndpoint(
|
||||
const canCancel = hasPermission({
|
||||
role: member.role,
|
||||
options: ctx.context.orgOptions,
|
||||
permission: {
|
||||
permissions: {
|
||||
invitation: ["cancel"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -244,7 +244,7 @@ export const removeMember = createAuthEndpoint(
|
||||
const canDeleteMember = hasPermission({
|
||||
role: member.role,
|
||||
options: ctx.context.orgOptions,
|
||||
permission: {
|
||||
permissions: {
|
||||
member: ["delete"],
|
||||
},
|
||||
});
|
||||
@@ -399,7 +399,7 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
|
||||
const canUpdateMember = hasPermission({
|
||||
role: member.role,
|
||||
options: ctx.context.orgOptions,
|
||||
permission: {
|
||||
permissions: {
|
||||
member: ["update"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -321,7 +321,7 @@ export const updateOrganization = createAuthEndpoint(
|
||||
});
|
||||
}
|
||||
const canUpdateOrg = hasPermission({
|
||||
permission: {
|
||||
permissions: {
|
||||
organization: ["update"],
|
||||
},
|
||||
role: member.role,
|
||||
@@ -403,7 +403,7 @@ export const deleteOrganization = createAuthEndpoint(
|
||||
}
|
||||
const canDeleteOrg = hasPermission({
|
||||
role: member.role,
|
||||
permission: {
|
||||
permissions: {
|
||||
organization: ["delete"],
|
||||
},
|
||||
options: ctx.context.orgOptions,
|
||||
|
||||
@@ -99,7 +99,7 @@ export const createTeam = <O extends OrganizationOptions | undefined>(
|
||||
const canCreate = hasPermission({
|
||||
role: member.role,
|
||||
options: ctx.context.orgOptions,
|
||||
permission: {
|
||||
permissions: {
|
||||
team: ["create"],
|
||||
},
|
||||
});
|
||||
@@ -208,7 +208,7 @@ export const removeTeam = createAuthEndpoint(
|
||||
const canRemove = hasPermission({
|
||||
role: member.role,
|
||||
options: ctx.context.orgOptions,
|
||||
permission: {
|
||||
permissions: {
|
||||
team: ["delete"],
|
||||
},
|
||||
});
|
||||
@@ -329,7 +329,7 @@ export const updateTeam = createAuthEndpoint(
|
||||
const canUpdate = hasPermission({
|
||||
role: member.role,
|
||||
options: ctx.context.orgOptions,
|
||||
permission: {
|
||||
permissions: {
|
||||
team: ["update"],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user