Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AUTH-6588 Support Access apps destinations field #4661

Merged
merged 2 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/4649.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/cloudflare_authenticated_origin_pulls: Fix issue where resources are disabled instead of being destroyed on `tf destroy`
```
3 changes: 3 additions & 0 deletions .changelog/4661.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/access_application: add support for destinations and domain_type
```
16 changes: 15 additions & 1 deletion docs/resources/access_application.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ resource "cloudflare_zero_trust_access_application" "infra-app-example" {
- `custom_deny_url` (String) Option that redirects to a custom URL when a user is denied access to the application via identity based rules.
- `custom_non_identity_deny_url` (String) Option that redirects to a custom URL when a user is denied access to the application via non identity rules.
- `custom_pages` (Set of String) The custom pages selected for the application.
- `destinations` (Block List) A destination secured by Access. Only present for self_hosted, vnc, and ssh applications. Always includes the value set as `domain`. Supersedes `self_hosted_domains` to allow for more flexibility in defining different types of destinations. Conflicts with `self_hosted_domains`. (see [below for nested schema](#nestedblock--destinations))
- `domain` (String) The primary hostname and path that Access will secure. If the app is visible in the App Launcher dashboard, this is the domain that will be displayed.
- `domain_type` (String) The type of the primary domain. Available values: `public`, `private`.
- `enable_binding_cookie` (Boolean) Option to provide increased security against compromised authorization tokens and CSRF attacks by requiring an additional "binding" cookie on requests. Defaults to `false`.
- `footer_links` (Block Set) The footer links of the app launcher. (see [below for nested schema](#nestedblock--footer_links))
- `header_bg_color` (String) The background color of the header bar in the app launcher.
Expand All @@ -103,7 +105,7 @@ resource "cloudflare_zero_trust_access_application" "infra-app-example" {
- `saas_app` (Block List, Max: 1) SaaS configuration for the Access Application. (see [below for nested schema](#nestedblock--saas_app))
- `same_site_cookie_attribute` (String) Defines the same-site cookie setting for access tokens. Available values: `none`, `lax`, `strict`.
- `scim_config` (Block List, Max: 1) Configuration for provisioning to this application via SCIM. This is currently in closed beta. (see [below for nested schema](#nestedblock--scim_config))
- `self_hosted_domains` (Set of String) List of domains that access will secure. Only present for self_hosted, vnc, and ssh applications. Always includes the value set as `domain`.
- `self_hosted_domains` (Set of String, Deprecated) List of public domains secured by Access. Only present for self_hosted, vnc, and ssh applications. Always includes the value set as `domain`. Deprecated in favor of `destinations` and will be removed in the next major version. Conflicts with `destinations`.
- `service_auth_401_redirect` (Boolean) Option to return a 401 status code in service authentication rules on failed requests. Defaults to `false`.
- `session_duration` (String) How often a user will be forced to re-authorise. Must be in the format `48h` or `2h45m`. Defaults to `24h`.
- `skip_app_launcher_login_page` (Boolean) Option to skip the App Launcher landing page. Defaults to `false`.
Expand Down Expand Up @@ -133,6 +135,18 @@ Optional:
- `max_age` (Number) The maximum time a preflight request will be cached.


<a id="nestedblock--destinations"></a>
### Nested Schema for `destinations`

Required:

- `uri` (String) The URI of the destination. Public destinations can include a domain and path with wildcards. Private destinations are an early access feature and gated behind a feature flag. Private destinations support private IPv4, IPv6, and Server Name Indications (SNI) with optional port ranges.

Optional:

- `type` (String) The destination type. Available values: `public`, `private`. Defaults to `public`.


<a id="nestedblock--footer_links"></a>
### Nested Schema for `footer_links`

Expand Down
16 changes: 15 additions & 1 deletion docs/resources/zero_trust_access_application.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ resource "cloudflare_zero_trust_access_application" "staging_app" {
- `custom_deny_url` (String) Option that redirects to a custom URL when a user is denied access to the application via identity based rules.
- `custom_non_identity_deny_url` (String) Option that redirects to a custom URL when a user is denied access to the application via non identity rules.
- `custom_pages` (Set of String) The custom pages selected for the application.
- `destinations` (Block List) A destination secured by Access. Only present for self_hosted, vnc, and ssh applications. Always includes the value set as `domain`. Supersedes `self_hosted_domains` to allow for more flexibility in defining different types of destinations. Conflicts with `self_hosted_domains`. (see [below for nested schema](#nestedblock--destinations))
- `domain` (String) The primary hostname and path that Access will secure. If the app is visible in the App Launcher dashboard, this is the domain that will be displayed.
- `domain_type` (String) The type of the primary domain. Available values: `public`, `private`.
- `enable_binding_cookie` (Boolean) Option to provide increased security against compromised authorization tokens and CSRF attacks by requiring an additional "binding" cookie on requests. Defaults to `false`.
- `footer_links` (Block Set) The footer links of the app launcher. (see [below for nested schema](#nestedblock--footer_links))
- `header_bg_color` (String) The background color of the header bar in the app launcher.
Expand All @@ -84,7 +86,7 @@ resource "cloudflare_zero_trust_access_application" "staging_app" {
- `saas_app` (Block List, Max: 1) SaaS configuration for the Access Application. (see [below for nested schema](#nestedblock--saas_app))
- `same_site_cookie_attribute` (String) Defines the same-site cookie setting for access tokens. Available values: `none`, `lax`, `strict`.
- `scim_config` (Block List, Max: 1) Configuration for provisioning to this application via SCIM. This is currently in closed beta. (see [below for nested schema](#nestedblock--scim_config))
- `self_hosted_domains` (Set of String) List of domains that access will secure. Only present for self_hosted, vnc, and ssh applications. Always includes the value set as `domain`.
- `self_hosted_domains` (Set of String, Deprecated) List of public domains secured by Access. Only present for self_hosted, vnc, and ssh applications. Always includes the value set as `domain`. Deprecated in favor of `destinations` and will be removed in the next major version. Conflicts with `destinations`.
- `service_auth_401_redirect` (Boolean) Option to return a 401 status code in service authentication rules on failed requests. Defaults to `false`.
- `session_duration` (String) How often a user will be forced to re-authorise. Must be in the format `48h` or `2h45m`. Defaults to `24h`.
- `skip_app_launcher_login_page` (Boolean) Option to skip the App Launcher landing page. Defaults to `false`.
Expand Down Expand Up @@ -114,6 +116,18 @@ Optional:
- `max_age` (Number) The maximum time a preflight request will be cached.


<a id="nestedblock--destinations"></a>
### Nested Schema for `destinations`

Required:

- `uri` (String) The URI of the destination. Public destinations can include a domain and path with wildcards. Private destinations are an early access feature and gated behind a feature flag. Private destinations support private IPv4, IPv6, and Server Name Indications (SNI) with optional port ranges.

Optional:

- `type` (String) The destination type. Available values: `public`, `private`. Defaults to `public`.


<a id="nestedblock--footer_links"></a>
### Nested Schema for `footer_links`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,23 @@ func resourceCloudflareAccessApplicationCreate(ctx context.Context, d *schema.Re
}

if value, ok := d.GetOk("self_hosted_domains"); ok {
newAccessApplication.SelfHostedDomains = expandInterfaceToStringList(value.(*schema.Set).List())
selfHostedDomains := expandInterfaceToStringList(value.(*schema.Set).List())
destinations := make([]cloudflare.AccessDestination, len(selfHostedDomains))
for i, uri := range selfHostedDomains {
destinations[i] = cloudflare.AccessDestination{
Type: cloudflare.AccessDestinationPublic,
URI: uri,
}
}
newAccessApplication.Destinations = destinations
}

if value, ok := d.GetOk("destinations"); ok {
destinations, err := convertDestinationsToStruct(value.([]interface{}))
if err != nil {
return diag.FromErr(err)
}
newAccessApplication.Destinations = destinations
}

if _, ok := d.GetOk("cors_headers"); ok {
Expand Down Expand Up @@ -258,7 +274,17 @@ func resourceCloudflareAccessApplicationRead(ctx context.Context, d *schema.Reso
}

if _, ok := d.GetOk("self_hosted_domains"); ok {
d.Set("self_hosted_domains", accessApplication.SelfHostedDomains)
publicDomains := make([]string, 0, len(accessApplication.Destinations))
for _, dest := range accessApplication.Destinations {
if dest.Type == cloudflare.AccessDestinationPublic {
publicDomains = append(publicDomains, dest.URI)
}
}
d.Set("self_hosted_domains", publicDomains)
}

if _, ok := d.GetOk("destinations"); ok {
d.Set("destinations", convertDestinationsToSchema(accessApplication.Destinations))
}

scimConfig := convertScimConfigStructToSchema(accessApplication.SCIMConfig)
Expand Down Expand Up @@ -320,7 +346,23 @@ func resourceCloudflareAccessApplicationUpdate(ctx context.Context, d *schema.Re
}

if value, ok := d.GetOk("self_hosted_domains"); ok {
updatedAccessApplication.SelfHostedDomains = expandInterfaceToStringList(value.(*schema.Set).List())
selfHostedDomains := expandInterfaceToStringList(value.(*schema.Set).List())
destinations := make([]cloudflare.AccessDestination, len(selfHostedDomains))
for i, uri := range selfHostedDomains {
destinations[i] = cloudflare.AccessDestination{
Type: cloudflare.AccessDestinationPublic,
URI: uri,
}
}
updatedAccessApplication.Destinations = destinations
}

if value, ok := d.GetOk("destinations"); ok {
destinations, err := convertDestinationsToStruct(value.([]interface{}))
if err != nil {
return diag.FromErr(err)
}
updatedAccessApplication.Destinations = destinations
}

if d.HasChange("policies") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,58 @@ func TestAccCloudflareAccessApplication_WithTargetContexts(t *testing.T) {
})
}

func TestAccCloudflareAccessApplication_WithDestinations(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_zero_trust_access_application.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
testAccPreCheckAccount(t)
},
ProviderFactories: providerFactories,
CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudflareAccessApplicationWithDestinations(rnd, domain, cloudflare.AccountIdentifier(accountID)),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "destinations.#", "2"),
resource.TestCheckResourceAttr(name, "destinations.0.type", "public"),
resource.TestCheckResourceAttr(name, "destinations.0.uri", fmt.Sprintf("d1.%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "destinations.1.type", "public"),
resource.TestCheckResourceAttr(name, "destinations.1.uri", fmt.Sprintf("d2.%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "type", "self_hosted"),
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
resource.TestCheckResourceAttr(name, "cors_headers.#", "0"),
resource.TestCheckResourceAttr(name, "sass_app.#", "0"),
resource.TestCheckResourceAttr(name, "auto_redirect_to_identity", "false"),
),
},
{
Config: testAccCloudflareAccessApplicationWithDestinations2(rnd, domain, cloudflare.AccountIdentifier(accountID)),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "destinations.#", "2"),
resource.TestCheckResourceAttr(name, "destinations.0.type", "public"),
resource.TestCheckResourceAttr(name, "destinations.0.uri", fmt.Sprintf("d3.%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "destinations.1.type", "public"),
resource.TestCheckResourceAttr(name, "destinations.1.uri", fmt.Sprintf("d4.%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "type", "self_hosted"),
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
resource.TestCheckResourceAttr(name, "cors_headers.#", "0"),
resource.TestCheckResourceAttr(name, "sass_app.#", "0"),
resource.TestCheckResourceAttr(name, "auto_redirect_to_identity", "false"),
),
},
},
})
}

func TestAccCloudflareAccessApplication_WithSelfHostedDomains(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_zero_trust_access_application.%s", rnd)
Expand Down Expand Up @@ -1443,6 +1495,42 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" {
`, rnd, domain, identifier.Type, identifier.Identifier)
}

func testAccCloudflareAccessApplicationWithDestinations(rnd string, domain string, identifier *cloudflare.ResourceContainer) string {
return fmt.Sprintf(`
resource "cloudflare_zero_trust_access_application" "%[1]s" {
%[3]s_id = "%[4]s"
name = "%[1]s"
type = "self_hosted"
session_duration = "24h"
auto_redirect_to_identity = false
destinations {
uri = "d1.%[1]s.%[2]s"
}
destinations {
uri = "d2.%[1]s.%[2]s"
}
}
`, rnd, domain, identifier.Type, identifier.Identifier)
}

func testAccCloudflareAccessApplicationWithDestinations2(rnd string, domain string, identifier *cloudflare.ResourceContainer) string {
return fmt.Sprintf(`
resource "cloudflare_zero_trust_access_application" "%[1]s" {
%[3]s_id = "%[4]s"
name = "%[1]s"
type = "self_hosted"
session_duration = "24h"
auto_redirect_to_identity = false
destinations {
uri = "d3.%[1]s.%[2]s"
}
destinations {
uri = "d4.%[1]s.%[2]s"
}
}
`, rnd, domain, identifier.Type, identifier.Identifier)
}

func testAccCloudflareAccessApplicationWithSelfHostedDomains(rnd string, domain string, identifier *cloudflare.ResourceContainer) string {
return fmt.Sprintf(`
resource "cloudflare_zero_trust_access_application" "%[1]s" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func resourceCloudflareAuthenticatedOriginPullsCreate(ctx context.Context, d *sc
conf := []cloudflare.PerHostnameAuthenticatedOriginPullsConfig{{
CertID: aopCert,
Hostname: hostname,
Enabled: isEnabled,
Enabled: &isEnabled,
}}
_, err := client.EditPerHostnameAuthenticatedOriginPullsConfig(ctx, zoneID, conf)
if err != nil {
Expand Down Expand Up @@ -123,7 +123,7 @@ func resourceCloudflareAuthenticatedOriginPullsDelete(ctx context.Context, d *sc
conf := []cloudflare.PerHostnameAuthenticatedOriginPullsConfig{{
CertID: aopCert,
Hostname: hostname,
Enabled: false,
Enabled: nil,
}}
_, err := client.EditPerHostnameAuthenticatedOriginPullsConfig(ctx, zoneID, conf)
if err != nil {
Expand Down
81 changes: 78 additions & 3 deletions internal/sdkv2provider/schema_cloudflare_access_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,53 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
return oldValue == newValue
},
},
"domain_type": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.StringInSlice([]string{"public", "private"}, false),
Description: fmt.Sprintf("The type of the primary domain. %s", renderAvailableDocumentationValuesStringSlice([]string{"public", "private"})),
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
appType := d.Get("type").(string)
// Suppress the diff if it's an app type that doesn't need a `domain` value.
if appType == "infrastructure" {
return true
}

return oldValue == newValue
},
},
"destinations": {
Copy link
Author

@veggiedefender veggiedefender Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be named destination (singular instead of plural) instead? It'll be used like this:

destinations {
  uri = "example.com"
}
destinations {
  uri = "foo.example.com"
}
destinations {
  type = "private"
  uri = "10.116.0.4"
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw the Attributes as Blocks page but it's kind of confusing to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type: schema.TypeList,
Optional: true,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be Computed as well? It's unclear what that actually does 🧐

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it probably shouldn't be computed then, thanks!

ConflictsWith: []string{"self_hosted_domains"},
Description: "A destination secured by Access. Only present for self_hosted, vnc, and ssh applications. Always includes the value set as `domain`. Supersedes `self_hosted_domains` to allow for more flexibility in defining different types of destinations.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": {
Type: schema.TypeString,
Default: "public",
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"public", "private"}, false),
Description: fmt.Sprintf("The destination type. %s", renderAvailableDocumentationValuesStringSlice([]string{"public", "private"})),
},
"uri": {
Type: schema.TypeString,
Required: true,
Description: "The URI of the destination. Public destinations can include a domain and path with wildcards. Private destinations are an early access feature and gated behind a feature flag. Private destinations support private IPv4, IPv6, and Server Name Indications (SNI) with optional port ranges.",
},
},
},
},
"self_hosted_domains": {
Type: schema.TypeSet,
Optional: true,
Type: schema.TypeSet,
Optional: true,
ConflictsWith: []string{"destinations"},
Elem: &schema.Schema{
Type: schema.TypeString,
},
Description: "List of domains that access will secure. Only present for self_hosted, vnc, and ssh applications. Always includes the value set as `domain`",
Description: "List of public domains secured by Access. Only present for self_hosted, vnc, and ssh applications. Always includes the value set as `domain`. Deprecated in favor of `destinations` and will be removed in the next major version.",
Deprecated: "Use `destinations` instead",
},
"type": {
Type: schema.TypeString,
Expand Down Expand Up @@ -997,6 +1037,30 @@ func convertSaasSchemaToStruct(d *schema.ResourceData) *cloudflare.SaasApplicati
return &SaasConfig
}

func convertDestinationsToStruct(destinationPayloads []interface{}) ([]cloudflare.AccessDestination, error) {
destinations := make([]cloudflare.AccessDestination, len(destinationPayloads))
for i, dp := range destinationPayloads {
dpMap := dp.(map[string]interface{})

if dType, ok := dpMap["type"].(string); ok {
switch dType {
case "public":
destinations[i].Type = cloudflare.AccessDestinationPublic
case "private":
destinations[i].Type = cloudflare.AccessDestinationPrivate
default:
return nil, fmt.Errorf("failed to parse destination type: value must be one of public or private")
}
}

if uri, ok := dpMap["uri"].(string); ok {
destinations[i].URI = uri
}
}

return destinations, nil
}

func convertTargetContextsToStruct(d *schema.ResourceData) (*[]cloudflare.AccessInfrastructureTargetContext, error) {
TargetContexts := []cloudflare.AccessInfrastructureTargetContext{}
if value, ok := d.GetOk("target_criteria"); ok {
Expand Down Expand Up @@ -1447,3 +1511,14 @@ func convertScimConfigMappingsStructsToSchema(mappingsData []*cloudflare.AccessA

return mappings
}

func convertDestinationsToSchema(destinations []cloudflare.AccessDestination) []interface{} {
schemas := make([]interface{}, len(destinations))
for i, dest := range destinations {
schemas[i] = map[string]interface{}{
"type": string(dest.Type),
"uri": dest.URI,
}
}
return schemas
}
Loading