diff --git a/temporalcloudcli/commands.gen.go b/temporalcloudcli/commands.gen.go index 584408e..4bfc133 100644 --- a/temporalcloudcli/commands.gen.go +++ b/temporalcloudcli/commands.gen.go @@ -1857,7 +1857,10 @@ func NewCloudNamespaceCommand(cctx *CommandContext, parent *CloudCommand) *Cloud s.Command.AddCommand(&NewCloudNamespaceMtlsCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudNamespaceRetentionCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudNamespaceSearchAttributeCommand(cctx, &s).Command) + s.Command.AddCommand(&NewCloudNamespaceServiceAccountCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudNamespaceTagCommand(cctx, &s).Command) + s.Command.AddCommand(&NewCloudNamespaceUserCommand(cctx, &s).Command) + s.Command.AddCommand(&NewCloudNamespaceUserGroupCommand(cctx, &s).Command) return &s } @@ -3955,6 +3958,55 @@ func NewCloudNamespaceSearchAttributeRenameCommand(cctx *CommandContext, parent return &s } +type CloudNamespaceServiceAccountCommand struct { + Parent *CloudNamespaceCommand + Command cobra.Command +} + +func NewCloudNamespaceServiceAccountCommand(cctx *CommandContext, parent *CloudNamespaceCommand) *CloudNamespaceServiceAccountCommand { + var s CloudNamespaceServiceAccountCommand + s.Parent = parent + s.Command.Use = "service-account" + s.Command.Short = "Inspect service accounts with access to a namespace" + s.Command.Long = "Commands for inspecting the service accounts that have access to a\nTemporal Cloud namespace." + s.Command.Args = cobra.NoArgs + s.Command.AddCommand(&NewCloudNamespaceServiceAccountListCommand(cctx, &s).Command) + return &s +} + +type CloudNamespaceServiceAccountListCommand struct { + Parent *CloudNamespaceServiceAccountCommand + Command cobra.Command + ClientOptions + NamespaceOptions + PageSize int + PageToken string +} + +func NewCloudNamespaceServiceAccountListCommand(cctx *CommandContext, parent *CloudNamespaceServiceAccountCommand) *CloudNamespaceServiceAccountListCommand { + var s CloudNamespaceServiceAccountListCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "list [flags]" + s.Command.Short = "List service accounts with access to a namespace" + if hasHighlighting { + s.Command.Long = "List the service accounts that have access to a Temporal Cloud namespace,\nincluding both directly-assigned and inherited access.\n\nExample:\n\n\x1b[1mtemporal cloud namespace service-account list --namespace my-namespace.my-account\x1b[0m" + } else { + s.Command.Long = "List the service accounts that have access to a Temporal Cloud namespace,\nincluding both directly-assigned and inherited access.\n\nExample:\n\n```\ntemporal cloud namespace service-account list --namespace my-namespace.my-account\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().IntVar(&s.PageSize, "page-size", 0, "Number of service accounts to return per page. Use for paginated results.") + s.Command.Flags().StringVar(&s.PageToken, "page-token", "", "Token for retrieving the next page of results in a paginated list.") + s.ClientOptions.BuildFlags(s.Command.Flags()) + s.NamespaceOptions.BuildFlags(s.Command.Flags()) + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type CloudNamespaceTagCommand struct { Parent *CloudNamespaceCommand Command cobra.Command @@ -4111,6 +4163,104 @@ func NewCloudNamespaceTagUpdateCommand(cctx *CommandContext, parent *CloudNamesp return &s } +type CloudNamespaceUserCommand struct { + Parent *CloudNamespaceCommand + Command cobra.Command +} + +func NewCloudNamespaceUserCommand(cctx *CommandContext, parent *CloudNamespaceCommand) *CloudNamespaceUserCommand { + var s CloudNamespaceUserCommand + s.Parent = parent + s.Command.Use = "user" + s.Command.Short = "Inspect users with access to a namespace" + s.Command.Long = "Commands for inspecting the users that have access to a Temporal Cloud\nnamespace." + s.Command.Args = cobra.NoArgs + s.Command.AddCommand(&NewCloudNamespaceUserListCommand(cctx, &s).Command) + return &s +} + +type CloudNamespaceUserListCommand struct { + Parent *CloudNamespaceUserCommand + Command cobra.Command + ClientOptions + NamespaceOptions + PageSize int + PageToken string +} + +func NewCloudNamespaceUserListCommand(cctx *CommandContext, parent *CloudNamespaceUserCommand) *CloudNamespaceUserListCommand { + var s CloudNamespaceUserListCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "list [flags]" + s.Command.Short = "List users with access to a namespace" + if hasHighlighting { + s.Command.Long = "List the users that have access to a Temporal Cloud namespace, including\nboth directly-assigned and inherited access.\n\nExample:\n\n\x1b[1mtemporal cloud namespace user list --namespace my-namespace.my-account\x1b[0m" + } else { + s.Command.Long = "List the users that have access to a Temporal Cloud namespace, including\nboth directly-assigned and inherited access.\n\nExample:\n\n```\ntemporal cloud namespace user list --namespace my-namespace.my-account\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().IntVar(&s.PageSize, "page-size", 0, "Number of users to return per page. Use for paginated results.") + s.Command.Flags().StringVar(&s.PageToken, "page-token", "", "Token for retrieving the next page of results in a paginated list.") + s.ClientOptions.BuildFlags(s.Command.Flags()) + s.NamespaceOptions.BuildFlags(s.Command.Flags()) + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + +type CloudNamespaceUserGroupCommand struct { + Parent *CloudNamespaceCommand + Command cobra.Command +} + +func NewCloudNamespaceUserGroupCommand(cctx *CommandContext, parent *CloudNamespaceCommand) *CloudNamespaceUserGroupCommand { + var s CloudNamespaceUserGroupCommand + s.Parent = parent + s.Command.Use = "user-group" + s.Command.Short = "Inspect user groups with access to a namespace" + s.Command.Long = "Commands for inspecting the user groups that have access to a Temporal\nCloud namespace." + s.Command.Args = cobra.NoArgs + s.Command.AddCommand(&NewCloudNamespaceUserGroupListCommand(cctx, &s).Command) + return &s +} + +type CloudNamespaceUserGroupListCommand struct { + Parent *CloudNamespaceUserGroupCommand + Command cobra.Command + ClientOptions + NamespaceOptions + PageSize int + PageToken string +} + +func NewCloudNamespaceUserGroupListCommand(cctx *CommandContext, parent *CloudNamespaceUserGroupCommand) *CloudNamespaceUserGroupListCommand { + var s CloudNamespaceUserGroupListCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "list [flags]" + s.Command.Short = "List user groups with access to a namespace" + if hasHighlighting { + s.Command.Long = "List the user groups that have access to a Temporal Cloud namespace,\nincluding both directly-assigned and inherited access.\n\nExample:\n\n\x1b[1mtemporal cloud namespace user-group list --namespace my-namespace.my-account\x1b[0m" + } else { + s.Command.Long = "List the user groups that have access to a Temporal Cloud namespace,\nincluding both directly-assigned and inherited access.\n\nExample:\n\n```\ntemporal cloud namespace user-group list --namespace my-namespace.my-account\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().IntVar(&s.PageSize, "page-size", 0, "Number of user groups to return per page. Use for paginated results.") + s.Command.Flags().StringVar(&s.PageToken, "page-token", "", "Token for retrieving the next page of results in a paginated list.") + s.ClientOptions.BuildFlags(s.Command.Flags()) + s.NamespaceOptions.BuildFlags(s.Command.Flags()) + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type CloudNexusCommand struct { Parent *CloudCommand Command cobra.Command diff --git a/temporalcloudcli/commands.namespace.access.go b/temporalcloudcli/commands.namespace.access.go new file mode 100644 index 0000000..3620faa --- /dev/null +++ b/temporalcloudcli/commands.namespace.access.go @@ -0,0 +1,96 @@ +package temporalcloudcli + +import ( + cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1" + identityv1 "go.temporal.io/cloud-sdk/api/identity/v1" + + "github.com/temporalio/cloud-cli/temporalcloudcli/internal/printer" +) + +// AIDEV-NOTE: These commands wrap the read-only Get*NamespaceAssignments RPCs +// added in cloud-sdk-go v0.14.x. They list the identities (users, service +// accounts, user groups) that have explicit access to a namespace. + +func (c *CloudNamespaceUserListCommand) run(cctx *CommandContext, _ []string) error { + client, err := cctx.GetCloudClient(c.ClientOptions) + if err != nil { + return err + } + res, err := client.GetUserNamespaceAssignments(cctx, &cloudservice.GetUserNamespaceAssignmentsRequest{ + Namespace: c.Namespace, + PageSize: int32(c.PageSize), + PageToken: c.PageToken, + }) + if err != nil { + return err + } + return cctx.Printer.PrintResourceList( + struct { + Users []*identityv1.UserNamespaceAssignment + NextPageToken string + }{ + Users: res.Users, + NextPageToken: res.NextPageToken, + }, + printer.PrintResourceOptions{ + Fields: []string{"Id", "Email", "NamespaceAccess", "InheritedAccess"}, + }, + printer.TableOptions{}, + ) +} + +func (c *CloudNamespaceServiceAccountListCommand) run(cctx *CommandContext, _ []string) error { + client, err := cctx.GetCloudClient(c.ClientOptions) + if err != nil { + return err + } + res, err := client.GetServiceAccountNamespaceAssignments(cctx, &cloudservice.GetServiceAccountNamespaceAssignmentsRequest{ + Namespace: c.Namespace, + PageSize: int32(c.PageSize), + PageToken: c.PageToken, + }) + if err != nil { + return err + } + return cctx.Printer.PrintResourceList( + struct { + ServiceAccounts []*identityv1.ServiceAccountNamespaceAssignment + NextPageToken string + }{ + ServiceAccounts: res.ServiceAccounts, + NextPageToken: res.NextPageToken, + }, + printer.PrintResourceOptions{ + Fields: []string{"Id", "Name", "NamespaceAccess", "InheritedAccess"}, + }, + printer.TableOptions{}, + ) +} + +func (c *CloudNamespaceUserGroupListCommand) run(cctx *CommandContext, _ []string) error { + client, err := cctx.GetCloudClient(c.ClientOptions) + if err != nil { + return err + } + res, err := client.GetUserGroupNamespaceAssignments(cctx, &cloudservice.GetUserGroupNamespaceAssignmentsRequest{ + Namespace: c.Namespace, + PageSize: int32(c.PageSize), + PageToken: c.PageToken, + }) + if err != nil { + return err + } + return cctx.Printer.PrintResourceList( + struct { + Groups []*identityv1.UserGroupNamespaceAssignment + NextPageToken string + }{ + Groups: res.Groups, + NextPageToken: res.NextPageToken, + }, + printer.PrintResourceOptions{ + Fields: []string{"Id", "DisplayName", "NamespaceAccess", "InheritedAccess"}, + }, + printer.TableOptions{}, + ) +} diff --git a/temporalcloudcli/commands.namespace.access_test.go b/temporalcloudcli/commands.namespace.access_test.go new file mode 100644 index 0000000..263cd91 --- /dev/null +++ b/temporalcloudcli/commands.namespace.access_test.go @@ -0,0 +1,324 @@ +package temporalcloudcli_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/mock" + cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1" + identityv1 "go.temporal.io/cloud-sdk/api/identity/v1" + + cloudmock "github.com/temporalio/cloud-cli/internal/cloudservice/mock" + "github.com/temporalio/cloud-cli/temporalcloudcli" +) + +func TestNamespaceUserList(t *testing.T) { + type listOutput struct { + Users []*identityv1.UserNamespaceAssignment + NextPageToken string + } + apiErr := errors.New("api error") + + tests := []struct { + name string + cmd temporalcloudcli.CloudNamespaceUserListCommand + cloudClientExpectations func(*cloudmock.MockCloudServiceClient) + expectedOutputJson any + expectedErr string + }{ + { + name: "Success", + cmd: temporalcloudcli.CloudNamespaceUserListCommand{NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}}, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetUserNamespaceAssignments(mock.Anything, &cloudservice.GetUserNamespaceAssignmentsRequest{ + Namespace: "my-namespace.my-account", + }, mock.Anything). + Return(&cloudservice.GetUserNamespaceAssignmentsResponse{ + Users: []*identityv1.UserNamespaceAssignment{ + {Id: "user-1", Email: "alice@example.com", NamespaceAccess: &identityv1.NamespaceAccess{Permission: identityv1.NamespaceAccess_PERMISSION_WRITE}}, + {Id: "user-2", Email: "bob@example.com", InheritedAccess: true}, + }, + }, nil) + }, + expectedOutputJson: listOutput{ + Users: []*identityv1.UserNamespaceAssignment{ + {Id: "user-1", Email: "alice@example.com", NamespaceAccess: &identityv1.NamespaceAccess{Permission: identityv1.NamespaceAccess_PERMISSION_WRITE}}, + {Id: "user-2", Email: "bob@example.com", InheritedAccess: true}, + }, + }, + }, + { + name: "WithPagination", + cmd: temporalcloudcli.CloudNamespaceUserListCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}, + PageSize: 10, + PageToken: "tok-abc", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetUserNamespaceAssignments(mock.Anything, &cloudservice.GetUserNamespaceAssignmentsRequest{ + Namespace: "my-namespace.my-account", + PageSize: 10, + PageToken: "tok-abc", + }, mock.Anything). + Return(&cloudservice.GetUserNamespaceAssignmentsResponse{ + Users: []*identityv1.UserNamespaceAssignment{{Id: "user-3", Email: "carol@example.com"}}, + NextPageToken: "tok-def", + }, nil) + }, + expectedOutputJson: listOutput{ + Users: []*identityv1.UserNamespaceAssignment{{Id: "user-3", Email: "carol@example.com"}}, + NextPageToken: "tok-def", + }, + }, + { + name: "ApiError", + cmd: temporalcloudcli.CloudNamespaceUserListCommand{NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}}, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetUserNamespaceAssignments(mock.Anything, mock.Anything, mock.Anything). + Return(nil, apiErr) + }, + expectedErr: "api error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + temporalcloudcli.TestCommand(t, &tt.cmd, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: tt.cloudClientExpectations, + JSONOutput: true, + ExpectedError: tt.expectedErr, + ExpectedOutputJson: tt.expectedOutputJson, + }) + }) + } +} + +// AIDEV-NOTE: Table (non-JSON) output guard tests. The list commands select +// per-type columns (Email/Name/DisplayName + InheritedAccess); these assert the +// right identity and inherited-access columns render. See PR #85 review. +func TestNamespaceUserList_TableOutput(t *testing.T) { + temporalcloudcli.TestCommand(t, &temporalcloudcli.CloudNamespaceUserListCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}, + }, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetUserNamespaceAssignments(mock.Anything, mock.Anything, mock.Anything). + Return(&cloudservice.GetUserNamespaceAssignmentsResponse{ + Users: []*identityv1.UserNamespaceAssignment{ + {Id: "user-1", Email: "alice@example.com", NamespaceAccess: &identityv1.NamespaceAccess{Permission: identityv1.NamespaceAccess_PERMISSION_WRITE}}, + {Id: "user-2", Email: "bob@example.com", InheritedAccess: true}, + }, + }, nil) + }, + ExpectedOutput: " Id Email NamespaceAccess InheritedAccess\n" + + " user-1 alice@example.com {\"permission\":\"PERMISSION_WRITE\"} \n" + + " user-2 bob@example.com true \n", + }) +} + +func TestNamespaceServiceAccountList_TableOutput(t *testing.T) { + temporalcloudcli.TestCommand(t, &temporalcloudcli.CloudNamespaceServiceAccountListCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}, + }, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccountNamespaceAssignments(mock.Anything, mock.Anything, mock.Anything). + Return(&cloudservice.GetServiceAccountNamespaceAssignmentsResponse{ + ServiceAccounts: []*identityv1.ServiceAccountNamespaceAssignment{ + {Id: "sa-1", Name: "ci-runner", NamespaceAccess: &identityv1.NamespaceAccess{Permission: identityv1.NamespaceAccess_PERMISSION_READ}}, + {Id: "sa-2", Name: "deploy-bot", InheritedAccess: true}, + }, + }, nil) + }, + ExpectedOutput: " Id Name NamespaceAccess InheritedAccess\n" + + " sa-1 ci-runner {\"permission\":\"PERMISSION_READ\"} \n" + + " sa-2 deploy-bot true \n", + }) +} + +func TestNamespaceUserGroupList_TableOutput(t *testing.T) { + temporalcloudcli.TestCommand(t, &temporalcloudcli.CloudNamespaceUserGroupListCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}, + }, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetUserGroupNamespaceAssignments(mock.Anything, mock.Anything, mock.Anything). + Return(&cloudservice.GetUserGroupNamespaceAssignmentsResponse{ + Groups: []*identityv1.UserGroupNamespaceAssignment{ + {Id: "grp-1", DisplayName: "Platform", NamespaceAccess: &identityv1.NamespaceAccess{Permission: identityv1.NamespaceAccess_PERMISSION_ADMIN}}, + {Id: "grp-2", DisplayName: "SRE", InheritedAccess: true}, + }, + }, nil) + }, + ExpectedOutput: " Id DisplayName NamespaceAccess InheritedAccess\n" + + " grp-1 Platform {\"permission\":\"PERMISSION_ADMIN\"} \n" + + " grp-2 SRE true \n", + }) +} + +func TestNamespaceServiceAccountList(t *testing.T) { + type listOutput struct { + ServiceAccounts []*identityv1.ServiceAccountNamespaceAssignment + NextPageToken string + } + apiErr := errors.New("api error") + + tests := []struct { + name string + cmd temporalcloudcli.CloudNamespaceServiceAccountListCommand + cloudClientExpectations func(*cloudmock.MockCloudServiceClient) + expectedOutputJson any + expectedErr string + }{ + { + name: "Success", + cmd: temporalcloudcli.CloudNamespaceServiceAccountListCommand{NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}}, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccountNamespaceAssignments(mock.Anything, &cloudservice.GetServiceAccountNamespaceAssignmentsRequest{ + Namespace: "my-namespace.my-account", + }, mock.Anything). + Return(&cloudservice.GetServiceAccountNamespaceAssignmentsResponse{ + ServiceAccounts: []*identityv1.ServiceAccountNamespaceAssignment{ + {Id: "sa-1", Name: "ci-runner", NamespaceAccess: &identityv1.NamespaceAccess{Permission: identityv1.NamespaceAccess_PERMISSION_READ}}, + }, + }, nil) + }, + expectedOutputJson: listOutput{ + ServiceAccounts: []*identityv1.ServiceAccountNamespaceAssignment{ + {Id: "sa-1", Name: "ci-runner", NamespaceAccess: &identityv1.NamespaceAccess{Permission: identityv1.NamespaceAccess_PERMISSION_READ}}, + }, + }, + }, + { + name: "WithPagination", + cmd: temporalcloudcli.CloudNamespaceServiceAccountListCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}, + PageSize: 5, + PageToken: "tok-abc", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccountNamespaceAssignments(mock.Anything, &cloudservice.GetServiceAccountNamespaceAssignmentsRequest{ + Namespace: "my-namespace.my-account", + PageSize: 5, + PageToken: "tok-abc", + }, mock.Anything). + Return(&cloudservice.GetServiceAccountNamespaceAssignmentsResponse{ + ServiceAccounts: []*identityv1.ServiceAccountNamespaceAssignment{{Id: "sa-2", Name: "deploy-bot"}}, + NextPageToken: "tok-def", + }, nil) + }, + expectedOutputJson: listOutput{ + ServiceAccounts: []*identityv1.ServiceAccountNamespaceAssignment{{Id: "sa-2", Name: "deploy-bot"}}, + NextPageToken: "tok-def", + }, + }, + { + name: "ApiError", + cmd: temporalcloudcli.CloudNamespaceServiceAccountListCommand{NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}}, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccountNamespaceAssignments(mock.Anything, mock.Anything, mock.Anything). + Return(nil, apiErr) + }, + expectedErr: "api error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + temporalcloudcli.TestCommand(t, &tt.cmd, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: tt.cloudClientExpectations, + JSONOutput: true, + ExpectedError: tt.expectedErr, + ExpectedOutputJson: tt.expectedOutputJson, + }) + }) + } +} + +func TestNamespaceUserGroupList(t *testing.T) { + type listOutput struct { + Groups []*identityv1.UserGroupNamespaceAssignment + NextPageToken string + } + apiErr := errors.New("api error") + + tests := []struct { + name string + cmd temporalcloudcli.CloudNamespaceUserGroupListCommand + cloudClientExpectations func(*cloudmock.MockCloudServiceClient) + expectedOutputJson any + expectedErr string + }{ + { + name: "Success", + cmd: temporalcloudcli.CloudNamespaceUserGroupListCommand{NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}}, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetUserGroupNamespaceAssignments(mock.Anything, &cloudservice.GetUserGroupNamespaceAssignmentsRequest{ + Namespace: "my-namespace.my-account", + }, mock.Anything). + Return(&cloudservice.GetUserGroupNamespaceAssignmentsResponse{ + Groups: []*identityv1.UserGroupNamespaceAssignment{ + {Id: "grp-1", DisplayName: "Platform", NamespaceAccess: &identityv1.NamespaceAccess{Permission: identityv1.NamespaceAccess_PERMISSION_ADMIN}}, + }, + }, nil) + }, + expectedOutputJson: listOutput{ + Groups: []*identityv1.UserGroupNamespaceAssignment{ + {Id: "grp-1", DisplayName: "Platform", NamespaceAccess: &identityv1.NamespaceAccess{Permission: identityv1.NamespaceAccess_PERMISSION_ADMIN}}, + }, + }, + }, + { + name: "WithPagination", + cmd: temporalcloudcli.CloudNamespaceUserGroupListCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}, + PageSize: 5, + PageToken: "tok-abc", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetUserGroupNamespaceAssignments(mock.Anything, &cloudservice.GetUserGroupNamespaceAssignmentsRequest{ + Namespace: "my-namespace.my-account", + PageSize: 5, + PageToken: "tok-abc", + }, mock.Anything). + Return(&cloudservice.GetUserGroupNamespaceAssignmentsResponse{ + Groups: []*identityv1.UserGroupNamespaceAssignment{{Id: "grp-2", DisplayName: "SRE"}}, + NextPageToken: "tok-def", + }, nil) + }, + expectedOutputJson: listOutput{ + Groups: []*identityv1.UserGroupNamespaceAssignment{{Id: "grp-2", DisplayName: "SRE"}}, + NextPageToken: "tok-def", + }, + }, + { + name: "ApiError", + cmd: temporalcloudcli.CloudNamespaceUserGroupListCommand{NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-namespace.my-account"}}, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetUserGroupNamespaceAssignments(mock.Anything, mock.Anything, mock.Anything). + Return(nil, apiErr) + }, + expectedErr: "api error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + temporalcloudcli.TestCommand(t, &tt.cmd, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: tt.cloudClientExpectations, + JSONOutput: true, + ExpectedError: tt.expectedErr, + ExpectedOutputJson: tt.expectedOutputJson, + }) + }) + } +} diff --git a/temporalcloudcli/commands.yml b/temporalcloudcli/commands.yml index f602d8a..30623b1 100644 --- a/temporalcloudcli/commands.yml +++ b/temporalcloudcli/commands.yml @@ -1638,6 +1638,98 @@ commands: must be greater than 0 when --capacity-mode is 'provisioned'. Ignored when --capacity-mode is 'on_demand'. + # Namespace access commands (who has access to a namespace) + - name: cloud namespace user + summary: Inspect users with access to a namespace + description: | + Commands for inspecting the users that have access to a Temporal Cloud + namespace. + has-init: false + - name: cloud namespace user list + summary: List users with access to a namespace + description: | + List the users that have access to a Temporal Cloud namespace, including + both directly-assigned and inherited access. + + Example: + + ``` + temporal cloud namespace user list --namespace my-namespace.my-account + ``` + has-init: false + option-sets: + - client + - namespace + options: + - name: page-size + type: int + description: | + Number of users to return per page. Use for paginated results. + - name: page-token + type: string + description: | + Token for retrieving the next page of results in a paginated list. + - name: cloud namespace service-account + summary: Inspect service accounts with access to a namespace + description: | + Commands for inspecting the service accounts that have access to a + Temporal Cloud namespace. + has-init: false + - name: cloud namespace service-account list + summary: List service accounts with access to a namespace + description: | + List the service accounts that have access to a Temporal Cloud namespace, + including both directly-assigned and inherited access. + + Example: + + ``` + temporal cloud namespace service-account list --namespace my-namespace.my-account + ``` + has-init: false + option-sets: + - client + - namespace + options: + - name: page-size + type: int + description: | + Number of service accounts to return per page. Use for paginated results. + - name: page-token + type: string + description: | + Token for retrieving the next page of results in a paginated list. + - name: cloud namespace user-group + summary: Inspect user groups with access to a namespace + description: | + Commands for inspecting the user groups that have access to a Temporal + Cloud namespace. + has-init: false + - name: cloud namespace user-group list + summary: List user groups with access to a namespace + description: | + List the user groups that have access to a Temporal Cloud namespace, + including both directly-assigned and inherited access. + + Example: + + ``` + temporal cloud namespace user-group list --namespace my-namespace.my-account + ``` + has-init: false + option-sets: + - client + - namespace + options: + - name: page-size + type: int + description: | + Number of user groups to return per page. Use for paginated results. + - name: page-token + type: string + description: | + Token for retrieving the next page of results in a paginated list. + # Nexus endpoint commands - name: cloud nexus summary: Manage Temporal Cloud Nexus Operations