Repository: GoogleCloudPlatform/magic-modules
Pull request #14070: Private CA - Differentiate unset and default values for is_ca/max_issuer_path_length in Certificate Templates
Author: login: rohanchawla23
Labels: name: service/privateca
Created: 2025-05-22T20:53:27Z | Merged: 2025-05-23T00:22:54Z

<!--
Complete the self-review checklist to help speed up the review process: https://googlecloudplatform.github.io/magic-modules/contribute/review-pr/

If your PR is still work in progress, please create it in draft mode.

Put a description of what this PR is for here, along with any references to issues that this resolves or contributes to.
For example: Fixes https://github.com/hashicorp/terraform-provider-google/issues/ISSUE_ID
-->

Previously, unset and default values for the `is_ca` and `max_issuer_path_length` fields were treated as exactly the same for Certificate Templates, though the API service does distinguish between the two. This led to an issue where it was not possible to use a Certificate Template to create a CA with a `max_issuer_path_length` of 0. To address this, two new fields have been added to the Certificate Template interface to distinguish between the unset and default values for these two fields. The name_constraints field has also been added to the Certificate Template resource, to enable compatibility with a new expander. This expander contains the logic for using the two new fields to distinguish between the unset and default values. Note that the pre-existing expander for X509 Configs could not be used, as the behavior there would not be backwards compatible with the current behavior of CertificateTemplate when the `is_ca` field is false.

I acknowledge that I have:

- [x] Searched through the [issue tracker](https://github.com/hashicorp/terraform-provider-google/issues) for an open issue that this either resolves or contributes to, commented on it to claim it, and written "fixes {url}" or "part of {url}" in this PR description. If there were no relevant open issues, I opened one and commented that I would like to work on it (not necessary for very small changes).
- [x] Ensured that all new fields I added that can be set by a user appear in at least one [example](https://github.com/GoogleCloudPlatform/magic-modules/tree/main/mmv1/templates/terraform/examples) (for generated resources) or [third_party test](https://github.com/GoogleCloudPlatform/magic-modules/tree/main/mmv1/third_party/terraform/tests) (for handwritten resources or update tests).
- [x] [Generated Terraform providers](https://github.com/GoogleCloudPlatform/magic-modules/blob/main/docs/content/docs/getting-started/generate-providers.md), and ran [make test and make lint](https://googlecloudplatform.github.io/magic-modules/docs/getting-started/run-provider-tests/#run-unit-tests) in the generated providers to ensure it passes unit and linter tests.
- [x] [Ran](https://github.com/GoogleCloudPlatform/magic-modules/blob/main/docs/content/docs/getting-started/run-provider-tests.md) relevant acceptance tests using my own Google Cloud project and credentials (If the acceptance tests do not yet pass or you are unable to run them, please let your reviewer know).
- [x] Read the [Release Notes Guide](https://googlecloudplatform.github.io/magic-modules/contribute/release-notes/) before writing my release note below.

**Release Note Template for Downstream PRs (will be copied)**

See [Write release notes](https://googlecloudplatform.github.io/magic-modules/contribute/release-notes/) for guidance.

```release-note:enhancement
privateca: added support for setting default values for basic constraints for `google_privateca_certificate_template` via the `null_ca` and `zero_max_issuer_path_length` fields. also added `name_constraints` field for `google_privateca_certificate_template`.
```



Diff:
diff --git a/mmv1/products/privateca/CertificateTemplate.yaml b/mmv1/products/privateca/CertificateTemplate.yaml
index 2f440e386815..311c1bd9d6f1 100644
--- a/mmv1/products/privateca/CertificateTemplate.yaml
+++ b/mmv1/products/privateca/CertificateTemplate.yaml
@@ -52,6 +52,11 @@ examples:
     primary_resource_name: 'fmt.Sprintf("tf-test-my-template%s", context["random_suffix"])'
     vars:
       name: 'my-template'
+  - name: 'privateca_template_zero_max_issuer_path_length_null_ca'
+    primary_resource_id: 'default'
+    primary_resource_name: 'fmt.Sprintf("tf-test-my-template%s", context["random_suffix"])'
+    vars:
+      name: 'my-template'
 parameters:
 properties:
   - name: 'name'
@@ -69,6 +74,8 @@ properties:
   - name: 'predefinedValues'
     type: NestedObject
     description: Optional. A set of X.509 values that will be applied to all issued certificates that use this template. If the certificate request includes conflicting values for the same properties, they will be overwritten by the values defined here. If the issuing CaPool's IssuancePolicy defines conflicting baseline_values for the same properties, the certificate issuance request will fail.
+    custom_flatten: 'templates/terraform/custom_flatten/privateca_certificate_template_509_config.go.tmpl'
+    custom_expand: 'templates/terraform/custom_expand/privateca_certificate_template_509_config.go.tmpl'
     properties:
       - name: 'keyUsage'
         type: NestedObject
@@ -178,11 +185,29 @@ properties:
         properties:
           - name: 'isCa'
             type: Boolean
-            description: Optional. Refers to the "CA" X.509 extension, which is a boolean value. When this value is missing, the extension will be omitted from the CA certificate.
+            description: |
+              Optional. Refers to the "CA" X.509 extension, which is a boolean value. When this value is true, the "CA" in Basic Constraints extension will be set to true.
+            send_empty_value: true
+          - name: 'nullCa'
+            type: Boolean
+            description: |
+              Optional. When true, the "CA" in Basic Constraints extension will be set to null and omitted from the CA certificate.
+              If both `is_ca` and `null_ca` are unset, the "CA" in Basic Constraints extension will be set to false.
+              Note that the behavior when `is_ca = false` for this resource is different from the behavior in the Certificate Authority, Certificate and CaPool resources.
+            url_param_only: true
             send_empty_value: true
           - name: 'maxIssuerPathLength'
             type: Integer
-            description: Optional. Refers to the path length restriction X.509 extension. For a CA certificate, this value describes the depth of subordinate CA certificates that are allowed. If this value is less than 0, the request will fail. If this value is missing, the max path length will be omitted from the CA certificate.
+            description: |
+              Optional. Refers to the "path length constraint" in Basic Constraints extension. For a CA certificate, this value describes the depth of
+              subordinate CA certificates that are allowed. If this value is less than 0, the request will fail.
+          - name: 'zeroMaxIssuerPathLength'
+            type: Boolean
+            description: |
+              Optional. When true, the "path length constraint" in Basic Constraints extension will be set to 0.
+              if both `max_issuer_path_length` and `zero_max_issuer_path_length` are unset,
+              the max path length will be omitted from the CA certificate.
+            url_param_only: true
       - name: 'policyIds'
         type: Array
         description: Optional. Describes the X.509 certificate policy object identifiers, per https://tools.ietf.org/html/rfc5280#section-4.2.1.4.
@@ -227,6 +252,89 @@ properties:
               type: String
               description: Required. The value of this X.509 extension.
               required: true
+      - name: 'nameConstraints'
+        type: NestedObject
+        description: |
+          Describes the X.509 name constraints extension.
+        properties:
+          - name: 'critical'
+            type: Boolean
+            description:
+              Indicates whether or not the name constraints are marked
+              critical.
+            required: true
+          - name: 'permittedDnsNames'
+            type: Array
+            description: |
+              Contains permitted DNS names. Any DNS name that can be
+              constructed by simply adding zero or more labels to
+              the left-hand side of the name satisfies the name constraint.
+              For example, `example.com`, `www.example.com`, `www.sub.example.com`
+              would satisfy `example.com` while `example1.com` does not.
+            item_type:
+              type: String
+          - name: 'excludedDnsNames'
+            type: Array
+            description: |
+              Contains excluded DNS names. Any DNS name that can be
+              constructed by simply adding zero or more labels to
+              the left-hand side of the name satisfies the name constraint.
+              For example, `example.com`, `www.example.com`, `www.sub.example.com`
+              would satisfy `example.com` while `example1.com` does not.
+            item_type:
+              type: String
+          - name: 'permittedIpRanges'
+            type: Array
+            description: |
+              Contains the permitted IP ranges. For IPv4 addresses, the ranges
+              are expressed using CIDR notation as specified in RFC 4632.
+              For IPv6 addresses, the ranges are expressed in similar encoding as IPv4
+              addresses.
+            item_type:
+              type: String
+          - name: 'excludedIpRanges'
+            type: Array
+            description: |
+              Contains the excluded IP ranges. For IPv4 addresses, the ranges
+              are expressed using CIDR notation as specified in RFC 4632.
+              For IPv6 addresses, the ranges are expressed in similar encoding as IPv4
+              addresses.
+            item_type:
+              type: String
+          - name: 'permittedEmailAddresses'
+            type: Array
+            description: |
+              Contains the permitted email addresses. The value can be a particular
+              email address, a hostname to indicate all email addresses on that host or
+              a domain with a leading period (e.g. `.example.com`) to indicate
+              all email addresses in that domain.
+            item_type:
+              type: String
+          - name: 'excludedEmailAddresses'
+            type: Array
+            description: |
+              Contains the excluded email addresses. The value can be a particular
+              email address, a hostname to indicate all email addresses on that host or
+              a domain with a leading period (e.g. `.example.com`) to indicate
+              all email addresses in that domain.
+            item_type:
+              type: String
+          - name: 'permittedUris'
+            type: Array
+            description: |
+              Contains the permitted URIs that apply to the host part of the name.
+              The value can be a hostname or a domain with a
+              leading period (like `.example.com`)
+            item_type:
+              type: String
+          - name: 'excludedUris'
+            type: Array
+            description: |
+              Contains the excluded URIs that apply to the host part of the name.
+              The value can be a hostname or a domain with a
+              leading period (like `.example.com`)
+            item_type:
+              type: String
   - name: 'identityConstraints'
     type: NestedObject
     description: Optional. Describes constraints on identities that may be appear in Certificates issued using this template. If this is omitted, then this template will not add restrictions on a certificate's identity.
diff --git a/mmv1/templates/terraform/custom_expand/privateca_certificate_template_509_config.go.tmpl b/mmv1/templates/terraform/custom_expand/privateca_certificate_template_509_config.go.tmpl
new file mode 100644
index 000000000000..5cd89e071a77
--- /dev/null
+++ b/mmv1/templates/terraform/custom_expand/privateca_certificate_template_509_config.go.tmpl
@@ -0,0 +1,53 @@
+{{/* See mmv1/third_party/terraform/utils/privateca_utils.go for the sub-expanders and explanation */}}
+func expand{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
+        if v == nil {
+               return v, nil
+        }
+        l := v.([]interface{})
+        if len(l) == 0 || l[0] == nil {
+                return nil, nil
+        }
+        raw := l[0]
+        original := raw.(map[string]interface{})
+        if len(original) == 0 {
+                return nil, nil
+        }
+        transformed := make(map[string]interface{})
+
+        caOptions, err := expandPrivatecaCertificateTemplateConfigX509ConfigCaOptions(original["ca_options"], d, config)
+        if err != nil {
+                return nil, err
+        }
+        transformed["caOptions"] = caOptions
+
+        keyUsage, err := expandPrivatecaCertificateConfigX509ConfigKeyUsage(original["key_usage"], d, config)
+        if err != nil {
+                return nil, err
+        }
+        transformed["keyUsage"] = keyUsage
+
+        policyIds, err := expandPrivatecaCertificateConfigX509ConfigPolicyIds(original["policy_ids"], d, config)
+        if err != nil {
+                return nil, err
+        }
+        transformed["policyIds"] = policyIds
+
+        aiaOcspServers, err := expandPrivatecaCertificateConfigX509ConfigAiaOcspServers(original["aia_ocsp_servers"], d, config)
+        if err != nil {
+                return nil, err
+        }
+        transformed["aiaOcspServers"] = aiaOcspServers
+
+        addExts, err := expandPrivatecaCertificateConfigX509ConfigAdditionalExtensions(original["additional_extensions"], d, config)
+        if err != nil {
+                return nil, err
+        }
+        transformed["additionalExtensions"] = addExts
+
+        nameConstraints, err := expandPrivatecaCertificateConfigX509ConfigNameConstraints(original["name_constraints"], d, config)
+        if err != nil {
+                return nil, err
+        }
+        transformed["nameConstraints"] = nameConstraints
+        return transformed, nil
+}
diff --git a/mmv1/templates/terraform/custom_flatten/privateca_certificate_template_509_config.go.tmpl b/mmv1/templates/terraform/custom_flatten/privateca_certificate_template_509_config.go.tmpl
new file mode 100644
index 000000000000..97703b6c7264
--- /dev/null
+++ b/mmv1/templates/terraform/custom_flatten/privateca_certificate_template_509_config.go.tmpl
@@ -0,0 +1,20 @@
+{{/* See mmv1/third_party/terraform/utils/privateca_utils.go for the sub-expanders and explanation */}}
+func flatten{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} {
+	if v == nil {
+		v = make(map[string]interface{})
+	}
+	original := v.(map[string]interface{})
+	transformed := make(map[string]interface{})
+	transformed["additional_extensions"] =
+		flattenPrivatecaCertificateConfigX509ConfigAdditionalExtensions(original["additionalExtensions"], d, config)
+	transformed["policy_ids"] =
+		flattenPrivatecaCertificateConfigX509ConfigPolicyIds(original["policyIds"], d, config)
+	transformed["aia_ocsp_servers"] = flattenPrivatecaCertificateConfigX509ConfigAiaOcspServers(original["aiaOcspServers"], d, config)	
+	transformed["ca_options"] =
+		flattenPrivatecaCertificateTemplateConfigX509ConfigCaOptions(original["caOptions"], d, config)
+	transformed["key_usage"] =
+		flattenPrivatecaCertificateConfigX509ConfigKeyUsage(original["keyUsage"], d, config)
+	transformed["name_constraints"] =
+		flattenPrivatecaCertificateConfigX509ConfigNameConstraints(original["nameConstraints"], d, config)
+	return []interface{}{transformed}
+}
diff --git a/mmv1/templates/terraform/examples/privateca_template_zero_max_issuer_path_length_null_ca.tf.tmpl b/mmv1/templates/terraform/examples/privateca_template_zero_max_issuer_path_length_null_ca.tf.tmpl
new file mode 100644
index 000000000000..09bd036806b5
--- /dev/null
+++ b/mmv1/templates/terraform/examples/privateca_template_zero_max_issuer_path_length_null_ca.tf.tmpl
@@ -0,0 +1,85 @@
+resource "google_privateca_certificate_template" "{{$.PrimaryResourceId}}" {
+  name = "{{index $.Vars "name"}}"
+  location = "us-central1"
+  description = "A sample certificate template"
+
+  identity_constraints {
+    allow_subject_alt_names_passthrough = true
+    allow_subject_passthrough           = true
+
+    cel_expression {
+      description = "Always true"
+      expression  = "true"
+      location    = "any.file.anywhere"
+      title       = "Sample expression"
+    }
+  }
+
+  maximum_lifetime = "86400s"
+
+  passthrough_extensions {
+    additional_extensions {
+      object_id_path = [1, 6]
+    }
+    known_extensions = ["EXTENDED_KEY_USAGE"]
+  }
+
+  predefined_values {
+    additional_extensions {
+      object_id {
+        object_id_path = [1, 6]
+      }
+      value    = "c3RyaW5nCg=="
+      critical = true
+    }
+    aia_ocsp_servers = ["string"]
+    ca_options {
+      is_ca                       = false
+      null_ca                     = true
+      zero_max_issuer_path_length = true
+      max_issuer_path_length      = 0
+    }
+    key_usage {
+      base_key_usage {
+        cert_sign          = false
+        content_commitment = true
+        crl_sign           = false
+        data_encipherment  = true
+        decipher_only      = true
+        digital_signature  = true
+        encipher_only      = true
+        key_agreement      = true
+        key_encipherment   = true
+      }
+      extended_key_usage {
+        client_auth      = true
+        code_signing     = true
+        email_protection = true
+        ocsp_signing     = true
+        server_auth      = true
+        time_stamping    = true
+      }
+      unknown_extended_key_usages {
+        object_id_path = [1, 6]
+      }
+    }
+    policy_ids {
+      object_id_path = [1, 6]
+    }
+    name_constraints {
+      critical                  = true
+      permitted_dns_names       = ["*.example1.com", "*.example2.com"]
+      excluded_dns_names        = ["*.deny.example1.com", "*.deny.example2.com"]
+      permitted_ip_ranges       = ["10.0.0.0/8", "11.0.0.0/8"]
+      excluded_ip_ranges        = ["10.1.1.0/24", "11.1.1.0/24"]
+      permitted_email_addresses = [".example1.com", ".example2.com"]
+      excluded_email_addresses  = [".deny.example1.com", ".deny.example2.com"]
+      permitted_uris            = [".example1.com", ".example2.com"]
+      excluded_uris             = [".deny.example1.com", ".deny.example2.com"]
+    }
+  }
+
+  labels = {
+    label-one = "value-one"
+  }
+}
\ No newline at end of file
diff --git a/mmv1/third_party/terraform/services/privateca/privateca_utils.go b/mmv1/third_party/terraform/services/privateca/privateca_utils.go
index 51211be49454..10d85abdfa43 100644
--- a/mmv1/third_party/terraform/services/privateca/privateca_utils.go
+++ b/mmv1/third_party/terraform/services/privateca/privateca_utils.go
@@ -8,8 +8,12 @@ import (
 	transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
 )
 
-// This file contains shared flatteners between PrivateCA Certificate, CaPool and CertificateAuthority.
-// These resources share the x509Config (Certificate, CertificateAuthorty)/baselineValues (CaPool) object.
+// This file contains shared flatteners between PrivateCA Certificate, CaPool, CertificateTemplate and
+// CertificateAuthority. These resources share the x509Config (Certificate, CertificateAuthority)/
+// baselineValues (CaPool) object. CertificateTemplate contains the predefinedValues object, which is slightly
+// different from the other two, and so requires its own functions to process. These functions are also contained
+// in this file.
+//
 // The API does not return this object if it only contains booleans with the default (false) value. This
 // causes problems if a user specifies only default values, as Terraform detects that the object has been
 // deleted on the API-side. This flattener creates default objects for sub-objects that match this pattern
@@ -64,6 +68,50 @@ func expandPrivatecaCertificateConfigX509ConfigCaOptions(v interface{}, d tpgres
 	return transformed, nil
 }
 
+func expandPrivatecaCertificateTemplateConfigX509ConfigCaOptions(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
+	// Similar to expandPrivatecaCertificateConfigX509ConfigCaOptions, but only for use in
+	// Certificate Templates, which use a null_ca field instead of the non_ca field.
+	// Fields null_ca, zero_max_issuer_path_length are used to distinguish between
+	// unset booleans and booleans set with a default value.
+	// Unset is_ca or unset max_issuer_path_length either allow any values for these fields when
+	// used in an issuance policy, or allow the API to use default values when used in a
+	// certificate config. A default value of is_ca=false means that issued certificates cannot
+	// be CA certificates. A default value of max_issuer_path_length=0 means that the CA cannot
+	// issue CA certificates.
+	if v == nil {
+		return nil, nil
+	}
+	l := v.([]interface{})
+	if len(l) == 0 || l[0] == nil {
+		return nil, nil
+	}
+	raw := l[0]
+	original := raw.(map[string]interface{})
+
+	nullCa := original["null_ca"].(bool)
+	isCa := original["is_ca"].(bool)
+
+	zeroPathLength := original["zero_max_issuer_path_length"].(bool)
+	maxIssuerPathLength := original["max_issuer_path_length"].(int)
+
+	transformed := make(map[string]interface{})
+
+	if nullCa && isCa {
+		return nil, fmt.Errorf("null_ca, is_ca can not be set to true at the same time.")
+	}
+	if zeroPathLength && maxIssuerPathLength > 0 {
+		return nil, fmt.Errorf("zero_max_issuer_path_length can not be set to true while max_issuer_path_length being set to a positive integer.")
+	}
+
+	if !nullCa {
+		transformed["isCa"] = original["is_ca"]
+	}
+	if maxIssuerPathLength > 0 || zeroPathLength {
+		transformed["maxIssuerPathLength"] = original["max_issuer_path_length"]
+	}
+	return transformed, nil
+}
+
 func expandPrivatecaCertificateConfigX509ConfigKeyUsage(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
 	if v == nil {
 		return v, nil
@@ -361,6 +409,33 @@ func flattenPrivatecaCertificateConfigX509ConfigCaOptions(v interface{}, d *sche
 
 	return []interface{}{transformed}
 }
+
+func flattenPrivatecaCertificateTemplateConfigX509ConfigCaOptions(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} {
+	// Special case here as the CaPool API returns an empty object rather than nil unlike the Certificate
+	// and CertificateAuthority APIs.
+	if v == nil || len(v.(map[string]interface{})) == 0 {
+		v = make(map[string]interface{})
+	}
+	original := v.(map[string]interface{})
+	transformed := make(map[string]interface{})
+
+	val, exists := original["isCa"]
+	transformed["is_ca"] =
+		flattenPrivatecaCertificateConfigX509ConfigCaOptionsIsCa(val, d, config)
+	if !exists {
+		transformed["null_ca"] = true
+	}
+
+	val, exists = original["maxIssuerPathLength"]
+	transformed["max_issuer_path_length"] =
+		flattenPrivatecaCertificateConfigX509ConfigCaOptionsMaxIssuerPathLength(val, d, config)
+	if exists && int(val.(float64)) == 0 {
+		transformed["zero_max_issuer_path_length"] = true
+	}
+
+	return []interface{}{transformed}
+}
+
 func flattenPrivatecaCertificateConfigX509ConfigCaOptionsIsCa(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} {
 	return v
 }
diff --git a/mmv1/third_party/terraform/services/privateca/resource_privateca_certificate_template_test.go b/mmv1/third_party/terraform/services/privateca/resource_privateca_certificate_template_test.go
index 35dccbfb4d1c..ff7e486ee91c 100644
--- a/mmv1/third_party/terraform/services/privateca/resource_privateca_certificate_template_test.go
+++ b/mmv1/third_party/terraform/services/privateca/resource_privateca_certificate_template_test.go
@@ -81,6 +81,60 @@ func TestAccPrivatecaCertificateTemplate_BasicCertificateTemplateLongForm(t *tes
 	})
 }
 
+func TestAccPrivatecaCertificateTemplate_updateCaOption(t *testing.T) {
+	t.Parallel()
+
+	context := map[string]interface{}{
+		"project_name":  envvar.GetTestProjectFromEnv(),
+		"region":        envvar.GetTestRegionFromEnv(),
+		"random_suffix": acctest.RandString(t, 10),
+	}
+
+	acctest.VcrTest(t, resource.TestCase{
+		PreCheck:                 func() { acctest.AccTestPreCheck(t) },
+		ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
+		CheckDestroy:             testAccCheckPrivatecaCertificateTemplateDestroyProducer(t),
+		Steps: []resource.TestStep{
+			{
+				Config: testAccPrivatecaCertificateTemplate_CertificateTemplateCaOptionIsCaIsTrueAndMaxPathIsPositive(context),
+			},
+			{
+				ResourceName:            "google_privateca_certificate_template.primary",
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"predefined_values.0.key_usage.0.extended_key_usage", "labels", "terraform_labels", "project", "location", "name"},
+			},
+			{
+				Config: testAccPrivatecaCertificateTemplate_CertificateTemplateCaOptionIsCaIsFalse(context),
+			},
+			{
+				ResourceName:            "google_privateca_certificate_template.primary",
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"predefined_values.0.key_usage.0.extended_key_usage", "labels", "terraform_labels", "project", "location", "name"},
+			},
+			{
+				Config: testAccPrivatecaCertificateTemplate_CertificateTemplateCaOptionIsCaIsNull(context),
+			},
+			{
+				ResourceName:            "google_privateca_certificate_template.primary",
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"predefined_values.0.key_usage.0.extended_key_usage", "labels", "terraform_labels", "project", "location", "name"},
+			},
+			{
+				Config: testAccPrivatecaCertificateTemplate_CertificateTemplateCaOptionMaxIssuerPathLenghIsZero(context),
+			},
+			{
+				ResourceName:            "google_privateca_certificate_template.primary",
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"predefined_values.0.key_usage.0.extended_key_usage", "labels", "terraform_labels", "project", "location", "name"},
+			},
+		},
+	})
+}
+
 func testAccPrivatecaCertificateTemplate_BasicCertificateTemplate(context map[string]interface{}) string {
 	return acctest.Nprintf(`
 resource "google_privateca_certificate_template" "primary" {
@@ -430,3 +484,290 @@ resource "google_privateca_certificate_template" "primary" {
 
 `, context)
 }
+
+func testAccPrivatecaCertificateTemplate_CertificateTemplateCaOptionIsCaIsTrueAndMaxPathIsPositive(context map[string]interface{}) string {
+	return acctest.Nprintf(`
+resource "google_privateca_certificate_template" "primary" {
+  location         = "%{region}"
+  name             = "tf-test-template%{random_suffix}"
+  maximum_lifetime = "86400s"
+  description      = "A sample certificate template"
+  identity_constraints {
+    allow_subject_alt_names_passthrough = true
+    allow_subject_passthrough           = true
+    cel_expression {
+      description = "Always true"
+      expression  = "true"
+      location    = "any.file.anywhere"
+      title       = "Sample expression"
+    }
+  }
+  passthrough_extensions {
+    additional_extensions {
+      object_id_path = [1, 6]
+    }
+    known_extensions = ["EXTENDED_KEY_USAGE"]
+  }
+  predefined_values {
+    additional_extensions {
+      object_id {
+        object_id_path = [1, 6]
+      }
+      value    = "c3RyaW5nCg=="
+      critical = true
+    }
+    aia_ocsp_servers = ["string"]
+    ca_options {
+      is_ca                  = true
+      max_issuer_path_length = 6
+    }
+    key_usage {
+      base_key_usage {
+        cert_sign          = false
+        content_commitment = true
+        crl_sign           = false
+        data_encipherment  = true
+        decipher_only      = true
+        digital_signature  = true
+        encipher_only      = true
+        key_agreement      = true
+        key_encipherment   = true
+      }
+      extended_key_usage {
+        client_auth      = true
+        code_signing     = true
+        email_protection = true
+        ocsp_signing     = true
+        server_auth      = true
+        time_stamping    = true
+      }
+      unknown_extended_key_usages {
+        object_id_path = [1, 6]
+      }
+    }
+    policy_ids {
+      object_id_path = [1, 6]
+    }
+  }
+  project = "%{project_name}"
+  labels = {
+    label-two = "value-two"
+  }
+}
+`, context)
+}
+
+func testAccPrivatecaCertificateTemplate_CertificateTemplateCaOptionIsCaIsFalse(context map[string]interface{}) string {
+	return acctest.Nprintf(`
+resource "google_privateca_certificate_template" "primary" {
+  location         = "%{region}"
+  name             = "tf-test-template%{random_suffix}"
+  maximum_lifetime = "86400s"
+  description      = "An updated sample certificate template"
+  identity_constraints {
+    allow_subject_alt_names_passthrough = true
+    allow_subject_passthrough           = true
+    cel_expression {
+      description = "Always true"
+      expression  = "true"
+      location    = "any.file.anywhere"
+      title       = "Sample expression"
+    }
+  }
+  passthrough_extensions {
+    additional_extensions {
+      object_id_path = [1, 6]
+    }
+    known_extensions = ["EXTENDED_KEY_USAGE"]
+  }
+  predefined_values {
+    additional_extensions {
+      object_id {
+        object_id_path = [1, 6]
+      }
+      value    = "c3RyaW5nCg=="
+      critical = true
+    }
+    aia_ocsp_servers = ["string"]
+    ca_options {
+      is_ca = false
+    }
+    key_usage {
+      base_key_usage {
+        cert_sign          = false
+        content_commitment = true
+        crl_sign           = false
+        data_encipherment  = true
+        decipher_only      = true
+        digital_signature  = true
+        encipher_only      = true
+        key_agreement      = true
+        key_encipherment   = true
+      }
+      extended_key_usage {
+        client_auth      = true
+        code_signing     = true
+        email_protection = true
+        ocsp_signing     = true
+        server_auth      = true
+        time_stamping    = true
+      }
+      unknown_extended_key_usages {
+        object_id_path = [1, 6]
+      }
+    }
+    policy_ids {
+      object_id_path = [1, 6]
+    }
+  }
+  project = "%{project_name}"
+  labels = {
+    label-two = "value-two"
+  }
+}
+`, context)
+}
+
+func testAccPrivatecaCertificateTemplate_CertificateTemplateCaOptionIsCaIsNull(context map[string]interface{}) string {
+	return acctest.Nprintf(`
+resource "google_privateca_certificate_template" "primary" {
+  location         = "%{region}"
+  name             = "tf-test-template%{random_suffix}"
+  maximum_lifetime = "86400s"
+  description      = "An updated sample certificate template"
+  identity_constraints {
+    allow_subject_alt_names_passthrough = true
+    allow_subject_passthrough           = true
+    cel_expression {
+      description = "Always true"
+      expression  = "true"
+      location    = "any.file.anywhere"
+      title       = "Sample expression"
+    }
+  }
+  passthrough_extensions {
+    additional_extensions {
+      object_id_path = [1, 6]
+    }
+    known_extensions = ["EXTENDED_KEY_USAGE"]
+  }
+  predefined_values {
+    additional_extensions {
+      object_id {
+        object_id_path = [1, 6]
+      }
+      value    = "c3RyaW5nCg=="
+      critical = true
+    }
+    aia_ocsp_servers = ["string"]
+    ca_options {
+      null_ca = true
+      is_ca = false
+    }
+    key_usage {
+      base_key_usage {
+        cert_sign          = false
+        content_commitment = true
+        crl_sign           = false
+        data_encipherment  = true
+        decipher_only      = true
+        digital_signature  = true
+        encipher_only      = true
+        key_agreement      = true
+        key_encipherment   = true
+      }
+      extended_key_usage {
+        client_auth      = true
+        code_signing     = true
+        email_protection = true
+        ocsp_signing     = true
+        server_auth      = true
+        time_stamping    = true
+      }
+      unknown_extended_key_usages {
+        object_id_path = [1, 6]
+      }
+    }
+    policy_ids {
+      object_id_path = [1, 6]
+    }
+  }
+  project = "%{project_name}"
+  labels = {
+    label-two = "value-two"
+  }
+}
+`, context)
+}
+
+func testAccPrivatecaCertificateTemplate_CertificateTemplateCaOptionMaxIssuerPathLenghIsZero(context map[string]interface{}) string {
+	return acctest.Nprintf(`
+resource "google_privateca_certificate_template" "primary" {
+  location         = "%{region}"
+  name             = "tf-test-template%{random_suffix}"
+  maximum_lifetime = "86400s"
+  description      = "Another updated sample certificate template"
+  identity_constraints {
+    allow_subject_alt_names_passthrough = true
+    allow_subject_passthrough           = true
+    cel_expression {
+      description = "Always true"
+      expression  = "true"
+      location    = "any.file.anywhere"
+      title       = "Sample expression"
+    }
+  }
+  passthrough_extensions {
+    additional_extensions {
+      object_id_path = [1, 6]
+    }
+    known_extensions = ["EXTENDED_KEY_USAGE"]
+  }
+  predefined_values {
+    additional_extensions {
+      object_id {
+        object_id_path = [1, 6]
+      }
+      value    = "c3RyaW5nCg=="
+      critical = true
+    }
+    aia_ocsp_servers = ["string"]
+    ca_options {
+      zero_max_issuer_path_length = true
+      max_issuer_path_length = 0
+    }
+    key_usage {
+      base_key_usage {
+        cert_sign          = false
+        content_commitment = true
+        crl_sign           = false
+        data_encipherment  = true
+        decipher_only      = true
+        digital_signature  = true
+        encipher_only      = true
+        key_agreement      = true
+        key_encipherment   = true
+      }
+      extended_key_usage {
+        client_auth      = true
+        code_signing     = true
+        email_protection = true
+        ocsp_signing     = true
+        server_auth      = true
+        time_stamping    = true
+      }
+      unknown_extended_key_usages {
+        object_id_path = [1, 6]
+      }
+    }
+    policy_ids {
+      object_id_path = [1, 6]
+    }
+  }
+  project = "%{project_name}"
+  labels = {
+    label-two = "value-two"
+  }
+}
+`, context)
+}


Reviews:
user: login: SirGitsalot
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T23:34:46Z
user: login: rohanchawla23
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T23:37:58Z
user: login: SirGitsalot
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T00:04:51Z

Review comments:
diff_hunk: @@ -178,11 +185,29 @@ properties:
         properties:
           - name: 'isCa'
             type: Boolean
-            description: Optional. Refers to the "CA" X.509 extension, which is a boolean value. When this value is missing, the extension will be omitted from the CA certificate.
+            description: |
+              Optional. Refers to the "CA" X.509 extension, which is a boolean value. When this value is true, the "CA" in Basic Constraints extension will be set to true.
+            send_empty_value: true
+          - name: 'nullCa'
path: mmv1/products/privateca/CertificateTemplate.yaml
user: login: SirGitsalot
body: I'm *pretty* sure this PR is good to go but want to make sure my understanding is correct: the current behavior is that a basic constraints extension is always created; setting`isCa` to `true` creates a BC extension with `ca: true`, and setting it to `false` or omitting it entirely creates a BC extension with `ca: false` (and as a result, matching the behavior of other cert resources that use `nonCa` would cause existing `google_privateca_certificate_template` configs with `isCa = false` to stop creating BC extensions rather than creating them with `ca: false`)
created_at: 2025-05-22T23:33:11Z
author_association: MEMBER
diff_hunk: @@ -178,11 +185,29 @@ properties:
         properties:
           - name: 'isCa'
             type: Boolean
-            description: Optional. Refers to the "CA" X.509 extension, which is a boolean value. When this value is missing, the extension will be omitted from the CA certificate.
+            description: |
+              Optional. Refers to the "CA" X.509 extension, which is a boolean value. When this value is true, the "CA" in Basic Constraints extension will be set to true.
+            send_empty_value: true
+          - name: 'nullCa'
path: mmv1/products/privateca/CertificateTemplate.yaml
user: login: rohanchawla23
body: Yes, that's correct!
created_at: 2025-05-22T23:37:58Z
author_association: CONTRIBUTOR

PR comments:
user: login: modular-magician
created_at: 2025-05-22T21:05:52Z
body: Hi there, I'm the Modular magician. I've detected the following information about your changes:

## Diff report

Your PR generated some diffs in downstreams - here they are.

`google` provider: [Diff](https://github.com/modular-magician/terraform-provider-google/compare/ec8ddad76f96698b82e75876000942154fd7b1b7..d6485881175b5cc036a02da8417da384049fcbea) ( 6 files changed, 871 insertions(+), 666 deletions(-))
`google-beta` provider: [Diff](https://github.com/modular-magician/terraform-provider-google-beta/compare/6585c943ebd560892971154403e4cd8dfd384a4b..23ba2b283095f53d4879764eea01523cc3f6a814) ( 6 files changed, 871 insertions(+), 666 deletions(-))
`terraform-google-conversion`: [Diff](https://github.com/modular-magician/terraform-google-conversion/compare/00cd01116768c1870ee858f17a9c52d059dca0e9..1de8bd606385d2e894266691fe6bded00a518521) ( 2 files changed, 92 insertions(+), 390 deletions(-))
Open in Cloud Shell: [Diff](https://github.com/modular-magician/docs-examples/compare/847b648f9d3b7bab2a61e29e19fda16427f006ac..0b4755a63be8346513693d47c8f38e57167652fd) ( 4 files changed, 186 insertions(+))





author_association: COLLABORATOR
user: login: modular-magician
created_at: 2025-05-22T21:09:57Z
body: #### Tests analytics
Total tests: 44
Passed tests: 24
Skipped tests: 8
Affected tests: 12

<details>
<summary>Click here to see the affected service packages</summary>
<blockquote>

<ul>
<li>privateca</li>

</ul>

</blockquote>
</details>
#### Action taken
<details>
<summary>Found 12 affected test(s) by replaying old test recordings. Starting RECORDING based on the most recent commit. Click here to see the affected tests
</summary>
<blockquote>
<ul>
<li>TestAccPrivatecaCertificateTemplateIamBindingGenerated</li>
<li>TestAccPrivatecaCertificateTemplateIamBindingGenerated_withCondition</li>
<li>TestAccPrivatecaCertificateTemplateIamMemberGenerated</li>
<li>TestAccPrivatecaCertificateTemplateIamMemberGenerated_withCondition</li>
<li>TestAccPrivatecaCertificateTemplateIamPolicyGenerated</li>
<li>TestAccPrivatecaCertificateTemplateIamPolicyGenerated_withCondition</li>
<li>TestAccPrivatecaCertificateTemplate_BasicCertificateTemplate</li>
<li>TestAccPrivatecaCertificateTemplate_BasicCertificateTemplateLongForm</li>
<li>TestAccPrivatecaCertificateTemplate_privatecaTemplateBasicExample</li>
<li>TestAccPrivatecaCertificateTemplate_privatecaTemplateZeroMaxIssuerPathLengthNullCaExample</li>
<li>TestAccPrivatecaCertificateTemplate_updateCaOption</li>
<li>TestAccPrivatecaCertificate_privatecaCertificateWithTemplateExample</li>

</ul>
</blockquote>
</details>

[Get to know how VCR tests work](https://googlecloudplatform.github.io/magic-modules/develop/test/test/)
author_association: COLLABORATOR
user: login: modular-magician
created_at: 2025-05-22T21:11:26Z
body: 🟢 Tests passed during RECORDING mode:
`TestAccPrivatecaCertificateTemplateIamBindingGenerated` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplateIamBindingGenerated.log)]
`TestAccPrivatecaCertificateTemplateIamBindingGenerated_withCondition` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplateIamBindingGenerated_withCondition.log)]
`TestAccPrivatecaCertificateTemplateIamMemberGenerated` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplateIamMemberGenerated.log)]
`TestAccPrivatecaCertificateTemplateIamMemberGenerated_withCondition` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplateIamMemberGenerated_withCondition.log)]
`TestAccPrivatecaCertificateTemplateIamPolicyGenerated` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplateIamPolicyGenerated.log)]
`TestAccPrivatecaCertificateTemplateIamPolicyGenerated_withCondition` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplateIamPolicyGenerated_withCondition.log)]
`TestAccPrivatecaCertificateTemplate_BasicCertificateTemplate` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplate_BasicCertificateTemplate.log)]
`TestAccPrivatecaCertificateTemplate_BasicCertificateTemplateLongForm` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplate_BasicCertificateTemplateLongForm.log)]
`TestAccPrivatecaCertificateTemplate_privatecaTemplateBasicExample` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplate_privatecaTemplateBasicExample.log)]
`TestAccPrivatecaCertificateTemplate_privatecaTemplateZeroMaxIssuerPathLengthNullCaExample` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplate_privatecaTemplateZeroMaxIssuerPathLengthNullCaExample.log)]
`TestAccPrivatecaCertificateTemplate_updateCaOption` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificateTemplate_updateCaOption.log)]
`TestAccPrivatecaCertificate_privatecaCertificateWithTemplateExample` [[Debug log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording/TestAccPrivatecaCertificate_privatecaCertificateWithTemplateExample.log)]

🟢 No issues found for passed tests after REPLAYING rerun.

---






🟢 All tests passed!

View the [build log](https://storage.cloud.google.com/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/build-log/recording_test.log) or the [debug log](https://console.cloud.google.com/storage/browser/ci-vcr-logs/beta/refs/heads/auto-pr-14070/artifacts/675aa1a1-345c-447d-912f-8ad4836c6774/recording) for each test
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-05-22T21:23:15Z
body: Hello! I am a robot. Tests will require approval from a repository maintainer to run. 

**Googlers:** For automatic test runs see go/terraform-auto-test-runs.

@SirGitsalot, a repository maintainer, has been assigned to [review your changes](https://googlecloudplatform.github.io/magic-modules/contribute/review-pr/). If you have not received review feedback within 2 business days, please leave a comment on this PR asking them to take a look.

You can help make sure that review is quick by [doing a self-review](https://googlecloudplatform.github.io/magic-modules/contribute/review-pr/) and by [running impacted tests locally](https://googlecloudplatform.github.io/magic-modules/get-started/run-provider-tests/).

author_association: NONE