diff --git a/internal/cmd/completion/iam.go b/internal/cmd/completion/iam.go index 9e31243..54a42ff 100644 --- a/internal/cmd/completion/iam.go +++ b/internal/cmd/completion/iam.go @@ -8,6 +8,34 @@ import ( "github.com/qdrant/qcloud-cli/internal/state" ) +// RoleCompletion returns a completion function that completes IAM role names +// with their ID as description. +func RoleCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + ctx := cmd.Context() + client, err := s.Client(ctx) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + accountID, err := s.AccountID() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + resp, err := client.IAM().ListRoles(ctx, &iamv1.ListRolesRequest{AccountId: accountID}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + completions := make([]string, 0, len(resp.GetItems())) + for _, r := range resp.GetItems() { + completions = append(completions, r.GetName()+"\t"+r.GetId()) + } + return completions, cobra.ShellCompDirectiveNoFileComp + } +} + // RoleIDCompletion returns a ValidArgsFunction that completes role IDs. func RoleIDCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { diff --git a/internal/cmd/iam/completion.go b/internal/cmd/iam/completion.go new file mode 100644 index 0000000..7b28594 --- /dev/null +++ b/internal/cmd/iam/completion.go @@ -0,0 +1,39 @@ +package iam + +import ( + "github.com/spf13/cobra" + + iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1" + + "github.com/qdrant/qcloud-cli/internal/state" +) + +// userCompletion returns a ValidArgsFunction that completes user IDs with +// their email as description. It only completes the first positional argument. +func userCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + ctx := cmd.Context() + client, err := s.Client(ctx) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + accountID, err := s.AccountID() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + resp, err := client.IAM().ListUsers(ctx, &iamv1.ListUsersRequest{AccountId: accountID}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + completions := make([]string, 0, len(resp.GetItems())) + for _, u := range resp.GetItems() { + completions = append(completions, u.GetId()+"\t"+u.GetEmail()) + } + return completions, cobra.ShellCompDirectiveNoFileComp + } +} diff --git a/internal/cmd/iam/completion_test.go b/internal/cmd/iam/completion_test.go index bad5c27..bd44551 100644 --- a/internal/cmd/iam/completion_test.go +++ b/internal/cmd/iam/completion_test.go @@ -11,6 +11,47 @@ import ( "github.com/qdrant/qcloud-cli/internal/testutil" ) +func TestUserCompletion(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{ + {Id: "user-uuid-1", Email: "alice@example.com"}, + {Id: "user-uuid-2", Email: "bob@example.com"}, + }, + }, nil) + + stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "user", "describe", "") + require.NoError(t, err) + assert.Contains(t, stdout, "user-uuid-1") + assert.Contains(t, stdout, "alice@example.com") + assert.Contains(t, stdout, "user-uuid-2") + assert.Contains(t, stdout, "bob@example.com") +} + +func TestUserCompletion_StopsAfterFirstArg(t *testing.T) { + env := testutil.NewTestEnv(t) + + stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "user", "describe", "user-uuid-1", "") + require.NoError(t, err) + assert.NotContains(t, stdout, "user-uuid") +} + +func TestUserThenRoleCompletion_FirstArg(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{ + {Id: "user-uuid-1", Email: "alice@example.com"}, + }, + }, nil) + + stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "user", "assign-role", "") + require.NoError(t, err) + assert.Contains(t, stdout, "user-uuid-1") + assert.Contains(t, stdout, "alice@example.com") +} + func TestRoleIDCompletion_Describe(t *testing.T) { env := testutil.NewTestEnv(t) env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{ diff --git a/internal/cmd/iam/iam.go b/internal/cmd/iam/iam.go index c670b84..bf712bb 100644 --- a/internal/cmd/iam/iam.go +++ b/internal/cmd/iam/iam.go @@ -16,6 +16,7 @@ func NewCommand(s *state.State) *cobra.Command { } cmd.AddCommand( newKeyCommand(s), + newUserCommand(s), newRoleCommand(s), newPermissionCommand(s), ) diff --git a/internal/cmd/iam/iam_test.go b/internal/cmd/iam/iam_test.go new file mode 100644 index 0000000..ba2bb90 --- /dev/null +++ b/internal/cmd/iam/iam_test.go @@ -0,0 +1,7 @@ +package iam_test + +// Shared test constants used across iam subcommand test files. +const ( + testUserID = "7b2ea926-724b-4de2-b73a-8675c42a6ebe" + testRoleID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" +) diff --git a/internal/cmd/iam/resolve.go b/internal/cmd/iam/resolve.go new file mode 100644 index 0000000..0c07837 --- /dev/null +++ b/internal/cmd/iam/resolve.go @@ -0,0 +1,113 @@ +package iam + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1" + + "github.com/qdrant/qcloud-cli/internal/cmd/output" + "github.com/qdrant/qcloud-cli/internal/cmd/util" + "github.com/qdrant/qcloud-cli/internal/qcloudapi" + "github.com/qdrant/qcloud-cli/internal/state" +) + +// resolveUser looks up a user by UUID or email from the account's user list. +func resolveUser(cmd *cobra.Command, client *qcloudapi.Client, accountID, idOrEmail string) (*iamv1.User, error) { + ctx := cmd.Context() + resp, err := client.IAM().ListUsers(ctx, &iamv1.ListUsersRequest{AccountId: accountID}) + if err != nil { + return nil, fmt.Errorf("failed to list users: %w", err) + } + for _, u := range resp.GetItems() { + if util.IsUUID(idOrEmail) { + if u.GetId() == idOrEmail { + return u, nil + } + } else { + if u.GetEmail() == idOrEmail { + return u, nil + } + } + } + return nil, fmt.Errorf("user %s not found", idOrEmail) +} + +// resolveRoleIDs converts a slice of role names or UUIDs to UUIDs. +// Values that already look like UUIDs are passed through unchanged. +// Non-UUID values are resolved by name via ListRoles. +func resolveRoleIDs(ctx context.Context, client *qcloudapi.Client, accountID string, namesOrIDs []string) ([]string, error) { + if len(namesOrIDs) == 0 { + return nil, nil + } + + // Check whether any name resolution is needed. + var needsLookup bool + for _, v := range namesOrIDs { + if !util.IsUUID(v) { + needsLookup = true + break + } + } + + var rolesByName map[string]string + if needsLookup { + resp, err := client.IAM().ListRoles(ctx, &iamv1.ListRolesRequest{AccountId: accountID}) + if err != nil { + return nil, fmt.Errorf("failed to list roles: %w", err) + } + rolesByName = make(map[string]string, len(resp.GetItems())) + for _, r := range resp.GetItems() { + rolesByName[r.GetName()] = r.GetId() + } + } + + ids := make([]string, 0, len(namesOrIDs)) + for _, v := range namesOrIDs { + if util.IsUUID(v) { + ids = append(ids, v) + } else { + id, ok := rolesByName[v] + if !ok { + return nil, fmt.Errorf("role %q not found", v) + } + ids = append(ids, id) + } + } + return ids, nil +} + +// modifyUserRoles calls AssignUserRoles with the given add/delete IDs, then +// fetches and prints the resulting role list. +func modifyUserRoles(s *state.State, cmd *cobra.Command, client *qcloudapi.Client, accountID string, user *iamv1.User, addIDs, removeIDs []string) error { + ctx := cmd.Context() + + _, err := client.IAM().AssignUserRoles(ctx, &iamv1.AssignUserRolesRequest{ + AccountId: accountID, + UserId: user.GetId(), + RoleIdsToAdd: addIDs, + RoleIdsToDelete: removeIDs, + }) + if err != nil { + return fmt.Errorf("failed to modify roles: %w", err) + } + + rolesResp, err := client.IAM().ListUserRoles(ctx, &iamv1.ListUserRolesRequest{ + AccountId: accountID, + UserId: user.GetId(), + }) + if err != nil { + return fmt.Errorf("failed to list user roles: %w", err) + } + + if s.Config.JSONOutput() { + return output.PrintJSON(cmd.OutOrStdout(), rolesResp) + } + + w := cmd.OutOrStdout() + fmt.Fprintf(w, "Roles for %s:\n", user.GetEmail()) + printRoles(w, rolesResp.GetRoles()) + return nil +} diff --git a/internal/cmd/iam/user.go b/internal/cmd/iam/user.go new file mode 100644 index 0000000..fa8d4d8 --- /dev/null +++ b/internal/cmd/iam/user.go @@ -0,0 +1,26 @@ +package iam + +import ( + "github.com/spf13/cobra" + + "github.com/qdrant/qcloud-cli/internal/state" +) + +func newUserCommand(s *state.State) *cobra.Command { + cmd := &cobra.Command{ + Use: "user", + Short: "Manage users in Qdrant Cloud", + Long: `Manage users in the Qdrant Cloud account. + +Provides commands to list users, view user details and assigned roles, and +manage role assignments.`, + Args: cobra.NoArgs, + } + cmd.AddCommand( + newUserListCommand(s), + newUserDescribeCommand(s), + newUserAssignRoleCommand(s), + newUserRemoveRoleCommand(s), + ) + return cmd +} diff --git a/internal/cmd/iam/user_assign_role.go b/internal/cmd/iam/user_assign_role.go new file mode 100644 index 0000000..35b51df --- /dev/null +++ b/internal/cmd/iam/user_assign_role.go @@ -0,0 +1,67 @@ +package iam + +import ( + "github.com/spf13/cobra" + + "github.com/qdrant/qcloud-cli/internal/cmd/base" + "github.com/qdrant/qcloud-cli/internal/cmd/completion" + "github.com/qdrant/qcloud-cli/internal/cmd/util" + "github.com/qdrant/qcloud-cli/internal/state" +) + +func newUserAssignRoleCommand(s *state.State) *cobra.Command { + return base.Cmd{ + BaseCobraCommand: func() *cobra.Command { + cmd := &cobra.Command{ + Use: "assign-role ", + Short: "Assign one or more roles to a user", + Args: util.ExactArgs(1, "a user ID or email"), + } + + _ = cmd.Flags().StringSliceP("role", "r", nil, "A role ID or name") + _ = cmd.RegisterFlagCompletionFunc("role", completion.RoleCompletion(s)) + return cmd + }, + ValidArgsFunction: userCompletion(s), + Long: `Assign one or more roles to a user in the account. + +Accepts either a user ID (UUID) or an email address to identify the user. +Each role accepts either a role UUID or a role name, which is +resolved to an ID via the IAM service. Prints the user's resulting roles +after the assignment.`, + Example: `# Assign a role by name +qcloud iam user assign-role user@example.com --role admin + +# Assign a role by ID +qcloud iam user assign-role user@example.com --role 7b2ea926-724b-4de2-b73a-8675c42a6ebe + +# Assign multiple roles at once +qcloud iam user assign-role user@example.com --role admin --role viewer + +# Assign multiple roles at once using comma separated values +qcloud iam user assign-role user@example.com --role admin,viewer`, + Run: func(s *state.State, cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := s.Client(ctx) + if err != nil { + return err + } + accountID, err := s.AccountID() + if err != nil { + return err + } + user, err := resolveUser(cmd, client, accountID, args[0]) + if err != nil { + return err + } + + roles, _ := cmd.Flags().GetStringSlice("role") + roleIDs, err := resolveRoleIDs(ctx, client, accountID, roles) + if err != nil { + return err + } + + return modifyUserRoles(s, cmd, client, accountID, user, roleIDs, nil) + }, + }.CobraCommand(s) +} diff --git a/internal/cmd/iam/user_assign_role_test.go b/internal/cmd/iam/user_assign_role_test.go new file mode 100644 index 0000000..0fd1629 --- /dev/null +++ b/internal/cmd/iam/user_assign_role_test.go @@ -0,0 +1,144 @@ +package iam_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1" + + "github.com/qdrant/qcloud-cli/internal/testutil" +) + +func TestUserAssignRole_ByRoleID(t *testing.T) { + env := testutil.NewTestEnv(t) + + userID := testUserID + roleID := testRoleID + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: userID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.AssignUserRolesCalls.Returns(&iamv1.AssignUserRolesResponse{}, nil) + env.IAMServer.ListUserRolesCalls.Returns(&iamv1.ListUserRolesResponse{ + Roles: []*iamv1.Role{{Id: roleID, Name: "admin"}}, + }, nil) + + stdout, _, err := testutil.Exec(t, env, "iam", "user", "assign-role", + "alice@example.com", "--role", roleID) + require.NoError(t, err) + assert.Contains(t, stdout, "alice@example.com") + assert.Contains(t, stdout, roleID) + assert.Contains(t, stdout, "admin") + + req, ok := env.IAMServer.AssignUserRolesCalls.Last() + require.True(t, ok) + assert.Equal(t, userID, req.GetUserId()) + assert.Equal(t, []string{roleID}, req.GetRoleIdsToAdd()) +} + +func TestUserAssignRole_ByRoleName(t *testing.T) { + env := testutil.NewTestEnv(t) + + userID := testUserID + roleID := testRoleID + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: userID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{ + Items: []*iamv1.Role{{Id: roleID, Name: "admin"}}, + }, nil) + env.IAMServer.AssignUserRolesCalls.Returns(&iamv1.AssignUserRolesResponse{}, nil) + env.IAMServer.ListUserRolesCalls.Returns(&iamv1.ListUserRolesResponse{ + Roles: []*iamv1.Role{{Id: roleID, Name: "admin"}}, + }, nil) + + stdout, _, err := testutil.Exec(t, env, "iam", "user", "assign-role", + "alice@example.com", "--role", "admin") + require.NoError(t, err) + assert.Contains(t, stdout, "admin") + + req, ok := env.IAMServer.AssignUserRolesCalls.Last() + require.True(t, ok) + assert.Equal(t, []string{roleID}, req.GetRoleIdsToAdd()) +} + +func TestUserAssignRole_MissingRole(t *testing.T) { + env := testutil.NewTestEnv(t) + + _, _, err := testutil.Exec(t, env, "iam", "user", "assign-role", "alice@example.com") + require.Error(t, err) +} + +func TestUserAssignRole_ResolveUserError(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(nil, fmt.Errorf("connection refused")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "assign-role", + "alice@example.com", "--role", testRoleID) + require.Error(t, err) + assert.Contains(t, err.Error(), "connection refused") +} + +func TestUserAssignRole_ResolveRoleError(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: testUserID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.ListRolesCalls.Returns(nil, fmt.Errorf("service unavailable")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "assign-role", + "alice@example.com", "--role", "admin") + require.Error(t, err) + assert.Contains(t, err.Error(), "service unavailable") +} + +func TestUserAssignRole_RoleNameNotFound(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: testUserID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{ + Items: []*iamv1.Role{{Id: testRoleID, Name: "viewer"}}, + }, nil) + + _, _, err := testutil.Exec(t, env, "iam", "user", "assign-role", + "alice@example.com", "--role", "nonexistent") + require.Error(t, err) + assert.Contains(t, err.Error(), `role "nonexistent" not found`) +} + +func TestUserAssignRole_AssignError(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: testUserID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.AssignUserRolesCalls.Returns(nil, fmt.Errorf("forbidden")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "assign-role", + "alice@example.com", "--role", testRoleID) + require.Error(t, err) + assert.Contains(t, err.Error(), "forbidden") +} + +func TestUserAssignRole_ListUserRolesError(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: testUserID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.AssignUserRolesCalls.Returns(&iamv1.AssignUserRolesResponse{}, nil) + env.IAMServer.ListUserRolesCalls.Returns(nil, fmt.Errorf("timeout")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "assign-role", + "alice@example.com", "--role", testRoleID) + require.Error(t, err) + assert.Contains(t, err.Error(), "timeout") +} diff --git a/internal/cmd/iam/user_describe.go b/internal/cmd/iam/user_describe.go new file mode 100644 index 0000000..3b0d9f2 --- /dev/null +++ b/internal/cmd/iam/user_describe.go @@ -0,0 +1,141 @@ +package iam + +import ( + "fmt" + "io" + "sort" + "strings" + + "github.com/spf13/cobra" + + iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1" + + "github.com/qdrant/qcloud-cli/internal/cmd/base" + "github.com/qdrant/qcloud-cli/internal/cmd/output" + "github.com/qdrant/qcloud-cli/internal/cmd/util" + "github.com/qdrant/qcloud-cli/internal/state" +) + +func newUserDescribeCommand(s *state.State) *cobra.Command { + return base.DescribeCmd[*iamv1.User]{ + Use: "describe ", + Short: "Describe a user and their assigned roles", + Args: util.ExactArgs(1, "a user ID or email"), + ValidArgsFunction: userCompletion(s), + Long: `Describe a user and their assigned roles. + +Accepts either a user ID (UUID) or an email address. Displays the user's +details and the roles currently assigned to them in the account.`, + Example: `# Describe a user by ID +qcloud iam user describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe + +# Describe a user by email +qcloud iam user describe user@example.com + +# Output as JSON +qcloud iam user describe user@example.com --json`, + Fetch: func(s *state.State, cmd *cobra.Command, args []string) (*iamv1.User, error) { + client, err := s.Client(cmd.Context()) + if err != nil { + return nil, err + } + accountID, err := s.AccountID() + if err != nil { + return nil, err + } + return resolveUser(cmd, client, accountID, args[0]) + }, + PrintText: func(cmd *cobra.Command, w io.Writer, user *iamv1.User) error { + ctx := cmd.Context() + client, err := s.Client(ctx) + if err != nil { + return err + } + accountID, err := s.AccountID() + if err != nil { + return err + } + rolesResp, err := client.IAM().ListUserRoles(ctx, &iamv1.ListUserRolesRequest{ + AccountId: accountID, + UserId: user.GetId(), + }) + if err != nil { + return fmt.Errorf("failed to list user roles: %w", err) + } + roles := rolesResp.GetRoles() + return printUserWithRoles(w, user, roles, effectivePermissions(roles)) + }, + }.CobraCommand(s) +} + +func printUserWithRoles(w io.Writer, user *iamv1.User, roles []*iamv1.Role, permissions []rolePermission) error { + fmt.Fprintf(w, "ID: %s\n", user.GetId()) + fmt.Fprintf(w, "Email: %s\n", user.GetEmail()) + fmt.Fprintf(w, "Status: %s\n", output.UserStatus(user.GetStatus())) + if user.GetCreatedAt() != nil { + t := user.GetCreatedAt().AsTime() + fmt.Fprintf(w, "Created: %s (%s)\n", output.HumanTime(t), output.FullDateTime(t)) + } + if len(roles) > 0 { + fmt.Fprintln(w) + fmt.Fprintln(w, "Roles:") + printRoles(w, roles) + } + if len(permissions) > 0 { + fmt.Fprintln(w) + fmt.Fprintln(w, "Effective Permissions:") + printPermissions(w, permissions) + } + return nil +} + +func printRoles(w io.Writer, roles []*iamv1.Role) { + t := output.NewTable[*iamv1.Role](w) + t.AddField("ID", func(v *iamv1.Role) string { return v.GetId() }) + t.AddField("NAME", func(v *iamv1.Role) string { return v.GetName() }) + t.Write(roles) +} + +type rolePermission struct { + permission *iamv1.Permission + roleNames []string +} + +func printPermissions(w io.Writer, rps []rolePermission) { + t := output.NewTable[rolePermission](w) + t.AddField("PERMISSION", func(v rolePermission) string { return v.permission.GetValue() }) + t.AddField("CATEGORY", func(v rolePermission) string { return v.permission.GetCategory() }) + t.AddField("FROM ROLES", func(v rolePermission) string { return strings.Join(v.roleNames, ", ") }) + t.Write(rps) +} + +// effectivePermissions collects unique permissions across all roles, with the +// sorted list of role names that grant each permission. Results are sorted by +// permission value. +func effectivePermissions(roles []*iamv1.Role) []rolePermission { + type entry struct { + permission *iamv1.Permission + roleNames []string + } + seen := make(map[string]*entry) + order := []string{} + for _, role := range roles { + for _, p := range role.GetPermissions() { + v := p.GetValue() + if e, ok := seen[v]; ok { + e.roleNames = append(e.roleNames, role.GetName()) + } else { + seen[v] = &entry{permission: p, roleNames: []string{role.GetName()}} + order = append(order, v) + } + } + } + sort.Strings(order) + out := make([]rolePermission, 0, len(order)) + for _, v := range order { + e := seen[v] + sort.Strings(e.roleNames) + out = append(out, rolePermission{permission: e.permission, roleNames: e.roleNames}) + } + return out +} diff --git a/internal/cmd/iam/user_describe_test.go b/internal/cmd/iam/user_describe_test.go new file mode 100644 index 0000000..6354a52 --- /dev/null +++ b/internal/cmd/iam/user_describe_test.go @@ -0,0 +1,189 @@ +package iam_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1" + + "github.com/qdrant/qcloud-cli/internal/testutil" +) + +const testRoleCategory = "Cluster" + +func TestUserDescribe_ByID(t *testing.T) { + env := testutil.NewTestEnv(t) + + userID := testUserID + cat := testRoleCategory + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{ + {Id: userID, Email: "alice@example.com", Status: iamv1.UserStatus_USER_STATUS_ACTIVE}, + }, + }, nil) + env.IAMServer.ListUserRolesCalls.Returns(&iamv1.ListUserRolesResponse{ + Roles: []*iamv1.Role{ + { + Id: "role-id-1", + Name: "admin", + Permissions: []*iamv1.Permission{ + {Value: "read:clusters", Category: &cat}, + {Value: "write:clusters", Category: &cat}, + }, + }, + }, + }, nil) + + stdout, _, err := testutil.Exec(t, env, "iam", "user", "describe", userID) + require.NoError(t, err) + + assert.Contains(t, stdout, userID) + assert.Contains(t, stdout, "alice@example.com") + assert.Contains(t, stdout, "ACTIVE") + assert.Contains(t, stdout, "role-id-1") + assert.Contains(t, stdout, "admin") + assert.Contains(t, stdout, "read:clusters") + assert.Contains(t, stdout, "write:clusters") + assert.Contains(t, stdout, "Cluster") + + req, ok := env.IAMServer.ListUserRolesCalls.Last() + require.True(t, ok) + assert.Equal(t, userID, req.GetUserId()) +} + +func TestUserDescribe_PermissionsDeduplicatedWithRoles(t *testing.T) { + env := testutil.NewTestEnv(t) + + userID := testUserID + cat := testRoleCategory + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: userID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.ListUserRolesCalls.Returns(&iamv1.ListUserRolesResponse{ + Roles: []*iamv1.Role{ + { + Id: "role-id-1", + Name: "admin", + Permissions: []*iamv1.Permission{ + {Value: "read:clusters", Category: &cat}, + {Value: "write:clusters", Category: &cat}, + }, + }, + { + Id: "role-id-2", + Name: "viewer", + Permissions: []*iamv1.Permission{ + {Value: "read:clusters", Category: &cat}, + }, + }, + }, + }, nil) + + stdout, _, err := testutil.Exec(t, env, "iam", "user", "describe", userID) + require.NoError(t, err) + + // read:clusters appears in both roles — should be listed once with both role names + assert.Contains(t, stdout, "admin, viewer") + // write:clusters only in admin + assert.Contains(t, stdout, "write:clusters") +} + +func TestUserDescribe_NoPermissions(t *testing.T) { + env := testutil.NewTestEnv(t) + + userID := testUserID + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{ + {Id: userID, Email: "alice@example.com"}, + }, + }, nil) + env.IAMServer.ListUserRolesCalls.Returns(&iamv1.ListUserRolesResponse{ + Roles: []*iamv1.Role{ + {Id: "role-id-1", Name: "viewer"}, + }, + }, nil) + + stdout, _, err := testutil.Exec(t, env, "iam", "user", "describe", userID) + require.NoError(t, err) + + assert.NotContains(t, stdout, "Effective Permissions") +} + +func TestUserDescribe_ByEmail(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{ + {Id: "user-id-abc", Email: "alice@example.com"}, + }, + }, nil) + env.IAMServer.ListUserRolesCalls.Returns(&iamv1.ListUserRolesResponse{}, nil) + + stdout, _, err := testutil.Exec(t, env, "iam", "user", "describe", "alice@example.com") + require.NoError(t, err) + + assert.Contains(t, stdout, "alice@example.com") + req, ok := env.IAMServer.ListUserRolesCalls.Last() + require.True(t, ok) + assert.Equal(t, "user-id-abc", req.GetUserId()) +} + +func TestUserDescribe_JSON(t *testing.T) { + env := testutil.NewTestEnv(t) + + userID := testUserID + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{ + {Id: userID, Email: "alice@example.com", Status: iamv1.UserStatus_USER_STATUS_ACTIVE}, + }, + }, nil) + // JSON output does not fetch roles; no ListUserRoles call expected. + + stdout, _, err := testutil.Exec(t, env, "iam", "user", "describe", userID, "--json") + require.NoError(t, err) + + var got struct { + Id string `json:"id"` + Email string `json:"email"` + } + require.NoError(t, json.Unmarshal([]byte(stdout), &got)) + assert.Equal(t, userID, got.Id) + assert.Equal(t, "alice@example.com", got.Email) +} + +func TestUserDescribe_NotFound(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{Items: nil}, nil) + + _, _, err := testutil.Exec(t, env, "iam", "user", "describe", "nobody@example.com") + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestUserDescribe_ListUsersError(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(nil, fmt.Errorf("permission denied")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "describe", testUserID) + require.Error(t, err) + assert.Contains(t, err.Error(), "permission denied") +} + +func TestUserDescribe_ListUserRolesError(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: testUserID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.ListUserRolesCalls.Returns(nil, fmt.Errorf("internal server error")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "describe", testUserID) + require.Error(t, err) + assert.Contains(t, err.Error(), "internal server error") +} diff --git a/internal/cmd/iam/user_list.go b/internal/cmd/iam/user_list.go new file mode 100644 index 0000000..4f03b4b --- /dev/null +++ b/internal/cmd/iam/user_list.go @@ -0,0 +1,62 @@ +package iam + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1" + + "github.com/qdrant/qcloud-cli/internal/cmd/base" + "github.com/qdrant/qcloud-cli/internal/cmd/output" + "github.com/qdrant/qcloud-cli/internal/state" +) + +func newUserListCommand(s *state.State) *cobra.Command { + return base.ListCmd[*iamv1.ListUsersResponse]{ + Use: "list", + Short: "List users in the account", + Long: `List users in the account. + +Lists all users who are members of the current account. Requires the read:users +permission.`, + Example: `# List all users in the account +qcloud iam user list + +# Output as JSON +qcloud iam user list --json`, + Fetch: func(s *state.State, cmd *cobra.Command) (*iamv1.ListUsersResponse, error) { + ctx := cmd.Context() + client, err := s.Client(ctx) + if err != nil { + return nil, err + } + accountID, err := s.AccountID() + if err != nil { + return nil, err + } + resp, err := client.IAM().ListUsers(ctx, &iamv1.ListUsersRequest{ + AccountId: accountID, + }) + if err != nil { + return nil, fmt.Errorf("failed to list users: %w", err) + } + return resp, nil + }, + PrintText: func(_ *cobra.Command, w io.Writer, resp *iamv1.ListUsersResponse) error { + t := output.NewTable[*iamv1.User](w) + t.AddField("ID", func(v *iamv1.User) string { return v.GetId() }) + t.AddField("EMAIL", func(v *iamv1.User) string { return v.GetEmail() }) + t.AddField("STATUS", func(v *iamv1.User) string { return output.UserStatus(v.GetStatus()) }) + t.AddField("CREATED", func(v *iamv1.User) string { + if v.GetCreatedAt() != nil { + return output.HumanTime(v.GetCreatedAt().AsTime()) + } + return "" + }) + t.Write(resp.GetItems()) + return nil + }, + }.CobraCommand(s) +} diff --git a/internal/cmd/iam/user_list_test.go b/internal/cmd/iam/user_list_test.go new file mode 100644 index 0000000..3042f6e --- /dev/null +++ b/internal/cmd/iam/user_list_test.go @@ -0,0 +1,70 @@ +package iam_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1" + + "github.com/qdrant/qcloud-cli/internal/testutil" +) + +func TestUserList_TableOutput(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{ + { + Id: "user-1", + Email: "alice@example.com", + Status: iamv1.UserStatus_USER_STATUS_ACTIVE, + }, + { + Id: "user-2", + Email: "bob@example.com", + Status: iamv1.UserStatus_USER_STATUS_BLOCKED, + }, + }, + }, nil) + + stdout, _, err := testutil.Exec(t, env, "iam", "user", "list") + require.NoError(t, err) + + assert.Contains(t, stdout, "user-1") + assert.Contains(t, stdout, "alice@example.com") + assert.Contains(t, stdout, "ACTIVE") + assert.Contains(t, stdout, "user-2") + assert.Contains(t, stdout, "bob@example.com") + assert.Contains(t, stdout, "BLOCKED") + + req, ok := env.IAMServer.ListUsersCalls.Last() + require.True(t, ok) + assert.Equal(t, "test-account-id", req.GetAccountId()) +} + +func TestUserList_JSONOutput(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: "user-1", Email: "alice@example.com"}}, + }, nil) + + stdout, _, err := testutil.Exec(t, env, "iam", "user", "list", "--json") + require.NoError(t, err) + + assert.Contains(t, stdout, `"id"`) + assert.Contains(t, stdout, "user-1") +} + +func TestUserList_Error(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(nil, fmt.Errorf("permission denied")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "list") + require.Error(t, err) + assert.Contains(t, err.Error(), "permission denied") +} diff --git a/internal/cmd/iam/user_remove_role.go b/internal/cmd/iam/user_remove_role.go new file mode 100644 index 0000000..484c715 --- /dev/null +++ b/internal/cmd/iam/user_remove_role.go @@ -0,0 +1,64 @@ +package iam + +import ( + "github.com/spf13/cobra" + + "github.com/qdrant/qcloud-cli/internal/cmd/base" + "github.com/qdrant/qcloud-cli/internal/cmd/completion" + "github.com/qdrant/qcloud-cli/internal/cmd/util" + "github.com/qdrant/qcloud-cli/internal/state" +) + +func newUserRemoveRoleCommand(s *state.State) *cobra.Command { + return base.Cmd{ + BaseCobraCommand: func() *cobra.Command { + cmd := &cobra.Command{ + Use: "remove-role ", + Short: "Remove one or more roles from a user", + Args: util.ExactArgs(1, "a user ID or email"), + } + + _ = cmd.Flags().StringSliceP("role", "r", nil, "A role ID or name") + _ = cmd.RegisterFlagCompletionFunc("role", completion.RoleCompletion(s)) + return cmd + }, + ValidArgsFunction: userCompletion(s), + Long: `Remove one or more roles from a user in the account. + +Accepts either a user ID (UUID) or an email address to identify the user. +Each role accepts either a role UUID or a role name, which is +resolved to an ID via the IAM service. Prints the user's resulting roles +after the removal.`, + Example: `# Remove a role by name +qcloud iam user remove-role user@example.com --role admin + +# Remove a role by ID +qcloud iam user remove-role user@example.com --role 7b2ea926-724b-4de2-b73a-8675c42a6ebe + +# Remove multiple roles at once +qcloud iam user remove-role user@example.com --role admin --role viewer`, + Run: func(s *state.State, cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := s.Client(ctx) + if err != nil { + return err + } + accountID, err := s.AccountID() + if err != nil { + return err + } + user, err := resolveUser(cmd, client, accountID, args[0]) + if err != nil { + return err + } + + roles, _ := cmd.Flags().GetStringSlice("role") + roleIDs, err := resolveRoleIDs(ctx, client, accountID, roles) + if err != nil { + return err + } + + return modifyUserRoles(s, cmd, client, accountID, user, nil, roleIDs) + }, + }.CobraCommand(s) +} diff --git a/internal/cmd/iam/user_remove_role_test.go b/internal/cmd/iam/user_remove_role_test.go new file mode 100644 index 0000000..e8f5a71 --- /dev/null +++ b/internal/cmd/iam/user_remove_role_test.go @@ -0,0 +1,137 @@ +package iam_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1" + + "github.com/qdrant/qcloud-cli/internal/testutil" +) + +func TestUserRemoveRole_ByRoleID(t *testing.T) { + env := testutil.NewTestEnv(t) + + userID := testUserID + roleID := testRoleID + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: userID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.AssignUserRolesCalls.Returns(&iamv1.AssignUserRolesResponse{}, nil) + env.IAMServer.ListUserRolesCalls.Returns(&iamv1.ListUserRolesResponse{}, nil) + + stdout, _, err := testutil.Exec(t, env, "iam", "user", "remove-role", + "alice@example.com", "--role", roleID) + require.NoError(t, err) + assert.Contains(t, stdout, "alice@example.com") + + req, ok := env.IAMServer.AssignUserRolesCalls.Last() + require.True(t, ok) + assert.Equal(t, userID, req.GetUserId()) + assert.Equal(t, []string{roleID}, req.GetRoleIdsToDelete()) +} + +func TestUserRemoveRole_ByRoleName(t *testing.T) { + env := testutil.NewTestEnv(t) + + userID := testUserID + roleID := testRoleID + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: userID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{ + Items: []*iamv1.Role{{Id: roleID, Name: "viewer"}}, + }, nil) + env.IAMServer.AssignUserRolesCalls.Returns(&iamv1.AssignUserRolesResponse{}, nil) + env.IAMServer.ListUserRolesCalls.Returns(&iamv1.ListUserRolesResponse{}, nil) + + _, _, err := testutil.Exec(t, env, "iam", "user", "remove-role", + "alice@example.com", "--role", "viewer") + require.NoError(t, err) + + req, ok := env.IAMServer.AssignUserRolesCalls.Last() + require.True(t, ok) + assert.Equal(t, []string{roleID}, req.GetRoleIdsToDelete()) +} + +func TestUserRemoveRole_MissingRole(t *testing.T) { + env := testutil.NewTestEnv(t) + + _, _, err := testutil.Exec(t, env, "iam", "user", "remove-role", "alice@example.com") + require.Error(t, err) +} + +func TestUserRemoveRole_ResolveUserError(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(nil, fmt.Errorf("connection refused")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "remove-role", + "alice@example.com", "--role", testRoleID) + require.Error(t, err) + assert.Contains(t, err.Error(), "connection refused") +} + +func TestUserRemoveRole_ResolveRoleError(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: testUserID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.ListRolesCalls.Returns(nil, fmt.Errorf("service unavailable")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "remove-role", + "alice@example.com", "--role", "admin") + require.Error(t, err) + assert.Contains(t, err.Error(), "service unavailable") +} + +func TestUserRemoveRole_RoleNameNotFound(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: testUserID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{ + Items: []*iamv1.Role{{Id: testRoleID, Name: "viewer"}}, + }, nil) + + _, _, err := testutil.Exec(t, env, "iam", "user", "remove-role", + "alice@example.com", "--role", "nonexistent") + require.Error(t, err) + assert.Contains(t, err.Error(), `role "nonexistent" not found`) +} + +func TestUserRemoveRole_RemoveError(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: testUserID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.AssignUserRolesCalls.Returns(nil, fmt.Errorf("forbidden")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "remove-role", + "alice@example.com", "--role", testRoleID) + require.Error(t, err) + assert.Contains(t, err.Error(), "forbidden") +} + +func TestUserRemoveRole_ListUserRolesError(t *testing.T) { + env := testutil.NewTestEnv(t) + + env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{ + Items: []*iamv1.User{{Id: testUserID, Email: "alice@example.com"}}, + }, nil) + env.IAMServer.AssignUserRolesCalls.Returns(&iamv1.AssignUserRolesResponse{}, nil) + env.IAMServer.ListUserRolesCalls.Returns(nil, fmt.Errorf("timeout")) + + _, _, err := testutil.Exec(t, env, "iam", "user", "remove-role", + "alice@example.com", "--role", testRoleID) + require.Error(t, err) + assert.Contains(t, err.Error(), "timeout") +} diff --git a/internal/cmd/output/iam.go b/internal/cmd/output/iam.go index ec5c9a7..d7905c7 100644 --- a/internal/cmd/output/iam.go +++ b/internal/cmd/output/iam.go @@ -6,6 +6,11 @@ import ( iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1" ) +// UserStatus formats an iamv1.UserStatus enum for display. +func UserStatus(x iamv1.UserStatus) string { + return strings.TrimPrefix(x.String(), "USER_STATUS_") +} + // RoleType formats a RoleType enum for display. func RoleType(v iamv1.RoleType) string { return strings.TrimPrefix(v.String(), "ROLE_TYPE_") diff --git a/internal/testutil/fake_account.go b/internal/testutil/fake_account.go index 41e9eac..58b6f1d 100644 --- a/internal/testutil/fake_account.go +++ b/internal/testutil/fake_account.go @@ -11,11 +11,39 @@ import ( type FakeAccountService struct { accountv1.UnimplementedAccountServiceServer - ListAccountsCalls MethodSpy[*accountv1.ListAccountsRequest, *accountv1.ListAccountsResponse] - GetAccountCalls MethodSpy[*accountv1.GetAccountRequest, *accountv1.GetAccountResponse] - UpdateAccountCalls MethodSpy[*accountv1.UpdateAccountRequest, *accountv1.UpdateAccountResponse] - ListAccountMembersCalls MethodSpy[*accountv1.ListAccountMembersRequest, *accountv1.ListAccountMembersResponse] - GetAccountMemberCalls MethodSpy[*accountv1.GetAccountMemberRequest, *accountv1.GetAccountMemberResponse] + ListAccountInvitesCalls MethodSpy[*accountv1.ListAccountInvitesRequest, *accountv1.ListAccountInvitesResponse] + GetAccountInviteCalls MethodSpy[*accountv1.GetAccountInviteRequest, *accountv1.GetAccountInviteResponse] + CreateAccountInviteCalls MethodSpy[*accountv1.CreateAccountInviteRequest, *accountv1.CreateAccountInviteResponse] + DeleteAccountInviteCalls MethodSpy[*accountv1.DeleteAccountInviteRequest, *accountv1.DeleteAccountInviteResponse] + ListAccountsCalls MethodSpy[*accountv1.ListAccountsRequest, *accountv1.ListAccountsResponse] + GetAccountCalls MethodSpy[*accountv1.GetAccountRequest, *accountv1.GetAccountResponse] + UpdateAccountCalls MethodSpy[*accountv1.UpdateAccountRequest, *accountv1.UpdateAccountResponse] + ListAccountMembersCalls MethodSpy[*accountv1.ListAccountMembersRequest, *accountv1.ListAccountMembersResponse] + GetAccountMemberCalls MethodSpy[*accountv1.GetAccountMemberRequest, *accountv1.GetAccountMemberResponse] +} + +// ListAccountInvites records the call and dispatches via ListAccountInvitesCalls. +func (f *FakeAccountService) ListAccountInvites(ctx context.Context, req *accountv1.ListAccountInvitesRequest) (*accountv1.ListAccountInvitesResponse, error) { + f.ListAccountInvitesCalls.record(req) + return f.ListAccountInvitesCalls.dispatch(ctx, req, f.UnimplementedAccountServiceServer.ListAccountInvites) +} + +// GetAccountInvite records the call and dispatches via GetAccountInviteCalls. +func (f *FakeAccountService) GetAccountInvite(ctx context.Context, req *accountv1.GetAccountInviteRequest) (*accountv1.GetAccountInviteResponse, error) { + f.GetAccountInviteCalls.record(req) + return f.GetAccountInviteCalls.dispatch(ctx, req, f.UnimplementedAccountServiceServer.GetAccountInvite) +} + +// CreateAccountInvite records the call and dispatches via CreateAccountInviteCalls. +func (f *FakeAccountService) CreateAccountInvite(ctx context.Context, req *accountv1.CreateAccountInviteRequest) (*accountv1.CreateAccountInviteResponse, error) { + f.CreateAccountInviteCalls.record(req) + return f.CreateAccountInviteCalls.dispatch(ctx, req, f.UnimplementedAccountServiceServer.CreateAccountInvite) +} + +// DeleteAccountInvite records the call and dispatches via DeleteAccountInviteCalls. +func (f *FakeAccountService) DeleteAccountInvite(ctx context.Context, req *accountv1.DeleteAccountInviteRequest) (*accountv1.DeleteAccountInviteResponse, error) { + f.DeleteAccountInviteCalls.record(req) + return f.DeleteAccountInviteCalls.dispatch(ctx, req, f.UnimplementedAccountServiceServer.DeleteAccountInvite) } // ListAccounts records the call and dispatches via ListAccountsCalls. diff --git a/internal/testutil/fake_iam.go b/internal/testutil/fake_iam.go index 3009600..4cb7347 100644 --- a/internal/testutil/fake_iam.go +++ b/internal/testutil/fake_iam.go @@ -11,12 +11,40 @@ import ( type FakeIAMService struct { iamv1.UnimplementedIAMServiceServer - ListRolesCalls MethodSpy[*iamv1.ListRolesRequest, *iamv1.ListRolesResponse] - GetRoleCalls MethodSpy[*iamv1.GetRoleRequest, *iamv1.GetRoleResponse] - CreateRoleCalls MethodSpy[*iamv1.CreateRoleRequest, *iamv1.CreateRoleResponse] - UpdateRoleCalls MethodSpy[*iamv1.UpdateRoleRequest, *iamv1.UpdateRoleResponse] - DeleteRoleCalls MethodSpy[*iamv1.DeleteRoleRequest, *iamv1.DeleteRoleResponse] - ListPermissionsCalls MethodSpy[*iamv1.ListPermissionsRequest, *iamv1.ListPermissionsResponse] + GetAuthenticatedUserCalls MethodSpy[*iamv1.GetAuthenticatedUserRequest, *iamv1.GetAuthenticatedUserResponse] + ListUsersCalls MethodSpy[*iamv1.ListUsersRequest, *iamv1.ListUsersResponse] + ListUserRolesCalls MethodSpy[*iamv1.ListUserRolesRequest, *iamv1.ListUserRolesResponse] + AssignUserRolesCalls MethodSpy[*iamv1.AssignUserRolesRequest, *iamv1.AssignUserRolesResponse] + ListRolesCalls MethodSpy[*iamv1.ListRolesRequest, *iamv1.ListRolesResponse] + GetRoleCalls MethodSpy[*iamv1.GetRoleRequest, *iamv1.GetRoleResponse] + CreateRoleCalls MethodSpy[*iamv1.CreateRoleRequest, *iamv1.CreateRoleResponse] + UpdateRoleCalls MethodSpy[*iamv1.UpdateRoleRequest, *iamv1.UpdateRoleResponse] + DeleteRoleCalls MethodSpy[*iamv1.DeleteRoleRequest, *iamv1.DeleteRoleResponse] + ListPermissionsCalls MethodSpy[*iamv1.ListPermissionsRequest, *iamv1.ListPermissionsResponse] +} + +// GetAuthenticatedUser records the call and dispatches via GetAuthenticatedUserCalls. +func (f *FakeIAMService) GetAuthenticatedUser(ctx context.Context, req *iamv1.GetAuthenticatedUserRequest) (*iamv1.GetAuthenticatedUserResponse, error) { + f.GetAuthenticatedUserCalls.record(req) + return f.GetAuthenticatedUserCalls.dispatch(ctx, req, f.UnimplementedIAMServiceServer.GetAuthenticatedUser) +} + +// ListUsers records the call and dispatches via ListUsersCalls. +func (f *FakeIAMService) ListUsers(ctx context.Context, req *iamv1.ListUsersRequest) (*iamv1.ListUsersResponse, error) { + f.ListUsersCalls.record(req) + return f.ListUsersCalls.dispatch(ctx, req, f.UnimplementedIAMServiceServer.ListUsers) +} + +// ListUserRoles records the call and dispatches via ListUserRolesCalls. +func (f *FakeIAMService) ListUserRoles(ctx context.Context, req *iamv1.ListUserRolesRequest) (*iamv1.ListUserRolesResponse, error) { + f.ListUserRolesCalls.record(req) + return f.ListUserRolesCalls.dispatch(ctx, req, f.UnimplementedIAMServiceServer.ListUserRoles) +} + +// AssignUserRoles records the call and dispatches via AssignUserRolesCalls. +func (f *FakeIAMService) AssignUserRoles(ctx context.Context, req *iamv1.AssignUserRolesRequest) (*iamv1.AssignUserRolesResponse, error) { + f.AssignUserRolesCalls.record(req) + return f.AssignUserRolesCalls.dispatch(ctx, req, f.UnimplementedIAMServiceServer.AssignUserRoles) } // ListRoles records the call and dispatches via ListRolesCalls.