Skip to content

Commit

Permalink
Merge pull request #113 from hashicorp/hcpie-1234-add-group-manager-s…
Browse files Browse the repository at this point in the history
…upport

Add Group Manager Support to Groups
  • Loading branch information
itsjaspermilan authored Jun 11, 2024
2 parents c829ac5 + 6cd9a8c commit a2647b5
Show file tree
Hide file tree
Showing 12 changed files with 1,389 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .changelog/113.txt
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.
```
2 changes: 2 additions & 0 deletions internal/commands/iam/groups/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package groups

import (
"github.com/hashicorp/hcp/internal/commands/iam/groups/iam"
"github.com/hashicorp/hcp/internal/commands/iam/groups/members"
"github.com/hashicorp/hcp/internal/pkg/cmd"
"github.com/hashicorp/hcp/internal/pkg/heredoc"
Expand All @@ -30,5 +31,6 @@ func NewCmdGroups(ctx *cmd.Context) *cmd.Command {
cmd.AddChild(NewCmdDelete(ctx, nil))
cmd.AddChild(NewCmdUpdate(ctx, nil))
cmd.AddChild(members.NewCmdMembers(ctx))
cmd.AddChild(iam.NewCmdIAM(ctx))
return cmd
}
140 changes: 140 additions & 0 deletions internal/commands/iam/groups/iam/add_binding.go
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
}
202 changes: 202 additions & 0 deletions internal/commands/iam/groups/iam/add_binding_test.go
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"`)
})
}
}
Loading

0 comments on commit a2647b5

Please sign in to comment.