Skip to content

Commit e680498

Browse files
feat: apply View As to org-level permissions and tabs
When View As is active, also fetch the impersonated user's org membership to get their org role, and use that to compute effective org permissions. Changes: - Added orgRoleToPermissions() and getEffectiveOrgPermissions() to map org roles (admin/editor/viewer) to org permissions - Updated root layout to fetch view-as user's org role and compute effective org permissions - OrganizationTabs now uses effective org permissions (Users/Settings tabs hidden for viewers) - TopNavigationBar receives effective org permissions - BillingBannerManager also receives effective org permissions This ensures that when viewing as an org-level viewer: - Org Users tab is hidden - Org Settings tab is hidden - Project-level admin features are also hidden based on their project role Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
1 parent 458d8e2 commit e680498

2 files changed

Lines changed: 156 additions & 42 deletions

File tree

web-admin/src/features/view-as-user/getViewAsUserPermissions.ts

Lines changed: 118 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { derived } from "svelte/store";
21
import {
3-
createAdminServiceGetProjectMemberUser,
42
type V1ProjectPermissions,
3+
type V1OrganizationPermissions,
54
} from "../../client";
6-
import { viewAsUserStore, viewAsUserStateStore$ } from "./viewAsUserStore";
75

86
/**
97
* Project roles and their corresponding permissions.
108
* Based on: https://docs.rilldata.com/guide/administration/users-and-access/roles-permissions
119
*/
12-
const ROLE_PERMISSIONS: Record<string, Partial<V1ProjectPermissions>> = {
10+
const PROJECT_ROLE_PERMISSIONS: Record<
11+
string,
12+
Partial<V1ProjectPermissions>
13+
> = {
1314
admin: {
1415
readProject: true,
1516
manageProject: true,
@@ -34,9 +35,53 @@ const ROLE_PERMISSIONS: Record<string, Partial<V1ProjectPermissions>> = {
3435
};
3536

3637
/**
37-
* Returns the default (most restrictive) permissions for unknown roles.
38+
* Organization roles and their corresponding permissions.
39+
* Based on: https://docs.rilldata.com/guide/administration/users-and-access/roles-permissions
40+
*/
41+
const ORG_ROLE_PERMISSIONS: Record<
42+
string,
43+
Partial<V1OrganizationPermissions>
44+
> = {
45+
admin: {
46+
admin: true,
47+
readOrg: true,
48+
manageOrg: true,
49+
readProjects: true,
50+
createProjects: true,
51+
manageProjects: true,
52+
readOrgMembers: true,
53+
manageOrgMembers: true,
54+
manageOrgAdmins: true,
55+
},
56+
editor: {
57+
admin: false,
58+
readOrg: true,
59+
manageOrg: false,
60+
readProjects: true,
61+
createProjects: true,
62+
manageProjects: false,
63+
readOrgMembers: true,
64+
manageOrgMembers: false,
65+
manageOrgAdmins: false,
66+
},
67+
viewer: {
68+
admin: false,
69+
guest: false,
70+
readOrg: true,
71+
manageOrg: false,
72+
readProjects: true,
73+
createProjects: false,
74+
manageProjects: false,
75+
readOrgMembers: false,
76+
manageOrgMembers: false,
77+
manageOrgAdmins: false,
78+
},
79+
};
80+
81+
/**
82+
* Returns the default (most restrictive) project permissions for unknown roles.
3883
*/
39-
function getDefaultPermissions(): Partial<V1ProjectPermissions> {
84+
function getDefaultProjectPermissions(): Partial<V1ProjectPermissions> {
4085
return {
4186
readProject: true,
4287
manageProject: false,
@@ -46,47 +91,46 @@ function getDefaultPermissions(): Partial<V1ProjectPermissions> {
4691
};
4792
}
4893

94+
/**
95+
* Returns the default (most restrictive) org permissions for unknown roles.
96+
*/
97+
function getDefaultOrgPermissions(): Partial<V1OrganizationPermissions> {
98+
return {
99+
admin: false,
100+
guest: false,
101+
readOrg: true,
102+
manageOrg: false,
103+
readProjects: true,
104+
createProjects: false,
105+
manageProjects: false,
106+
readOrgMembers: false,
107+
manageOrgMembers: false,
108+
manageOrgAdmins: false,
109+
};
110+
}
111+
49112
/**
50113
* Maps a role name to project permissions.
51114
*/
52115
export function roleToPermissions(
53116
roleName: string | undefined,
54117
): Partial<V1ProjectPermissions> {
55-
if (!roleName) return getDefaultPermissions();
118+
if (!roleName) return getDefaultProjectPermissions();
56119
const normalizedRole = roleName.toLowerCase();
57-
return ROLE_PERMISSIONS[normalizedRole] ?? getDefaultPermissions();
120+
return (
121+
PROJECT_ROLE_PERMISSIONS[normalizedRole] ?? getDefaultProjectPermissions()
122+
);
58123
}
59124

60125
/**
61-
* Creates a query to fetch the impersonated user's project membership.
62-
* Returns their role name which can be mapped to permissions.
126+
* Maps an org role name to organization permissions.
63127
*/
64-
export function useViewAsUserRole(organization: string, project: string) {
65-
return derived(viewAsUserStore, ($viewAsUser, set) => {
66-
if (!$viewAsUser?.email || !organization || !project) {
67-
set(null);
68-
return;
69-
}
70-
71-
// Create the query - this will be reactive to viewAsUser changes
72-
const query = createAdminServiceGetProjectMemberUser(
73-
organization,
74-
project,
75-
$viewAsUser.email,
76-
{
77-
query: {
78-
enabled: !!$viewAsUser?.email && !!organization && !!project,
79-
},
80-
},
81-
);
82-
83-
// Subscribe to the query and update our derived store
84-
const unsubscribe = query.subscribe((result) => {
85-
set(result.data?.member?.roleName ?? null);
86-
});
87-
88-
return unsubscribe;
89-
});
128+
export function orgRoleToPermissions(
129+
roleName: string | undefined,
130+
): Partial<V1OrganizationPermissions> {
131+
if (!roleName) return getDefaultOrgPermissions();
132+
const normalizedRole = roleName.toLowerCase();
133+
return ORG_ROLE_PERMISSIONS[normalizedRole] ?? getDefaultOrgPermissions();
90134
}
91135

92136
/**
@@ -122,3 +166,41 @@ export function getEffectivePermissions(
122166
viewAsPermissions.manageProjectAdmins,
123167
};
124168
}
169+
170+
/**
171+
* Computes the effective organization permissions when View As is active.
172+
* Returns the impersonated user's permissions based on their org role,
173+
* or the actual permissions if View As is not active.
174+
*/
175+
export function getEffectiveOrgPermissions(
176+
actualPermissions: V1OrganizationPermissions,
177+
viewAsOrgRoleName: string | null | undefined,
178+
isViewAsActive: boolean,
179+
): V1OrganizationPermissions {
180+
if (!isViewAsActive || !viewAsOrgRoleName) {
181+
return actualPermissions;
182+
}
183+
184+
const viewAsPermissions = orgRoleToPermissions(viewAsOrgRoleName);
185+
186+
// Return permissions that are the intersection of actual and view-as permissions
187+
// This ensures we never grant more permissions than the actual user has
188+
return {
189+
admin: actualPermissions.admin && viewAsPermissions.admin,
190+
guest: actualPermissions.guest && viewAsPermissions.guest,
191+
readOrg: actualPermissions.readOrg && viewAsPermissions.readOrg,
192+
manageOrg: actualPermissions.manageOrg && viewAsPermissions.manageOrg,
193+
readProjects:
194+
actualPermissions.readProjects && viewAsPermissions.readProjects,
195+
createProjects:
196+
actualPermissions.createProjects && viewAsPermissions.createProjects,
197+
manageProjects:
198+
actualPermissions.manageProjects && viewAsPermissions.manageProjects,
199+
readOrgMembers:
200+
actualPermissions.readOrgMembers && viewAsPermissions.readOrgMembers,
201+
manageOrgMembers:
202+
actualPermissions.manageOrgMembers && viewAsPermissions.manageOrgMembers,
203+
manageOrgAdmins:
204+
actualPermissions.manageOrgAdmins && viewAsPermissions.manageOrgAdmins,
205+
};
206+
}

web-admin/src/routes/+layout.svelte

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@
3030
import "@rilldata/web-common/app.css";
3131
import { themeControl } from "@rilldata/web-common/features/themes/theme-control";
3232
import { getThemedLogoUrl } from "@rilldata/web-admin/features/themes/organization-logo";
33-
import type { V1Organization } from "@rilldata/web-admin/client";
33+
import {
34+
type V1Organization,
35+
createAdminServiceGetOrganizationMemberUser,
36+
} from "@rilldata/web-admin/client";
37+
import { viewAsUserStore } from "@rilldata/web-admin/features/view-as-user/viewAsUserStore";
38+
import { getEffectiveOrgPermissions } from "@rilldata/web-admin/features/view-as-user/getViewAsUserPermissions";
3439
3540
export let data;
3641
@@ -54,6 +59,26 @@
5459
5560
$: organization = organizationName;
5661
62+
// Fetch the impersonated user's org membership to get their role
63+
$: viewAsUserEmail = $viewAsUserStore?.email;
64+
$: viewAsOrgMemberQuery = createAdminServiceGetOrganizationMemberUser(
65+
organization ?? "",
66+
viewAsUserEmail ?? "",
67+
{
68+
query: {
69+
enabled: !!viewAsUserEmail && !!organization,
70+
},
71+
},
72+
);
73+
$: viewAsOrgRole = $viewAsOrgMemberQuery.data?.member?.roleName;
74+
75+
// Compute effective org permissions based on the impersonated user's role
76+
$: effectiveOrgPermissions = getEffectiveOrgPermissions(
77+
organizationPermissions ?? {},
78+
viewAsOrgRole,
79+
!!$viewAsUserStore,
80+
);
81+
5782
// Remember:
5883
// - https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose#a-bad-api
5984
// - https://tkdodo.eu/blog/react-query-error-handling#the-global-callbacks
@@ -146,22 +171,29 @@
146171
>
147172
<BannerCenter />
148173
{#if !hideBillingManager}
149-
<BillingBannerManager {organization} {organizationPermissions} />
174+
<BillingBannerManager
175+
{organization}
176+
organizationPermissions={effectiveOrgPermissions}
177+
/>
150178
{/if}
151179
{#if !isEmbed && !hideTopBar}
152180
<TopNavigationBar
153181
createMagicAuthTokens={projectPermissions?.createMagicAuthTokens}
154182
manageProjectMembers={projectPermissions?.manageProjectMembers}
155183
manageProjectAdmins={projectPermissions?.manageProjectAdmins}
156-
manageOrgAdmins={organizationPermissions?.manageOrgAdmins}
157-
manageOrgMembers={organizationPermissions?.manageOrgMembers}
158-
readProjects={organizationPermissions?.readProjects}
184+
manageOrgAdmins={effectiveOrgPermissions?.manageOrgAdmins}
185+
manageOrgMembers={effectiveOrgPermissions?.manageOrgMembers}
186+
readProjects={effectiveOrgPermissions?.readProjects}
159187
{planDisplayName}
160188
{organizationLogoUrl}
161189
/>
162190

163191
{#if withinOnlyOrg}
164-
<OrganizationTabs {organization} {organizationPermissions} {pathname} />
192+
<OrganizationTabs
193+
{organization}
194+
organizationPermissions={effectiveOrgPermissions}
195+
{pathname}
196+
/>
165197
{/if}
166198
{/if}
167199
<ErrorBoundary>

0 commit comments

Comments
 (0)