mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
+3
-2
@@ -329,9 +329,10 @@ func New(options *Options) *API {
|
||||
panic("developer error: options.PrometheusRegistry is nil and not running a unit test")
|
||||
}
|
||||
|
||||
if options.DeploymentValues.DisableOwnerWorkspaceExec {
|
||||
if options.DeploymentValues.DisableOwnerWorkspaceExec || options.DeploymentValues.DisableWorkspaceSharing {
|
||||
rbac.ReloadBuiltinRoles(&rbac.RoleOptions{
|
||||
NoOwnerWorkspaceExec: true,
|
||||
NoOwnerWorkspaceExec: bool(options.DeploymentValues.DisableOwnerWorkspaceExec),
|
||||
NoWorkspaceSharing: bool(options.DeploymentValues.DisableWorkspaceSharing),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+38
-18
@@ -244,6 +244,7 @@ func SystemRoleName(name string) bool {
|
||||
|
||||
type RoleOptions struct {
|
||||
NoOwnerWorkspaceExec bool
|
||||
NoWorkspaceSharing bool
|
||||
}
|
||||
|
||||
// ReservedRoleName exists because the database should only allow unique role
|
||||
@@ -267,12 +268,23 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
opts = &RoleOptions{}
|
||||
}
|
||||
|
||||
denyPermissions := []Permission{}
|
||||
if opts.NoWorkspaceSharing {
|
||||
denyPermissions = append(denyPermissions, Permission{
|
||||
Negate: true,
|
||||
ResourceType: ResourceWorkspace.Type,
|
||||
Action: policy.ActionShare,
|
||||
})
|
||||
}
|
||||
|
||||
ownerWorkspaceActions := ResourceWorkspace.AvailableActions()
|
||||
if opts.NoOwnerWorkspaceExec {
|
||||
// Remove ssh and application connect from the owner role. This
|
||||
// prevents owners from have exec access to all workspaces.
|
||||
ownerWorkspaceActions = slice.Omit(ownerWorkspaceActions,
|
||||
policy.ActionApplicationConnect, policy.ActionSSH)
|
||||
ownerWorkspaceActions = slice.Omit(
|
||||
ownerWorkspaceActions,
|
||||
policy.ActionApplicationConnect, policy.ActionSSH,
|
||||
)
|
||||
}
|
||||
|
||||
// Static roles that never change should be allocated in a closure.
|
||||
@@ -295,7 +307,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
// Explicitly setting PrebuiltWorkspace permissions for clarity.
|
||||
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
|
||||
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
|
||||
})...),
|
||||
})...,
|
||||
),
|
||||
User: []Permission{},
|
||||
ByOrgID: map[string]OrgPermissions{},
|
||||
}.withCachedRegoValue()
|
||||
@@ -303,13 +316,17 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
memberRole := Role{
|
||||
Identifier: RoleMember(),
|
||||
DisplayName: "Member",
|
||||
Site: Permissions(map[string][]policy.Action{
|
||||
ResourceAssignRole.Type: {policy.ActionRead},
|
||||
// All users can see OAuth2 provider applications.
|
||||
ResourceOauth2App.Type: {policy.ActionRead},
|
||||
ResourceWorkspaceProxy.Type: {policy.ActionRead},
|
||||
}),
|
||||
User: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage),
|
||||
Site: append(
|
||||
Permissions(map[string][]policy.Action{
|
||||
ResourceAssignRole.Type: {policy.ActionRead},
|
||||
// All users can see OAuth2 provider applications.
|
||||
ResourceOauth2App.Type: {policy.ActionRead},
|
||||
ResourceWorkspaceProxy.Type: {policy.ActionRead},
|
||||
}),
|
||||
denyPermissions...,
|
||||
),
|
||||
User: append(
|
||||
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage),
|
||||
Permissions(map[string][]policy.Action{
|
||||
// Users cannot do create/update/delete on themselves, but they
|
||||
// can read their own details.
|
||||
@@ -433,14 +450,17 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
ByOrgID: map[string]OrgPermissions{
|
||||
// Org admins should not have workspace exec perms.
|
||||
organizationID.String(): {
|
||||
Org: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage), Permissions(map[string][]policy.Action{
|
||||
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
|
||||
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
|
||||
// PrebuiltWorkspaces are a subset of Workspaces.
|
||||
// Explicitly setting PrebuiltWorkspace permissions for clarity.
|
||||
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
|
||||
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
|
||||
})...),
|
||||
Org: append(
|
||||
allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage),
|
||||
Permissions(map[string][]policy.Action{
|
||||
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
|
||||
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
|
||||
// PrebuiltWorkspaces are a subset of Workspaces.
|
||||
// Explicitly setting PrebuiltWorkspace permissions for clarity.
|
||||
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
|
||||
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
|
||||
})...,
|
||||
),
|
||||
Member: []Permission{},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5572,6 +5572,10 @@ func TestWorkspaceSharingDisabled(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("NoAccessWhenDisabled", func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
rbac.ReloadBuiltinRoles(nil)
|
||||
})
|
||||
|
||||
var (
|
||||
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
|
||||
|
||||
@@ -248,7 +248,7 @@ export const patchRoleSyncSettings = (
|
||||
};
|
||||
};
|
||||
|
||||
const getWorkspaceSharingSettingsKey = (organization: string) => [
|
||||
export const getWorkspaceSharingSettingsKey = (organization: string) => [
|
||||
"organization",
|
||||
organization,
|
||||
"workspaceSharingSettings",
|
||||
|
||||
@@ -479,7 +479,7 @@ export const workspacePermissions = (workspace?: Workspace) => {
|
||||
checks: workspace ? workspaceChecks(workspace) : {},
|
||||
}),
|
||||
queryKey: ["workspaces", workspace?.id, "permissions"],
|
||||
enabled: !!workspace,
|
||||
enabled: Boolean(workspace),
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { workspaceSharingSettings } from "api/queries/organizations";
|
||||
import type {
|
||||
Group,
|
||||
WorkspaceACL,
|
||||
@@ -37,6 +38,7 @@ import { TableLoader } from "components/TableLoader/TableLoader";
|
||||
import { EllipsisVertical, UserPlusIcon } from "lucide-react";
|
||||
import { getGroupSubtitle } from "modules/groups";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
interface RoleSelectProps {
|
||||
value: WorkspaceRole;
|
||||
@@ -139,6 +141,7 @@ export const RoleSelectField: FC<RoleSelectFieldProps> = ({
|
||||
};
|
||||
|
||||
interface WorkspaceSharingFormProps {
|
||||
organizationId: string;
|
||||
workspaceACL: WorkspaceACL | undefined;
|
||||
canUpdatePermissions: boolean;
|
||||
isTaskWorkspace: boolean;
|
||||
@@ -155,6 +158,7 @@ interface WorkspaceSharingFormProps {
|
||||
}
|
||||
|
||||
export const WorkspaceSharingForm: FC<WorkspaceSharingFormProps> = ({
|
||||
organizationId,
|
||||
workspaceACL,
|
||||
canUpdatePermissions,
|
||||
isTaskWorkspace,
|
||||
@@ -169,6 +173,46 @@ export const WorkspaceSharingForm: FC<WorkspaceSharingFormProps> = ({
|
||||
isCompact,
|
||||
showRestartWarning,
|
||||
}) => {
|
||||
const sharingSettingsQuery = useQuery(
|
||||
workspaceSharingSettings(organizationId),
|
||||
);
|
||||
|
||||
if (sharingSettingsQuery.isLoading) {
|
||||
return (
|
||||
<TableBody>
|
||||
<TableLoader />
|
||||
</TableBody>
|
||||
);
|
||||
}
|
||||
|
||||
if (!sharingSettingsQuery.data) {
|
||||
return (
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<ErrorAlert error={sharingSettingsQuery.error} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
);
|
||||
}
|
||||
|
||||
if (sharingSettingsQuery.data.sharing_disabled) {
|
||||
return (
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message="This workspace cannot be shared"
|
||||
description="Workspace sharing has been disabled for this organization."
|
||||
isCompact={isCompact}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
);
|
||||
}
|
||||
|
||||
const isEmpty = Boolean(
|
||||
workspaceACL &&
|
||||
workspaceACL.users.length === 0 &&
|
||||
|
||||
@@ -10,6 +10,14 @@ export const workspaceChecks = (workspace: Workspace) =>
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
shareWorkspace: {
|
||||
object: {
|
||||
resource_type: "workspace",
|
||||
resource_id: workspace.id,
|
||||
owner_id: workspace.owner_id,
|
||||
},
|
||||
action: "share",
|
||||
},
|
||||
updateWorkspace: {
|
||||
object: {
|
||||
resource_type: "workspace",
|
||||
@@ -34,14 +42,6 @@ export const workspaceChecks = (workspace: Workspace) =>
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
// To run a build in debug mode we need to be able to read the deployment
|
||||
// config (enable_terraform_debug_mode).
|
||||
deploymentConfig: {
|
||||
object: {
|
||||
resource_type: "deployment_config",
|
||||
},
|
||||
action: "read",
|
||||
},
|
||||
}) satisfies Record<string, AuthorizationCheck>;
|
||||
|
||||
export type WorkspacePermissions = Record<
|
||||
|
||||
@@ -24,9 +24,9 @@ const createTimestamp = (
|
||||
|
||||
const permissions: WorkspacePermissions = {
|
||||
readWorkspace: true,
|
||||
shareWorkspace: true,
|
||||
updateWorkspace: true,
|
||||
updateWorkspaceVersion: true,
|
||||
deploymentConfig: true,
|
||||
deleteFailedWorkspace: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
latestVersion,
|
||||
permissions,
|
||||
timings,
|
||||
sharingDisabled,
|
||||
handleStart,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
@@ -111,7 +110,6 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
latestVersion={latestVersion}
|
||||
isUpdating={isUpdating}
|
||||
isRestarting={isRestarting}
|
||||
sharingDisabled={sharingDisabled}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
handleRestart={handleRestart}
|
||||
|
||||
@@ -38,6 +38,7 @@ export const ShareButton: FC<ShareButtonProps> = ({
|
||||
<FeatureStageBadge contentType="beta" size="sm" />
|
||||
</div>
|
||||
<WorkspaceSharingForm
|
||||
organizationId={workspace.organization_id}
|
||||
workspaceACL={sharing.workspaceACL}
|
||||
canUpdatePermissions={canUpdatePermissions}
|
||||
isTaskWorkspace={Boolean(workspace.task_id)}
|
||||
|
||||
@@ -16,11 +16,11 @@ const meta: Meta<typeof WorkspaceActions> = {
|
||||
args: {
|
||||
isUpdating: false,
|
||||
permissions: {
|
||||
deleteFailedWorkspace: true,
|
||||
deploymentConfig: true,
|
||||
readWorkspace: true,
|
||||
shareWorkspace: true,
|
||||
updateWorkspace: true,
|
||||
updateWorkspaceVersion: true,
|
||||
deleteFailedWorkspace: true,
|
||||
},
|
||||
},
|
||||
decorators: [withDashboardProvider, withDesktopViewport, withAuthProvider],
|
||||
@@ -172,11 +172,11 @@ export const FailedWithDebug: Story = {
|
||||
args: {
|
||||
workspace: Mocks.MockFailedWorkspace,
|
||||
permissions: {
|
||||
deploymentConfig: true,
|
||||
deleteFailedWorkspace: true,
|
||||
readWorkspace: true,
|
||||
shareWorkspace: true,
|
||||
updateWorkspace: true,
|
||||
updateWorkspaceVersion: true,
|
||||
deleteFailedWorkspace: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -29,7 +29,6 @@ interface WorkspaceActionsProps {
|
||||
isUpdating: boolean;
|
||||
isRestarting: boolean;
|
||||
permissions: WorkspacePermissions;
|
||||
sharingDisabled?: boolean;
|
||||
handleToggleFavorite: () => void;
|
||||
handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void;
|
||||
handleStop: () => void;
|
||||
@@ -46,7 +45,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
isUpdating,
|
||||
isRestarting,
|
||||
permissions,
|
||||
sharingDisabled,
|
||||
handleToggleFavorite,
|
||||
handleStart,
|
||||
handleStop,
|
||||
@@ -57,10 +55,13 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
handleDebug,
|
||||
handleDormantActivate,
|
||||
}) => {
|
||||
const { user } = useAuthenticated();
|
||||
const {
|
||||
permissions: { viewDeploymentConfig },
|
||||
user,
|
||||
} = useAuthenticated();
|
||||
const { data: deployment } = useQuery({
|
||||
...deploymentConfig(),
|
||||
enabled: permissions.deploymentConfig,
|
||||
enabled: viewDeploymentConfig,
|
||||
});
|
||||
const { actions, canCancel, canAcceptJobs } = abilitiesByWorkspaceStatus(
|
||||
workspace,
|
||||
@@ -191,7 +192,7 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
onToggle={handleToggleFavorite}
|
||||
/>
|
||||
|
||||
{!sharingDisabled && (
|
||||
{permissions.shareWorkspace && (
|
||||
<ShareButton
|
||||
workspace={workspace}
|
||||
canUpdatePermissions={permissions.updateWorkspace}
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ import { WorkspaceNotifications } from "./WorkspaceNotifications";
|
||||
|
||||
export const defaultPermissions: WorkspacePermissions = {
|
||||
readWorkspace: true,
|
||||
updateWorkspaceVersion: true,
|
||||
shareWorkspace: true,
|
||||
updateWorkspace: true,
|
||||
deploymentConfig: true,
|
||||
updateWorkspaceVersion: true,
|
||||
deleteFailedWorkspace: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -125,11 +125,11 @@ describe("WorkspacePage", () => {
|
||||
server.use(
|
||||
http.post("/api/v2/authcheck", async () => {
|
||||
const permissions: WorkspacePermissions = {
|
||||
deleteFailedWorkspace: true,
|
||||
deploymentConfig: true,
|
||||
readWorkspace: true,
|
||||
shareWorkspace: true,
|
||||
updateWorkspace: true,
|
||||
updateWorkspaceVersion: true,
|
||||
deleteFailedWorkspace: true,
|
||||
};
|
||||
return HttpResponse.json(permissions);
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { watchWorkspace } from "api/api";
|
||||
import { workspaceSharingSettings } from "api/queries/organizations";
|
||||
import { template as templateQueryOptions } from "api/queries/templates";
|
||||
import { workspaceBuildsKey } from "api/queries/workspaceBuilds";
|
||||
import {
|
||||
@@ -45,12 +44,6 @@ const WorkspacePage: FC = () => {
|
||||
const permissionsQuery = useQuery(workspacePermissions(workspace));
|
||||
const permissions = permissionsQuery.data;
|
||||
|
||||
const sharingSettingsQuery = useQuery({
|
||||
...workspaceSharingSettings(workspace?.organization_id ?? ""),
|
||||
enabled: !!workspace,
|
||||
});
|
||||
const sharingDisabled = sharingSettingsQuery.data?.sharing_disabled ?? false;
|
||||
|
||||
// Watch workspace changes
|
||||
const updateWorkspaceData = useEffectEvent(
|
||||
async (newWorkspaceData: Workspace) => {
|
||||
@@ -121,7 +114,6 @@ const WorkspacePage: FC = () => {
|
||||
workspace={workspace}
|
||||
template={template}
|
||||
permissions={permissions}
|
||||
sharingDisabled={sharingDisabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,14 +34,12 @@ interface WorkspaceReadyPageProps {
|
||||
template: TypesGen.Template;
|
||||
workspace: TypesGen.Workspace;
|
||||
permissions: WorkspacePermissions;
|
||||
sharingDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
workspace,
|
||||
template,
|
||||
permissions,
|
||||
sharingDisabled,
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -285,7 +283,6 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
template={template}
|
||||
buildLogs={buildLogs}
|
||||
timings={timingsQuery.data}
|
||||
sharingDisabled={sharingDisabled}
|
||||
handleStart={async (buildParameters) => {
|
||||
const { hasEphemeral, ephemeralParameters } =
|
||||
await checkEphemeralParameters(buildParameters);
|
||||
|
||||
@@ -35,9 +35,9 @@ const meta: Meta<typeof WorkspaceTopbar> = {
|
||||
latestVersion: MockTemplateVersion,
|
||||
permissions: {
|
||||
readWorkspace: true,
|
||||
updateWorkspaceVersion: true,
|
||||
shareWorkspace: true,
|
||||
updateWorkspace: true,
|
||||
deploymentConfig: true,
|
||||
updateWorkspaceVersion: true,
|
||||
deleteFailedWorkspace: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -44,7 +44,6 @@ interface WorkspaceProps {
|
||||
template: TypesGen.Template;
|
||||
permissions: WorkspacePermissions;
|
||||
latestVersion?: TypesGen.TemplateVersion;
|
||||
sharingDisabled?: boolean;
|
||||
handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
|
||||
handleStop: () => void;
|
||||
handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
|
||||
@@ -63,7 +62,6 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
|
||||
permissions,
|
||||
isUpdating,
|
||||
isRestarting,
|
||||
sharingDisabled,
|
||||
handleStart,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
@@ -238,7 +236,6 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
|
||||
permissions={permissions}
|
||||
isUpdating={isUpdating}
|
||||
isRestarting={isRestarting}
|
||||
sharingDisabled={sharingDisabled}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
handleRestart={handleRestart}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||
import {
|
||||
@@ -12,19 +11,11 @@ import {
|
||||
TimerIcon as ScheduleIcon,
|
||||
Users as SharingIcon,
|
||||
} from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useWorkspaceSettings } from "./WorkspaceSettingsLayout";
|
||||
|
||||
interface SidebarProps {
|
||||
username: string;
|
||||
workspace: Workspace;
|
||||
sharingDisabled?: boolean;
|
||||
}
|
||||
export const Sidebar: React.FC = () => {
|
||||
const { owner, workspace, permissions } = useWorkspaceSettings();
|
||||
|
||||
export const Sidebar: FC<SidebarProps> = ({
|
||||
username,
|
||||
workspace,
|
||||
sharingDisabled,
|
||||
}) => {
|
||||
return (
|
||||
<BaseSidebar>
|
||||
<SidebarHeader
|
||||
@@ -36,7 +27,7 @@ export const Sidebar: FC<SidebarProps> = ({
|
||||
/>
|
||||
}
|
||||
title={workspace.name}
|
||||
linkTo={`/@${username}/${workspace.name}`}
|
||||
linkTo={`/@${owner}/${workspace.name}`}
|
||||
subtitle={workspace.template_display_name ?? workspace.template_name}
|
||||
/>
|
||||
|
||||
@@ -49,7 +40,7 @@ export const Sidebar: FC<SidebarProps> = ({
|
||||
<SidebarNavItem href="schedule" icon={ScheduleIcon}>
|
||||
Schedule
|
||||
</SidebarNavItem>
|
||||
{!sharingDisabled && (
|
||||
{permissions?.shareWorkspace && (
|
||||
<SidebarNavItem href="sharing" icon={SharingIcon}>
|
||||
Sharing
|
||||
<FeatureStageBadge contentType="beta" size="sm" />
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import WorkspaceParametersPage from "./WorkspaceParametersPage";
|
||||
import WorkspaceParametersPageExperimental from "./WorkspaceParametersPageExperimental";
|
||||
|
||||
const WorkspaceParametersExperimentRouter: FC = () => {
|
||||
const workspace = useWorkspaceSettings();
|
||||
const { workspace } = useWorkspaceSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ import {
|
||||
} from "./WorkspaceParametersForm";
|
||||
|
||||
const WorkspaceParametersPage: FC = () => {
|
||||
const workspace = useWorkspaceSettings();
|
||||
const { workspace } = useWorkspaceSettings();
|
||||
const build = workspace.latest_build;
|
||||
const { data: templateVersionParameters } = useQuery(
|
||||
richParameters(build.template_version_id),
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ import { useWorkspaceSettings } from "../WorkspaceSettingsLayout";
|
||||
import { WorkspaceParametersPageViewExperimental } from "./WorkspaceParametersPageViewExperimental";
|
||||
|
||||
const WorkspaceParametersPageExperimental: FC = () => {
|
||||
const workspace = useWorkspaceSettings();
|
||||
const { workspace } = useWorkspaceSettings();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const templateVersionId = searchParams.get("templateVersionId") ?? undefined;
|
||||
|
||||
+9
-14
@@ -6,10 +6,10 @@ import {
|
||||
} from "testHelpers/entities";
|
||||
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { getAuthorizationKey } from "api/queries/authCheck";
|
||||
import { templateByNameKey } from "api/queries/templates";
|
||||
import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import type { WorkspacePermissions } from "modules/workspaces/permissions";
|
||||
import {
|
||||
reactRouterOutlet,
|
||||
reactRouterParameters,
|
||||
@@ -68,19 +68,14 @@ function workspaceQueries(workspace: Workspace) {
|
||||
data: workspace,
|
||||
},
|
||||
{
|
||||
key: getAuthorizationKey({
|
||||
checks: {
|
||||
updateWorkspace: {
|
||||
object: {
|
||||
resource_type: "workspace",
|
||||
resource_id: MockWorkspace.id,
|
||||
owner_id: MockWorkspace.owner_id,
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
},
|
||||
}),
|
||||
data: { updateWorkspace: true },
|
||||
key: ["workspaces", workspace.id, "permissions"],
|
||||
data: {
|
||||
readWorkspace: true,
|
||||
shareWorkspace: true,
|
||||
updateWorkspace: true,
|
||||
updateWorkspaceVersion: true,
|
||||
deleteFailedWorkspace: true,
|
||||
} satisfies WorkspacePermissions,
|
||||
},
|
||||
{
|
||||
key: templateByNameKey(
|
||||
|
||||
+3
-19
@@ -1,5 +1,4 @@
|
||||
import { API } from "api/api";
|
||||
import { checkAuthorization } from "api/queries/authCheck";
|
||||
import { templateByName } from "api/queries/templates";
|
||||
import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
@@ -28,28 +27,13 @@ import {
|
||||
} from "./formToRequest";
|
||||
import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm";
|
||||
|
||||
const permissionsToCheck = (workspace: TypesGen.Workspace) =>
|
||||
({
|
||||
updateWorkspace: {
|
||||
object: {
|
||||
resource_type: "workspace",
|
||||
resource_id: workspace.id,
|
||||
owner_id: workspace.owner_id,
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
}) as const;
|
||||
|
||||
const WorkspaceSchedulePage: FC = () => {
|
||||
const params = useParams() as { username: string; workspace: string };
|
||||
const navigate = useNavigate();
|
||||
const username = params.username.replace("@", "");
|
||||
const workspaceName = params.workspace;
|
||||
const queryClient = useQueryClient();
|
||||
const workspace = useWorkspaceSettings();
|
||||
const { data: permissions, error: checkPermissionsError } = useQuery(
|
||||
checkAuthorization({ checks: permissionsToCheck(workspace) }),
|
||||
);
|
||||
const { permissions, workspace } = useWorkspaceSettings();
|
||||
const { data: template, error: getTemplateError } = useQuery(
|
||||
templateByName(workspace.organization_id, workspace.template_name),
|
||||
);
|
||||
@@ -66,8 +50,8 @@ const WorkspaceSchedulePage: FC = () => {
|
||||
},
|
||||
onError: () => displayError("Failed to update workspace schedule"),
|
||||
});
|
||||
const error = checkPermissionsError || getTemplateError;
|
||||
const isLoading = !template || !permissions;
|
||||
const error = getTemplateError;
|
||||
const isLoading = !template;
|
||||
|
||||
const [isConfirmingApply, setIsConfirmingApply] = useState(false);
|
||||
const { mutate: updateWorkspace } = useMutation({
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { workspaceSharingSettings } from "api/queries/organizations";
|
||||
import { workspaceByOwnerAndName } from "api/queries/workspaces";
|
||||
import {
|
||||
workspaceByOwnerAndName,
|
||||
workspacePermissions,
|
||||
} from "api/queries/workspaces";
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import type { WorkspacePermissions } from "modules/workspaces/permissions";
|
||||
import { createContext, type FC, Suspense, useContext } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { Outlet, useParams } from "react-router";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
const WorkspaceSettings = createContext<Workspace | undefined>(undefined);
|
||||
type WorkspaceSettingsContext = {
|
||||
owner: string;
|
||||
workspace: Workspace;
|
||||
permissions?: WorkspacePermissions;
|
||||
};
|
||||
|
||||
const WorkspaceSettings = createContext<WorkspaceSettingsContext | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function useWorkspaceSettings() {
|
||||
const value = useContext(WorkspaceSettings);
|
||||
@@ -31,39 +42,36 @@ export const WorkspaceSettingsLayout: FC = () => {
|
||||
};
|
||||
const workspaceName = params.workspace;
|
||||
const username = params.username.replace("@", "");
|
||||
const {
|
||||
data: workspace,
|
||||
error,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery(workspaceByOwnerAndName(username, workspaceName));
|
||||
const workspaceQuery = useQuery(
|
||||
workspaceByOwnerAndName(username, workspaceName),
|
||||
);
|
||||
|
||||
const sharingSettingsQuery = useQuery({
|
||||
...workspaceSharingSettings(workspace?.organization_id ?? ""),
|
||||
enabled: !!workspace,
|
||||
});
|
||||
const sharingDisabled = sharingSettingsQuery.data?.sharing_disabled ?? false;
|
||||
const permissionsQuery = useQuery(workspacePermissions(workspaceQuery.data));
|
||||
|
||||
if (isLoading) {
|
||||
if (workspaceQuery.isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
const error = workspaceQuery.error || permissionsQuery.error;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{pageTitle(workspaceName, "Settings")}</title>
|
||||
|
||||
<Margins>
|
||||
<Stack css={{ padding: "48px 0" }} direction="row" spacing={10}>
|
||||
{isError ? (
|
||||
{error ? (
|
||||
<ErrorAlert error={error} />
|
||||
) : (
|
||||
workspace && (
|
||||
<WorkspaceSettings.Provider value={workspace}>
|
||||
<Sidebar
|
||||
workspace={workspace}
|
||||
username={username}
|
||||
sharingDisabled={sharingDisabled}
|
||||
/>
|
||||
workspaceQuery.data && (
|
||||
<WorkspaceSettings.Provider
|
||||
value={{
|
||||
owner: username,
|
||||
workspace: workspaceQuery.data,
|
||||
permissions: permissionsQuery.data,
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<Suspense fallback={<Loader />}>
|
||||
<main css={{ width: "100%" }}>
|
||||
<Outlet />
|
||||
|
||||
@@ -15,7 +15,7 @@ const WorkspaceSettingsPage: FC = () => {
|
||||
};
|
||||
const workspaceName = params.workspace;
|
||||
const username = params.username.replace("@", "");
|
||||
const workspace = useWorkspaceSettings();
|
||||
const { workspace } = useWorkspaceSettings();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const mutation = useMutation({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useWorkspaceSettings } from "../WorkspaceSettingsLayout";
|
||||
import { WorkspaceSharingPageView } from "./WorkspaceSharingPageView";
|
||||
|
||||
const WorkspaceSharingPage: FC = () => {
|
||||
const workspace = useWorkspaceSettings();
|
||||
const { workspace } = useWorkspaceSettings();
|
||||
const sharing = useWorkspaceSharing(workspace);
|
||||
|
||||
const checks = workspaceChecks(workspace);
|
||||
@@ -25,7 +25,7 @@ const WorkspaceSharingPage: FC = () => {
|
||||
sharing.error ?? permissionsQuery.error ?? sharing.mutationError;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12 max-w-screen-md">
|
||||
<div className="flex flex-col gap-12">
|
||||
<title>{pageTitle(workspace.name, "Sharing")}</title>
|
||||
|
||||
<header className="flex flex-col">
|
||||
|
||||
+9
@@ -7,6 +7,7 @@ import {
|
||||
mockApiError,
|
||||
} from "testHelpers/entities";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { getWorkspaceSharingSettingsKey } from "api/queries/organizations";
|
||||
import type {
|
||||
WorkspaceACL,
|
||||
WorkspaceGroup,
|
||||
@@ -63,6 +64,14 @@ const aclWithUsersAndGroups: WorkspaceACL = {
|
||||
const meta: Meta<typeof WorkspaceSharingPageView> = {
|
||||
title: "pages/WorkspaceSharingPageView",
|
||||
component: WorkspaceSharingPageView,
|
||||
parameters: {
|
||||
queries: [
|
||||
{
|
||||
key: getWorkspaceSharingSettingsKey(MockWorkspace.organization_id),
|
||||
data: { sharing_disabled: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
args: {
|
||||
workspace: MockWorkspace,
|
||||
workspaceACL: emptyACL,
|
||||
|
||||
@@ -52,6 +52,7 @@ export const WorkspaceSharingPageView: FC<WorkspaceSharingPageViewProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<WorkspaceSharingForm
|
||||
organizationId={workspace.organization_id}
|
||||
workspaceACL={workspaceACL}
|
||||
canUpdatePermissions={canUpdatePermissions}
|
||||
isTaskWorkspace={Boolean(workspace.task_id)}
|
||||
|
||||
Reference in New Issue
Block a user