From e752b4595e44ea7500d7de19852411bbe15edb1f Mon Sep 17 00:00:00 2001 From: Adam Barreiro Date: Mon, 20 May 2024 09:23:44 +0200 Subject: [PATCH] Add OIDC support (#671) Signed-off-by: abarreiro --- .changes/v2.25.0/671-features.md | 2 + .github/workflows/check-code.yml | 2 +- .github/workflows/check-security.yml | 2 +- go.mod | 2 +- govcd/api_vcd_test.go | 6 +- govcd/org_oidc.go | 286 ++++++++++++ govcd/org_oidc_test.go | 642 +++++++++++++++++++++++++++ govcd/sample_govcd_test_config.yaml | 7 + scripts/staticcheck-config.sh | 2 +- types/v56/constants.go | 6 +- types/v56/oidc.go | 99 +++++ types/v56/saml.go | 4 +- 12 files changed, 1052 insertions(+), 8 deletions(-) create mode 100644 .changes/v2.25.0/671-features.md create mode 100644 govcd/org_oidc.go create mode 100644 govcd/org_oidc_test.go create mode 100644 types/v56/oidc.go diff --git a/.changes/v2.25.0/671-features.md b/.changes/v2.25.0/671-features.md new file mode 100644 index 000000000..adea88743 --- /dev/null +++ b/.changes/v2.25.0/671-features.md @@ -0,0 +1,2 @@ +* Added `AdminOrg` methods `GetOpenIdConnectSettings`, `SetOpenIdConnectSettings` and `DeleteOpenIdConnectSettings` + to manage OpenID Connect settings [GH-671] diff --git a/.github/workflows/check-code.yml b/.github/workflows/check-code.yml index b82510178..f0a8b7158 100644 --- a/.github/workflows/check-code.yml +++ b/.github/workflows/check-code.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v3 with: - go-version: '1.21' + go-version: '1.22' - name: Check out code into the Go module directory uses: actions/checkout@v3 diff --git a/.github/workflows/check-security.yml b/.github/workflows/check-security.yml index ec9b037ef..265e2f7af 100644 --- a/.github/workflows/check-security.yml +++ b/.github/workflows/check-security.yml @@ -9,7 +9,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v3 with: - go-version: '1.21' + go-version: '1.22' - name: Checkout Source uses: actions/checkout@v2 - name: gosec diff --git a/go.mod b/go.mod index 43d32c621..10b6b3d97 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/vmware/go-vcloud-director/v2 -go 1.21 +go 1.22 require ( github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 diff --git a/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index 040f52734..3f61b6c3c 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -184,7 +184,11 @@ type TestConfig struct { ExternalNetworkPortGroupType string `yaml:"externalNetworkPortGroupType,omitempty"` VimServer string `yaml:"vimServer,omitempty"` LdapServer string `yaml:"ldapServer,omitempty"` - Nsxt struct { + OidcServer struct { + Url string `yaml:"url,omitempty"` + WellKnownEndpoint string `yaml:"wellKnownEndpoint,omitempty"` + } `yaml:"oidcServer,omitempty"` + Nsxt struct { Manager string `yaml:"manager"` Tier0router string `yaml:"tier0router"` Tier0routerVrf string `yaml:"tier0routerVrf"` diff --git a/govcd/org_oidc.go b/govcd/org_oidc.go new file mode 100644 index 000000000..e581c81a6 --- /dev/null +++ b/govcd/org_oidc.go @@ -0,0 +1,286 @@ +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "bytes" + "cmp" + "encoding/xml" + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "io" + "net/http" + "net/url" + "strconv" + "strings" +) + +// GetOpenIdConnectSettings retrieves the current OpenID Connect settings for a given Organization +func (adminOrg *AdminOrg) GetOpenIdConnectSettings() (*types.OrgOAuthSettings, error) { + return oidcExecuteRequest(adminOrg, http.MethodGet, nil) +} + +// SetOpenIdConnectSettings sets the OpenID Connect configuration for a given Organization. If the well-known configuration +// endpoint is provided, the configuration is automatically retrieved from that URL. +// If other fields have been set in the input structure, the corresponding values retrieved from the well-known endpoint are overridden. +// If there are no fields set, the configuration retrieved from the well-known configuration endpoint is applied as-is. +// ClientId and ClientSecret properties are always mandatory, with and without well-known endpoint. +// This method returns an error if the settings can't be saved in VCD for any reason or if the provided settings are wrong. +func (adminOrg *AdminOrg) SetOpenIdConnectSettings(settings types.OrgOAuthSettings) (*types.OrgOAuthSettings, error) { + if settings.ClientId == "" { + return nil, fmt.Errorf("the Client ID is mandatory to configure OpenID Connect") + } + if settings.ClientSecret == "" { + return nil, fmt.Errorf("the Client Secret is mandatory to configure OpenID Connect") + } + if settings.WellKnownEndpoint != "" { + err := oidcValidateConnection(adminOrg.client, settings.WellKnownEndpoint) + if err != nil { + return nil, err + } + wellKnownSettings, err := oidcConfigureWithEndpoint(adminOrg.client, adminOrg.AdminOrg.HREF, settings.WellKnownEndpoint) + if err != nil { + return nil, err + } + + // The following statements allow users to override the well-known automatic configuration values with their own, + // mimicking what users can do in UI. + // If an attribute was not set in the input settings, the well-known endpoint value will be chosen. + settings.AccessTokenEndpoint = cmp.Or(settings.AccessTokenEndpoint, wellKnownSettings.AccessTokenEndpoint) + settings.IssuerId = cmp.Or(settings.IssuerId, wellKnownSettings.IssuerId) + settings.JwksUri = cmp.Or(settings.JwksUri, wellKnownSettings.JwksUri) + settings.UserInfoEndpoint = cmp.Or(settings.UserInfoEndpoint, wellKnownSettings.UserInfoEndpoint) + settings.UserAuthorizationEndpoint = cmp.Or(settings.UserAuthorizationEndpoint, wellKnownSettings.UserAuthorizationEndpoint) + settings.ScimEndpoint = cmp.Or(settings.ScimEndpoint, wellKnownSettings.ScimEndpoint) + + if settings.Scope == nil || len(settings.Scope) == 0 { + settings.Scope = wellKnownSettings.Scope + } + + if settings.OIDCAttributeMapping == nil { + // The whole mapping is missing, we take the whole struct from well-known endpoint + settings.OIDCAttributeMapping = wellKnownSettings.OIDCAttributeMapping + } else { + // Some mappings are present, others are missing. We take the missing ones from well-known endpoint + settings.OIDCAttributeMapping.EmailAttributeName = cmp.Or(settings.OIDCAttributeMapping.EmailAttributeName, wellKnownSettings.OIDCAttributeMapping.EmailAttributeName) + settings.OIDCAttributeMapping.SubjectAttributeName = cmp.Or(settings.OIDCAttributeMapping.SubjectAttributeName, wellKnownSettings.OIDCAttributeMapping.SubjectAttributeName) + settings.OIDCAttributeMapping.LastNameAttributeName = cmp.Or(settings.OIDCAttributeMapping.LastNameAttributeName, wellKnownSettings.OIDCAttributeMapping.LastNameAttributeName) + settings.OIDCAttributeMapping.RolesAttributeName = cmp.Or(settings.OIDCAttributeMapping.RolesAttributeName, wellKnownSettings.OIDCAttributeMapping.RolesAttributeName) + settings.OIDCAttributeMapping.FullNameAttributeName = cmp.Or(settings.OIDCAttributeMapping.FullNameAttributeName, wellKnownSettings.OIDCAttributeMapping.FullNameAttributeName) + settings.OIDCAttributeMapping.GroupsAttributeName = cmp.Or(settings.OIDCAttributeMapping.GroupsAttributeName, wellKnownSettings.OIDCAttributeMapping.GroupsAttributeName) + settings.OIDCAttributeMapping.FirstNameAttributeName = cmp.Or(settings.OIDCAttributeMapping.FirstNameAttributeName, wellKnownSettings.OIDCAttributeMapping.FirstNameAttributeName) + } + + if settings.OAuthKeyConfigurations == nil { + settings.OAuthKeyConfigurations = wellKnownSettings.OAuthKeyConfigurations + } + } + // Perform early validations. These are required in UI before sending the payload. + if settings.UserAuthorizationEndpoint == "" { + return nil, fmt.Errorf("the User Authorization Endpoint is mandatory to configure OpenID Connect") + } + if settings.AccessTokenEndpoint == "" { + return nil, fmt.Errorf("the Access Token Endpoint is mandatory to configure OpenID Connect") + } + if settings.UserInfoEndpoint == "" { + return nil, fmt.Errorf("the User Info Endpoint is mandatory to configure OpenID Connect") + } + if settings.MaxClockSkew < 0 { + return nil, fmt.Errorf("the Max Clock Skew must be positive to correctly configure OpenID Connect") + } + if settings.OIDCAttributeMapping == nil || settings.OIDCAttributeMapping.SubjectAttributeName == "" || + settings.OIDCAttributeMapping.EmailAttributeName == "" || settings.OIDCAttributeMapping.FullNameAttributeName == "" || + settings.OIDCAttributeMapping.FirstNameAttributeName == "" || settings.OIDCAttributeMapping.LastNameAttributeName == "" { + return nil, fmt.Errorf("the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect") + } + if settings.OAuthKeyConfigurations == nil || len(settings.OAuthKeyConfigurations.OAuthKeyConfiguration) == 0 { + return nil, fmt.Errorf("the OIDC Key Configuration is mandatory to configure OpenID Connect") + } + + // Perform connectivity validations + err := oidcValidateConnection(adminOrg.client, settings.UserAuthorizationEndpoint) + if err != nil { + return nil, err + } + err = oidcValidateConnection(adminOrg.client, settings.AccessTokenEndpoint) + if err != nil { + return nil, err + } + err = oidcValidateConnection(adminOrg.client, settings.UserInfoEndpoint) + if err != nil { + return nil, err + } + + // The namespace must be set for all structures, otherwise the API call fails + settings.Xmlns = types.XMLNamespaceVCloud + settings.OAuthKeyConfigurations.Xmlns = types.XMLNamespaceVCloud + for i := range settings.OAuthKeyConfigurations.OAuthKeyConfiguration { + settings.OAuthKeyConfigurations.OAuthKeyConfiguration[i].Xmlns = types.XMLNamespaceVCloud + } + settings.OIDCAttributeMapping.Xmlns = types.XMLNamespaceVCloud + + result, err := oidcExecuteRequest(adminOrg, http.MethodPut, &settings) + if err != nil { + return nil, err + } + + return result, nil +} + +// DeleteOpenIdConnectSettings deletes the current OpenID Connect settings from a given Organization +func (adminOrg *AdminOrg) DeleteOpenIdConnectSettings() error { + _, err := oidcExecuteRequest(adminOrg, http.MethodDelete, nil) + if err != nil { + return err + } + return nil +} + +// oidcExecuteRequest executes a request to the OIDC endpoint with the given payload and HTTP method +func oidcExecuteRequest(adminOrg *AdminOrg, method string, payload *types.OrgOAuthSettings) (*types.OrgOAuthSettings, error) { + if adminOrg.AdminOrg.HREF == "" { + return nil, fmt.Errorf("the HREF of the Organization is required to use OpenID Connect") + } + endpoint, err := url.Parse(adminOrg.AdminOrg.HREF + "/settings/oauth") + if err != nil { + return nil, fmt.Errorf("error parsing Organization '%s' OpenID Connect URL: %s", adminOrg.AdminOrg.Name, err) + } + if endpoint == nil { + return nil, fmt.Errorf("error parsing Organization '%s' OpenID Connect URL: it is nil", adminOrg.AdminOrg.Name) + } + if method == http.MethodPut && payload == nil { + return nil, fmt.Errorf("the OIDC settings cannot be nil when performing a PUT call") + } + + // Set Organization "tenant context" headers + headers := make(http.Header) + headers.Set("Content-Type", types.MimeOAuthSettingsXml) + for k, v := range getTenantContextHeader(&TenantContext{ + OrgId: adminOrg.AdminOrg.ID, + OrgName: adminOrg.AdminOrg.Name, + }) { + headers.Add(k, v) + } + + // If the call is a PUT, we prepare the body with the input settings + var body io.Reader + if method == http.MethodPut { + text := bytes.Buffer{} + encoder := xml.NewEncoder(&text) + err = encoder.Encode(*payload) + if err != nil { + return nil, err + } + body = strings.NewReader(text.String()) + } + + // Perform the HTTP call with the custom headers and obtained API version + req := adminOrg.client.newRequest(nil, nil, method, *endpoint, body, getHighestOidcApiVersion(adminOrg.client), headers) + resp, err := checkResp(adminOrg.client.Http.Do(req)) + + // Check the errors and get the response + switch method { + case http.MethodDelete: + if err != nil { + return nil, fmt.Errorf("error deleting Organization OpenID Connect settings: %s", err) + } + if resp != nil && resp.StatusCode != http.StatusNoContent { + return nil, fmt.Errorf("error deleting Organization OpenID Connect settings, expected status code %d - received %d", http.StatusNoContent, resp.StatusCode) + } + return nil, nil + case http.MethodGet: + if err != nil { + return nil, fmt.Errorf("error getting Organization OpenID Connect settings: %s", err) + } + var result types.OrgOAuthSettings + err = decodeBody(types.BodyTypeXML, resp, &result) + if err != nil { + return nil, fmt.Errorf("error decoding Organization OpenID Connect settings: %s", err) + } + return &result, nil + case http.MethodPut: + if err != nil { + return nil, fmt.Errorf("error setting Organization OpenID Connect settings: %s", err) + } + // Note: This branch of the switch should be exactly the same as the GET operation, however there is a bug found in VCD 10.5.1.1: + // the PUT call returns a wrong redirect URL. + // For that reason, we ignore the response body and call GetOpenIdConnectSettings() to return the correct response body to the caller. + if resp != nil && resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error saving Organization OpenID Connect settings, expected status code %d - received %d", http.StatusOK, resp.StatusCode) + } + return adminOrg.GetOpenIdConnectSettings() + default: + return nil, fmt.Errorf("not supported HTTP method %s", method) + } +} + +// oidcValidateConnection executes a test probe against the given endpoint to validate that the client +// can establish a connection. +func oidcValidateConnection(client *Client, endpoint string) error { + uri, err := url.Parse(endpoint) + 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) + if err != nil { + return err + } + + result, err := client.TestConnection(types.TestConnection{ + Host: uri.Hostname(), + Port: port, + Secure: &isSecure, + }) + if err != nil { + return err + } + + if result.TargetProbe == nil || !result.TargetProbe.CanConnect || (isSecure && !result.TargetProbe.SSLHandshake) { + return fmt.Errorf("could not establish a connection to %s://%s", uri.Scheme, uri.Host) + } + return nil +} + +// oidcConfigureWithEndpoint uses the given endpoint to retrieve an OpenID Connect configuration +func oidcConfigureWithEndpoint(client *Client, orgHref, endpoint string) (types.OrgOAuthSettings, error) { + payload := types.OpenIdProviderInfo{ + Xmlns: types.XMLNamespaceVCloud, + OpenIdProviderConfigurationEndpoint: endpoint, + } + var result types.OpenIdProviderConfiguration + + _, err := client.ExecuteRequestWithApiVersion(orgHref+"/settings/oauth/openIdProviderConfig", http.MethodPost, + types.MimeOpenIdProviderInfoXml, "error getting OpenID Connect settings from endpoint: %s", payload, &result, + getHighestOidcApiVersion(client)) + if err != nil { + return types.OrgOAuthSettings{}, err + } + + return result.OrgOAuthSettings, nil +} + +// getHighestOidcApiVersion tries to get the highest possible version for the OpenID Connect endpoint +func getHighestOidcApiVersion(client *Client) string { + // v38.1 adds CustomUiButtonLabel + targetVersion := client.GetSpecificApiVersionOnCondition(">= 38.1", "38.1") + if targetVersion != "38.1" { + // v38.0 adds SendClientCredentialsAsAuthorizationHeader, UsePKCE, + targetVersion = client.GetSpecificApiVersionOnCondition(">= 38.0", "38.0") + if targetVersion != "38.0" { + // v37.1 adds EnableIdTokenClaims + targetVersion = client.GetSpecificApiVersionOnCondition(">= 37.1", "37.1") + } + } // Otherwise we get the default API version + return targetVersion +} diff --git a/govcd/org_oidc_test.go b/govcd/org_oidc_test.go new file mode 100644 index 000000000..1faf3a6ee --- /dev/null +++ b/govcd/org_oidc_test.go @@ -0,0 +1,642 @@ +//go:build org || functional || ALL + +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + _ "embed" + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "net/url" + "strings" + "time" +) + +// Test_OrgOidcSettingsSystemAdminCreateWithWellKnownEndpoint configures OIDC +// with a wellknown endpoint. +func (vcd *TestVCD) Test_OrgOidcSettingsSystemAdminCreateWithWellKnownEndpoint(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + check.Assert(settings.Enabled, Equals, false) + check.Assert(settings.AccessTokenEndpoint, Equals, "") + check.Assert(settings.UserInfoEndpoint, Equals, "") + check.Assert(settings.UserAuthorizationEndpoint, Equals, "") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + + settings, err = setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + MaxClockSkew: 60, + WellKnownEndpoint: oidcServerUrl.String(), + }) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + + check.Assert(settings, NotNil) + check.Assert(settings.Xmlns, Equals, "http://www.vmware.com/vcloud/v1.5") + check.Assert(settings.Href, Equals, adminOrg.AdminOrg.HREF+"/settings/oauth") + check.Assert(settings.Type, Equals, "application/vnd.vmware.admin.organizationOAuthSettings+xml") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + check.Assert(settings.IssuerId, Not(Equals), "") + check.Assert(settings.Enabled, Equals, true) + check.Assert(settings.ClientId, Equals, "clientId") + check.Assert(settings.ClientSecret, Equals, "clientSecret") + check.Assert(settings.UserAuthorizationEndpoint, Not(Equals), "") + check.Assert(settings.AccessTokenEndpoint, Not(Equals), "") + check.Assert(settings.UserInfoEndpoint, Not(Equals), "") + check.Assert(settings.ScimEndpoint, Equals, "") + check.Assert(len(settings.Scope), Not(Equals), 0) + check.Assert(settings.MaxClockSkew, Equals, 60) + check.Assert(settings.WellKnownEndpoint, Not(Equals), "") + check.Assert(settings.OIDCAttributeMapping, NotNil) + check.Assert(settings.OAuthKeyConfigurations, NotNil) + check.Assert(len(settings.OAuthKeyConfigurations.OAuthKeyConfiguration), Not(Equals), 0) +} + +// Test_OrgOidcSettingsSystemAdminCreateWithWellKnownEndpointAndOverridingOptions configures OIDC +// with a wellknown endpoint, but overrides the obtained values with custom ones. +func (vcd *TestVCD) Test_OrgOidcSettingsSystemAdminCreateWithWellKnownEndpointAndOverridingOptions(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + + accessTokenEndpoint := fmt.Sprintf("%s://%s/foo", oidcServerUrl.Scheme, oidcServerUrl.Host) + userAuthorizationEndpoint := fmt.Sprintf("%s://%s/foo2", oidcServerUrl.Scheme, oidcServerUrl.Host) + + settings, err = setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + MaxClockSkew: 60, + AccessTokenEndpoint: accessTokenEndpoint, + UserAuthorizationEndpoint: userAuthorizationEndpoint, + WellKnownEndpoint: oidcServerUrl.String(), + }) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + + check.Assert(settings, NotNil) + check.Assert(settings.AccessTokenEndpoint, Equals, accessTokenEndpoint) + check.Assert(settings.UserAuthorizationEndpoint, Equals, userAuthorizationEndpoint) + check.Assert(settings.Xmlns, Equals, "http://www.vmware.com/vcloud/v1.5") + check.Assert(settings.Href, Equals, adminOrg.AdminOrg.HREF+"/settings/oauth") + check.Assert(settings.Type, Equals, "application/vnd.vmware.admin.organizationOAuthSettings+xml") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + check.Assert(settings.IssuerId, Not(Equals), "") + check.Assert(settings.Enabled, Equals, true) + check.Assert(settings.ClientId, Equals, "clientId") + check.Assert(settings.ClientSecret, Equals, "clientSecret") + check.Assert(settings.UserInfoEndpoint, Not(Equals), "") + check.Assert(settings.ScimEndpoint, Equals, "") + check.Assert(len(settings.Scope), Not(Equals), 0) + check.Assert(settings.MaxClockSkew, Equals, 60) + check.Assert(settings.WellKnownEndpoint, Not(Equals), "") + check.Assert(settings.OIDCAttributeMapping, NotNil) + check.Assert(settings.OAuthKeyConfigurations, NotNil) + check.Assert(len(settings.OAuthKeyConfigurations.OAuthKeyConfiguration), Not(Equals), 0) +} + +// Test_OrgOidcSettingsSystemAdminCreateWithCustomValues configures OIDC +// without the wellknown endpoint, by hand. +func (vcd *TestVCD) Test_OrgOidcSettingsSystemAdminCreateWithCustomValues(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + accessTokenEndpoint := fmt.Sprintf("%s://%s/accessToken", oidcServerUrl.Scheme, oidcServerUrl.Host) + userAuthorizationEndpoint := fmt.Sprintf("%s://%s/userAuth", oidcServerUrl.Scheme, oidcServerUrl.Host) + issuerId := fmt.Sprintf("%s://%s/issuerId", oidcServerUrl.Scheme, oidcServerUrl.Host) + userInfoEndpoint := fmt.Sprintf("%s://%s/userInfo", oidcServerUrl.Scheme, oidcServerUrl.Host) + + expirationDate := "2123-12-31T01:59:59.000Z" + dummyKey := "-----BEGIN PUBLIC KEY-----\n" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9gXitSASYbVS56gBkQ3UOCS7F\n" + + "8SnFABs44sxXykt8DW4y1mxdyCcM0X/lVPf+DNfXbIISmPk/mqoRS9uZSuQIUtC2\n" + + "4iaGkWyUALvrq8FJcR8Krf5EtDt1W9AkLEREDJ7VkpJx/VoCd9ZNe8NFstAvbQ6+\n" + + "bM0Jg9lJJdr+VPNvywIDAQAB\n" + + "-----END PUBLIC KEY-----" + + settings, err := setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + UserAuthorizationEndpoint: userAuthorizationEndpoint, + AccessTokenEndpoint: accessTokenEndpoint, + IssuerId: issuerId, + UserInfoEndpoint: userInfoEndpoint, + MaxClockSkew: 60, + Scope: []string{"foo", "bar"}, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "subject", + EmailAttributeName: "email", + FullNameAttributeName: "fullname", + FirstNameAttributeName: "first", + LastNameAttributeName: "last", + GroupsAttributeName: "groups", + RolesAttributeName: "roles", + }, + OAuthKeyConfigurations: &types.OAuthKeyConfigurationsList{ + OAuthKeyConfiguration: []types.OAuthKeyConfiguration{ + { + KeyId: "rsa1", + Algorithm: "RSA", + Key: dummyKey, + ExpirationDate: expirationDate, + }, + }, + }, + }) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + + check.Assert(settings, NotNil) + check.Assert(settings.Xmlns, Equals, "http://www.vmware.com/vcloud/v1.5") + check.Assert(settings.Href, Equals, adminOrg.AdminOrg.HREF+"/settings/oauth") + check.Assert(settings.Type, Equals, "application/vnd.vmware.admin.organizationOAuthSettings+xml") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + check.Assert(settings.Enabled, Equals, true) + check.Assert(settings.ClientId, Equals, "clientId") + check.Assert(settings.ClientSecret, Equals, "clientSecret") + check.Assert(settings.IssuerId, Equals, issuerId) + check.Assert(settings.UserAuthorizationEndpoint, Equals, userAuthorizationEndpoint) + check.Assert(settings.AccessTokenEndpoint, Equals, accessTokenEndpoint) + check.Assert(settings.UserInfoEndpoint, Equals, userInfoEndpoint) + check.Assert(settings.ScimEndpoint, Equals, "") + check.Assert(len(settings.Scope), Equals, 2) + check.Assert(settings.MaxClockSkew, Equals, 60) + check.Assert(settings.WellKnownEndpoint, Equals, "") + check.Assert(settings.OIDCAttributeMapping, NotNil) + check.Assert(settings.OIDCAttributeMapping.EmailAttributeName, Equals, "email") + check.Assert(settings.OIDCAttributeMapping.LastNameAttributeName, Equals, "last") + check.Assert(settings.OIDCAttributeMapping.FirstNameAttributeName, Equals, "first") + check.Assert(settings.OIDCAttributeMapping.SubjectAttributeName, Equals, "subject") + check.Assert(settings.OIDCAttributeMapping.GroupsAttributeName, Equals, "groups") + check.Assert(settings.OIDCAttributeMapping.FullNameAttributeName, Equals, "fullname") + check.Assert(settings.OIDCAttributeMapping.RolesAttributeName, Equals, "roles") + check.Assert(settings.OAuthKeyConfigurations, NotNil) + check.Assert(len(settings.OAuthKeyConfigurations.OAuthKeyConfiguration), Equals, 1) + check.Assert(settings.OAuthKeyConfigurations.OAuthKeyConfiguration[0].KeyId, Equals, "rsa1") + check.Assert(settings.OAuthKeyConfigurations.OAuthKeyConfiguration[0].Algorithm, Equals, "RSA") + check.Assert(settings.OAuthKeyConfigurations.OAuthKeyConfiguration[0].Key, Equals, dummyKey) + check.Assert(settings.OAuthKeyConfigurations.OAuthKeyConfiguration[0].ExpirationDate, Equals, expirationDate) +} + +// Test_OrgOidcSettingsSystemAdminUpdate configures OIDC settings with a wellknown endpoint, then updates some values. +func (vcd *TestVCD) Test_OrgOidcSettingsSystemAdminUpdate(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + check.Assert(settings.Enabled, Equals, false) + check.Assert(settings.AccessTokenEndpoint, Equals, "") + check.Assert(settings.UserInfoEndpoint, Equals, "") + check.Assert(settings.UserAuthorizationEndpoint, Equals, "") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + + settings, err = setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + MaxClockSkew: 60, + WellKnownEndpoint: oidcServerUrl.String(), + }) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + check.Assert(settings, NotNil) + + updatedSettings, err := setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId2", + ClientSecret: "clientSecret2", + Enabled: false, + MaxClockSkew: 120, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "subject2", + EmailAttributeName: "email2", + FullNameAttributeName: "fullname2", + FirstNameAttributeName: "first2", + LastNameAttributeName: "last2", + GroupsAttributeName: "groups2", + RolesAttributeName: "roles2", + }, + WellKnownEndpoint: oidcServerUrl.String(), + }) + check.Assert(err, IsNil) + check.Assert(updatedSettings, NotNil) + + check.Assert(updatedSettings.Enabled, Equals, false) + check.Assert(updatedSettings.ClientId, Equals, "clientId2") + check.Assert(updatedSettings.ClientSecret, Equals, "clientSecret2") + check.Assert(updatedSettings.MaxClockSkew, Equals, 120) + check.Assert(updatedSettings.OIDCAttributeMapping, NotNil) + check.Assert(updatedSettings.OIDCAttributeMapping.EmailAttributeName, Equals, "email2") + check.Assert(updatedSettings.OIDCAttributeMapping.LastNameAttributeName, Equals, "last2") + check.Assert(updatedSettings.OIDCAttributeMapping.FirstNameAttributeName, Equals, "first2") + check.Assert(updatedSettings.OIDCAttributeMapping.SubjectAttributeName, Equals, "subject2") + check.Assert(updatedSettings.OIDCAttributeMapping.GroupsAttributeName, Equals, "groups2") + check.Assert(updatedSettings.OIDCAttributeMapping.FullNameAttributeName, Equals, "fullname2") + check.Assert(updatedSettings.OIDCAttributeMapping.RolesAttributeName, Equals, "roles2") +} + +// Test_OrgOidcSettingsWithTenantUser configures OIDC settings with a tenant user instead of System administrator. +func (vcd *TestVCD) Test_OrgOidcSettingsWithTenantUser(check *C) { + if len(vcd.config.Tenants) == 0 { + check.Skip(check.TestName() + " requires at least one tenant in the configuration") + } + + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + orgName := vcd.config.Tenants[0].SysOrg + userName := vcd.config.Tenants[0].User + password := vcd.config.Tenants[0].Password + + vcdClient := NewVCDClient(vcd.client.Client.VCDHREF, true) + err := vcdClient.Authenticate(userName, password, orgName) + check.Assert(err, IsNil) + + adminOrg, err := vcd.client.GetAdminOrgByName(orgName) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + MaxClockSkew: 60, + WellKnownEndpoint: oidcServerUrl.String(), + }) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + check.Assert(settings, NotNil) + check.Assert(settings.Xmlns, Equals, "http://www.vmware.com/vcloud/v1.5") + check.Assert(settings.Href, Equals, adminOrg.AdminOrg.HREF+"/settings/oauth") + check.Assert(settings.Type, Equals, "application/vnd.vmware.admin.organizationOAuthSettings+xml") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, orgName)) + check.Assert(settings.IssuerId, Not(Equals), "") + check.Assert(settings.Enabled, Equals, true) + check.Assert(settings.ClientId, Equals, "clientId") + check.Assert(settings.ClientSecret, Equals, "clientSecret") + check.Assert(settings.UserAuthorizationEndpoint, Not(Equals), "") + check.Assert(settings.AccessTokenEndpoint, Not(Equals), "") + check.Assert(settings.UserInfoEndpoint, Not(Equals), "") + check.Assert(settings.ScimEndpoint, Equals, "") + check.Assert(len(settings.Scope), Not(Equals), 0) + check.Assert(settings.MaxClockSkew, Equals, 60) + check.Assert(settings.WellKnownEndpoint, Not(Equals), "") + check.Assert(settings.OIDCAttributeMapping, NotNil) + check.Assert(settings.OAuthKeyConfigurations, NotNil) + check.Assert(len(settings.OAuthKeyConfigurations.OAuthKeyConfiguration), Not(Equals), 0) + + settings2, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings2, DeepEquals, settings) +} + +// Test_OrgOidcSettingsDifferentVersions tests the parameters that are only available in certain +// VCD versions, like the UI button label. This test only makes sense when it is run in several +// VCD versions. +func (vcd *TestVCD) Test_OrgOidcSettingsDifferentVersions(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + check.Assert(settings.Enabled, Equals, false) + check.Assert(settings.AccessTokenEndpoint, Equals, "") + check.Assert(settings.UserInfoEndpoint, Equals, "") + check.Assert(settings.UserAuthorizationEndpoint, Equals, "") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + + s := types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + MaxClockSkew: 60, + WellKnownEndpoint: oidcServerUrl.String(), + } + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.1") { + s.EnableIdTokenClaims = addrOf(true) + } + if vcd.client.Client.APIVCDMaxVersionIs(">= 38.0") { + s.SendClientCredentialsAsAuthorizationHeader = addrOf(true) + s.UsePKCE = addrOf(true) + } + if vcd.client.Client.APIVCDMaxVersionIs(">= 38.1") { + s.CustomUiButtonLabel = addrOf("this is a test") + } + + settings, err = setOIDCSettings(adminOrg, s) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + + check.Assert(settings, NotNil) + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.1") { + check.Assert(settings.EnableIdTokenClaims, NotNil) + check.Assert(*settings.EnableIdTokenClaims, Equals, true) + } else { + check.Assert(settings.EnableIdTokenClaims, IsNil) + } + if vcd.client.Client.APIVCDMaxVersionIs(">= 38.0") { + check.Assert(settings.SendClientCredentialsAsAuthorizationHeader, NotNil) + check.Assert(settings.UsePKCE, NotNil) + check.Assert(*settings.SendClientCredentialsAsAuthorizationHeader, Equals, true) + check.Assert(*settings.UsePKCE, Equals, true) + } else { + check.Assert(settings.SendClientCredentialsAsAuthorizationHeader, IsNil) + check.Assert(settings.UsePKCE, IsNil) + } + if vcd.client.Client.APIVCDMaxVersionIs(">= 38.1") { + check.Assert(settings.CustomUiButtonLabel, NotNil) + check.Assert(*settings.CustomUiButtonLabel, Equals, "this is a test") + } else { + check.Assert(settings.CustomUiButtonLabel, IsNil) + } +} + +// Test_OrgOidcSettingsValidationErrors tests the validation rules when setting OpenID Connect Settings with AdminOrg.SetOpenIdConnectSettings +func (vcd *TestVCD) Test_OrgOidcSettingsValidationErrors(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + tests := []struct { + wrongConfig types.OrgOAuthSettings + errorMsg string + }{ + { + wrongConfig: types.OrgOAuthSettings{}, + errorMsg: "the Client ID is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + }, + errorMsg: "the Client Secret is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + }, + errorMsg: "the User Authorization Endpoint is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + }, + errorMsg: "the Access Token Endpoint is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + }, + errorMsg: "the User Info Endpoint is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: -1, + }, + errorMsg: "the Max Clock Skew must be positive to correctly configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{}, + }, + errorMsg: "the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + }, + }, + errorMsg: "the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + EmailAttributeName: "b", + }, + }, + errorMsg: "the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + EmailAttributeName: "b", + FullNameAttributeName: "c", + }, + }, + errorMsg: "the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + EmailAttributeName: "b", + FullNameAttributeName: "c", + FirstNameAttributeName: "d", + }, + }, + errorMsg: "the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + EmailAttributeName: "b", + FullNameAttributeName: "c", + FirstNameAttributeName: "d", + LastNameAttributeName: "e", + }, + }, + errorMsg: "the OIDC Key Configuration is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + EmailAttributeName: "b", + FullNameAttributeName: "c", + FirstNameAttributeName: "d", + LastNameAttributeName: "e", + }, + OAuthKeyConfigurations: &types.OAuthKeyConfigurationsList{}, + }, + errorMsg: "the OIDC Key Configuration is mandatory to configure OpenID Connect", + }, + } + + for _, test := range tests { + _, err := adminOrg.SetOpenIdConnectSettings(test.wrongConfig) + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), test.errorMsg)) + } +} + +// setOIDCSettings sets the given OIDC settings for the given Organization. It does this operation +// with some tries to avoid test failures due to network glitches. +func setOIDCSettings(adminOrg *AdminOrg, settings types.OrgOAuthSettings) (*types.OrgOAuthSettings, error) { + tries := 0 + var newSettings *types.OrgOAuthSettings + var err error + for tries < 5 { + tries++ + newSettings, err = adminOrg.SetOpenIdConnectSettings(settings) + if err == nil { + break + } + if strings.Contains(err.Error(), "could not establish a connection") || strings.Contains(err.Error(), "connect timed out") { + time.Sleep(10 * time.Second) + } + } + if err != nil { + return nil, err + } + return newSettings, nil +} + +// deleteOIDCSettings deletes the current OIDC settings for the given Organization +func deleteOIDCSettings(check *C, adminOrg *AdminOrg) { + err := adminOrg.DeleteOpenIdConnectSettings() + check.Assert(err, IsNil) + + settings, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + check.Assert(settings.Enabled, Equals, false) + check.Assert(settings.AccessTokenEndpoint, Equals, "") + check.Assert(settings.UserInfoEndpoint, Equals, "") + check.Assert(settings.UserAuthorizationEndpoint, Equals, "") + check.Assert(settings.OrgRedirectUri, Not(Equals), "") +} + +func validateAndGetOidcServerUrl(check *C, vcd *TestVCD) *url.URL { + if vcd.config.VCD.OidcServer.Url == "" || vcd.config.VCD.OidcServer.WellKnownEndpoint == "" { + check.Skip("test requires OIDC configuration") + } + + oidcServer, err := url.Parse(vcd.config.VCD.OidcServer.Url) + if err != nil { + check.Skip(check.TestName() + " requires OIDC Server URL and its well-known endpoint") + } + return oidcServer.JoinPath(vcd.config.VCD.OidcServer.WellKnownEndpoint) +} diff --git a/govcd/sample_govcd_test_config.yaml b/govcd/sample_govcd_test_config.yaml index f54375db4..ea525542c 100644 --- a/govcd/sample_govcd_test_config.yaml +++ b/govcd/sample_govcd_test_config.yaml @@ -189,6 +189,13 @@ vcd: # IP of a pre-configured LDAP server # using Docker image https://github.com/rroemhild/docker-test-openldap ldap_server: 10.10.10.99 + # + # Details of pre-configured OIDC server + oidcServer: + # Server URL + url: "10.10.10.100/oidc-server" + # Well-known endpoint + wellKnownEndpoint: "/.well-known/openid-configuration" vsphere: # resource pools needed to create new provider VDCs resourcePoolForVcd1: resource-pool-for-vcd-01 diff --git a/scripts/staticcheck-config.sh b/scripts/staticcheck-config.sh index d96f1882e..1ddfab252 100644 --- a/scripts/staticcheck-config.sh +++ b/scripts/staticcheck-config.sh @@ -1,3 +1,3 @@ export STATICCHECK_URL=https://github.com/dominikh/go-tools/releases/download -export STATICCHECK_VERSION=2023.1.6 +export STATICCHECK_VERSION=2023.1.7 export STATICCHECK_FILE=staticcheck_linux_amd64.tar.gz diff --git a/types/v56/constants.go b/types/v56/constants.go index 868aae3ec..4d9bfaf14 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -151,9 +151,13 @@ const ( MimeProviderVdc = "application/vnd.vmware.admin.vmwprovidervdc+xml" // Mime to identify SAML metadata MimeSamlMetadata = "application/samlmetadata+xml" - // Mime to identify organization federation settings (SAML) XML and JSON + // Mime to identify organization federation settings (SAML) MimeFederationSettingsXml = "application/vnd.vmware.admin.organizationFederationSettings+xml" MimeFederationSettingsJson = "application/vnd.vmware.admin.organizationFederationSettings+json" + // Mime to identify organization OpenID Connect (OIDC) settings + MimeOAuthSettingsXml = "application/vnd.vmware.admin.organizationoauthsettings+xml" + // Mime to identify the OpenID Provider info + MimeOpenIdProviderInfoXml = "application/vnd.vmware.vcloud.admin.openIdProviderInfo+xml" // Mime to handle virtual hardware versions MimeVirtualHardwareVersion = "application/vnd.vmware.vcloud.virtualHardwareVersion+xml" ) diff --git a/types/v56/oidc.go b/types/v56/oidc.go new file mode 100644 index 000000000..833682492 --- /dev/null +++ b/types/v56/oidc.go @@ -0,0 +1,99 @@ +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package types + +// OrgOAuthSettings contains OAuth identity provider settings for an Organization. +type OrgOAuthSettings struct { + Xmlns string `xml:"xmlns,attr"` + Href string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object + OrgRedirectUri string `xml:"OrgRedirectUri,omitempty"` // OAuth redirect URI for this org. This value is read only + IssuerId string `xml:"IssuerId,omitempty"` // Issuer Id for the OAuth Identity Provider + OAuthKeyConfigurations *OAuthKeyConfigurationsList `xml:"OAuthKeyConfigurations,omitempty"` // A list of OAuth Key configurations + Enabled bool `xml:"Enabled"` // True if the OAuth Identity Provider for this organization is enabled. Unset or empty defaults to true + ClientId string `xml:"ClientId,omitempty"` // Client ID for VCD to use when talking to the Identity Provider + ClientSecret string `xml:"ClientSecret,omitempty"` // Client Secret for vCD to use when talking to the Identity Provider + UserAuthorizationEndpoint string `xml:"UserAuthorizationEndpoint,omitempty"` // Identity Provider's OpenID Connect user authorization endpoint + AccessTokenEndpoint string `xml:"AccessTokenEndpoint,omitempty"` // Identity Provider's OpenId Connect access token endpoint + UserInfoEndpoint string `xml:"UserInfoEndpoint,omitempty"` // Identity Provider's OpenID Connect user info endpoint + ScimEndpoint string `xml:"ScimEndpoint,omitempty"` // Identity Provider's SCIM user information endpoint + Scope []string `xml:"Scope,omitempty"` // Scope that VCD needs access to for authenticating the user + OIDCAttributeMapping *OIDCAttributeMapping `xml:"OIDCAttributeMapping,omitempty"` // Custom claim keys for the /userinfo endpoint + MaxClockSkew int `xml:"MaxClockSkew,omitempty"` // Allowed difference between token expiration and vCD system time in seconds + JwksUri string `xml:"JwksUri,omitempty"` // Endpoint to fetch the keys from + AutoRefreshKey bool `xml:"AutoRefreshKey"` // Flag indicating whether VCD should auto-refresh the keys + + // Strategy to use when updated list of keys does not include keys known to VCD. + // The values must be one of the below: ADD: Will add new keys to set of keys that VCD will use. + // REPLACE: The retrieved list of keys will replace the existing list of keys and will become the definitive list of keys used by VCD going forward. + // EXPIRE_AFTER: Keys known to VCD that are no longer returned by the OIDC server will be marked as expired, 'KeyExpireDurationInHours' specified hours after the key refresh is performed. After that later time, VCD will no longer use the keys. + KeyRefreshStrategy string `xml:"KeyRefreshStrategy,omitempty"` + + KeyRefreshFrequencyInHours int `xml:"KeyRefreshFrequencyInHours,omitempty"` // Time interval, in hours, between subsequent key refresh attempts + KeyExpireDurationInHours int `xml:"KeyExpireDurationInHours,omitempty"` // Duration in which the keys are set to expire + WellKnownEndpoint string `xml:"WellKnownEndpoint,omitempty"` // Endpoint from the Identity Provider that serves OpenID Connect configuration value + LastKeyRefreshAttempt string `xml:"LastKeyRefreshAttempt,omitempty"` // Last time refresh of the keys was attempted + LastKeySuccessfulRefresh string `xml:"LastKeySuccessfulRefresh,omitempty"` // Last time refresh of the keys was successful + + // Added in v37.1 + EnableIdTokenClaims *bool `xml:"EnableIdTokenClaims"` // Flag indicating whether Id-Token Claims should be used when establishing user details + // Added in v38.0 + UsePKCE *bool `xml:"UsePKCE"` // Flag indicating whether client must use PKCE (Proof Key for Code Exchange), which provides additional verification against potential authorization code interception. Default is false + SendClientCredentialsAsAuthorizationHeader *bool `xml:"SendClientCredentialsAsAuthorizationHeader"` // Flag indicating whether client credentials should be sent as an Authorization header when fetching the token. Default is false, which means client credentials will be sent within the body of the request + // Added in v38.1 + CustomUiButtonLabel *string `xml:"CustomUiButtonLabel,omitempty"` // Custom label to use when displaying this OpenID Connect configuration on the VCD login pane. If null, a default label will be used +} + +// OAuthKeyConfigurationsList contains a list of OAuth Key configurations +type OAuthKeyConfigurationsList struct { + Xmlns string `xml:"xmlns,attr"` + + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object + OAuthKeyConfiguration []OAuthKeyConfiguration `xml:"OAuthKeyConfiguration,omitempty"` // OAuth key configuration +} + +// OAuthKeyConfiguration describes the OAuth key configuration +type OAuthKeyConfiguration struct { + Xmlns string `xml:"xmlns,attr"` + + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object + KeyId string `xml:"KeyId,omitempty"` // Identifier for the key used by the Identity Provider. This key id is expected to be present in the header portion of OAuth tokens issued by the Identity provider + Algorithm string `xml:"Algorithm,omitempty"` // Identifies the cryptographic algorithm family of the key. Supported values are RSA and EC for asymmetric keys + Key string `xml:"Key,omitempty"` // PEM formatted key body. Key is used during validation of OAuth tokens for this Org + ExpirationDate string `xml:"ExpirationDate,omitempty"` // Expiration date for this key. If specified, tokens signed with this key should be considered invalid after this time +} + +// OIDCAttributeMapping contains custom claim keys for the /userinfo endpoint +type OIDCAttributeMapping struct { + Xmlns string `xml:"xmlns,attr"` + + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object + SubjectAttributeName string `xml:"SubjectAttributeName,omitempty"` // The name of the OIDC attribute used to get the username from the IDP's userInfo + EmailAttributeName string `xml:"EmailAttributeName,omitempty"` // The name of the OIDC attribute used to get the email from the IDP's userInfo + FullNameAttributeName string `xml:"FullNameAttributeName,omitempty"` // The name of the OIDC attribute used to get the full name from the IDP's userInfo. The full name attribute overrides the use of the firstName and lastName attributes + FirstNameAttributeName string `xml:"FirstNameAttributeName,omitempty"` // The name of the OIDC attribute used to get the first name from the IDP's userInfo. This is only used if the Full Name key is not specified + LastNameAttributeName string `xml:"LastNameAttributeName,omitempty"` // The name of the OIDC attribute used to get the last name from the IDP's userInfo. This is only used if the Full Name key is not specified + GroupsAttributeName string `xml:"GroupsAttributeName,omitempty"` // The name of the OIDC attribute used to get the full name from the IDP's userInfo. The full name attribute overrides the use of the firstName and lastName attributes + RolesAttributeName string `xml:"RolesAttributeName,omitempty"` // The name of the OIDC attribute used to get the user's roles from the IDP's userInfo +} + +// OpenIdProviderInfo contains the information about the OpenID Connect provider for creating initial org oauth settings +type OpenIdProviderInfo struct { + Xmlns string `xml:"xmlns,attr"` + + OpenIdProviderConfigurationEndpoint string `xml:"OpenIdProviderConfigurationEndpoint,omitempty"` // URL for the OAuth IDP well known openId connect configuration endpoint + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object +} + +// OpenIdProviderConfiguration is result from reading the IDP OpenID Provider config endpoint +type OpenIdProviderConfiguration struct { + Xmlns string `xml:"xmlns,attr"` + + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object + OrgOAuthSettings OrgOAuthSettings `xml:"OrgOAuthSettings,omitempty"` // OrgOauthSettings object configured using information from the IDP + ProviderConfigResponse string `xml:"ProviderConfigResponse,omitempty"` // Raw response from the IDP config endpoint +} diff --git a/types/v56/saml.go b/types/v56/saml.go index 6848095a0..f8ade72e2 100644 --- a/types/v56/saml.go +++ b/types/v56/saml.go @@ -89,12 +89,12 @@ type AdfsAuthErrorEnvelope struct { } // Error satisfies Go's default `error` interface for AdfsAuthErrorEnvelope and formats -// error for humand readable output +// error for human readable output func (samlErr AdfsAuthErrorEnvelope) Error() string { return fmt.Sprintf("SAML request got error: %s", samlErr.Body.Fault.Reason.Text) } -// AdfsAuthResponseEnvelope helps to marshal ADFS reponse to authentication request. +// AdfsAuthResponseEnvelope helps to marshal ADFS response to authentication request. // // Note. This structure is not complete and has many more fields. type AdfsAuthResponseEnvelope struct {