feat: add new workspace:share action type (#20198)

Closes
[coder/internal#1012](https://github.com/coder/internal/issues/1012)
This commit is contained in:
Brett Kolodny
2025-10-20 18:28:10 -04:00
committed by GitHub
parent 66f1603f6a
commit b022ccefa7
18 changed files with 101 additions and 10 deletions
+6
View File
@@ -11971,6 +11971,7 @@ const docTemplate = `{
"workspace:delete",
"workspace:delete_agent",
"workspace:read",
"workspace:share",
"workspace:ssh",
"workspace:start",
"workspace:stop",
@@ -11988,6 +11989,7 @@ const docTemplate = `{
"workspace_dormant:delete",
"workspace_dormant:delete_agent",
"workspace_dormant:read",
"workspace_dormant:share",
"workspace_dormant:ssh",
"workspace_dormant:start",
"workspace_dormant:stop",
@@ -12167,6 +12169,7 @@ const docTemplate = `{
"APIKeyScopeWorkspaceDelete",
"APIKeyScopeWorkspaceDeleteAgent",
"APIKeyScopeWorkspaceRead",
"APIKeyScopeWorkspaceShare",
"APIKeyScopeWorkspaceSsh",
"APIKeyScopeWorkspaceStart",
"APIKeyScopeWorkspaceStop",
@@ -12184,6 +12187,7 @@ const docTemplate = `{
"APIKeyScopeWorkspaceDormantDelete",
"APIKeyScopeWorkspaceDormantDeleteAgent",
"APIKeyScopeWorkspaceDormantRead",
"APIKeyScopeWorkspaceDormantShare",
"APIKeyScopeWorkspaceDormantSsh",
"APIKeyScopeWorkspaceDormantStart",
"APIKeyScopeWorkspaceDormantStop",
@@ -16926,6 +16930,7 @@ const docTemplate = `{
"read",
"read_personal",
"ssh",
"share",
"unassign",
"update",
"update_personal",
@@ -16944,6 +16949,7 @@ const docTemplate = `{
"ActionRead",
"ActionReadPersonal",
"ActionSSH",
"ActionShare",
"ActionUnassign",
"ActionUpdate",
"ActionUpdatePersonal",
+6
View File
@@ -10675,6 +10675,7 @@
"workspace:delete",
"workspace:delete_agent",
"workspace:read",
"workspace:share",
"workspace:ssh",
"workspace:start",
"workspace:stop",
@@ -10692,6 +10693,7 @@
"workspace_dormant:delete",
"workspace_dormant:delete_agent",
"workspace_dormant:read",
"workspace_dormant:share",
"workspace_dormant:ssh",
"workspace_dormant:start",
"workspace_dormant:stop",
@@ -10871,6 +10873,7 @@
"APIKeyScopeWorkspaceDelete",
"APIKeyScopeWorkspaceDeleteAgent",
"APIKeyScopeWorkspaceRead",
"APIKeyScopeWorkspaceShare",
"APIKeyScopeWorkspaceSsh",
"APIKeyScopeWorkspaceStart",
"APIKeyScopeWorkspaceStop",
@@ -10888,6 +10891,7 @@
"APIKeyScopeWorkspaceDormantDelete",
"APIKeyScopeWorkspaceDormantDeleteAgent",
"APIKeyScopeWorkspaceDormantRead",
"APIKeyScopeWorkspaceDormantShare",
"APIKeyScopeWorkspaceDormantSsh",
"APIKeyScopeWorkspaceDormantStart",
"APIKeyScopeWorkspaceDormantStop",
@@ -15448,6 +15452,7 @@
"read",
"read_personal",
"ssh",
"share",
"unassign",
"update",
"update_personal",
@@ -15466,6 +15471,7 @@
"ActionRead",
"ActionReadPersonal",
"ActionSSH",
"ActionShare",
"ActionUnassign",
"ActionUpdate",
"ActionUpdatePersonal",
+3 -3
View File
@@ -1792,7 +1792,7 @@ func (q *querier) DeleteWorkspaceACLByID(ctx context.Context, id uuid.UUID) erro
return w.WorkspaceTable(), nil
}
return fetchAndExec(q.log, q.auth, policy.ActionUpdate, fetch, q.db.DeleteWorkspaceACLByID)(ctx, id)
return fetchAndExec(q.log, q.auth, policy.ActionShare, fetch, q.db.DeleteWorkspaceACLByID)(ctx, id)
}
func (q *querier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg database.DeleteWorkspaceAgentPortShareParams) error {
@@ -3388,7 +3388,7 @@ func (q *querier) GetWorkspaceACLByID(ctx context.Context, id uuid.UUID) (databa
if err != nil {
return database.GetWorkspaceACLByIDRow{}, err
}
if err := q.authorizeContext(ctx, policy.ActionCreate, workspace); err != nil {
if err := q.authorizeContext(ctx, policy.ActionShare, workspace); err != nil {
return database.GetWorkspaceACLByIDRow{}, err
}
return q.db.GetWorkspaceACLByID(ctx, id)
@@ -5312,7 +5312,7 @@ func (q *querier) UpdateWorkspaceACLByID(ctx context.Context, arg database.Updat
return w.WorkspaceTable(), nil
}
return fetchAndExec(q.log, q.auth, policy.ActionCreate, fetch, q.db.UpdateWorkspaceACLByID)(ctx, arg)
return fetchAndExec(q.log, q.auth, policy.ActionShare, fetch, q.db.UpdateWorkspaceACLByID)(ctx, arg)
}
func (q *querier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg database.UpdateWorkspaceAgentConnectionByIDParams) error {
+3 -3
View File
@@ -1732,20 +1732,20 @@ func (s *MethodTestSuite) TestWorkspace() {
ws := testutil.Fake(s.T(), faker, database.Workspace{})
dbM.EXPECT().GetWorkspaceByID(gomock.Any(), ws.ID).Return(ws, nil).AnyTimes()
dbM.EXPECT().GetWorkspaceACLByID(gomock.Any(), ws.ID).Return(database.GetWorkspaceACLByIDRow{}, nil).AnyTimes()
check.Args(ws.ID).Asserts(ws, policy.ActionCreate)
check.Args(ws.ID).Asserts(ws, policy.ActionShare)
}))
s.Run("UpdateWorkspaceACLByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
w := testutil.Fake(s.T(), faker, database.Workspace{})
arg := database.UpdateWorkspaceACLByIDParams{ID: w.ID}
dbm.EXPECT().GetWorkspaceByID(gomock.Any(), w.ID).Return(w, nil).AnyTimes()
dbm.EXPECT().UpdateWorkspaceACLByID(gomock.Any(), arg).Return(nil).AnyTimes()
check.Args(arg).Asserts(w, policy.ActionCreate)
check.Args(arg).Asserts(w, policy.ActionShare)
}))
s.Run("DeleteWorkspaceACLByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
w := testutil.Fake(s.T(), faker, database.Workspace{})
dbm.EXPECT().GetWorkspaceByID(gomock.Any(), w.ID).Return(w, nil).AnyTimes()
dbm.EXPECT().DeleteWorkspaceACLByID(gomock.Any(), w.ID).Return(nil).AnyTimes()
check.Args(w.ID).Asserts(w, policy.ActionUpdate)
check.Args(w.ID).Asserts(w, policy.ActionShare)
}))
s.Run("GetLatestWorkspaceBuildByWorkspaceID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
w := testutil.Fake(s.T(), faker, database.Workspace{})
+3 -1
View File
@@ -202,7 +202,9 @@ CREATE TYPE api_key_scope AS ENUM (
'task:read',
'task:update',
'task:delete',
'task:*'
'task:*',
'workspace:share',
'workspace_dormant:share'
);
CREATE TYPE app_sharing_level AS ENUM (
@@ -0,0 +1,3 @@
-- No-op: keep enum values to avoid dependency churn.
-- If strict removal is required, create a new enum type without these values,
-- cast columns, drop the old type, and rename.
@@ -0,0 +1,2 @@
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:share';
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:share';
+7 -1
View File
@@ -211,6 +211,8 @@ const (
ApiKeyScopeTaskUpdate APIKeyScope = "task:update"
ApiKeyScopeTaskDelete APIKeyScope = "task:delete"
ApiKeyScopeTask APIKeyScope = "task:*"
ApiKeyScopeWorkspaceShare APIKeyScope = "workspace:share"
ApiKeyScopeWorkspaceDormantShare APIKeyScope = "workspace_dormant:share"
)
func (e *APIKeyScope) Scan(src interface{}) error {
@@ -441,7 +443,9 @@ func (e APIKeyScope) Valid() bool {
ApiKeyScopeTaskRead,
ApiKeyScopeTaskUpdate,
ApiKeyScopeTaskDelete,
ApiKeyScopeTask:
ApiKeyScopeTask,
ApiKeyScopeWorkspaceShare,
ApiKeyScopeWorkspaceDormantShare:
return true
}
return false
@@ -641,6 +645,8 @@ func AllAPIKeyScopeValues() []APIKeyScope {
ApiKeyScopeTaskUpdate,
ApiKeyScopeTaskDelete,
ApiKeyScopeTask,
ApiKeyScopeWorkspaceShare,
ApiKeyScopeWorkspaceDormantShare,
}
}
+3
View File
@@ -356,6 +356,7 @@ var (
// - "ActionDelete" :: delete workspace
// - "ActionDeleteAgent" :: delete an existing workspace agent
// - "ActionRead" :: read workspace data to view on the UI
// - "ActionShare" :: share a workspace with other users or groups
// - "ActionSSH" :: ssh into a given workspace
// - "ActionWorkspaceStart" :: allows starting a workspace
// - "ActionWorkspaceStop" :: allows stopping a workspace
@@ -388,6 +389,7 @@ var (
// - "ActionDelete" :: delete workspace
// - "ActionDeleteAgent" :: delete an existing workspace agent
// - "ActionRead" :: read workspace data to view on the UI
// - "ActionShare" :: share a workspace with other users or groups
// - "ActionSSH" :: ssh into a given workspace
// - "ActionWorkspaceStart" :: allows starting a workspace
// - "ActionWorkspaceStop" :: allows stopping a workspace
@@ -465,6 +467,7 @@ func AllActions() []policy.Action {
policy.ActionRead,
policy.ActionReadPersonal,
policy.ActionSSH,
policy.ActionShare,
policy.ActionUnassign,
policy.ActionUpdate,
policy.ActionUpdatePersonal,
+5
View File
@@ -27,6 +27,8 @@ const (
ActionCreateAgent Action = "create_agent"
ActionDeleteAgent Action = "delete_agent"
ActionShare Action = "share"
)
type PermissionDefinition struct {
@@ -61,6 +63,9 @@ var workspaceActions = map[Action]ActionDefinition{
ActionCreateAgent: "create a new workspace agent",
ActionDeleteAgent: "delete an existing workspace agent",
// Sharing a workspace
ActionShare: "share a workspace with other users or groups",
}
var taskActions = map[Action]ActionDefinition{
+33
View File
@@ -235,6 +235,39 @@ func TestRolePermissions(t *testing.T) {
false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgMemberMeBanWorkspace},
},
},
{
Name: "ShareMyWorkspace",
Actions: []policy.Action{policy.ActionShare},
Resource: rbac.ResourceWorkspace.
WithID(workspaceID).
InOrg(orgID).
WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgMemberMe, orgAdmin, orgMemberMeBanWorkspace},
false: {
memberMe, setOtherOrg,
templateAdmin, userAdmin,
orgTemplateAdmin, orgUserAdmin, orgAuditor,
},
},
},
{
Name: "ShareWorkspaceDormant",
Actions: []policy.Action{policy.ActionShare},
Resource: rbac.ResourceWorkspaceDormant.
WithID(uuid.New()).
InOrg(orgID).
WithOwner(memberMe.Actor.ID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {},
false: {
orgMemberMe, orgAdmin, owner, setOtherOrg,
userAdmin, memberMe,
templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor,
orgMemberMeBanWorkspace,
},
},
},
{
Name: "Templates",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
+6
View File
@@ -127,6 +127,7 @@ const (
ScopeWorkspaceDelete ScopeName = "workspace:delete"
ScopeWorkspaceDeleteAgent ScopeName = "workspace:delete_agent"
ScopeWorkspaceRead ScopeName = "workspace:read"
ScopeWorkspaceShare ScopeName = "workspace:share"
ScopeWorkspaceSsh ScopeName = "workspace:ssh"
ScopeWorkspaceStart ScopeName = "workspace:start"
ScopeWorkspaceStop ScopeName = "workspace:stop"
@@ -141,6 +142,7 @@ const (
ScopeWorkspaceDormantDelete ScopeName = "workspace_dormant:delete"
ScopeWorkspaceDormantDeleteAgent ScopeName = "workspace_dormant:delete_agent"
ScopeWorkspaceDormantRead ScopeName = "workspace_dormant:read"
ScopeWorkspaceDormantShare ScopeName = "workspace_dormant:share"
ScopeWorkspaceDormantSsh ScopeName = "workspace_dormant:ssh"
ScopeWorkspaceDormantStart ScopeName = "workspace_dormant:start"
ScopeWorkspaceDormantStop ScopeName = "workspace_dormant:stop"
@@ -280,6 +282,7 @@ func (e ScopeName) Valid() bool {
ScopeWorkspaceDelete,
ScopeWorkspaceDeleteAgent,
ScopeWorkspaceRead,
ScopeWorkspaceShare,
ScopeWorkspaceSsh,
ScopeWorkspaceStart,
ScopeWorkspaceStop,
@@ -294,6 +297,7 @@ func (e ScopeName) Valid() bool {
ScopeWorkspaceDormantDelete,
ScopeWorkspaceDormantDeleteAgent,
ScopeWorkspaceDormantRead,
ScopeWorkspaceDormantShare,
ScopeWorkspaceDormantSsh,
ScopeWorkspaceDormantStart,
ScopeWorkspaceDormantStop,
@@ -434,6 +438,7 @@ func AllScopeNameValues() []ScopeName {
ScopeWorkspaceDelete,
ScopeWorkspaceDeleteAgent,
ScopeWorkspaceRead,
ScopeWorkspaceShare,
ScopeWorkspaceSsh,
ScopeWorkspaceStart,
ScopeWorkspaceStop,
@@ -448,6 +453,7 @@ func AllScopeNameValues() []ScopeName {
ScopeWorkspaceDormantDelete,
ScopeWorkspaceDormantDeleteAgent,
ScopeWorkspaceDormantRead,
ScopeWorkspaceDormantShare,
ScopeWorkspaceDormantSsh,
ScopeWorkspaceDormantStart,
ScopeWorkspaceDormantStop,
+2
View File
@@ -172,6 +172,7 @@ const (
APIKeyScopeWorkspaceDelete APIKeyScope = "workspace:delete"
APIKeyScopeWorkspaceDeleteAgent APIKeyScope = "workspace:delete_agent"
APIKeyScopeWorkspaceRead APIKeyScope = "workspace:read"
APIKeyScopeWorkspaceShare APIKeyScope = "workspace:share"
APIKeyScopeWorkspaceSsh APIKeyScope = "workspace:ssh"
APIKeyScopeWorkspaceStart APIKeyScope = "workspace:start"
APIKeyScopeWorkspaceStop APIKeyScope = "workspace:stop"
@@ -189,6 +190,7 @@ const (
APIKeyScopeWorkspaceDormantDelete APIKeyScope = "workspace_dormant:delete"
APIKeyScopeWorkspaceDormantDeleteAgent APIKeyScope = "workspace_dormant:delete_agent"
APIKeyScopeWorkspaceDormantRead APIKeyScope = "workspace_dormant:read"
APIKeyScopeWorkspaceDormantShare APIKeyScope = "workspace_dormant:share"
APIKeyScopeWorkspaceDormantSsh APIKeyScope = "workspace_dormant:ssh"
APIKeyScopeWorkspaceDormantStart APIKeyScope = "workspace_dormant:start"
APIKeyScopeWorkspaceDormantStop APIKeyScope = "workspace_dormant:stop"
+3 -2
View File
@@ -60,6 +60,7 @@ const (
ActionRead RBACAction = "read"
ActionReadPersonal RBACAction = "read_personal"
ActionSSH RBACAction = "ssh"
ActionShare RBACAction = "share"
ActionUnassign RBACAction = "unassign"
ActionUpdate RBACAction = "update"
ActionUpdatePersonal RBACAction = "update_personal"
@@ -109,9 +110,9 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal},
ResourceUserSecret: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceWebpushSubscription: {ActionCreate, ActionDelete, ActionRead},
ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionCreateAgent, ActionDelete, ActionDeleteAgent, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionCreateAgent, ActionDelete, ActionDeleteAgent, ActionRead, ActionShare, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
ResourceWorkspaceAgentDevcontainers: {ActionCreate},
ResourceWorkspaceAgentResourceMonitor: {ActionCreate, ActionRead, ActionUpdate},
ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionCreateAgent, ActionDelete, ActionDeleteAgent, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionCreateAgent, ActionDelete, ActionDeleteAgent, ActionRead, ActionShare, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
ResourceWorkspaceProxy: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
}
+5
View File
@@ -175,6 +175,7 @@ Status Code **200**
| `action` | `read` |
| `action` | `read_personal` |
| `action` | `ssh` |
| `action` | `share` |
| `action` | `unassign` |
| `action` | `update` |
| `action` | `update_personal` |
@@ -349,6 +350,7 @@ Status Code **200**
| `action` | `read` |
| `action` | `read_personal` |
| `action` | `ssh` |
| `action` | `share` |
| `action` | `unassign` |
| `action` | `update` |
| `action` | `update_personal` |
@@ -523,6 +525,7 @@ Status Code **200**
| `action` | `read` |
| `action` | `read_personal` |
| `action` | `ssh` |
| `action` | `share` |
| `action` | `unassign` |
| `action` | `update` |
| `action` | `update_personal` |
@@ -666,6 +669,7 @@ Status Code **200**
| `action` | `read` |
| `action` | `read_personal` |
| `action` | `ssh` |
| `action` | `share` |
| `action` | `unassign` |
| `action` | `update` |
| `action` | `update_personal` |
@@ -1031,6 +1035,7 @@ Status Code **200**
| `action` | `read` |
| `action` | `read_personal` |
| `action` | `ssh` |
| `action` | `share` |
| `action` | `unassign` |
| `action` | `update` |
| `action` | `update_personal` |
+3
View File
@@ -953,6 +953,7 @@
| `workspace:delete` |
| `workspace:delete_agent` |
| `workspace:read` |
| `workspace:share` |
| `workspace:ssh` |
| `workspace:start` |
| `workspace:stop` |
@@ -970,6 +971,7 @@
| `workspace_dormant:delete` |
| `workspace_dormant:delete_agent` |
| `workspace_dormant:read` |
| `workspace_dormant:share` |
| `workspace_dormant:ssh` |
| `workspace_dormant:start` |
| `workspace_dormant:stop` |
@@ -7069,6 +7071,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
| `read` |
| `read_personal` |
| `ssh` |
| `share` |
| `unassign` |
| `update` |
| `update_personal` |
+2
View File
@@ -201,6 +201,7 @@ export const RBACResourceActions: Partial<
delete: "delete workspace",
delete_agent: "delete an existing workspace agent",
read: "read workspace data to view on the UI",
share: "share a workspace with other users or groups",
ssh: "ssh into a given workspace",
start: "allows starting a workspace",
stop: "allows stopping a workspace",
@@ -221,6 +222,7 @@ export const RBACResourceActions: Partial<
delete: "delete workspace",
delete_agent: "delete an existing workspace agent",
read: "read workspace data to view on the UI",
share: "share a workspace with other users or groups",
ssh: "ssh into a given workspace",
start: "allows starting a workspace",
stop: "allows stopping a workspace",
+6
View File
@@ -324,6 +324,7 @@ export type APIKeyScope =
| "workspace_dormant:delete"
| "workspace_dormant:delete_agent"
| "workspace_dormant:read"
| "workspace_dormant:share"
| "workspace_dormant:ssh"
| "workspace_dormant:start"
| "workspace_dormant:stop"
@@ -334,6 +335,7 @@ export type APIKeyScope =
| "workspace_proxy:read"
| "workspace_proxy:update"
| "workspace:read"
| "workspace:share"
| "workspace:ssh"
| "workspace:start"
| "workspace:stop"
@@ -520,6 +522,7 @@ export const APIKeyScopes: APIKeyScope[] = [
"workspace_dormant:delete",
"workspace_dormant:delete_agent",
"workspace_dormant:read",
"workspace_dormant:share",
"workspace_dormant:ssh",
"workspace_dormant:start",
"workspace_dormant:stop",
@@ -530,6 +533,7 @@ export const APIKeyScopes: APIKeyScope[] = [
"workspace_proxy:read",
"workspace_proxy:update",
"workspace:read",
"workspace:share",
"workspace:ssh",
"workspace:start",
"workspace:stop",
@@ -3812,6 +3816,7 @@ export type RBACAction =
| "read"
| "read_personal"
| "ssh"
| "share"
| "unassign"
| "update"
| "update_personal"
@@ -3830,6 +3835,7 @@ export const RBACActions: RBACAction[] = [
"read",
"read_personal",
"ssh",
"share",
"unassign",
"update",
"update_personal",