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 Down Expand 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 Down Expand 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 Down Expand 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 %q or%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 Down Expand 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 }