diff --git a/packages/better-auth/src/plugins/organization/has-permission.ts b/packages/better-auth/src/plugins/organization/has-permission.ts index 0a4f771bbe..e2b58d2dab 100644 --- a/packages/better-auth/src/plugins/organization/has-permission.ts +++ b/packages/better-auth/src/plugins/organization/has-permission.ts @@ -18,13 +18,22 @@ export const hasPermission = ( input: { role: string; options: OrganizationOptions; + allowCreatorAllPermissions?: boolean; } & PermissionExclusive, ) => { if (!input.permissions && !input.permission) { return false; } + const roles = input.role.split(","); const acRoles = input.options.roles || defaultRoles; + + const creatorRole = input.options.creatorRole || "owner"; + const isCreator = roles.includes(creatorRole); + + const allowCreatorsAllPermissions = input.allowCreatorAllPermissions || false; + if (isCreator && allowCreatorsAllPermissions) return true; + for (const role of roles) { const _role = acRoles[role as keyof typeof acRoles]; const result = _role?.authorize(input.permissions ?? input.permission); diff --git a/packages/better-auth/src/plugins/organization/organization.test.ts b/packages/better-auth/src/plugins/organization/organization.test.ts index 63a0170823..6b127cf651 100644 --- a/packages/better-auth/src/plugins/organization/organization.test.ts +++ b/packages/better-auth/src/plugins/organization/organization.test.ts @@ -6,6 +6,8 @@ import { organizationClient } from "./client"; import { createAccessControl } from "../access"; import { ORGANIZATION_ERROR_CODES } from "./error-codes"; import { APIError } from "better-call"; +import { admin } from "../admin"; +import { ownerAc } from "./access"; describe("organization", async (it) => { const { auth, signInWithTestUser, signInWithUser, cookieSetter } = @@ -1028,6 +1030,176 @@ describe("cancel pending invitations on re-invite", async () => { }); }); +describe("owner can update roles", async () => { + const statement = { + custom: ["custom"], + } as const; + + const ac = createAccessControl(statement); + + const custom = ac.newRole({ + custom: ["custom"], + }); + + const { auth } = await getTestInstance({ + emailAndPassword: { + enabled: true, + }, + plugins: [ + admin(), + organization({ + ac, + roles: { + custom, + owner: ownerAc, + }, + }), + ], + }); + + const adminEmail = "admin@email.com"; + const adminPassword = "adminpassword"; + + await auth.api.createUser({ + body: { + email: adminEmail, + password: adminPassword, + name: "Admin", + role: "admin", + }, + }); + + const { headers } = await auth.api.signInEmail({ + returnHeaders: true, + body: { + email: adminEmail, + password: adminPassword, + }, + }); + + const adminCookie = headers.getSetCookie()[0]; + + const org = await auth.api.createOrganization({ + headers: { cookie: adminCookie }, + body: { + name: "Org", + slug: "org", + }, + }); + + if (!org) { + throw new Error("couldn't create an organization"); + } + + const ownerId = org.members.at(0)?.id; + if (!ownerId) { + throw new Error("couldn't get the owner id"); + } + + it("allows setting custom role to a user", async () => { + const userEmail = "user@email.com"; + const userPassword = "userpassword"; + + const { user } = await auth.api.createUser({ + headers: { cookie: adminCookie }, + body: { + name: "user", + email: userEmail, + password: userPassword, + }, + }); + + const addMemberRes = await auth.api.addMember({ + headers: { cookie: adminCookie }, + body: { + organizationId: org.id, + userId: user.id, + role: [], + }, + }); + + if (!addMemberRes) { + throw new Error("couldn't add user as a member to a repo"); + } + + await auth.api.updateMemberRole({ + headers: { cookie: adminCookie }, + body: { + organizationId: org.id, + memberId: addMemberRes.id, + role: ["custom"], + }, + }); + + const signInRes = await auth.api.signInEmail({ + returnHeaders: true, + body: { + email: userEmail, + password: userPassword, + }, + }); + + const userCookie = signInRes.headers.getSetCookie()[0]; + + const permissionRes = await auth.api.hasPermission({ + headers: { cookie: userCookie }, + body: { + organizationId: org.id, + permissions: { + custom: ["custom"], + }, + }, + }); + + expect(permissionRes.success).toBe(true); + expect(permissionRes.error).toBeNull(); + }); + + it("allows org owner to set a custom role for themselves", async () => { + await auth.api.updateMemberRole({ + headers: { cookie: adminCookie }, + body: { + organizationId: org.id, + memberId: ownerId, + role: ["owner", "custom"], + }, + }); + + const permissionRes = await auth.api.hasPermission({ + headers: { cookie: adminCookie }, + body: { + organizationId: org.id, + permissions: { + custom: ["custom"], + }, + }, + }); + + expect(permissionRes.success).toBe(true); + expect(permissionRes.error).toBeNull(); + }); + + // TODO: We might not want to allow this. + it("allows an org owner to remove their own creator role", async () => { + await auth.api.updateMemberRole({ + headers: { cookie: adminCookie }, + body: { + organizationId: org.id, + memberId: ownerId, + role: [], + }, + }); + + const member = await auth.api.getActiveMember({ + headers: { cookie: adminCookie }, + }); + + console.log({ member }); + + expect(member?.role).toBe(""); + }); +}); + describe("types", async (it) => { const { auth } = await getTestInstance({ plugins: [organization({})], diff --git a/packages/better-auth/src/plugins/organization/routes/crud-members.ts b/packages/better-auth/src/plugins/organization/routes/crud-members.ts index 37a5d5c7df..a2442d3228 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-members.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-members.ts @@ -381,15 +381,19 @@ export const updateMemberRole = (option: O) => }); } - const toBeUpdatedMemberRoles = toBeUpdatedMember.role.split(","); - const updatingMemberRoles = member.role.split(","); const creatorRole = ctx.context.orgOptions?.creatorRole || "owner"; + const updatingMemberRoles = member.role.split(","); + const toBeUpdatedMemberRoles = toBeUpdatedMember.role.split(","); + + const isUpdatingCreator = toBeUpdatedMemberRoles.includes(creatorRole); + const updaterIsCreator = updatingMemberRoles.includes(creatorRole); + + const isSettingCreatorRole = roleToSet.includes(creatorRole); + if ( - (toBeUpdatedMemberRoles.includes(creatorRole) && - !updatingMemberRoles.includes(creatorRole)) || - (roleToSet.includes(creatorRole) && - !updatingMemberRoles.includes(creatorRole)) + (isUpdatingCreator && !updaterIsCreator) || + (isSettingCreatorRole && !updaterIsCreator) ) { throw new APIError("FORBIDDEN", { message: @@ -403,6 +407,7 @@ export const updateMemberRole = (option: O) => permissions: { member: ["update"], }, + allowCreatorAllPermissions: true, }); if (!canUpdateMember) {