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:
sean-brydonandHariom authored and GitHub committed 2024-04-22 13:45:28 +00:00
1 parent ad3ee14501
commit 3ce6c23603
8 files changed
+228 -12

No files matched your search

+5 -1
View File
@@ -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
? {
+1 -1
View File
@@ -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,
+4 -2
View File
@@ -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}>