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,