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 support for externally managed Group Member IDs to Vault Identity Group #1630

Merged
merged 9 commits into from
Oct 26, 2022
7 changes: 7 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ const (
FieldClientToken = "client_token"
FieldWrappedToken = "wrapped_token"
FieldOrphan = "orphan"
FieldMemberEntityIDs = "member_entity_ids"
FieldMemberGroupIDs = "member_group_ids"
FieldExclusive = "exclusive"
FieldGroupID = "group_id"
FieldGroupName = "group_name"
FieldExternal = "external"
FieldInternal = "internal"

/*
common environment variables
Expand Down
31 changes: 31 additions & 0 deletions internal/identity/entity/entity.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package entity

import (
"errors"
"fmt"
"log"

"github.com/hashicorp/vault/api"
"github.com/mitchellh/mapstructure"

"github.com/hashicorp/terraform-provider-vault/internal/provider"
"github.com/hashicorp/terraform-provider-vault/util"
)

const (
Expand All @@ -15,6 +20,8 @@ const (
LookupPath = "identity/lookup/entity"
)

var ErrEntityNotFound = errors.New("entity not found")

// Entity represents a Vault identity entity
type Entity struct {
Aliases []*Alias `mapstructure:"aliases" json:"aliases,omitempty"`
Expand Down Expand Up @@ -152,3 +159,27 @@ func LookupEntityAlias(client *api.Client, params *FindAliasParams) (*Alias, err

return nil, nil
}

func ReadEntity(client *api.Client, path string, retry bool) (*api.Secret, error) {
log.Printf("[DEBUG] Reading Entity from %q", path)

var err error
if retry {
client, err = client.Clone()
if err != nil {
return nil, fmt.Errorf("error cloning client: %w", err)
}
util.SetupCCCRetryClient(client, provider.MaxHTTPRetriesCCC)
}

resp, err := client.Logical().Read(path)
if err != nil {
return resp, fmt.Errorf("failed reading %q", path)
}

if resp == nil {
return nil, fmt.Errorf("%w: %q", ErrEntityNotFound, path)
}

return resp, nil
}
304 changes: 303 additions & 1 deletion internal/identity/group/group.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,307 @@
package group

import (
"context"
"errors"
"fmt"
"log"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/vault/api"

"github.com/hashicorp/terraform-provider-vault/internal/consts"
"github.com/hashicorp/terraform-provider-vault/internal/identity/entity"
"github.com/hashicorp/terraform-provider-vault/internal/provider"
)

const (
LookupPath = "identity/lookup/group"
LookupPath = "identity/lookup/group"
IdentityGroupPath = "/identity/group"

GroupResourceType = iota
EntityResourceType
)

func IdentityGroupIDPath(id string) string {
return fmt.Sprintf("%s/id/%s", IdentityGroupPath, id)
}

// ReadIdentityGroup may return `nil` for the IdentityGroup if it does not exist
func ReadIdentityGroup(client *api.Client, groupID string, retry bool) (*api.Secret, error) {
path := IdentityGroupIDPath(groupID)
log.Printf("[DEBUG] Reading IdentityGroup %s from %q", groupID, path)

return entity.ReadEntity(client, path, retry)
}

func IsIdentityNotFoundError(err error) bool {
return err != nil && errors.Is(err, entity.ErrEntityNotFound)
}

func getFieldFromResourceType(resourceType int) string {
var ret string
switch resourceType {
case GroupResourceType:
ret = consts.FieldMemberGroupIDs
case EntityResourceType:
ret = consts.FieldMemberEntityIDs
}
benashz marked this conversation as resolved.
Show resolved Hide resolved

return ret
}

// GetGroupMemberUpdateContextFunc is a common context function for all
// Update operations to be performed on Identity Group Members
func GetGroupMemberUpdateContextFunc(resourceType int) func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics {
return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
gid := d.Get(consts.FieldGroupID).(string)
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
path := IdentityGroupIDPath(gid)
provider.VaultMutexKV.Lock(path)
defer provider.VaultMutexKV.Unlock(path)

client, e := provider.GetClient(d, meta)
if e != nil {
return diag.FromErr(e)
}

memberField := getFieldFromResourceType(resourceType)

log.Printf("[DEBUG] Updating field %q on Identity Group %q", memberField, gid)

if d.HasChange(consts.FieldGroupID) {
o, n := d.GetChange(consts.FieldGroupID)
log.Printf("[DEBUG] Group ID has changed old=%q, new=%q", o, n)
}

resp, err := ReadIdentityGroup(client, gid, d.IsNewResource())
if err != nil {
return diag.FromErr(err)
}

data, err := GetGroupMember(d, resp, memberField)
if err != nil {
return diag.FromErr(err)
}

_, err = client.Logical().Write(path, data)
if err != nil {
return diag.Errorf("error updating field %q on Identity Group %s: err=%s", memberField, gid, err)
}
log.Printf("[DEBUG] Updated field %q on Identity Group %s", memberField, gid)

d.SetId(gid)

return nil
}
}

// GetGroupMemberReadContextFunc is a common context function for all
// read operations to be performed on Identity Group Members
func GetGroupMemberReadContextFunc(resourceType int, setGroupName bool) schema.ReadContextFunc {
return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return diag.FromErr(e)
}

id := d.Id()

memberField := getFieldFromResourceType(resourceType)

log.Printf("[DEBUG] Reading Identity Group %s with field %q", id, memberField)
resp, err := ReadIdentityGroup(client, id, d.IsNewResource())
if err != nil {
if IsIdentityNotFoundError(err) {
log.Printf("[WARN] Identity Group %s not found, removing from state", id)
d.SetId("")
return nil
}
return diag.FromErr(err)
}

if err := d.Set(consts.FieldGroupID, id); err != nil {
return diag.FromErr(err)
}

if setGroupName {
if err := d.Set(consts.FieldGroupName, resp.Data[consts.FieldName]); err != nil {
return diag.FromErr(err)
}
}

if err := SetGroupMember(d, resp, memberField); err != nil {
return diag.FromErr(err)
}

return nil
}
}

// GetGroupMemberDeleteContextFunc is a common context function for all
// delete operations to be performed on Identity Group Members
func GetGroupMemberDeleteContextFunc(resourceType int) schema.DeleteContextFunc {
return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
id := d.Get(consts.FieldGroupID).(string)
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
path := IdentityGroupIDPath(id)
provider.VaultMutexKV.Lock(path)
defer provider.VaultMutexKV.Unlock(path)

client, e := provider.GetClient(d, meta)
if e != nil {
return diag.FromErr(e)
}

memberField := getFieldFromResourceType(resourceType)

log.Printf("[DEBUG] Deleting Identity Group %q with field %q", memberField, id)

resp, err := ReadIdentityGroup(client, id, false)
if err != nil {
if IsIdentityNotFoundError(err) {
return nil
}
return diag.FromErr(err)
}

data, err := DeleteGroupMember(d, resp, memberField)
if err != nil {
return diag.FromErr(err)
}

_, err = client.Logical().Write(path, data)
if err != nil {
return diag.Errorf("error deleting Identity Group %q with field %q; err=%s", id, memberField, err)
}
log.Printf("[DEBUG] Deleted Identity Group %q with field %q", memberField, id)

return nil
}
}

// GetGroupMember returns group member data based on an input
// 'memberField'. It manages the lifecycle of internal group
// members appropriately by performing any necessary deduplication
func GetGroupMember(d *schema.ResourceData, resp *api.Secret, memberField string) (map[string]interface{}, error) {
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
data := map[string]interface{}{}

switch memberField {
case consts.FieldMemberGroupIDs, consts.FieldMemberEntityIDs:
default:
return nil, fmt.Errorf("invalid value for member field")
}
var curIDS []interface{}
if t, ok := resp.Data[consts.FieldType]; ok && t.(string) != consts.FieldExternal {
if v, ok := resp.Data[memberField]; ok && v != nil {
curIDS = v.([]interface{})
}

if d.Get(consts.FieldExclusive).(bool) || len(curIDS) == 0 {
data[memberField] = d.Get(memberField).(*schema.Set).List()
} else {
set := map[interface{}]bool{}
for _, v := range curIDS {
set[v] = true
}

o, _ := d.GetChange(memberField)
if !d.IsNewResource() && o != nil {
// set.delete()
for _, i := range o.(*schema.Set).List() {
delete(set, i)
}
}

if ids, ok := d.GetOk(memberField); ok {
for _, id := range ids.(*schema.Set).List() {
// set.add()
set[id] = true
}
}

// set.keys()
var result []interface{}
for k := range set {
result = append(result, k)
}
data[memberField] = result
}
}

return data, nil
}

// SetGroupMember sets group member data to the TF state based
// on a 'memberField'
func SetGroupMember(d *schema.ResourceData, resp *api.Secret, memberField string) error {
curIDS := resp.Data[memberField]
if d.Get(consts.FieldExclusive).(bool) {
if err := d.Set(memberField, curIDS); err != nil {
return err
}
} else {
set := map[interface{}]bool{}
if curIDS != nil {
for _, v := range curIDS.([]interface{}) {
set[v] = true
}
}

var result []interface{}
// set.intersection()
if i, ok := d.GetOk(memberField); ok && i != nil {
for _, v := range i.(*schema.Set).List() {
if _, ok := set[v]; ok {
result = append(result, v)
}
}
}
if err := d.Set(memberField, result); err != nil {
return err
}
}

return nil
}

// DeleteGroupMember deletes group member data from Vault and the TF
// state based on a 'memberField'
func DeleteGroupMember(d *schema.ResourceData, resp *api.Secret, memberField string) (map[string]interface{}, error) {
data := map[string]interface{}{}

switch memberField {
case consts.FieldMemberGroupIDs, consts.FieldMemberEntityIDs:
default:
return nil, fmt.Errorf("invalid value for member field")
}

t, ok := resp.Data[consts.FieldType]
if ok && t != consts.FieldExternal {
if d.Get(consts.FieldExclusive).(bool) {
data[memberField] = make([]string, 0)
} else {
set := map[interface{}]bool{}
if v, ok := resp.Data[memberField]; ok && v != nil {
for _, id := range v.([]interface{}) {
set[id] = true
}
}

result := []interface{}{}
if len(set) > 0 {
if v, ok := d.GetOk(memberField); ok {
for _, id := range v.(*schema.Set).List() {
delete(set, id)
}
}

for k := range set {
result = append(result, k)
}
}
data[memberField] = result
}
}

return data, nil
}
Loading