Skip to content

Commit 6576989

Browse files
authored
Merge pull request #4661 from veggiedefender/jesse/destinations
AUTH-6588 Support Access apps `destinations` field
2 parents d61ee7a + 937a007 commit 6576989

8 files changed

+249
-10
lines changed

.changelog/4649.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
resource/cloudflare_authenticated_origin_pulls: Fix issue where resources are disabled instead of being destroyed on `tf destroy`
3+
```

.changelog/4661.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/access_application: add support for destinations and domain_type
3+
```

docs/resources/access_application.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ resource "cloudflare_zero_trust_access_application" "infra-app-example" {
9090
- `custom_deny_url` (String) Option that redirects to a custom URL when a user is denied access to the application via identity based rules.
9191
- `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.
9292
- `custom_pages` (Set of String) The custom pages selected for the application.
93+
- `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))
9394
- `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.
95+
- `domain_type` (String) The type of the primary domain. Available values: `public`, `private`.
9496
- `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`.
9597
- `footer_links` (Block Set) The footer links of the app launcher. (see [below for nested schema](#nestedblock--footer_links))
9698
- `header_bg_color` (String) The background color of the header bar in the app launcher.
@@ -103,7 +105,7 @@ resource "cloudflare_zero_trust_access_application" "infra-app-example" {
103105
- `saas_app` (Block List, Max: 1) SaaS configuration for the Access Application. (see [below for nested schema](#nestedblock--saas_app))
104106
- `same_site_cookie_attribute` (String) Defines the same-site cookie setting for access tokens. Available values: `none`, `lax`, `strict`.
105107
- `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))
106-
- `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`.
108+
- `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`.
107109
- `service_auth_401_redirect` (Boolean) Option to return a 401 status code in service authentication rules on failed requests. Defaults to `false`.
108110
- `session_duration` (String) How often a user will be forced to re-authorise. Must be in the format `48h` or `2h45m`. Defaults to `24h`.
109111
- `skip_app_launcher_login_page` (Boolean) Option to skip the App Launcher landing page. Defaults to `false`.
@@ -133,6 +135,18 @@ Optional:
133135
- `max_age` (Number) The maximum time a preflight request will be cached.
134136

135137

138+
<a id="nestedblock--destinations"></a>
139+
### Nested Schema for `destinations`
140+
141+
Required:
142+
143+
- `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.
144+
145+
Optional:
146+
147+
- `type` (String) The destination type. Available values: `public`, `private`. Defaults to `public`.
148+
149+
136150
<a id="nestedblock--footer_links"></a>
137151
### Nested Schema for `footer_links`
138152

docs/resources/zero_trust_access_application.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ resource "cloudflare_zero_trust_access_application" "staging_app" {
7171
- `custom_deny_url` (String) Option that redirects to a custom URL when a user is denied access to the application via identity based rules.
7272
- `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.
7373
- `custom_pages` (Set of String) The custom pages selected for the application.
74+
- `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))
7475
- `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.
76+
- `domain_type` (String) The type of the primary domain. Available values: `public`, `private`.
7577
- `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`.
7678
- `footer_links` (Block Set) The footer links of the app launcher. (see [below for nested schema](#nestedblock--footer_links))
7779
- `header_bg_color` (String) The background color of the header bar in the app launcher.
@@ -84,7 +86,7 @@ resource "cloudflare_zero_trust_access_application" "staging_app" {
8486
- `saas_app` (Block List, Max: 1) SaaS configuration for the Access Application. (see [below for nested schema](#nestedblock--saas_app))
8587
- `same_site_cookie_attribute` (String) Defines the same-site cookie setting for access tokens. Available values: `none`, `lax`, `strict`.
8688
- `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))
87-
- `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`.
89+
- `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`.
8890
- `service_auth_401_redirect` (Boolean) Option to return a 401 status code in service authentication rules on failed requests. Defaults to `false`.
8991
- `session_duration` (String) How often a user will be forced to re-authorise. Must be in the format `48h` or `2h45m`. Defaults to `24h`.
9092
- `skip_app_launcher_login_page` (Boolean) Option to skip the App Launcher landing page. Defaults to `false`.
@@ -114,6 +116,18 @@ Optional:
114116
- `max_age` (Number) The maximum time a preflight request will be cached.
115117

116118

119+
<a id="nestedblock--destinations"></a>
120+
### Nested Schema for `destinations`
121+
122+
Required:
123+
124+
- `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.
125+
126+
Optional:
127+
128+
- `type` (String) The destination type. Available values: `public`, `private`. Defaults to `public`.
129+
130+
117131
<a id="nestedblock--footer_links"></a>
118132
### Nested Schema for `footer_links`
119133

internal/sdkv2provider/resource_cloudflare_access_application.go

+45-3
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,23 @@ func resourceCloudflareAccessApplicationCreate(ctx context.Context, d *schema.Re
8888
}
8989

9090
if value, ok := d.GetOk("self_hosted_domains"); ok {
91-
newAccessApplication.SelfHostedDomains = expandInterfaceToStringList(value.(*schema.Set).List())
91+
selfHostedDomains := expandInterfaceToStringList(value.(*schema.Set).List())
92+
destinations := make([]cloudflare.AccessDestination, len(selfHostedDomains))
93+
for i, uri := range selfHostedDomains {
94+
destinations[i] = cloudflare.AccessDestination{
95+
Type: cloudflare.AccessDestinationPublic,
96+
URI: uri,
97+
}
98+
}
99+
newAccessApplication.Destinations = destinations
100+
}
101+
102+
if value, ok := d.GetOk("destinations"); ok {
103+
destinations, err := convertDestinationsToStruct(value.([]interface{}))
104+
if err != nil {
105+
return diag.FromErr(err)
106+
}
107+
newAccessApplication.Destinations = destinations
92108
}
93109

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

260276
if _, ok := d.GetOk("self_hosted_domains"); ok {
261-
d.Set("self_hosted_domains", accessApplication.SelfHostedDomains)
277+
publicDomains := make([]string, 0, len(accessApplication.Destinations))
278+
for _, dest := range accessApplication.Destinations {
279+
if dest.Type == cloudflare.AccessDestinationPublic {
280+
publicDomains = append(publicDomains, dest.URI)
281+
}
282+
}
283+
d.Set("self_hosted_domains", publicDomains)
284+
}
285+
286+
if _, ok := d.GetOk("destinations"); ok {
287+
d.Set("destinations", convertDestinationsToSchema(accessApplication.Destinations))
262288
}
263289

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

322348
if value, ok := d.GetOk("self_hosted_domains"); ok {
323-
updatedAccessApplication.SelfHostedDomains = expandInterfaceToStringList(value.(*schema.Set).List())
349+
selfHostedDomains := expandInterfaceToStringList(value.(*schema.Set).List())
350+
destinations := make([]cloudflare.AccessDestination, len(selfHostedDomains))
351+
for i, uri := range selfHostedDomains {
352+
destinations[i] = cloudflare.AccessDestination{
353+
Type: cloudflare.AccessDestinationPublic,
354+
URI: uri,
355+
}
356+
}
357+
updatedAccessApplication.Destinations = destinations
358+
}
359+
360+
if value, ok := d.GetOk("destinations"); ok {
361+
destinations, err := convertDestinationsToStruct(value.([]interface{}))
362+
if err != nil {
363+
return diag.FromErr(err)
364+
}
365+
updatedAccessApplication.Destinations = destinations
324366
}
325367

326368
if d.HasChange("policies") {

internal/sdkv2provider/resource_cloudflare_access_application_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,58 @@ func TestAccCloudflareAccessApplication_WithTargetContexts(t *testing.T) {
948948
})
949949
}
950950

951+
func TestAccCloudflareAccessApplication_WithDestinations(t *testing.T) {
952+
rnd := generateRandomResourceName()
953+
name := fmt.Sprintf("cloudflare_zero_trust_access_application.%s", rnd)
954+
955+
resource.Test(t, resource.TestCase{
956+
PreCheck: func() {
957+
testAccPreCheck(t)
958+
testAccPreCheckAccount(t)
959+
},
960+
ProviderFactories: providerFactories,
961+
CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy,
962+
Steps: []resource.TestStep{
963+
{
964+
Config: testAccCloudflareAccessApplicationWithDestinations(rnd, domain, cloudflare.AccountIdentifier(accountID)),
965+
Check: resource.ComposeTestCheckFunc(
966+
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
967+
resource.TestCheckResourceAttr(name, "name", rnd),
968+
resource.TestCheckResourceAttr(name, "destinations.#", "2"),
969+
resource.TestCheckResourceAttr(name, "destinations.0.type", "public"),
970+
resource.TestCheckResourceAttr(name, "destinations.0.uri", fmt.Sprintf("d1.%s.%s", rnd, domain)),
971+
resource.TestCheckResourceAttr(name, "destinations.1.type", "public"),
972+
resource.TestCheckResourceAttr(name, "destinations.1.uri", fmt.Sprintf("d2.%s.%s", rnd, domain)),
973+
resource.TestCheckResourceAttr(name, "name", rnd),
974+
resource.TestCheckResourceAttr(name, "type", "self_hosted"),
975+
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
976+
resource.TestCheckResourceAttr(name, "cors_headers.#", "0"),
977+
resource.TestCheckResourceAttr(name, "sass_app.#", "0"),
978+
resource.TestCheckResourceAttr(name, "auto_redirect_to_identity", "false"),
979+
),
980+
},
981+
{
982+
Config: testAccCloudflareAccessApplicationWithDestinations2(rnd, domain, cloudflare.AccountIdentifier(accountID)),
983+
Check: resource.ComposeTestCheckFunc(
984+
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
985+
resource.TestCheckResourceAttr(name, "name", rnd),
986+
resource.TestCheckResourceAttr(name, "destinations.#", "2"),
987+
resource.TestCheckResourceAttr(name, "destinations.0.type", "public"),
988+
resource.TestCheckResourceAttr(name, "destinations.0.uri", fmt.Sprintf("d3.%s.%s", rnd, domain)),
989+
resource.TestCheckResourceAttr(name, "destinations.1.type", "public"),
990+
resource.TestCheckResourceAttr(name, "destinations.1.uri", fmt.Sprintf("d4.%s.%s", rnd, domain)),
991+
resource.TestCheckResourceAttr(name, "name", rnd),
992+
resource.TestCheckResourceAttr(name, "type", "self_hosted"),
993+
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
994+
resource.TestCheckResourceAttr(name, "cors_headers.#", "0"),
995+
resource.TestCheckResourceAttr(name, "sass_app.#", "0"),
996+
resource.TestCheckResourceAttr(name, "auto_redirect_to_identity", "false"),
997+
),
998+
},
999+
},
1000+
})
1001+
}
1002+
9511003
func TestAccCloudflareAccessApplication_WithSelfHostedDomains(t *testing.T) {
9521004
rnd := generateRandomResourceName()
9531005
name := fmt.Sprintf("cloudflare_zero_trust_access_application.%s", rnd)
@@ -1443,6 +1495,42 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" {
14431495
`, rnd, domain, identifier.Type, identifier.Identifier)
14441496
}
14451497

1498+
func testAccCloudflareAccessApplicationWithDestinations(rnd string, domain string, identifier *cloudflare.ResourceContainer) string {
1499+
return fmt.Sprintf(`
1500+
resource "cloudflare_zero_trust_access_application" "%[1]s" {
1501+
%[3]s_id = "%[4]s"
1502+
name = "%[1]s"
1503+
type = "self_hosted"
1504+
session_duration = "24h"
1505+
auto_redirect_to_identity = false
1506+
destinations {
1507+
uri = "d1.%[1]s.%[2]s"
1508+
}
1509+
destinations {
1510+
uri = "d2.%[1]s.%[2]s"
1511+
}
1512+
}
1513+
`, rnd, domain, identifier.Type, identifier.Identifier)
1514+
}
1515+
1516+
func testAccCloudflareAccessApplicationWithDestinations2(rnd string, domain string, identifier *cloudflare.ResourceContainer) string {
1517+
return fmt.Sprintf(`
1518+
resource "cloudflare_zero_trust_access_application" "%[1]s" {
1519+
%[3]s_id = "%[4]s"
1520+
name = "%[1]s"
1521+
type = "self_hosted"
1522+
session_duration = "24h"
1523+
auto_redirect_to_identity = false
1524+
destinations {
1525+
uri = "d3.%[1]s.%[2]s"
1526+
}
1527+
destinations {
1528+
uri = "d4.%[1]s.%[2]s"
1529+
}
1530+
}
1531+
`, rnd, domain, identifier.Type, identifier.Identifier)
1532+
}
1533+
14461534
func testAccCloudflareAccessApplicationWithSelfHostedDomains(rnd string, domain string, identifier *cloudflare.ResourceContainer) string {
14471535
return fmt.Sprintf(`
14481536
resource "cloudflare_zero_trust_access_application" "%[1]s" {

internal/sdkv2provider/resource_cloudflare_authenticated_origin_pulls.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func resourceCloudflareAuthenticatedOriginPullsCreate(ctx context.Context, d *sc
4848
conf := []cloudflare.PerHostnameAuthenticatedOriginPullsConfig{{
4949
CertID: aopCert,
5050
Hostname: hostname,
51-
Enabled: isEnabled,
51+
Enabled: &isEnabled,
5252
}}
5353
_, err := client.EditPerHostnameAuthenticatedOriginPullsConfig(ctx, zoneID, conf)
5454
if err != nil {
@@ -123,7 +123,7 @@ func resourceCloudflareAuthenticatedOriginPullsDelete(ctx context.Context, d *sc
123123
conf := []cloudflare.PerHostnameAuthenticatedOriginPullsConfig{{
124124
CertID: aopCert,
125125
Hostname: hostname,
126-
Enabled: false,
126+
Enabled: nil,
127127
}}
128128
_, err := client.EditPerHostnameAuthenticatedOriginPullsConfig(ctx, zoneID, conf)
129129
if err != nil {

internal/sdkv2provider/schema_cloudflare_access_application.go

+78-3
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,53 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
5454
return oldValue == newValue
5555
},
5656
},
57+
"domain_type": {
58+
Type: schema.TypeString,
59+
Optional: true,
60+
Computed: true,
61+
ValidateFunc: validation.StringInSlice([]string{"public", "private"}, false),
62+
Description: fmt.Sprintf("The type of the primary domain. %s", renderAvailableDocumentationValuesStringSlice([]string{"public", "private"})),
63+
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
64+
appType := d.Get("type").(string)
65+
// Suppress the diff if it's an app type that doesn't need a `domain` value.
66+
if appType == "infrastructure" {
67+
return true
68+
}
69+
70+
return oldValue == newValue
71+
},
72+
},
73+
"destinations": {
74+
Type: schema.TypeList,
75+
Optional: true,
76+
ConflictsWith: []string{"self_hosted_domains"},
77+
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.",
78+
Elem: &schema.Resource{
79+
Schema: map[string]*schema.Schema{
80+
"type": {
81+
Type: schema.TypeString,
82+
Default: "public",
83+
Optional: true,
84+
ValidateFunc: validation.StringInSlice([]string{"public", "private"}, false),
85+
Description: fmt.Sprintf("The destination type. %s", renderAvailableDocumentationValuesStringSlice([]string{"public", "private"})),
86+
},
87+
"uri": {
88+
Type: schema.TypeString,
89+
Required: true,
90+
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.",
91+
},
92+
},
93+
},
94+
},
5795
"self_hosted_domains": {
58-
Type: schema.TypeSet,
59-
Optional: true,
96+
Type: schema.TypeSet,
97+
Optional: true,
98+
ConflictsWith: []string{"destinations"},
6099
Elem: &schema.Schema{
61100
Type: schema.TypeString,
62101
},
63-
Description: "List of domains that access will secure. Only present for self_hosted, vnc, and ssh applications. Always includes the value set as `domain`",
102+
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.",
103+
Deprecated: "Use `destinations` instead",
64104
},
65105
"type": {
66106
Type: schema.TypeString,
@@ -997,6 +1037,30 @@ func convertSaasSchemaToStruct(d *schema.ResourceData) *cloudflare.SaasApplicati
9971037
return &SaasConfig
9981038
}
9991039

1040+
func convertDestinationsToStruct(destinationPayloads []interface{}) ([]cloudflare.AccessDestination, error) {
1041+
destinations := make([]cloudflare.AccessDestination, len(destinationPayloads))
1042+
for i, dp := range destinationPayloads {
1043+
dpMap := dp.(map[string]interface{})
1044+
1045+
if dType, ok := dpMap["type"].(string); ok {
1046+
switch dType {
1047+
case "public":
1048+
destinations[i].Type = cloudflare.AccessDestinationPublic
1049+
case "private":
1050+
destinations[i].Type = cloudflare.AccessDestinationPrivate
1051+
default:
1052+
return nil, fmt.Errorf("failed to parse destination type: value must be one of public or private")
1053+
}
1054+
}
1055+
1056+
if uri, ok := dpMap["uri"].(string); ok {
1057+
destinations[i].URI = uri
1058+
}
1059+
}
1060+
1061+
return destinations, nil
1062+
}
1063+
10001064
func convertTargetContextsToStruct(d *schema.ResourceData) (*[]cloudflare.AccessInfrastructureTargetContext, error) {
10011065
TargetContexts := []cloudflare.AccessInfrastructureTargetContext{}
10021066
if value, ok := d.GetOk("target_criteria"); ok {
@@ -1447,3 +1511,14 @@ func convertScimConfigMappingsStructsToSchema(mappingsData []*cloudflare.AccessA
14471511

14481512
return mappings
14491513
}
1514+
1515+
func convertDestinationsToSchema(destinations []cloudflare.AccessDestination) []interface{} {
1516+
schemas := make([]interface{}, len(destinations))
1517+
for i, dest := range destinations {
1518+
schemas[i] = map[string]interface{}{
1519+
"type": string(dest.Type),
1520+
"uri": dest.URI,
1521+
}
1522+
}
1523+
return schemas
1524+
}

0 commit comments

Comments
 (0)