feat: add service_accounts workspace sharing mode (#23093)

Introduce a three-way workspace sharing setting (none, everyone,
service_accounts) replacing the boolean workspace_sharing_disabled.
In service_accounts mode, only service account-owned workspaces can be
shared while regular members' share permissions are removed. Adds a
new organization-service-account system role with per-org permissions
reconciled alongside the existing organization-member system role.

Related to:
https://linear.app/codercom/issue/PLAT-28/feat-service-accounts-sharing-mode-and-rbac-role

---------

Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
Co-authored-by: Kayla はな <mckayla@hey.com>
This commit is contained in:
George K
2026-03-17 12:16:43 -07:00
committed by GitHub
co-authored by Steven Masley Kayla はな
parent 6b76e30321
commit 91ec0f1484
38 changed files with 1437 additions and 421 deletions
+48 -25
View File
@@ -11035,7 +11035,7 @@ func (q *sqlQuerier) UpdateMemberRoles(ctx context.Context, arg UpdateMemberRole
const getDefaultOrganization = `-- name: GetDefaultOrganization :one
SELECT
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, workspace_sharing_disabled
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
FROM
organizations
WHERE
@@ -11057,14 +11057,14 @@ func (q *sqlQuerier) GetDefaultOrganization(ctx context.Context) (Organization,
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.WorkspaceSharingDisabled,
&i.ShareableWorkspaceOwners,
)
return i, err
}
const getOrganizationByID = `-- name: GetOrganizationByID :one
SELECT
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, workspace_sharing_disabled
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
FROM
organizations
WHERE
@@ -11084,14 +11084,14 @@ func (q *sqlQuerier) GetOrganizationByID(ctx context.Context, id uuid.UUID) (Org
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.WorkspaceSharingDisabled,
&i.ShareableWorkspaceOwners,
)
return i, err
}
const getOrganizationByName = `-- name: GetOrganizationByName :one
SELECT
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, workspace_sharing_disabled
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
FROM
organizations
WHERE
@@ -11120,7 +11120,7 @@ func (q *sqlQuerier) GetOrganizationByName(ctx context.Context, arg GetOrganizat
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.WorkspaceSharingDisabled,
&i.ShareableWorkspaceOwners,
)
return i, err
}
@@ -11191,7 +11191,7 @@ func (q *sqlQuerier) GetOrganizationResourceCountByID(ctx context.Context, organ
const getOrganizations = `-- name: GetOrganizations :many
SELECT
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, workspace_sharing_disabled
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
FROM
organizations
WHERE
@@ -11235,7 +11235,7 @@ func (q *sqlQuerier) GetOrganizations(ctx context.Context, arg GetOrganizationsP
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.WorkspaceSharingDisabled,
&i.ShareableWorkspaceOwners,
); err != nil {
return nil, err
}
@@ -11252,7 +11252,7 @@ func (q *sqlQuerier) GetOrganizations(ctx context.Context, arg GetOrganizationsP
const getOrganizationsByUserID = `-- name: GetOrganizationsByUserID :many
SELECT
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, workspace_sharing_disabled
id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
FROM
organizations
WHERE
@@ -11297,7 +11297,7 @@ func (q *sqlQuerier) GetOrganizationsByUserID(ctx context.Context, arg GetOrgani
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.WorkspaceSharingDisabled,
&i.ShareableWorkspaceOwners,
); err != nil {
return nil, err
}
@@ -11317,7 +11317,7 @@ INSERT INTO
organizations (id, "name", display_name, description, icon, created_at, updated_at, is_default)
VALUES
-- If no organizations exist, and this is the first, make it the default.
($1, $2, $3, $4, $5, $6, $7, (SELECT TRUE FROM organizations LIMIT 1) IS NULL) RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, workspace_sharing_disabled
($1, $2, $3, $4, $5, $6, $7, (SELECT TRUE FROM organizations LIMIT 1) IS NULL) RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
`
type InsertOrganizationParams struct {
@@ -11351,7 +11351,7 @@ func (q *sqlQuerier) InsertOrganization(ctx context.Context, arg InsertOrganizat
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.WorkspaceSharingDisabled,
&i.ShareableWorkspaceOwners,
)
return i, err
}
@@ -11367,7 +11367,7 @@ SET
icon = $5
WHERE
id = $6
RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, workspace_sharing_disabled
RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
`
type UpdateOrganizationParams struct {
@@ -11399,7 +11399,7 @@ func (q *sqlQuerier) UpdateOrganization(ctx context.Context, arg UpdateOrganizat
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.WorkspaceSharingDisabled,
&i.ShareableWorkspaceOwners,
)
return i, err
}
@@ -11428,21 +11428,21 @@ const updateOrganizationWorkspaceSharingSettings = `-- name: UpdateOrganizationW
UPDATE
organizations
SET
workspace_sharing_disabled = $1,
shareable_workspace_owners = $1,
updated_at = $2
WHERE
id = $3
RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, workspace_sharing_disabled
RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
`
type UpdateOrganizationWorkspaceSharingSettingsParams struct {
WorkspaceSharingDisabled bool `db:"workspace_sharing_disabled" json:"workspace_sharing_disabled"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
ShareableWorkspaceOwners ShareableWorkspaceOwners `db:"shareable_workspace_owners" json:"shareable_workspace_owners"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateOrganizationWorkspaceSharingSettings(ctx context.Context, arg UpdateOrganizationWorkspaceSharingSettingsParams) (Organization, error) {
row := q.db.QueryRowContext(ctx, updateOrganizationWorkspaceSharingSettings, arg.WorkspaceSharingDisabled, arg.UpdatedAt, arg.ID)
row := q.db.QueryRowContext(ctx, updateOrganizationWorkspaceSharingSettings, arg.ShareableWorkspaceOwners, arg.UpdatedAt, arg.ID)
var i Organization
err := row.Scan(
&i.ID,
@@ -11454,7 +11454,7 @@ func (q *sqlQuerier) UpdateOrganizationWorkspaceSharingSettings(ctx context.Cont
&i.DisplayName,
&i.Icon,
&i.Deleted,
&i.WorkspaceSharingDisabled,
&i.ShareableWorkspaceOwners,
)
return i, err
}
@@ -19373,9 +19373,21 @@ SELECT
array_agg(org_roles || ':' || organization_members.organization_id::text)
FROM
organization_members,
-- All org_members get the organization-member role for their orgs
-- All org members get an implied role for their orgs. Most members
-- get organization-member, but service accounts will get
-- organization-service-account instead. They're largely the same,
-- but having them be distinct means we can allow configuring
-- service-accounts to have slightly broader permissions–such as
-- for workspace sharing.
unnest(
array_append(roles, 'organization-member')
array_append(
roles,
CASE WHEN users.is_service_account THEN
'organization-service-account'
ELSE
'organization-member'
END
)
) AS org_roles
WHERE
user_id = users.id
@@ -25466,10 +25478,21 @@ SET
user_acl = '{}'::jsonb
WHERE
organization_id = $1
AND (
NOT $2::boolean
OR owner_id NOT IN (
SELECT id FROM users WHERE is_service_account = true
)
)
`
func (q *sqlQuerier) DeleteWorkspaceACLsByOrganization(ctx context.Context, organizationID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteWorkspaceACLsByOrganization, organizationID)
type DeleteWorkspaceACLsByOrganizationParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
ExcludeServiceAccounts bool `db:"exclude_service_accounts" json:"exclude_service_accounts"`
}
func (q *sqlQuerier) DeleteWorkspaceACLsByOrganization(ctx context.Context, arg DeleteWorkspaceACLsByOrganizationParams) error {
_, err := q.db.ExecContext(ctx, deleteWorkspaceACLsByOrganization, arg.OrganizationID, arg.ExcludeServiceAccounts)
return err
}