diff --git a/cli/sharing.go b/cli/sharing.go index aa1678e7a9e81..accc930ea4b60 100644 --- a/cli/sharing.go +++ b/cli/sharing.go @@ -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" @@ -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", @@ -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 } @@ -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{ @@ -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 --user --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{ @@ -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) } } @@ -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 +} diff --git a/cli/sharing_test.go b/cli/sharing_test.go index 01bfc0a83873a..9044ed4968170 100644 --- a/cli/sharing_test.go +++ b/cli/sharing_test.go @@ -41,10 +41,10 @@ func TestSharingShare(t *testing.T) { ) ctx := testutil.Context(t, testutil.WaitMedium) - inv, root := clitest.New(t, "sharing", "add", workspace.Name, "--org", orgOwner.OrganizationID.String(), "--user", toShareWithUser.Username) + inv, root := clitest.New(t, "sharing", "add", workspace.Name, "--user", toShareWithUser.Username) clitest.SetupConfig(t, workspaceOwnerClient, root) - out := bytes.NewBuffer(nil) + out := new(bytes.Buffer) inv.Stdout = out err := inv.WithContext(ctx).Run() require.NoError(t, err) @@ -86,12 +86,12 @@ func TestSharingShare(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) inv, root := clitest.New(t, "sharing", - "add", workspace.Name, "--org", orgOwner.OrganizationID.String(), + "add", workspace.Name, fmt.Sprintf("--user=%s,%s", toShareWithUser1.Username, toShareWithUser2.Username), ) clitest.SetupConfig(t, workspaceOwnerClient, root) - out := bytes.NewBuffer(nil) + out := new(bytes.Buffer) inv.Stdout = out err := inv.WithContext(ctx).Run() require.NoError(t, err) @@ -137,12 +137,11 @@ func TestSharingShare(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) inv, root := clitest.New(t, "sharing", "add", workspace.Name, - "--org", orgOwner.OrganizationID.String(), "--user", fmt.Sprintf("%s:admin", toShareWithUser.Username), ) clitest.SetupConfig(t, workspaceOwnerClient, root) - out := bytes.NewBuffer(nil) + out := new(bytes.Buffer) inv.Stdout = out err := inv.WithContext(ctx).Run() require.NoError(t, err) @@ -199,10 +198,10 @@ func TestSharingStatus(t *testing.T) { }) require.NoError(t, err) - inv, root := clitest.New(t, "sharing", "status", workspace.Name, "--org", orgOwner.OrganizationID.String()) + inv, root := clitest.New(t, "sharing", "status", workspace.Name) clitest.SetupConfig(t, workspaceOwnerClient, root) - out := bytes.NewBuffer(nil) + out := new(bytes.Buffer) inv.Stdout = out err = inv.WithContext(ctx).Run() require.NoError(t, err) @@ -217,3 +216,115 @@ func TestSharingStatus(t *testing.T) { assert.True(t, found, "expected to find username %s with role %s in the output: %s", toShareWithUser.Username, codersdk.WorkspaceRoleUse, out.String()) }) } + +func TestSharingRemove(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentWorkspaceSharing)} + + t.Run("RemoveSharedUser_Simple", func(t *testing.T) { + t.Parallel() + + var ( + client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{ + DeploymentValues: dv, + }) + orgOwner = coderdtest.CreateFirstUser(t, client) + workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID)) + workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OwnerID: workspaceOwner.ID, + OrganizationID: orgOwner.OrganizationID, + }).Do().Workspace + _, toRemoveUser = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID) + _, toShareWithUser = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID) + ) + + ctx := testutil.Context(t, testutil.WaitMedium) + + // Share the workspace with a user to later remove + err := client.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{ + UserRoles: map[string]codersdk.WorkspaceRole{ + toShareWithUser.ID.String(): codersdk.WorkspaceRoleUse, + toRemoveUser.ID.String(): codersdk.WorkspaceRoleUse, + }, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, + "sharing", + "remove", + workspace.Name, + "--user", toRemoveUser.Username, + ) + clitest.SetupConfig(t, workspaceOwnerClient, root) + + out := new(bytes.Buffer) + inv.Stdout = out + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + + acl, err := workspaceOwnerClient.WorkspaceACL(inv.Context(), workspace.ID) + require.NoError(t, err) + + removedCorrectUser := true + keptOtherUser := false + for _, user := range acl.Users { + if user.ID == toRemoveUser.ID { + removedCorrectUser = false + } + + if user.ID == toShareWithUser.ID { + keptOtherUser = true + } + } + assert.True(t, removedCorrectUser) + assert.True(t, keptOtherUser) + }) + + t.Run("RemoveSharedUser_Multiple", func(t *testing.T) { + t.Parallel() + + var ( + client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{ + DeploymentValues: dv, + }) + orgOwner = coderdtest.CreateFirstUser(t, client) + workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID)) + workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OwnerID: workspaceOwner.ID, + OrganizationID: orgOwner.OrganizationID, + }).Do().Workspace + _, toRemoveUser1 = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID) + _, toRemoveUser2 = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID) + ) + + ctx := testutil.Context(t, testutil.WaitMedium) + + // Share the workspace with a user to later remove + err := client.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{ + UserRoles: map[string]codersdk.WorkspaceRole{ + toRemoveUser2.ID.String(): codersdk.WorkspaceRoleUse, + toRemoveUser1.ID.String(): codersdk.WorkspaceRoleUse, + }, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, + "sharing", + "remove", + workspace.Name, + fmt.Sprintf("--user=%s,%s", toRemoveUser1.Username, toRemoveUser2.Username), + ) + clitest.SetupConfig(t, workspaceOwnerClient, root) + + out := new(bytes.Buffer) + inv.Stdout = out + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + + acl, err := workspaceOwnerClient.WorkspaceACL(inv.Context(), workspace.ID) + require.NoError(t, err) + assert.Empty(t, acl.Users) + }) +} diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 3b8e35c003682..64ef5c9f8171f 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -2215,12 +2215,18 @@ func (api *API) workspaceACL(rw http.ResponseWriter, r *http.Request) { } groupIDs = append(groupIDs, id) } - // For context see https://github.com/coder/coder/pull/19375 - // nolint:gocritic - dbGroups, err := api.Database.GetGroups(dbauthz.AsSystemRestricted(ctx), database.GetGroupsParams{GroupIds: groupIDs}) - if err != nil && !xerrors.Is(err, sql.ErrNoRows) { - httpapi.InternalServerError(rw, err) - return + + // `GetGroups` returns all groups if `GroupIds` is empty so we check the length + // before making the DB call. + dbGroups := make([]database.GetGroupsRow, 0) + if len(groupIDs) > 0 { + // For context see https://github.com/coder/coder/pull/19375 + // nolint:gocritic + dbGroups, err = api.Database.GetGroups(dbauthz.AsSystemRestricted(ctx), database.GetGroupsParams{GroupIds: groupIDs}) + if err != nil && !xerrors.Is(err, sql.ErrNoRows) { + httpapi.InternalServerError(rw, err) + return + } } groups := make([]codersdk.WorkspaceGroup, 0, len(dbGroups)) diff --git a/enterprise/cli/sharing_test.go b/enterprise/cli/sharing_test.go index 906c02a148d4b..65b8ce53a2bf1 100644 --- a/enterprise/cli/sharing_test.go +++ b/enterprise/cli/sharing_test.go @@ -23,7 +23,7 @@ import ( "github.com/coder/coder/v2/testutil" ) -func TestSharingShareEnterprise(t *testing.T) { +func TestSharingShare(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) @@ -56,10 +56,10 @@ func TestSharingShareEnterprise(t *testing.T) { group, err := createGroupWithMembers(ctx, client, orgOwner.OrganizationID, "new-group", []uuid.UUID{orgMember.ID}) require.NoError(t, err) - inv, root := clitest.New(t, "sharing", "share", workspace.Name, "--org", orgOwner.OrganizationID.String(), "--group", group.Name) + inv, root := clitest.New(t, "sharing", "share", workspace.Name, "--group", group.Name) clitest.SetupConfig(t, workspaceOwnerClient, root) - out := bytes.NewBuffer(nil) + out := new(bytes.Buffer) inv.Stdout = out err = inv.WithContext(ctx).Run() require.NoError(t, err) @@ -113,11 +113,11 @@ func TestSharingShareEnterprise(t *testing.T) { wobbleGroup, err := createGroupWithMembers(ctx, client, orgOwner.OrganizationID, "wobble", []uuid.UUID{wobbleMember.ID}) require.NoError(t, err) - inv, root := clitest.New(t, "sharing", "share", workspace.Name, "--org", orgOwner.OrganizationID.String(), + inv, root := clitest.New(t, "sharing", "share", workspace.Name, fmt.Sprintf("--group=%s,%s", wibbleGroup.Name, wobbleGroup.Name)) clitest.SetupConfig(t, workspaceOwnerClient, root) - out := bytes.NewBuffer(nil) + out := new(bytes.Buffer) inv.Stdout = out err = inv.WithContext(ctx).Run() require.NoError(t, err) @@ -161,10 +161,10 @@ func TestSharingShareEnterprise(t *testing.T) { group, err := createGroupWithMembers(ctx, client, orgOwner.OrganizationID, "new-group", []uuid.UUID{orgMember.ID}) require.NoError(t, err) - inv, root := clitest.New(t, "sharing", "share", workspace.Name, "--org", orgOwner.OrganizationID.String(), "--group", fmt.Sprintf("%s:admin", group.Name)) + inv, root := clitest.New(t, "sharing", "share", workspace.Name, "--group", fmt.Sprintf("%s:admin", group.Name)) clitest.SetupConfig(t, workspaceOwnerClient, root) - out := bytes.NewBuffer(nil) + out := new(bytes.Buffer) inv.Stdout = out err = inv.WithContext(ctx).Run() require.NoError(t, err) @@ -226,10 +226,10 @@ func TestSharingStatus(t *testing.T) { }) require.NoError(t, err) - inv, root := clitest.New(t, "sharing", "status", workspace.Name, "--org", orgOwner.OrganizationID.String()) + inv, root := clitest.New(t, "sharing", "status", workspace.Name) clitest.SetupConfig(t, workspaceOwnerClient, root) - out := bytes.NewBuffer(nil) + out := new(bytes.Buffer) inv.Stdout = out err = inv.WithContext(ctx).Run() require.NoError(t, err) @@ -245,6 +245,155 @@ func TestSharingStatus(t *testing.T) { }) } +func TestSharingRemove(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentWorkspaceSharing)} + + t.Run("RemoveSharedGroup_Single", func(t *testing.T) { + t.Parallel() + + var ( + client, db, orgOwner = coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, + }, + }) + workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID)) + workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OwnerID: workspaceOwner.ID, + OrganizationID: orgOwner.OrganizationID, + }).Do().Workspace + _, groupUser1 = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID) + _, groupUser2 = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID) + ) + + ctx := testutil.Context(t, testutil.WaitMedium) + + group1, err := createGroupWithMembers(ctx, client, orgOwner.OrganizationID, "group-1", []uuid.UUID{groupUser1.ID, groupUser2.ID}) + require.NoError(t, err) + + group2, err := createGroupWithMembers(ctx, client, orgOwner.OrganizationID, "group-2", []uuid.UUID{groupUser1.ID, groupUser2.ID}) + require.NoError(t, err) + + // Share the workspace with a user to later remove + err = client.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{ + GroupRoles: map[string]codersdk.WorkspaceRole{ + group1.ID.String(): codersdk.WorkspaceRoleUse, + group2.ID.String(): codersdk.WorkspaceRoleUse, + }, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, + "sharing", + "remove", + workspace.Name, + "--group", group1.Name, + ) + clitest.SetupConfig(t, workspaceOwnerClient, root) + + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + + acl, err := workspaceOwnerClient.WorkspaceACL(inv.Context(), workspace.ID) + require.NoError(t, err) + + removedGroup1 := true + removedGroup2 := true + for _, group := range acl.Groups { + if group.ID == group1.ID { + removedGroup1 = false + continue + } + + if group.ID == group2.ID { + removedGroup2 = false + continue + } + } + assert.True(t, removedGroup1) + assert.False(t, removedGroup2) + }) + + t.Run("RemoveSharedGroup_Multiple", func(t *testing.T) { + t.Parallel() + + var ( + client, db, orgOwner = coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, + }, + }) + workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID)) + workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OwnerID: workspaceOwner.ID, + OrganizationID: orgOwner.OrganizationID, + }).Do().Workspace + _, groupUser1 = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID) + _, groupUser2 = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID) + ) + + ctx := testutil.Context(t, testutil.WaitMedium) + + group1, err := createGroupWithMembers(ctx, client, orgOwner.OrganizationID, "group-1", []uuid.UUID{groupUser1.ID, groupUser2.ID}) + require.NoError(t, err) + + group2, err := createGroupWithMembers(ctx, client, orgOwner.OrganizationID, "group-2", []uuid.UUID{groupUser1.ID, groupUser2.ID}) + require.NoError(t, err) + + // Share the workspace with a user to later remove + err = client.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{ + GroupRoles: map[string]codersdk.WorkspaceRole{ + group1.ID.String(): codersdk.WorkspaceRoleUse, + group2.ID.String(): codersdk.WorkspaceRoleUse, + }, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, + "sharing", + "remove", + workspace.Name, + fmt.Sprintf("--group=%s,%s", group1.Name, group2.Name), + ) + clitest.SetupConfig(t, workspaceOwnerClient, root) + + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + + acl, err := workspaceOwnerClient.WorkspaceACL(inv.Context(), workspace.ID) + require.NoError(t, err) + + removedGroup1 := true + removedGroup2 := true + for _, group := range acl.Groups { + if group.ID == group1.ID { + removedGroup1 = false + continue + } + + if group.ID == group2.ID { + removedGroup2 = false + continue + } + } + assert.True(t, removedGroup1) + assert.True(t, removedGroup2) + }) +} + func createGroupWithMembers(ctx context.Context, client *codersdk.Client, orgID uuid.UUID, name string, memberIDs []uuid.UUID) (codersdk.Group, error) { group, err := client.CreateGroup(ctx, orgID, codersdk.CreateGroupRequest{ Name: name,