fix: Update visibility of team members based on user role (#14629)
* fix: team memebrs being hidden for admin when org is private and they are not org admin * chore: remove console.loog * chore: make permissionCheck more readable * test: push e2e test for normal privacy banner in orgs * test: add a few more edge cases * chore: negate names * feat: update func to make more sense * Fix --------- Co-authored-by: Hariom <hariombalhara@gmail.com>
This commit is contained in:
1 parent
ad3ee14501
commit
3ce6c23603
8 files changed
+228
-12
No files matched your search
@@ -10,11 +10,12 @@ const getRandomSlug = () => `org-${Math.random().toString(36).substring(7)}`;
|
||||
export const createOrgsFixture = (page: Page) => {
|
||||
const store = { orgs: [], page } as { orgs: Team[]; page: typeof page };
|
||||
return {
|
||||
create: async (opts: { name: string; slug?: string; requestedSlug?: string }) => {
|
||||
create: async (opts: { name: string; slug?: string; requestedSlug?: string; isPrivate?: boolean }) => {
|
||||
const org = await createOrgInDb({
|
||||
name: opts.name,
|
||||
slug: opts.slug || getRandomSlug(),
|
||||
requestedSlug: opts.requestedSlug,
|
||||
isPrivate: opts.isPrivate,
|
||||
});
|
||||
const orgWithMetadata = {
|
||||
...org,
|
||||
@@ -39,16 +40,19 @@ export async function createOrgInDb({
|
||||
name,
|
||||
slug,
|
||||
requestedSlug,
|
||||
isPrivate,
|
||||
}: {
|
||||
name: string;
|
||||
slug: string | null;
|
||||
requestedSlug?: string;
|
||||
isPrivate?: boolean;
|
||||
}) {
|
||||
return await prisma.team.create({
|
||||
data: {
|
||||
name: name,
|
||||
slug: slug,
|
||||
isOrganization: true,
|
||||
isPrivate: isPrivate,
|
||||
metadata: {
|
||||
...(requestedSlug
|
||||
? {
|
||||
|
||||
@@ -788,7 +788,7 @@ const createUser = (
|
||||
},
|
||||
},
|
||||
accepted: true,
|
||||
role: MembershipRole.ADMIN,
|
||||
role,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(async ({ users, orgs }) => {
|
||||
await users.deleteAll();
|
||||
await orgs.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Organization - Privacy", () => {
|
||||
test(`Private Org \n
|
||||
1) Org Member cannot see members of orgs\n
|
||||
2) Org Owner/Admin can see members`, async ({ page, users, orgs }) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
isPrivate: true,
|
||||
});
|
||||
const teamMatesObj = [
|
||||
{ name: "teammate-1" },
|
||||
{ name: "teammate-2" },
|
||||
{ name: "teammate-3" },
|
||||
{ name: "teammate-4" },
|
||||
];
|
||||
|
||||
const owner = await users.create(
|
||||
{
|
||||
username: "pro-user",
|
||||
name: "pro-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: "OWNER",
|
||||
},
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
}
|
||||
);
|
||||
const memberInOrg = await users.create({
|
||||
username: "org-member-user",
|
||||
name: "org-member-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: "MEMBER",
|
||||
});
|
||||
|
||||
await owner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
const tableLocator = await page.getByTestId("user-list-data-table");
|
||||
|
||||
await expect(tableLocator).toBeVisible();
|
||||
|
||||
await memberInOrg.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
const userDataTable = await page.getByTestId("user-list-data-table");
|
||||
const membersPrivacyWarning = await page.getByTestId("members-privacy-warning");
|
||||
await expect(userDataTable).toBeHidden();
|
||||
await expect(membersPrivacyWarning).toBeVisible();
|
||||
});
|
||||
test(`Private Org - Private Team\n
|
||||
1) Team Member cannot see members in team\n
|
||||
2) Team Admin/Owner can see members in team`, async ({ page, users, orgs }) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
isPrivate: true,
|
||||
});
|
||||
const teamMatesObj = [
|
||||
{ name: "teammate-1" },
|
||||
{ name: "teammate-2" },
|
||||
{ name: "teammate-3" },
|
||||
{ name: "teammate-4" },
|
||||
];
|
||||
|
||||
const owner = await users.create(
|
||||
{
|
||||
username: "pro-user",
|
||||
name: "pro-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: "OWNER",
|
||||
},
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
}
|
||||
);
|
||||
|
||||
await owner.apiLogin();
|
||||
const membership = await owner.getFirstTeamMembership();
|
||||
const teamId = membership.team.id;
|
||||
|
||||
// Update team to be private
|
||||
await page.goto(`/settings/teams/${teamId}/members`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
const togglePrivateSwitch = await page.getByTestId("make-team-private-check");
|
||||
await togglePrivateSwitch.click();
|
||||
|
||||
// As admin/owner we can see the user list
|
||||
const tableLocator = await page.getByTestId("team-member-list-container");
|
||||
await expect(tableLocator).toBeVisible();
|
||||
|
||||
const memberUser = await prisma.membership.findFirst({
|
||||
where: {
|
||||
teamId,
|
||||
role: "MEMBER",
|
||||
},
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(memberUser?.user.email).toBeDefined();
|
||||
// @ts-expect-error expect doesnt assert on a type level
|
||||
const memberOfTeam = await users.set(memberUser?.user.email);
|
||||
await memberOfTeam.apiLogin();
|
||||
|
||||
await page.goto(`/settings/teams/${teamId}/members`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
// As a user we can not see the user list when a team is private
|
||||
const hiddenTableLocator = await page.getByTestId("team-member-list-container");
|
||||
await expect(hiddenTableLocator).toBeHidden();
|
||||
});
|
||||
test(`Private Org - Public Team\n
|
||||
1) All team members can see members in team \n
|
||||
2) Team Admin/Owner can see members in team`, async ({ page, users, orgs }) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
const teamMatesObj = [
|
||||
{ name: "teammate-1" },
|
||||
{ name: "teammate-2" },
|
||||
{ name: "teammate-3" },
|
||||
{ name: "teammate-4" },
|
||||
];
|
||||
|
||||
const owner = await users.create(
|
||||
{
|
||||
username: "pro-user",
|
||||
name: "pro-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: "OWNER",
|
||||
},
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
}
|
||||
);
|
||||
|
||||
await owner.apiLogin();
|
||||
const membership = await owner.getFirstTeamMembership();
|
||||
const teamId = membership.team.id;
|
||||
|
||||
// Update team to be private
|
||||
await page.goto(`/settings/teams/${teamId}/members`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
// As admin/owner we can see the user list
|
||||
const tableLocator = await page.getByTestId("team-member-list-container");
|
||||
await expect(tableLocator).toBeVisible();
|
||||
|
||||
const memberUser = await prisma.membership.findFirst({
|
||||
where: {
|
||||
teamId,
|
||||
role: "MEMBER",
|
||||
},
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(memberUser?.user.email).toBeDefined();
|
||||
// @ts-expect-error expect doesnt assert on a type level
|
||||
const memberOfTeam = await users.set(memberUser?.user.email);
|
||||
await memberOfTeam.apiLogin();
|
||||
|
||||
await page.goto(`/settings/teams/${teamId}/members`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
// As a user we can not see the user list when a team is private
|
||||
const hiddenTableLocator = await page.getByTestId("team-member-list-container");
|
||||
await expect(hiddenTableLocator).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,7 @@ const MembersView = () => {
|
||||
<Meta title={t("organization_members")} description={t("organization_description")} />
|
||||
<div>{!isPending && canLoggedInUserSeeMembers && <UserListTable />}</div>
|
||||
{!canLoggedInUserSeeMembers && (
|
||||
<div className="border-subtle rounded-xl border p-6">
|
||||
<div className="border-subtle rounded-xl border p-6" data-testId="members-privacy-warning">
|
||||
<h2 className="text-default">{t("only_admin_can_see_members_of_org")}</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -57,7 +57,9 @@ function MembersList(props: MembersListProps) {
|
||||
placeholder={`${t("search")}...`}
|
||||
/>
|
||||
{membersList?.length && team ? (
|
||||
<ul className="divide-subtle border-subtle divide-y rounded-md border ">
|
||||
<ul
|
||||
className="divide-subtle border-subtle divide-y rounded-md border "
|
||||
data-testId="team-member-list-container">
|
||||
{membersList.map((member) => {
|
||||
return (
|
||||
<MemberListItem
|
||||
|
||||
@@ -318,6 +318,7 @@ export function UserListTable() {
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
data-testId="user-list-data-table"
|
||||
onSearch={(value) => setDebouncedSearchTerm(value)}
|
||||
selectionOptions={[
|
||||
{
|
||||
|
||||
@@ -36,15 +36,27 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
// Hide Members of team when 1) Org is private and logged in user is not admin or owner
|
||||
// OR
|
||||
// 2)Team is private and logged in user is not admin or owner of team or Organization's admin or owner
|
||||
const hideMembers =
|
||||
(ctx.user.profile?.organization?.isPrivate && !ctx.user.organization?.isOrgAdmin) ||
|
||||
(team.isPrivate &&
|
||||
!(membership.role === MembershipRole.OWNER || membership.role === MembershipRole.ADMIN) &&
|
||||
!ctx.user.organization?.isOrgAdmin);
|
||||
function shouldHideMembers() {
|
||||
const isOrgPrivate = ctx.user.profile?.organization?.isPrivate;
|
||||
const isOrgAdminOrOwner = ctx.user.organization?.isOrgAdmin;
|
||||
const isTargetingOrg = input.teamId === ctx.user.organizationId;
|
||||
|
||||
if (isTargetingOrg) {
|
||||
return isOrgPrivate && !isOrgAdminOrOwner;
|
||||
}
|
||||
|
||||
const isTeamAdminOrOwner =
|
||||
membership?.role === MembershipRole.OWNER || membership?.role === MembershipRole.ADMIN;
|
||||
|
||||
if (team?.isPrivate && !isTeamAdminOrOwner && !isOrgAdminOrOwner) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
...restTeam,
|
||||
members: hideMembers ? [] : members,
|
||||
members: shouldHideMembers() ? [] : members,
|
||||
safeBio: markdownToSafeHTML(team.bio),
|
||||
membership: {
|
||||
role: membership.role,
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface DataTableProps<TData, TValue> {
|
||||
CTA?: React.ReactNode;
|
||||
tableOverlay?: React.ReactNode;
|
||||
variant?: "default" | "compact";
|
||||
"data-testId"?: string;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
@@ -57,6 +58,7 @@ export function DataTable<TData, TValue>({
|
||||
onSearch,
|
||||
onRowMouseclick,
|
||||
onScroll,
|
||||
...rest
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
@@ -107,8 +109,8 @@ export function DataTable<TData, TValue>({
|
||||
onSearch={onSearch}
|
||||
tableCTA={tableCTA}
|
||||
/>
|
||||
<div ref={tableContainerRef} onScroll={onScroll}>
|
||||
<Table>
|
||||
<div ref={tableContainerRef} onScroll={onScroll} data-testId={rest["data-testId"] ?? "data-table"}>
|
||||
<Table data-testId="">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
|
||||
Reference in new issue
Block a user