From e35717bc19f732a3460edf1cc2d6e4259b4ba5e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kayla=20=E3=81=AF=E3=81=AA?= Date: Wed, 4 Mar 2026 11:14:52 -0700 Subject: [PATCH] fix: show a notice when workspace sharing is disabled globally in organization settings (#22580) --- cli/organizationsettings.go | 2 +- coderd/apidoc/docs.go | 14 +++- coderd/apidoc/swagger.json | 14 +++- codersdk/workspacesharing.go | 14 +++- docs/reference/api/enterprise.md | 12 ++-- docs/reference/api/schemas.md | 24 +++++-- enterprise/coderd/workspacesharing.go | 6 +- enterprise/coderd/workspacesharing_test.go | 14 ++-- site/src/api/api.ts | 2 +- site/src/api/typesGenerated.ts | 17 ++++- .../OrganizationSettingsPage.tsx | 5 +- .../OrganizationSettingsPageView.tsx | 70 ++++++++++++------- 12 files changed, 142 insertions(+), 52 deletions(-) diff --git a/cli/organizationsettings.go b/cli/organizationsettings.go index 27cafa7d14..175d64414b 100644 --- a/cli/organizationsettings.go +++ b/cli/organizationsettings.go @@ -70,7 +70,7 @@ func (r *RootCmd) organizationSettings(orgContext *OrganizationContext) *serpent Aliases: []string{"workspacesharing"}, Short: "Workspace sharing settings for the organization.", Patch: func(ctx context.Context, cli *codersdk.Client, org uuid.UUID, input json.RawMessage) (any, error) { - var req codersdk.WorkspaceSharingSettings + var req codersdk.UpdateWorkspaceSharingSettingsRequest err := json.Unmarshal(input, &req) if err != nil { return nil, xerrors.Errorf("unmarshalling workspace sharing settings: %w", err) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 3ba9d8c92c..57288f672d 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4808,7 +4808,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceSharingSettings" + "$ref": "#/definitions/codersdk.UpdateWorkspaceSharingSettingsRequest" } } } @@ -20283,6 +20283,14 @@ const docTemplate = `{ } } }, + "codersdk.UpdateWorkspaceSharingSettingsRequest": { + "type": "object", + "properties": { + "sharing_disabled": { + "type": "boolean" + } + } + }, "codersdk.UpdateWorkspaceTTLRequest": { "type": "object", "properties": { @@ -22121,6 +22129,10 @@ const docTemplate = `{ "properties": { "sharing_disabled": { "type": "boolean" + }, + "sharing_globally_disabled": { + "description": "SharingGloballyDisabled is true if sharing has been disabled for this\norganization because of a deployment-wide setting.", + "type": "boolean" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index a55fbcfc49..ae328b04d1 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4253,7 +4253,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.WorkspaceSharingSettings" + "$ref": "#/definitions/codersdk.UpdateWorkspaceSharingSettingsRequest" } } } @@ -18601,6 +18601,14 @@ } } }, + "codersdk.UpdateWorkspaceSharingSettingsRequest": { + "type": "object", + "properties": { + "sharing_disabled": { + "type": "boolean" + } + } + }, "codersdk.UpdateWorkspaceTTLRequest": { "type": "object", "properties": { @@ -20337,6 +20345,10 @@ "properties": { "sharing_disabled": { "type": "boolean" + }, + "sharing_globally_disabled": { + "description": "SharingGloballyDisabled is true if sharing has been disabled for this\norganization because of a deployment-wide setting.", + "type": "boolean" } } }, diff --git a/codersdk/workspacesharing.go b/codersdk/workspacesharing.go index 3912c3dc0b..0d635063b6 100644 --- a/codersdk/workspacesharing.go +++ b/codersdk/workspacesharing.go @@ -7,8 +7,18 @@ import ( "net/http" ) -// WorkspaceSharingSettings represents workspace sharing settings for an organization. +// WorkspaceSharingSettings represents workspace sharing settings affecting an +// organization. type WorkspaceSharingSettings struct { + // SharingGloballyDisabled is true if sharing has been disabled for this + // organization because of a deployment-wide setting. + SharingGloballyDisabled bool `json:"sharing_globally_disabled"` + SharingDisabled bool `json:"sharing_disabled"` +} + +// UpdateWorkspaceSharingSettingsRequest represents workspace sharing settings +// that can be updated for an organization. +type UpdateWorkspaceSharingSettingsRequest struct { SharingDisabled bool `json:"sharing_disabled"` } @@ -28,7 +38,7 @@ func (c *Client) WorkspaceSharingSettings(ctx context.Context, orgID string) (Wo } // PatchWorkspaceSharingSettings modifies the workspace sharing settings for an organization. -func (c *Client) PatchWorkspaceSharingSettings(ctx context.Context, orgID string, req WorkspaceSharingSettings) (WorkspaceSharingSettings, error) { +func (c *Client) PatchWorkspaceSharingSettings(ctx context.Context, orgID string, req UpdateWorkspaceSharingSettingsRequest) (WorkspaceSharingSettings, error) { res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s/settings/workspace-sharing", orgID), req) if err != nil { return WorkspaceSharingSettings{}, err diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 57b674d8ff..524bdad000 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -2851,7 +2851,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/setting ```json { - "sharing_disabled": true + "sharing_disabled": true, + "sharing_globally_disabled": true } ``` @@ -2881,7 +2882,8 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/setti ```json { - "sharing_disabled": true + "sharing_disabled": true, + "sharing_globally_disabled": true } ``` @@ -2904,9 +2906,9 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization}/setti ### Responses -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceSharingSettings](schemas.md#codersdkworkspacesharingsettings) | +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.UpdateWorkspaceSharingSettingsRequest](schemas.md#codersdkupdateworkspacesharingsettingsrequest) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index cfa29730c2..1cb7987895 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -9809,6 +9809,20 @@ If the schedule is empty, the user will be updated to use the default schedule.| |--------|--------|----------|--------------|-------------| | `name` | string | false | | | +## codersdk.UpdateWorkspaceSharingSettingsRequest + +```json +{ + "sharing_disabled": true +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|--------------------|---------|----------|--------------|-------------| +| `sharing_disabled` | boolean | false | | | + ## codersdk.UpdateWorkspaceTTLRequest ```json @@ -12278,15 +12292,17 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { - "sharing_disabled": true + "sharing_disabled": true, + "sharing_globally_disabled": true } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------------|---------|----------|--------------|-------------| -| `sharing_disabled` | boolean | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------------|---------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------| +| `sharing_disabled` | boolean | false | | | +| `sharing_globally_disabled` | boolean | false | | Sharing globally disabled is true if sharing has been disabled for this organization because of a deployment-wide setting. | ## codersdk.WorkspaceStatus diff --git a/enterprise/coderd/workspacesharing.go b/enterprise/coderd/workspacesharing.go index e4814a9c8b..2f5af4728f 100644 --- a/enterprise/coderd/workspacesharing.go +++ b/enterprise/coderd/workspacesharing.go @@ -34,8 +34,10 @@ func (api *API) workspaceSharingSettings(rw http.ResponseWriter, r *http.Request return } + globallyDisabled := bool(api.DeploymentValues.DisableWorkspaceSharing) httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceSharingSettings{ - SharingDisabled: org.WorkspaceSharingDisabled, + SharingGloballyDisabled: globallyDisabled, + SharingDisabled: org.WorkspaceSharingDisabled || globallyDisabled, }) } @@ -47,7 +49,7 @@ func (api *API) workspaceSharingSettings(rw http.ResponseWriter, r *http.Request // @Tags Enterprise // @Param organization path string true "Organization ID" format(uuid) // @Param request body codersdk.WorkspaceSharingSettings true "Workspace sharing settings" -// @Success 200 {object} codersdk.WorkspaceSharingSettings +// @Success 200 {object} codersdk.UpdateWorkspaceSharingSettingsRequest // @Router /organizations/{organization}/settings/workspace-sharing [patch] func (api *API) patchWorkspaceSharingSettings(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/enterprise/coderd/workspacesharing_test.go b/enterprise/coderd/workspacesharing_test.go index e55c095a2a..37acf21e19 100644 --- a/enterprise/coderd/workspacesharing_test.go +++ b/enterprise/coderd/workspacesharing_test.go @@ -54,7 +54,7 @@ func TestWorkspaceSharingSettings(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) orgAdminClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID)) - settings, err := orgAdminClient.PatchWorkspaceSharingSettings(ctx, first.OrganizationID.String(), codersdk.WorkspaceSharingSettings{ + settings, err := orgAdminClient.PatchWorkspaceSharingSettings(ctx, first.OrganizationID.String(), codersdk.UpdateWorkspaceSharingSettingsRequest{ SharingDisabled: true, }) require.NoError(t, err) @@ -64,7 +64,7 @@ func TestWorkspaceSharingSettings(t *testing.T) { require.NoError(t, err) require.True(t, settings.SharingDisabled) - settings, err = orgAdminClient.PatchWorkspaceSharingSettings(ctx, first.OrganizationID.String(), codersdk.WorkspaceSharingSettings{ + settings, err = orgAdminClient.PatchWorkspaceSharingSettings(ctx, first.OrganizationID.String(), codersdk.UpdateWorkspaceSharingSettingsRequest{ SharingDisabled: false, }) require.NoError(t, err) @@ -85,7 +85,7 @@ func TestWorkspaceSharingSettings(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) memberClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) - _, err := memberClient.PatchWorkspaceSharingSettings(ctx, first.OrganizationID.String(), codersdk.WorkspaceSharingSettings{ + _, err := memberClient.PatchWorkspaceSharingSettings(ctx, first.OrganizationID.String(), codersdk.UpdateWorkspaceSharingSettingsRequest{ SharingDisabled: true, }) var apiErr *codersdk.Error @@ -116,7 +116,7 @@ func TestWorkspaceSharingSettings(t *testing.T) { orgAdminClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID)) auditor.ResetLogs() - _, err := orgAdminClient.PatchWorkspaceSharingSettings(ctx, first.OrganizationID.String(), codersdk.WorkspaceSharingSettings{ + _, err := orgAdminClient.PatchWorkspaceSharingSettings(ctx, first.OrganizationID.String(), codersdk.UpdateWorkspaceSharingSettingsRequest{ SharingDisabled: true, }) require.NoError(t, err) @@ -152,7 +152,7 @@ func TestWorkspaceSharingDisabled(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) orgAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) - _, err := orgAdminClient.PatchWorkspaceSharingSettings(ctx, owner.OrganizationID.String(), codersdk.WorkspaceSharingSettings{ + _, err := orgAdminClient.PatchWorkspaceSharingSettings(ctx, owner.OrganizationID.String(), codersdk.UpdateWorkspaceSharingSettingsRequest{ SharingDisabled: true, }) require.NoError(t, err) @@ -235,12 +235,12 @@ func TestWorkspaceSharingDisabled(t *testing.T) { require.Equal(t, codersdk.WorkspaceRoleUse, acl.Groups[0].Role) orgAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) - _, err = orgAdminClient.PatchWorkspaceSharingSettings(ctx, owner.OrganizationID.String(), codersdk.WorkspaceSharingSettings{ + _, err = orgAdminClient.PatchWorkspaceSharingSettings(ctx, owner.OrganizationID.String(), codersdk.UpdateWorkspaceSharingSettingsRequest{ SharingDisabled: true, }) require.NoError(t, err) - _, err = orgAdminClient.PatchWorkspaceSharingSettings(ctx, owner.OrganizationID.String(), codersdk.WorkspaceSharingSettings{ + _, err = orgAdminClient.PatchWorkspaceSharingSettings(ctx, owner.OrganizationID.String(), codersdk.UpdateWorkspaceSharingSettingsRequest{ SharingDisabled: false, }) require.NoError(t, err) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index e1db002c32..7506e10747 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -828,7 +828,7 @@ class ApiMethods { */ patchWorkspaceSharingSettings = async ( organization: string, - data: TypesGen.WorkspaceSharingSettings, + data: TypesGen.UpdateWorkspaceSharingSettingsRequest, ): Promise => { const response = await this.axios.patch( `/api/v2/organizations/${organization}/settings/workspace-sharing`, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 94402e4c92..1198f206de 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -6523,6 +6523,15 @@ export interface UpdateWorkspaceRequest { readonly name?: string; } +// From codersdk/workspacesharing.go +/** + * UpdateWorkspaceSharingSettingsRequest represents workspace sharing settings + * that can be updated for an organization. + */ +export interface UpdateWorkspaceSharingSettingsRequest { + readonly sharing_disabled: boolean; +} + // From codersdk/workspaces.go /** * UpdateWorkspaceTTLRequest is a request to update a workspace's TTL. @@ -7556,9 +7565,15 @@ export const WorkspaceRoles: WorkspaceRole[] = ["admin", "", "use"]; // From codersdk/workspacesharing.go /** - * WorkspaceSharingSettings represents workspace sharing settings for an organization. + * WorkspaceSharingSettings represents workspace sharing settings affecting an + * organization. */ export interface WorkspaceSharingSettings { + /** + * SharingGloballyDisabled is true if sharing has been disabled for this + * organization because of a deployment-wide setting. + */ + readonly sharing_globally_disabled: boolean; readonly sharing_disabled: boolean; } diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx index a17fcf7a50..fdd82551dc 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx @@ -29,7 +29,7 @@ const OrganizationSettingsPage: FC = () => { const sharingSettingsQuery = useQuery({ ...workspaceSharingSettings(organization?.id ?? ""), - enabled: !!organization, + enabled: Boolean(organization), }); const patchSharingSettingsMutation = useMutation( @@ -112,6 +112,9 @@ const OrganizationSettingsPage: FC = () => { ); } }} + workspaceSharingGloballyDisabled={ + sharingSettingsQuery.data?.sharing_globally_disabled + } workspaceSharingEnabled={ !(sharingSettingsQuery.data?.sharing_disabled ?? false) } diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx index d033e71288..e34b4ee5a4 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx @@ -4,6 +4,7 @@ import type { Organization, UpdateOrganizationRequest, } from "api/typesGenerated"; +import { Alert, AlertTitle } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Button } from "components/Button/Button"; import { Checkbox } from "components/Checkbox/Checkbox"; @@ -50,9 +51,10 @@ interface OrganizationSettingsPageViewProps { error: unknown; onSubmit: (values: UpdateOrganizationRequest) => Promise; onDeleteOrganization: () => void; - workspaceSharingEnabled?: boolean; - onToggleWorkspaceSharing?: (enabled: boolean) => void; - isTogglingWorkspaceSharing?: boolean; + workspaceSharingGloballyDisabled?: boolean; + workspaceSharingEnabled: boolean; + onToggleWorkspaceSharing: (enabled: boolean) => void; + isTogglingWorkspaceSharing: boolean; } export const OrganizationSettingsPageView: FC< @@ -62,7 +64,8 @@ export const OrganizationSettingsPageView: FC< error, onSubmit, onDeleteOrganization, - workspaceSharingEnabled = true, + workspaceSharingGloballyDisabled, + workspaceSharingEnabled, onToggleWorkspaceSharing, isTogglingWorkspaceSharing, }) => { @@ -156,29 +159,44 @@ export const OrganizationSettingsPageView: FC< } description="Control whether workspace owners can share their workspaces." > -
- { - if (checked) { - onToggleWorkspaceSharing(true); - } else { - setIsDisableSharingDialogOpen(true); +
+ {workspaceSharingGloballyDisabled && ( + + Disabled by deployment settings + Workspace sharing has been disallowed by an administrator. + Sharing must be allowed by an administrator before sharing can + be used in this organization. + + )} +
+ -
- -
- When enabled, workspace owners can share their workspaces with - other users in this organization. + disabled={ + workspaceSharingGloballyDisabled || + isTogglingWorkspaceSharing + } + onCheckedChange={(checked) => { + if (checked) { + onToggleWorkspaceSharing(true); + } else { + setIsDisableSharingDialogOpen(true); + } + }} + /> +
+ +
+ When enabled, workspace owners can share their workspaces + with other users in this organization. +