diff --git a/docs/content/docs/plugins/admin.mdx b/docs/content/docs/plugins/admin.mdx index 5be0392810..ce7830f8bf 100644 --- a/docs/content/docs/plugins/admin.mdx +++ b/docs/content/docs/plugins/admin.mdx @@ -437,6 +437,19 @@ type impersonateUser = { ``` +By default, admins cannot impersonate other admin users. To allow this, grant the `impersonate-admins` permission to a role: + +```ts title="auth.ts" +const superAdmin = ac.newRole({ + ...adminAc.statements, + user: ["impersonate-admins", ...adminAc.statements.user], +}); +``` + + + The legacy `allowImpersonatingAdmins` option is still supported, but is deprecated and will be removed in a future version. + + ### Stop Impersonating User To stop impersonating a user and continue with the admin account, you can use `stopImpersonating` @@ -489,7 +502,7 @@ By default, there are two roles: By default, there are two resources with up to six permissions. **user**: - `create` `list` `set-role` `ban` `impersonate` `delete` `set-password` + `create` `list` `set-role` `ban` `impersonate` `impersonate-admins` `delete` `set-password` **session**: `list` `revoke` `delete` @@ -865,12 +878,3 @@ admin({ }); ``` -### allowImpersonatingAdmins - -Whether to allow impersonating other admin users. Defaults to `false`. - -```ts title="auth.ts" -admin({ - allowImpersonatingAdmins: true, -}); -``` diff --git a/packages/better-auth/src/plugins/admin/access/statement.ts b/packages/better-auth/src/plugins/admin/access/statement.ts index 0236ce32e4..680b44870e 100644 --- a/packages/better-auth/src/plugins/admin/access/statement.ts +++ b/packages/better-auth/src/plugins/admin/access/statement.ts @@ -7,6 +7,7 @@ export const defaultStatements = { "set-role", "ban", "impersonate", + "impersonate-admins", "delete", "set-password", "get", diff --git a/packages/better-auth/src/plugins/admin/admin.test.ts b/packages/better-auth/src/plugins/admin/admin.test.ts index e20c39abd2..0761106cd3 100644 --- a/packages/better-auth/src/plugins/admin/admin.test.ts +++ b/packages/better-auth/src/plugins/admin/admin.test.ts @@ -738,7 +738,7 @@ describe("Admin plugin", async () => { expect(res.error?.status).toBe(403); }); - it("should not allow to impersonate admins", async () => { + it("should not allow to impersonate admins without impersonate-admins permission", async () => { const userToImpersonate = await client.signUp.email({ email: "impersonate-admin@mail.com", password: "password", @@ -762,6 +762,190 @@ describe("Admin plugin", async () => { expect(res.error?.status).toBe(403); }); + it("should allow impersonating admins with impersonate-admins permission", async () => { + const ac = createAccessControl({ + user: [ + "create", + "list", + "set-role", + "ban", + "impersonate", + "impersonate-admins", + "delete", + "set-password", + "get", + "update", + ], + session: ["list", "revoke", "delete"], + } as const); + + const superAdminRole = ac.newRole({ + user: [ + "create", + "list", + "set-role", + "ban", + "impersonate", + "impersonate-admins", + "delete", + "set-password", + "get", + "update", + ], + session: ["list", "revoke", "delete"], + }); + const regularAdminRole = ac.newRole({ + user: [ + "create", + "list", + "set-role", + "ban", + "impersonate", + "delete", + "set-password", + "get", + "update", + ], + session: ["list", "revoke", "delete"], + }); + const userRole = ac.newRole({ + user: [], + session: [], + }); + + const { + signInWithTestUser: signInSuperAdmin, + signInWithUser: signInAsUser, + customFetchImpl: fetchImpl, + } = await getTestInstance( + { + plugins: [ + admin({ + ac, + roles: { + "super-admin": superAdminRole, + admin: regularAdminRole, + user: userRole, + }, + }), + ], + databaseHooks: { + user: { + create: { + before: async (user) => { + if (user.name === "Super Admin") { + return { data: { ...user, role: "super-admin" } }; + } + }, + }, + }, + }, + }, + { testUser: { name: "Super Admin" } }, + ); + const c = createAuthClient({ + fetchOptions: { customFetchImpl: fetchImpl }, + plugins: [ + adminClient({ + ac, + roles: { + "super-admin": superAdminRole, + admin: regularAdminRole, + user: userRole, + }, + }), + ], + baseURL: "http://localhost:3000", + }); + const { headers: superAdminHeaders } = await signInSuperAdmin(); + + const { data: targetAdmin } = await c.signUp.email({ + email: "target-admin-perm@test.com", + password: "password", + name: "Target Admin Perm", + }); + await c.admin.setRole( + { userId: targetAdmin!.user.id, role: "admin" }, + { headers: superAdminHeaders }, + ); + + // super-admin has impersonate-admins permission, should succeed + const res = await c.admin.impersonateUser( + { userId: targetAdmin!.user.id }, + { headers: superAdminHeaders }, + ); + expect(res.data?.session).toBeDefined(); + expect(res.data?.user?.id).toBe(targetAdmin!.user.id); + + // regular admin does NOT have impersonate-admins permission, should fail + const { data: regularAdmin } = await c.signUp.email({ + email: "regular-admin-perm@test.com", + password: "password", + name: "Regular Admin Perm", + }); + await c.admin.setRole( + { userId: regularAdmin!.user.id, role: "admin" }, + { headers: superAdminHeaders }, + ); + const { headers: regularAdminHeaders } = await signInAsUser( + "regular-admin-perm@test.com", + "password", + ); + const res2 = await c.admin.impersonateUser( + { userId: targetAdmin!.user.id }, + { headers: regularAdminHeaders }, + ); + expect(res2.error?.status).toBe(403); + }); + + it("should allow impersonating admins with legacy allowImpersonatingAdmins option", async () => { + const { signInWithTestUser: signInAdmin, customFetchImpl: fetchImpl } = + await getTestInstance( + { + plugins: [ + admin({ + allowImpersonatingAdmins: true, + }), + ], + databaseHooks: { + user: { + create: { + before: async (user) => { + if (user.name === "Admin") { + return { data: { ...user, role: "admin" } }; + } + }, + }, + }, + }, + }, + { testUser: { name: "Admin" } }, + ); + const c = createAuthClient({ + fetchOptions: { customFetchImpl: fetchImpl }, + plugins: [adminClient()], + baseURL: "http://localhost:3000", + }); + const { headers: aHeaders } = await signInAdmin(); + + const { data: targetAdmin } = await c.signUp.email({ + email: "target-admin-legacy@test.com", + password: "password", + name: "Target Admin Legacy", + }); + await c.admin.setRole( + { userId: targetAdmin!.user.id, role: "admin" }, + { headers: aHeaders }, + ); + + const res = await c.admin.impersonateUser( + { userId: targetAdmin!.user.id }, + { headers: aHeaders }, + ); + expect(res.data?.session).toBeDefined(); + expect(res.data?.user?.id).toBe(targetAdmin!.user.id); + }); + it("should filter impersonated sessions", async () => { const { headers } = await signInWithUser(data.email, data.password); const res = await client.listSessions({ diff --git a/packages/better-auth/src/plugins/admin/routes.ts b/packages/better-auth/src/plugins/admin/routes.ts index 6e079acee6..72b4742428 100644 --- a/packages/better-auth/src/plugins/admin/routes.ts +++ b/packages/better-auth/src/plugins/admin/routes.ts @@ -1079,15 +1079,26 @@ export const impersonateUser = (opts: AdminOptions) => opts.defaultRole || "user" ).split(","); - if ( - opts.allowImpersonatingAdmins !== true && - (targetUserRole.some((role) => adminRoles.includes(role)) || - opts.adminUserIds?.includes(targetUser.id)) - ) { - throw APIError.from( - "FORBIDDEN", - ADMIN_ERROR_CODES.YOU_CANNOT_IMPERSONATE_ADMINS, - ); + const isTargetAdmin = + targetUserRole.some((role) => adminRoles.includes(role)) || + !!opts.adminUserIds?.includes(targetUser.id); + if (isTargetAdmin) { + const canImpersonateAdmins = + opts.allowImpersonatingAdmins === true || + hasPermission({ + userId: ctx.context.session.user.id, + role: ctx.context.session.user.role, + options: opts, + permissions: { + user: ["impersonate-admins"], + }, + }); + if (!canImpersonateAdmins) { + throw APIError.from( + "FORBIDDEN", + ADMIN_ERROR_CODES.YOU_CANNOT_IMPERSONATE_ADMINS, + ); + } } const session = await ctx.context.internalAdapter.createSession( diff --git a/packages/better-auth/src/plugins/admin/types.ts b/packages/better-auth/src/plugins/admin/types.ts index 70931e16ea..650e45e36b 100644 --- a/packages/better-auth/src/plugins/admin/types.ts +++ b/packages/better-auth/src/plugins/admin/types.ts @@ -77,7 +77,9 @@ export interface AdminOptions { */ bannedUserMessage?: string | undefined; /** - * Whether to allow impersonating other admins + * Whether to allow impersonating other admins. + * + * @deprecated Use the `impersonate-admins` permission instead. * * @default false */