diff --git a/cli/exp_chat.go b/cli/exp_chat.go index 840539b9e2..1246db3851 100644 --- a/cli/exp_chat.go +++ b/cli/exp_chat.go @@ -42,6 +42,7 @@ func (r *RootCmd) chatShareCommand() *serpent.Command { }, Children: []*serpent.Command{ r.chatShareAddCommand(), + r.chatShareRemoveCommand(), }, } } @@ -140,6 +141,100 @@ func (r *RootCmd) chatShareAddCommand() *serpent.Command { } } +func (r *RootCmd) chatShareRemoveCommand() *serpent.Command { + var users []string + var groups []string + + return &serpent.Command{ + Use: "remove --user --group ", + Short: "Remove shared access for users or groups from a chat.", + Options: serpent.OptionSet{ + { + Name: "user", + Description: "A comma separated list of users to remove shared chat access from.", + Flag: "user", + Value: serpent.StringArrayOf(&users), + }, { + Name: "group", + Description: "A comma separated list of groups to remove shared chat access from.", + Flag: "group", + Value: serpent.StringArrayOf(&groups), + }, + }, + Middleware: serpent.Chain( + 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") + } + + chatID, err := parseChatShareID(inv.Args[0]) + if err != nil { + return err + } + + userRoleStrings := make([][2]string, len(users)) + for i, user := range users { + parsed, err := parseChatShareActor(user) + if err != nil { + return xerrors.Errorf("invalid user format %q: %w", user, err) + } + userRoleStrings[i] = parsed + } + + groupRoleStrings := make([][2]string, len(groups)) + for i, group := range groups { + parsed, err := parseChatShareActor(group) + if err != nil { + return xerrors.Errorf("invalid group format %q: %w", group, err) + } + groupRoleStrings[i] = parsed + } + + client, err := r.InitClient(inv) + if err != nil { + return err + } + experimentalClient := codersdk.NewExperimentalClient(client) + + chat, err := experimentalClient.GetChat(inv.Context(), chatID) + if err != nil { + return xerrors.Errorf("unable to fetch chat %s: %w", inv.Args[0], err) + } + + userRoles, groupRoles, err := fetchChatUsersAndGroups(inv.Context(), chatRoleLookupParams{ + Client: client, + OrgID: chat.OrganizationID, + Users: userRoleStrings, + Groups: groupRoleStrings, + DefaultRole: codersdk.ChatRoleDeleted, + }) + if err != nil { + return err + } + + if err := experimentalClient.UpdateChatACL(inv.Context(), chat.ID, codersdk.UpdateChatACL{ + UserRoles: userRoles, + GroupRoles: groupRoles, + }); err != nil { + return err + } + + acl, err := experimentalClient.GetChatACL(inv.Context(), chat.ID) + if err != nil { + return xerrors.Errorf("could not fetch current chat ACL after sharing: %w", err) + } + out, err := chatACLToTable(inv.Context(), &acl) + if err != nil { + return err + } + _, err = fmt.Fprintln(inv.Stdout, out) + return err + }, + } +} + func (r *RootCmd) chatContextCommand() *serpent.Command { return &serpent.Command{ Use: "context", diff --git a/cli/exp_chat_test.go b/cli/exp_chat_test.go index 660aea6691..a4b5d2f544 100644 --- a/cli/exp_chat_test.go +++ b/cli/exp_chat_test.go @@ -187,6 +187,78 @@ func TestExpChatShareAdd(t *testing.T) { }) } +func TestExpChatShareRemove(t *testing.T) { + t.Parallel() + + t.Run("RemoveSharedUser", func(t *testing.T) { + t.Parallel() + + client, db := coderdtest.NewWithDatabase(t, nil) + firstUser := coderdtest.CreateFirstUser(t, client) + _, sharedUser := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID) + modelConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{}) + chat := dbgen.Chat(t, db, database.Chat{ + OrganizationID: firstUser.OrganizationID, + OwnerID: firstUser.UserID, + LastModelConfigID: modelConfig.ID, + Title: "share remove user", + }) + experimentalClient := codersdk.NewExperimentalClient(client) + ctx := testutil.Context(t, testutil.WaitMedium) + err := experimentalClient.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{ + UserRoles: map[string]codersdk.ChatRole{sharedUser.ID.String(): codersdk.ChatRoleRead}, + }) + require.NoError(t, err) + + inv, root := clitest.New(t, "exp", "chat", "share", "remove", chat.ID.String(), "--user", sharedUser.Username) + clitest.SetupConfig(t, client, root) + + out := new(bytes.Buffer) + inv.Stdout = out + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + + acl, err := experimentalClient.GetChatACL(ctx, chat.ID) + require.NoError(t, err) + for _, user := range acl.Users { + assert.NotEqual(t, sharedUser.ID, user.ID) + } + assert.NotContains(t, out.String(), sharedUser.Username) + }) + + t.Run("RequiresActor", func(t *testing.T) { + t.Parallel() + + chatID := "00000000-0000-0000-0000-000000000001" + inv, _ := clitest.New(t, "exp", "chat", "share", "remove", chatID) + + err := inv.Run() + require.Error(t, err) + require.Contains(t, err.Error(), "at least one user or group must be provided") + }) + + t.Run("RejectsRoleSyntax", func(t *testing.T) { + t.Parallel() + + chatID := "00000000-0000-0000-0000-000000000001" + inv, _ := clitest.New(t, "exp", "chat", "share", "remove", chatID, "--user", "alice:read") + + err := inv.Run() + require.Error(t, err) + require.Contains(t, err.Error(), "roles are only accepted by chat share add") + }) + + t.Run("RejectsInvalidChatID", func(t *testing.T) { + t.Parallel() + + inv, _ := clitest.New(t, "exp", "chat", "share", "remove", "not-a-uuid", "--user", "alice") + + err := inv.Run() + require.Error(t, err) + require.Contains(t, err.Error(), "invalid chat ID") + }) +} + func TestExpChatContextAdd(t *testing.T) { t.Parallel()