Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Unreleased

FEATURES:
* Add new resource `vault_ldap_group_policy_attachment` to manage policies

## 5.0.0 (May 21, 2025)

**Important**: `5.X` multiplexes the Vault provider to use the [Terraform Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework),
Expand Down
4 changes: 4 additions & 0 deletions vault/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,10 @@ var (
Resource: UpdateSchemaResource(ldapAuthBackendGroupResource()),
PathInventory: []string{"/auth/ldap/groups/{name}"},
},
"vault_ldap_group_policy_attachment": {
Resource: UpdateSchemaResource(ldapGroupPolicyAttachmentResource()),
PathInventory: []string{"/auth/ldap/groups/{name}"},
},
"vault_ldap_secret_backend": {
Resource: UpdateSchemaResource(ldapSecretBackendResource()),
PathInventory: []string{"/ldap/config"},
Expand Down
147 changes: 147 additions & 0 deletions vault/resource_ldap_group_policy_attachment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vault

import (
"fmt"
"log"
"slices"
"strings"

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

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

func ldapGroupPolicyAttachmentResource() *schema.Resource {
return &schema.Resource{
SchemaVersion: 1,

Create: ldapGroupPolicyAttachmentResourceWrite,
Update: ldapGroupPolicyAttachmentResourceWrite,
Read: provider.ReadWrapper(ldapAuthBackendGroupResourceRead),
Delete: ldapGroupPolicyAttachmentResourceDelete,
Exists: ldapAuthBackendUserResourceExists,

Schema: map[string]*schema.Schema{
"groupname": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
consts.FieldPolicies: {
Type: schema.TypeSet,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Required: true,
},
consts.FieldBackend: {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: consts.MountTypeLDAP,
StateFunc: func(v interface{}) string {
return strings.Trim(v.(string), "/")
},
},
},
}
}

func ldapGroupPolicyAttachmentResourceWrite(d *schema.ResourceData, meta interface{}) error {
client, e := provider.GetClient(d, meta)
if e != nil {
return e
}

backend := d.Get(consts.FieldBackend).(string)
groupname := d.Get("groupname").(string)
path := ldapAuthBackendGroupResourcePath(backend, groupname)

resp, err := client.Logical().Read(path)

if err != nil {
return fmt.Errorf("error reading ldap group %q: %s", path, err)
}

if resp == nil {
return fmt.Errorf("error: ldap group not found %s", groupname)
}

data := map[string]interface{}{}
if v, ok := d.GetOk(consts.FieldPolicies); ok {
existingPolicies := []interface{}{}
if resp.Data != nil {
if val, ok := resp.Data[consts.FieldPolicies]; ok {
existingPolicies = val.([]interface{})
}
}
desiredPolicies := v.(*schema.Set).List()
data[consts.FieldPolicies] = schema.NewSet(schema.HashString, append(existingPolicies, desiredPolicies...)).List()
}

log.Printf("[DEBUG] Updating %q", path)
_, err = client.Logical().Write(path, data)

d.SetId(path)

if err != nil {
d.SetId("")
return fmt.Errorf("error writing ldap group %q: %s", path, err)
}
log.Printf("[DEBUG] Wrote LDAP group %q", path)

return ldapAuthBackendGroupResourceRead(d, meta)
}

func ldapGroupPolicyAttachmentResourceDelete(d *schema.ResourceData, meta interface{}) error {
client, e := provider.GetClient(d, meta)
if e != nil {
return e
}

path := d.Id()

if v, ok := d.GetOk(consts.FieldPolicies); ok {
policiesToDelete := v.(*schema.Set).List()

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

attachedPolicies := []interface{}{}
if resp != nil {
attachedPolicies = resp.Data[consts.FieldPolicies].([]interface{})
}

newPolicies := policiesWithout(attachedPolicies, policiesToDelete)

data := map[string]interface{}{}
data[consts.FieldPolicies] = schema.NewSet(
schema.HashString, newPolicies,
).List()

log.Printf("[DEBUG] Deleting LDAP group policies %q", path)
_, err = client.Logical().Write(path, data)
if err != nil {
return fmt.Errorf("error deleting policies from ldap group %q", path)
}
log.Printf("[DEBUG] Deleted LDAP group policies %q", path)
}

return nil
}

func policiesWithout(attachedPolicies []interface{}, policiesToDelete []interface{}) []interface{} {
newPolicies := []interface{}{}
for _, policy := range attachedPolicies {
if !slices.Contains(policiesToDelete, policy) {
newPolicies = append(newPolicies, policy)
}
}
return newPolicies
}
223 changes: 223 additions & 0 deletions vault/resource_ldap_group_policy_attachment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package vault

import (
"context"
"fmt"
"regexp"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"

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

func TestLDAPGroupPolicyAttachment_basic(t *testing.T) {
t.Parallel()

backend := acctest.RandomWithPrefix("tf-test-ldap-backend")
groupname := acctest.RandomWithPrefix("tf-test-ldap-group")

policies := []string{
acctest.RandomWithPrefix("policy"),
acctest.RandomWithPrefix("policy"),
}

resourceName := "vault_ldap_group_policy_attachment.test"
resource.Test(t, resource.TestCase{
PreCheck: func() { testutil.TestAccPreCheck(t) },
ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(context.Background(), t),
CheckDestroy: testLDAPGroupPolicyAttachmentDestroy,
Steps: []resource.TestStep{
{
Config: testLDAPGroupPolicyAttachmentConfig_basic(backend, groupname, policies),
Check: testLDAPGroupPolicyAttachmentCheckAttrs(resourceName, backend, groupname, policies),
},
},
})
}

func TestLDAPGroupPolicyAttachment_nonexistentGroup(t *testing.T) {
t.Parallel()

backend := acctest.RandomWithPrefix("tf-test-ldap-backend")
nonexistentGroup := acctest.RandomWithPrefix("tf-test-nonexistent-group")
policies := []string{acctest.RandomWithPrefix("policy")}

config := fmt.Sprintf(`
resource "vault_auth_backend" "ldap" {
path = "%s"
type = "ldap"
}

resource "vault_ldap_group_policy_attachment" "test" {
backend = vault_auth_backend.ldap.path
groupname = "%s"
policies = %s
}
`, backend, nonexistentGroup, util.ArrayToTerraformList(policies))

resource.Test(t, resource.TestCase{
PreCheck: func() { testutil.TestAccPreCheck(t) },
ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(context.Background(), t),
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile(`error: ldap group not found .*`),
},
},
})
}

func TestLDAPGroupPolicyAttachment_missingPolicies(t *testing.T) {
t.Parallel()

backend := acctest.RandomWithPrefix("tf-test-ldap-backend")
groupname := acctest.RandomWithPrefix("tf-test-ldap-group")

config := fmt.Sprintf(`
resource "vault_auth_backend" "ldap" {
path = "%s"
type = "ldap"
}

resource "vault_ldap_auth_backend_group" "test" {
backend = vault_auth_backend.ldap.path
groupname = "%s"
}

resource "vault_ldap_group_policy_attachment" "test" {
backend = vault_auth_backend.ldap.path
groupname = vault_ldap_auth_backend_group.test.groupname
}
`, backend, groupname)

resource.Test(t, resource.TestCase{
PreCheck: func() { testutil.TestAccPreCheck(t) },
ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(context.Background(), t),
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile(`The argument "policies" is required`),
},
},
})
}

func TestLDAPGroupPolicyAttachment_multiplePolicies(t *testing.T) {
t.Parallel()

backend := acctest.RandomWithPrefix("tf-test-ldap-backend")
groupname := acctest.RandomWithPrefix("tf-test-ldap-group")
policies := []string{
acctest.RandomWithPrefix("policy"),
acctest.RandomWithPrefix("policy"),
acctest.RandomWithPrefix("policy"),
}

resourceName := "vault_ldap_group_policy_attachment.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testutil.TestAccPreCheck(t) },
ProtoV5ProviderFactories: testAccProtoV5ProviderFactories(context.Background(), t),
CheckDestroy: testLDAPGroupPolicyAttachmentDestroy,
Steps: []resource.TestStep{
{
Config: testLDAPGroupPolicyAttachmentConfig_basic(backend, groupname, policies),
Check: testLDAPGroupPolicyAttachmentCheckAttrs(resourceName, backend, groupname, policies),
},
},
})
}

func testLDAPGroupPolicyAttachmentDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "vault_ldap_group_policy_attachment" {
continue
}

client, err := provider.GetClient(rs.Primary, testProvider.Meta())
if err != nil {
return err
}

secret, err := client.Logical().Read(rs.Primary.ID)
if err != nil {
return fmt.Errorf("error checking for ldap group %q: %s", rs.Primary.ID, err)
}

if secret != nil {
if policies, ok := secret.Data["policies"]; ok && len(policies.([]interface{})) > 0 {
return fmt.Errorf("policies still attached to group %q", rs.Primary.ID)
}
}
}
return nil
}

func testLDAPGroupPolicyAttachmentCheckAttrs(resourceName, backend, groupname string, expectedPolicies []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, err := testutil.GetResourceFromRootModule(s, resourceName)
if err != nil {
return err
}

expectedID := fmt.Sprintf("auth/%s/groups/%s", strings.Trim(backend, "/"), groupname)
if rs.Primary.ID != expectedID {
return fmt.Errorf("expected ID %q, got %q", expectedID, rs.Primary.ID)
}

client, err := provider.GetClient(rs.Primary, testProvider.Meta())
if err != nil {
return err
}

group, err := client.Logical().Read(rs.Primary.ID)
if err != nil {
return err
}
if group == nil || group.Data == nil {
return fmt.Errorf("group %q not found", rs.Primary.ID)
}

actualPolicies := group.Data["policies"].([]interface{})
for _, expected := range expectedPolicies {
found := false
for _, actual := range actualPolicies {
if actual.(string) == expected {
found = true
break
}
}
if !found {
return fmt.Errorf("expected policy %q not found in attached policies", expected)
}
}

return nil
}
}

func testLDAPGroupPolicyAttachmentConfig_basic(backend, groupname string, policies []string) string {
return fmt.Sprintf(`
resource "vault_auth_backend" "ldap" {
path = "%s"
type = "ldap"
}

resource "vault_ldap_auth_backend_group" "test" {
backend = vault_auth_backend.ldap.path
groupname = "%s"
}

resource "vault_ldap_group_policy_attachment" "test" {
backend = vault_auth_backend.ldap.path
groupname = vault_ldap_auth_backend_group.test.groupname
policies = %s
}
`, backend, groupname, util.ArrayToTerraformList(policies))
}
Loading