feat: add workspace sharing page (#19107)

This commit is contained in:
ケイラ
2025-07-31 15:05:09 +00:00
committed by GitHub
parent ddb5b87815
commit 1cffd11619
28 changed files with 668 additions and 52 deletions
+24
View File
@@ -24,6 +24,7 @@ import (
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/render"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionersdk/proto"
@@ -781,6 +782,29 @@ func TemplateRoleActions(role codersdk.TemplateRole) []policy.Action {
return []policy.Action{}
}
func WorkspaceRoleActions(role codersdk.WorkspaceRole) []policy.Action {
switch role {
case codersdk.WorkspaceRoleAdmin:
return slice.Omit(
// Small note: This intentionally includes "create" because it's sort of
// double purposed as "can edit ACL". That's maybe a bit "incorrect", but
// it's what templates do already and we're copying that implementation.
rbac.ResourceWorkspace.AvailableActions(),
// Don't let anyone delete something they can't recreate.
policy.ActionDelete,
)
case codersdk.WorkspaceRoleUse:
return []policy.Action{
policy.ActionApplicationConnect,
policy.ActionRead,
policy.ActionSSH,
policy.ActionWorkspaceStart,
policy.ActionWorkspaceStop,
}
}
return []policy.Action{}
}
func ConnectionLogConnectionTypeFromAgentProtoConnectionType(typ agentproto.Connection_Type) (database.ConnectionType, error) {
switch typ {
case agentproto.Connection_SSH:
+12
View File
@@ -4919,6 +4919,18 @@ func (q *querier) UpdateWorkspace(ctx context.Context, arg database.UpdateWorksp
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateWorkspace)(ctx, arg)
}
func (q *querier) UpdateWorkspaceACLByID(ctx context.Context, arg database.UpdateWorkspaceACLByIDParams) error {
fetch := func(ctx context.Context, arg database.UpdateWorkspaceACLByIDParams) (database.WorkspaceTable, error) {
w, err := q.db.GetWorkspaceByID(ctx, arg.ID)
if err != nil {
return database.WorkspaceTable{}, err
}
return w.WorkspaceTable(), nil
}
return fetchAndExec(q.log, q.auth, policy.ActionCreate, fetch, q.db.UpdateWorkspaceACLByID)(ctx, arg)
}
func (q *querier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg database.UpdateWorkspaceAgentConnectionByIDParams) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
return err
+16
View File
@@ -2146,6 +2146,22 @@ func (s *MethodTestSuite) TestWorkspace() {
// no asserts here because SQLFilter
check.Args([]uuid.UUID{}, emptyPreparedAuthorized{}).Asserts()
}))
s.Run("UpdateWorkspaceACLByID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
o := dbgen.Organization(s.T(), db, database.Organization{})
tpl := dbgen.Template(s.T(), db, database.Template{
OrganizationID: o.ID,
CreatedBy: u.ID,
})
ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{
OwnerID: u.ID,
OrganizationID: o.ID,
TemplateID: tpl.ID,
})
check.Args(database.UpdateWorkspaceACLByIDParams{
ID: ws.ID,
}).Asserts(ws, policy.ActionCreate)
}))
s.Run("GetLatestWorkspaceBuildByWorkspaceID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
o := dbgen.Organization(s.T(), db, database.Organization{})
@@ -3029,6 +3029,13 @@ func (m queryMetricsStore) UpdateWorkspace(ctx context.Context, arg database.Upd
return workspace, err
}
func (m queryMetricsStore) UpdateWorkspaceACLByID(ctx context.Context, arg database.UpdateWorkspaceACLByIDParams) error {
start := time.Now()
r0 := m.s.UpdateWorkspaceACLByID(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateWorkspaceACLByID").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg database.UpdateWorkspaceAgentConnectionByIDParams) error {
start := time.Now()
err := m.s.UpdateWorkspaceAgentConnectionByID(ctx, arg)
+14
View File
@@ -6461,6 +6461,20 @@ func (mr *MockStoreMockRecorder) UpdateWorkspace(ctx, arg any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspace", reflect.TypeOf((*MockStore)(nil).UpdateWorkspace), ctx, arg)
}
// UpdateWorkspaceACLByID mocks base method.
func (m *MockStore) UpdateWorkspaceACLByID(ctx context.Context, arg database.UpdateWorkspaceACLByIDParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspaceACLByID", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateWorkspaceACLByID indicates an expected call of UpdateWorkspaceACLByID.
func (mr *MockStoreMockRecorder) UpdateWorkspaceACLByID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceACLByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceACLByID), ctx, arg)
}
// UpdateWorkspaceAgentConnectionByID mocks base method.
func (m *MockStore) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg database.UpdateWorkspaceAgentConnectionByIDParams) error {
m.ctrl.T.Helper()
+3 -1
View File
@@ -276,7 +276,9 @@ func (w WorkspaceTable) RBACObject() rbac.Object {
return rbac.ResourceWorkspace.WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
WithOwner(w.OwnerID.String()).
WithGroupACL(w.GroupACL.RBACACL()).
WithACLUserList(w.UserACL.RBACACL())
}
func (w WorkspaceTable) DormantRBAC() rbac.Object {
+1
View File
@@ -628,6 +628,7 @@ type sqlcQuerier interface {
UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error)
UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error
UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error)
UpdateWorkspaceACLByID(ctx context.Context, arg UpdateWorkspaceACLByIDParams) error
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error
UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentLogOverflowByIDParams) error
+21
View File
@@ -20872,6 +20872,27 @@ func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspacePar
return i, err
}
const updateWorkspaceACLByID = `-- name: UpdateWorkspaceACLByID :exec
UPDATE
workspaces
SET
group_acl = $1,
user_acl = $2
WHERE
id = $3
`
type UpdateWorkspaceACLByIDParams struct {
GroupACL WorkspaceACL `db:"group_acl" json:"group_acl"`
UserACL WorkspaceACL `db:"user_acl" json:"user_acl"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateWorkspaceACLByID(ctx context.Context, arg UpdateWorkspaceACLByIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceACLByID, arg.GroupACL, arg.UserACL, arg.ID)
return err
}
const updateWorkspaceAutomaticUpdates = `-- name: UpdateWorkspaceAutomaticUpdates :exec
UPDATE
workspaces
+9
View File
@@ -873,3 +873,12 @@ GROUP BY workspaces.id, workspaces.name, latest_build.job_status, latest_build.j
-- name: GetWorkspacesByTemplateID :many
SELECT * FROM workspaces WHERE template_id = $1 AND deleted = false;
-- name: UpdateWorkspaceACLByID :exec
UPDATE
workspaces
SET
group_acl = @group_acl,
user_acl = @user_acl
WHERE
id = @id;
+11
View File
@@ -91,6 +91,17 @@ func (t *WorkspaceACL) Scan(src interface{}) error {
return xerrors.Errorf("unexpected type %T", src)
}
//nolint:revive
func (w WorkspaceACL) RBACACL() map[string][]policy.Action {
// Convert WorkspaceACL to a map of string to []policy.Action.
// This is used for RBAC checks.
rbacACL := make(map[string][]policy.Action, len(w))
for id, entry := range w {
rbacACL[id] = entry.Permissions
}
return rbacACL
}
func (t WorkspaceACL) Value() (driver.Value, error) {
return json.Marshal(t)
}