-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #113 from hashicorp/hcpie-1234-add-group-manager-s…
…upport Add Group Manager Support to Groups
- Loading branch information
Showing
12 changed files
with
1,389 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
```release-note:feature | ||
iam: Adds `read-policy`, `set-policy`, `add-binding`, and `delete-binding` subcommands to `hcp iam groups iam` which allow the ability to manage an IAM policy on a group. | ||
- `read-policy` Reads an IAM policy for a specified group. | ||
- `set-policy` Sets an IAM policy for a group using a JSON file. | ||
- `add-binding` Adds a single role binding to a user principal. | ||
- `delete-binding` Removes a single role binding from a user principal. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package iam | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/hashicorp/hcp-sdk-go/clients/cloud-iam/stable/2019-12-10/client/groups_service" | ||
"github.com/hashicorp/hcp-sdk-go/clients/cloud-iam/stable/2019-12-10/client/iam_service" | ||
"github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/organization_service" | ||
"github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/resource_service" | ||
"github.com/hashicorp/hcp/internal/commands/iam/groups/helper" | ||
"github.com/hashicorp/hcp/internal/pkg/api/iampolicy" | ||
"github.com/hashicorp/hcp/internal/pkg/cmd" | ||
"github.com/hashicorp/hcp/internal/pkg/flagvalue" | ||
"github.com/hashicorp/hcp/internal/pkg/heredoc" | ||
"github.com/hashicorp/hcp/internal/pkg/iostreams" | ||
"github.com/hashicorp/hcp/internal/pkg/profile" | ||
) | ||
|
||
func NewCmdAddBinding(ctx *cmd.Context, runF func(*AddBindingOpts) error) *cmd.Command { | ||
opts := &AddBindingOpts{ | ||
Ctx: ctx.ShutdownCtx, | ||
Profile: ctx.Profile, | ||
IO: ctx.IO, | ||
|
||
GroupsClient: groups_service.New(ctx.HCP, nil), | ||
ResourceClient: resource_service.New(ctx.HCP, nil), | ||
} | ||
|
||
cmd := &cmd.Command{ | ||
Name: "add-binding", | ||
ShortHelp: "Add an IAM policy binding for a group.", | ||
LongHelp: heredoc.New(ctx.IO).Must(` | ||
The {{ template "mdCodeOrBold" "hcp iam groups iam add-binding" }} | ||
command adds an IAM policy binding for the given group. A binding grants the | ||
specified principal the given role on the group. | ||
To view the available roles to bind, run {{ template "mdCodeOrBold" "hcp iam roles list" }}. | ||
Currently, the only supported role on a principal in a group is {{ template "mdCodeOrBold" "roles/iam.group-manager" }}. | ||
A group manager can add/remove members from the group and update the group name/description. | ||
`), | ||
Examples: []cmd.Example{ | ||
{ | ||
Preamble: heredoc.New(ctx.IO).Must(`Bind a principal to role {{ template "mdCodeOrBold" "roles/iam.group-manager" }}:`), | ||
Command: heredoc.New(ctx.IO, heredoc.WithPreserveNewlines()).Must(` | ||
$ hcp iam groups iam add-binding \ | ||
--group=Group-Name \ | ||
--member=ef938a22-09cf-4be9-b4d0-1f4587f80f53 \ | ||
--role=roles/iam.group-manager | ||
`), | ||
}, | ||
}, | ||
Flags: cmd.Flags{ | ||
Local: []*cmd.Flag{ | ||
{ | ||
Name: "group", | ||
Shorthand: "g", | ||
DisplayValue: "NAME", | ||
Description: "The name of the group to add the role binding to.", | ||
Value: flagvalue.Simple("", &opts.GroupName), | ||
Autocomplete: helper.PredictGroupResourceNameSuffix(opts.Ctx, opts.Profile.OrganizationID, opts.GroupsClient), | ||
Required: true, | ||
}, | ||
{ | ||
Name: "member", | ||
Shorthand: "m", | ||
DisplayValue: "PRINCIPAL_ID", | ||
Description: "The ID of the principal to add the role binding to.", | ||
Value: flagvalue.Simple("", &opts.PrincipalID), | ||
Required: true, | ||
}, | ||
{ | ||
Name: "role", | ||
Shorthand: "r", | ||
DisplayValue: "ROLE_ID", | ||
Description: `The role ID (e.g. "roles/iam.group-manager") to bind the member to.`, | ||
Value: flagvalue.Simple("", &opts.Role), | ||
Required: true, | ||
Autocomplete: iampolicy.AutocompleteRoles(opts.Ctx, ctx.Profile.OrganizationID, organization_service.New(ctx.HCP, nil)), | ||
}, | ||
}, | ||
}, | ||
RunF: func(c *cmd.Command, args []string) error { | ||
resourceName := helper.ResourceName(opts.GroupName, ctx.Profile.OrganizationID) | ||
|
||
// Create the group IAM Updater | ||
u := &iamUpdater{ | ||
resourceName: resourceName, | ||
client: opts.ResourceClient, | ||
} | ||
|
||
// Create the policy setter | ||
opts.Setter = iampolicy.NewSetter( | ||
ctx.Profile.OrganizationID, | ||
u, | ||
iam_service.New(ctx.HCP, nil), | ||
c.Logger()) | ||
|
||
if runF != nil { | ||
return runF(opts) | ||
} | ||
|
||
return addBindingRun(opts) | ||
}, | ||
PersistentPreRun: func(c *cmd.Command, args []string) error { | ||
return cmd.RequireOrganization(ctx) | ||
}, | ||
} | ||
|
||
return cmd | ||
} | ||
|
||
type AddBindingOpts struct { | ||
Ctx context.Context | ||
Profile *profile.Profile | ||
IO iostreams.IOStreams | ||
|
||
Setter iampolicy.Setter | ||
GroupName string | ||
PrincipalID string | ||
Role string | ||
GroupsClient groups_service.ClientService | ||
ResourceClient resource_service.ClientService | ||
} | ||
|
||
func addBindingRun(opts *AddBindingOpts) error { | ||
_, err := opts.Setter.AddBinding(opts.Ctx, opts.PrincipalID, opts.Role) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
fmt.Fprintf(opts.IO.Err(), "%s Principal %q bound to role %q.\n", | ||
opts.IO.ColorScheme().SuccessIcon(), opts.PrincipalID, opts.Role) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package iam | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/go-openapi/runtime/client" | ||
"github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/models" | ||
"github.com/hashicorp/hcp/internal/pkg/api/iampolicy" | ||
"github.com/hashicorp/hcp/internal/pkg/cmd" | ||
"github.com/hashicorp/hcp/internal/pkg/format" | ||
"github.com/hashicorp/hcp/internal/pkg/iostreams" | ||
"github.com/hashicorp/hcp/internal/pkg/profile" | ||
"github.com/stretchr/testify/mock" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestNewCmdAddBinding(t *testing.T) { | ||
t.Parallel() | ||
|
||
cases := []struct { | ||
Name string | ||
Args []string | ||
Profile func(t *testing.T) *profile.Profile | ||
Error string | ||
Expect *AddBindingOpts | ||
}{ | ||
{ | ||
Name: "No Org", | ||
Profile: profile.TestProfile, | ||
Args: []string{ | ||
"--group=test-group", | ||
"--member=123", | ||
"--role=roles/iam.group-manager", | ||
}, | ||
Error: "Organization ID must be configured", | ||
}, | ||
{ | ||
Name: "Too many args", | ||
Profile: func(t *testing.T) *profile.Profile { | ||
return profile.TestProfile(t).SetOrgID("123") | ||
}, | ||
Args: []string{ | ||
"--group=test-group", | ||
"--member=123", | ||
"--role=roles/iam.group-manager", | ||
"foo", | ||
"bar", | ||
}, | ||
Error: "no arguments allowed, but received 2", | ||
}, | ||
{ | ||
Name: "Missing group", | ||
Profile: func(t *testing.T) *profile.Profile { | ||
return profile.TestProfile(t).SetOrgID("123") | ||
}, | ||
Args: []string{ | ||
"--member=123", | ||
"--role=roles/iam.group-manager", | ||
}, | ||
Error: "ERROR: missing required flag: --group=NAME", | ||
}, | ||
{ | ||
Name: "Missing member", | ||
Profile: func(t *testing.T) *profile.Profile { | ||
return profile.TestProfile(t).SetOrgID("123") | ||
}, | ||
Args: []string{ | ||
"--group=test-group", | ||
"--role=roles/iam.group-manager", | ||
}, | ||
Error: "ERROR: missing required flag: --member=PRINCIPAL_ID", | ||
}, | ||
{ | ||
Name: "Missing role", | ||
Profile: func(t *testing.T) *profile.Profile { | ||
return profile.TestProfile(t).SetOrgID("123") | ||
}, | ||
Args: []string{ | ||
"--group=test-group", | ||
"--member=123", | ||
}, | ||
Error: "ERROR: missing required flag: --role=ROLE_ID", | ||
}, | ||
{ | ||
Name: "Good", | ||
Profile: func(t *testing.T) *profile.Profile { | ||
return profile.TestProfile(t).SetOrgID("123") | ||
}, | ||
Args: []string{ | ||
"--group=test-group", | ||
"--member=123", | ||
"--role=roles/iam.group-manager"}, | ||
Expect: &AddBindingOpts{ | ||
GroupName: "test-group", | ||
PrincipalID: "123", | ||
Role: "roles/iam.group-manager", | ||
}, | ||
}, | ||
} | ||
|
||
for _, c := range cases { | ||
c := c | ||
t.Run(c.Name, func(t *testing.T) { | ||
t.Parallel() | ||
r := require.New(t) | ||
|
||
// Create a context. | ||
io := iostreams.Test() | ||
ctx := &cmd.Context{ | ||
IO: io, | ||
Profile: c.Profile(t), | ||
Output: format.New(io), | ||
HCP: &client.Runtime{}, | ||
ShutdownCtx: context.Background(), | ||
} | ||
|
||
var gotOpts *AddBindingOpts | ||
bindingCmd := NewCmdAddBinding(ctx, func(o *AddBindingOpts) error { | ||
gotOpts = o | ||
return nil | ||
}) | ||
bindingCmd.SetIO(io) | ||
|
||
code := bindingCmd.Run(c.Args) | ||
if c.Error != "" { | ||
r.NotZero(code) | ||
r.Contains(io.Error.String(), c.Error) | ||
return | ||
} | ||
|
||
r.Zero(code, io.Error.String()) | ||
r.NotNil(gotOpts) | ||
r.Equal(c.Expect.GroupName, gotOpts.GroupName) | ||
r.Equal(c.Expect.PrincipalID, gotOpts.PrincipalID) | ||
r.Equal(c.Expect.Role, gotOpts.Role) | ||
r.NotNil(gotOpts.Setter) | ||
}) | ||
} | ||
} | ||
|
||
func TestAddBindingRun(t *testing.T) { | ||
t.Parallel() | ||
|
||
cases := []struct { | ||
Name string | ||
RespErr error | ||
Error string | ||
}{ | ||
{ | ||
Name: "Server error", | ||
RespErr: fmt.Errorf("failed to add policy"), | ||
Error: "failed to add policy", | ||
}, | ||
{ | ||
Name: "Good", | ||
}, | ||
} | ||
|
||
for _, c := range cases { | ||
c := c | ||
t.Run(c.Name, func(t *testing.T) { | ||
t.Parallel() | ||
r := require.New(t) | ||
|
||
io := iostreams.Test() | ||
setter := iampolicy.NewMockSetter(t) | ||
opts := &AddBindingOpts{ | ||
Ctx: context.Background(), | ||
IO: io, | ||
Setter: setter, | ||
GroupName: "test-group", | ||
PrincipalID: "principal-123", | ||
Role: "roles/test", | ||
} | ||
|
||
// Expect a request to add a binding. | ||
call := setter.EXPECT().AddBinding(mock.Anything, opts.PrincipalID, opts.Role).Once() | ||
|
||
if c.RespErr != nil { | ||
call.Return(nil, c.RespErr) | ||
} else { | ||
call.Return(&models.HashicorpCloudResourcemanagerPolicy{}, nil) | ||
} | ||
|
||
// Run the command | ||
err := addBindingRun(opts) | ||
if c.Error != "" { | ||
r.ErrorContains(err, c.Error) | ||
return | ||
} | ||
|
||
// Check we outputted the project | ||
r.NoError(err) | ||
r.Contains(io.Error.String(), `Principal "principal-123" bound to role "roles/test"`) | ||
}) | ||
} | ||
} |
Oops, something went wrong.