-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add user management commands #102
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
911bd67
feat: add user and invite commands
Davidonium ec7a3cd
chore: code format
Davidonium 15a5f34
refactor: move tests to their command files for users
Davidonium 5aa44f4
refactor: extract duplicated code into functions and reuse resolveUser
Davidonium a272ccb
fix: included the user object in the user describe command
Davidonium 8634013
refactor: add util.MinimumNArgs function to return a descriptive error
Davidonium 4094084
chore: code format
Davidonium cde16b7
fix: bring back the use of DescribeCmd for user describe
Davidonium 7cd0e8e
feat: add completion for iam user/invite commands
Davidonium 5387098
fix: remove user invite command because the api call can only be issu…
Davidonium 675c973
fix: remove invite management commands
Davidonium db34490
chore: code format
Davidonium cc07103
refactor: use --role flags instead of arguments for listing roles on
Davidonium 1715f47
fix: add api error test cases for user management commands
Davidonium 4c9a260
Merge branch 'main' into feat/dhernando/user-management
Davidonium 2514c0c
chore: remove unused lsitUserCompletion function that was only used in
Davidonium 45204ca
chore: remove unused AccountInviteStatus function
Davidonium 62061f6
Merge branch 'main' into feat/dhernando/user-management
Davidonium 8507272
Merge branch 'main' into feat/dhernando/user-management
Davidonium 6d45e91
chore: code format
Davidonium c2ed03f
refactor: removed errVerb argument from the modifyUserRoles function,…
Davidonium 1e83157
chore: remove comment regarding errVerb
Davidonium 7bf9b5a
Merge branch 'main' into feat/dhernando/user-management
Davidonium File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package completion | ||
|
|
||
| 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" | ||
| ) | ||
|
|
||
| // 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 | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package iam_test | ||
|
|
||
| import ( | ||
| "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 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") | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| 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. errVerb is used in the error | ||
| // message ("failed to <errVerb> roles"). | ||
| func modifyUserRoles(s *state.State, cmd *cobra.Command, client *qcloudapi.Client, accountID string, user *iamv1.User, addIDs, removeIDs []string, errVerb 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 %s roles: %w", errVerb, 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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <user-id-or-email>", | ||
| 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, "assign") | ||
| }, | ||
| }.CobraCommand(s) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
errVerbnecessary? I think we can assume it from the arguments or just printfailed to modify roles: %wand keep an argument list a bit shorter.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's a good point, lemme change it, the less arguments the better.