mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add workspace sharing page (#19107)
This commit is contained in:
Generated
+76
-1
@@ -5289,7 +5289,7 @@ const docTemplate = `{
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update template request",
|
||||
"description": "Update template ACL request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -9942,6 +9942,50 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/acl": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Workspaces"
|
||||
],
|
||||
"summary": "Update workspace ACL",
|
||||
"operationId": "update-workspace-acl",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update workspace ACL request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateWorkspaceACL"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/autostart": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -17233,6 +17277,24 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateWorkspaceACL": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group_roles": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceRole"
|
||||
}
|
||||
},
|
||||
"user_roles": {
|
||||
"description": "Keys must be valid UUIDs. To remove a user/group from the ACL use \"\" as the\nrole name (available as a constant named ` + "`" + `codersdk.WorkspaceRoleDeleted` + "`" + `)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceRole"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateWorkspaceAutomaticUpdatesRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -18965,6 +19027,19 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.WorkspaceRole": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"admin",
|
||||
"use",
|
||||
""
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"WorkspaceRoleAdmin",
|
||||
"WorkspaceRoleUse",
|
||||
"WorkspaceRoleDeleted"
|
||||
]
|
||||
},
|
||||
"codersdk.WorkspaceStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
Generated
+66
-1
@@ -4658,7 +4658,7 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update template request",
|
||||
"description": "Update template ACL request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -8792,6 +8792,44 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/acl": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Workspaces"],
|
||||
"summary": "Update workspace ACL",
|
||||
"operationId": "update-workspace-acl",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update workspace ACL request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateWorkspaceACL"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/autostart": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -15731,6 +15769,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateWorkspaceACL": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group_roles": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceRole"
|
||||
}
|
||||
},
|
||||
"user_roles": {
|
||||
"description": "Keys must be valid UUIDs. To remove a user/group from the ACL use \"\" as the\nrole name (available as a constant named `codersdk.WorkspaceRoleDeleted`)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceRole"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateWorkspaceAutomaticUpdatesRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -17363,6 +17419,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.WorkspaceRole": {
|
||||
"type": "string",
|
||||
"enum": ["admin", "use", ""],
|
||||
"x-enum-varnames": [
|
||||
"WorkspaceRoleAdmin",
|
||||
"WorkspaceRoleUse",
|
||||
"WorkspaceRoleDeleted"
|
||||
]
|
||||
},
|
||||
"codersdk.WorkspaceStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -1413,6 +1413,12 @@ func New(options *Options) *API {
|
||||
r.Delete("/", api.deleteWorkspaceAgentPortShare)
|
||||
})
|
||||
r.Get("/timings", api.workspaceTimings)
|
||||
r.Route("/acl", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentWorkspaceSharing))
|
||||
|
||||
r.Patch("/", api.patchWorkspaceACL)
|
||||
})
|
||||
})
|
||||
})
|
||||
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
|
||||
|
||||
@@ -360,7 +360,8 @@ func assertProduce(t *testing.T, comment SwaggerComment) {
|
||||
(comment.router == "/workspaceagents/me/startup/logs" && comment.method == "patch") ||
|
||||
(comment.router == "/licenses/{id}" && comment.method == "delete") ||
|
||||
(comment.router == "/debug/coordinator" && comment.method == "get") ||
|
||||
(comment.router == "/debug/tailnet" && comment.method == "get") {
|
||||
(comment.router == "/debug/tailnet" && comment.method == "get") ||
|
||||
(comment.router == "/workspaces/{workspace}/acl" && comment.method == "patch") {
|
||||
return // Exception: HTTP 200 is returned without response entity
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -15,14 +15,18 @@ var (
|
||||
_ sqltypes.Node = ACLMappingVar{}
|
||||
)
|
||||
|
||||
// ACLMappingVar is a variable matcher that handles group_acl and user_acl.
|
||||
// The sql type is a jsonb object with the following structure:
|
||||
// ACLMappingVar is a variable matcher that matches ACL map variables to their
|
||||
// SQL storage. Usually the actual backing implementation is a pair of `jsonb`
|
||||
// columns named `group_acl` and `user_acl`. Each column contains an object that
|
||||
// looks like...
|
||||
//
|
||||
// "group_acl": {
|
||||
// "<group_name>": ["<actions>"]
|
||||
// ```json
|
||||
//
|
||||
// {
|
||||
// "<actor_id>": ["<action>", "<action>"]
|
||||
// }
|
||||
//
|
||||
// This is a custom variable matcher as json objects have arbitrary complexity.
|
||||
// ```
|
||||
type ACLMappingVar struct {
|
||||
// SelectSQL is used to `SELECT` the ACL mapping from the table for the
|
||||
// given resource. ie. if the full query might look like `SELECT group_acl
|
||||
@@ -59,9 +63,10 @@ func (g ACLMappingVar) UsingSubfield(subfield string) ACLMappingVar {
|
||||
func (ACLMappingVar) UseAs() sqltypes.Node { return ACLMappingVar{} }
|
||||
|
||||
func (g ACLMappingVar) ConvertVariable(rego ast.Ref) (sqltypes.Node, bool) {
|
||||
// "left" will be a map of group names to actions in rego.
|
||||
// left is the rego variable that maps the actor's id to the actions they
|
||||
// are allowed to take.
|
||||
// {
|
||||
// "all_users": ["read"]
|
||||
// "<actor_id>": ["<action>", "<action>"]
|
||||
// }
|
||||
left, err := sqltypes.RegoVarPath(g.StructPath, rego)
|
||||
if err != nil {
|
||||
|
||||
@@ -2041,6 +2041,104 @@ func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, timings)
|
||||
}
|
||||
|
||||
// @Summary Update workspace ACL
|
||||
// @ID update-workspace-acl
|
||||
// @Security CoderSessionToken
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Param request body codersdk.UpdateWorkspaceACL true "Update workspace ACL request"
|
||||
// @Success 204
|
||||
// @Router /workspaces/{workspace}/acl [patch]
|
||||
func (api *API) patchWorkspaceACL(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
workspace = httpmw.WorkspaceParam(r)
|
||||
auditor = api.Auditor.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.WorkspaceTable](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionWrite,
|
||||
OrganizationID: workspace.OrganizationID,
|
||||
})
|
||||
)
|
||||
defer commitAudit()
|
||||
aReq.Old = workspace.WorkspaceTable()
|
||||
|
||||
var req codersdk.UpdateWorkspaceACL
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
validErrs := validateWorkspaceACLPerms(ctx, api.Database, req.UserRoles, "user_roles")
|
||||
validErrs = append(validErrs, validateWorkspaceACLPerms(
|
||||
ctx,
|
||||
api.Database,
|
||||
req.GroupRoles,
|
||||
"group_roles",
|
||||
)...)
|
||||
|
||||
if len(validErrs) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid request to update template metadata!",
|
||||
Validations: validErrs,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := api.Database.InTx(func(tx database.Store) error {
|
||||
var err error
|
||||
workspace, err = tx.GetWorkspaceByID(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template by ID: %w", err)
|
||||
}
|
||||
|
||||
for id, role := range req.UserRoles {
|
||||
if role == codersdk.WorkspaceRoleDeleted {
|
||||
delete(workspace.UserACL, id)
|
||||
continue
|
||||
}
|
||||
workspace.UserACL[id] = database.WorkspaceACLEntry{
|
||||
Permissions: db2sdk.WorkspaceRoleActions(role),
|
||||
}
|
||||
}
|
||||
|
||||
for id, role := range req.GroupRoles {
|
||||
if role == codersdk.WorkspaceRoleDeleted {
|
||||
delete(workspace.GroupACL, id)
|
||||
continue
|
||||
}
|
||||
workspace.GroupACL[id] = database.WorkspaceACLEntry{
|
||||
Permissions: db2sdk.WorkspaceRoleActions(role),
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.UpdateWorkspaceACLByID(ctx, database.UpdateWorkspaceACLByIDParams{
|
||||
ID: workspace.ID,
|
||||
UserACL: workspace.UserACL,
|
||||
GroupACL: workspace.GroupACL,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update workspace ACL by ID: %w", err)
|
||||
}
|
||||
workspace, err = tx.GetWorkspaceByID(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get updated workspace by ID: %w", err)
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
aReq.New = workspace.WorkspaceTable()
|
||||
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type workspaceData struct {
|
||||
templates []database.Template
|
||||
builds []codersdk.WorkspaceBuild
|
||||
@@ -2379,3 +2477,64 @@ func (api *API) publishWorkspaceAgentLogsUpdate(ctx context.Context, workspaceAg
|
||||
api.Logger.Warn(ctx, "failed to publish workspace agent logs update", slog.F("workspace_agent_id", workspaceAgentID), slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func validateWorkspaceACLPerms(ctx context.Context, db database.Store, perms map[string]codersdk.WorkspaceRole, field string) []codersdk.ValidationError {
|
||||
// nolint:gocritic // Validate requires full read access to users and groups
|
||||
ctx = dbauthz.AsSystemRestricted(ctx)
|
||||
var validErrs []codersdk.ValidationError
|
||||
for idStr, role := range perms {
|
||||
if err := validateWorkspaceRole(role); err != nil {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: field, Detail: err.Error()})
|
||||
continue
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: field, Detail: idStr + "is not a valid UUID."})
|
||||
continue
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "user_roles":
|
||||
// TODO(lilac): put this back after Kirby button shenanigans are over
|
||||
// This could get slow if we get a ton of user perm updates.
|
||||
// _, err = db.GetUserByID(ctx, id)
|
||||
// if err != nil {
|
||||
// validErrs = append(validErrs, codersdk.ValidationError{Field: field, Detail: fmt.Sprintf("Failed to find resource with ID %q: %v", idStr, err.Error())})
|
||||
// continue
|
||||
// }
|
||||
case "group_roles":
|
||||
// This could get slow if we get a ton of group perm updates.
|
||||
_, err = db.GetGroupByID(ctx, id)
|
||||
if err != nil {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: field, Detail: fmt.Sprintf("Failed to find resource with ID %q: %v", idStr, err.Error())})
|
||||
continue
|
||||
}
|
||||
default:
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: field, Detail: "invalid field"})
|
||||
}
|
||||
}
|
||||
|
||||
return validErrs
|
||||
}
|
||||
|
||||
func validateWorkspaceRole(role codersdk.WorkspaceRole) error {
|
||||
actions := db2sdk.WorkspaceRoleActions(role)
|
||||
if len(actions) == 0 && role != codersdk.WorkspaceRoleDeleted {
|
||||
return xerrors.Errorf("role %q is not a valid Workspace role", role)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: This will go here
|
||||
// func convertToWorkspaceRole(actions []policy.Action) codersdk.TemplateRole {
|
||||
// switch {
|
||||
// case len(actions) == 2 && slice.SameElements(actions, []policy.Action{policy.ActionUse, policy.ActionRead}):
|
||||
// return codersdk.TemplateRoleUse
|
||||
// case len(actions) == 1 && actions[0] == policy.WildcardSymbol:
|
||||
// return codersdk.TemplateRoleAdmin
|
||||
// }
|
||||
|
||||
// return ""
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user