diff --git a/.changelog/3353.txt b/.changelog/3353.txt new file mode 100644 index 00000000000..02c06e1a8b1 --- /dev/null +++ b/.changelog/3353.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +accesscontextmanager: added `spec` and `use_explicit_dry_run_spec` to `google_access_context_manager_service_perimeter` to test perimeter configurations in dry-run mode. +``` diff --git a/google/resource_access_context_manager_service_perimeter.go b/google/resource_access_context_manager_service_perimeter.go index a3b917a6bb8..d73d88be10a 100644 --- a/google/resource_access_context_manager_service_perimeter.go +++ b/google/resource_access_context_manager_service_perimeter.go @@ -92,6 +92,87 @@ with a common perimeter, but should not be able to share data among themselves.`, Default: "PERIMETER_TYPE_REGULAR", }, + "spec": { + Type: schema.TypeList, + Optional: true, + Description: `Proposed (or dry run) ServicePerimeter configuration. +This configuration allows to specify and test ServicePerimeter configuration +without enforcing actual access restrictions. Only allowed to be set when +the 'useExplicitDryRunSpec' flag is set.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "access_levels": { + Type: schema.TypeList, + Optional: true, + Description: `A list of AccessLevel resource names that allow resources within +the ServicePerimeter to be accessed from the internet. +AccessLevels listed must be in the same policy as this +ServicePerimeter. Referencing a nonexistent AccessLevel is a +syntax error. If no AccessLevel names are listed, resources within +the perimeter can only be accessed via GCP calls with request +origins within the perimeter. For Service Perimeter Bridge, must +be empty. + +Format: accessPolicies/{policy_id}/accessLevels/{access_level_name}`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + AtLeastOneOf: []string{"status.0.resources", "status.0.access_levels", "status.0.restricted_services"}, + }, + "resources": { + Type: schema.TypeList, + Optional: true, + Description: `A list of GCP resources that are inside of the service perimeter. +Currently only projects are allowed. +Format: projects/{project_number}`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + AtLeastOneOf: []string{"status.0.resources", "status.0.access_levels", "status.0.restricted_services"}, + }, + "restricted_services": { + Type: schema.TypeList, + Optional: true, + Description: `GCP services that are subject to the Service Perimeter +restrictions. Must contain a list of services. For example, if +'storage.googleapis.com' is specified, access to the storage +buckets inside the perimeter must meet the perimeter's access +restrictions.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + AtLeastOneOf: []string{"status.0.resources", "status.0.access_levels", "status.0.restricted_services"}, + }, + "vpc_accessible_services": { + Type: schema.TypeList, + Optional: true, + Description: `Specifies how APIs are allowed to communicate within the Service +Perimeter.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allowed_services": { + Type: schema.TypeList, + Optional: true, + Description: `The list of APIs usable within the Service Perimeter. +Must be empty unless 'enableRestriction' is True.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "enable_restriction": { + Type: schema.TypeBool, + Optional: true, + Description: `Whether to restrict API calls within the Service Perimeter to the +list of APIs specified in 'allowedServices'.`, + }, + }, + }, + }, + }, + }, + }, "status": { Type: schema.TypeList, Optional: true, @@ -174,6 +255,19 @@ list of APIs specified in 'allowedServices'.`, }, }, }, + "use_explicit_dry_run_spec": { + Type: schema.TypeBool, + Optional: true, + Description: `Use explicit dry run spec flag. Ordinarily, a dry-run spec implicitly exists +for all Service Perimeters, and that spec is identical to the status for those +Service Perimeters. When this flag is set, it inhibits the generation of the +implicit spec, thereby allowing the user to explicitly provide a +configuration ("spec") to use in a dry-run version of the Service Perimeter. +This allows the user to test changes to the enforced config ("status") without +actually enforcing them. This testing is done through analyzing the differences +between currently enforced and suggested restrictions. useExplicitDryRunSpec must +bet set to True if any of the fields in the spec are set to non-default values.`, + }, "create_time": { Type: schema.TypeString, Computed: true, @@ -216,6 +310,18 @@ func resourceAccessContextManagerServicePerimeterCreate(d *schema.ResourceData, } else if v, ok := d.GetOkExists("status"); !isEmptyValue(reflect.ValueOf(statusProp)) && (ok || !reflect.DeepEqual(v, statusProp)) { obj["status"] = statusProp } + specProp, err := expandAccessContextManagerServicePerimeterSpec(d.Get("spec"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("spec"); !isEmptyValue(reflect.ValueOf(specProp)) && (ok || !reflect.DeepEqual(v, specProp)) { + obj["spec"] = specProp + } + useExplicitDryRunSpecProp, err := expandAccessContextManagerServicePerimeterUseExplicitDryRunSpec(d.Get("use_explicit_dry_run_spec"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("use_explicit_dry_run_spec"); !isEmptyValue(reflect.ValueOf(useExplicitDryRunSpecProp)) && (ok || !reflect.DeepEqual(v, useExplicitDryRunSpecProp)) { + obj["useExplicitDryRunSpec"] = useExplicitDryRunSpecProp + } parentProp, err := expandAccessContextManagerServicePerimeterParent(d.Get("parent"), d, config) if err != nil { return err @@ -318,6 +424,12 @@ func resourceAccessContextManagerServicePerimeterRead(d *schema.ResourceData, me if err := d.Set("status", flattenAccessContextManagerServicePerimeterStatus(res["status"], d, config)); err != nil { return fmt.Errorf("Error reading ServicePerimeter: %s", err) } + if err := d.Set("spec", flattenAccessContextManagerServicePerimeterSpec(res["spec"], d, config)); err != nil { + return fmt.Errorf("Error reading ServicePerimeter: %s", err) + } + if err := d.Set("use_explicit_dry_run_spec", flattenAccessContextManagerServicePerimeterUseExplicitDryRunSpec(res["useExplicitDryRunSpec"], d, config)); err != nil { + return fmt.Errorf("Error reading ServicePerimeter: %s", err) + } if err := d.Set("name", flattenAccessContextManagerServicePerimeterName(res["name"], d, config)); err != nil { return fmt.Errorf("Error reading ServicePerimeter: %s", err) } @@ -347,6 +459,18 @@ func resourceAccessContextManagerServicePerimeterUpdate(d *schema.ResourceData, } else if v, ok := d.GetOkExists("status"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, statusProp)) { obj["status"] = statusProp } + specProp, err := expandAccessContextManagerServicePerimeterSpec(d.Get("spec"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("spec"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, specProp)) { + obj["spec"] = specProp + } + useExplicitDryRunSpecProp, err := expandAccessContextManagerServicePerimeterUseExplicitDryRunSpec(d.Get("use_explicit_dry_run_spec"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("use_explicit_dry_run_spec"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, useExplicitDryRunSpecProp)) { + obj["useExplicitDryRunSpec"] = useExplicitDryRunSpecProp + } obj, err = resourceAccessContextManagerServicePerimeterEncoder(d, meta, obj) if err != nil { @@ -379,6 +503,14 @@ func resourceAccessContextManagerServicePerimeterUpdate(d *schema.ResourceData, if d.HasChange("status") { updateMask = append(updateMask, "status") } + + if d.HasChange("spec") { + updateMask = append(updateMask, "spec") + } + + if d.HasChange("use_explicit_dry_run_spec") { + updateMask = append(updateMask, "useExplicitDryRunSpec") + } // updateMask is a URL parameter but not present in the schema, so replaceVars // won't set it url, err = addQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) @@ -536,6 +668,64 @@ func flattenAccessContextManagerServicePerimeterStatusVPCAccessibleServicesAllow return schema.NewSet(schema.HashString, v.([]interface{})) } +func flattenAccessContextManagerServicePerimeterSpec(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["resources"] = + flattenAccessContextManagerServicePerimeterSpecResources(original["resources"], d, config) + transformed["access_levels"] = + flattenAccessContextManagerServicePerimeterSpecAccessLevels(original["accessLevels"], d, config) + transformed["restricted_services"] = + flattenAccessContextManagerServicePerimeterSpecRestrictedServices(original["restrictedServices"], d, config) + transformed["vpc_accessible_services"] = + flattenAccessContextManagerServicePerimeterSpecVPCAccessibleServices(original["vpcAccessibleServices"], d, config) + return []interface{}{transformed} +} +func flattenAccessContextManagerServicePerimeterSpecResources(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAccessContextManagerServicePerimeterSpecAccessLevels(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAccessContextManagerServicePerimeterSpecRestrictedServices(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAccessContextManagerServicePerimeterSpecVPCAccessibleServices(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["enable_restriction"] = + flattenAccessContextManagerServicePerimeterSpecVPCAccessibleServicesEnableRestriction(original["enableRestriction"], d, config) + transformed["allowed_services"] = + flattenAccessContextManagerServicePerimeterSpecVPCAccessibleServicesAllowedServices(original["allowedServices"], d, config) + return []interface{}{transformed} +} +func flattenAccessContextManagerServicePerimeterSpecVPCAccessibleServicesEnableRestriction(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAccessContextManagerServicePerimeterSpecVPCAccessibleServicesAllowedServices(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAccessContextManagerServicePerimeterUseExplicitDryRunSpec(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + func flattenAccessContextManagerServicePerimeterName(v interface{}, d *schema.ResourceData, config *Config) interface{} { return v } @@ -640,6 +830,96 @@ func expandAccessContextManagerServicePerimeterStatusVPCAccessibleServicesAllowe return v, nil } +func expandAccessContextManagerServicePerimeterSpec(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedResources, err := expandAccessContextManagerServicePerimeterSpecResources(original["resources"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedResources); val.IsValid() && !isEmptyValue(val) { + transformed["resources"] = transformedResources + } + + transformedAccessLevels, err := expandAccessContextManagerServicePerimeterSpecAccessLevels(original["access_levels"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedAccessLevels); val.IsValid() && !isEmptyValue(val) { + transformed["accessLevels"] = transformedAccessLevels + } + + transformedRestrictedServices, err := expandAccessContextManagerServicePerimeterSpecRestrictedServices(original["restricted_services"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedRestrictedServices); val.IsValid() && !isEmptyValue(val) { + transformed["restrictedServices"] = transformedRestrictedServices + } + + transformedVPCAccessibleServices, err := expandAccessContextManagerServicePerimeterSpecVPCAccessibleServices(original["vpc_accessible_services"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedVPCAccessibleServices); val.IsValid() && !isEmptyValue(val) { + transformed["vpcAccessibleServices"] = transformedVPCAccessibleServices + } + + return transformed, nil +} + +func expandAccessContextManagerServicePerimeterSpecResources(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAccessContextManagerServicePerimeterSpecAccessLevels(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAccessContextManagerServicePerimeterSpecRestrictedServices(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAccessContextManagerServicePerimeterSpecVPCAccessibleServices(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedEnableRestriction, err := expandAccessContextManagerServicePerimeterSpecVPCAccessibleServicesEnableRestriction(original["enable_restriction"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedEnableRestriction); val.IsValid() && !isEmptyValue(val) { + transformed["enableRestriction"] = transformedEnableRestriction + } + + transformedAllowedServices, err := expandAccessContextManagerServicePerimeterSpecVPCAccessibleServicesAllowedServices(original["allowed_services"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedAllowedServices); val.IsValid() && !isEmptyValue(val) { + transformed["allowedServices"] = transformedAllowedServices + } + + return transformed, nil +} + +func expandAccessContextManagerServicePerimeterSpecVPCAccessibleServicesEnableRestriction(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAccessContextManagerServicePerimeterSpecVPCAccessibleServicesAllowedServices(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAccessContextManagerServicePerimeterUseExplicitDryRunSpec(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + func expandAccessContextManagerServicePerimeterParent(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { return v, nil } diff --git a/google/resource_access_context_manager_service_perimeter_test.go b/google/resource_access_context_manager_service_perimeter_test.go index b4ba8ace29a..2ec49e06ea3 100644 --- a/google/resource_access_context_manager_service_perimeter_test.go +++ b/google/resource_access_context_manager_service_perimeter_test.go @@ -62,6 +62,22 @@ func testAccAccessContextManagerServicePerimeter_updateTest(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + { + Config: testAccAccessContextManagerServicePerimeter_updateDryrun(org, "my policy", "level", "perimeter"), + }, + { + ResourceName: "google_access_context_manager_service_perimeter.test-access", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAccessContextManagerServicePerimeter_updateAllowed(org, "my policy", "level", "perimeter"), + }, + { + ResourceName: "google_access_context_manager_service_perimeter.test-access", + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -190,3 +206,42 @@ resource "google_access_context_manager_service_perimeter" "test-access" { } `, org, policyTitle, levelTitleName, levelTitleName, perimeterTitleName, perimeterTitleName) } + +func testAccAccessContextManagerServicePerimeter_updateDryrun(org, policyTitle, levelTitleName, perimeterTitleName string) string { + return fmt.Sprintf(` +resource "google_access_context_manager_access_policy" "test-access" { + parent = "organizations/%s" + title = "%s" +} + +resource "google_access_context_manager_access_level" "test-access" { + parent = "accessPolicies/${google_access_context_manager_access_policy.test-access.name}" + name = "accessPolicies/${google_access_context_manager_access_policy.test-access.name}/accessLevels/%s" + title = "%s" + description = "hello" + basic { + combining_function = "AND" + conditions { + ip_subnetworks = ["192.0.4.0/24"] + } + } +} + +resource "google_access_context_manager_service_perimeter" "test-access" { + parent = "accessPolicies/${google_access_context_manager_access_policy.test-access.name}" + name = "accessPolicies/${google_access_context_manager_access_policy.test-access.name}/servicePerimeters/%s" + title = "%s" + perimeter_type = "PERIMETER_TYPE_REGULAR" + status { + restricted_services = ["bigquery.googleapis.com"] + } + + spec { + restricted_services = ["storage.googleapis.com"] + access_levels = [google_access_context_manager_access_level.test-access.name] + } + + use_explicit_dry_run_spec = true +} +`, org, policyTitle, levelTitleName, levelTitleName, perimeterTitleName, perimeterTitleName) +} diff --git a/website/docs/r/access_context_manager_service_perimeter.html.markdown b/website/docs/r/access_context_manager_service_perimeter.html.markdown index a57d5314889..bd527e7a4d7 100644 --- a/website/docs/r/access_context_manager_service_perimeter.html.markdown +++ b/website/docs/r/access_context_manager_service_perimeter.html.markdown @@ -47,8 +47,8 @@ To get more information about ServicePerimeter, see: ```hcl resource "google_access_context_manager_service_perimeter" "service-perimeter" { parent = "accessPolicies/${google_access_context_manager_access_policy.access-policy.name}" - name = "accessPolicies/${google_access_context_manager_access_policy.access-policy.name}/servicePerimeters/restrict_all" - title = "restrict_all" + name = "accessPolicies/${google_access_context_manager_access_policy.access-policy.name}/servicePerimeters/restrict_storage" + title = "restrict_storage" status { restricted_services = ["storage.googleapis.com"] } @@ -67,12 +67,40 @@ resource "google_access_context_manager_access_level" "access-level" { } } regions = [ - "CH", - "IT", - "US", + "CH", + "IT", + "US", ] + } } +} + +resource "google_access_context_manager_access_policy" "access-policy" { + parent = "organizations/123456789" + title = "my policy" +} +``` +## Example Usage - Access Context Manager Service Perimeter Dry Run + + +```hcl +resource "google_access_context_manager_service_perimeter" "service-perimeter" { + parent = "accessPolicies/${google_access_context_manager_access_policy.access-policy.name}" + name = "accessPolicies/${google_access_context_manager_access_policy.access-policy.name}/servicePerimeters/restrict_bigquery_dryrun_storage" + title = "restrict_bigquery_dryrun_storage" + + # Service 'bigquery.googleapis.com' will be restricted. + status { + restricted_services = ["bigquery.googleapis.com"] + } + + # Service 'storage.googleapis.com' will be in dry-run mode. + spec { + restricted_services = ["storage.googleapis.com"] } + + use_explicit_dry_run_spec = true + } resource "google_access_context_manager_access_policy" "access-policy" { @@ -133,6 +161,25 @@ The following arguments are supported: restricted services and access levels that determine perimeter content and boundaries. Structure is documented below. +* `spec` - + (Optional) + Proposed (or dry run) ServicePerimeter configuration. + This configuration allows to specify and test ServicePerimeter configuration + without enforcing actual access restrictions. Only allowed to be set when + the `useExplicitDryRunSpec` flag is set. Structure is documented below. + +* `use_explicit_dry_run_spec` - + (Optional) + Use explicit dry run spec flag. Ordinarily, a dry-run spec implicitly exists + for all Service Perimeters, and that spec is identical to the status for those + Service Perimeters. When this flag is set, it inhibits the generation of the + implicit spec, thereby allowing the user to explicitly provide a + configuration ("spec") to use in a dry-run version of the Service Perimeter. + This allows the user to test changes to the enforced config ("status") without + actually enforcing them. This testing is done through analyzing the differences + between currently enforced and suggested restrictions. useExplicitDryRunSpec must + bet set to True if any of the fields in the spec are set to non-default values. + The `status` block supports: @@ -168,6 +215,52 @@ The `status` block supports: Perimeter. Structure is documented below. +The `vpc_accessible_services` block supports: + +* `enable_restriction` - + (Optional) + Whether to restrict API calls within the Service Perimeter to the + list of APIs specified in 'allowedServices'. + +* `allowed_services` - + (Optional) + The list of APIs usable within the Service Perimeter. + Must be empty unless `enableRestriction` is True. + +The `spec` block supports: + +* `resources` - + (Optional) + A list of GCP resources that are inside of the service perimeter. + Currently only projects are allowed. + Format: projects/{project_number} + +* `access_levels` - + (Optional) + A list of AccessLevel resource names that allow resources within + the ServicePerimeter to be accessed from the internet. + AccessLevels listed must be in the same policy as this + ServicePerimeter. Referencing a nonexistent AccessLevel is a + syntax error. If no AccessLevel names are listed, resources within + the perimeter can only be accessed via GCP calls with request + origins within the perimeter. For Service Perimeter Bridge, must + be empty. + Format: accessPolicies/{policy_id}/accessLevels/{access_level_name} + +* `restricted_services` - + (Optional) + GCP services that are subject to the Service Perimeter + restrictions. Must contain a list of services. For example, if + `storage.googleapis.com` is specified, access to the storage + buckets inside the perimeter must meet the perimeter's access + restrictions. + +* `vpc_accessible_services` - + (Optional) + Specifies how APIs are allowed to communicate within the Service + Perimeter. Structure is documented below. + + The `vpc_accessible_services` block supports: * `enable_restriction` -