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
+159
View File
@@ -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 ""
// }