diff --git a/coderd/coderd.go b/coderd/coderd.go index 282c13c2e9..0e7b113650 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -662,6 +662,7 @@ func New(options *Options) *API { api.SiteHandler, err = site.New(&site.Options{ CacheDir: siteCacheDir, Database: options.Database, + Authorizer: options.Authorizer, SiteFS: site.FS(), OAuth2Configs: oauthConfigs, DocsURL: options.DeploymentValues.DocsURL.String(), diff --git a/site/index.html b/site/index.html index 8666fab5fa..2473634d76 100644 --- a/site/index.html +++ b/site/index.html @@ -29,6 +29,8 @@ + + [AUTHORIZATION_KEY, req] as const; -export const checkAuthorization = ( +export function checkAuthorization( req: AuthorizationRequest, -) => { - return { + metadata?: MetadataState, +) { + const base = { queryKey: getAuthorizationKey(req), queryFn: () => API.checkAuthorization(req), }; -}; + + if (metadata?.available) { + return { + ...base, + initialData: metadata.value as TResponse, + ...disabledRefetchOptions, + }; + } + return base; +} diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index f727e4387f..03e0d1e94a 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -6,11 +6,13 @@ import { import type { CreateOrganizationRequest, GroupSyncSettings, + Organization, PaginatedMembersRequest, PaginatedMembersResponse, RoleSyncSettings, UpdateOrganizationRequest, } from "api/typesGenerated"; +import type { MetadataState } from "hooks/useEmbeddedMetadata"; import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery"; import { type OrganizationPermissionName, @@ -24,6 +26,7 @@ import { } from "modules/permissions/workspaces"; import type { QueryClient, UseQueryOptions } from "react-query"; import { meKey } from "./users"; +import { cachedQuery } from "./util"; export const createOrganization = (queryClient: QueryClient) => { return { @@ -160,11 +163,14 @@ export const updateOrganizationMemberRoles = ( export const organizationsKey = ["organizations"] as const; -export const organizations = () => { - return { +const notAvailable = { available: false, value: undefined } as const; + +export const organizations = (metadata?: MetadataState) => { + return cachedQuery({ + metadata: metadata ?? notAvailable, queryKey: organizationsKey, queryFn: () => API.getOrganizations(), - }; + }); }; export const getProvisionerDaemonsKey = ( diff --git a/site/src/contexts/auth/AuthProvider.tsx b/site/src/contexts/auth/AuthProvider.tsx index 718a8a356d..83cec2c1f9 100644 --- a/site/src/contexts/auth/AuthProvider.tsx +++ b/site/src/contexts/auth/AuthProvider.tsx @@ -50,7 +50,10 @@ export const AuthProvider: FC = ({ children }) => { const hasFirstUserQuery = useQuery(hasFirstUser(userMetadataState)); const permissionsQuery = useQuery({ - ...checkAuthorization({ checks: permissionChecks }), + ...checkAuthorization( + { checks: permissionChecks }, + metadata.permissions, + ), enabled: userQuery.data !== undefined, }); diff --git a/site/src/hooks/useEmbeddedMetadata.test.ts b/site/src/hooks/useEmbeddedMetadata.test.ts index 9b501ab954..eb1485ba30 100644 --- a/site/src/hooks/useEmbeddedMetadata.test.ts +++ b/site/src/hooks/useEmbeddedMetadata.test.ts @@ -4,6 +4,8 @@ import { MockBuildInfo, MockEntitlements, MockExperiments, + MockOrganization, + MockPermissions, MockTasksTabVisible, MockUserAppearanceSettings, MockUserOwner, @@ -45,6 +47,8 @@ const mockDataForTags = { regions: MockRegions, "tasks-tab-visible": MockTasksTabVisible, "agents-tab-visible": MockAgentsTabVisible, + permissions: MockPermissions, + organizations: [MockOrganization], } as const satisfies Record; const emptyMetadata: RuntimeHtmlMetadata = { @@ -84,6 +88,14 @@ const emptyMetadata: RuntimeHtmlMetadata = { available: false, value: undefined, }, + permissions: { + available: false, + value: undefined, + }, + organizations: { + available: false, + value: undefined, + }, }; const populatedMetadata: RuntimeHtmlMetadata = { @@ -123,6 +135,14 @@ const populatedMetadata: RuntimeHtmlMetadata = { available: true, value: MockAgentsTabVisible, }, + permissions: { + available: true, + value: MockPermissions, + }, + organizations: { + available: true, + value: [MockOrganization], + }, }; function seedInitialMetadata(metadataKey: string): () => void { diff --git a/site/src/hooks/useEmbeddedMetadata.ts b/site/src/hooks/useEmbeddedMetadata.ts index 819d15ccd3..79a3118b24 100644 --- a/site/src/hooks/useEmbeddedMetadata.ts +++ b/site/src/hooks/useEmbeddedMetadata.ts @@ -3,10 +3,12 @@ import type { BuildInfoResponse, Entitlements, Experiment, + Organization, Region, User, UserAppearanceSettings, } from "api/typesGenerated"; +import type { Permissions } from "modules/permissions"; import { useMemo, useSyncExternalStore } from "react"; export const DEFAULT_METADATA_KEY = "property"; @@ -31,6 +33,8 @@ type AvailableMetadata = Readonly<{ "build-info": BuildInfoResponse; "tasks-tab-visible": boolean; "agents-tab-visible": boolean; + permissions: Permissions; + organizations: Organization[]; }>; export type MetadataKey = keyof AvailableMetadata; @@ -94,6 +98,8 @@ export class MetadataManager implements MetadataManagerApi { regions: this.registerRegionValue(), "tasks-tab-visible": this.registerValue("tasks-tab-visible"), "agents-tab-visible": this.registerValue("agents-tab-visible"), + permissions: this.registerValue("permissions"), + organizations: this.registerValue("organizations"), }; } diff --git a/site/src/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx index c6bc31a788..b9d0934e5f 100644 --- a/site/src/modules/dashboard/DashboardProvider.tsx +++ b/site/src/modules/dashboard/DashboardProvider.tsx @@ -40,7 +40,7 @@ export const DashboardProvider: FC = ({ children }) => { const experimentsQuery = useQuery(experiments(metadata.experiments)); const appearanceQuery = useQuery(appearance(metadata.appearance)); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - const organizationsQuery = useQuery(organizations()); + const organizationsQuery = useQuery(organizations(metadata.organizations)); const error = entitlementsQuery.error || diff --git a/site/src/modules/permissions/index.ts b/site/src/modules/permissions/index.ts index 595f1089a4..c9ff5b4002 100644 --- a/site/src/modules/permissions/index.ts +++ b/site/src/modules/permissions/index.ts @@ -1,4 +1,5 @@ import type { AuthorizationCheck } from "api/typesGenerated"; +import permissionChecksData from "../../../permissions.json"; export type Permissions = { [k in PermissionName]: boolean; @@ -7,200 +8,12 @@ export type Permissions = { type PermissionName = keyof typeof permissionChecks; /** - * Site-wide permission checks + * Site-wide permission checks, loaded from the shared + * permissions.json that is also used by the Go backend. */ -export const permissionChecks = { - viewAllUsers: { - object: { - resource_type: "user", - }, - action: "read", - }, - updateUsers: { - object: { - resource_type: "user", - }, - action: "update", - }, - createUser: { - object: { - resource_type: "user", - }, - action: "create", - }, - createTemplates: { - object: { - resource_type: "template", - any_org: true, - }, - action: "create", - }, - updateTemplates: { - object: { - resource_type: "template", - }, - action: "update", - }, - deleteTemplates: { - object: { - resource_type: "template", - }, - action: "delete", - }, - viewDeploymentConfig: { - object: { - resource_type: "deployment_config", - }, - action: "read", - }, - editDeploymentConfig: { - object: { - resource_type: "deployment_config", - }, - action: "update", - }, - viewDeploymentStats: { - object: { - resource_type: "deployment_stats", - }, - action: "read", - }, - readWorkspaceProxies: { - object: { - resource_type: "workspace_proxy", - }, - action: "read", - }, - editWorkspaceProxies: { - object: { - resource_type: "workspace_proxy", - }, - action: "create", - }, - createOrganization: { - object: { - resource_type: "organization", - }, - action: "create", - }, - viewAnyGroup: { - object: { - resource_type: "group", - }, - action: "read", - }, - createGroup: { - object: { - resource_type: "group", - }, - action: "create", - }, - viewAllLicenses: { - object: { - resource_type: "license", - }, - action: "read", - }, - viewNotificationTemplate: { - object: { - resource_type: "notification_template", - }, - action: "read", - }, - viewOrganizationIDPSyncSettings: { - object: { - resource_type: "idpsync_settings", - }, - action: "read", - }, - - viewAnyMembers: { - object: { - resource_type: "organization_member", - any_org: true, - }, - action: "read", - }, - editAnyGroups: { - object: { - resource_type: "group", - any_org: true, - }, - action: "update", - }, - assignAnyRoles: { - object: { - resource_type: "assign_org_role", - any_org: true, - }, - action: "assign", - }, - viewAnyIdpSyncSettings: { - object: { - resource_type: "idpsync_settings", - any_org: true, - }, - action: "read", - }, - editAnySettings: { - object: { - resource_type: "organization", - any_org: true, - }, - action: "update", - }, - viewAnyAuditLog: { - object: { - resource_type: "audit_log", - any_org: true, - }, - action: "read", - }, - viewAnyConnectionLog: { - object: { - resource_type: "connection_log", - any_org: true, - }, - action: "read", - }, - viewDebugInfo: { - object: { - resource_type: "debug_info", - }, - action: "read", - }, - viewAnyAIBridgeInterception: { - object: { - resource_type: "aibridge_interception", - any_org: true, - }, - action: "read", - }, - createOAuth2App: { - object: { - resource_type: "oauth2_app", - }, - action: "create", - }, - editOAuth2App: { - object: { - resource_type: "oauth2_app", - }, - action: "update", - }, - deleteOAuth2App: { - object: { - resource_type: "oauth2_app", - }, - action: "delete", - }, - viewOAuth2AppSecrets: { - object: { - resource_type: "oauth2_app_secret", - }, - action: "read", - }, -} as const satisfies Record; +export const permissionChecks = + permissionChecksData as typeof permissionChecksData & + Record; export const canViewDeploymentSettings = ( permissions: Permissions | undefined, diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx index ccd145d78a..3bc2349499 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPage.tsx @@ -15,8 +15,8 @@ const WorkspaceSharingPage: FC = () => { const sharing = useWorkspaceSharing(workspace); const checks = workspaceChecks(workspace); - const permissionsQuery = useQuery({ - ...checkAuthorization({ checks }), + const permissionsQuery = useQuery({ + ...checkAuthorization({ checks }), }); const permissions = permissionsQuery.data; const canUpdatePermissions = Boolean(permissions?.updateWorkspace);