diff --git a/internal/provider/resource_cloudflare_access_application.go b/internal/provider/resource_cloudflare_access_application.go index 9efd67777b..4fbe09abf5 100644 --- a/internal/provider/resource_cloudflare_access_application.go +++ b/internal/provider/resource_cloudflare_access_application.go @@ -60,6 +60,14 @@ func resourceCloudflareAccessApplicationCreate(ctx context.Context, d *schema.Re newAccessApplication.CorsHeaders = CORSConfig } + if _, ok := d.GetOk("saas_app"); ok { + SaasConfig, err := convertSaasSchemaToStruct(d) + if err != nil { + return diag.FromErr(err) + } + newAccessApplication.SaasApplication = SaasConfig + } + tflog.Debug(ctx, fmt.Sprintf("Creating Cloudflare Access Application from struct: %+v", newAccessApplication)) identifier, err := initIdentifier(d) diff --git a/internal/provider/resource_cloudflare_access_application_test.go b/internal/provider/resource_cloudflare_access_application_test.go index 7845a8e602..b8b04b2c1e 100644 --- a/internal/provider/resource_cloudflare_access_application_test.go +++ b/internal/provider/resource_cloudflare_access_application_test.go @@ -674,6 +674,89 @@ func TestAccCloudflareAccessApplicationMisconfiguredCORSCredentialsAllowingAllOr }) } +func testAccessApplicationSaasApp(resourceID, zone, zoneID string) string { + return fmt.Sprintf(` + resource "cloudflare_access_application" "%[1]s" { + name = "%[1]s-updated" + zone_id = "%[3]s" + domain = "%[1]s.%[2]s" + type = "saas" + + saas_app { + consumer_service_url = "https://dashtest.cloudflare.com/api/v4/saml/acs" + sp_entity_id = "dash.cloudflare.com" + name_id_format = "id" + custom_attributes { + + } + } + } + `, resourceID, zone, zoneID) +} + +func TestAccCloudflareAccessApplicationWithSaasApp(t *testing.T) { + rnd := generateRandomResourceName() + name := "cloudflare_access_application." + rnd + zone := os.Getenv("CLOUDFLARE_DOMAIN") + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + updatedName := fmt.Sprintf("%s-updated", rnd) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccessAccPreCheck(t) + testAccPreCheckAccount(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccessApplicationSaasApp(rnd, zone, zoneID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "saas_app.consumer_service_url", "https://dashtest.cloudflare.com/api/v4/saml/acs"), + resource.TestCheckResourceAttr(name, "saas_app.sp_entity_id", "dash.cloudflare.com"), + resource.TestCheckResourceAttr(name, "saas_app.name_id_format", "id"), + ), + }, + { + Config: testAccessApplicationSaasApp(rnd, zone, zoneID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "name", updatedName), + resource.TestCheckResourceAttr(name, "zone_id", zoneID), + ), + }, + }, + }) +} + +func TestAccCloudflareAccessApplicationBookmark(t *testing.T) { + rnd := generateRandomResourceName() + name := "cloudflare_access_application." + rnd + zone := os.Getenv("CLOUDFLARE_DOMAIN") + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + updatedName := fmt.Sprintf("%s-updated", rnd) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccessAccPreCheck(t) + testAccPreCheckAccount(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccessApplicationBookmark(rnd, zone, zoneID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "type", "bookmark"), + ), + }, + { + Config: testAccessApplicationBookmark(rnd, zone, zoneID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "name", updatedName), + resource.TestCheckResourceAttr(name, "zone_id", zoneID), + ), + }, + }, + }) +} func TestAccCloudflareAccessApplicationMisconfiguredCORSCredentialsAllowingWildcardOrigins(t *testing.T) { rnd := generateRandomResourceName() zone := os.Getenv("CLOUDFLARE_DOMAIN") @@ -694,6 +777,18 @@ func TestAccCloudflareAccessApplicationMisconfiguredCORSCredentialsAllowingWildc }) } +func testAccessApplicationBookmark(resourceID, zone, zoneID string) string { + return fmt.Sprintf(` + resource "cloudflare_access_application" "%[1]s" { + name = "%[1]s-updated" + zone_id = "%[3]s" + domain = "%[1]s.%[2]s" + type = "bookmark" + + } + `, resourceID, zone, zoneID) +} + func testAccessApplicationWithZoneID(resourceID, zone, zoneID string) string { return fmt.Sprintf(` resource "cloudflare_access_application" "%[1]s" { diff --git a/internal/provider/resource_cloudflare_access_bookmark.go b/internal/provider/resource_cloudflare_access_bookmark.go deleted file mode 100644 index 29c060786a..0000000000 --- a/internal/provider/resource_cloudflare_access_bookmark.go +++ /dev/null @@ -1,175 +0,0 @@ -package provider - -import ( - "context" - "errors" - "fmt" - "strings" - - cloudflare "github.com/cloudflare/cloudflare-go" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceCloudflareAccessBookmark() *schema.Resource { - return &schema.Resource{ - Schema: resourceCloudflareAccessBookmarkSchema(), - CreateContext: resourceCloudflareAccessBookmarkCreate, - ReadContext: resourceCloudflareAccessBookmarkRead, - UpdateContext: resourceCloudflareAccessBookmarkUpdate, - DeleteContext: resourceCloudflareAccessBookmarkDelete, - Importer: &schema.ResourceImporter{ - StateContext: resourceCloudflareAccessBookmarkImport, - }, - } -} - -func resourceCloudflareAccessBookmarkCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*cloudflare.API) - - newAccessBookmark := cloudflare.AccessBookmark{ - Name: d.Get("name").(string), - Domain: d.Get("domain").(string), - LogoURL: d.Get("logo_url").(string), - AppLauncherVisible: d.Get("app_launcher_visible").(bool), - } - - tflog.Debug(ctx, fmt.Sprintf("Creating Cloudflare Access Bookmark from struct: %+v", newAccessBookmark)) - - identifier, err := initIdentifier(d) - if err != nil { - return diag.FromErr(err) - } - - var accessBookmark cloudflare.AccessBookmark - if identifier.Type == AccountType { - accessBookmark, err = client.CreateAccessBookmark(ctx, identifier.Value, newAccessBookmark) - } else { - accessBookmark, err = client.CreateZoneLevelAccessBookmark(ctx, identifier.Value, newAccessBookmark) - } - if err != nil { - return diag.FromErr(fmt.Errorf("error creating Access Bookmark for %s %q: %w", identifier.Type, identifier.Value, err)) - } - - d.SetId(accessBookmark.ID) - - return resourceCloudflareAccessBookmarkRead(ctx, d, meta) -} - -func resourceCloudflareAccessBookmarkRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*cloudflare.API) - - identifier, err := initIdentifier(d) - if err != nil { - return diag.FromErr(err) - } - - var accessBookmark cloudflare.AccessBookmark - if identifier.Type == AccountType { - accessBookmark, err = client.AccessBookmark(ctx, identifier.Value, d.Id()) - } else { - accessBookmark, err = client.ZoneLevelAccessBookmark(ctx, identifier.Value, d.Id()) - } - - if err != nil { - if strings.Contains(err.Error(), "HTTP status 404") { - tflog.Info(ctx, fmt.Sprintf("Access Bookmark %s no longer exists", d.Id())) - d.SetId("") - return nil - } - return diag.FromErr(fmt.Errorf("error finding Access Bookmark %q: %w", d.Id(), err)) - } - - d.Set("name", accessBookmark.Name) - d.Set("domain", accessBookmark.Domain) - d.Set("logo_url", accessBookmark.LogoURL) - d.Set("app_launcher_visible", accessBookmark.AppLauncherVisible) - - return nil -} - -func resourceCloudflareAccessBookmarkUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*cloudflare.API) - - updatedAccessBookmark := cloudflare.AccessBookmark{ - ID: d.Id(), - Name: d.Get("name").(string), - Domain: d.Get("domain").(string), - LogoURL: d.Get("logo_url").(string), - AppLauncherVisible: d.Get("app_launcher_visible").(bool), - } - - tflog.Debug(ctx, fmt.Sprintf("Updating Cloudflare Access Bookmark from struct: %+v", updatedAccessBookmark)) - - identifier, err := initIdentifier(d) - if err != nil { - return diag.FromErr(err) - } - - var accessBookmark cloudflare.AccessBookmark - if identifier.Type == AccountType { - accessBookmark, err = client.UpdateAccessBookmark(ctx, identifier.Value, updatedAccessBookmark) - } else { - accessBookmark, err = client.UpdateZoneLevelAccessBookmark(ctx, identifier.Value, updatedAccessBookmark) - } - if err != nil { - return diag.FromErr(fmt.Errorf("error updating Access Bookmark for %s %q: %w", identifier.Type, identifier.Value, err)) - } - - if accessBookmark.ID == "" { - return diag.FromErr(fmt.Errorf("failed to find Access Bookmark ID in update response; resource was empty")) - } - - return resourceCloudflareAccessBookmarkRead(ctx, d, meta) -} - -func resourceCloudflareAccessBookmarkDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*cloudflare.API) - bookmarkID := d.Id() - - tflog.Debug(ctx, fmt.Sprintf("Deleting Cloudflare Access Bookmark using ID: %s", bookmarkID)) - - identifier, err := initIdentifier(d) - if err != nil { - return diag.FromErr(err) - } - - if identifier.Type == AccountType { - err = client.DeleteAccessBookmark(ctx, identifier.Value, bookmarkID) - } else { - err = client.DeleteZoneLevelAccessBookmark(ctx, identifier.Value, bookmarkID) - } - if err != nil { - return diag.FromErr(fmt.Errorf("error deleting Access Bookmark for %s %q: %w", identifier.Type, identifier.Value, err)) - } - - readErr := resourceCloudflareAccessBookmarkRead(ctx, d, meta) - if readErr != nil { - return readErr - } - - return nil -} - -func resourceCloudflareAccessBookmarkImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - attributes := strings.SplitN(d.Id(), "/", 2) - - if len(attributes) != 2 { - return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"accountID/accessBookmarkID\"", d.Id()) - } - - accountID, accessBookmarkID := attributes[0], attributes[1] - - tflog.Debug(ctx, fmt.Sprintf("Importing Cloudflare Access Bookmark: id %s for account %s", accessBookmarkID, accountID)) - - d.Set("account_id", accountID) - d.SetId(accessBookmarkID) - - readErr := resourceCloudflareAccessBookmarkRead(ctx, d, meta) - if readErr != nil { - return nil, errors.New("failed to read Access Bookmark state") - } - - return []*schema.ResourceData{d}, nil -} diff --git a/internal/provider/resource_cloudflare_access_bookmark_test.go b/internal/provider/resource_cloudflare_access_bookmark_test.go deleted file mode 100644 index f321e308b4..0000000000 --- a/internal/provider/resource_cloudflare_access_bookmark_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package provider - -import ( - "context" - "fmt" - "os" - "testing" - - "github.com/cloudflare/cloudflare-go" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func TestAccCloudflareAccessBookmark_Basic(t *testing.T) { - // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the OTP Access - // endpoint does not yet support the API tokens for updates and it results in - // state error messages. - if os.Getenv("CLOUDFLARE_API_TOKEN") != "" { - defer func(apiToken string) { - os.Setenv("CLOUDFLARE_API_TOKEN", apiToken) - }(os.Getenv("CLOUDFLARE_API_TOKEN")) - os.Setenv("CLOUDFLARE_API_TOKEN", "") - } - - rnd := generateRandomResourceName() - name := fmt.Sprintf("cloudflare_access_bookmark.%s", rnd) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { - testAccessAccPreCheck(t) - }, - ProviderFactories: providerFactories, - CheckDestroy: testAccCheckCloudflareAccessBookmarkDestroy, - Steps: []resource.TestStep{ - { - Config: testAccCloudflareAccessBookmarkConfigBasic(rnd, domain, AccessIdentifier{Type: ZoneType, Value: zoneID}), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(name, "zone_id", zoneID), - resource.TestCheckResourceAttr(name, "name", rnd), - resource.TestCheckResourceAttr(name, "domain", fmt.Sprintf("%s.%s", rnd, domain)), - resource.TestCheckResourceAttr(name, "logo_url", "https://www.cloudflare.com/img/logo-web-badges/cf-logo-on-white-bg.svg"), - resource.TestCheckResourceAttr(name, "app_launcher_visible", "true"), - ), - }, - }, - }) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { - testAccessAccPreCheck(t) - testAccPreCheckAccount(t) - }, - ProviderFactories: providerFactories, - CheckDestroy: testAccCheckCloudflareAccessBookmarkDestroy, - Steps: []resource.TestStep{ - { - Config: testAccCloudflareAccessBookmarkConfigBasic(rnd, domain, AccessIdentifier{Type: AccountType, Value: accountID}), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(name, "account_id", accountID), - resource.TestCheckResourceAttr(name, "name", rnd), - resource.TestCheckResourceAttr(name, "domain", fmt.Sprintf("%s.%s", rnd, domain)), - resource.TestCheckResourceAttr(name, "logo_url", "https://www.cloudflare.com/img/logo-web-badges/cf-logo-on-white-bg.svg"), - resource.TestCheckResourceAttr(name, "app_launcher_visible", "true"), - ), - }, - }, - }) -} - -func testAccCloudflareAccessBookmarkConfigBasic(rnd string, domain string, identifier AccessIdentifier) string { - return fmt.Sprintf(` -resource "cloudflare_access_bookmark" "%[1]s" { - %[3]s_id = "%[4]s" - name = "%[1]s" - domain = "%[1]s.%[2]s" - logo_url = "https://www.cloudflare.com/img/logo-web-badges/cf-logo-on-white-bg.svg" - app_launcher_visible = true -} -`, rnd, domain, identifier.Type, identifier.Value) -} - -func testAccCheckCloudflareAccessBookmarkDestroy(s *terraform.State) error { - client := testAccProvider.Meta().(*cloudflare.API) - - for _, rs := range s.RootModule().Resources { - if rs.Type != "cloudflare_access_bookmark" { - continue - } - - _, err := client.AccessBookmark(context.Background(), rs.Primary.Attributes["zone_id"], rs.Primary.ID) - if err == nil { - return fmt.Errorf("AccessBookmark still exists") - } - - _, err = client.AccessBookmark(context.Background(), rs.Primary.Attributes["account_id"], rs.Primary.ID) - if err == nil { - return fmt.Errorf("AccessBookmark still exists") - } - } - - return nil -} - -func TestAccCloudflareAccessBookmark_WithZoneID(t *testing.T) { - // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the Access - // service does not yet support the API tokens and it results in - // misleading state error messages. - if os.Getenv("CLOUDFLARE_API_TOKEN") != "" { - defer func(apiToken string) { - os.Setenv("CLOUDFLARE_API_TOKEN", apiToken) - }(os.Getenv("CLOUDFLARE_API_TOKEN")) - os.Setenv("CLOUDFLARE_API_TOKEN", "") - } - - rnd := generateRandomResourceName() - name := "cloudflare_access_bookmark." + rnd - zone := os.Getenv("CLOUDFLARE_DOMAIN") - zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") - updatedName := fmt.Sprintf("%s-updated", rnd) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { - testAccessAccPreCheck(t) - testAccPreCheckAccount(t) - }, - ProviderFactories: providerFactories, - Steps: []resource.TestStep{ - { - Config: testAccessBookmarkWithZoneID(rnd, zone, zoneID), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(name, "name", rnd), - resource.TestCheckResourceAttr(name, "zone_id", zoneID), - ), - }, - { - Config: testAccessBookmarkWithZoneIDUpdated(rnd, zone, zoneID), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(name, "name", updatedName), - resource.TestCheckResourceAttr(name, "zone_id", zoneID), - ), - }, - }, - }) -} - -func testAccessBookmarkWithZoneID(resourceID, zone, zoneID string) string { - return fmt.Sprintf(` - resource "cloudflare_access_bookmark" "%[1]s" { - name = "%[1]s" - zone_id = "%[3]s" - domain = "%[1]s.%[2]s" - logo_url = "https://image.com/img" - } - `, resourceID, zone, zoneID) -} - -func testAccessBookmarkWithZoneIDUpdated(resourceID, zone, zoneID string) string { - return fmt.Sprintf(` - resource "cloudflare_access_bookmark" "%[1]s" { - name = "%[1]s-updated" - zone_id = "%[3]s" - domain = "%[1]s.%[2]s" - logo_url = "https://image.com/img" - } - `, resourceID, zone, zoneID) -} diff --git a/internal/provider/schema_cloudflare_access_application.go b/internal/provider/schema_cloudflare_access_application.go index 60d041fa33..bd63655c2c 100644 --- a/internal/provider/schema_cloudflare_access_application.go +++ b/internal/provider/schema_cloudflare_access_application.go @@ -42,7 +42,14 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema { Type: schema.TypeString, Optional: true, Default: "self_hosted", - ValidateFunc: validation.StringInSlice([]string{"self_hosted", "ssh", "vnc", "file"}, false), + ValidateFunc: validation.StringInSlice([]string{"self_hosted", "ssh", "vnc", "file", "biso", "app_launcher", "warp", "bookmark", "saas"}, false), + }, + "saas_app" : { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, }, "session_duration": { Type: schema.TypeString, @@ -163,6 +170,37 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema { } } +func convertSaasSchemaToStruct(d *schema.ResourceData) (*cloudflare.SaasApplication, error) { + SaasConfig := cloudflare.SaasApplication{} + + if _, ok := d.GetOk("saas_app"); ok { + SaasConfig.ConsumerServiceUrl = d.Get("saas_app.consumer_service_url").(string) + SaasConfig.SPEntityID = d.Get("saas_app.sp_entity_id").(string) + SaasConfig.NameIDFormat = d.Get("saas_app.name_id_format").(string) + + customAttrs := d.Get("saas_app.custom_attributes").([]interface{}) + for _, value := range customAttrs { + if value != nil { + customAttrMap := value.(map[string]interface{}) + SaasConfig.CustomAttributes = append(SaasConfig.CustomAttributes, schemaAccessSaasAppCustomAttrToAPI(customAttrMap)) + } + } + } + return &SaasConfig, nil + } + +func schemaAccessSaasAppCustomAttrToAPI(data map[string]interface{}) cloudflare.SAMLAttributeConfig { + var customAttr cloudflare.SAMLAttributeConfig + + customAttr.Name = data["name"].(string) + customAttr.NameFormat, _ = data["name_format"].(string) + customAttr.FriendlyName, _ = data["friendly_name"].(string) + customAttr.Required, _ = data["required"].(bool) + customAttr.Source, _ = data["source"].(cloudflare.SourceConfig) + + return customAttr +} + func convertCORSSchemaToStruct(d *schema.ResourceData) (*cloudflare.AccessApplicationCorsHeaders, error) { CORSConfig := cloudflare.AccessApplicationCorsHeaders{}