diff --git a/.changes/v3.0.0/714-features.md b/.changes/v3.0.0/714-features.md index 55f672c00..4d1286e79 100644 --- a/.changes/v3.0.0/714-features.md +++ b/.changes/v3.0.0/714-features.md @@ -1,8 +1,9 @@ * Add trusted certificate management types `TrustedCertificate` and `types.TrustedCertificate` together with `VCDClient.AutoTrustCertificate`, `VCDClient.CreateTrustedCertificate`, - `VCDClient.GetAllTrustedCertificates`, `GetTrustedCertificateByName`, - `VCDClient.GetTrustedCertificateById`, `TrustedCertificate.Update`, `TrustedCertificate.Delete` - [GH-714] + `TmOrg.CreateTrustedCertificate`, `VCDClient.GetAllTrustedCertificates`, `TmOrg.GetAllTrustedCertificates`, + `VCDClient.GetTrustedCertificateByName`, `TmOrg.GetTrustedCertificateByName`, + `VCDClient.GetTrustedCertificateById`, `TmOrg.GetTrustedCertificateById`, `TrustedCertificate.Update`, `TrustedCertificate.Delete` + [GH-714, GH-746] * vCenter management types `VCenter` and `types.VSphereVirtualCenter` adds Create, Update and Delete methods: `VCDClient.CreateVcenter`, `VCDClient.GetAllVCenters`, `VCDClient.GetVCenterByName`, `VCDClient.GetVCenterById`, `VCenter.Update`, `VCenter.Delete`, `VCenter.RefreshVcenter`, diff --git a/.changes/v3.0.0/732-features.md b/.changes/v3.0.0/732-features.md index 9e0c733f3..c1de597f1 100644 --- a/.changes/v3.0.0/732-features.md +++ b/.changes/v3.0.0/732-features.md @@ -1,3 +1,3 @@ -* Created new type `TmLdapSettings` to manage LDAP settings for the Tenant Manager [GH-732] +* Created new type `TmLdapSettings` to manage LDAP settings for the Tenant Manager [GH-732, GH-746] * 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] + `VCDClient.TmGetLdapConfiguration`, `TmOrg.GetLdapConfiguration` to configure LDAP in Tenant Manager and regular Organizations [GH-732, GH-746] diff --git a/govcd/api.go b/govcd/api.go index 306ec59ec..ac2198b8d 100644 --- a/govcd/api.go +++ b/govcd/api.go @@ -378,6 +378,10 @@ func checkResp(resp *http.Response, err error) (*http.Response, error) { return checkRespWithErrType(types.BodyTypeXML, resp, err, &types.Error{}) } +func checkJsonResp(resp *http.Response, err error) (*http.Response, error) { + return checkRespWithErrType(types.BodyTypeJSON, resp, err, &types.Error{}) +} + // checkRespWithErrType allows to specify custom error errType for checkResp unmarshaling // the error. func checkRespWithErrType(bodyType types.BodyType, resp *http.Response, err, errType error) (*http.Response, error) { diff --git a/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index abc8e9679..f17cadc85 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -162,6 +162,16 @@ type TestConfig struct { NsxtManagerUrl string `yaml:"nsxtManagerUrl"` NsxtEdgeCluster string `yaml:"nsxtEdgeCluster"` NsxtTier0Gateway string `yaml:"nsxtTier0Gateway"` + + Ldap struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + IsSsl bool `yaml:"isSsl"` + Username string `yaml:"username"` + Password string `yaml:"password"` + BaseDistinguishedName string `yaml:"baseDistinguishedName"` + Type string `yaml:"type"` + } `yaml:"ldap,omitempty"` } `yaml:"tm,omitempty"` VCD struct { Org string `yaml:"org"` diff --git a/govcd/nsxt_manager_openapi_test.go b/govcd/nsxt_manager_openapi_test.go index 33d0b8237..397064b23 100644 --- a/govcd/nsxt_manager_openapi_test.go +++ b/govcd/nsxt_manager_openapi_test.go @@ -27,7 +27,7 @@ func (vcd *TestVCD) Test_NsxtManagerOpenApi(check *C) { // Certificate must be trusted before adding NSX-T Manager url, err := url.Parse(cfg.Url) check.Assert(err, IsNil) - trustedCert, err := vcd.client.AutoTrustCertificate(url) + trustedCert, err := vcd.client.AutoTrustHttpsCertificate(url, nil) check.Assert(err, IsNil) if trustedCert != nil { AddToCleanupListOpenApi(trustedCert.TrustedCertificate.ID, check.TestName()+"trusted-cert", types.OpenApiPathVersion1_0_0+types.OpenApiEndpointTrustedCertificates+trustedCert.TrustedCertificate.ID) diff --git a/govcd/org_oidc.go b/govcd/org_oidc.go index 74b45756b..282c4f146 100644 --- a/govcd/org_oidc.go +++ b/govcd/org_oidc.go @@ -12,7 +12,6 @@ import ( "io" "net/http" "net/url" - "strconv" "strings" "github.com/vmware/go-vcloud-director/v3/types/v56" @@ -225,20 +224,13 @@ func oidcValidateConnection(client *Client, endpoint string) error { if err != nil { return err } - isSecure := strings.ToLower(uri.Scheme) == "https" - - rawPort := uri.Port() - if rawPort == "" { - rawPort = "80" - if isSecure { - rawPort = "443" - } - } - port, err := strconv.Atoi(rawPort) + port, err := getEndpointPort(uri) if err != nil { - return err + return fmt.Errorf("error getting port number for host '%s': %s", uri.Hostname(), err) } + isSecure := port == 443 + result, err := client.TestConnection(types.TestConnection{ Host: uri.Hostname(), Port: port, diff --git a/govcd/sample_govcd_test_config.yaml b/govcd/sample_govcd_test_config.yaml index 66aa6ffc6..ce61826e3 100644 --- a/govcd/sample_govcd_test_config.yaml +++ b/govcd/sample_govcd_test_config.yaml @@ -318,3 +318,12 @@ tm: nsxtManagerPassword: password for NSX-T Manager nsxtManagerUrl: https://HOST nsxtTier0Gateway: tier0gateway + + ldap: + host: my.ldap.server.net + port: 636 + isSsl: true + username: admin + password: StrongPassword + baseDistinguishedName: OU=demo,DC=foo,DC=bar,DC=test,DC=net + type: ACTIVE_DIRECTORY # ACTIVE_DIRECTORY or OPEN_LDAP diff --git a/govcd/tm_common_test.go b/govcd/tm_common_test.go index ae6484966..2571f0515 100644 --- a/govcd/tm_common_test.go +++ b/govcd/tm_common_test.go @@ -48,7 +48,7 @@ func getOrCreateVCenter(vcd *TestVCD, check *C) (*VCenter, func()) { // Certificate must be trusted before adding vCenter url, err := url.Parse(vcCfg.Url) check.Assert(err, IsNil) - trustedCert, err := vcd.client.AutoTrustCertificate(url) + trustedCert, err := vcd.client.AutoTrustHttpsCertificate(url, nil) check.Assert(err, IsNil) if trustedCert != nil { printVerbose("# Certificate for vCenter is trusted %s\n", trustedCert.TrustedCertificate.ID) @@ -136,7 +136,7 @@ func getOrCreateNsxtManager(vcd *TestVCD, check *C) (*NsxtManagerOpenApi, func() // Certificate must be trusted before adding NSX-T Manager url, err := url.Parse(nsxtCfg.Url) check.Assert(err, IsNil) - trustedCert, err := vcd.client.AutoTrustCertificate(url) + trustedCert, err := vcd.client.AutoTrustHttpsCertificate(url, nil) check.Assert(err, IsNil) if trustedCert != nil { printVerbose("# Certificate for NSX-T Manager is trusted %s\n", trustedCert.TrustedCertificate.ID) diff --git a/govcd/tm_content_library_test.go b/govcd/tm_content_library_test.go index 972ef75fd..af8eb02bd 100644 --- a/govcd/tm_content_library_test.go +++ b/govcd/tm_content_library_test.go @@ -227,7 +227,7 @@ func (vcd *TestVCD) Test_ContentLibrarySubscribed(check *C) { // Certificate must be trusted before adding subscribed content library url, err := url.Parse(vcd.config.Tm.SubscriptionContentLibraryUrl) check.Assert(err, IsNil) - trustedCert, err := vcd.client.AutoTrustCertificate(url) + trustedCert, err := vcd.client.AutoTrustHttpsCertificate(url, nil) check.Assert(err, IsNil) if trustedCert != nil { AddToCleanupListOpenApi(trustedCert.TrustedCertificate.ID, check.TestName()+"trusted-cert", types.OpenApiPathVersion1_0_0+types.OpenApiEndpointTrustedCertificates+trustedCert.TrustedCertificate.ID) diff --git a/govcd/tm_ldap.go b/govcd/tm_ldap.go index 08c2e9097..dfadb910c 100644 --- a/govcd/tm_ldap.go +++ b/govcd/tm_ldap.go @@ -15,40 +15,140 @@ import ( "strings" ) -// TmLdapConfigure configures LDAP for the Tenant Manager "System" organization -func (vcdClient *VCDClient) TmLdapConfigure(settings *types.TmLdapSettings) (*types.TmLdapSettings, error) { +// TmLdapConfigure configures LDAP for the Tenant Manager "System" organization. If trustSslCertificate=true, +// it will automatically trust the certificate of LDAP server, only if settings.IsSsl=true. +// If settings.IsSsl=true and trustSslCertificate=false this method returns an error +func (vcdClient *VCDClient) TmLdapConfigure(settings *types.TmLdapSettings, trustSslCertificate bool) (*types.TmLdapSettings, error) { + var trustedCert *TrustedCertificate + if !vcdClient.Client.IsTm() { return nil, fmt.Errorf("this method is only supported in TM") } + if settings == nil { + return nil, fmt.Errorf("LDAP settings cannot be nil") + } + + // Always test connection, because this method always changes LDAP. There's no NONE mode like in + // the TmOrg receiver methods (that consume types.OrgLdapSettingsType) + connResult, err := ldapValidateConnection(&vcdClient.Client, settings.HostName, settings.Port, settings.IsSsl) + if err != nil { + return nil, err + } + + if !trustSslCertificate && connResult.TargetProbe.SSLResult == types.UntrustedCertificate { + return nil, fmt.Errorf("cannot configure LDAP '%s:%d' over SSL without trusting the SSL certificate", settings.HostName, settings.Port) + } + + if trustSslCertificate && connResult.TargetProbe.SSLResult == types.UntrustedCertificate { + // We retrieve existing LDAP configuration to check if the given input settings are for a fresh connection, or to override + // an existing one + existing, err := vcdClient.TmGetLdapConfiguration() + if err != nil { + return nil, fmt.Errorf("error retrieving existing LDAP configuration: %s", err) + } + + // If SSL is configured and retrieved settings are empty, or the hostname changed, we need to trust the certificate + needsCheckCert := settings.IsSsl && (existing.HostName == "" || existing.HostName != settings.HostName) + if needsCheckCert { + trustedCert, err = trustCertificate(vcdClient, nil, settings.HostName, connResult.TargetProbe.CertificateChain) + if err != nil { + return nil, fmt.Errorf("could not trust certificate for %s, Connection result: '%s', SSL result: '%s': %s", + settings.HostName, connResult.TargetProbe.ConnectionResult, connResult.TargetProbe.SSLResult, err) + } + } + } result, err := ldapExecuteRequest(vcdClient, "", http.MethodPut, settings) if err != nil { + // An error happened, so we must clean the trusted certificate + if trustedCert != nil { + innerErr := trustedCert.Delete() + if innerErr != nil { + return nil, fmt.Errorf("%s - Also, cleanup of SSL certificate '%s' failed: %s", err, trustedCert.TrustedCertificate.ID, innerErr) + } + } 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) { +// LdapConfigure configures LDAP for the receiver Organization. If trustSslCertificate=true, +// it will automatically trust the certificate of LDAP server, only if settings.IsSsl=true. +// If settings.IsSsl=true and trustSslCertificate=false this method returns an error +func (org *TmOrg) LdapConfigure(settings *types.OrgLdapSettingsType, trustSslCertificate bool) (*types.OrgLdapSettingsType, error) { + var trustedCert *TrustedCertificate + + if settings == nil { + return nil, fmt.Errorf("LDAP settings cannot be nil") + } + + // Validate the connection with LDAP only when types.LdapModeCustom mode is selected (as it's the only one that configures an LDAP server) + if settings.CustomOrgLdapSettings != nil && settings.OrgLdapMode == types.LdapModeCustom { + connResult, err := ldapValidateConnection(&org.vcdClient.Client, settings.CustomOrgLdapSettings.HostName, settings.CustomOrgLdapSettings.Port, settings.CustomOrgLdapSettings.IsSsl) + if err != nil { + return nil, err + } + + if !trustSslCertificate && connResult.TargetProbe.SSLResult == types.UntrustedCertificate { + return nil, fmt.Errorf("cannot configure LDAP '%s:%d' over SSL without trusting the SSL certificate", settings.CustomOrgLdapSettings.HostName, settings.CustomOrgLdapSettings.Port) + } + + if trustSslCertificate && connResult.TargetProbe.SSLResult == types.UntrustedCertificate { + // We retrieve existing LDAP configuration to check if the given input settings are for a fresh connection, or to override + // an existing one + existing, err := org.GetLdapConfiguration() + if err != nil { + return nil, fmt.Errorf("error retrieving existing LDAP configuration: %s", err) + } + + // Pre-condition here: Settings are always LDAP=CUSTOM and CustomOrgLdapSettings is not nil. + // Just check if existing LDAP config is new (different from CUSTOM), or if the host changed, as we need to trust certificate + // in that case + needsCheckCert := settings.CustomOrgLdapSettings.IsSsl && (existing.OrgLdapMode != types.LdapModeCustom || + (existing.CustomOrgLdapSettings != nil && existing.CustomOrgLdapSettings.HostName != settings.CustomOrgLdapSettings.HostName)) + if needsCheckCert { + trustedCert, err = trustCertificate(org.vcdClient, &TenantContext{ + OrgId: org.TmOrg.ID, + OrgName: org.TmOrg.Name, + }, settings.CustomOrgLdapSettings.HostName, connResult.TargetProbe.CertificateChain) + if err != nil { + return nil, fmt.Errorf("could not trust certificate for %s, Connection result: '%s', SSL result: '%s': %s", + settings.CustomOrgLdapSettings.HostName, connResult.TargetProbe.ConnectionResult, connResult.TargetProbe.SSLResult, err) + } + } + } + } + result, err := ldapExecuteRequest(org.vcdClient, org.TmOrg.ID, http.MethodPut, settings) - if err != nil { - return nil, err + if err == nil { + return result.(*types.OrgLdapSettingsType), nil } - return result.(*types.OrgLdapSettingsType), nil + + // An error happened, so we must clean the trusted certificate + if trustedCert != nil { + innerErr := trustedCert.Delete() + if innerErr != nil { + return nil, fmt.Errorf("%s - Also, cleanup of SSL certificate '%s' failed: %s", err, trustedCert.TrustedCertificate.ID, innerErr) + } + } + return nil, err } -// TmLdapDisable wraps LdapConfigure to disable LDAP configuration for the "System" organization +// TmLdapDisable wraps LdapConfigure to disable LDAP configuration for the "System" organization. +// Disabling LDAP does not remove any trusted certificate. func (vcdClient *VCDClient) TmLdapDisable() error { if !vcdClient.Client.IsTm() { return fmt.Errorf("this method is only supported in TM") } + // It is a different endpoint, so we don't need to check certificates. _, err := ldapExecuteRequest(vcdClient, "", http.MethodDelete, nil) return err } // LdapDisable wraps LdapConfigure to disable LDAP configuration for the given organization +// Disabling LDAP does not remove any trusted certificate. func (org *TmOrg) LdapDisable() error { - // For Orgs, deletion is PUT call with empty payload + // For Orgs, deletion is PUT call with empty payload. We don't need to check certificates. _, err := ldapExecuteRequest(org.vcdClient, org.TmOrg.ID, http.MethodPut, &types.OrgLdapSettingsType{OrgLdapMode: types.LdapModeNone}) return err } @@ -74,6 +174,30 @@ func (org *TmOrg) GetLdapConfiguration() (*types.OrgLdapSettingsType, error) { return result.(*types.OrgLdapSettingsType), nil } +// ldapValidateConnection executes a test probe against the given endpoint to validate that the client +// can establish a connection. +func ldapValidateConnection(client *Client, endpoint string, port int, isSecure bool) (*types.TestConnectionResult, error) { + uri, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + + result, err := client.TestConnection(types.TestConnection{ + Host: endpoint, + HostnameVerificationAlgorithm: "LDAPS", + Port: port, + Secure: &isSecure, + }) + if err != nil { + return nil, err + } + + if result.TargetProbe == nil || !result.TargetProbe.CanConnect || result.TargetProbe.ConnectionResult != "SUCCESS" { + return nil, fmt.Errorf("could not establish a connection to %s. Result: %s", uri.String(), result.TargetProbe.Result) + } + return result, 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 { @@ -115,7 +239,7 @@ func ldapExecuteRequest(vcdClient *VCDClient, orgId, method string, payload inte // 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)) + resp, err := checkJsonResp(vcdClient.Client.Http.Do(req)) if err != nil { return nil, fmt.Errorf("error getting LDAP settings: %s", err) } diff --git a/govcd/tm_ldap_test.go b/govcd/tm_ldap_test.go index 35acb7d3c..a82a38116 100644 --- a/govcd/tm_ldap_test.go +++ b/govcd/tm_ldap_test.go @@ -10,73 +10,113 @@ import ( "fmt" "github.com/vmware/go-vcloud-director/v3/types/v56" . "gopkg.in/check.v1" - "regexp" + "net/url" + "strings" ) -// Test_TmLdapSystemWithVCenterLdap tests LDAP configuration in Provider (System) org by using -// vCenter as LDAP -func (vcd *TestVCD) Test_TmLdapSystemWithVCenterLdap(check *C) { +// Test_TmLdapSystem tests LDAP configuration in Provider (System) org. This test +// checks LDAP connection with SSL and without SSL +func (vcd *TestVCD) Test_TmLdapSystem(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", + if vcd.config.Tm.Ldap.Host == "" || vcd.config.Tm.Ldap.Username == "" || vcd.config.Tm.Ldap.Password == "" || vcd.config.Tm.Ldap.Type == "" || + vcd.config.Tm.Ldap.Port == 0 || vcd.config.Tm.Ldap.BaseDistinguishedName == "" { + check.Skip("LDAP testing configuration is required") } - receivedSettings, err := vcd.client.TmLdapConfigure(&ldapSettings) - check.Assert(err, IsNil) - check.Assert(receivedSettings, NotNil) + // Definition of the different use cases for LDAP configuration + type testCase struct { + isSsl bool + } + testCases := []testCase{ + {isSsl: false}, + {isSsl: true}, + } - receivedSettings2, err := vcd.client.TmGetLdapConfiguration() - check.Assert(err, IsNil) - check.Assert(receivedSettings, DeepEquals, receivedSettings2) + for _, t := range testCases { + fmt.Printf("%s - %#v\n", check.TestName(), t) + + ldapSettings := types.TmLdapSettings{ + AuthenticationMechanism: "SIMPLE", + ConnectorType: vcd.config.Tm.Ldap.Type, + CustomUiButtonLabel: addrOf("Hello there"), + GroupAttributes: &types.LdapGroupAttributesType{ + BackLinkIdentifier: "objectSid", + GroupName: "cn", + Membership: "member", + MembershipIdentifier: "dn", + ObjectClass: "group", + ObjectIdentifier: "objectGuid", + }, + HostName: vcd.config.Tm.Ldap.Host, + IsSsl: t.isSsl, + MaxResults: 200, + MaxUserGroups: 1015, + PageSize: 200, + PagedSearchDisabled: false, + Password: vcd.config.Tm.Ldap.Password, + Port: vcd.config.Tm.Ldap.Port, + SearchBase: vcd.config.Tm.Ldap.BaseDistinguishedName, + UserAttributes: &types.LdapUserAttributesType{ + Email: "mail", + FullName: "displayName", + GivenName: "givenName", + GroupBackLinkIdentifier: "tokenGroups", + GroupMembershipIdentifier: "dn", + ObjectClass: "user", + ObjectIdentifier: "objectGuid", + Surname: "sn", + Telephone: "telephoneNumber", + UserName: "sAMAccountName", + }, + UserName: vcd.config.Tm.Ldap.Username, + } + + if t.isSsl { + _, err := vcd.client.TmLdapConfigure(&ldapSettings, false) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "cannot configure LDAP"), Equals, true) + } + + receivedSettings, err := vcd.client.TmLdapConfigure(&ldapSettings, t.isSsl) + check.Assert(err, IsNil) + check.Assert(receivedSettings, NotNil) + + receivedSettings2, err := vcd.client.TmGetLdapConfiguration() + check.Assert(err, IsNil) + check.Assert(receivedSettings2, NotNil) + check.Assert(receivedSettings2, DeepEquals, receivedSettings) + + // Update LDAP configuration. It should not trust any new certificate unless the host is changed + ldapSettings.MaxUserGroups = ldapSettings.MaxUserGroups + 1 + receivedSettings2, err = vcd.client.TmLdapConfigure(&ldapSettings, t.isSsl) + check.Assert(err, IsNil) + check.Assert(receivedSettings2, NotNil) + check.Assert(receivedSettings2.MaxUserGroups, Equals, receivedSettings.MaxUserGroups+1) - defer func() { - fmt.Println("Unconfiguring LDAP") // Clear LDAP configuration err = vcd.client.TmLdapDisable() check.Assert(err, IsNil) - }() + + if t.isSsl { + // Clean up trusted certificate + certs, err := vcd.client.GetAllTrustedCertificates(url.Values{ + "filter": []string{fmt.Sprintf("alias==*%s*", vcd.config.Tm.Ldap.Host)}, + }, nil) + check.Assert(err, IsNil) + check.Assert(len(certs), Equals, 1) // Important to check that only one certificate was added + err = certs[0].Delete() + 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) { +// Test_TmLdapOrg tests LDAP configuration in a regular Organization +func (vcd *TestVCD) Test_TmLdapOrg(check *C) { skipNonTm(vcd, check) if vcd.skipAdminTests { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) @@ -103,65 +143,101 @@ func (vcd *TestVCD) Test_TmLdapOrgWithVCenterLdap(check *C) { // 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", - }, - }, + // Definition of the different use cases for LDAP configuration + type testCase struct { + isSsl bool + } + testCases := []testCase{ + {isSsl: false}, + {isSsl: true}, } - receivedSettings, err := org.LdapConfigure(ldapSettings) - check.Assert(err, IsNil) - check.Assert(receivedSettings, NotNil) + for _, t := range testCases { + fmt.Printf("%s - %#v\n", check.TestName(), t) + ldapSettings := &types.OrgLdapSettingsType{ + OrgLdapMode: types.LdapModeCustom, + CustomOrgLdapSettings: &types.CustomOrgLdapSettings{ + HostName: vcd.config.Tm.Ldap.Host, + Port: vcd.config.Tm.Ldap.Port, + SearchBase: vcd.config.Tm.Ldap.BaseDistinguishedName, + AuthenticationMechanism: "SIMPLE", + IsSsl: t.isSsl, + ConnectorType: vcd.config.Tm.Ldap.Type, + Username: vcd.config.Tm.Ldap.Username, + 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", + }, + }, + } - receivedSettings2, err := org.GetLdapConfiguration() - check.Assert(err, IsNil) - check.Assert(receivedSettings, DeepEquals, receivedSettings2) + if t.isSsl { + _, err := org.LdapConfigure(ldapSettings, false) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "cannot configure LDAP"), Equals, true) + } - ldapSettings.OrgLdapMode = types.LdapModeSystem - ldapSettings.CustomOrgLdapSettings = nil - receivedSettings, err = org.LdapConfigure(ldapSettings) - check.Assert(err, IsNil) - check.Assert(receivedSettings, NotNil) + receivedSettings, err := org.LdapConfigure(ldapSettings, t.isSsl) + check.Assert(err, IsNil) + check.Assert(receivedSettings, NotNil) - receivedSettings2, err = org.GetLdapConfiguration() - check.Assert(err, IsNil) - check.Assert(receivedSettings, DeepEquals, receivedSettings2) + receivedSettings2, err := org.GetLdapConfiguration() + check.Assert(err, IsNil) + check.Assert(receivedSettings2, NotNil) + check.Assert(receivedSettings2, DeepEquals, receivedSettings) - // This is same to deletion - ldapSettings.OrgLdapMode = types.LdapModeNone - receivedSettings, err = org.LdapConfigure(ldapSettings) - check.Assert(err, IsNil) - check.Assert(receivedSettings, NotNil) + // Update LDAP configuration. It should not trust any new certificate unless the host is changed + ldapSettings.CustomOrgLdapSettings.SearchBase = vcd.config.Tm.Ldap.BaseDistinguishedName + ",DC=foo" + receivedSettings2, err = org.LdapConfigure(ldapSettings, t.isSsl) + check.Assert(err, IsNil) + check.Assert(receivedSettings2, NotNil) + check.Assert(receivedSettings2.CustomOrgLdapSettings.SearchBase, Equals, receivedSettings.CustomOrgLdapSettings.SearchBase+",DC=foo") - 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, t.isSsl) + check.Assert(err, IsNil) + check.Assert(receivedSettings, NotNil) - err = org.LdapDisable() - check.Assert(err, IsNil) + 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, t.isSsl) + check.Assert(err, IsNil) + check.Assert(receivedSettings, NotNil) + + receivedSettings2, err = org.GetLdapConfiguration() + check.Assert(err, IsNil) + check.Assert(receivedSettings, DeepEquals, receivedSettings2) + + if t.isSsl { + // Clean up trusted certificate. This step is not needed as it would be gone with the Org, but it's a meaningful check (see comment when + // checking number of retrieved certs) + certs, err := org.GetAllTrustedCertificates(url.Values{ + "filter": []string{fmt.Sprintf("alias==*%s*", vcd.config.Tm.Ldap.Host)}, + }) + check.Assert(err, IsNil) + check.Assert(len(certs), Equals, 1) // Important to check that only one certificate was added + err = certs[0].Delete() + check.Assert(err, IsNil) + } + } } diff --git a/govcd/trusted_certificates.go b/govcd/trusted_certificates.go index a68499c06..39943450f 100644 --- a/govcd/trusted_certificates.go +++ b/govcd/trusted_certificates.go @@ -27,9 +27,9 @@ func (g TrustedCertificate) wrap(inner *types.TrustedCertificate) *TrustedCertif return &g } -// AutoTrustCertificate will automatically trust certificate for a given endpoint +// AutoTrustHttpsCertificate will automatically trust certificate for a given HTTPS endpoint // Note. The URL must be accessible -func (vcdClient *VCDClient) AutoTrustCertificate(endpoint *url.URL) (*TrustedCertificate, error) { +func (vcdClient *VCDClient) AutoTrustHttpsCertificate(endpoint *url.URL, ctx *TenantContext) (*TrustedCertificate, error) { port, err := getEndpointPort(endpoint) if err != nil { return nil, fmt.Errorf("error getting port number for host '%s': %s", endpoint.Hostname(), err) @@ -50,47 +50,53 @@ func (vcdClient *VCDClient) AutoTrustCertificate(endpoint *url.URL) (*TrustedCer var trustedCert *TrustedCertificate if res != nil && res.TargetProbe != nil && res.TargetProbe.SSLResult != "SUCCESS" { - if res.TargetProbe.SSLResult == "ERROR_UNTRUSTED_CERTIFICATE" { - // Need to trust certificate - cert := res.TargetProbe.CertificateChain - if cert == "" { - return nil, fmt.Errorf("error - certificate chain is empty. Connection result: '%s', SSL result: '%s'", - res.TargetProbe.ConnectionResult, res.TargetProbe.SSLResult) - } - - // The CertificateChain may contain a single certificate or a chain of certificates. - // In case of a single certificate - only it should be submitted. - // In case of a chain - the last certificate is submitted to trust. - certCount := strings.Count(cert, "-----END CERTIFICATE-----") - var trust *types.TrustedCertificate - - if certCount == 1 { - // Certificate - trust = &types.TrustedCertificate{ - Alias: fmt.Sprintf("%s_%s", endpoint.Hostname(), time.Now().UTC().Format(time.RFC3339)), - Certificate: cert, - } - } else { - splitCerts := strings.SplitAfter(cert, "-----END CERTIFICATE-----") - trust = &types.TrustedCertificate{ - Alias: fmt.Sprintf("ca_%s", time.Now().UTC().Format(time.RFC3339)), - Certificate: splitCerts[len(splitCerts)-2], - } - } - - trustedCert, err = vcdClient.CreateTrustedCertificate(trust) + if res.TargetProbe.SSLResult == types.UntrustedCertificate { + trustedCert, err = trustCertificate(vcdClient, ctx, endpoint.Hostname(), res.TargetProbe.CertificateChain) if err != nil { - return nil, fmt.Errorf("error trusting Certificate %s: %s", trust.Alias, err) + return nil, fmt.Errorf("could not trust certificate for %s, Connection result: '%s', SSL result: '%s': %s", + endpoint.Hostname(), res.TargetProbe.ConnectionResult, res.TargetProbe.SSLResult, err) } - - util.Logger.Printf("[DEBUG] Certificate trust established ID - %s, Alias - %s", - trustedCert.TrustedCertificate.ID, trustedCert.TrustedCertificate.Alias) - } else { return nil, fmt.Errorf("SSL verification result - %s", res.TargetProbe.SSLResult) } + } + return trustedCert, nil +} + +// trustCertificate trusts the given certificate for the given hostname with the given certificate chain +func trustCertificate(vcdClient *VCDClient, ctx *TenantContext, hostname string, certificateChain string) (*TrustedCertificate, error) { + if certificateChain == "" { + return nil, fmt.Errorf("certificate chain is empty") + } + + // The CertificateChain may contain a single certificate or a chain of certificates. + // In case of a single certificate - only it should be submitted. + // In case of a chain - the last certificate is submitted to trust. + certCount := strings.Count(certificateChain, "-----END CERTIFICATE-----") + var trust *types.TrustedCertificate + + if certCount == 1 { + // Certificate + trust = &types.TrustedCertificate{ + Alias: fmt.Sprintf("%s_%s", hostname, time.Now().UTC().Format(time.RFC3339)), + Certificate: certificateChain, + } + } else { + splitCerts := strings.SplitAfter(certificateChain, "-----END CERTIFICATE-----") + trust = &types.TrustedCertificate{ + Alias: fmt.Sprintf("ca_%s", time.Now().UTC().Format(time.RFC3339)), + Certificate: splitCerts[len(splitCerts)-2], + } + } + trustedCert, err := vcdClient.CreateTrustedCertificate(trust, ctx) + if err != nil { + return nil, fmt.Errorf("error trusting Certificate %s: %s", trust.Alias, err) } + + util.Logger.Printf("[DEBUG] Certificate trust established ID - %s, Alias - %s", + trustedCert.TrustedCertificate.ID, trustedCert.TrustedCertificate.Alias) + return trustedCert, nil } @@ -112,30 +118,50 @@ func getEndpointPort(u *url.URL) (int, error) { return port, nil } -// CreateTrustedCertificate creates an entry in the trusted certificate records -func (vcdClient *VCDClient) CreateTrustedCertificate(config *types.TrustedCertificate) (*TrustedCertificate, error) { +// CreateTrustedCertificate creates an entry in the trusted certificate records with the given tenant context. If tenant context is nil, +// it assumes that the certificate will be stored in System org. +func (vcdClient *VCDClient) CreateTrustedCertificate(config *types.TrustedCertificate, ctx *TenantContext) (*TrustedCertificate, error) { c := crudConfig{ - entityLabel: labelTrustedCertificate, - endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTrustedCertificates, + entityLabel: labelTrustedCertificate, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTrustedCertificates, + additionalHeader: getTenantContextHeader(ctx), } outerType := TrustedCertificate{vcdClient: vcdClient} return createOuterEntity(&vcdClient.Client, outerType, c, config) } -// GetAllTrustedCertificates retrieves all trusted certificates with optional query filter -func (vcdClient *VCDClient) GetAllTrustedCertificates(queryParameters url.Values) ([]*TrustedCertificate, error) { +// CreateTrustedCertificate creates an entry in the trusted certificate records of the receiver Organization +func (org *TmOrg) CreateTrustedCertificate(config *types.TrustedCertificate) (*TrustedCertificate, error) { + return org.vcdClient.CreateTrustedCertificate(config, &TenantContext{ + OrgId: org.TmOrg.ID, + OrgName: org.TmOrg.Name, + }) +} + +// GetAllTrustedCertificates retrieves all trusted certificates with optional query filter with the given tenant context. If tenant context is nil, +// it assumes that the certificates to get are stored in System org. +func (vcdClient *VCDClient) GetAllTrustedCertificates(queryParameters url.Values, ctx *TenantContext) ([]*TrustedCertificate, error) { c := crudConfig{ - entityLabel: labelTrustedCertificate, - endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTrustedCertificates, - queryParameters: queryParameters, + entityLabel: labelTrustedCertificate, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTrustedCertificates, + queryParameters: queryParameters, + additionalHeader: getTenantContextHeader(ctx), } outerType := TrustedCertificate{vcdClient: vcdClient} return getAllOuterEntities(&vcdClient.Client, outerType, c) } +// GetAllTrustedCertificates retrieves all trusted certificates with optional query filter from the receiver Organization +func (org *TmOrg) GetAllTrustedCertificates(queryParameters url.Values) ([]*TrustedCertificate, error) { + return org.vcdClient.GetAllTrustedCertificates(queryParameters, &TenantContext{ + OrgId: org.TmOrg.ID, + OrgName: org.TmOrg.Name, + }) +} + // GetTrustedCertificateByAlias retrieves trusted certificate by alias -func (vcdClient *VCDClient) GetTrustedCertificateByAlias(alias string) (*TrustedCertificate, error) { +func (vcdClient *VCDClient) GetTrustedCertificateByAlias(alias string, ctx *TenantContext) (*TrustedCertificate, error) { if alias == "" { return nil, fmt.Errorf("%s lookup requires name", labelTrustedCertificate) } @@ -143,7 +169,7 @@ func (vcdClient *VCDClient) GetTrustedCertificateByAlias(alias string) (*Trusted queryParams := url.Values{} queryParams.Add("filter", "alias=="+alias) - filteredEntities, err := vcdClient.GetAllTrustedCertificates(queryParams) + filteredEntities, err := vcdClient.GetAllTrustedCertificates(queryParams, ctx) if err != nil { return nil, err } @@ -153,21 +179,38 @@ func (vcdClient *VCDClient) GetTrustedCertificateByAlias(alias string) (*Trusted return nil, err } - return vcdClient.GetTrustedCertificateById(singleEntity.TrustedCertificate.ID) + return vcdClient.GetTrustedCertificateById(singleEntity.TrustedCertificate.ID, ctx) +} + +// GetTrustedCertificateByAlias retrieves trusted certificate by alias from the receiver Organization +func (org *TmOrg) GetTrustedCertificateByAlias(alias string) (*TrustedCertificate, error) { + return org.vcdClient.GetTrustedCertificateByAlias(alias, &TenantContext{ + OrgId: org.TmOrg.ID, + OrgName: org.TmOrg.Name, + }) } // GetTrustedCertificateById retrieves trusted certificate by ID -func (vcdClient *VCDClient) GetTrustedCertificateById(id string) (*TrustedCertificate, error) { +func (vcdClient *VCDClient) GetTrustedCertificateById(id string, ctx *TenantContext) (*TrustedCertificate, error) { c := crudConfig{ - entityLabel: labelTrustedCertificate, - endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTrustedCertificates, - endpointParams: []string{id}, + entityLabel: labelTrustedCertificate, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTrustedCertificates, + endpointParams: []string{id}, + additionalHeader: getTenantContextHeader(ctx), } outerType := TrustedCertificate{vcdClient: vcdClient} return getOuterEntity(&vcdClient.Client, outerType, c) } +// GetTrustedCertificateById retrieves trusted certificate by ID from the receiver Organization +func (org *TmOrg) GetTrustedCertificateById(id string) (*TrustedCertificate, error) { + return org.vcdClient.GetTrustedCertificateById(id, &TenantContext{ + OrgId: org.TmOrg.ID, + OrgName: org.TmOrg.Name, + }) +} + // Update trusted certificate entry func (t *TrustedCertificate) Update(TrustedCertificateConfig *types.TrustedCertificate) (*TrustedCertificate, error) { c := crudConfig{ diff --git a/govcd/trusted_certificates_test.go b/govcd/trusted_certificates_test.go index 96e0a9ae4..043657bfe 100644 --- a/govcd/trusted_certificates_test.go +++ b/govcd/trusted_certificates_test.go @@ -13,7 +13,8 @@ import ( . "gopkg.in/check.v1" ) -func (vcd *TestVCD) Test_TrustedCertificates(check *C) { +// Test_TrustedCertificatesSystem tests CRUD of certificates in System +func (vcd *TestVCD) Test_TrustedCertificatesSystem(check *C) { skipNonTm(vcd, check) sysadminOnly(vcd, check) @@ -22,7 +23,7 @@ func (vcd *TestVCD) Test_TrustedCertificates(check *C) { Certificate: certificate, // using embedded certificate } - v, err := vcd.client.CreateTrustedCertificate(cfg) + v, err := vcd.client.CreateTrustedCertificate(cfg, nil) check.Assert(err, IsNil) check.Assert(v, NotNil) @@ -30,17 +31,17 @@ func (vcd *TestVCD) Test_TrustedCertificates(check *C) { PrependToCleanupListOpenApi(v.TrustedCertificate.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointTrustedCertificates+v.TrustedCertificate.ID) // Get By Name - byName, err := vcd.client.GetTrustedCertificateByAlias(cfg.Alias) + byName, err := vcd.client.GetTrustedCertificateByAlias(cfg.Alias, nil) check.Assert(err, IsNil) check.Assert(byName, NotNil) // Get By ID - byId, err := vcd.client.GetTrustedCertificateById(v.TrustedCertificate.ID) + byId, err := vcd.client.GetTrustedCertificateById(v.TrustedCertificate.ID, nil) check.Assert(err, IsNil) check.Assert(byId, NotNil) // Get All - allTmOrgs, err := vcd.client.GetAllTrustedCertificates(nil) + allTmOrgs, err := vcd.client.GetAllTrustedCertificates(nil, nil) check.Assert(err, IsNil) check.Assert(allTmOrgs, NotNil) check.Assert(len(allTmOrgs) > 0, Equals, true) @@ -55,7 +56,72 @@ func (vcd *TestVCD) Test_TrustedCertificates(check *C) { err = v.Delete() check.Assert(err, IsNil) - notFoundByName, err := vcd.client.GetTrustedCertificateByAlias(updated.TrustedCertificate.Alias) + notFoundByName, err := vcd.client.GetTrustedCertificateByAlias(updated.TrustedCertificate.Alias, nil) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundByName, IsNil) +} + +// Test_TrustedCertificatesTenant tests CRUD of certificates in an Organization +func (vcd *TestVCD) Test_TrustedCertificatesTenant(check *C) { + skipNonTm(vcd, check) + sysadminOnly(vcd, check) + + // We are testing trusted certificates for a regular Organization + orgSettings := &types.TmOrg{ + Name: check.TestName(), + DisplayName: check.TestName(), + IsEnabled: true, + } + org, err := vcd.client.CreateTmOrg(orgSettings) + 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 Organization to cleanup list + PrependToCleanupListOpenApi(org.TmOrg.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointOrgs+org.TmOrg.ID) + + cfg := &types.TrustedCertificate{ + Alias: check.TestName(), + Certificate: certificate, // using embedded certificate + } + + v, err := org.CreateTrustedCertificate(cfg) + check.Assert(err, IsNil) + check.Assert(v, NotNil) + + // Get By Name + byName, err := org.GetTrustedCertificateByAlias(cfg.Alias) + check.Assert(err, IsNil) + check.Assert(byName, NotNil) + + // Get By ID + byId, err := org.GetTrustedCertificateById(v.TrustedCertificate.ID) + check.Assert(err, IsNil) + check.Assert(byId, NotNil) + + // Get All + allTmOrgs, err := org.GetAllTrustedCertificates(nil) + check.Assert(err, IsNil) + check.Assert(allTmOrgs, NotNil) + check.Assert(len(allTmOrgs) > 0, Equals, true) + + // Update + v.TrustedCertificate.Alias = check.TestName() + "-rename" + updated, err := v.Update(v.TrustedCertificate) + check.Assert(err, IsNil) + check.Assert(updated, NotNil) + + // Delete + err = v.Delete() + check.Assert(err, IsNil) + + notFoundByName, err := org.GetTrustedCertificateByAlias(updated.TrustedCertificate.Alias) check.Assert(ContainsNotFound(err), Equals, true) check.Assert(notFoundByName, IsNil) } diff --git a/govcd/vsphere_vcenter_tm_test.go b/govcd/vsphere_vcenter_tm_test.go index e5349ee6a..a4941b62c 100644 --- a/govcd/vsphere_vcenter_tm_test.go +++ b/govcd/vsphere_vcenter_tm_test.go @@ -28,7 +28,7 @@ func (vcd *TestVCD) Test_VCenter(check *C) { // Certificate must be trusted before adding vCenter url, err := url.Parse(cfg.Url) check.Assert(err, IsNil) - trustedCert, err := vcd.client.AutoTrustCertificate(url) + trustedCert, err := vcd.client.AutoTrustHttpsCertificate(url, nil) check.Assert(err, IsNil) if trustedCert != nil { AddToCleanupListOpenApi(trustedCert.TrustedCertificate.ID, check.TestName()+"trusted-cert", types.OpenApiPathVersion1_0_0+types.OpenApiEndpointTrustedCertificates+trustedCert.TrustedCertificate.ID) diff --git a/types/v56/constants.go b/types/v56/constants.go index 8c266a2d6..335e28542 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -352,11 +352,17 @@ func (qf VmQueryFilter) String() string { }[qf] } +// Test connection results +const ( + UntrustedCertificate = "ERROR_UNTRUSTED_CERTIFICATE" +) + // LDAP modes for Organization const ( - LdapModeNone = "NONE" - LdapModeSystem = "SYSTEM" - LdapModeCustom = "CUSTOM" + LdapModeNone = "NONE" + LdapModeSystem = "SYSTEM" + LdapModeCustom = "CUSTOM" + LdapDefaultPort = 389 ) // Access control modes diff --git a/types/v56/types.go b/types/v56/types.go index e0a22f996..0f41ba291 100644 --- a/types/v56/types.go +++ b/types/v56/types.go @@ -1146,8 +1146,8 @@ type CustomOrgLdapSettings struct { UserAttributes *OrgLdapUserAttributes `xml:"UserAttributes" json:"userAttributes,omitempty"` // Defines how LDAP attributes are used when importing a user. GroupAttributes *OrgLdapGroupAttributes `xml:"GroupAttributes" json:"groupAttributes,omitempty"` // Defines how LDAP attributes are used when importing a group. UseExternalKerberos bool `xml:"UseExternalKerberos"` - - Realm string `xml:"Realm,omitempty"` + CustomUiButtonLabel *string `xml:"CustomUiButtonLabel,omitempty" json:"customUiButtonLabel,omitempty"` + Realm string `xml:"Realm,omitempty"` } // OrgLdapGroupAttributes represents the ldap group attribute settings for a VMware Cloud Director organization.