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

Adjust LDAP for TM #732

Merged
merged 10 commits into from
Jan 31, 2025
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/v3.0.0/732-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Created new type `TmLdapSettings` to manage LDAP settings for the Tenant Manager [GH-732]
* Added methods `VCDClient.TmLdapConfigure`, `TmOrg.LdapConfigure`, `VCDClient.TmLdapDisable`, `TmOrg.LdapDisable`,
`VCDClient.TmGetLdapConfiguration`, `TmOrg.GetLdapConfiguration` to configure LDAP in Tenant Manager and regular Organizations [GH-732]
143 changes: 143 additions & 0 deletions govcd/tm_ldap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License.
*/

package govcd

import (
"bytes"
"encoding/json"
"fmt"
"github.com/vmware/go-vcloud-director/v3/types/v56"
"io"
"net/http"
"net/url"
"strings"
)

// TmLdapConfigure configures LDAP for the Tenant Manager "System" organization
func (vcdClient *VCDClient) TmLdapConfigure(settings *types.TmLdapSettings) (*types.TmLdapSettings, error) {
if !vcdClient.Client.IsTm() {
return nil, fmt.Errorf("this method is only supported in TM")
}

result, err := ldapExecuteRequest(vcdClient, "", http.MethodPut, settings)
if err != nil {
return nil, err
}
return result.(*types.TmLdapSettings), nil
}

// LdapConfigure configures LDAP for the given organization
func (org *TmOrg) LdapConfigure(settings *types.OrgLdapSettingsType) (*types.OrgLdapSettingsType, error) {
result, err := ldapExecuteRequest(org.vcdClient, org.TmOrg.ID, http.MethodPut, settings)
if err != nil {
return nil, err
}
return result.(*types.OrgLdapSettingsType), nil
}

// TmLdapDisable wraps LdapConfigure to disable LDAP configuration for the "System" organization
func (vcdClient *VCDClient) TmLdapDisable() error {
if !vcdClient.Client.IsTm() {
return fmt.Errorf("this method is only supported in TM")
}
_, err := ldapExecuteRequest(vcdClient, "", http.MethodDelete, nil)
return err
}

// LdapDisable wraps LdapConfigure to disable LDAP configuration for the given organization
func (org *TmOrg) LdapDisable() error {
// For Orgs, deletion is PUT call with empty payload
_, err := ldapExecuteRequest(org.vcdClient, org.TmOrg.ID, http.MethodPut, &types.OrgLdapSettingsType{OrgLdapMode: types.LdapModeNone})
return err
}

// TmGetLdapConfiguration retrieves LDAP configuration structure for the "System" organization in Tenant Manager
func (vcdClient *VCDClient) TmGetLdapConfiguration() (*types.TmLdapSettings, error) {
if !vcdClient.Client.IsTm() {
return nil, fmt.Errorf("this method is only supported in TM")
}
result, err := ldapExecuteRequest(vcdClient, "", http.MethodGet, nil)
if err != nil {
return nil, err
}
return result.(*types.TmLdapSettings), nil
}

// GetLdapConfiguration retrieves LDAP configuration structure of the given organization
func (org *TmOrg) GetLdapConfiguration() (*types.OrgLdapSettingsType, error) {
result, err := ldapExecuteRequest(org.vcdClient, org.TmOrg.ID, http.MethodGet, nil)
if err != nil {
return nil, err
}
return result.(*types.OrgLdapSettingsType), nil
}

// ldapExecuteRequest executes a request to the LDAP endpoint with the given payload and HTTP method
func ldapExecuteRequest(vcdClient *VCDClient, orgId, method string, payload interface{}) (interface{}, error) {
if method == http.MethodPut && payload == nil {
return nil, fmt.Errorf("the LDAP settings cannot be nil when performing a PUT call")
}

var endpoint *url.URL
var err error
if orgId != "" {
endpoint, err = url.ParseRequestURI(fmt.Sprintf("%s/admin/org/%s/settings/ldap", vcdClient.Client.VCDHREF.String(), extractUuid(orgId)))
} else {
endpoint, err = url.ParseRequestURI(fmt.Sprintf("%s/admin/extension/settings/ldapSettings", vcdClient.Client.VCDHREF.String()))
}
if err != nil {
return nil, err
}

// If the call is PUT, we prepare the body with the input settings
var body io.Reader
if method == http.MethodPut {
text := bytes.Buffer{}
encoder := json.NewEncoder(&text)
err = encoder.Encode(payload)
if err != nil {
return nil, err
}
body = strings.NewReader(text.String())
}
// Minimum version is 40.0 for TM LDAP
apiVersion := vcdClient.Client.APIVersion
if vcdClient.Client.APIClientVersionIs("< 40.0") {
apiVersion = "40.0"
}

// Set headers with content type + version
headers := http.Header{}
headers.Set("Accept", fmt.Sprintf("%s;version=%s", types.JSONAllMime, apiVersion))
headers.Set("Content-Type", types.JSONAllMime)

// Perform the HTTP call with the obtained endpoint and method
req := vcdClient.Client.newRequest(nil, nil, method, *endpoint, body, "", headers)
resp, err := checkResp(vcdClient.Client.Http.Do(req))
if err != nil {
return nil, fmt.Errorf("error getting LDAP settings: %s", err)
}

// Other than DELETE with get a body response
if method != http.MethodDelete {
// Organization result type is different from System type
if orgId != "" {
var result types.OrgLdapSettingsType
err = decodeBody(types.BodyTypeJSON, resp, &result)
if err != nil {
return nil, fmt.Errorf("error decoding Organization LDAP settings: %s", err)
}
return &result, nil
} else {
var result types.TmLdapSettings
err = decodeBody(types.BodyTypeJSON, resp, &result)
if err != nil {
return nil, fmt.Errorf("error decoding LDAP settings: %s", err)
}
return &result, nil
}
}
return nil, nil
}
167 changes: 167 additions & 0 deletions govcd/tm_ldap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//go:build tm || functional || ALL

/*
* Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License.
*/

package govcd

import (
"fmt"
"github.com/vmware/go-vcloud-director/v3/types/v56"
. "gopkg.in/check.v1"
"regexp"
)

// Test_TmLdapSystemWithVCenterLdap tests LDAP configuration in Provider (System) org by using
// vCenter as LDAP
func (vcd *TestVCD) Test_TmLdapSystemWithVCenterLdap(check *C) {
skipNonTm(vcd, check)
if vcd.skipAdminTests {
check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName()))
}
vcd.checkSkipWhenApiToken(check)

ldapSettings := types.TmLdapSettings{
AuthenticationMechanism: "SIMPLE",
ConnectorType: "ACTIVE_DIRECTORY",
CustomUiButtonLabel: addrOf("Hello there"),
GroupAttributes: &types.LdapGroupAttributesType{
BackLinkIdentifier: "objectSid",
GroupName: "cn",
Membership: "member",
MembershipIdentifier: "dn",
ObjectClass: "group",
ObjectIdentifier: "objectGuid",
},
HostName: regexp.MustCompile(`https?://`).ReplaceAllString(vcd.config.Tm.VcenterUrl, ""),
IsSsl: false,
MaxResults: 200,
MaxUserGroups: 1015,
PageSize: 200,
PagedSearchDisabled: false,
Password: vcd.config.Tm.VcenterPassword,
Port: 389,
SearchBase: "dc=vsphere,dc=local",
UserAttributes: &types.LdapUserAttributesType{
Email: "mail",
FullName: "displayName",
GivenName: "givenName",
GroupBackLinkIdentifier: "tokenGroups",
GroupMembershipIdentifier: "dn",
ObjectClass: "user",
ObjectIdentifier: "objectGuid",
Surname: "sn",
Telephone: "telephoneNumber",
UserName: "sAMAccountName",
},
UserName: "cn=Administrator,cn=Users,dc=vsphere,dc=local",
}

receivedSettings, err := vcd.client.TmLdapConfigure(&ldapSettings)
check.Assert(err, IsNil)
check.Assert(receivedSettings, NotNil)

receivedSettings2, err := vcd.client.TmGetLdapConfiguration()
check.Assert(err, IsNil)
check.Assert(receivedSettings, DeepEquals, receivedSettings2)

defer func() {
fmt.Println("Unconfiguring LDAP")
// Clear LDAP configuration
err = vcd.client.TmLdapDisable()
check.Assert(err, IsNil)
}()
}

// Test_TmLdapOrgWithVCenterLdap tests LDAP configuration in a regular Organization by using
// vCenter as LDAP
func (vcd *TestVCD) Test_TmLdapOrgWithVCenterLdap(check *C) {
skipNonTm(vcd, check)
if vcd.skipAdminTests {
check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName()))
}
vcd.checkSkipWhenApiToken(check)

// We are testing LDAP for a regular Organization
cfg := &types.TmOrg{
Name: check.TestName(),
DisplayName: check.TestName(),
IsEnabled: true,
}
org, err := vcd.client.CreateTmOrg(cfg)
check.Assert(err, IsNil)
check.Assert(org, NotNil)

defer func() {
err = org.Disable()
check.Assert(err, IsNil)
err = org.Delete()
check.Assert(err, IsNil)
}()

// Add to cleanup list
PrependToCleanupListOpenApi(org.TmOrg.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointOrgs+org.TmOrg.ID)

ldapSettings := &types.OrgLdapSettingsType{
OrgLdapMode: types.LdapModeCustom,
CustomOrgLdapSettings: &types.CustomOrgLdapSettings{
HostName: regexp.MustCompile(`https?://`).ReplaceAllString(vcd.config.Tm.VcenterUrl, ""),
Port: 389,
SearchBase: "dc=vsphere,dc=local",
AuthenticationMechanism: "SIMPLE",
ConnectorType: "OPEN_LDAP",
Username: "cn=Administrator,cn=Users,dc=vsphere,dc=local",
Password: vcd.config.Tm.VcenterPassword,
UserAttributes: &types.OrgLdapUserAttributes{
ObjectClass: "inetOrgPerson",
ObjectIdentifier: "uid",
Username: "uid",
Email: "mail",
FullName: "cn",
GivenName: "givenName",
Surname: "sn",
Telephone: "telephoneNumber",
GroupMembershipIdentifier: "dn",
},
GroupAttributes: &types.OrgLdapGroupAttributes{
ObjectClass: "group",
ObjectIdentifier: "cn",
GroupName: "cn",
Membership: "member",
MembershipIdentifier: "dn",
},
},
}

receivedSettings, err := org.LdapConfigure(ldapSettings)
check.Assert(err, IsNil)
check.Assert(receivedSettings, NotNil)

receivedSettings2, err := org.GetLdapConfiguration()
check.Assert(err, IsNil)
check.Assert(receivedSettings, DeepEquals, receivedSettings2)

ldapSettings.OrgLdapMode = types.LdapModeSystem
ldapSettings.CustomOrgLdapSettings = nil
receivedSettings, err = org.LdapConfigure(ldapSettings)
check.Assert(err, IsNil)
check.Assert(receivedSettings, NotNil)

receivedSettings2, err = org.GetLdapConfiguration()
check.Assert(err, IsNil)
check.Assert(receivedSettings, DeepEquals, receivedSettings2)

// This is same to deletion
ldapSettings.OrgLdapMode = types.LdapModeNone
receivedSettings, err = org.LdapConfigure(ldapSettings)
check.Assert(err, IsNil)
check.Assert(receivedSettings, NotNil)

receivedSettings2, err = org.GetLdapConfiguration()
check.Assert(err, IsNil)
check.Assert(receivedSettings, DeepEquals, receivedSettings2)

err = org.LdapDisable()
check.Assert(err, IsNil)
}
3 changes: 2 additions & 1 deletion types/v56/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const (
// SoapXML mime type
SoapXML = "application/soap+xml"
// JSONMime
JSONMime = "application/json"
JSONMime = "application/json"
JSONAllMime = "application/*+json"
)

const (
Expand Down
47 changes: 47 additions & 0 deletions types/v56/tm.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,3 +620,50 @@ type TmRegionalNetworkingVpcConnectivityProfile struct {
// ExternalCidrBlocks is a comma separated list of the external IP CIDRs which are available for use by the VPC.
ExternalCidrBlocks string `json:"externalCidrBlocks,omitempty"`
}

// TmLdapSettings is the type that defines LDAP settings in Tenant Manager
type TmLdapSettings struct {
AuthenticationMechanism string `json:"authenticationMechanism"` // Can be "SIMPLE", "KERBEROS", "MD5DIGEST", "NTLM"
ConnectorType string `json:"connectorType"` // Can be "ACTIVE_DIRECTORY", "OPENLDAP"
CustomTruststore *string `json:"customTruststore,omitempty"`
CustomUiButtonLabel *string `json:"customUiButtonLabel,omitempty"`
GroupAttributes *LdapGroupAttributesType `json:"groupAttributes"`
GroupSearchBase string `json:"groupSearchBase,omitempty"`
HostName string `json:"hostName,omitempty"`
IsGroupSearchBaseEnabled bool `json:"isGroupSearchBaseEnabled"`
IsSsl bool `json:"isSsl"`
IsSslAcceptAll bool `json:"isSslAcceptAll,omitempty"`
MaxResults int `json:"maxResults,omitempty"`
MaxUserGroups int `json:"maxUserGroups,omitempty"`
PageSize int `json:"pageSize,omitempty"`
PagedSearchDisabled bool `json:"pagedSearchDisabled"`
Password string `json:"password,omitempty"`
Port int `json:"port"`
Realm *string `json:"realm,omitempty"`
SearchBase string `json:"searchBase,omitempty"`
UseExternalKerberos bool `json:"useExternalKerberos,omitempty"`
UserAttributes *LdapUserAttributesType `json:"userAttributes"`
UserName string `json:"userName,omitempty"`
}

type LdapGroupAttributesType struct {
BackLinkIdentifier string `json:"backLinkIdentifier,omitempty"`
GroupName string `json:"groupName"`
Membership string `json:"membership"`
MembershipIdentifier string `json:"membershipIdentifier"`
ObjectClass string `json:"objectClass"`
ObjectIdentifier string `json:"objectIdentifier"`
}

type LdapUserAttributesType struct {
Email string `json:"email"`
FullName string `json:"fullName"`
GivenName string `json:"givenName"`
GroupBackLinkIdentifier string `json:"groupBackLinkIdentifier,omitempty"`
GroupMembershipIdentifier string `json:"groupMembershipIdentifier"`
ObjectClass string `json:"objectClass"`
ObjectIdentifier string `json:"objectIdentifier"`
Surname string `json:"surname"`
Telephone string `json:"telephone"`
UserName string `json:"userName"`
}
Loading