mirror of
https://github.com/better-auth/better-auth.git
synced 2026-08-01 10:50:40 -05:00
fix(organization): allow org owner to update their own roles (#3426)
* fix: allow org owner to update their own roles * fix(org): move the `is creator` check to the `hasPermissions` function to make it more reusable
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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({})],
|
||||
|
||||
@@ -381,15 +381,19 @@ export const updateMemberRole = <O extends OrganizationOptions>(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 = <O extends OrganizationOptions>(option: O) =>
|
||||
permissions: {
|
||||
member: ["update"],
|
||||
},
|
||||
allowCreatorAllPermissions: true,
|
||||
});
|
||||
|
||||
if (!canUpdateMember) {
|
||||
|
||||
Reference in New Issue
Block a user