diff --git a/temporalcloudcli/commands.gen.go b/temporalcloudcli/commands.gen.go index 78adfa9..6e630f9 100644 --- a/temporalcloudcli/commands.gen.go +++ b/temporalcloudcli/commands.gen.go @@ -4829,6 +4829,7 @@ func NewCloudProjectCommand(cctx *CommandContext, parent *CloudCommand) *CloudPr s.Command.AddCommand(&NewCloudProjectEditCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudProjectGetCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudProjectListCommand(cctx, &s).Command) + s.Command.AddCommand(&NewCloudProjectServiceAccountCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudProjectUpdateCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudProjectUserCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudProjectUserGroupCommand(cctx, &s).Command) @@ -5040,6 +5041,56 @@ func NewCloudProjectListCommand(cctx *CommandContext, parent *CloudProjectComman return &s } +type CloudProjectServiceAccountCommand struct { + Parent *CloudProjectCommand + Command cobra.Command +} + +func NewCloudProjectServiceAccountCommand(cctx *CommandContext, parent *CloudProjectCommand) *CloudProjectServiceAccountCommand { + var s CloudProjectServiceAccountCommand + s.Parent = parent + s.Command.Use = "service-account" + s.Command.Short = "Inspect service accounts with access to a project" + s.Command.Long = "Commands for inspecting the service accounts that have access to a\nTemporal Cloud project." + s.Command.Args = cobra.NoArgs + s.Command.AddCommand(&NewCloudProjectServiceAccountListCommand(cctx, &s).Command) + return &s +} + +type CloudProjectServiceAccountListCommand struct { + Parent *CloudProjectServiceAccountCommand + Command cobra.Command + ClientOptions + ProjectId string + PageSize int + PageToken string +} + +func NewCloudProjectServiceAccountListCommand(cctx *CommandContext, parent *CloudProjectServiceAccountCommand) *CloudProjectServiceAccountListCommand { + var s CloudProjectServiceAccountListCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "list [flags]" + s.Command.Short = "List service accounts with access to a project" + if hasHighlighting { + s.Command.Long = "List the service accounts that have access to a Temporal Cloud project,\nincluding both directly-assigned and inherited access.\n\nExample:\n\n\x1b[1mtemporal cloud project service-account list --project-id my-project-id\x1b[0m" + } else { + s.Command.Long = "List the service accounts that have access to a Temporal Cloud project,\nincluding both directly-assigned and inherited access.\n\nExample:\n\n```\ntemporal cloud project service-account list --project-id my-project-id\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "The ID of the project. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "project-id") + 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.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type CloudProjectUpdateCommand struct { Parent *CloudProjectCommand Command cobra.Command @@ -5268,11 +5319,14 @@ func NewCloudServiceAccountCommand(cctx *CommandContext, parent *CloudCommand) * s.Command.Args = cobra.NoArgs s.Command.AddCommand(&NewCloudServiceAccountCreateCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudServiceAccountCreateNamespaceScopedCommand(cctx, &s).Command) + s.Command.AddCommand(&NewCloudServiceAccountCreateProjectScopedCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudServiceAccountDeleteCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudServiceAccountEditCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudServiceAccountGetCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudServiceAccountListCommand(cctx, &s).Command) + s.Command.AddCommand(&NewCloudServiceAccountRemoveProjectAccessCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudServiceAccountSetCustomRolesCommand(cctx, &s).Command) + s.Command.AddCommand(&NewCloudServiceAccountSetProjectAccessCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudServiceAccountUpdateCommand(cctx, &s).Command) return &s } @@ -5286,6 +5340,7 @@ type CloudServiceAccountCreateCommand struct { Description string AccountRole string NamespaceAccess []string + ProjectAccess []string CustomRole []string } @@ -5296,9 +5351,9 @@ func NewCloudServiceAccountCreateCommand(cctx *CommandContext, parent *CloudServ s.Command.Use = "create [flags]" s.Command.Short = "Create a service account" if hasHighlighting { - s.Command.Long = "Create a new Temporal Cloud service account with account-level access.\nOptionally assign an account role and namespace-level permissions.\n\nAccount roles: owner, admin, developer, finance-admin, read, metrics-read.\nNamespace access format: 'namespace=permission' where permission is one of: admin, write, read.\n\nExample:\n\n\x1b[1mtemporal cloud service-account create --name my-sa --account-role developer \\\n --namespace-access my-namespace.my-account=write\x1b[0m" + s.Command.Long = "Create a new Temporal Cloud service account with account-level access.\nOptionally assign an account role, namespace-level permissions, and\nproject-level roles.\n\nAccount roles: owner, admin, developer, finance-admin, read, metrics-read.\nNamespace access format: 'namespace=permission' where permission is one of: admin, write, read.\nProject access format: 'project-id=role' where role is one of: admin, write, read, list, contribute, member.\n\nExample:\n\n\x1b[1mtemporal cloud service-account create --name my-sa --account-role developer \\\n --namespace-access my-namespace.my-account=write \\\n --project-access my-project-id=write\x1b[0m" } else { - s.Command.Long = "Create a new Temporal Cloud service account with account-level access.\nOptionally assign an account role and namespace-level permissions.\n\nAccount roles: owner, admin, developer, finance-admin, read, metrics-read.\nNamespace access format: 'namespace=permission' where permission is one of: admin, write, read.\n\nExample:\n\n```\ntemporal cloud service-account create --name my-sa --account-role developer \\\n --namespace-access my-namespace.my-account=write\n```" + s.Command.Long = "Create a new Temporal Cloud service account with account-level access.\nOptionally assign an account role, namespace-level permissions, and\nproject-level roles.\n\nAccount roles: owner, admin, developer, finance-admin, read, metrics-read.\nNamespace access format: 'namespace=permission' where permission is one of: admin, write, read.\nProject access format: 'project-id=role' where role is one of: admin, write, read, list, contribute, member.\n\nExample:\n\n```\ntemporal cloud service-account create --name my-sa --account-role developer \\\n --namespace-access my-namespace.my-account=write \\\n --project-access my-project-id=write\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.Name, "name", "", "The name of the service account. Must be unique across all active service accounts. Required.") @@ -5306,6 +5361,7 @@ func NewCloudServiceAccountCreateCommand(cctx *CommandContext, parent *CloudServ s.Command.Flags().StringVar(&s.Description, "description", "", "An optional description for the service account.") s.Command.Flags().StringVar(&s.AccountRole, "account-role", "", "The account-level role to assign. Valid values: owner, admin, developer, finance-admin, read, metrics-read.") s.Command.Flags().StringArrayVar(&s.NamespaceAccess, "namespace-access", nil, "Namespace access to grant, in the format 'namespace=permission'. Permission must be one of: admin, write, read. Can be repeated.") + s.Command.Flags().StringArrayVar(&s.ProjectAccess, "project-access", nil, "Project access to grant, in the format 'project-id=role'. Role must be one of: admin, write, read, list, contribute, member. Can be repeated.") s.Command.Flags().StringArrayVar(&s.CustomRole, "custom-role", nil, "Custom role ID to assign. Repeat to assign multiple.") s.ClientOptions.BuildFlags(s.Command.Flags()) s.AsyncOperationOptions.BuildFlags(s.Command.Flags()) @@ -5357,6 +5413,48 @@ func NewCloudServiceAccountCreateNamespaceScopedCommand(cctx *CommandContext, pa return &s } +type CloudServiceAccountCreateProjectScopedCommand struct { + Parent *CloudServiceAccountCommand + Command cobra.Command + ClientOptions + AsyncOperationOptions + Name string + Description string + ProjectId string + ProjectRole string + NamespaceAccess []string +} + +func NewCloudServiceAccountCreateProjectScopedCommand(cctx *CommandContext, parent *CloudServiceAccountCommand) *CloudServiceAccountCreateProjectScopedCommand { + var s CloudServiceAccountCreateProjectScopedCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "create-project-scoped [flags]" + s.Command.Short = "Create a project-scoped service account" + if hasHighlighting { + s.Command.Long = "Create a new Temporal Cloud service account scoped to a single project.\nOptionally assign namespace-level permissions within the project.\n\nProject roles: admin, write, read, list, contribute, member.\nNamespace access format: 'namespace=permission' where permission is one of: admin, write, read.\n\nExample:\n\n\x1b[1mtemporal cloud service-account create-project-scoped --name my-sa \\\n --project-id my-project-id --project-role write \\\n --namespace-access my-namespace.my-account=read\x1b[0m" + } else { + s.Command.Long = "Create a new Temporal Cloud service account scoped to a single project.\nOptionally assign namespace-level permissions within the project.\n\nProject roles: admin, write, read, list, contribute, member.\nNamespace access format: 'namespace=permission' where permission is one of: admin, write, read.\n\nExample:\n\n```\ntemporal cloud service-account create-project-scoped --name my-sa \\\n --project-id my-project-id --project-role write \\\n --namespace-access my-namespace.my-account=read\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.Name, "name", "", "The name of the service account. Must be unique across all active service accounts. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "name") + s.Command.Flags().StringVar(&s.Description, "description", "", "An optional description for the service account.") + s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "The ID of the project to scope the service account to. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "project-id") + s.Command.Flags().StringVar(&s.ProjectRole, "project-role", "", "The project-level role to assign. Valid values: admin, write, read, list, contribute, member. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "project-role") + s.Command.Flags().StringArrayVar(&s.NamespaceAccess, "namespace-access", nil, "Namespace access to grant, in the format 'namespace=permission'. Permission must be one of: admin, write, read. Can be repeated.") + s.ClientOptions.BuildFlags(s.Command.Flags()) + s.AsyncOperationOptions.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 CloudServiceAccountDeleteCommand struct { Parent *CloudServiceAccountCommand Command cobra.Command @@ -5461,6 +5559,7 @@ type CloudServiceAccountListCommand struct { Parent *CloudServiceAccountCommand Command cobra.Command ClientOptions + ProjectId string PageSize int PageToken string } @@ -5472,11 +5571,12 @@ func NewCloudServiceAccountListCommand(cctx *CommandContext, parent *CloudServic s.Command.Use = "list [flags]" s.Command.Short = "List service accounts" if hasHighlighting { - s.Command.Long = "List all Temporal Cloud service accounts accessible with the current\nauthentication credentials.\n\nExample:\n\n\x1b[1mtemporal cloud service-account list\x1b[0m" + s.Command.Long = "List all Temporal Cloud service accounts accessible with the current\nauthentication credentials.\n\nExample:\n\n\x1b[1mtemporal cloud service-account list\ntemporal cloud service-account list --project-id my-project-id\x1b[0m" } else { - s.Command.Long = "List all Temporal Cloud service accounts accessible with the current\nauthentication credentials.\n\nExample:\n\n```\ntemporal cloud service-account list\n```" + s.Command.Long = "List all Temporal Cloud service accounts accessible with the current\nauthentication credentials.\n\nExample:\n\n```\ntemporal cloud service-account list\ntemporal cloud service-account list --project-id my-project-id\n```" } s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "The ID of the project to list project-scoped service accounts for.") 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()) @@ -5488,6 +5588,43 @@ func NewCloudServiceAccountListCommand(cctx *CommandContext, parent *CloudServic return &s } +type CloudServiceAccountRemoveProjectAccessCommand struct { + Parent *CloudServiceAccountCommand + Command cobra.Command + ClientOptions + AsyncOperationOptions + ResourceVersionOptions + ServiceAccountId string + ProjectId string +} + +func NewCloudServiceAccountRemoveProjectAccessCommand(cctx *CommandContext, parent *CloudServiceAccountCommand) *CloudServiceAccountRemoveProjectAccessCommand { + var s CloudServiceAccountRemoveProjectAccessCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "remove-project-access [flags]" + s.Command.Short = "Remove project access for a service account" + if hasHighlighting { + s.Command.Long = "Remove a Temporal Cloud service account's direct project-level access.\n\nExample:\n\n\x1b[1mtemporal cloud service-account remove-project-access --service-account-id my-sa-id \\\n --project-id my-project-id\x1b[0m" + } else { + s.Command.Long = "Remove a Temporal Cloud service account's direct project-level access.\n\nExample:\n\n```\ntemporal cloud service-account remove-project-access --service-account-id my-sa-id \\\n --project-id my-project-id\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.ServiceAccountId, "service-account-id", "", "The ID of the service account. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "service-account-id") + s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "The ID of the project. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "project-id") + s.ClientOptions.BuildFlags(s.Command.Flags()) + s.AsyncOperationOptions.BuildFlags(s.Command.Flags()) + s.ResourceVersionOptions.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 CloudServiceAccountSetCustomRolesCommand struct { Parent *CloudServiceAccountCommand Command cobra.Command @@ -5524,6 +5661,46 @@ func NewCloudServiceAccountSetCustomRolesCommand(cctx *CommandContext, parent *C return &s } +type CloudServiceAccountSetProjectAccessCommand struct { + Parent *CloudServiceAccountCommand + Command cobra.Command + ClientOptions + AsyncOperationOptions + ResourceVersionOptions + ServiceAccountId string + ProjectId string + ProjectRole string +} + +func NewCloudServiceAccountSetProjectAccessCommand(cctx *CommandContext, parent *CloudServiceAccountCommand) *CloudServiceAccountSetProjectAccessCommand { + var s CloudServiceAccountSetProjectAccessCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "set-project-access [flags]" + s.Command.Short = "Set project access for a service account" + if hasHighlighting { + s.Command.Long = "Set project-level access for a Temporal Cloud service account.\n\nProject roles: admin, write, read, list, contribute, member.\n\nExample:\n\n\x1b[1mtemporal cloud service-account set-project-access --service-account-id my-sa-id \\\n --project-id my-project-id --project-role write\x1b[0m" + } else { + s.Command.Long = "Set project-level access for a Temporal Cloud service account.\n\nProject roles: admin, write, read, list, contribute, member.\n\nExample:\n\n```\ntemporal cloud service-account set-project-access --service-account-id my-sa-id \\\n --project-id my-project-id --project-role write\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.ServiceAccountId, "service-account-id", "", "The ID of the service account. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "service-account-id") + s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "The ID of the project. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "project-id") + s.Command.Flags().StringVar(&s.ProjectRole, "project-role", "", "The project-level role to assign. Valid values: admin, write, read, list, contribute, member. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "project-role") + s.ClientOptions.BuildFlags(s.Command.Flags()) + s.AsyncOperationOptions.BuildFlags(s.Command.Flags()) + s.ResourceVersionOptions.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 CloudServiceAccountUpdateCommand struct { Parent *CloudServiceAccountCommand Command cobra.Command @@ -5536,6 +5713,8 @@ type CloudServiceAccountUpdateCommand struct { Description string AccountRole string NamespaceAccess []string + ProjectAccess []string + ProjectRole string NamespacePermission string } @@ -5546,9 +5725,9 @@ func NewCloudServiceAccountUpdateCommand(cctx *CommandContext, parent *CloudServ s.Command.Use = "update [flags]" s.Command.Short = "Update a service account" if hasHighlighting { - s.Command.Long = "Update a Temporal Cloud service account. Only flags that are explicitly provided\nare changed.\n\nFor account-scoped service accounts, use --account-role and/or --namespace-access.\nFor namespace-scoped service accounts, use --namespace-permission.\n\nNamespace access format: 'namespace=permission' where permission is one of: admin, write, read.\nUse 'namespace=' (empty permission) to remove access to a namespace.\n\nExample:\n\n\x1b[1mtemporal cloud service-account update --service-account-id my-sa-id --name new-name\ntemporal cloud service-account update --service-account-id my-sa-id --account-role admin\ntemporal cloud service-account update --service-account-id my-sa-id --namespace-permission write\x1b[0m" + s.Command.Long = "Update a Temporal Cloud service account. Only flags that are explicitly provided\nare changed.\n\nFor account-scoped service accounts, use --account-role, --namespace-access, and/or --project-access.\nFor namespace-scoped service accounts, use --namespace-permission.\nFor project-scoped service accounts, use --project-role and/or --namespace-access.\n\nNamespace access format: 'namespace=permission' where permission is one of: admin, write, read.\nUse 'namespace=' (empty permission) to remove access to a namespace.\nProject access format: 'project-id=role' where role is one of: admin, write, read, list, contribute, member.\nUse 'project-id=' (empty role) to remove access to a project.\nProject-scoped service account roles: admin, write, read, list, contribute, member.\n\nExample:\n\n\x1b[1mtemporal cloud service-account update --service-account-id my-sa-id --name new-name\ntemporal cloud service-account update --service-account-id my-sa-id --account-role admin\ntemporal cloud service-account update --service-account-id my-sa-id --project-access my-project-id=write\ntemporal cloud service-account update --service-account-id my-sa-id --project-role write\ntemporal cloud service-account update --service-account-id my-sa-id --namespace-permission write\x1b[0m" } else { - s.Command.Long = "Update a Temporal Cloud service account. Only flags that are explicitly provided\nare changed.\n\nFor account-scoped service accounts, use --account-role and/or --namespace-access.\nFor namespace-scoped service accounts, use --namespace-permission.\n\nNamespace access format: 'namespace=permission' where permission is one of: admin, write, read.\nUse 'namespace=' (empty permission) to remove access to a namespace.\n\nExample:\n\n```\ntemporal cloud service-account update --service-account-id my-sa-id --name new-name\ntemporal cloud service-account update --service-account-id my-sa-id --account-role admin\ntemporal cloud service-account update --service-account-id my-sa-id --namespace-permission write\n```" + s.Command.Long = "Update a Temporal Cloud service account. Only flags that are explicitly provided\nare changed.\n\nFor account-scoped service accounts, use --account-role, --namespace-access, and/or --project-access.\nFor namespace-scoped service accounts, use --namespace-permission.\nFor project-scoped service accounts, use --project-role and/or --namespace-access.\n\nNamespace access format: 'namespace=permission' where permission is one of: admin, write, read.\nUse 'namespace=' (empty permission) to remove access to a namespace.\nProject access format: 'project-id=role' where role is one of: admin, write, read, list, contribute, member.\nUse 'project-id=' (empty role) to remove access to a project.\nProject-scoped service account roles: admin, write, read, list, contribute, member.\n\nExample:\n\n```\ntemporal cloud service-account update --service-account-id my-sa-id --name new-name\ntemporal cloud service-account update --service-account-id my-sa-id --account-role admin\ntemporal cloud service-account update --service-account-id my-sa-id --project-access my-project-id=write\ntemporal cloud service-account update --service-account-id my-sa-id --project-role write\ntemporal cloud service-account update --service-account-id my-sa-id --namespace-permission write\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.ServiceAccountId, "service-account-id", "", "The ID of the service account to update. Required.") @@ -5556,7 +5735,9 @@ func NewCloudServiceAccountUpdateCommand(cctx *CommandContext, parent *CloudServ s.Command.Flags().StringVar(&s.Name, "name", "", "New name for the service account.") s.Command.Flags().StringVar(&s.Description, "description", "", "New description for the service account.") s.Command.Flags().StringVar(&s.AccountRole, "account-role", "", "The account-level role to assign. Valid values: owner, admin, developer, finance-admin, read, metrics-read. Only valid for account-scoped service accounts.") - s.Command.Flags().StringArrayVar(&s.NamespaceAccess, "namespace-access", nil, "Namespace access to set, in the format 'namespace=permission'. Use 'namespace=' to remove access. Permission must be one of: admin, write, read. Can be repeated. Only valid for account-scoped service accounts.") + s.Command.Flags().StringArrayVar(&s.NamespaceAccess, "namespace-access", nil, "Namespace access to set, in the format 'namespace=permission'. Use 'namespace=' to remove access. Permission must be one of: admin, write, read. Can be repeated. Only valid for account-scoped and project-scoped service accounts.") + s.Command.Flags().StringArrayVar(&s.ProjectAccess, "project-access", nil, "Project access to set, in the format 'project-id=role'. Use 'project-id=' to remove access. Role must be one of: admin, write, read, list, contribute, member. Can be repeated. Only valid for account-scoped service accounts.") + s.Command.Flags().StringVar(&s.ProjectRole, "project-role", "", "The project-level role to assign. Valid values: admin, write, read, list, contribute, member. Only valid for project-scoped service accounts.") s.Command.Flags().StringVar(&s.NamespacePermission, "namespace-permission", "", "The permission to grant on the scoped namespace. Valid values: admin, write, read. Only valid for namespace-scoped service accounts.") s.ClientOptions.BuildFlags(s.Command.Flags()) s.AsyncOperationOptions.BuildFlags(s.Command.Flags()) diff --git a/temporalcloudcli/commands.service_account.go b/temporalcloudcli/commands.service_account.go index 281f772..08cc0a9 100644 --- a/temporalcloudcli/commands.service_account.go +++ b/temporalcloudcli/commands.service_account.go @@ -48,6 +48,10 @@ func (c *CloudServiceAccountCreateCommand) run(cctx *CommandContext, _ []string) if err != nil { return err } + projectAccesses, err := parseProjectAccesses(c.ProjectAccess) + if err != nil { + return err + } if c.Command.Flags().Changed("custom-role") { if accountAccess == nil { return errors.New("--custom-role requires --account-role; a principal must have a account role") @@ -72,6 +76,7 @@ func (c *CloudServiceAccountCreateCommand) run(cctx *CommandContext, _ []string) Access: &identityv1.Access{ AccountAccess: accountAccess, NamespaceAccesses: namespaceAccesses, + ProjectAccesses: projectAccesses, }, }, AsyncOperationId: c.AsyncOperationId, @@ -109,10 +114,47 @@ func (c *CloudServiceAccountCreateNamespaceScopedCommand) run(cctx *CommandConte return cctx.GetPoller(client, c.AsyncOperationOptions).HandleCreateAsyncOperationResponse(cctx, resp, err) } +func (c *CloudServiceAccountCreateProjectScopedCommand) run(cctx *CommandContext, _ []string) error { + projectAccess, err := parseProjectRole(c.ProjectRole) + if err != nil { + return err + } + namespaceAccesses, err := parseNamespaceAccesses(c.NamespaceAccess) + if err != nil { + return err + } + yes, err := cctx.GetPrompter().PromptYes("Create") + if err != nil { + return err + } + if !yes { + return errors.New("Aborting create.") + } + client, err := cctx.GetCloudClient(c.ClientOptions) + if err != nil { + return err + } + resp, err := client.CreateServiceAccount(cctx, &cloudservice.CreateServiceAccountRequest{ + Spec: &identityv1.ServiceAccountSpec{ + Name: c.Name, + Description: c.Description, + ProjectScopedAccess: &identityv1.ProjectScopedAccess{ + ProjectId: c.ProjectId, + Access: projectAccess, + NamespaceAccesses: namespaceAccesses, + }, + }, + AsyncOperationId: c.AsyncOperationId, + }) + return cctx.GetPoller(client, c.AsyncOperationOptions).HandleCreateAsyncOperationResponse(cctx, resp, err) +} + func (c *CloudServiceAccountUpdateCommand) run(cctx *CommandContext, _ []string) error { // Validate input formats before any API call. accountRoleChanged := c.Command.Flags().Changed("account-role") namespaceAccessChanged := c.Command.Flags().Changed("namespace-access") + projectAccessChanged := c.Command.Flags().Changed("project-access") + projectRoleChanged := c.Command.Flags().Changed("project-role") namespacePermissionChanged := c.Command.Flags().Changed("namespace-permission") customRoleChanged := c.Command.Flags().Changed("custom-role") @@ -126,6 +168,16 @@ func (c *CloudServiceAccountUpdateCommand) run(cctx *CommandContext, _ []string) return err } } + if projectAccessChanged { + if _, err := applyProjectAccessChanges(nil, c.ProjectAccess); err != nil { + return err + } + } + if projectRoleChanged { + if _, err := parseProjectRole(c.ProjectRole); err != nil { + return err + } + } if namespacePermissionChanged { if _, ok := namespacePermissionNames[c.NamespacePermission]; !ok { return fmt.Errorf("invalid namespace permission %q: must be one of admin, write, read", c.NamespacePermission) @@ -144,12 +196,22 @@ func (c *CloudServiceAccountUpdateCommand) run(cctx *CommandContext, _ []string) newSpec := proto.Clone(sa.Spec).(*identityv1.ServiceAccountSpec) isNamespaceScoped := newSpec.NamespaceScopedAccess != nil + isProjectScoped := newSpec.ProjectScopedAccess != nil - if isNamespaceScoped && (accountRoleChanged || namespaceAccessChanged || customRoleChanged) { - return errors.New("--account-role, --namespace-access, and --custom-role are not valid for namespace-scoped service accounts") + if isNamespaceScoped && + (accountRoleChanged || namespaceAccessChanged || projectAccessChanged || projectRoleChanged || customRoleChanged) { + return errors.New("--account-role, --namespace-access, --project-access, --project-role, " + + "and --custom-role are not valid for namespace-scoped service accounts") + } + if isProjectScoped && (accountRoleChanged || projectAccessChanged || namespacePermissionChanged || customRoleChanged) { + return errors.New("--account-role, --project-access, --namespace-permission, " + + "and --custom-role are not valid for project-scoped service accounts") + } + if !isProjectScoped && projectRoleChanged { + return errors.New("--project-role is only valid for project-scoped service accounts") } if !isNamespaceScoped && namespacePermissionChanged { - return errors.New("--namespace-permission is not valid for account-scoped service accounts") + return errors.New("--namespace-permission is only valid for namespace-scoped service accounts") } if c.Command.Flags().Changed("name") { @@ -170,14 +232,40 @@ func (c *CloudServiceAccountUpdateCommand) run(cctx *CommandContext, _ []string) newSpec.Access.AccountAccess = newAccountAccess } if namespaceAccessChanged { + if isProjectScoped { + newSpec.ProjectScopedAccess.NamespaceAccesses, err = applyNamespaceAccessChanges( + newSpec.ProjectScopedAccess.NamespaceAccesses, + c.NamespaceAccess, + ) + } else { + if newSpec.Access == nil { + newSpec.Access = &identityv1.Access{} + } + newSpec.Access.NamespaceAccesses, err = applyNamespaceAccessChanges(newSpec.Access.NamespaceAccesses, c.NamespaceAccess) + } + if err != nil { + return err + } + } + if projectAccessChanged { if newSpec.Access == nil { newSpec.Access = &identityv1.Access{} } - newSpec.Access.NamespaceAccesses, err = applyNamespaceAccessChanges(newSpec.Access.NamespaceAccesses, c.NamespaceAccess) + newSpec.Access.ProjectAccesses, err = applyProjectAccessChanges(newSpec.Access.ProjectAccesses, c.ProjectAccess) if err != nil { return err } } + if projectRoleChanged { + projectAccess, err := parseProjectRole(c.ProjectRole) + if err != nil { + return err + } + if newSpec.ProjectScopedAccess.Access == nil { + newSpec.ProjectScopedAccess.Access = &identityv1.ProjectAccess{} + } + newSpec.ProjectScopedAccess.Access = projectAccess + } if customRoleChanged { if newSpec.Access == nil || newSpec.Access.AccountAccess == nil { return errors.New("service account has no account access; assign an account role with --account-role first") @@ -215,6 +303,107 @@ func (c *CloudServiceAccountUpdateCommand) run(cctx *CommandContext, _ []string) return cctx.GetPoller(client, c.AsyncOperationOptions).HandleUpdateOperation(cctx, resp, err) } +func (c *CloudServiceAccountSetProjectAccessCommand) run(cctx *CommandContext, _ []string) error { + projectAccess, err := parseProjectRole(c.ProjectRole) + if err != nil { + return err + } + client, err := cctx.GetCloudClient(c.ClientOptions) + if err != nil { + return err + } + res, err := client.GetServiceAccount(cctx, &cloudservice.GetServiceAccountRequest{ServiceAccountId: c.ServiceAccountId}) + if err != nil { + return err + } + yes, err := cctx.GetPrompter().PromptYes("Set project access") + if err != nil { + return err + } + if !yes { + return errors.New("Aborting set.") + } + rv := res.ServiceAccount.ResourceVersion + if c.ResourceVersion != "" { + rv = c.ResourceVersion + } + resp, err := client.SetServiceAccountProjectAccess(cctx, &cloudservice.SetServiceAccountProjectAccessRequest{ + ProjectId: c.ProjectId, + ServiceAccountId: c.ServiceAccountId, + Access: projectAccess, + ResourceVersion: rv, + AsyncOperationId: c.AsyncOperationId, + }) + return cctx.GetPoller(client, c.AsyncOperationOptions).HandleUpdateOperation(cctx, resp, err) +} + +func (c *CloudServiceAccountRemoveProjectAccessCommand) run(cctx *CommandContext, _ []string) error { + client, err := cctx.GetCloudClient(c.ClientOptions) + if err != nil { + return err + } + res, err := client.GetServiceAccount(cctx, &cloudservice.GetServiceAccountRequest{ServiceAccountId: c.ServiceAccountId}) + if err != nil { + return err + } + yes, err := cctx.GetPrompter().PromptYes("Remove project access") + if err != nil { + return err + } + if !yes { + return errors.New("Aborting remove.") + } + rv := res.ServiceAccount.ResourceVersion + if c.ResourceVersion != "" { + rv = c.ResourceVersion + } + resp, err := client.SetServiceAccountProjectAccess(cctx, &cloudservice.SetServiceAccountProjectAccessRequest{ + ProjectId: c.ProjectId, + ServiceAccountId: c.ServiceAccountId, + ResourceVersion: rv, + AsyncOperationId: c.AsyncOperationId, + }) + return cctx.GetPoller(client, c.AsyncOperationOptions).HandleUpdateOperation(cctx, resp, err) +} + +func (c *CloudProjectServiceAccountListCommand) run(cctx *CommandContext, _ []string) error { + return listServiceAccountProjectAssignments(cctx, c.ProjectId, c.PageSize, c.PageToken, c.ClientOptions) +} + +func listServiceAccountProjectAssignments( + cctx *CommandContext, + projectID string, + pageSize int, + pageToken string, + clientOptions ClientOptions, +) error { + client, err := cctx.GetCloudClient(clientOptions) + if err != nil { + return err + } + res, err := client.GetServiceAccountProjectAssignments(cctx, &cloudservice.GetServiceAccountProjectAssignmentsRequest{ + ProjectId: projectID, + PageSize: int32(pageSize), + PageToken: pageToken, + }) + if err != nil { + return err + } + return cctx.Printer.PrintResourceList( + struct { + ServiceAccounts []*identityv1.ServiceAccountProjectAssignment + NextPageToken string + }{ + ServiceAccounts: res.ServiceAccounts, + NextPageToken: res.NextPageToken, + }, + printer.PrintResourceOptions{ + Fields: []string{"Id", "Name", "ProjectAccess", "InheritedAccess"}, + }, + printer.TableOptions{}, + ) +} + func (c *CloudServiceAccountEditCommand) run(cctx *CommandContext, _ []string) error { client, err := cctx.GetCloudClient(c.ClientOptions) if err != nil { @@ -270,6 +459,30 @@ func (c *CloudServiceAccountListCommand) run(cctx *CommandContext, _ []string) e if err != nil { return err } + if c.ProjectId != "" { + res, err := client.GetProjectScopedServiceAccounts(cctx, &cloudservice.GetProjectScopedServiceAccountsRequest{ + ProjectId: c.ProjectId, + PageSize: int32(c.PageSize), + PageToken: c.PageToken, + }) + if err != nil { + return err + } + return cctx.Printer.PrintResourceList( + struct { + ServiceAccounts []*identityv1.ServiceAccount + NextPageToken string + }{ + ServiceAccounts: res.ServiceAccounts, + NextPageToken: res.NextPageToken, + }, + printer.PrintResourceOptions{ + Fields: []string{"Id", "State", "CreatedTime"}, + SpecFields: []string{"Name"}, + }, + printer.TableOptions{}, + ) + } res, err := client.GetServiceAccounts(cctx, &cloudservice.GetServiceAccountsRequest{ PageSize: int32(c.PageSize), PageToken: c.PageToken, @@ -306,6 +519,9 @@ func (c *CloudServiceAccountSetCustomRolesCommand) run(cctx *CommandContext, _ [ if sa.Spec.NamespaceScopedAccess != nil { return errors.New("--custom-role is not valid for namespace-scoped service accounts") } + if sa.Spec.ProjectScopedAccess != nil { + return errors.New("--custom-role is not valid for project-scoped service accounts") + } newSpec := proto.Clone(sa.Spec).(*identityv1.ServiceAccountSpec) if newSpec.Access == nil || newSpec.Access.AccountAccess == nil { return errors.New("service account has no account access; assign an account role with `temporal cloud service-account update --account-role` first") diff --git a/temporalcloudcli/commands.service_account_test.go b/temporalcloudcli/commands.service_account_test.go index c34a5c3..05e7462 100644 --- a/temporalcloudcli/commands.service_account_test.go +++ b/temporalcloudcli/commands.service_account_test.go @@ -135,6 +135,7 @@ func TestCreateServiceAccount(t *testing.T) { Description: "a test SA", AccountRole: "developer", NamespaceAccess: []string{"my-ns.acct=write"}, + ProjectAccess: []string{"project-1=member"}, } temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { @@ -148,6 +149,9 @@ func TestCreateServiceAccount(t *testing.T) { NamespaceAccesses: map[string]*identityv1.NamespaceAccess{ "my-ns.acct": {Permission: identityv1.NamespaceAccess_PERMISSION_WRITE}, }, + ProjectAccesses: map[string]*identityv1.ProjectAccess{ + "project-1": {Role: identityv1.ProjectAccess_PROJECT_ROLE_MEMBER}, + }, }, }, }, mock.Anything). @@ -183,6 +187,13 @@ func TestCreateServiceAccount(t *testing.T) { }) }) + t.Run("InvalidProjectAccess", func(t *testing.T) { + cmd := temporalcloudcli.CloudServiceAccountCreateCommand{Name: "my-sa", ProjectAccess: []string{"project-1=developer"}} + temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ + ExpectedError: `invalid project role "developer": must be one of admin, write, read, list, contribute, member in project-access "project-1=developer"`, + }) + }) + t.Run("APIError", func(t *testing.T) { cmd := temporalcloudcli.CloudServiceAccountCreateCommand{Name: "my-sa"} temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ @@ -271,6 +282,83 @@ func TestCreateNamespaceScopedServiceAccount(t *testing.T) { }) } +// --- CreateProjectScopedServiceAccount --- + +func TestCreateProjectScopedServiceAccount(t *testing.T) { + op := &operation.AsyncOperation{Id: "op-create-project-sa"} + + t.Run("Success", func(t *testing.T) { + cmd := temporalcloudcli.CloudServiceAccountCreateProjectScopedCommand{ + Name: "my-project-sa", + Description: "a project SA", + ProjectId: "project-1", + ProjectRole: "write", + NamespaceAccess: []string{"my-ns.acct=read"}, + } + temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + CreateServiceAccount(mock.Anything, &cloudservice.CreateServiceAccountRequest{ + Spec: &identityv1.ServiceAccountSpec{ + Name: "my-project-sa", + Description: "a project SA", + ProjectScopedAccess: &identityv1.ProjectScopedAccess{ + ProjectId: "project-1", + Access: &identityv1.ProjectAccess{ + Role: identityv1.ProjectAccess_PROJECT_ROLE_WRITE, + }, + NamespaceAccesses: map[string]*identityv1.NamespaceAccess{ + "my-ns.acct": {Permission: identityv1.NamespaceAccess_PERMISSION_READ}, + }, + }, + }, + }, mock.Anything). + Return(&cloudservice.CreateServiceAccountResponse{ + ServiceAccountId: "sa-project-new", + AsyncOperation: op, + }, nil) + }, + AsyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-create-project-sa"}, + PromptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: true}, + }) + }) + + t.Run("InvalidProjectRole", func(t *testing.T) { + cmd := temporalcloudcli.CloudServiceAccountCreateProjectScopedCommand{ + Name: "my-project-sa", + ProjectId: "project-1", + ProjectRole: "developer", + } + temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ + ExpectedError: `invalid project role "developer"`, + }) + }) + + t.Run("InvalidNamespaceAccess", func(t *testing.T) { + cmd := temporalcloudcli.CloudServiceAccountCreateProjectScopedCommand{ + Name: "my-project-sa", + ProjectId: "project-1", + ProjectRole: "write", + NamespaceAccess: []string{"my-ns.acct=superread"}, + } + temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ + ExpectedError: `invalid permission "superread" in namespace-access "my-ns.acct=superread"`, + }) + }) + + t.Run("PromptDeclined", func(t *testing.T) { + cmd := temporalcloudcli.CloudServiceAccountCreateProjectScopedCommand{ + Name: "my-project-sa", + ProjectId: "project-1", + ProjectRole: "write", + } + temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ + PromptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: false}, + ExpectedError: "Aborting create.", + }) + }) +} + // --- UpdateServiceAccount --- // // AIDEV-NOTE: Tests use a setupCmd func to manually register flags on the cobra FlagSet and call @@ -291,6 +379,9 @@ func TestUpdateServiceAccount(t *testing.T) { NamespaceAccesses: map[string]*identityv1.NamespaceAccess{ "ns1.acct": {Permission: identityv1.NamespaceAccess_PERMISSION_READ}, }, + ProjectAccesses: map[string]*identityv1.ProjectAccess{ + "project-1": {Role: identityv1.ProjectAccess_PROJECT_ROLE_READ}, + }, }, }, } @@ -307,6 +398,21 @@ func TestUpdateServiceAccount(t *testing.T) { }, } + projectScopedSA := &identityv1.ServiceAccount{ + Id: "sa-3", + ResourceVersion: "rv-3", + Spec: &identityv1.ServiceAccountSpec{ + Name: "my-project-sa", + ProjectScopedAccess: &identityv1.ProjectScopedAccess{ + ProjectId: "project-1", + Access: &identityv1.ProjectAccess{Role: identityv1.ProjectAccess_PROJECT_ROLE_READ}, + NamespaceAccesses: map[string]*identityv1.NamespaceAccess{ + "ns1.acct": {Permission: identityv1.NamespaceAccess_PERMISSION_READ}, + }, + }, + }, + } + tests := []struct { name string setupCmd func(*temporalcloudcli.CloudServiceAccountUpdateCommand) @@ -336,6 +442,9 @@ func TestUpdateServiceAccount(t *testing.T) { NamespaceAccesses: map[string]*identityv1.NamespaceAccess{ "ns1.acct": {Permission: identityv1.NamespaceAccess_PERMISSION_READ}, }, + ProjectAccesses: map[string]*identityv1.ProjectAccess{ + "project-1": {Role: identityv1.ProjectAccess_PROJECT_ROLE_READ}, + }, }, }, ResourceVersion: "rv-1", @@ -367,6 +476,43 @@ func TestUpdateServiceAccount(t *testing.T) { NamespaceAccesses: map[string]*identityv1.NamespaceAccess{ "ns2.acct": {Permission: identityv1.NamespaceAccess_PERMISSION_WRITE}, }, + ProjectAccesses: map[string]*identityv1.ProjectAccess{ + "project-1": {Role: identityv1.ProjectAccess_PROJECT_ROLE_READ}, + }, + }, + }, + ResourceVersion: "rv-1", + }, mock.Anything). + Return(&cloudservice.UpdateServiceAccountResponse{AsyncOperation: op}, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPrompApply: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-update-sa"}, + }, + { + name: "UpdateAccountScopedProjectAccess", + setupCmd: func(cmd *temporalcloudcli.CloudServiceAccountUpdateCommand) { + cmd.Command.Flags().StringArrayVar(&cmd.ProjectAccess, "project-access", nil, "") + require.NoError(t, cmd.Command.Flags().Set("project-access", "project-2=write")) + require.NoError(t, cmd.Command.Flags().Set("project-access", "project-1=")) + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, &cloudservice.GetServiceAccountRequest{ServiceAccountId: "sa-1"}, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: accountScopedSA}, nil) + c.EXPECT(). + UpdateServiceAccount(mock.Anything, &cloudservice.UpdateServiceAccountRequest{ + ServiceAccountId: "sa-1", + Spec: &identityv1.ServiceAccountSpec{ + Name: "my-sa", + Description: "original desc", + Access: &identityv1.Access{ + AccountAccess: &identityv1.AccountAccess{Role: identityv1.AccountAccess_ROLE_DEVELOPER}, + NamespaceAccesses: map[string]*identityv1.NamespaceAccess{ + "ns1.acct": {Permission: identityv1.NamespaceAccess_PERMISSION_READ}, + }, + ProjectAccesses: map[string]*identityv1.ProjectAccess{ + "project-2": {Role: identityv1.ProjectAccess_PROJECT_ROLE_WRITE}, + }, }, }, ResourceVersion: "rv-1", @@ -425,6 +571,9 @@ func TestUpdateServiceAccount(t *testing.T) { NamespaceAccesses: map[string]*identityv1.NamespaceAccess{ "ns1.acct": {Permission: identityv1.NamespaceAccess_PERMISSION_READ}, }, + ProjectAccesses: map[string]*identityv1.ProjectAccess{ + "project-1": {Role: identityv1.ProjectAccess_PROJECT_ROLE_READ}, + }, }, }, ResourceVersion: "rv-1", @@ -434,6 +583,69 @@ func TestUpdateServiceAccount(t *testing.T) { promptOptions: temporalcloudcli.TestPromptOptions{ExpectPrompApply: true, PromptResult: true}, asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-update-sa"}, }, + { + name: "UpdateProjectScopedRole", + setupCmd: func(cmd *temporalcloudcli.CloudServiceAccountUpdateCommand) { + cmd.ServiceAccountId = "sa-3" + cmd.Command.Flags().StringVar(&cmd.ProjectRole, "project-role", "", "") + require.NoError(t, cmd.Command.Flags().Set("project-role", "write")) + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, &cloudservice.GetServiceAccountRequest{ServiceAccountId: "sa-3"}, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: projectScopedSA}, nil) + c.EXPECT(). + UpdateServiceAccount(mock.Anything, &cloudservice.UpdateServiceAccountRequest{ + ServiceAccountId: "sa-3", + Spec: &identityv1.ServiceAccountSpec{ + Name: "my-project-sa", + ProjectScopedAccess: &identityv1.ProjectScopedAccess{ + ProjectId: "project-1", + Access: &identityv1.ProjectAccess{Role: identityv1.ProjectAccess_PROJECT_ROLE_WRITE}, + NamespaceAccesses: map[string]*identityv1.NamespaceAccess{ + "ns1.acct": {Permission: identityv1.NamespaceAccess_PERMISSION_READ}, + }, + }, + }, + ResourceVersion: "rv-3", + }, mock.Anything). + Return(&cloudservice.UpdateServiceAccountResponse{AsyncOperation: op}, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPrompApply: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-update-sa"}, + }, + { + name: "UpdateProjectScopedNamespaceAccess", + setupCmd: func(cmd *temporalcloudcli.CloudServiceAccountUpdateCommand) { + cmd.ServiceAccountId = "sa-3" + cmd.Command.Flags().StringArrayVar(&cmd.NamespaceAccess, "namespace-access", nil, "") + require.NoError(t, cmd.Command.Flags().Set("namespace-access", "ns2.acct=write")) + require.NoError(t, cmd.Command.Flags().Set("namespace-access", "ns1.acct=")) + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, &cloudservice.GetServiceAccountRequest{ServiceAccountId: "sa-3"}, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: projectScopedSA}, nil) + c.EXPECT(). + UpdateServiceAccount(mock.Anything, &cloudservice.UpdateServiceAccountRequest{ + ServiceAccountId: "sa-3", + Spec: &identityv1.ServiceAccountSpec{ + Name: "my-project-sa", + ProjectScopedAccess: &identityv1.ProjectScopedAccess{ + ProjectId: "project-1", + Access: &identityv1.ProjectAccess{Role: identityv1.ProjectAccess_PROJECT_ROLE_READ}, + NamespaceAccesses: map[string]*identityv1.NamespaceAccess{ + "ns2.acct": {Permission: identityv1.NamespaceAccess_PERMISSION_WRITE}, + }, + }, + }, + ResourceVersion: "rv-3", + }, mock.Anything). + Return(&cloudservice.UpdateServiceAccountResponse{AsyncOperation: op}, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPrompApply: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-update-sa"}, + }, { name: "PromptDeclined", setupCmd: func(cmd *temporalcloudcli.CloudServiceAccountUpdateCommand) { @@ -460,7 +672,21 @@ func TestUpdateServiceAccount(t *testing.T) { GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: namespaceScopedSA}, nil) }, - expectedErr: "--account-role, --namespace-access, and --custom-role are not valid for namespace-scoped service accounts", + expectedErr: "--account-role, --namespace-access, --project-access, --project-role, and --custom-role are not valid for namespace-scoped service accounts", + }, + { + name: "AccountRoleOnProjectScopedSA", + setupCmd: func(cmd *temporalcloudcli.CloudServiceAccountUpdateCommand) { + cmd.ServiceAccountId = "sa-3" + cmd.Command.Flags().StringVar(&cmd.AccountRole, "account-role", "", "") + require.NoError(t, cmd.Command.Flags().Set("account-role", "admin")) + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: projectScopedSA}, nil) + }, + expectedErr: "--account-role, --project-access, --namespace-permission, and --custom-role are not valid for project-scoped service accounts", }, { name: "NamespacePermissionOnAccountScopedSA", @@ -473,7 +699,34 @@ func TestUpdateServiceAccount(t *testing.T) { GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: accountScopedSA}, nil) }, - expectedErr: "--namespace-permission is not valid for account-scoped service accounts", + expectedErr: "--namespace-permission is only valid for namespace-scoped service accounts", + }, + { + name: "ProjectRoleOnAccountScopedSA", + setupCmd: func(cmd *temporalcloudcli.CloudServiceAccountUpdateCommand) { + cmd.Command.Flags().StringVar(&cmd.ProjectRole, "project-role", "", "") + require.NoError(t, cmd.Command.Flags().Set("project-role", "write")) + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: accountScopedSA}, nil) + }, + expectedErr: "--project-role is only valid for project-scoped service accounts", + }, + { + name: "ProjectAccessOnProjectScopedSA", + setupCmd: func(cmd *temporalcloudcli.CloudServiceAccountUpdateCommand) { + cmd.ServiceAccountId = "sa-3" + cmd.Command.Flags().StringArrayVar(&cmd.ProjectAccess, "project-access", nil, "") + require.NoError(t, cmd.Command.Flags().Set("project-access", "project-2=write")) + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: projectScopedSA}, nil) + }, + expectedErr: "--account-role, --project-access, --namespace-permission, and --custom-role are not valid for project-scoped service accounts", }, { name: "InvalidAccountRole", @@ -658,6 +911,258 @@ func TestEditServiceAccount(t *testing.T) { } } +// --- SetServiceAccountProjectAccess --- + +func TestSetServiceAccountProjectAccess(t *testing.T) { + testSA := &identityv1.ServiceAccount{ + Id: "sa-1", + ResourceVersion: "rv-1", + Spec: &identityv1.ServiceAccountSpec{Name: "my-sa"}, + } + op := &operation.AsyncOperation{Id: "op-set-project-access"} + + tests := []struct { + name string + cmd temporalcloudcli.CloudServiceAccountSetProjectAccessCommand + cloudClientExpectations func(*cloudmock.MockCloudServiceClient) + promptOptions temporalcloudcli.TestPromptOptions + asyncPollerOptions temporalcloudcli.TestAsyncPollerOptions + expectedErr string + }{ + { + name: "Success", + cmd: temporalcloudcli.CloudServiceAccountSetProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + ProjectRole: "write", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, &cloudservice.GetServiceAccountRequest{ServiceAccountId: "sa-1"}, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: testSA}, nil) + c.EXPECT(). + SetServiceAccountProjectAccess(mock.Anything, &cloudservice.SetServiceAccountProjectAccessRequest{ + ProjectId: "project-1", + ServiceAccountId: "sa-1", + Access: &identityv1.ProjectAccess{Role: identityv1.ProjectAccess_PROJECT_ROLE_WRITE}, + ResourceVersion: "rv-1", + }, mock.Anything). + Return(&cloudservice.SetServiceAccountProjectAccessResponse{AsyncOperation: op}, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-set-project-access"}, + }, + { + name: "ResourceVersionOverride", + cmd: temporalcloudcli.CloudServiceAccountSetProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + ProjectRole: "member", + ResourceVersionOptions: temporalcloudcli.ResourceVersionOptions{ResourceVersion: "rv-override"}, + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, &cloudservice.GetServiceAccountRequest{ServiceAccountId: "sa-1"}, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: testSA}, nil) + c.EXPECT(). + SetServiceAccountProjectAccess(mock.Anything, &cloudservice.SetServiceAccountProjectAccessRequest{ + ProjectId: "project-1", + ServiceAccountId: "sa-1", + Access: &identityv1.ProjectAccess{Role: identityv1.ProjectAccess_PROJECT_ROLE_MEMBER}, + ResourceVersion: "rv-override", + }, mock.Anything). + Return(&cloudservice.SetServiceAccountProjectAccessResponse{AsyncOperation: op}, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-set-project-access"}, + }, + { + name: "InvalidProjectRole", + cmd: temporalcloudcli.CloudServiceAccountSetProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + ProjectRole: "developer", + }, + expectedErr: `invalid project role "developer"`, + }, + { + name: "GetServiceAccountError", + cmd: temporalcloudcli.CloudServiceAccountSetProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + ProjectRole: "write", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("not found")) + }, + expectedErr: "not found", + }, + { + name: "PromptDeclined", + cmd: temporalcloudcli.CloudServiceAccountSetProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + ProjectRole: "write", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: testSA}, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: false}, + expectedErr: "Aborting set.", + }, + { + name: "APIError", + cmd: temporalcloudcli.CloudServiceAccountSetProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + ProjectRole: "write", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: testSA}, nil) + c.EXPECT(). + SetServiceAccountProjectAccess(mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("internal error")) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: true}, + expectedErr: "internal error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + temporalcloudcli.TestCommand(t, &tt.cmd, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: tt.cloudClientExpectations, + PromptOptions: tt.promptOptions, + AsyncPollerOptions: tt.asyncPollerOptions, + ExpectedError: tt.expectedErr, + }) + }) + } +} + +// --- RemoveServiceAccountProjectAccess --- + +func TestRemoveServiceAccountProjectAccess(t *testing.T) { + testSA := &identityv1.ServiceAccount{ + Id: "sa-1", + ResourceVersion: "rv-1", + Spec: &identityv1.ServiceAccountSpec{Name: "my-sa"}, + } + op := &operation.AsyncOperation{Id: "op-remove-project-access"} + + tests := []struct { + name string + cmd temporalcloudcli.CloudServiceAccountRemoveProjectAccessCommand + cloudClientExpectations func(*cloudmock.MockCloudServiceClient) + promptOptions temporalcloudcli.TestPromptOptions + asyncPollerOptions temporalcloudcli.TestAsyncPollerOptions + expectedErr string + }{ + { + name: "Success", + cmd: temporalcloudcli.CloudServiceAccountRemoveProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, &cloudservice.GetServiceAccountRequest{ServiceAccountId: "sa-1"}, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: testSA}, nil) + c.EXPECT(). + SetServiceAccountProjectAccess(mock.Anything, &cloudservice.SetServiceAccountProjectAccessRequest{ + ProjectId: "project-1", + ServiceAccountId: "sa-1", + ResourceVersion: "rv-1", + }, mock.Anything). + Return(&cloudservice.SetServiceAccountProjectAccessResponse{AsyncOperation: op}, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-remove-project-access"}, + }, + { + name: "ResourceVersionOverride", + cmd: temporalcloudcli.CloudServiceAccountRemoveProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + ResourceVersionOptions: temporalcloudcli.ResourceVersionOptions{ResourceVersion: "rv-override"}, + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, &cloudservice.GetServiceAccountRequest{ServiceAccountId: "sa-1"}, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: testSA}, nil) + c.EXPECT(). + SetServiceAccountProjectAccess(mock.Anything, &cloudservice.SetServiceAccountProjectAccessRequest{ + ProjectId: "project-1", + ServiceAccountId: "sa-1", + ResourceVersion: "rv-override", + }, mock.Anything). + Return(&cloudservice.SetServiceAccountProjectAccessResponse{AsyncOperation: op}, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-remove-project-access"}, + }, + { + name: "GetServiceAccountError", + cmd: temporalcloudcli.CloudServiceAccountRemoveProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("not found")) + }, + expectedErr: "not found", + }, + { + name: "PromptDeclined", + cmd: temporalcloudcli.CloudServiceAccountRemoveProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: testSA}, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: false}, + expectedErr: "Aborting remove.", + }, + { + name: "APIError", + cmd: temporalcloudcli.CloudServiceAccountRemoveProjectAccessCommand{ + ServiceAccountId: "sa-1", + ProjectId: "project-1", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccount(mock.Anything, mock.Anything, mock.Anything). + Return(&cloudservice.GetServiceAccountResponse{ServiceAccount: testSA}, nil) + c.EXPECT(). + SetServiceAccountProjectAccess(mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("internal error")) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: true}, + expectedErr: "internal error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + temporalcloudcli.TestCommand(t, &tt.cmd, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: tt.cloudClientExpectations, + PromptOptions: tt.promptOptions, + AsyncPollerOptions: tt.asyncPollerOptions, + ExpectedError: tt.expectedErr, + }) + }) + } +} + // --- GetServiceAccount --- func TestGetServiceAccount(t *testing.T) { @@ -761,6 +1266,33 @@ func TestListServiceAccounts(t *testing.T) { NextPageToken: "tok-next", }, }, + { + name: "WithProjectID", + cmd: temporalcloudcli.CloudServiceAccountListCommand{ + ProjectId: "project-1", + PageSize: 10, + PageToken: "tok-abc", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetProjectScopedServiceAccounts(mock.Anything, &cloudservice.GetProjectScopedServiceAccountsRequest{ + ProjectId: "project-1", + PageSize: 10, + PageToken: "tok-abc", + }, mock.Anything). + Return(&cloudservice.GetProjectScopedServiceAccountsResponse{ + ServiceAccounts: testServiceAccounts, + NextPageToken: "tok-next", + }, nil) + }, + expectedJsonOutput: struct { + ServiceAccounts []*identityv1.ServiceAccount `json:"ServiceAccounts"` + NextPageToken string `json:"NextPageToken"` + }{ + ServiceAccounts: testServiceAccounts, + NextPageToken: "tok-next", + }, + }, { name: "Empty", cmd: temporalcloudcli.CloudServiceAccountListCommand{}, @@ -799,3 +1331,73 @@ func TestListServiceAccounts(t *testing.T) { }) } } + +// --- ProjectServiceAccountList --- + +func TestProjectServiceAccountList(t *testing.T) { + assignments := []*identityv1.ServiceAccountProjectAssignment{ + { + Id: "sa-1", + Name: "my-sa", + ProjectAccess: &identityv1.ProjectAccess{ + Role: identityv1.ProjectAccess_PROJECT_ROLE_WRITE, + }, + }, + } + + tests := []struct { + name string + cmd temporalcloudcli.CloudProjectServiceAccountListCommand + cloudClientExpectations func(*cloudmock.MockCloudServiceClient) + expectedErr string + expectedJsonOutput any + }{ + { + name: "Success", + cmd: temporalcloudcli.CloudProjectServiceAccountListCommand{ + ProjectId: "project-1", + PageSize: 10, + PageToken: "tok-abc", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccountProjectAssignments(mock.Anything, &cloudservice.GetServiceAccountProjectAssignmentsRequest{ + ProjectId: "project-1", + PageSize: 10, + PageToken: "tok-abc", + }, mock.Anything). + Return(&cloudservice.GetServiceAccountProjectAssignmentsResponse{ + ServiceAccounts: assignments, + NextPageToken: "tok-next", + }, nil) + }, + expectedJsonOutput: struct { + ServiceAccounts []*identityv1.ServiceAccountProjectAssignment `json:"ServiceAccounts"` + NextPageToken string `json:"NextPageToken"` + }{ + ServiceAccounts: assignments, + NextPageToken: "tok-next", + }, + }, + { + name: "APIError", + cmd: temporalcloudcli.CloudProjectServiceAccountListCommand{ProjectId: "project-1"}, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetServiceAccountProjectAssignments(mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("internal error")) + }, + expectedErr: "internal 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.expectedJsonOutput, + }) + }) + } +} diff --git a/temporalcloudcli/commands.yml b/temporalcloudcli/commands.yml index 2a1d3e8..c5c8d73 100644 --- a/temporalcloudcli/commands.yml +++ b/temporalcloudcli/commands.yml @@ -2372,6 +2372,41 @@ commands: description: | Token for retrieving the next page of results in a paginated list. + - name: cloud project service-account + summary: Inspect service accounts with access to a project + description: | + Commands for inspecting the service accounts that have access to a + Temporal Cloud project. + has-init: false + - name: cloud project service-account list + summary: List service accounts with access to a project + description: | + List the service accounts that have access to a Temporal Cloud project, + including both directly-assigned and inherited access. + + Example: + + ``` + temporal cloud project service-account list --project-id my-project-id + ``` + has-init: false + option-sets: + - client + options: + - name: project-id + type: string + required: true + description: | + The ID of the project. + - 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. + # User group commands - name: cloud user-group summary: Manage Temporal Cloud user groups @@ -3620,11 +3655,16 @@ commands: ``` temporal cloud service-account list + temporal cloud service-account list --project-id my-project-id ``` has-init: false option-sets: - client options: + - name: project-id + type: string + description: | + The ID of the project to list project-scoped service accounts for. - name: page-size type: int description: | @@ -3660,16 +3700,19 @@ commands: summary: Create a service account description: | Create a new Temporal Cloud service account with account-level access. - Optionally assign an account role and namespace-level permissions. + Optionally assign an account role, namespace-level permissions, and + project-level roles. Account roles: owner, admin, developer, finance-admin, read, metrics-read. Namespace access format: 'namespace=permission' where permission is one of: admin, write, read. + Project access format: 'project-id=role' where role is one of: admin, write, read, list, contribute, member. Example: ``` temporal cloud service-account create --name my-sa --account-role developer \ - --namespace-access my-namespace.my-account=write + --namespace-access my-namespace.my-account=write \ + --project-access my-project-id=write ``` has-init: false option-sets: @@ -3695,6 +3738,11 @@ commands: description: | Namespace access to grant, in the format 'namespace=permission'. Permission must be one of: admin, write, read. Can be repeated. + - name: project-access + type: string[] + description: | + Project access to grant, in the format 'project-id=role'. + Role must be one of: admin, write, read, list, contribute, member. Can be repeated. - name: custom-role type: string[] description: | @@ -3736,23 +3784,76 @@ commands: description: | The permission to grant on the namespace. Valid values: admin, write, read. + - name: cloud service-account create-project-scoped + summary: Create a project-scoped service account + description: | + Create a new Temporal Cloud service account scoped to a single project. + Optionally assign namespace-level permissions within the project. + + Project roles: admin, write, read, list, contribute, member. + Namespace access format: 'namespace=permission' where permission is one of: admin, write, read. + + Example: + + ``` + temporal cloud service-account create-project-scoped --name my-sa \ + --project-id my-project-id --project-role write \ + --namespace-access my-namespace.my-account=read + ``` + has-init: false + option-sets: + - client + - async-operation + options: + - name: name + type: string + required: true + description: | + The name of the service account. Must be unique across all active service accounts. + - name: description + type: string + description: | + An optional description for the service account. + - name: project-id + type: string + required: true + description: | + The ID of the project to scope the service account to. + - name: project-role + type: string + required: true + description: | + The project-level role to assign. Valid values: admin, write, read, + list, contribute, member. + - name: namespace-access + type: string[] + description: | + Namespace access to grant, in the format 'namespace=permission'. + Permission must be one of: admin, write, read. Can be repeated. + - name: cloud service-account update summary: Update a service account description: | Update a Temporal Cloud service account. Only flags that are explicitly provided are changed. - For account-scoped service accounts, use --account-role and/or --namespace-access. + For account-scoped service accounts, use --account-role, --namespace-access, and/or --project-access. For namespace-scoped service accounts, use --namespace-permission. + For project-scoped service accounts, use --project-role and/or --namespace-access. Namespace access format: 'namespace=permission' where permission is one of: admin, write, read. Use 'namespace=' (empty permission) to remove access to a namespace. + Project access format: 'project-id=role' where role is one of: admin, write, read, list, contribute, member. + Use 'project-id=' (empty role) to remove access to a project. + Project-scoped service account roles: admin, write, read, list, contribute, member. Example: ``` temporal cloud service-account update --service-account-id my-sa-id --name new-name temporal cloud service-account update --service-account-id my-sa-id --account-role admin + temporal cloud service-account update --service-account-id my-sa-id --project-access my-project-id=write + temporal cloud service-account update --service-account-id my-sa-id --project-role write temporal cloud service-account update --service-account-id my-sa-id --namespace-permission write ``` has-init: false @@ -3785,13 +3886,88 @@ commands: description: | Namespace access to set, in the format 'namespace=permission'. Use 'namespace=' to remove access. Permission must be one of: admin, write, read. Can be repeated. - Only valid for account-scoped service accounts. + Only valid for account-scoped and project-scoped service accounts. + - name: project-access + type: string[] + description: | + Project access to set, in the format 'project-id=role'. Use 'project-id=' + to remove access. Role must be one of: admin, write, read, list, contribute, member. + Can be repeated. Only valid for account-scoped service accounts. + - name: project-role + type: string + description: | + The project-level role to assign. Valid values: admin, write, read, + list, contribute, member. Only valid for project-scoped service accounts. - name: namespace-permission type: string description: | The permission to grant on the scoped namespace. Valid values: admin, write, read. Only valid for namespace-scoped service accounts. + - name: cloud service-account set-project-access + summary: Set project access for a service account + description: | + Set project-level access for a Temporal Cloud service account. + + Project roles: admin, write, read, list, contribute, member. + + Example: + + ``` + temporal cloud service-account set-project-access --service-account-id my-sa-id \ + --project-id my-project-id --project-role write + ``` + has-init: false + option-sets: + - client + - async-operation + - resource-version + options: + - name: service-account-id + type: string + required: true + description: | + The ID of the service account. + - name: project-id + type: string + required: true + description: | + The ID of the project. + - name: project-role + type: string + required: true + description: | + The project-level role to assign. Valid values: admin, write, read, + list, contribute, member. + + - name: cloud service-account remove-project-access + summary: Remove project access for a service account + description: | + Remove a Temporal Cloud service account's direct project-level access. + + Example: + + ``` + temporal cloud service-account remove-project-access --service-account-id my-sa-id \ + --project-id my-project-id + ``` + has-init: false + option-sets: + - client + - async-operation + - resource-version + options: + - name: service-account-id + type: string + required: true + description: | + The ID of the service account. + - name: project-id + type: string + required: true + description: | + The ID of the project. + - name: cloud service-account set-custom-roles summary: '[Experimental] Set custom role assignments for a service account' description: |