From 3be06c1e8364d7e0dbc7b67eb09bac7f7b6a67e2 Mon Sep 17 00:00:00 2001 From: Pierre-Jean Sauvage Date: Fri, 10 Nov 2023 15:39:58 +0100 Subject: [PATCH 1/6] feat(me): add support for SSO configuration --- ovh/provider.go | 1 + ovh/resource_me_identity_provider.go | 182 ++++++++++++++++++ ovh/resource_me_identity_provider_test.go | 136 +++++++++++++ ovh/types_me.go | 75 ++++++++ .../docs/r/me_identity_provider.html.markdown | 63 ++++++ 5 files changed, 457 insertions(+) create mode 100644 ovh/resource_me_identity_provider.go create mode 100644 ovh/resource_me_identity_provider_test.go create mode 100644 website/docs/r/me_identity_provider.html.markdown diff --git a/ovh/provider.go b/ovh/provider.go index 27b1f71cb..17a22012a 100644 --- a/ovh/provider.go +++ b/ovh/provider.go @@ -214,6 +214,7 @@ func Provider() *schema.Provider { "ovh_iploadbalancing_vrack_network": resourceIPLoadbalancingVrackNetwork(), "ovh_me_identity_group": resourceMeIdentityGroup(), "ovh_me_api_oauth2_client": resourceApiOauth2Client(), + "ovh_me_identity_provider": resourceMeIdentityProvider(), "ovh_me_identity_user": resourceMeIdentityUser(), "ovh_me_installation_template": resourceMeInstallationTemplate(), "ovh_me_installation_template_partition_scheme": resourceMeInstallationTemplatePartitionScheme(), diff --git a/ovh/resource_me_identity_provider.go b/ovh/resource_me_identity_provider.go new file mode 100644 index 000000000..0699d0b16 --- /dev/null +++ b/ovh/resource_me_identity_provider.go @@ -0,0 +1,182 @@ +package ovh + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceMeIdentityProvider() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceMeIdentityProviderCreate, + ReadContext: resourceMeIdentityProviderRead, + UpdateContext: resourceMeIdentityProviderUpdate, + DeleteContext: resourceMeIdentityProviderDelete, + + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "metadata": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "group_attribute_name": { + Type: schema.TypeString, + Optional: true, + }, + "requested_attributes": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "is_required": { + Type: schema.TypeBool, + Required: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "name_format": { + Type: schema.TypeString, + Required: true, + }, + "values": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Required: true, + }, + }, + }, + }, + "disable_users": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "creation": { + Type: schema.TypeString, + Computed: true, + }, + "last_update": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceMeIdentityProviderRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + + providerConfDetails := &MeIdentityProviderResponse{} + if err := config.OVHClient.GetWithContext(ctx, "/me/identity/provider", providerConfDetails); err != nil { + return diag.FromErr(err) + } + + d.Set("group_attribute_name", providerConfDetails.GroupAttributeName) + d.Set("disable_users", providerConfDetails.DisableUsers) + d.Set("requested_attributes", requestedAttributesToMapList(providerConfDetails.Extensions.RequestedAttributes)) + d.Set("creation", providerConfDetails.Creation) + d.Set("last_update", providerConfDetails.LastUpdate) + + return nil +} + +// requestedAttributesToMapList transforms an array of MeIdentityProviderAttribute to an array of map +func requestedAttributesToMapList(attributes []MeIdentityProviderAttribute) []map[string]interface{} { + requestedAttributes := []map[string]interface{}{} + for _, v := range attributes { + requestedAttributes = append(requestedAttributes, map[string]interface{}{ + "is_required": v.IsRequired, + "name": v.Name, + "name_format": v.NameFormat, + "values": v.Values, + }) + } + return requestedAttributes +} + +func resourceMeIdentityProviderCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + + metadata := d.Get("metadata").(string) + + groupAttributeName := d.Get("group_attribute_name").(string) + disableUsers := d.Get("disable_users").(bool) + requestedAttributes, err := loadMeIdentityProviderAttributeListFromResource(d.Get("requested_attributes")) + if err != nil { + return diag.FromErr(err) + } + + params := &MeIdentityProviderCreateOpts{ + Metadata: metadata, + GroupAttributeName: groupAttributeName, + DisableUsers: disableUsers, + Extensions: MeIdentityProviderExtensions{ + RequestedAttributes: requestedAttributes, + }, + } + + err = config.OVHClient.PostWithContext(ctx, "/me/identity/provider", params, nil) + if err != nil { + return diag.Errorf("Error creating identity provider:\n\t %v", err) + } + + // As there is only one Identity Provider configurable, we use a constant ID + d.SetId("ovh_sso") + + return resourceMeIdentityProviderRead(ctx, d, meta) +} + +func resourceMeIdentityProviderUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + + groupAttributeName := d.Get("group_attribute_name").(string) + disableUsers := d.Get("disable_users").(bool) + requestedAttributes, err := loadMeIdentityProviderAttributeListFromResource(d.Get("requested_attributes")) + if err != nil { + return diag.FromErr(err) + } + + params := &MeIdentityProviderUpdateOpts{ + GroupAttributeName: groupAttributeName, + DisableUsers: disableUsers, + Extensions: MeIdentityProviderExtensions{ + RequestedAttributes: requestedAttributes, + }, + } + err = config.OVHClient.PutWithContext(ctx, + "/me/identity/provider", + params, + nil, + ) + if err != nil { + return diag.Errorf("Unable to update identity provider:\n\t %q", err) + } + + return resourceMeIdentityProviderRead(ctx, d, meta) +} + +func resourceMeIdentityProviderDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + + err := config.OVHClient.DeleteWithContext(ctx, + "/me/identity/provider", + nil, + ) + if err != nil { + return diag.Errorf("Unable to delete identity provider:\n\t %q", err) + } + + d.SetId("") + return nil +} diff --git a/ovh/resource_me_identity_provider_test.go b/ovh/resource_me_identity_provider_test.go new file mode 100644 index 000000000..571651002 --- /dev/null +++ b/ovh/resource_me_identity_provider_test.go @@ -0,0 +1,136 @@ +package ovh + +import ( + "fmt" + "log" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const samlIDPMetadata string = ` + + + + + + MIIFlTCCA32gAwIBAgIUP8WQwHQwrvTa00RU9JROZAJj9ccwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UEBhMCRlIxEzARBgNVBAgMClNvbWUtU3RhdGUxDDAKBgNVBAcMA1JCWDERMA8GA1UECgwIT1ZIY2xvdWQxFTATBgNVBAMMDG92aGNsb3VkLmNvbTAeFw0yMzExMDkxMDA2MjRaFw0zMzExMDYxMDA2MjRaMFoxCzAJBgNVBAYTAkZSMRMwEQYDVQQIDApTb21lLVN0YXRlMQwwCgYDVQQHDANSQlgxETAPBgNVBAoMCE9WSGNsb3VkMRUwEwYDVQQDDAxvdmhjbG91ZC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC4V3HulFBksxpgkgR6KgDaSSIKkKRgyDGCF06oQN/WPxGDSHTQQHTMN7jnsbr2uJieNKh+iasGvE9JFmd6nutloL1UHoO/ecrE8P2PYgpgezl7WfyoscBDZAjWM8E9FdENnonhvlga2DgV2DGIB4+D7aN6TIPfWukOB2MjfQloA9Iw71+peO9R55S7x7zixgpLO9NovbmaAyClbz06Tsm/7ezM+Vte7BfFqGUnwuNzqgOYfQm88EqXTpCT3QfR8i2IydGgAFLMFs9YvMnCaNLw9PCN7U6VPkY6M6cFQhO/moRb3H/euJnLNRMsXp99K8ruUnQ6902NXpOOnQu5Ewzfahmx0WWvlpFGdJK34oXjaWeTodGuvHtDxCY4tiHr8jCHf9h4cmC20xAyd/V7XBtu1Pc5UAg4I0w5ehWvHtVdxCsuPEh7c4qtuLyN9Qh15r+eRbiqnWTH/xJTwfo6q6iafXXcFOlTn7WoWmmeq0R8whg6XjcxMIzBXjtynTDbQa4LVq3T8iJiGfuDgwv5OwDPRN1CsawxefETsCUQ+jf/Iw/4nZpD/YqCI5xvYtDgPSt3v2TsoOnwOSjOqKmEOoHxGTN3mhbcD+I1QKJW79zqu6GVXVwMkgWdP4pkIWGccB0FqhIVzY19xQ40DbfnCkMTv2XN4t53c/q7CYhtvyN3XwIDAQABo1MwUTAdBgNVHQ4EFgQUC8yuX4Ub/Od5jSaz7NdwHUSlq5wwHwYDVR0jBBgwFoAUC8yuX4Ub/Od5jSaz7NdwHUSlq5wwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAR4MzroH8kEwqcZeB94hetY/NQGZI+kZ26iKnvLaZa8r56UeiIrEGdEeys5JdQh/XJDWsEU6piJ0dwrIkkpgZELmUmToylcxndzjjcHbiKLlqkL+kBu9QeO/r6JTHaNyWs0An2VvCUfo+Frt8hvrJCINlCDylOaWIxHH3P0TG7ThFGWSy8nW+VMMXDS8vQIGRM66HqgYlu6HBryecf0SsCkVYbUb1zYJ+lEhYK0pj4RORainJX+PU+mIMUwQtfBByuI7RP0a2Vny0gffrtPuNfhRJb8Pwt2UYw2niWUDOfXuk9RYgqX/1wLVqk72KJJlD3c7+abZ6BcNEJax5e/icilUrxcs4MymDPjk63kQURRVzcC4hCXYqJVQmRfVT4fdLLKPmeg3ysl+U4eJZ8odmaqoVGqZryncdAC+nT5lnLRm6m2lv3v+YhConctLxzCwV/xA8jU2w9VVRw2gkY8bdkvOb7c2OpXU6J3TYtaltG7foQiuXbRd37GWzzzEspxiAI9y8uIEJTsASaufsEdpR+a1sPy3rYJom/Li3dH9p9Ch+tp51pMYhSRGEiNu9g5918zMbrKvwkl6h/PQlTOlb65qUUoNKC5Baxhz3VkGxSKMUwS4Lj/WHvCGU5OteGFHglDgDm125FDakOYU1dnMm/P55yNhnSUH2sXngybxnw/w= + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + + +` + +func init() { + resource.AddTestSweepers("ovh_me_identity_provider", &resource.Sweeper{ + Name: "ovh_me_identity_provider", + F: testSweepMeIdentityProvider, + }) +} + +func testSweepMeIdentityProvider(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + err = resource.Retry(5*time.Minute, func() *resource.RetryError { + log.Printf("[INFO] Deleting identity provider") + if err := client.Delete("/me/identity/provider", nil); err != nil { + return resource.RetryableError(err) + } + + // Successful delete + return nil + }) + + return err +} + +func TestAccMeIdentityProvider_basic(t *testing.T) { + groupeAttribute := acctest.RandomWithPrefix(test_prefix) + disableUsers := "false" + config := fmt.Sprintf(testAccMeIdentityProviderConfig_basic, groupeAttribute, disableUsers, samlIDPMetadata) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckCredentials(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + checkIdentityProviderResourceAttr("ovh_me_identity_provider.my_provider", groupeAttribute, disableUsers, samlIDPMetadata, nil)..., + ), + }, + }, + }) +} + +func TestAccMeIdentityProvider_requestedAttributes(t *testing.T) { + groupeAttribute := acctest.RandomWithPrefix(test_prefix) + disableUsers := "false" + requestedAttribute := map[string]string{ + "is_required": "false", + "name": "test1", + "name_format": "test2", + "values": "test3", + } + config := fmt.Sprintf(testAccMeIdentityProviderConfig_requestedAttribute, groupeAttribute, disableUsers, samlIDPMetadata, requestedAttribute["is_required"], requestedAttribute["name"], requestedAttribute["name_format"], requestedAttribute["values"]) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckCredentials(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + checkIdentityProviderResourceAttr("ovh_me_identity_provider.my_provider", groupeAttribute, disableUsers, samlIDPMetadata, requestedAttribute)..., + ), + }, + }, + }) +} + +func checkIdentityProviderResourceAttr(name, group_attribute, disable_users, metadata string, requestedAttributes map[string]string) []resource.TestCheckFunc { + checks := []resource.TestCheckFunc{} + checks = append(checks, resource.TestCheckResourceAttr(name, "group_attribute_name", group_attribute)) + checks = append(checks, resource.TestCheckResourceAttr(name, "disable_users", disable_users)) + checks = append(checks, resource.TestCheckResourceAttr(name, "metadata", metadata+"\n")) + if requestedAttributes != nil { + checks = append(checks, resource.TestCheckResourceAttr(name, "requested_attributes.0.is_required", requestedAttributes["is_required"])) + checks = append(checks, resource.TestCheckResourceAttr(name, "requested_attributes.0.name", requestedAttributes["name"])) + checks = append(checks, resource.TestCheckResourceAttr(name, "requested_attributes.0.name_format", requestedAttributes["name_format"])) + checks = append(checks, resource.TestCheckResourceAttr(name, "requested_attributes.0.values.0", requestedAttributes["values"])) + } + return checks +} + +const testAccMeIdentityProviderConfig_basic = ` +resource "ovh_me_identity_provider" "my_provider" { + group_attribute_name = "%s" + disable_users = %s + metadata = < + + + + + + MIIFlTCCA32gAwIBAgIUP8WQwHQwrvTa00RU9JROZAJj9ccwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UEBhMCRlIxEzARBgNVBAgMClNvbWUtU3RhdGUxDDAKBgNVBAcMA1JCWDERMA8GA1UECgwIT1ZIY2xvdWQxFTATBgNVBAMMDG92aGNsb3VkLmNvbTAeFw0yMzExMDkxMDA2[...]xA8jU2w9VVRw2gkY8bdkvOb7c2OpXU6J3TYtaltG7foQiuXbRd37GWzzzEspxiAI9y8uIEJTsASaufsEdpR+a1sPy3rYJom/Li3dH9p9Ch+tp51pMYhSRGEiNu9g5918zMbrKvwkl6h/PQlTOlb65qUUoNKC5Baxhz3VkGxSKMUwS4Lj/WHvCGU5OteGFHglDgDm125FDakOYU1dnMm/P55yNhnSUH2sXngybxnw/w= + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + + + +EOT + + disable_users = false + + requested_attributes { + is_required = false + name = "group" + name_format = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic" + values = ["test"] + } + requested_attributes { + is_required = false + name = "email" + name_format = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic" + values = ["test@example.org"] + } +} +``` + +## Argument Reference + +* `group_attribute_name` - The name of the attribute containing the information of which group the connecting users belong to. +* `metadata` - The SAML xml metadata of the Identity Provider to federate to. +* `disable_users` - Whether account users should still be usable as a login method or not (optional, defaults to true). +* `requested_attributes` A SAML 2.0 requested attribute that should be added to SAML requests when using this provider (optional). + * `is_required` Expresses that this RequestedAttribute is mandatory. + * `name` Name of the SAML RequestedAttribute. + * `name_format` NameFormat of the SAML RequestedAttribute. + * `values` List of AttributeValues allowed for this RequestedAttribute + +## Attributes Reference + +* `creation` - Creation date of the SAML Federation. +* `last_update` - Date of the last update of the SAML Federation. From ef3a3bd3be0b6742da83d8b6b03a7267d5180c9c Mon Sep 17 00:00:00 2001 From: Pierre-Jean Sauvage Date: Tue, 14 Nov 2023 15:54:48 +0100 Subject: [PATCH 2/6] remove resource importer --- ovh/resource_me_identity_provider.go | 23 +++------------- ovh/types_me.go | 40 ++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/ovh/resource_me_identity_provider.go b/ovh/resource_me_identity_provider.go index 0699d0b16..a6ea5fe27 100644 --- a/ovh/resource_me_identity_provider.go +++ b/ovh/resource_me_identity_provider.go @@ -14,11 +14,10 @@ func resourceMeIdentityProvider() *schema.Resource { UpdateContext: resourceMeIdentityProviderUpdate, DeleteContext: resourceMeIdentityProviderDelete, - Importer: &schema.ResourceImporter{ - StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - return []*schema.ResourceData{d}, nil - }, - }, + // Importer is voluntarily disabled for this resource. As the `metadata` + // attribute is not retrievable with GET /me/identity/provider, there + // is no way to create a valid resource using `terraform import` + Importer: nil, Schema: map[string]*schema.Schema{ "metadata": { @@ -91,20 +90,6 @@ func resourceMeIdentityProviderRead(ctx context.Context, d *schema.ResourceData, return nil } -// requestedAttributesToMapList transforms an array of MeIdentityProviderAttribute to an array of map -func requestedAttributesToMapList(attributes []MeIdentityProviderAttribute) []map[string]interface{} { - requestedAttributes := []map[string]interface{}{} - for _, v := range attributes { - requestedAttributes = append(requestedAttributes, map[string]interface{}{ - "is_required": v.IsRequired, - "name": v.Name, - "name_format": v.NameFormat, - "values": v.Values, - }) - } - return requestedAttributes -} - func resourceMeIdentityProviderCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { config := meta.(*Config) diff --git a/ovh/types_me.go b/ovh/types_me.go index 15465b4e8..bc26ca918 100644 --- a/ovh/types_me.go +++ b/ovh/types_me.go @@ -179,17 +179,20 @@ type MeIdentityUserUpdateOpts struct { } type MeIdentityProviderResponse struct { - GroupAttributeName string `json:"groupAttributeName"` - IdpSigningCertificates []struct { - Expiration string `json:"expiration"` - Subject string `json:"subject"` - } `json:"idpSigningCertificates"` - DisableUsers bool `json:"disableUsers"` - Extensions MeIdentityProviderExtensions `json:"extensions"` - - SsoServiceUrl string `json:"ssoServiceUrl"` - Creation string `json:"creation"` - LastUpdate string `json:"lastUpdate"` + GroupAttributeName string `json:"groupAttributeName"` + IdpSigningCertificates []MeIdentityProviderIDPCertificates `json:"idpSigningCertificates"` + DisableUsers bool `json:"disableUsers"` + Extensions MeIdentityProviderExtensions `json:"extensions"` + + UserAttributeName string `json:"userAttributeName"` + SsoServiceUrl string `json:"ssoServiceUrl"` + Creation string `json:"creation"` + LastUpdate string `json:"lastUpdate"` +} + +type MeIdentityProviderIDPCertificates struct { + Expiration string `json:"expiration"` + Subject string `json:"subject"` } type MeIdentityProviderCreateOpts struct { @@ -333,3 +336,18 @@ func loadMeIdentityProviderAttributeFromResource(i interface{}) (MeIdentityProvi return requestedAttribute, nil } + +// requestedAttributesToMapList transforms an array of MeIdentityProviderAttribute to an array of map +func requestedAttributesToMapList(attributes []MeIdentityProviderAttribute) []map[string]interface{} { + requestedAttributes := []map[string]interface{}{} + for _, v := range attributes { + requestedAttributes = append(requestedAttributes, map[string]interface{}{ + "is_required": v.IsRequired, + "name": v.Name, + "name_format": v.NameFormat, + "values": v.Values, + }) + } + return requestedAttributes +} + From 7d18e25476f2aadf82dbf9694eb77cec7ad1ac2a Mon Sep 17 00:00:00 2001 From: Pierre-Jean Sauvage Date: Tue, 14 Nov 2023 15:54:57 +0100 Subject: [PATCH 3/6] add data source --- ovh/data_me_identity_provider.go | 105 ++++++++++++++++++++++ ovh/data_me_identity_provider_test.go | 125 ++++++++++++++++++++++++++ ovh/provider.go | 1 + ovh/types_me.go | 11 +++ 4 files changed, 242 insertions(+) create mode 100644 ovh/data_me_identity_provider.go create mode 100644 ovh/data_me_identity_provider_test.go diff --git a/ovh/data_me_identity_provider.go b/ovh/data_me_identity_provider.go new file mode 100644 index 000000000..776a5915e --- /dev/null +++ b/ovh/data_me_identity_provider.go @@ -0,0 +1,105 @@ +package ovh + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceMeIdentityProvider() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceMeIdentityProviderRead, + + Schema: map[string]*schema.Schema{ + "group_attribute_name": { + Type: schema.TypeString, + Computed: true, + }, + "requested_attributes": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "is_required": { + Type: schema.TypeBool, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "name_format": { + Type: schema.TypeString, + Computed: true, + }, + "values": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + }, + }, + }, + "idp_signing_certificates": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "expiration": { + Type: schema.TypeString, + Computed: true, + }, + "subject": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "sso_service_url": { + Type: schema.TypeString, + Computed: true, + }, + "user_attribute_name": { + Type: schema.TypeString, + Computed: true, + }, + "disable_users": { + Type: schema.TypeBool, + Computed: true, + }, + "creation": { + Type: schema.TypeString, + Computed: true, + }, + "last_update": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceMeIdentityProviderRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + + providerConfDetails := &MeIdentityProviderResponse{} + if err := config.OVHClient.GetWithContext(ctx, "/me/identity/provider", providerConfDetails); err != nil { + return diag.FromErr(err) + } + + d.SetId("ovh_sso") + d.Set("group_attribute_name", providerConfDetails.GroupAttributeName) + d.Set("disable_users", providerConfDetails.DisableUsers) + d.Set("requested_attributes", requestedAttributesToMapList(providerConfDetails.Extensions.RequestedAttributes)) + d.Set("idp_signing_certificates", idpSigningCertificatesToMapList(providerConfDetails.IdpSigningCertificates)) + d.Set("sso_service_url", providerConfDetails.SsoServiceUrl) + d.Set("user_attribute_name", providerConfDetails.UserAttributeName) + d.Set("creation", providerConfDetails.Creation) + d.Set("last_update", providerConfDetails.LastUpdate) + + return nil +} diff --git a/ovh/data_me_identity_provider_test.go b/ovh/data_me_identity_provider_test.go new file mode 100644 index 000000000..7319d3ef0 --- /dev/null +++ b/ovh/data_me_identity_provider_test.go @@ -0,0 +1,125 @@ +package ovh + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccMeIdentityProviderDataSource_basic(t *testing.T) { + groupAttributeName := "http://schemas.xmlsoap.org/claims/Group" + disableUsers := "false" + reqAttributeRequired := "false" + reqAttributeName := "identity" + reqAttributeNameFormat := "urn:oasis:names:tc:SAML:2.0:attrname-format:basic" + reqAttributeValue := "foobar" + + preSetup := fmt.Sprintf( + testAccMeIdentityProviderDataSourceConfig_preSetup, + groupAttributeName, + samlIDPMetadata, + disableUsers, + reqAttributeRequired, + reqAttributeName, + reqAttributeNameFormat, + reqAttributeValue, + ) + config := fmt.Sprintf( + testAccMeIdentityProviderDataSourceConfig_keys, + groupAttributeName, + samlIDPMetadata, + disableUsers, + reqAttributeRequired, + reqAttributeName, + reqAttributeNameFormat, + reqAttributeValue, + ) + + userAttributeName := "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" + ssoServiceUrl := "https://ovhcloud.com/" + certificateExpiration := "2033-11-06T10:06:24Z" + certificateSubject := "CN=ovhcloud.com,O=OVHcloud,L=RBX,ST=Some-State,C=FR" + + requestedAttributes := map[string]string{ + "is_required": reqAttributeRequired, + "name": reqAttributeName, + "name_format": reqAttributeNameFormat, + "values": reqAttributeValue, + } + + checks := checkIdentityProviderResourceAttr("ovh_me_identity_provider.sso", groupAttributeName, disableUsers, samlIDPMetadata, requestedAttributes) + dataSourceChecks := checkIdentityProviderDataSourceAttr("data.ovh_me_identity_provider.sso", groupAttributeName, userAttributeName, ssoServiceUrl, disableUsers, certificateExpiration, certificateSubject, requestedAttributes) + dataSourceChecks = append(dataSourceChecks, resource.TestCheckOutput("keys_present", "true")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckCredentials(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: preSetup, + Check: resource.ComposeTestCheckFunc(checks...), + }, { + Config: config, + Check: resource.ComposeTestCheckFunc(dataSourceChecks...), + }, + }, + }) +} + +func checkIdentityProviderDataSourceAttr(name, group_attribute, user_attribute, sso_service_url, disable_users, certificateExpiration, certificateSubject string, requestedAttributes map[string]string) []resource.TestCheckFunc { + checks := []resource.TestCheckFunc{} + checks = append(checks, resource.TestCheckResourceAttr(name, "group_attribute_name", group_attribute)) + checks = append(checks, resource.TestCheckResourceAttr(name, "user_attribute_name", user_attribute)) + checks = append(checks, resource.TestCheckResourceAttr(name, "sso_service_url", sso_service_url)) + checks = append(checks, resource.TestCheckResourceAttr(name, "disable_users", disable_users)) + checks = append(checks, resource.TestCheckResourceAttr(name, "idp_signing_certificates.0.expiration", certificateExpiration)) + checks = append(checks, resource.TestCheckResourceAttr(name, "idp_signing_certificates.0.subject", certificateSubject)) + if requestedAttributes != nil { + checks = append(checks, resource.TestCheckResourceAttr(name, "requested_attributes.0.is_required", requestedAttributes["is_required"])) + checks = append(checks, resource.TestCheckResourceAttr(name, "requested_attributes.0.name", requestedAttributes["name"])) + checks = append(checks, resource.TestCheckResourceAttr(name, "requested_attributes.0.name_format", requestedAttributes["name_format"])) + checks = append(checks, resource.TestCheckResourceAttr(name, "requested_attributes.0.values.0", requestedAttributes["values"])) + } + return checks +} + +const testAccMeIdentityProviderDataSourceConfig_preSetup = ` +resource "ovh_me_identity_provider" "sso" { + group_attribute_name = "%s" + metadata = < Date: Thu, 23 Nov 2023 11:36:04 +0100 Subject: [PATCH 4/6] improve documentation --- .../docs/r/me_identity_provider.html.markdown | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/website/docs/r/me_identity_provider.html.markdown b/website/docs/r/me_identity_provider.html.markdown index 9cc029ec2..3a3604740 100644 --- a/website/docs/r/me_identity_provider.html.markdown +++ b/website/docs/r/me_identity_provider.html.markdown @@ -4,7 +4,7 @@ subcategory : "Account Management" # ovh_me_identity_provider -Configure SAML Fedration (SSO) to an identity provider. +Configure SAML Federation (SSO) to an identity provider. ## Example Usage @@ -29,19 +29,25 @@ resource "ovh_me_identity_provider" "sso" { EOT + # Local users will still be able to login if set to false. + # Administrator can always login regardless of this value. disable_users = false + # The assertion must contain the attribute "https://example.org/attributes/role" + # with the allowed values being "user" or "administrator" requested_attributes { - is_required = false - name = "group" - name_format = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic" - values = ["test"] + is_required = true + name = "https://example.org/attributes/role" + name_format = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri" + values = ["user", administrator] } + # If the attribute "https://example.org/attributes/group" is available, + # we want the IdP to provide it requested_attributes { is_required = false - name = "email" - name_format = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic" - values = ["test@example.org"] + name = "https://example.org/attributes/group" + name_format = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri" + values = [] } } ``` @@ -50,12 +56,12 @@ EOT * `group_attribute_name` - The name of the attribute containing the information of which group the connecting users belong to. * `metadata` - The SAML xml metadata of the Identity Provider to federate to. -* `disable_users` - Whether account users should still be usable as a login method or not (optional, defaults to true). -* `requested_attributes` A SAML 2.0 requested attribute that should be added to SAML requests when using this provider (optional). - * `is_required` Expresses that this RequestedAttribute is mandatory. - * `name` Name of the SAML RequestedAttribute. +* `disable_users` - Whether local users should still be usable as a login method or not (optional, defaults to true). Administrator will always be able to login, regardless of this value. +* `requested_attributes` A SAML 2.0 requested attribute as defined in [SAML-ReqAttrExt-v1.0](http://docs.oasis-open.org/security/saml-protoc-req-attr-req/v1.0/cs01/saml-protoc-req-attr-req-v1.0-cs01.pdf). A RequestedAttribute object will indicate that the Identity Provider should add the described attribute to the SAML assertions that will be given to the Service Provider (OVH). + * `is_required` Expresses that this Attribute is mandatory. If the requested attribute is not present in the assertion, the user won't be allowed to log in. + * `name` Name of the SAML Attribute that is required. * `name_format` NameFormat of the SAML RequestedAttribute. - * `values` List of AttributeValues allowed for this RequestedAttribute + * `values` List of AttributeValues allowed for this RequestedAttribute. ## Attributes Reference From 6c37dc49a6ae9a4277893a35f62ebee902791ff0 Mon Sep 17 00:00:00 2001 From: Pierre-Jean Sauvage Date: Thu, 23 Nov 2023 14:09:33 +0100 Subject: [PATCH 5/6] fix data structures --- ovh/resource_me_identity_provider.go | 31 ++++++++++++++++++++++++++-- ovh/types_me.go | 6 +++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/ovh/resource_me_identity_provider.go b/ovh/resource_me_identity_provider.go index a6ea5fe27..03f960d53 100644 --- a/ovh/resource_me_identity_provider.go +++ b/ovh/resource_me_identity_provider.go @@ -44,14 +44,14 @@ func resourceMeIdentityProvider() *schema.Resource { }, "name_format": { Type: schema.TypeString, - Required: true, + Optional: true, }, "values": { Type: schema.TypeList, Elem: &schema.Schema{ Type: schema.TypeString, }, - Required: true, + Optional: true, }, }, }, @@ -69,6 +69,30 @@ func resourceMeIdentityProvider() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "idp_signing_certificates": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "expiration": { + Type: schema.TypeString, + Computed: true, + }, + "subject": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "sso_service_url": { + Type: schema.TypeString, + Computed: true, + }, + "user_attribute_name": { + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -86,6 +110,9 @@ func resourceMeIdentityProviderRead(ctx context.Context, d *schema.ResourceData, d.Set("requested_attributes", requestedAttributesToMapList(providerConfDetails.Extensions.RequestedAttributes)) d.Set("creation", providerConfDetails.Creation) d.Set("last_update", providerConfDetails.LastUpdate) + d.Set("idp_signing_certificates", idpSigningCertificatesToMapList(providerConfDetails.IdpSigningCertificates)) + d.Set("sso_service_url", providerConfDetails.SsoServiceUrl) + d.Set("user_attribute_name", providerConfDetails.UserAttributeName) return nil } diff --git a/ovh/types_me.go b/ovh/types_me.go index 9c1acd471..8917bfce6 100644 --- a/ovh/types_me.go +++ b/ovh/types_me.go @@ -197,7 +197,7 @@ type MeIdentityProviderIDPCertificates struct { type MeIdentityProviderCreateOpts struct { Metadata string `json:"metadata"` - GroupAttributeName string `json:"groupAttributeName"` + GroupAttributeName string `json:"groupAttributeName,omitempty"` DisableUsers bool `json:"disableUsers"` Extensions MeIdentityProviderExtensions `json:"extensions,omitempty"` @@ -217,8 +217,8 @@ type MeIdentityProviderExtensions struct { type MeIdentityProviderAttribute struct { IsRequired bool `json:"isRequired"` Name string `json:"name"` - NameFormat string `json:"nameFormat"` - Values []string `json:"values"` + NameFormat string `json:"nameFormat,omitempty"` + Values []string `json:"values,omitempty"` } // MeSshKey Opts From 66af146c62c2eb757e52e3160785eb965b84dd85 Mon Sep 17 00:00:00 2001 From: Pierre-Jean Sauvage Date: Thu, 23 Nov 2023 14:09:55 +0100 Subject: [PATCH 6/6] improve documentation --- website/docs/r/me_identity_provider.html.markdown | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/r/me_identity_provider.html.markdown b/website/docs/r/me_identity_provider.html.markdown index 3a3604740..ff281b5e8 100644 --- a/website/docs/r/me_identity_provider.html.markdown +++ b/website/docs/r/me_identity_provider.html.markdown @@ -30,7 +30,7 @@ resource "ovh_me_identity_provider" "sso" { EOT # Local users will still be able to login if set to false. - # Administrator can always login regardless of this value. + # Owner of the OVHcloud account can always login using the nichandle ID and the nichandle password, regardless of this value. disable_users = false # The assertion must contain the attribute "https://example.org/attributes/role" @@ -56,10 +56,10 @@ EOT * `group_attribute_name` - The name of the attribute containing the information of which group the connecting users belong to. * `metadata` - The SAML xml metadata of the Identity Provider to federate to. -* `disable_users` - Whether local users should still be usable as a login method or not (optional, defaults to true). Administrator will always be able to login, regardless of this value. -* `requested_attributes` A SAML 2.0 requested attribute as defined in [SAML-ReqAttrExt-v1.0](http://docs.oasis-open.org/security/saml-protoc-req-attr-req/v1.0/cs01/saml-protoc-req-attr-req-v1.0-cs01.pdf). A RequestedAttribute object will indicate that the Identity Provider should add the described attribute to the SAML assertions that will be given to the Service Provider (OVH). +* `disable_users` - Whether local users should still be usable as a login method or not (optional, defaults to true). Owner of the OVHcloud account can always login using the nichandle ID and the nichandle password, regardless of this value. +* `requested_attributes` A SAML 2.0 requested attribute as defined in [SAML-ReqAttrExt-v1.0](http://docs.oasis-open.org/security/saml-protoc-req-attr-req/v1.0/cs01/saml-protoc-req-attr-req-v1.0-cs01.pdf). A RequestedAttribute object will indicate that the Identity Provider should add the described attribute to the SAML assertions that will be given to the Service Provider (OVHcloud). * `is_required` Expresses that this Attribute is mandatory. If the requested attribute is not present in the assertion, the user won't be allowed to log in. - * `name` Name of the SAML Attribute that is required. + * `name` Name of the SAML Attribute that is requested. * `name_format` NameFormat of the SAML RequestedAttribute. * `values` List of AttributeValues allowed for this RequestedAttribute.