Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 146 additions & 8 deletions temporalcloudcli/commands.gen.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions temporalcloudcli/commands.project.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ func (c *CloudProjectUserListCommand) run(cctx *CommandContext, _ []string) erro
return printProjectUserAssignments(cctx, c.ClientOptions, c.ProjectId, c.PageSize, c.PageToken)
}

func (c *CloudProjectUserGroupListCommand) run(cctx *CommandContext, _ []string) error {
return printProjectUserGroupAssignments(cctx, c.ClientOptions, c.ProjectId, c.PageSize, c.PageToken)
}

func (c *CloudProjectCreateCommand) run(cctx *CommandContext, _ []string) error {
spec := projectSpecFromFlags(c.DisplayName, c.Description, c.EnableDeleteProtection)

Expand Down
65 changes: 65 additions & 0 deletions temporalcloudcli/commands.project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,71 @@ func TestProjectUserList_ApiError(t *testing.T) {
})
}

func TestProjectUserGroupList(t *testing.T) {
type listOutput struct {
Groups []*identityv1.UserGroupProjectAssignment
NextPageToken string
}

cmd := &temporalcloudcli.CloudProjectUserGroupListCommand{
ProjectId: "project-a",
PageSize: 50,
PageToken: "next",
}
res := &cloudservice.GetUserGroupProjectAssignmentsResponse{
Groups: []*identityv1.UserGroupProjectAssignment{
{
Id: "group-1",
DisplayName: "Engineering",
ProjectAccess: &identityv1.ProjectAccess{
Role: identityv1.ProjectAccess_PROJECT_ROLE_WRITE,
},
},
{
Id: "group-2",
DisplayName: "Platform",
InheritedAccess: true,
ProjectAccess: &identityv1.ProjectAccess{
Role: identityv1.ProjectAccess_PROJECT_ROLE_DEVELOPER,
},
},
},
NextPageToken: "next-2",
}

temporalcloudcli.TestCommand(t, cmd, temporalcloudcli.TestCommandOptions{
CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) {
c.EXPECT().
GetUserGroupProjectAssignments(mock.Anything, &cloudservice.GetUserGroupProjectAssignmentsRequest{
ProjectId: "project-a",
PageSize: 50,
PageToken: "next",
}, mock.Anything).
Return(res, nil)
},
JSONOutput: true,
ExpectedOutputJson: listOutput{
Groups: res.Groups,
NextPageToken: "next-2",
},
})
}

func TestProjectUserGroupList_ApiError(t *testing.T) {
cmd := &temporalcloudcli.CloudProjectUserGroupListCommand{ProjectId: "project-a"}

temporalcloudcli.TestCommand(t, cmd, temporalcloudcli.TestCommandOptions{
CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) {
c.EXPECT().
GetUserGroupProjectAssignments(mock.Anything, &cloudservice.GetUserGroupProjectAssignmentsRequest{
ProjectId: "project-a",
}, mock.Anything).
Return(nil, errors.New("api error"))
},
ExpectedError: "api error",
})
}

func TestProjectCreate(t *testing.T) {
cmd := &temporalcloudcli.CloudProjectCreateCommand{
DisplayName: "Engineering",
Expand Down
28 changes: 28 additions & 0 deletions temporalcloudcli/commands.user.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,34 @@ func parseProjectAccesses(accesses []string) (map[string]*identityv1.ProjectAcce
return result, nil
}

// applyProjectAccessChanges merges project access changes into an existing map.
// Each change is in "project-id=role" format. An empty role removes that project.
func applyProjectAccessChanges(existing map[string]*identityv1.ProjectAccess, changes []string) (map[string]*identityv1.ProjectAccess, error) {
result := make(map[string]*identityv1.ProjectAccess, len(existing))
for k, v := range existing {
result[k] = v
}
for _, a := range changes {
projectID, role, ok := strings.Cut(a, "=")
if !ok {
return nil, fmt.Errorf("invalid project-access %q: must be in the format 'project-id=role'", a)
}
if role == "" {
delete(result, projectID)
continue
}
projectAccess, err := parseProjectRole(role)
if err != nil {
return nil, fmt.Errorf("%w in project-access %q", err, a)
}
result[projectID] = projectAccess
}
if len(result) == 0 {
return nil, nil
}
return result, nil
}

// applyCustomRoleChanges returns the new CustomRoles slice for an update
// command, given the existing list and whether --custom-role was passed.
//
Expand Down
139 changes: 134 additions & 5 deletions temporalcloudcli/commands.user_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ func (c *CloudUserGroupGetCommand) run(cctx *CommandContext, _ []string) error {
}

func (c *CloudUserGroupListCommand) run(cctx *CommandContext, _ []string) error {
if c.ProjectId != "" {
if c.Namespace != "" || c.DisplayName != "" || c.GoogleGroupEmailAddress != "" || c.ScimGroupIdpId != "" {
return errors.New("--project-id cannot be combined with --namespace, --display-name, --google-group-email-address, or --scim-group-idp-id")
}
return printProjectUserGroupAssignments(cctx, c.ClientOptions, c.ProjectId, c.PageSize, c.PageToken)
}

client, err := cctx.GetCloudClient(c.ClientOptions)
if err != nil {
return err
Expand Down Expand Up @@ -94,6 +101,10 @@ func (c *CloudUserGroupCreateCloudGroupCommand) run(cctx *CommandContext, _ []st
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")
Expand All @@ -115,10 +126,11 @@ func (c *CloudUserGroupCreateCloudGroupCommand) run(cctx *CommandContext, _ []st
DisplayName: c.DisplayName,
GroupType: &identityv1.UserGroupSpec_CloudGroup{CloudGroup: &identityv1.CloudGroupSpec{}},
}
if accountAccess != nil || len(namespaceAccesses) > 0 {
if accountAccess != nil || len(namespaceAccesses) > 0 || len(projectAccesses) > 0 {
spec.Access = &identityv1.Access{
AccountAccess: accountAccess,
NamespaceAccesses: namespaceAccesses,
ProjectAccesses: projectAccesses,
}
}
resp, err := client.CreateUserGroup(cctx, &cloudservice.CreateUserGroupRequest{
Expand All @@ -137,6 +149,10 @@ func (c *CloudUserGroupCreateGoogleGroupCommand) run(cctx *CommandContext, _ []s
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")
Expand All @@ -160,10 +176,11 @@ func (c *CloudUserGroupCreateGoogleGroupCommand) run(cctx *CommandContext, _ []s
GoogleGroup: &identityv1.GoogleGroupSpec{EmailAddress: c.GoogleGroupEmail},
},
}
if accountAccess != nil || len(namespaceAccesses) > 0 {
if accountAccess != nil || len(namespaceAccesses) > 0 || len(projectAccesses) > 0 {
spec.Access = &identityv1.Access{
AccountAccess: accountAccess,
NamespaceAccesses: namespaceAccesses,
ProjectAccesses: projectAccesses,
}
}
resp, err := client.CreateUserGroup(cctx, &cloudservice.CreateUserGroupRequest{
Expand All @@ -182,6 +199,10 @@ func (c *CloudUserGroupCreateScimGroupCommand) run(cctx *CommandContext, _ []str
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")
Expand All @@ -205,10 +226,11 @@ func (c *CloudUserGroupCreateScimGroupCommand) run(cctx *CommandContext, _ []str
ScimGroup: &identityv1.SCIMGroupSpec{IdpId: c.ScimIdpId},
},
}
if accountAccess != nil || len(namespaceAccesses) > 0 {
if accountAccess != nil || len(namespaceAccesses) > 0 || len(projectAccesses) > 0 {
spec.Access = &identityv1.Access{
AccountAccess: accountAccess,
NamespaceAccesses: namespaceAccesses,
ProjectAccesses: projectAccesses,
}
}
resp, err := client.CreateUserGroup(cctx, &cloudservice.CreateUserGroupRequest{
Expand Down Expand Up @@ -307,13 +329,16 @@ func (c *CloudUserGroupEditCommand) run(cctx *CommandContext, _ []string) error

func (c *CloudUserGroupUpdateCommand) run(cctx *CommandContext, _ []string) error {
customRoleProvided := c.Command.Flags().Changed("custom-role")
if c.AccountRole == "" && len(c.NamespaceAccess) == 0 && !customRoleProvided {
return errors.New("must provide at least one of --account-role, --namespace-access, or --custom-role")
if c.AccountRole == "" && len(c.NamespaceAccess) == 0 && len(c.ProjectAccess) == 0 && !customRoleProvided {
return errors.New("must provide at least one of --account-role, --namespace-access, --project-access, or --custom-role")
}
// Validate inputs before any API call.
if _, err := applyNamespaceAccessChanges(nil, c.NamespaceAccess); err != nil {
return err
}
if _, err := applyProjectAccessChanges(nil, c.ProjectAccess); err != nil {
return err
}
var accountAccess *identityv1.AccountAccess
if c.AccountRole != "" {
var err error
Expand Down Expand Up @@ -349,6 +374,13 @@ func (c *CloudUserGroupUpdateCommand) run(cctx *CommandContext, _ []string) erro
}
newSpec.Access.NamespaceAccesses = namespaceAccesses
}
if len(c.ProjectAccess) > 0 {
projectAccesses, err := applyProjectAccessChanges(newSpec.Access.ProjectAccesses, c.ProjectAccess)
if err != nil {
return err
}
newSpec.Access.ProjectAccesses = projectAccesses
}
if customRoleProvided {
if newSpec.Access.AccountAccess == nil {
return errors.New("group has no account access; assign an account role with --account-role first")
Expand Down Expand Up @@ -419,6 +451,69 @@ func (c *CloudUserGroupSetAccountRoleCommand) run(cctx *CommandContext, _ []stri
return cctx.GetPoller(client, c.AsyncOperationOptions).HandleUpdateOperation(cctx, resp, err)
}

func (c *CloudUserGroupSetProjectAccessCommand) 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.GetUserGroup(cctx, &cloudservice.GetUserGroupRequest{GroupId: c.GroupId})
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.Group.ResourceVersion
if c.ResourceVersion != "" {
rv = c.ResourceVersion
}
resp, err := client.SetUserGroupProjectAccess(cctx, &cloudservice.SetUserGroupProjectAccessRequest{
ProjectId: c.ProjectId,
GroupId: c.GroupId,
Access: projectAccess,
ResourceVersion: rv,
AsyncOperationId: c.AsyncOperationId,
})
return cctx.GetPoller(client, c.AsyncOperationOptions).HandleUpdateOperation(cctx, resp, err)
}

func (c *CloudUserGroupRemoveProjectAccessCommand) run(cctx *CommandContext, _ []string) error {
client, err := cctx.GetCloudClient(c.ClientOptions)
if err != nil {
return err
}
res, err := client.GetUserGroup(cctx, &cloudservice.GetUserGroupRequest{GroupId: c.GroupId})
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.Group.ResourceVersion
if c.ResourceVersion != "" {
rv = c.ResourceVersion
}
resp, err := client.SetUserGroupProjectAccess(cctx, &cloudservice.SetUserGroupProjectAccessRequest{
ProjectId: c.ProjectId,
GroupId: c.GroupId,
ResourceVersion: rv,
AsyncOperationId: c.AsyncOperationId,
})
return cctx.GetPoller(client, c.AsyncOperationOptions).HandleUpdateOperation(cctx, resp, err)
}

func (c *CloudUserGroupSetNamespacePermissionsCommand) run(cctx *CommandContext, _ []string) error {
// Validate inputs before any API call.
if _, err := applyNamespaceAccessChanges(nil, c.NamespaceAccess); err != nil {
Expand Down Expand Up @@ -461,6 +556,40 @@ func (c *CloudUserGroupSetNamespacePermissionsCommand) run(cctx *CommandContext,
return cctx.GetPoller(client, c.AsyncOperationOptions).HandleUpdateOperation(cctx, resp, err)
}

func printProjectUserGroupAssignments(
cctx *CommandContext,
clientOptions ClientOptions,
projectID string,
pageSize int,
pageToken string,
) error {
client, err := cctx.GetCloudClient(clientOptions)
if err != nil {
return err
}
res, err := client.GetUserGroupProjectAssignments(cctx, &cloudservice.GetUserGroupProjectAssignmentsRequest{
ProjectId: projectID,
PageSize: int32(pageSize),
PageToken: pageToken,
})
if err != nil {
return err
}
return cctx.Printer.PrintResourceList(
struct {
Groups []*identityv1.UserGroupProjectAssignment
NextPageToken string
}{
Groups: res.Groups,
NextPageToken: res.NextPageToken,
},
printer.PrintResourceOptions{
Fields: []string{"Id", "DisplayName", "ProjectAccess", "InheritedAccess"},
},
printer.TableOptions{},
)
}

func (c *CloudUserGroupSetCustomRolesCommand) run(cctx *CommandContext, _ []string) error {
client, err := cctx.GetCloudClient(c.ClientOptions)
if err != nil {
Expand Down
Loading
Loading