diff --git a/.changelog/4128.txt b/.changelog/4128.txt
new file mode 100644
index 0000000000..da520f8332
--- /dev/null
+++ b/.changelog/4128.txt
@@ -0,0 +1,7 @@
+```release-note:enhancement
+resource/cloudflare_access_application: added target contexts support for access application type infrastructure
+```
+
+```release-note:enhancement
+resource/cloudflare_access_policy: added infrastructure connection rule support for access policy
+```
diff --git a/docs/resources/access_application.md b/docs/resources/access_application.md
index a8d44fb735..38db3a0938 100644
--- a/docs/resources/access_application.md
+++ b/docs/resources/access_application.md
@@ -53,6 +53,25 @@ resource "cloudflare_access_application" "staging_app" {
max_age = 10
}
}
+
+# Infrastructure application configuration
+resource "cloudflare_zero_trust_access_application" "infra-app-example" {
+ account_id = "0da42c8d2132a9ddaf714f9e7c920711"
+ name = "infra-app"
+ type = "infrastructure"
+
+ target_criteria {
+ port = 22
+ protocol = "SSH"
+ target_attributes {
+ name = "hostname"
+ values = ["tfgo-tests-useast", "tfgo-tests-uswest"]
+ }
+ }
+
+ # specify existing access policies by id
+ policies = []
+}
```
## Schema
@@ -90,7 +109,8 @@ resource "cloudflare_access_application" "staging_app" {
- `skip_app_launcher_login_page` (Boolean) Option to skip the App Launcher landing page. Defaults to `false`.
- `skip_interstitial` (Boolean) Option to skip the authorization interstitial when using the CLI. Defaults to `false`.
- `tags` (Set of String) The itags associated with the application.
-- `type` (String) The application type. Available values: `app_launcher`, `bookmark`, `biso`, `dash_sso`, `saas`, `self_hosted`, `ssh`, `vnc`, `warp`. Defaults to `self_hosted`.
+- `target_criteria` (Block List) A list of mappings to apply to SCIM resources before provisioning them in this application. These can transform or filter the resources to be provisioned. (see [below for nested schema](#nestedblock--target_criteria))
+- `type` (String) The application type. Available values: `app_launcher`, `bookmark`, `biso`, `dash_sso`, `saas`, `self_hosted`, `ssh`, `vnc`, `warp`, `infrastructure`. Defaults to `self_hosted`.
- `zone_id` (String) The zone identifier to target for the resource. Conflicts with `account_id`.
### Read-Only
@@ -294,6 +314,26 @@ Optional:
- `delete` (Boolean) Whether or not this mapping applies to DELETE operations.
- `update` (Boolean) Whether or not this mapping applies to update (PATCH/PUT) operations.
+
+
+
+
+### Nested Schema for `target_criteria`
+
+Required:
+
+- `port` (Number) The port that the targets use for the chosen communication protocol. A port cannot be assigned to multiple protocols.
+- `protocol` (String) The communication protocol your application secures.
+- `target_attributes` (Block List, Min: 1) Contains a map of target attribute keys to target attribute values. (see [below for nested schema](#nestedblock--target_criteria--target_attributes))
+
+
+### Nested Schema for `target_criteria.target_attributes`
+
+Required:
+
+- `name` (String) The key of the attribute.
+- `values` (List of String) The values of the attribute.
+
## Import
Import is supported using the following syntax:
diff --git a/docs/resources/access_policy.md b/docs/resources/access_policy.md
index ed247a374c..78d958be54 100644
--- a/docs/resources/access_policy.md
+++ b/docs/resources/access_policy.md
@@ -54,6 +54,44 @@ resource "cloudflare_access_policy" "test_policy" {
ip = [var.office_ip]
}
}
+
+# Access policy for an infrastructure application
+resource "cloudflare_access_policy" "infra-app-example-allow" {
+ application_id = cloudflare_zero_trust_access_application.infra-app-example.id
+ account_id = "0da42c8d2132a9ddaf714f9e7c920711"
+ name = "infra-app-example-allow"
+ decision = "allow"
+ precedence = 1
+
+ include {
+ email = ["devuser@gmail.com"]
+ }
+
+ connection_rules {
+ ssh {
+ usernames = ["ec2-user"]
+ }
+ }
+}
+
+# Infrastructure application configuration for infra-app-example-allow
+resource "cloudflare_zero_trust_access_application" "infra-app-example" {
+ account_id = "0da42c8d2132a9ddaf714f9e7c920711"
+ name = "infra-app"
+ type = "infrastructure"
+
+ target_criteria {
+ port = 22
+ protocol = "SSH"
+ target_attributes {
+ name = "hostname"
+ values = ["tfgo-tests-useast", "tfgo-tests-uswest"]
+ }
+ }
+
+ # specify existing access policies by id
+ policies = []
+}
```
## Schema
@@ -70,6 +108,7 @@ resource "cloudflare_access_policy" "test_policy" {
- `application_id` (String) The ID of the application the policy is associated with. Required when using `precedence`. **Modifying this attribute will force creation of a new resource.**
- `approval_group` (Block List) (see [below for nested schema](#nestedblock--approval_group))
- `approval_required` (Boolean)
+- `connection_rules` (Block List, Max: 1) The rules that define how users may connect to the targets secured by your application. (see [below for nested schema](#nestedblock--connection_rules))
- `exclude` (Block List) A series of access conditions, see [Access Groups](https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/access_group#conditions). (see [below for nested schema](#nestedblock--exclude))
- `isolation_required` (Boolean) Require this application to be served in an isolated browser for users matching this policy.
- `precedence` (Number) The unique precedence for policies on a single application. Required when using `application_id`.
@@ -192,6 +231,22 @@ Optional:
- `email_list_uuid` (String)
+
+### Nested Schema for `connection_rules`
+
+Required:
+
+- `ssh` (Block List, Min: 1, Max: 1) The SSH-specific rules that define how users may connect to the targets secured by your application. (see [below for nested schema](#nestedblock--connection_rules--ssh))
+
+
+### Nested Schema for `connection_rules.ssh`
+
+Required:
+
+- `usernames` (List of String) Contains the Unix usernames that may be used when connecting over SSH.
+
+
+
### Nested Schema for `exclude`
diff --git a/docs/resources/infrastructure_access_target.md b/docs/resources/infrastructure_access_target.md
index 3c51b6b291..916bc42b73 100644
--- a/docs/resources/infrastructure_access_target.md
+++ b/docs/resources/infrastructure_access_target.md
@@ -9,7 +9,35 @@ description: |-
The [Infrastructure Access Target](https://developers.cloudflare.com/cloudflare-one/insights/risk-score/) resource allows you to configure Cloudflare Risk Behaviors for an account.
-
+## Example Usage
+
+```terraform
+resource "cloudflare_infrastructure_access_target" "example" {
+ account_id = "f037e56e89293a057740de681ac9abbe"
+ hostname = "example-target"
+ ip = {
+ ipv4 = {
+ ip_addr = "210.26.29.230"
+ virtual_network_id = "238dccd1-149b-463d-8228-560ab83a54fd"
+ }
+ ipv6 = {
+ ip_addr = "24c0:64e8:f0b4:8dbf:7104:72b0:ef8f:f5e0"
+ virtual_network_id = "238dccd1-149b-463d-8228-560ab83a54fd"
+ }
+ }
+}
+
+resource "cloudflare_infrastructure_access_target" "ipv4_only_example" {
+ account_id = "f037e56e89293a057740de681ac9abbe"
+ hostname = "example-ipv4-only"
+ ip = {
+ ipv4 = {
+ ip_addr = "210.26.29.230"
+ virtual_network_id = "238dccd1-149b-463d-8228-560ab83a54fd"
+ }
+ }
+}
+```
## Schema
@@ -50,4 +78,10 @@ Required:
- `ip_addr` (String) The IP address of the target.
- `virtual_network_id` (String) The private virtual network identifier for the target.
+## Import
+
+Import is supported using the following syntax:
+```shell
+$ terraform import cloudflare_infrastructure_access_target.example
+```
diff --git a/docs/resources/zero_trust_access_application.md b/docs/resources/zero_trust_access_application.md
index e2db34220d..09dd8d28b2 100644
--- a/docs/resources/zero_trust_access_application.md
+++ b/docs/resources/zero_trust_access_application.md
@@ -90,7 +90,8 @@ resource "cloudflare_zero_trust_access_application" "staging_app" {
- `skip_app_launcher_login_page` (Boolean) Option to skip the App Launcher landing page. Defaults to `false`.
- `skip_interstitial` (Boolean) Option to skip the authorization interstitial when using the CLI. Defaults to `false`.
- `tags` (Set of String) The itags associated with the application.
-- `type` (String) The application type. Available values: `app_launcher`, `bookmark`, `biso`, `dash_sso`, `saas`, `self_hosted`, `ssh`, `vnc`, `warp`. Defaults to `self_hosted`.
+- `target_criteria` (Block List) A list of mappings to apply to SCIM resources before provisioning them in this application. These can transform or filter the resources to be provisioned. (see [below for nested schema](#nestedblock--target_criteria))
+- `type` (String) The application type. Available values: `app_launcher`, `bookmark`, `biso`, `dash_sso`, `saas`, `self_hosted`, `ssh`, `vnc`, `warp`, `infrastructure`. Defaults to `self_hosted`.
- `zone_id` (String) The zone identifier to target for the resource. Conflicts with `account_id`.
### Read-Only
@@ -294,6 +295,26 @@ Optional:
- `delete` (Boolean) Whether or not this mapping applies to DELETE operations.
- `update` (Boolean) Whether or not this mapping applies to update (PATCH/PUT) operations.
+
+
+
+
+### Nested Schema for `target_criteria`
+
+Required:
+
+- `port` (Number) The port that the targets use for the chosen communication protocol. A port cannot be assigned to multiple protocols.
+- `protocol` (String) The communication protocol your application secures.
+- `target_attributes` (Block List, Min: 1) Contains a map of target attribute keys to target attribute values. (see [below for nested schema](#nestedblock--target_criteria--target_attributes))
+
+
+### Nested Schema for `target_criteria.target_attributes`
+
+Required:
+
+- `name` (String) The key of the attribute.
+- `values` (List of String) The values of the attribute.
+
## Import
Import is supported using the following syntax:
diff --git a/docs/resources/zero_trust_access_policy.md b/docs/resources/zero_trust_access_policy.md
index a250658754..debcb49eda 100644
--- a/docs/resources/zero_trust_access_policy.md
+++ b/docs/resources/zero_trust_access_policy.md
@@ -70,6 +70,7 @@ resource "cloudflare_zero_trust_access_policy" "test_policy" {
- `application_id` (String) The ID of the application the policy is associated with. Required when using `precedence`. **Modifying this attribute will force creation of a new resource.**
- `approval_group` (Block List) (see [below for nested schema](#nestedblock--approval_group))
- `approval_required` (Boolean)
+- `connection_rules` (Block List, Max: 1) The rules that define how users may connect to the targets secured by your application. (see [below for nested schema](#nestedblock--connection_rules))
- `exclude` (Block List) A series of access conditions, see [Access Groups](https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/access_group#conditions). (see [below for nested schema](#nestedblock--exclude))
- `isolation_required` (Boolean) Require this application to be served in an isolated browser for users matching this policy.
- `precedence` (Number) The unique precedence for policies on a single application. Required when using `application_id`.
@@ -192,6 +193,22 @@ Optional:
- `email_list_uuid` (String)
+
+### Nested Schema for `connection_rules`
+
+Required:
+
+- `ssh` (Block List, Min: 1, Max: 1) The SSH-specific rules that define how users may connect to the targets secured by your application. (see [below for nested schema](#nestedblock--connection_rules--ssh))
+
+
+### Nested Schema for `connection_rules.ssh`
+
+Required:
+
+- `usernames` (List of String) Contains the Unix usernames that may be used when connecting over SSH.
+
+
+
### Nested Schema for `exclude`
diff --git a/examples/data-sources/cloudflare_infrastructure_access_target/data-source.tf b/examples/data-sources/cloudflare_infrastructure_access_target/data-source.tf
new file mode 100644
index 0000000000..a9a5b1e3d5
--- /dev/null
+++ b/examples/data-sources/cloudflare_infrastructure_access_target/data-source.tf
@@ -0,0 +1,11 @@
+data "cloudflare_infrastructure_access_targets" "example" {
+ account_id = "f037e56e89293a057740de681ac9abbe"
+ # Query parameters
+ hostname_contains = "example"
+ ipv4 = "210.26.29.230"
+}
+
+# output the list of targets the data source contains
+output "targets" {
+ value = data.cloudflare_infrastructure_access_targets.example.targets
+}
diff --git a/examples/resources/cloudflare_access_application/resource.tf b/examples/resources/cloudflare_access_application/resource.tf
index 5c536b2494..80eff6e3e0 100644
--- a/examples/resources/cloudflare_access_application/resource.tf
+++ b/examples/resources/cloudflare_access_application/resource.tf
@@ -29,3 +29,22 @@ resource "cloudflare_access_application" "staging_app" {
max_age = 10
}
}
+
+# Infrastructure application configuration
+resource "cloudflare_zero_trust_access_application" "infra-app-example" {
+ account_id = "0da42c8d2132a9ddaf714f9e7c920711"
+ name = "infra-app"
+ type = "infrastructure"
+
+ target_criteria {
+ port = 22
+ protocol = "SSH"
+ target_attributes {
+ name = "hostname"
+ values = ["tfgo-tests-useast", "tfgo-tests-uswest"]
+ }
+ }
+
+ # specify existing access policies by id
+ policies = []
+}
diff --git a/examples/resources/cloudflare_access_policy/resource.tf b/examples/resources/cloudflare_access_policy/resource.tf
index 24a8c70bae..2ec5d95add 100644
--- a/examples/resources/cloudflare_access_policy/resource.tf
+++ b/examples/resources/cloudflare_access_policy/resource.tf
@@ -28,3 +28,41 @@ resource "cloudflare_access_policy" "test_policy" {
ip = [var.office_ip]
}
}
+
+# Access policy for an infrastructure application
+resource "cloudflare_access_policy" "infra-app-example-allow" {
+ application_id = cloudflare_zero_trust_access_application.infra-app-example.id
+ account_id = "0da42c8d2132a9ddaf714f9e7c920711"
+ name = "infra-app-example-allow"
+ decision = "allow"
+ precedence = 1
+
+ include {
+ email = ["devuser@gmail.com"]
+ }
+
+ connection_rules {
+ ssh {
+ usernames = ["ec2-user"]
+ }
+ }
+}
+
+# Infrastructure application configuration for infra-app-example-allow
+resource "cloudflare_zero_trust_access_application" "infra-app-example" {
+ account_id = "0da42c8d2132a9ddaf714f9e7c920711"
+ name = "infra-app"
+ type = "infrastructure"
+
+ target_criteria {
+ port = 22
+ protocol = "SSH"
+ target_attributes {
+ name = "hostname"
+ values = ["tfgo-tests-useast", "tfgo-tests-uswest"]
+ }
+ }
+
+ # specify existing access policies by id
+ policies = []
+}
diff --git a/examples/resources/cloudflare_infrastructure_access_target/import.sh b/examples/resources/cloudflare_infrastructure_access_target/import.sh
new file mode 100644
index 0000000000..7676dca7af
--- /dev/null
+++ b/examples/resources/cloudflare_infrastructure_access_target/import.sh
@@ -0,0 +1 @@
+$ terraform import cloudflare_infrastructure_access_target.example
diff --git a/examples/resources/cloudflare_infrastructure_access_target/resource.tf b/examples/resources/cloudflare_infrastructure_access_target/resource.tf
new file mode 100644
index 0000000000..fe533cf51c
--- /dev/null
+++ b/examples/resources/cloudflare_infrastructure_access_target/resource.tf
@@ -0,0 +1,25 @@
+resource "cloudflare_infrastructure_access_target" "example" {
+ account_id = "f037e56e89293a057740de681ac9abbe"
+ hostname = "example-target"
+ ip = {
+ ipv4 = {
+ ip_addr = "210.26.29.230"
+ virtual_network_id = "238dccd1-149b-463d-8228-560ab83a54fd"
+ }
+ ipv6 = {
+ ip_addr = "24c0:64e8:f0b4:8dbf:7104:72b0:ef8f:f5e0"
+ virtual_network_id = "238dccd1-149b-463d-8228-560ab83a54fd"
+ }
+ }
+}
+
+resource "cloudflare_infrastructure_access_target" "ipv4_only_example" {
+ account_id = "f037e56e89293a057740de681ac9abbe"
+ hostname = "example-ipv4-only"
+ ip = {
+ ipv4 = {
+ ip_addr = "210.26.29.230"
+ virtual_network_id = "238dccd1-149b-463d-8228-560ab83a54fd"
+ }
+ }
+}
diff --git a/internal/sdkv2provider/resource_cloudflare_access_application.go b/internal/sdkv2provider/resource_cloudflare_access_application.go
index 4eb7fb0611..22c22d8eb1 100644
--- a/internal/sdkv2provider/resource_cloudflare_access_application.go
+++ b/internal/sdkv2provider/resource_cloudflare_access_application.go
@@ -103,6 +103,14 @@ func resourceCloudflareAccessApplicationCreate(ctx context.Context, d *schema.Re
newAccessApplication.SaasApplication = convertSaasSchemaToStruct(d)
}
+ if _, ok := d.GetOk("target_criteria"); ok {
+ target_contexts, err := convertTargetContextsToStruct(d)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ newAccessApplication.TargetContexts = target_contexts
+ }
+
if value, ok := d.GetOk("tags"); ok {
newAccessApplication.Tags = expandInterfaceToStringList(value.(*schema.Set).List())
}
@@ -244,6 +252,11 @@ func resourceCloudflareAccessApplicationRead(ctx context.Context, d *schema.Reso
return diag.FromErr(fmt.Errorf("error setting Access Application SaaS app configuration: %w", saasConfigErr))
}
+ targetContexts := convertTargetContextsToSchema(accessApplication.TargetContexts)
+ if targetContextsErr := d.Set("target_criteria", targetContexts); targetContextsErr != nil {
+ return diag.FromErr(fmt.Errorf("error setting Access Application Infrastructure app configuration: %w", targetContextsErr))
+ }
+
if _, ok := d.GetOk("self_hosted_domains"); ok {
d.Set("self_hosted_domains", accessApplication.SelfHostedDomains)
}
@@ -328,6 +341,14 @@ func resourceCloudflareAccessApplicationUpdate(ctx context.Context, d *schema.Re
updatedAccessApplication.SaasApplication = saasConfig
}
+ if _, ok := d.GetOk("target_criteria"); ok {
+ target_contexts, err := convertTargetContextsToStruct(d)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ updatedAccessApplication.TargetContexts = target_contexts
+ }
+
if value, ok := d.GetOk("tags"); ok {
updatedAccessApplication.Tags = expandInterfaceToStringList(value.(*schema.Set).List())
}
diff --git a/internal/sdkv2provider/resource_cloudflare_access_application_test.go b/internal/sdkv2provider/resource_cloudflare_access_application_test.go
index 9af3d79834..946ab45d29 100644
--- a/internal/sdkv2provider/resource_cloudflare_access_application_test.go
+++ b/internal/sdkv2provider/resource_cloudflare_access_application_test.go
@@ -919,6 +919,34 @@ func TestAccCloudflareAccessApplication_WithAppLauncherVisible(t *testing.T) {
})
}
+func TestAccCloudflareAccessApplication_WithTargetContexts(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: testAccCloudflareAccessApplicationWithTargetContexts(rnd, domain, cloudflare.AccountIdentifier(accountID)),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
+ resource.TestCheckResourceAttr(name, "name", rnd),
+ resource.TestCheckResourceAttr(name, "type", "infrastructure"),
+ resource.TestCheckResourceAttr(name, "target_criteria.0.port", "22"),
+ resource.TestCheckResourceAttr(name, "target_criteria.0.protocol", "SSH"),
+ resource.TestCheckResourceAttr(name, "target_criteria.0.target_attributes.0.name", "hostname"),
+ resource.TestCheckResourceAttr(name, "target_criteria.0.target_attributes.0.values.0", "tfgo-acc-test"),
+ ),
+ },
+ },
+ })
+}
+
func TestAccCloudflareAccessApplication_WithSelfHostedDomains(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_zero_trust_access_application.%s", rnd)
@@ -1396,6 +1424,24 @@ func testAccessApplicationWithAppLauncherCustomizationFields(rnd, accountID stri
`, rnd, accountID)
}
+func testAccCloudflareAccessApplicationWithTargetContexts(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 = "infrastructure"
+ target_criteria {
+ port = 22
+ protocol = "SSH"
+ target_attributes {
+ name = "hostname"
+ values = ["tfgo-acc-test"]
+ }
+ }
+}
+`, 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" {
diff --git a/internal/sdkv2provider/resource_cloudflare_access_policy.go b/internal/sdkv2provider/resource_cloudflare_access_policy.go
index 6c00dd6c6e..9ecb7c4dd6 100644
--- a/internal/sdkv2provider/resource_cloudflare_access_policy.go
+++ b/internal/sdkv2provider/resource_cloudflare_access_policy.go
@@ -64,6 +64,46 @@ func apiAccessPolicyApprovalGroupToSchema(approvalGroup cloudflare.AccessApprova
return data
}
+func schemaAccessPolicyConnectionRulesToAPI(connectionRules map[string]interface{}) (*cloudflare.AccessInfrastructureConnectionRules, error) {
+ usernames := []string{}
+ if sshVal, ok := connectionRules["ssh"].([]interface{}); ok && len(sshVal) > 0 {
+ if sshMap, ok := sshVal[0].(map[string]interface{}); ok {
+ str_return := []string{}
+ if usernamesMap, ok := sshMap["usernames"].([]interface{}); ok {
+ for _, username := range usernamesMap {
+ str_return = append(str_return, username.(string))
+ }
+ }
+ usernames = str_return
+ }
+ }
+
+ return &cloudflare.AccessInfrastructureConnectionRules{
+ SSH: &cloudflare.AccessInfrastructureConnectionRulesSSH{
+ Usernames: usernames,
+ },
+ }, nil
+}
+
+func apiAccessPolicyConnectionRulesToSchema(connectionRules *cloudflare.AccessInfrastructureConnectionRules) []interface{} {
+ if connectionRules == nil {
+ return []interface{}{}
+ }
+
+ var connectionRulesSchema []interface{}
+ var usernameList []map[string]interface{}
+
+ usernameMap := map[string]interface{}{
+ "usernames": connectionRules.SSH.Usernames,
+ }
+ usernameList = append(usernameList, usernameMap)
+ connectionRulesSchema = append(connectionRulesSchema, map[string]interface{}{
+ "ssh": usernameList,
+ })
+
+ return connectionRulesSchema
+}
+
func schemaAccessPolicyApprovalGroupToAPI(data map[string]interface{}) cloudflare.AccessApprovalGroup {
var approvalGroup cloudflare.AccessApprovalGroup
@@ -87,6 +127,10 @@ func apiCloudflareAccessPolicyToResource(ctx context.Context, d *schema.Resource
d.Set("name", accessPolicy.Name)
d.Set("decision", accessPolicy.Decision)
+ if err := d.Set("connection_rules", apiAccessPolicyConnectionRulesToSchema(accessPolicy.InfrastructureConnectionRules)); err != nil {
+ return diag.FromErr(fmt.Errorf("failed to set connection_rules attribute: %w", err))
+ }
+
if err := d.Set("require", TransformAccessGroupForSchema(ctx, accessPolicy.Require)); err != nil {
return diag.FromErr(fmt.Errorf("failed to set require attribute: %w", err))
}
@@ -166,6 +210,15 @@ func resourceCloudflareAccessPolicyCreate(ctx context.Context, d *schema.Resourc
return diag.FromErr(fmt.Errorf("application_id is required for non-account level Access Policies"))
}
+ connectionRulesSchema, ok := d.Get("connection_rules").([]interface{})
+ if ok && len(connectionRulesSchema) > 0 {
+ connectionRules, err := schemaAccessPolicyConnectionRulesToAPI(connectionRulesSchema[0].(map[string]interface{}))
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ newAccessPolicy.InfrastructureConnectionRules = connectionRules
+ }
+
exclude := d.Get("exclude").([]interface{})
for _, value := range exclude {
if value != nil {
@@ -230,6 +283,15 @@ func resourceCloudflareAccessPolicyUpdate(ctx context.Context, d *schema.Resourc
SessionDuration: cloudflare.StringPtr(d.Get("session_duration").(string)),
}
+ connectionRulesSchema, ok := d.Get("connection_rules").([]interface{})
+ if ok && len(connectionRulesSchema) > 0 {
+ connectionRules, err := schemaAccessPolicyConnectionRulesToAPI(connectionRulesSchema[0].(map[string]interface{}))
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ updateReq.InfrastructureConnectionRules = connectionRules
+ }
+
exclude := d.Get("exclude").([]interface{})
for _, value := range exclude {
if value != nil {
diff --git a/internal/sdkv2provider/resource_cloudflare_access_policy_test.go b/internal/sdkv2provider/resource_cloudflare_access_policy_test.go
index 64e93092b1..de4a45a4eb 100644
--- a/internal/sdkv2provider/resource_cloudflare_access_policy_test.go
+++ b/internal/sdkv2provider/resource_cloudflare_access_policy_test.go
@@ -972,6 +972,68 @@ func testAccessPolicyExternalEvalautionConfig(resourceID, zone, accountID string
`, resourceID, zone, accountID)
}
+func TestAccCloudflareAccessPolicy_ConnectionRules(t *testing.T) {
+ rnd := generateRandomResourceName()
+ name := "cloudflare_access_policy." + rnd
+ zone := os.Getenv("CLOUDFLARE_DOMAIN")
+ accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() {
+ testAccPreCheck(t)
+ testAccPreCheckAccount(t)
+ },
+ ProviderFactories: providerFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccessPolicyConnectionRulesConfig(rnd, zone, accountID),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(name, "name", rnd),
+ resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
+ resource.TestCheckResourceAttr(name, "connection_rules.0.ssh.0.usernames.0", "tfgo-acc-test"),
+ resource.TestCheckResourceAttr(name, "include.0.email.0", "devuser@cloudflare.com"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccessPolicyConnectionRulesConfig(resourceID, zone, accountID string) string {
+ return fmt.Sprintf(`
+ resource "cloudflare_zero_trust_access_application" "%[1]s" {
+ name = "%[1]s"
+ type = "infrastructure"
+ account_id = "%[3]s"
+ domain = "%[1]s.%[2]s"
+ target_criteria {
+ port = 22
+ protocol = "SSH"
+ target_attributes {
+ name = "hostname"
+ values = ["tfgo-acc-test"]
+ }
+ }
+ }
+
+ resource "cloudflare_access_policy" "%[1]s" {
+ application_id = cloudflare_zero_trust_access_application.%[1]s.id
+ name = "%[1]s"
+ account_id = "%[3]s"
+ decision = "allow"
+ precedence = "1"
+ connection_rules {
+ ssh {
+ usernames = ["tfgo-acc-test"]
+ }
+ }
+ include {
+ email = ["devuser@cloudflare.com"]
+ }
+ }
+
+ `, resourceID, zone, accountID)
+}
+
func TestAccCloudflareAccessPolicy_IsolationRequired(t *testing.T) {
rnd := generateRandomResourceName()
name := "cloudflare_access_policy." + rnd
diff --git a/internal/sdkv2provider/schema_cloudflare_access_application.go b/internal/sdkv2provider/schema_cloudflare_access_application.go
index db61b9e02d..f89487b0b2 100644
--- a/internal/sdkv2provider/schema_cloudflare_access_application.go
+++ b/internal/sdkv2provider/schema_cloudflare_access_application.go
@@ -44,6 +44,15 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
Optional: true,
Computed: true,
Description: "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.",
+ 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
+ },
},
"self_hosted_domains": {
Type: schema.TypeSet,
@@ -57,8 +66,8 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
Type: schema.TypeString,
Optional: true,
Default: "self_hosted",
- ValidateFunc: validation.StringInSlice([]string{"app_launcher", "bookmark", "biso", "dash_sso", "saas", "self_hosted", "ssh", "vnc", "warp"}, false),
- Description: fmt.Sprintf("The application type. %s", renderAvailableDocumentationValuesStringSlice([]string{"app_launcher", "bookmark", "biso", "dash_sso", "saas", "self_hosted", "ssh", "vnc", "warp"})),
+ ValidateFunc: validation.StringInSlice([]string{"app_launcher", "bookmark", "biso", "dash_sso", "saas", "self_hosted", "ssh", "vnc", "warp", "infrastructure"}, false),
+ Description: fmt.Sprintf("The application type. %s", renderAvailableDocumentationValuesStringSlice([]string{"app_launcher", "bookmark", "biso", "dash_sso", "saas", "self_hosted", "ssh", "vnc", "warp", "infrastructure"})),
},
"policies": {
Type: schema.TypeList,
@@ -76,9 +85,8 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
Default: "24h",
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
appType := d.Get("type").(string)
- // Suppress the diff if it's a bookmark app type. Bookmarks don't have a session duration
- // field which always creates a diff because of the default '24h' value.
- if appType == "bookmark" {
+ // Suppress the diff if it's an app type that doesn't need a `session_duration` value.
+ if appType == "bookmark" || appType == "infrastructure" {
return true
}
@@ -405,6 +413,47 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
},
},
},
+ "target_criteria": {
+ Type: schema.TypeList,
+ Optional: true,
+ Description: "A list of mappings to apply to SCIM resources before provisioning them in this application. These can transform or filter the resources to be provisioned.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "port": {
+ Type: schema.TypeInt,
+ Required: true,
+ Description: "The port that the targets use for the chosen communication protocol. A port cannot be assigned to multiple protocols.",
+ },
+ "protocol": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The communication protocol your application secures.",
+ },
+ "target_attributes": {
+ Type: schema.TypeList,
+ Required: true,
+ Description: "Contains a map of target attribute keys to target attribute values.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "name": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The key of the attribute.",
+ },
+ "values": {
+ Type: schema.TypeList,
+ Required: true,
+ Description: "The values of the attribute.",
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
"auto_redirect_to_identity": {
Type: schema.TypeBool,
Optional: true,
@@ -467,6 +516,16 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
Optional: true,
Default: true,
Description: "Option to show/hide applications in App Launcher.",
+ 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 `app_launcher_visible`
+ // value.
+ if appType == "infrastructure" {
+ return true
+ }
+
+ return oldValue == newValue
+ },
},
"service_auth_401_redirect": {
Type: schema.TypeBool,
@@ -933,6 +992,52 @@ func convertSaasSchemaToStruct(d *schema.ResourceData) *cloudflare.SaasApplicati
return &SaasConfig
}
+func convertTargetContextsToStruct(d *schema.ResourceData) (*[]cloudflare.AccessInfrastructureTargetContext, error) {
+ TargetContexts := []cloudflare.AccessInfrastructureTargetContext{}
+ if value, ok := d.GetOk("target_criteria"); ok {
+ targetCriteria := value.([]interface{})
+ targetContext := cloudflare.AccessInfrastructureTargetContext{}
+ for _, item := range targetCriteria {
+ itemMap := item.(map[string]interface{})
+
+ if port, ok := itemMap["port"].(int); ok {
+ targetContext.Port = port
+ }
+ if protocol, ok := itemMap["protocol"].(string); ok {
+ switch protocol {
+ case "SSH":
+ targetContext.Protocol = cloudflare.AccessInfrastructureSSH
+ case "RDP":
+ targetContext.Protocol = cloudflare.AccessInfrastructureRDP
+ default:
+ return &[]cloudflare.AccessInfrastructureTargetContext{}, fmt.Errorf("failed to parse protocol: value must be one of SSH or RDP")
+ }
+ }
+
+ str_return := make(map[string][]string)
+ if sshVal, ok := itemMap["target_attributes"].([]interface{}); ok && len(sshVal) > 0 {
+ for _, attrItem := range sshVal {
+ if sshMap, ok := attrItem.(map[string]interface{}); ok {
+ attributes := make(map[string][]string)
+ key := sshMap["name"].(string)
+ if usernames, ok := sshMap["values"].([]interface{}); ok {
+ for _, username := range usernames {
+ attributes[key] = append(attributes[key], username.(string))
+ }
+ }
+ str_return = attributes
+ }
+ }
+ targetContext.TargetAttributes = str_return
+ }
+
+ TargetContexts = append(TargetContexts, targetContext)
+ }
+ }
+
+ return &TargetContexts, nil
+}
+
func convertLandingPageDesignSchemaToStruct(d *schema.ResourceData) *cloudflare.AccessLandingPageDesign {
LandingPageDesign := cloudflare.AccessLandingPageDesign{}
if _, ok := d.GetOk("landing_page_design"); ok {
@@ -1234,6 +1339,34 @@ func convertSaasStructToSchema(d *schema.ResourceData, app *cloudflare.SaasAppli
}
}
+func convertTargetContextsToSchema(targetContexts *[]cloudflare.AccessInfrastructureTargetContext) []interface{} {
+ if targetContexts == nil {
+ return []interface{}{}
+ }
+ var targetContextsSchema []interface{}
+
+ for _, targetContext := range *targetContexts {
+ //targetAttributesList := []map[string][]string{}
+ var attributesReturned []map[string]interface{}
+
+ for key, values := range targetContext.TargetAttributes {
+ attributeMap := map[string]interface{}{
+ "name": key,
+ "values": values,
+ }
+
+ attributesReturned = append(attributesReturned, attributeMap)
+ }
+
+ targetContextsSchema = append(targetContextsSchema, map[string]interface{}{
+ "port": targetContext.Port,
+ "protocol": targetContext.Protocol,
+ "target_attributes": attributesReturned,
+ })
+ }
+ return targetContextsSchema
+}
+
func convertScimConfigStructToSchema(scimConfig *cloudflare.AccessApplicationSCIMConfig) []interface{} {
if scimConfig == nil {
return []interface{}{}
diff --git a/internal/sdkv2provider/schema_cloudflare_access_policy.go b/internal/sdkv2provider/schema_cloudflare_access_policy.go
index 3a99d9279b..787d009719 100644
--- a/internal/sdkv2provider/schema_cloudflare_access_policy.go
+++ b/internal/sdkv2provider/schema_cloudflare_access_policy.go
@@ -103,6 +103,34 @@ func resourceCloudflareAccessPolicySchema() map[string]*schema.Schema {
},
Description: "How often a user will be forced to re-authorise. Must be in the format `48h` or `2h45m`",
},
+ "connection_rules": {
+ Type: schema.TypeList,
+ Optional: true,
+ MaxItems: 1,
+ Description: "The rules that define how users may connect to the targets secured by your application.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "ssh": {
+ Type: schema.TypeList,
+ Required: true,
+ MaxItems: 1,
+ Description: "The SSH-specific rules that define how users may connect to the targets secured by your application.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "usernames": {
+ Type: schema.TypeList,
+ Required: true,
+ Description: "Contains the Unix usernames that may be used when connecting over SSH.",
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
}
}