Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: use server-side permissions for View As UI gating
Use the existing GetProjectWithBearerToken pattern to fetch the
impersonated user's projectPermissions from the server, rather than
reconstructing permissions client-side.

Changes:
- Project layout calls GetProjectWithBearerToken with the mocked user's
  JWT (from GetDeploymentCredentials) to get their actual permissions
- Created effectivePermissionsStore to share permissions with TopNavBar
- ProjectTabs uses effectiveProjectPermissions (impersonated user's
  permissions when View As is active)
- TopNavigationBar uses effective permissions for Share button visibility

This follows the existing architecture where the server is the single
source of truth for permissions, rather than duplicating permission
logic on the client.

Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
  • Loading branch information
cursoragent and ericokuma committed Feb 18, 2026
commit b043ed8806078ef501f35abe6fd3d2ea82a735f2
16 changes: 14 additions & 2 deletions web-admin/src/features/navigation/TopNavigationBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
} from "../../client";
import ViewAsUserChip from "../../features/view-as-user/ViewAsUserChip.svelte";
import { viewAsUserStore } from "../../features/view-as-user/viewAsUserStore";
import { effectiveProjectPermissionsStore } from "../../features/view-as-user/effectivePermissionsStore";
import CreateAlert from "../alerts/CreateAlert.svelte";
import { useAlerts } from "../alerts/selectors";
import AvatarButton from "../authentication/AvatarButton.svelte";
Expand Down Expand Up @@ -69,6 +70,15 @@
$: onPublicURLPage = isPublicURLPage($page);
$: onOrgPage = isOrganizationPage($page);

// Use effective permissions when "View As" is active (from server)
// Otherwise fall back to the props passed from the root layout
$: effectiveManageProjectMembers =
$effectiveProjectPermissionsStore?.manageProjectMembers ??
manageProjectMembers;
$: effectiveCreateMagicAuthTokens =
$effectiveProjectPermissionsStore?.createMagicAuthTokens ??
createMagicAuthTokens;

$: loggedIn = !!$user.data?.user;
$: rillLogoHref = !loggedIn ? "https://www.rilldata.com" : "/";
$: logoUrl = organizationLogoUrl;
Expand Down Expand Up @@ -231,7 +241,7 @@
{/if}
<!-- NOTE: only project admin and editor can manage project members -->
<!-- https://docs.rilldata.com/guide/administration/users-and-access/roles-permissions -->
{#if onProjectPage && manageProjectMembers}
{#if onProjectPage && effectiveManageProjectMembers}
<ShareProjectPopover
{organization}
{project}
Expand Down Expand Up @@ -264,7 +274,9 @@
{#if $alertsFlag}
<CreateAlert />
{/if}
<ShareDashboardPopover {createMagicAuthTokens} />
<ShareDashboardPopover
createMagicAuthTokens={effectiveCreateMagicAuthTokens}
/>
{/if}
</StateManagersProvider>
{/key}
Expand Down
10 changes: 10 additions & 0 deletions web-admin/src/features/view-as-user/effectivePermissionsStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { writable } from "svelte/store";
import type { V1ProjectPermissions } from "../../client";

/**
* Store for effective project permissions when "View As" is active.
* When null, the actual user's permissions should be used.
* When set, these are the impersonated user's permissions (from server).
*/
export const effectiveProjectPermissionsStore =
writable<V1ProjectPermissions | null>(null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This store can be eliminated — see the top-level comment for the TanStack Query dedup approach. When TopNavigationBar creates its own query observers (matching the same query keys the project layout uses), TanStack gives it instant cache hits. No side-channel store needed. This also follows the existing ProjectAccessControls pattern, which independently queries GetProject.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This store can be eliminated — see the top-level comment for the TanStack Query dedup approach. When TopNavigationBar creates its own query observers (matching the same query keys the project layout uses), TanStack gives it instant cache hits. No side-channel store needed. This also follows the existing ProjectAccessControls pattern, which independently queries GetProject.

37 changes: 36 additions & 1 deletion web-admin/src/routes/[organization]/[project]/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import { createAdminServiceGetProjectWithBearerToken } from "@rilldata/web-admin/features/public-urls/get-project-with-bearer-token";
import { cloudVersion } from "@rilldata/web-admin/features/telemetry/initCloudMetrics";
import { viewAsUserStore } from "@rilldata/web-admin/features/view-as-user/viewAsUserStore";
import { effectiveProjectPermissionsStore } from "@rilldata/web-admin/features/view-as-user/effectivePermissionsStore";
import ErrorPage from "@rilldata/web-common/components/ErrorPage.svelte";
import { metricsService } from "@rilldata/web-common/metrics/initMetrics";
import RuntimeProvider from "@rilldata/web-common/runtime-client/RuntimeProvider.svelte";
Expand Down Expand Up @@ -122,7 +123,41 @@
$: ({ data: mockedUserDeploymentCredentials } =
$mockedUserDeploymentCredentialsQuery);

/**
* When "View As" is active, fetch the project using the mocked user's JWT.
* This returns the impersonated user's `projectPermissions` from the server.
*/
$: mockedUserProjectQuery = createAdminServiceGetProjectWithBearerToken(
organization,
project,
mockedUserDeploymentCredentials?.accessToken ?? "",
undefined,
{
query: {
enabled: !!mockedUserDeploymentCredentials?.accessToken,
},
},
);

$: ({ data: projectData, error: projectError } = $projectQuery);

/**
* Compute effective project permissions.
* When "View As" is active, use the impersonated user's permissions (from server).
* Otherwise, use the actual user's permissions.
*/
$: effectiveProjectPermissions =
mockedUserId && $mockedUserProjectQuery.data?.projectPermissions
? $mockedUserProjectQuery.data.projectPermissions
: projectData?.projectPermissions;

// Update the global store so TopNavigationBar can access effective permissions
$: effectiveProjectPermissionsStore.set(
mockedUserId && $mockedUserProjectQuery.data?.projectPermissions
? $mockedUserProjectQuery.data.projectPermissions
: null,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition mockedUserId && $mockedUserProjectQuery.data?.projectPermissions is duplicated between the local variable above and this store update. If you keep this pattern, compute once and reuse. But see the top-level comment for an approach that eliminates these lines entirely.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition mockedUserId && $mockedUserProjectQuery.data?.projectPermissions is duplicated between the local variable above and this store update. If you keep this pattern, compute once and reuse. But see the top-level comment for an approach that eliminates these lines entirely.


$: deploymentStatus = projectData?.deployment?.status;
// A re-deploy triggers `DEPLOYMENT_STATUS_UPDATING` status. But we can still show the project UI.
$: isProjectAvailable =
Expand Down Expand Up @@ -168,7 +203,7 @@

{#if onProjectPage && deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING}
<ProjectTabs
projectPermissions={projectData.projectPermissions}
projectPermissions={effectiveProjectPermissions}
{organization}
{pathname}
{project}
Expand Down
Loading