Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: addsharing remove command to the CLI#19767

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
brettkolodny merged 8 commits intomainfrombrett/i861
Sep 11, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 220 additions & 83 deletionscli/sharing.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,8 @@ import (

"golang.org/x/xerrors"

"github.com/google/uuid"

"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
Expand All@@ -15,8 +17,6 @@ import (
const defaultGroupDisplay = "-"

func (r *RootCmd) sharing() *serpent.Command {
orgContext := NewOrganizationContext()

cmd := &serpent.Command{
Use: "sharing [subcommand]",
Short: "Commands for managing shared workspaces",
Expand All@@ -25,13 +25,13 @@ func (r *RootCmd) sharing() *serpent.Command {
return inv.Command.HelpHandler(inv)
},
Children: []*serpent.Command{
r.shareWorkspace(orgContext),
r.shareWorkspace(),
r.unshareWorkspace(),
r.statusWorkspaceSharing(),
},
Hidden: true,
}

orgContext.AttachOptions(cmd)
return cmd
}

Expand DownExpand Up@@ -70,13 +70,14 @@ func (r *RootCmd) statusWorkspaceSharing() *serpent.Command {
return cmd
}

func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Command {
func (r *RootCmd) shareWorkspace() *serpent.Command {
var (
client = new(codersdk.Client)
users []string
groups []string

// Username regex taken from codersdk/name.go
nameRoleRegex = regexp.MustCompile(`(^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)+(?::([A-Za-z0-9-]+))?`)
client = new(codersdk.Client)
users []string
groups []string
)

cmd := &serpent.Command{
Expand DownExpand Up@@ -110,89 +111,130 @@ func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Comma
return xerrors.Errorf("could not fetch the workspace %s: %w", inv.Args[0], err)
}

org, err := orgContext.Selected(inv, client)
userRoleStrings := make([][2]string, len(users))
for index, user := range users {
userAndRole := nameRoleRegex.FindStringSubmatch(user)
if userAndRole == nil {
return xerrors.Errorf("invalid user format %q: must match pattern 'username:role'", user)
}

userRoleStrings[index] = [2]string{userAndRole[1], userAndRole[2]}
}

groupRoleStrings := make([][2]string, len(groups))
for index, group := range groups {
groupAndRole := nameRoleRegex.FindStringSubmatch(group)
if groupAndRole == nil {
return xerrors.Errorf("invalid group format %q: must match pattern 'group:role'", group)
}

groupRoleStrings[index] = [2]string{groupAndRole[1], groupAndRole[2]}
}

userRoles, groupRoles, err := fetchUsersAndGroups(inv.Context(), fetchUsersAndGroupsParams{
Client: client,
OrgID: workspace.OrganizationID,
OrgName: workspace.OrganizationName,
Users: userRoleStrings,
Groups: groupRoleStrings,
DefaultRole: codersdk.WorkspaceRoleUse,
})
if err != nil {
return err
}

userRoles := make(map[string]codersdk.WorkspaceRole, len(users))
if len(users) > 0 {
orgMembers, err := client.OrganizationMembers(inv.Context(), org.ID)
if err != nil {
return err
}
err = client.UpdateWorkspaceACL(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceACL{
UserRoles: userRoles,
GroupRoles: groupRoles,
})
if err != nil {
return err
}

for _, user := range users {
userAndRole := nameRoleRegex.FindStringSubmatch(user)
if userAndRole == nil {
return xerrors.Errorf("invalid user format %q: must match pattern 'username:role'", user)
}

username := userAndRole[1]
role := userAndRole[2]
if role == "" {
role = string(codersdk.WorkspaceRoleUse)
}

userID := ""
for _, member := range orgMembers {
if member.Username == username {
userID = member.UserID.String()
break
}
}
if userID == "" {
return xerrors.Errorf("could not find user %s in the organization %s", username, org.Name)
}

workspaceRole, err := stringToWorkspaceRole(role)
if err != nil {
return err
}

userRoles[userID] = workspaceRole
}
acl, err := client.WorkspaceACL(inv.Context(), workspace.ID)
if err != nil {
return xerrors.Errorf("could not fetch current workspace ACL after sharing %w", err)
}

out, err := workspaceACLToTable(inv.Context(), &acl)
if err != nil {
return err
}

groupRoles := make(map[string]codersdk.WorkspaceRole)
if len(groups) > 0 {
orgGroups, err := client.Groups(inv.Context(), codersdk.GroupArguments{
Organization: org.ID.String(),
})
if err != nil {
return err
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
}

return cmd
}

func (r *RootCmd) unshareWorkspace() *serpent.Command {
var (
client = new(codersdk.Client)
users []string
groups []string
)

cmd := &serpent.Command{
Use: "remove <workspace> --user <user> --group <group>",
Aliases: []string{"unshare"},
Short: "Remove shared access for users or groups from a workspace.",
Options: serpent.OptionSet{
{
Name: "user",
Description: "A comma separated list of users to share the workspace with.",
Flag: "user",
Value: serpent.StringArrayOf(&users),
}, {
Name: "group",
Description: "A comma separated list of groups to share the workspace with.",
Flag: "group",
Value: serpent.StringArrayOf(&groups),
},
},
Middleware: serpent.Chain(
r.InitClient(client),
serpent.RequireNArgs(1),
),
Handler: func(inv *serpent.Invocation) error {
if len(users) == 0 && len(groups) == 0 {
return xerrors.New("at least one user or group must be provided")
}

workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return xerrors.Errorf("could not fetch the workspace %s: %w", inv.Args[0], err)
}

userRoleStrings := make([][2]string, len(users))
for index, user := range users {
if !codersdk.UsernameValidRegex.MatchString(user) {
return xerrors.Errorf("invalid username")
}

for _, group := range groups {
groupAndRole := nameRoleRegex.FindStringSubmatch(group)
if groupAndRole == nil {
return xerrors.Errorf("invalid group format %q: must match pattern 'group:role'", group)
}
groupName := groupAndRole[1]
role := groupAndRole[2]
if role == "" {
role = string(codersdk.WorkspaceRoleUse)
}

var orgGroup *codersdk.Group
for _, group := range orgGroups {
if group.Name == groupName {
orgGroup = &group
break
}
}

if orgGroup == nil {
return xerrors.Errorf("could not find group named %s belonging to the organization %s", groupName, org.Name)
}

workspaceRole, err := stringToWorkspaceRole(role)
if err != nil {
return err
}

groupRoles[orgGroup.ID.String()] = workspaceRole
userRoleStrings[index] = [2]string{user, ""}
}

groupRoleStrings := make([][2]string, len(groups))
for index, group := range groups {
if !codersdk.UsernameValidRegex.MatchString(group) {
return xerrors.Errorf("invalid group name")
}

groupRoleStrings[index] = [2]string{group, ""}
}

userRoles, groupRoles, err := fetchUsersAndGroups(inv.Context(), fetchUsersAndGroupsParams{
Client: client,
OrgID: workspace.OrganizationID,
OrgName: workspace.OrganizationName,
Users: userRoleStrings,
Groups: groupRoleStrings,
DefaultRole: codersdk.WorkspaceRoleDeleted,
})
if err != nil {
return err
}

err = client.UpdateWorkspaceACL(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceACL{
Expand DownExpand Up@@ -227,9 +269,11 @@ func stringToWorkspaceRole(role string) (codersdk.WorkspaceRole, error) {
return codersdk.WorkspaceRoleUse, nil
case string(codersdk.WorkspaceRoleAdmin):
return codersdk.WorkspaceRoleAdmin, nil
case string(codersdk.WorkspaceRoleDeleted):
return codersdk.WorkspaceRoleDeleted, nil
default:
return "", xerrors.Errorf("invalid role %q: expected %qor%q",
role, codersdk.WorkspaceRoleAdmin, codersdk.WorkspaceRoleUse)
return "", xerrors.Errorf("invalid role %q: expected %q, %q,or\"%q\"",
role, codersdk.WorkspaceRoleAdmin, codersdk.WorkspaceRoleUse, codersdk.WorkspaceRoleDeleted)
}
}

Expand DownExpand Up@@ -277,3 +321,96 @@ func workspaceACLToTable(ctx context.Context, acl *codersdk.WorkspaceACL) (strin

return out, nil
}

type fetchUsersAndGroupsParams struct {
Client *codersdk.Client
OrgID uuid.UUID
OrgName string
Users [][2]string
Groups [][2]string
DefaultRole codersdk.WorkspaceRole
}

func fetchUsersAndGroups(ctx context.Context, params fetchUsersAndGroupsParams) (userRoles map[string]codersdk.WorkspaceRole, groupRoles map[string]codersdk.WorkspaceRole, err error) {
var (
client = params.Client
orgID = params.OrgID
orgName = params.OrgName
users = params.Users
groups = params.Groups
defaultRole = params.DefaultRole
)

userRoles = make(map[string]codersdk.WorkspaceRole, len(users))
if len(users) > 0 {
orgMembers, err := client.OrganizationMembers(ctx, orgID)
if err != nil {
return nil, nil, err
}

for _, user := range users {
username := user[0]
role := user[1]
if role == "" {
role = string(defaultRole)
}

userID := ""
for _, member := range orgMembers {
if member.Username == username {
userID = member.UserID.String()
break
}
}
if userID == "" {
return nil, nil, xerrors.Errorf("could not find user %s in the organization %s", username, orgName)
}

workspaceRole, err := stringToWorkspaceRole(role)
if err != nil {
return nil, nil, err
}

userRoles[userID] = workspaceRole
}
}

groupRoles = make(map[string]codersdk.WorkspaceRole)
if len(groups) > 0 {
orgGroups, err := client.Groups(ctx, codersdk.GroupArguments{
Organization: orgID.String(),
})
if err != nil {
return nil, nil, err
}

for _, group := range groups {
groupName := group[0]
role := group[1]
if role == "" {
role = string(defaultRole)
}

var orgGroup *codersdk.Group
for _, og := range orgGroups {
if og.Name == groupName {
orgGroup = &og
break
}
}

if orgGroup == nil {
return nil, nil, xerrors.Errorf("could not find group named %s belonging to the organization %s", groupName, orgName)
}

workspaceRole, err := stringToWorkspaceRole(role)
if err != nil {
return nil, nil, err
}

groupRoles[orgGroup.ID.String()] = workspaceRole
}
}

return userRoles, groupRoles, nil
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp