Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add group_member resource #46

Merged
merged 2 commits into from Aug 26, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/index.md
@@ -1,6 +1,6 @@
# Provider

Terraform provider for 1password usage with your infrastructure, for example you can share password from your admin panel via some vault in you 1password company account. This provider is based on 1Password CLI client version 0.5.5, but you can rewrite it by env variable `OP_VERSION`.
Terraform provider for 1password usage with your infrastructure, for example you can share password from your admin panel via some vault in you 1password company account. This provider is based on 1Password CLI client version 1.4.0, but you can rewrite it by env variable `OP_VERSION`.

## Example Usage

Expand Down
43 changes: 43 additions & 0 deletions docs/resources/group_member.md
@@ -0,0 +1,43 @@
# onepassword_group_member

This resource can manage group membership within a 1Password group.

## Example Usage

### Resource

```hcl
resource "onepassword_group" "group" {
group = "new-group"
}

data "onepassword_user" "user" {
email = "example@example.com"
}

resource "onepassword_group_member" "example" {
group = onepassword_group.group.id
user = data.onepassword_user.user.id
}
```

## Argument Reference

* `group` - (Required) group id.
* `user` - (Required) user id.

## Attribute Reference

In addition to the above arguments, the following attributes are exported:

* `id` - (Required) internal membership identifier.

## Import

1Password Group Members can be imported using the `id`, which consists of the group ID and user ID separated by a hyphen, e.g.

```
terraform import onepassword_group_member.example fmownretj6zdobn2cnjtqqyrae-KDLG56VTIJDXXBXC2KKCPHNHHI
```

**Note: this is case sensitive, and matches the case provided by 1Password.**
32 changes: 32 additions & 0 deletions onepassword/group.go
Expand Up @@ -2,6 +2,7 @@ package onepassword

import (
"encoding/json"
"fmt"
)

const (
Expand Down Expand Up @@ -35,6 +36,23 @@ func (o *OnePassClient) ReadGroup(id string) (*Group, error) {
return group, nil
}

// ListGroupMembers lists the existing Users in a given Group
func (o *OnePassClient) ListGroupMembers(id string) ([]User, error) {
users := []User{}
if id == "" {
return users, fmt.Errorf("Must provide an identifier to list group members")
}

res, err := o.runCmd(opPasswordList, "users", "--"+GroupResource, id)
if err != nil {
return nil, err
}
if err = json.Unmarshal(res, &users); err != nil {
return nil, err
}
return users, nil
}

// CreateGroup creates a new 1Password Group
func (o *OnePassClient) CreateGroup(v *Group) (*Group, error) {
args := []string{opPasswordCreate, GroupResource, v.Name}
Expand All @@ -48,6 +66,13 @@ func (o *OnePassClient) CreateGroup(v *Group) (*Group, error) {
return v, nil
}

// CreateGroupMember adds a User to a Group
func (o *OnePassClient) CreateGroupMember(groupID string, userID string) error {
args := []string{opPasswordAdd, UserResource, userID, groupID}
_, err := o.runCmd(args...)
return err
}

// UpdateGroup updates an existing 1Password Group
func (o *OnePassClient) UpdateGroup(id string, v *Group) error {
args := []string{opPasswordEdit, GroupResource, id, "--name=" + v.Name}
Expand All @@ -59,3 +84,10 @@ func (o *OnePassClient) UpdateGroup(id string, v *Group) error {
func (o *OnePassClient) DeleteGroup(id string) error {
return o.Delete(GroupResource, id)
}

// DeleteGroupMember removes a User from a Group
func (o *OnePassClient) DeleteGroupMember(groupID string, userID string) error {
args := []string{opPasswordRemove, UserResource, userID, groupID}
_, err := o.runCmd(args...)
return err
}
178 changes: 178 additions & 0 deletions onepassword/group_test.go
Expand Up @@ -268,3 +268,181 @@ func TestOnePassClient_DeleteGroup(t *testing.T) {
})
}
}

func TestOnePassClient_ListGroupMembers(t *testing.T) {
type fields struct {
runCmd func() (string, error)
}
type args struct {
id string
}
tests := []struct {
name string
fields fields
args args
wantExecResults []string
want []User
wantErr bool
}{
{
name: "success",
fields: fields{
runCmd: func() (string, error) {
return `[ { "uuid": "uniq", "firstname": "Testy", "lastname": "Testerton" } ]`, nil
},
},
args: args{id: "uniq"},
wantExecResults: []string{"op", "list", "users", "--group", "uniq", "--session="},
want: []User{{UUID: "uniq", FirstName: "Testy", LastName: "Testerton"}},
},
{
name: "error",
fields: fields{
runCmd: func() (string, error) {
return ``, fmt.Errorf("oops")
},
},
args: args{id: "uniq"},
wantExecResults: []string{"op", "list", "users", "--group", "uniq", "--session="},
wantErr: true,
},
{
name: "error-missing-id",
args: args{id: ""},
want: []User{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := &mockOnePassConfig{
runCmd: tt.fields.runCmd,
}
o := mockOnePassClient(config)

got, err := o.ListGroupMembers(tt.args.id)
if (err != nil) != tt.wantErr {
t.Errorf("OnePassClient.ListGroupMembers() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("OnePassClient.ListGroupMembers() = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(config.execCommandResults, tt.wantExecResults) {
t.Errorf("OnePassClient.ListGroupMembers() exec = %v, want %v", config.execCommandResults, tt.wantExecResults)
}
})
}
}

func TestOnePassClient_CreateGroupMember(t *testing.T) {
type fields struct {
runCmd func() (string, error)
}
type args struct {
userID string
groupID string
}
tests := []struct {
name string
fields fields
args args
wantExecResults []string
wantErr bool
}{
{
name: "success",
fields: fields{
runCmd: func() (string, error) {
return `{ }`, nil
},
},
args: args{userID: "userName", groupID: "groupName"},
wantExecResults: []string{"op", "add", "user", "groupName", "userName", "--session="},
},
{
name: "error",
fields: fields{
runCmd: func() (string, error) {
return ``, fmt.Errorf("oops")
},
},
args: args{userID: "userName", groupID: "groupName"},
wantExecResults: []string{"op", "add", "user", "groupName", "userName", "--session="},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := &mockOnePassConfig{
runCmd: tt.fields.runCmd,
}
o := mockOnePassClient(config)

err := o.CreateGroupMember(tt.args.userID, tt.args.groupID)
if (err != nil) != tt.wantErr {
t.Errorf("OnePassClient.ListGroupMembers() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(config.execCommandResults, tt.wantExecResults) {
t.Errorf("OnePassClient.ListGroupMembers() exec = %v, want %v", config.execCommandResults, tt.wantExecResults)
}
})
}
}

func TestOnePassClient_DeleteGroupMember(t *testing.T) {
type fields struct {
runCmd func() (string, error)
}
type args struct {
userID string
groupID string
}
tests := []struct {
name string
fields fields
args args
wantExecResults []string
wantErr bool
}{
{
name: "success",
fields: fields{
runCmd: func() (string, error) {
return `{ }`, nil
},
},
args: args{userID: "userName", groupID: "groupName"},
wantExecResults: []string{"op", "remove", "user", "groupName", "userName", "--session="},
},
{
name: "error",
fields: fields{
runCmd: func() (string, error) {
return ``, fmt.Errorf("oops")
},
},
args: args{userID: "userName", groupID: "groupName"},
wantExecResults: []string{"op", "remove", "user", "groupName", "userName", "--session="},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := &mockOnePassConfig{
runCmd: tt.fields.runCmd,
}
o := mockOnePassClient(config)

err := o.DeleteGroupMember(tt.args.userID, tt.args.groupID)
if (err != nil) != tt.wantErr {
t.Errorf("OnePassClient.ListGroupMembers() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(config.execCommandResults, tt.wantExecResults) {
t.Errorf("OnePassClient.ListGroupMembers() exec = %v, want %v", config.execCommandResults, tt.wantExecResults)
}
})
}
}
28 changes: 19 additions & 9 deletions onepassword/provider.go
Expand Up @@ -21,7 +21,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var version string = "0.7.1"
var version string = "1.4.0"

func Provider() *schema.Provider {
return &schema.Provider{
Expand Down Expand Up @@ -58,6 +58,7 @@ func Provider() *schema.Provider {
},
ResourcesMap: map[string]*schema.Resource{
"onepassword_group": resourceGroup(),
"onepassword_group_member": resourceGroupMember(),
"onepassword_item_common": resourceItemCommon(),
"onepassword_item_software_license": resourceItemSoftwareLicense(),
"onepassword_item_identity": resourceItemIdentity(),
Expand Down Expand Up @@ -89,10 +90,15 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}
return NewMeta(d)
}

const opPasswordCreate = "create"
const opPasswordEdit = "edit"
const opPasswordDelete = "delete"
const opPasswordGet = "get"
const (
opPasswordAdd = "add"
opPasswordCreate = "create"
opPasswordEdit = "edit"
opPasswordDelete = "delete"
opPasswordGet = "get"
opPasswordList = "list"
opPasswordRemove = "remove"
)

type OnePassClient struct {
Password string
Expand Down Expand Up @@ -196,6 +202,10 @@ func installOPClient() (string, error) {
}
version = semVer.String()
}
if runtime.GOOS == "darwin" {
return "", fmt.Errorf("Unable to automatically install v%s of the op client. Please install manually from https://app-updates.agilebits.com/product_history/CLI", version)
}

binZip := fmt.Sprintf("/tmp/op_%s.zip", version)
if _, err := os.Stat(binZip); os.IsNotExist(err) {
resp, err := http.Get(fmt.Sprintf(
Expand All @@ -206,20 +216,20 @@ func installOPClient() (string, error) {
version,
))
if err != nil {
return "", err
return "", fmt.Errorf("Could not retrieve zipped op release: %w", err)
}
defer resp.Body.Close()

out, err := os.Create(binZip)
if err != nil {
return "", err
return "", fmt.Errorf("Could not create temp file for op client: %w", err)
}
defer out.Close()
if _, err = io.Copy(out, resp.Body); err != nil {
return "", err
return "", fmt.Errorf("Could not copy zip contents to temp file for op client: %w", err)
}
if err := unzip(binZip, "/tmp/terraform-provider-onepassword/"+version); err != nil {
return "", err
return "", fmt.Errorf("Could not unzip temp file for op client: %w", err)
}
}
return "/tmp/terraform-provider-onepassword/" + version + "/op", nil
Expand Down