From c380e1e24d23658fcb19f97ab62f8b0f934c2adc Mon Sep 17 00:00:00 2001 From: Pierre-Jean Sauvage Date: Fri, 10 Nov 2023 15:39:58 +0100 Subject: [PATCH] feat(me): add support for SSO configuration --- ovh/provider.go | 1 + ovh/resource_me_identity_provider.go | 181 ++++++++++++++++++ ovh/resource_me_identity_provider_test.go | 136 +++++++++++++ ovh/types_me.go | 75 ++++++++ .../docs/r/me_identity_provider.html.markdown | 63 ++++++ 5 files changed, 456 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 6579553a5..a3f9bb1c9 100644 --- a/ovh/provider.go +++ b/ovh/provider.go @@ -208,6 +208,7 @@ func Provider() *schema.Provider { "ovh_iploadbalancing_tcp_route_rule": resourceIPLoadbalancingTcpRouteRule(), "ovh_iploadbalancing_vrack_network": resourceIPLoadbalancingVrackNetwork(), "ovh_me_identity_group": resourceMeIdentityGroup(), + "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..d2611c901 --- /dev/null +++ b/ovh/resource_me_identity_provider.go @@ -0,0 +1,181 @@ +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, + }, + }, + } +} + +// Common function with the datasource +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 +} + +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) + } + + 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.