Skip to content

Commit 28d3b38

Browse files
authored
feat: add role management commands (#107)
* feat: add role management commands * chore: use variadic args instead of calling AddCommand on every command under iam
1 parent fd71d02 commit 28d3b38

25 files changed

+1713
-1
lines changed

internal/cmd/completion/iam.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package completion
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1"
7+
8+
"github.com/qdrant/qcloud-cli/internal/state"
9+
)
10+
11+
// RoleIDCompletion returns a ValidArgsFunction that completes role IDs.
12+
func RoleIDCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
13+
return func(cmd *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
14+
if len(args) > 0 {
15+
return nil, cobra.ShellCompDirectiveNoFileComp
16+
}
17+
18+
ctx := cmd.Context()
19+
client, err := s.Client(ctx)
20+
if err != nil {
21+
return nil, cobra.ShellCompDirectiveError
22+
}
23+
24+
accountID, err := s.AccountID()
25+
if err != nil {
26+
return nil, cobra.ShellCompDirectiveError
27+
}
28+
29+
resp, err := client.IAM().ListRoles(ctx, &iamv1.ListRolesRequest{
30+
AccountId: accountID,
31+
})
32+
if err != nil {
33+
return nil, cobra.ShellCompDirectiveError
34+
}
35+
36+
completions := make([]string, 0, len(resp.GetItems()))
37+
for _, r := range resp.GetItems() {
38+
completions = append(completions, r.GetId()+"\t"+r.GetName())
39+
}
40+
return completions, cobra.ShellCompDirectiveNoFileComp
41+
}
42+
}
43+
44+
// PermissionCompletion returns a completion function for the --permission flag.
45+
func PermissionCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
46+
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
47+
ctx := cmd.Context()
48+
client, err := s.Client(ctx)
49+
if err != nil {
50+
return nil, cobra.ShellCompDirectiveError
51+
}
52+
53+
accountID, err := s.AccountID()
54+
if err != nil {
55+
return nil, cobra.ShellCompDirectiveError
56+
}
57+
58+
resp, err := client.IAM().ListPermissions(ctx, &iamv1.ListPermissionsRequest{
59+
AccountId: accountID,
60+
})
61+
if err != nil {
62+
return nil, cobra.ShellCompDirectiveError
63+
}
64+
65+
completions := make([]string, 0, len(resp.GetPermissions()))
66+
for _, p := range resp.GetPermissions() {
67+
completions = append(completions, p.GetValue()+"\t"+p.GetCategory())
68+
}
69+
return completions, cobra.ShellCompDirectiveNoFileComp
70+
}
71+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package iam_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1"
10+
11+
"github.com/qdrant/qcloud-cli/internal/testutil"
12+
)
13+
14+
func TestRoleIDCompletion_Describe(t *testing.T) {
15+
env := testutil.NewTestEnv(t)
16+
env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{
17+
Items: []*iamv1.Role{
18+
{Id: "role-uuid-1", Name: "Admin"},
19+
{Id: "role-uuid-2", Name: "Viewer"},
20+
},
21+
}, nil)
22+
23+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "describe", "")
24+
require.NoError(t, err)
25+
assert.Contains(t, stdout, "role-uuid-1")
26+
assert.Contains(t, stdout, "Admin")
27+
assert.Contains(t, stdout, "role-uuid-2")
28+
assert.Contains(t, stdout, "Viewer")
29+
}
30+
31+
func TestRoleIDCompletion_Delete(t *testing.T) {
32+
env := testutil.NewTestEnv(t)
33+
env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{
34+
Items: []*iamv1.Role{
35+
{Id: "role-uuid-1", Name: "Admin"},
36+
},
37+
}, nil)
38+
39+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "delete", "")
40+
require.NoError(t, err)
41+
assert.Contains(t, stdout, "role-uuid-1")
42+
assert.Contains(t, stdout, "Admin")
43+
}
44+
45+
func TestRoleIDCompletion_AssignPermission(t *testing.T) {
46+
env := testutil.NewTestEnv(t)
47+
env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{
48+
Items: []*iamv1.Role{
49+
{Id: "role-uuid-1", Name: "Custom Role"},
50+
},
51+
}, nil)
52+
53+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "assign-permission", "")
54+
require.NoError(t, err)
55+
assert.Contains(t, stdout, "role-uuid-1")
56+
assert.Contains(t, stdout, "Custom Role")
57+
}
58+
59+
func TestPermissionCompletion_Create(t *testing.T) {
60+
env := testutil.NewTestEnv(t)
61+
env.IAMServer.ListPermissionsCalls.Returns(&iamv1.ListPermissionsResponse{
62+
Permissions: []*iamv1.Permission{
63+
{Value: "read:clusters", Category: new("Cluster")},
64+
{Value: "write:backups", Category: new("Backup")},
65+
},
66+
}, nil)
67+
68+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "create", "--name", "test", "--permission", "")
69+
require.NoError(t, err)
70+
assert.Contains(t, stdout, "read:clusters")
71+
assert.Contains(t, stdout, "Cluster")
72+
assert.Contains(t, stdout, "write:backups")
73+
assert.Contains(t, stdout, "Backup")
74+
}
75+
76+
func TestPermissionCompletion_AssignPermission(t *testing.T) {
77+
env := testutil.NewTestEnv(t)
78+
env.IAMServer.ListPermissionsCalls.Returns(&iamv1.ListPermissionsResponse{
79+
Permissions: []*iamv1.Permission{
80+
{Value: "read:clusters", Category: new("Cluster")},
81+
},
82+
}, nil)
83+
84+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "assign-permission", "some-role-id", "--permission", "")
85+
require.NoError(t, err)
86+
assert.Contains(t, stdout, "read:clusters")
87+
assert.Contains(t, stdout, "Cluster")
88+
}
89+
90+
func TestPermissionCompletion_RemovePermission(t *testing.T) {
91+
env := testutil.NewTestEnv(t)
92+
env.IAMServer.ListPermissionsCalls.Returns(&iamv1.ListPermissionsResponse{
93+
Permissions: []*iamv1.Permission{
94+
{Value: "write:backups", Category: new("Backup")},
95+
},
96+
}, nil)
97+
98+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "remove-permission", "some-role-id", "--permission", "")
99+
require.NoError(t, err)
100+
assert.Contains(t, stdout, "write:backups")
101+
assert.Contains(t, stdout, "Backup")
102+
}

internal/cmd/iam/iam.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ func NewCommand(s *state.State) *cobra.Command {
1414
Long: `Manage IAM resources for the Qdrant Cloud account.`,
1515
Args: cobra.NoArgs,
1616
}
17-
cmd.AddCommand(newKeyCommand(s))
17+
cmd.AddCommand(
18+
newKeyCommand(s),
19+
newRoleCommand(s),
20+
newPermissionCommand(s),
21+
)
1822
return cmd
1923
}

internal/cmd/iam/permission.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package iam
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/qdrant/qcloud-cli/internal/state"
7+
)
8+
9+
func newPermissionCommand(s *state.State) *cobra.Command {
10+
cmd := &cobra.Command{
11+
Use: "permission",
12+
Short: "Manage permissions in Qdrant Cloud",
13+
Long: `Manage permissions for the Qdrant Cloud account.
14+
15+
Permissions represent individual access rights that can be assigned to roles.
16+
Use these commands to discover which permissions are available in the system.`,
17+
Args: cobra.NoArgs,
18+
}
19+
cmd.AddCommand(newPermissionListCommand(s))
20+
return cmd
21+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package iam
2+
3+
import (
4+
"io"
5+
6+
"github.com/spf13/cobra"
7+
8+
iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1"
9+
10+
"github.com/qdrant/qcloud-cli/internal/cmd/base"
11+
"github.com/qdrant/qcloud-cli/internal/cmd/output"
12+
"github.com/qdrant/qcloud-cli/internal/state"
13+
)
14+
15+
func newPermissionListCommand(s *state.State) *cobra.Command {
16+
return base.ListCmd[*iamv1.ListPermissionsResponse]{
17+
Use: "list",
18+
Short: "List all available permissions",
19+
Long: `List all permissions known in the system for the account.
20+
21+
Permissions are the individual access rights that can be assigned to roles.
22+
Each permission has a value (e.g. "read:clusters") and a category
23+
(e.g. "Cluster").`,
24+
Example: `# List all available permissions
25+
qcloud iam permission list
26+
27+
# Output as JSON
28+
qcloud iam permission list --json`,
29+
Fetch: func(s *state.State, cmd *cobra.Command) (*iamv1.ListPermissionsResponse, error) {
30+
ctx := cmd.Context()
31+
client, err := s.Client(ctx)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
accountID, err := s.AccountID()
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
return client.IAM().ListPermissions(ctx, &iamv1.ListPermissionsRequest{
42+
AccountId: accountID,
43+
})
44+
},
45+
PrintText: func(_ *cobra.Command, w io.Writer, resp *iamv1.ListPermissionsResponse) error {
46+
t := output.NewTable[*iamv1.Permission](w)
47+
t.AddField("PERMISSION", func(v *iamv1.Permission) string {
48+
return v.GetValue()
49+
})
50+
t.AddField("CATEGORY", func(v *iamv1.Permission) string {
51+
return v.GetCategory()
52+
})
53+
t.Write(resp.GetPermissions())
54+
return nil
55+
},
56+
}.CobraCommand(s)
57+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package iam_test
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1"
12+
13+
"github.com/qdrant/qcloud-cli/internal/testutil"
14+
)
15+
16+
func TestPermissionList_TableOutput(t *testing.T) {
17+
env := testutil.NewTestEnv(t, testutil.WithAccountID("test-account-id"))
18+
19+
env.IAMServer.ListPermissionsCalls.Returns(&iamv1.ListPermissionsResponse{
20+
Permissions: []*iamv1.Permission{
21+
{Value: "read:clusters", Category: new("Cluster")},
22+
{Value: "write:roles", Category: new("IAM")},
23+
},
24+
}, nil)
25+
26+
stdout, _, err := testutil.Exec(t, env, "iam", "permission", "list")
27+
require.NoError(t, err)
28+
assert.Contains(t, stdout, "PERMISSION")
29+
assert.Contains(t, stdout, "CATEGORY")
30+
assert.Contains(t, stdout, "read:clusters")
31+
assert.Contains(t, stdout, "Cluster")
32+
assert.Contains(t, stdout, "write:roles")
33+
assert.Contains(t, stdout, "IAM")
34+
35+
req, ok := env.IAMServer.ListPermissionsCalls.Last()
36+
require.True(t, ok)
37+
assert.Equal(t, "test-account-id", req.GetAccountId())
38+
}
39+
40+
func TestPermissionList_JSONOutput(t *testing.T) {
41+
env := testutil.NewTestEnv(t)
42+
43+
env.IAMServer.ListPermissionsCalls.Returns(&iamv1.ListPermissionsResponse{
44+
Permissions: []*iamv1.Permission{
45+
{Value: "read:clusters", Category: new("Cluster")},
46+
},
47+
}, nil)
48+
49+
stdout, _, err := testutil.Exec(t, env, "iam", "permission", "list", "--json")
50+
require.NoError(t, err)
51+
52+
var result struct {
53+
Permissions []struct {
54+
Value string `json:"value"`
55+
Category string `json:"category"`
56+
} `json:"permissions"`
57+
}
58+
require.NoError(t, json.Unmarshal([]byte(stdout), &result))
59+
require.Len(t, result.Permissions, 1)
60+
assert.Equal(t, "read:clusters", result.Permissions[0].Value)
61+
assert.Equal(t, "Cluster", result.Permissions[0].Category)
62+
}
63+
64+
func TestPermissionList_BackendError(t *testing.T) {
65+
env := testutil.NewTestEnv(t)
66+
67+
env.IAMServer.ListPermissionsCalls.Returns(nil, fmt.Errorf("internal server error"))
68+
69+
_, _, err := testutil.Exec(t, env, "iam", "permission", "list")
70+
require.Error(t, err)
71+
}

internal/cmd/iam/role.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package iam
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/qdrant/qcloud-cli/internal/state"
7+
)
8+
9+
func newRoleCommand(s *state.State) *cobra.Command {
10+
cmd := &cobra.Command{
11+
Use: "role",
12+
Short: "Manage roles in Qdrant Cloud",
13+
Long: `Manage roles for the Qdrant Cloud account.
14+
15+
Roles define sets of permissions that control access to resources. There are two
16+
types of roles: system roles (immutable, managed by Qdrant) and custom roles
17+
(created and managed by the account). Use these commands to list, inspect, create,
18+
update, and delete custom roles, as well as manage their permissions.`,
19+
Args: cobra.NoArgs,
20+
}
21+
cmd.AddCommand(newRoleListCommand(s))
22+
cmd.AddCommand(newRoleDescribeCommand(s))
23+
cmd.AddCommand(newRoleCreateCommand(s))
24+
cmd.AddCommand(newRoleUpdateCommand(s))
25+
cmd.AddCommand(newRoleDeleteCommand(s))
26+
cmd.AddCommand(newRoleAssignPermissionCommand(s))
27+
cmd.AddCommand(newRoleRemovePermissionCommand(s))
28+
return cmd
29+
}

0 commit comments

Comments
 (0)