Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions .changes/v1.0.0/26-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- **New Resource:** `vcfa_org_ldap` to manage LDAP settings of Organizations [GH-26]
- **New Data Source:** `vcfa_org_ldap` to read LDAP settings of Organizations [GH-26]
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,5 @@ require (
google.golang.org/protobuf v1.35.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

replace github.com/vmware/go-vcloud-director/v3 => github.com/adambarreiro/go-vcloud-director/v3 v3.0.0-20250130134147-f01d543cc41b
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg=
github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/adambarreiro/go-vcloud-director/v3 v3.0.0-20250130134147-f01d543cc41b h1:rb5H9kV/+AS3xS0+vjLQfM0Ek2ExPpLRrRN+EEmfdv8=
github.com/adambarreiro/go-vcloud-director/v3 v3.0.0-20250130134147-f01d543cc41b/go.mod h1:68KHsVns52dsq/w5JQYzauaU/+NAi1FmCxhBrFc/VoQ=
github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE=
github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
Expand Down Expand Up @@ -149,8 +151,6 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/vmware/go-vcloud-director/v3 v3.0.0-alpha.23 h1:GhQhlULBs/7oetCZ2IFvQfH7OqEo6Q5r9GT6v5YgZOQ=
github.com/vmware/go-vcloud-director/v3 v3.0.0-alpha.23/go.mod h1:68KHsVns52dsq/w5JQYzauaU/+NAi1FmCxhBrFc/VoQ=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
Expand Down
190 changes: 190 additions & 0 deletions vcfa/datasource_vcfa_org_ldap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package vcfa

import (
"context"

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

// datasourceLdapUserAttributes defines the elements of types.OrgLdapUserAttributes
// The field names are the ones used in the GUI, with a comment to indicate which structure field each one corresponds to
var datasourceLdapUserAttributes = &schema.Schema{
Type: schema.TypeList,
Computed: true,
Description: "Custom settings when `ldap_mode` is CUSTOM",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"object_class": { // ObjectClass
Type: schema.TypeString,
Computed: true,
Description: "LDAP objectClass of which imported users are members. For example, user or person",
},
"unique_identifier": { // ObjectIdentifier
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute to use as the unique identifier for a user. For example, objectGuid",
},
"username": { // Username
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute to use when looking up a user name to import. For example, userPrincipalName or samAccountName",
},
"email": { // Email
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute to use for the user's email address. For example, mail",
},
"display_name": { // FullName
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute to use for the user's full name. For example, displayName",
},
"given_name": { // GivenName
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute to use for the user's given name. For example, givenName",
},
"surname": { // Surname
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute to use for the user's surname. For example, sn",
},
"telephone": { // Telephone
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute to use for the user's telephone number. For example, telephoneNumber",
},
"group_membership_identifier": { // GroupMembershipIdentifier
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute that identifies a user as a member of a group. For example, dn",
},
"group_back_link_identifier": { // GroupBackLinkIdentifier
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute that returns the identifiers of all the groups of which the user is a member",
},
},
},
}

// datasourceLdapGroupAttributes defines the elements of types.OrgLdapGroupAttributes
// The field names are the ones used in the GUI, with a comment to indicate which structure field each one corresponds to
var datasourceLdapGroupAttributes = &schema.Schema{
Type: schema.TypeList,
Computed: true,
Description: "Custom settings when `ldap_mode` is CUSTOM",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"object_class": { // ObjectClass
Type: schema.TypeString,
Computed: true,
Description: "LDAP objectClass of which imported groups are members. For example, group",
},
"unique_identifier": { // ObjectIdentifier
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute to use as the unique identifier for a group. For example, objectGuid",
},
"name": { // GroupName
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute to use for the group name. For example, cn",
},
"membership": { // Membership
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute to use when getting the members of a group. For example, member",
},
"group_membership_identifier": { // MembershipIdentifier
Type: schema.TypeString,
Computed: true,
Description: "LDAP attribute that identifies a group as a member of another group. For example, dn",
},
"group_back_link_identifier": { // BackLinkIdentifier
Type: schema.TypeString,
Computed: true,
Description: "LDAP group attribute used to identify a group member",
},
},
},
}

func datasourceVcfaOrgLdap() *schema.Resource {
return &schema.Resource{
ReadContext: datasourceVcfaOrgLdapRead,
Schema: map[string]*schema.Schema{
"org_id": {
Type: schema.TypeString,
Required: true,
Description: "Organization ID",
},
"ldap_mode": { // OrgLdapMode
Type: schema.TypeString,
Computed: true,
Description: "Type of LDAP settings (one of NONE, SYSTEM, CUSTOM)",
},
"custom_user_ou": { // CustomUsersOu
Type: schema.TypeString,
Computed: true,
Description: "If ldap_mode is SYSTEM, specifies a LDAP attribute=value pair to use for OU (organizational unit)",
},
"custom_settings": { // CustomOrgLdapSettings
Type: schema.TypeList,
Computed: true,
Description: "Custom settings when `ldap_mode` is CUSTOM",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"server": { // Hostname
Type: schema.TypeString,
Computed: true,
Description: "host name or IP of the LDAP server",
},
"port": { // Port
Type: schema.TypeInt,
Computed: true,
Description: "Port number for LDAP service",
},
"authentication_method": { // AuthenticationMechanism
Type: schema.TypeString,
Computed: true,
Description: "authentication method: one of SIMPLE, MD5DIGEST, NTLM",
},
"connector_type": { // ConnectorType
Type: schema.TypeString,
Computed: true,
Description: "type of connector: one of OPEN_LDAP, ACTIVE_DIRECTORY",
},
"base_distinguished_name": { //SearchBase
Type: schema.TypeString,
Computed: true,
Description: "LDAP search base",
},
"is_ssl": { // IsSsl
Type: schema.TypeBool,
Computed: true,
Description: "True if the LDAP service requires an SSL connection",
},
"username": { // Username
Type: schema.TypeString,
Computed: true,
Description: `Username to use when logging in to LDAP, specified using LDAP attribute=value pairs (for example: cn="ldap-admin", c="example", dc="com")`,
},
"password": { // Password
Type: schema.TypeString,
Computed: true,
Description: `Password for the user identified by UserName. This value is never returned by GET. It is inspected on create and modify. On modify, the absence of this element indicates that the password should not be changed`,
},
"user_attributes": datasourceLdapUserAttributes,
"group_attributes": datasourceLdapGroupAttributes,
},
},
},
},
}
}

func datasourceVcfaOrgLdapRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return genericVcfaOrgLdapRead(ctx, d, meta, "datasource")
}
2 changes: 2 additions & 0 deletions vcfa/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ var globalDataSourceMap = map[string]*schema.Resource{
"vcfa_role": datasourceVcfaRole(), // 1.0
"vcfa_global_role": datasourceVcfaGlobalRole(), // 1.0
"vcfa_certificate": datasourceVcfaCertificate(), // 1.0
"vcfa_org_ldap": datasourceVcfaOrgLdap(), // 1.0
}

var globalResourceMap = map[string]*schema.Resource{
Expand All @@ -81,6 +82,7 @@ var globalResourceMap = map[string]*schema.Resource{
"vcfa_global_role": resourceVcfaGlobalRole(), // 1.0
"vcfa_api_token": resourceVcfaApiToken(), // 1.0
"vcfa_certificate": resourceVcfaCertificate(), // 1.0
"vcfa_org_ldap": resourceVcfaOrgLdap(), // 1.0
}

// Provider returns a terraform.ResourceProvider.
Expand Down
164 changes: 164 additions & 0 deletions vcfa/resource_vcd_org_ldap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//go:build ldap || org || ALL || functional

package vcfa

import (
"fmt"
"regexp"
"testing"

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

func TestAccVcfaOrgLdap(t *testing.T) {
preTestChecks(t)
skipIfNotSysAdmin(t)

var params = StringMap{
"Org": testConfig.Tm.Org,
"LdapServer": regexp.MustCompile(`https?://`).ReplaceAllString(testConfig.Tm.VcenterUrl, ""),
"Password": testConfig.Tm.VcenterPassword,
"Tags": "ldap org",
}
testParamsNotEmpty(t, params)

params["FuncName"] = t.Name()
configText := templateFill(testAccVcfaOrgLdap, params)

// TODO: TM: Missing System test

params["FuncName"] = t.Name() + "-DS"
configTextDS := templateFill(testAccVcfaOrgLdapDS, params)
if vcfaShortTest {
t.Skip(acceptanceTestsSkipped)
return
}
debugPrintf("#[DEBUG] CONFIGURATION Resource for Organization LDAP (Custom): %s\n", configText)
debugPrintf("#[DEBUG] CONFIGURATION Data source: %s\n", configTextDS)

orgDef := "vcfa_org.org1"
ldapResourceDef := "vcfa_org_ldap.ldap"
ldapDatasourceDef := "data.vcfa_org_ldap.ldap-ds"
resource.Test(t, resource.TestCase{
ProviderFactories: testAccProviders,
// TODO: TM: Check LDAP is destroyed before Organization is
// CheckDestroy: testAccCheckOrgLdapDestroy(ldapResourceDef),
Steps: []resource.TestStep{
{
Config: configText,
Check: resource.ComposeTestCheckFunc(
testAccCheckOrgLdapExists(ldapResourceDef),
resource.TestCheckResourceAttr(orgDef, "name", params["Org"].(string)),
resource.TestCheckResourceAttr(ldapResourceDef, "ldap_mode", "CUSTOM"),
resource.TestCheckResourceAttr(ldapResourceDef, "custom_settings.0.server", params["LdapServer"].(string)),
resource.TestCheckResourceAttr(ldapResourceDef, "custom_settings.0.authentication_method", "SIMPLE"),
resource.TestCheckResourceAttr(ldapResourceDef, "custom_settings.0.connector_type", "OPEN_LDAP"),
resource.TestCheckResourceAttr(ldapResourceDef, "custom_settings.0.user_attributes.0.object_class", "inetOrgPerson"),
resource.TestCheckResourceAttr(ldapResourceDef, "custom_settings.0.group_attributes.0.object_class", "group"),
resource.TestCheckResourceAttrPair(orgDef, "id", ldapResourceDef, "org_id"),
),
},
{
Config: configTextDS,
Check: resource.ComposeTestCheckFunc(
testAccCheckOrgLdapExists(ldapResourceDef),
resource.TestCheckResourceAttrPair(ldapResourceDef, "org_id", ldapDatasourceDef, "org_id"),
resource.TestCheckResourceAttrPair(ldapResourceDef, "ldap_mode", ldapDatasourceDef, "ldap_mode"),
resource.TestCheckResourceAttrPair(ldapResourceDef, "custom_settings.0.server", ldapDatasourceDef, "custom_settings.0.server"),
resource.TestCheckResourceAttrPair(ldapResourceDef, "custom_settings.0.authentication_method", ldapDatasourceDef, "custom_settings.0.authentication_method"),
resource.TestCheckResourceAttrPair(ldapResourceDef, "custom_settings.0.connector_type", ldapDatasourceDef, "custom_settings.0.connector_type"),
resource.TestCheckResourceAttrPair(ldapResourceDef, "custom_settings.0.user_attributes.0.object_class", ldapDatasourceDef, "custom_settings.0.user_attributes.0.object_class"),
resource.TestCheckResourceAttrPair(ldapResourceDef, "custom_settings.0.group_attributes.0.object_class", ldapDatasourceDef, "custom_settings.0.group_attributes.0.object_class"),
),
},
{
ResourceName: ldapResourceDef,
ImportState: true,
ImportStateVerify: true,
ImportStateIdFunc: func(state *terraform.State) (string, error) { return testConfig.Tm.Org, nil },
},
},
})
postTestChecks(t)
}

func testAccCheckOrgLdapExists(identifier string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[identifier]
if !ok {
return fmt.Errorf("not found: %s", identifier)
}

if rs.Primary.ID == "" {
return fmt.Errorf("no %s ID is set", labelVcfaOrg)
}

conn := testAccProvider.Meta().(*VCDClient)

tmOrg, err := conn.GetTmOrgById(rs.Primary.ID)
if err != nil {
return err
}
config, err := tmOrg.GetLdapConfiguration()
if err != nil {
return err
}
if config.OrgLdapMode == "NONE" {
return fmt.Errorf("resource %s not configured", identifier)
}
return nil
}
}

const testAccVcfaOrgLdap = `
resource "vcfa_org" "org1" {
name = "{{.Org}}"
display_name = "{{.Org}}"
description = "{{.Org}}"
}

resource "vcfa_org_ldap" "ldap" {
org_id = vcfa_org.org1.id
ldap_mode = "CUSTOM"
custom_settings {
server = "{{.LdapServer}}"
port = 389
is_ssl = false
username = "cn=Administrator,cn=Users,dc=vsphere,dc=local"
password = "{{.Password}}"
authentication_method = "SIMPLE"
base_distinguished_name = "dc=vsphere,dc=local"
connector_type = "OPEN_LDAP"
user_attributes {
object_class = "inetOrgPerson"
unique_identifier = "uid"
display_name = "cn"
username = "uid"
given_name = "givenName"
surname = "sn"
telephone = "telephoneNumber"
group_membership_identifier = "dn"
email = "mail"
}
group_attributes {
name = "cn"
object_class = "group"
membership = "member"
unique_identifier = "cn"
group_membership_identifier = "dn"
}
}
lifecycle {
# password value does not get returned by GET
ignore_changes = [custom_settings[0].password]
}
}
`

const testAccVcfaOrgLdapDS = testAccVcfaOrgLdap + `
data "vcfa_org_ldap" "ldap-ds" {
org_id = vcfa_org.org1.id
depends_on = [vcfa_org_ldap.ldap]
}
`
Loading