From 4a497b1b40707a98eef74aee5f76df5bd5a5c201 Mon Sep 17 00:00:00 2001 From: appilon Date: Wed, 7 Jul 2021 11:04:50 -0400 Subject: [PATCH] Chrome Policy API support (#97) * Implement chrome policy schema datasources * Add missing lines * make diffsuppressfunc, revert export of org_unit_id to contain prefix for consistency * Implement chrome policy resource * Fix ordering * Add resource * Implement update * add validations and get tests to pass * Fix implementation and write tests * make test more complex * Write even more thorough test, fix description * Add datasource test * Add examples * generate examples * return early if error * add some nil checks * add more nil checks Co-authored-by: Megan Bang --- .github/infra/gcp.tf | 6 + docs/data-sources/chrome_policy_schema.md | 95 +++ docs/resources/chrome_policy.md | 52 ++ .../data-source.tf | 7 + .../googleworkspace_chrome_policy/resource.tf | 14 + .../data_source_chrome_policy_schema.go | 266 ++++++++ .../data_source_chrome_policy_schema_test.go | 38 ++ internal/provider/data_source_privileges.go | 4 +- internal/provider/provider.go | 23 +- internal/provider/provider_config.go | 23 + internal/provider/resource_chrome_policy.go | 574 ++++++++++++++++++ .../provider/resource_chrome_policy_test.go | 252 ++++++++ internal/provider/resource_role_assignment.go | 26 +- internal/provider/retry_utils.go | 18 + internal/provider/services.go | 35 ++ 15 files changed, 1408 insertions(+), 25 deletions(-) create mode 100644 docs/data-sources/chrome_policy_schema.md create mode 100644 docs/resources/chrome_policy.md create mode 100644 examples/data-sources/googleworkspace_chrome_policy_schema/data-source.tf create mode 100644 examples/resources/googleworkspace_chrome_policy/resource.tf create mode 100644 internal/provider/data_source_chrome_policy_schema.go create mode 100644 internal/provider/data_source_chrome_policy_schema_test.go create mode 100644 internal/provider/resource_chrome_policy.go create mode 100644 internal/provider/resource_chrome_policy_test.go diff --git a/.github/infra/gcp.tf b/.github/infra/gcp.tf index e72263f4..ebfdfb98 100644 --- a/.github/infra/gcp.tf +++ b/.github/infra/gcp.tf @@ -57,3 +57,9 @@ resource "google_project_service" "group-settings" { project = data.google_project.project.project_id service = "groupssettings.googleapis.com" } + + // Enable the chrome policy api service + resource "google_project_service" "chrome-policy" { + project = data.google_project.project.project_id + service = "chromepolicy.googleapis.com" + } diff --git a/docs/data-sources/chrome_policy_schema.md b/docs/data-sources/chrome_policy_schema.md new file mode 100644 index 00000000..68eb260c --- /dev/null +++ b/docs/data-sources/chrome_policy_schema.md @@ -0,0 +1,95 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "googleworkspace_chrome_policy_schema Data Source - terraform-provider-googleworkspace" +subcategory: "" +description: |- + Chrome Policy Schema data source in the Terraform Googleworkspace provider. +--- + +# googleworkspace_chrome_policy_schema (Data Source) + +Chrome Policy Schema data source in the Terraform Googleworkspace provider. + +## Example Usage + +```terraform +data "googleworkspace_chrome_policy_schema" "example" { + schema_name = "chrome.printers.AllowForUsers" +} + +output "field_descriptions" { + value = data.googleworkspace_chrome_policy_schema.example.field_descriptions +} +``` + + +## Schema + +### Required + +- **schema_name** (String) The full qualified name of the policy schema + +### Optional + +- **id** (String) The ID of this resource. + +### Read-Only + +- **access_restrictions** (List of String) Specific access restrictions related to this policy. +- **additional_target_key_names** (List of Object) Additional key names that will be used to identify the target of the policy value. When specifying a policyTargetKey, each of the additional keys specified here will have to be included in the additionalTargetKeys map. (see [below for nested schema](#nestedatt--additional_target_key_names)) +- **definition** (List of Object) Schema definition using proto descriptor. (see [below for nested schema](#nestedatt--definition)) +- **field_descriptions** (String) Detailed description of each field that is part of the schema, represented as a JSON string. +- **notices** (List of Object) Special notice messages related to setting certain values in certain fields in the schema. (see [below for nested schema](#nestedatt--notices)) +- **policy_description** (String) Description about the policy schema for user consumption. +- **support_uri** (String) URI to related support article for this schema. + + +### Nested Schema for `additional_target_key_names` + +Read-Only: + +- **key** (String) +- **key_description** (String) + + + +### Nested Schema for `definition` + +Read-Only: + +- **enum_type** (List of Object) (see [below for nested schema](#nestedobjatt--definition--enum_type)) +- **message_type** (String) +- **name** (String) +- **package** (String) +- **syntax** (String) + + +### Nested Schema for `definition.enum_type` + +Read-Only: + +- **name** (String) +- **value** (List of Object) (see [below for nested schema](#nestedobjatt--definition--enum_type--value)) + + +### Nested Schema for `definition.enum_type.value` + +Read-Only: + +- **name** (String) +- **number** (Number) + + + + + +### Nested Schema for `notices` + +Read-Only: + +- **acknowledgement_required** (Boolean) +- **field** (String) +- **notice_message** (String) +- **notice_value** (String) + + diff --git a/docs/resources/chrome_policy.md b/docs/resources/chrome_policy.md new file mode 100644 index 00000000..223f49c6 --- /dev/null +++ b/docs/resources/chrome_policy.md @@ -0,0 +1,52 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "googleworkspace_chrome_policy Resource - terraform-provider-googleworkspace" +subcategory: "" +description: |- + Chrome Policy resource in the Terraform Googleworkspace provider. Currently only supports policies not requiring additionalTargetKeys. +--- + +# googleworkspace_chrome_policy (Resource) + +Chrome Policy resource in the Terraform Googleworkspace provider. Currently only supports policies not requiring additionalTargetKeys. + +## Example Usage + +```terraform +resource "googleworkspace_org_unit" "example" { + name = "example" + parent_org_unit_path = "/" +} + +resource "googleworkspace_chrome_policy" "example" { + org_unit_id = googleworkspace_org_unit.test.id + policies { + schema_name = "chrome.users.MaxConnectionsPerProxy" + schema_values = { + maxConnectionsPerProxy = jsonencode(34) + } + } +} +``` + + +## Schema + +### Required + +- **org_unit_id** (String) The target org unit on which this policy is applied. +- **policies** (Block List, Min: 1) Policies to set for the org unit (see [below for nested schema](#nestedblock--policies)) + +### Optional + +- **id** (String) The ID of this resource. + + +### Nested Schema for `policies` + +Required: + +- **schema_name** (String) The full qualified name of the policy schema. +- **schema_values** (Map of String) JSON encoded map that represents key/value pairs that correspond to the given schema. + + diff --git a/examples/data-sources/googleworkspace_chrome_policy_schema/data-source.tf b/examples/data-sources/googleworkspace_chrome_policy_schema/data-source.tf new file mode 100644 index 00000000..e4d78235 --- /dev/null +++ b/examples/data-sources/googleworkspace_chrome_policy_schema/data-source.tf @@ -0,0 +1,7 @@ +data "googleworkspace_chrome_policy_schema" "example" { + schema_name = "chrome.printers.AllowForUsers" +} + +output "field_descriptions" { + value = data.googleworkspace_chrome_policy_schema.example.field_descriptions +} \ No newline at end of file diff --git a/examples/resources/googleworkspace_chrome_policy/resource.tf b/examples/resources/googleworkspace_chrome_policy/resource.tf new file mode 100644 index 00000000..25663982 --- /dev/null +++ b/examples/resources/googleworkspace_chrome_policy/resource.tf @@ -0,0 +1,14 @@ +resource "googleworkspace_org_unit" "example" { + name = "example" + parent_org_unit_path = "/" +} + +resource "googleworkspace_chrome_policy" "example" { + org_unit_id = googleworkspace_org_unit.test.id + policies { + schema_name = "chrome.users.MaxConnectionsPerProxy" + schema_values = { + maxConnectionsPerProxy = jsonencode(34) + } + } +} \ No newline at end of file diff --git a/internal/provider/data_source_chrome_policy_schema.go b/internal/provider/data_source_chrome_policy_schema.go new file mode 100644 index 00000000..0e26e1f9 --- /dev/null +++ b/internal/provider/data_source_chrome_policy_schema.go @@ -0,0 +1,266 @@ +package googleworkspace + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "google.golang.org/api/chromepolicy/v1" +) + +func dataSourceChromePolicySchema() *schema.Resource { + return &schema.Resource{ + Description: "Chrome Policy Schema data source in the Terraform Googleworkspace provider.", + + ReadContext: dataSourceChromePolicySchemaRead, + + Schema: map[string]*schema.Schema{ + // Intentionally ignoring field 'name' https://developers.google.com/chrome/policy/reference/rest/v1/customers.policySchemas#PolicySchema + // it is a confusing field, that includes url segments the practitioner won't find useful. + // Format: name=customers/{customer}/policySchemas/{schema_namespace} + // Using the output field 'schema_name' instead as the field the practitioner specifies + "schema_name": { + Description: "The full qualified name of the policy schema", + Type: schema.TypeString, + Required: true, + }, + "policy_description": { + Description: "Description about the policy schema for user consumption.", + Type: schema.TypeString, + Computed: true, + }, + "additional_target_key_names": { + Description: "Additional key names that will be used to identify the target of the policy value. When specifying a policyTargetKey, each of the additional keys specified here will have to be included in the additionalTargetKeys map.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Description: "Key name.", + Type: schema.TypeString, + Computed: true, + }, + "key_description": { + Description: "Key description.", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "definition": { + Description: "Schema definition using proto descriptor.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Description: "file name, relative to root of source tree", + Type: schema.TypeString, + Computed: true, + }, + "package": { + Description: "e.g. 'foo', 'foo.bar', etc.", + Type: schema.TypeString, + Computed: true, + }, + "message_type": { + Description: "All top-level definitions in this file, represented as a JSON string", + Type: schema.TypeString, + Computed: true, + }, + "enum_type": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "number": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "syntax": { + Description: "The syntax of the proto file. The supported values are 'proto' and 'proto3'.", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "field_descriptions": { + Description: "Detailed description of each field that is part of the schema, represented as a JSON string.", + Type: schema.TypeString, + Computed: true, + }, + "access_restrictions": { + Description: "Specific access restrictions related to this policy.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "notices": { + Description: "Special notice messages related to setting certain values in certain fields in the schema.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "field": { + Description: "The field name associated with the notice.", + Type: schema.TypeString, + Computed: true, + }, + "notice_value": { + Description: "The value of the field that has a notice. When setting the field to this value, the user may be required to acknowledge the notice message in order for the value to be set.", + Type: schema.TypeString, + Computed: true, + }, + "notice_message": { + Description: "The notice message associate with the value of the field.", + Type: schema.TypeString, + Computed: true, + }, + "acknowledgement_required": { + Description: "Whether the user needs to acknowledge the notice message before the value can be set.", + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "support_uri": { + Description: "URI to related support article for this schema.", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceChromePolicySchemaRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*apiClient) + + chromePolicyService, diags := client.NewChromePolicyService() + if diags.HasError() { + return diags + } + + chromePolicySchemasService, diags := GetChromePolicySchemasService(chromePolicyService) + if diags.HasError() { + return diags + } + + policySchema, err := chromePolicySchemasService.Get(fmt.Sprintf("customers/%s/policySchemas/%s", client.Customer, d.Get("schema_name").(string))).Do() + if err != nil { + return diag.FromErr(err) + } + + d.SetId(policySchema.SchemaName) + d.Set("schema_name", policySchema.SchemaName) + d.Set("policy_description", policySchema.PolicyDescription) + d.Set("support_uri", policySchema.SupportUri) + if err := d.Set("additional_target_key_names", flattenAdditionalTargetKeyNames(policySchema.AdditionalTargetKeyNames)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("definition", flattenDefinition(policySchema.Definition)); err != nil { + return diag.FromErr(err) + } + + // this attribute contains recursive types, so we store it as json + fieldDescriptions, _ := json.MarshalIndent(policySchema.FieldDescriptions, "", " ") + d.Set("field_descriptions", string(fieldDescriptions)) + + if err := d.Set("access_restrictions", policySchema.AccessRestrictions); err != nil { + return diag.FromErr(err) + } + if err := d.Set("notices", flattenNotices(policySchema.Notices)); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func flattenNotices(ns []*chromepolicy.GoogleChromePolicyV1PolicySchemaNoticeDescription) []interface{} { + result := make([]interface{}, len(ns)) + + for i, n := range ns { + obj := make(map[string]interface{}) + obj["field"] = n.Field + obj["notice_value"] = n.NoticeValue + obj["notice_message"] = n.NoticeMessage + obj["acknowledgement_required"] = n.AcknowledgementRequired + result[i] = obj + } + + return result +} + +func flattenEnumType(es []*chromepolicy.Proto2EnumDescriptorProto) []interface{} { + result := make([]interface{}, len(es)) + + for i, e := range es { + obj := make(map[string]interface{}) + + obj["name"] = e.Name + values := make([]interface{}, len(e.Value)) + for j, v := range e.Value { + values[j] = map[string]interface{}{ + "name": v.Name, + "number": int(v.Number), + } + } + obj["value"] = values + + result[i] = obj + } + + return result +} + +func flattenDefinition(d *chromepolicy.Proto2FileDescriptorProto) []interface{} { + result := make([]interface{}, 1) + obj := make(map[string]interface{}) + + obj["name"] = d.Name + obj["package"] = d.Package + obj["syntax"] = d.Syntax + obj["enum_type"] = flattenEnumType(d.EnumType) + // this attribute contains recursive types, so we store it as json + msgType, _ := json.MarshalIndent(d.MessageType, "", " ") + obj["message_type"] = string(msgType) + + result[0] = obj + return result +} + +func flattenAdditionalTargetKeyNames(as []*chromepolicy.GoogleChromePolicyV1AdditionalTargetKeyName) []interface{} { + result := make([]interface{}, len(as)) + for i, a := range as { + obj := make(map[string]interface{}) + obj["key"] = a.Key + obj["key_description"] = a.KeyDescription + result[i] = obj + } + return result +} diff --git a/internal/provider/data_source_chrome_policy_schema_test.go b/internal/provider/data_source_chrome_policy_schema_test.go new file mode 100644 index 00000000..f4e10996 --- /dev/null +++ b/internal/provider/data_source_chrome_policy_schema_test.go @@ -0,0 +1,38 @@ +package googleworkspace + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceChromePolicySchema(t *testing.T) { + t.Parallel() + + schemaName := "chrome.printers.AllowForUsers" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceChromePolicySchema(schemaName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.googleworkspace_chrome_policy_schema.test", "schema_name", schemaName), + resource.TestCheckResourceAttr("data.googleworkspace_chrome_policy_schema.test", "policy_description", "Allows a printer for users in a given organization."), + resource.TestCheckResourceAttr("data.googleworkspace_chrome_policy_schema.test", "additional_target_key_names.#", "1"), + resource.TestCheckResourceAttr("data.googleworkspace_chrome_policy_schema.test", "additional_target_key_names.0.key", "printer_id"), + ), + }, + }, + }) +} + +func testAccDataSourceChromePolicySchema(schemaName string) string { + return fmt.Sprintf(` +data "googleworkspace_chrome_policy_schema" "test" { + schema_name = "%s" +} +`, schemaName) +} diff --git a/internal/provider/data_source_privileges.go b/internal/provider/data_source_privileges.go index 34aa0971..f9e04679 100644 --- a/internal/provider/data_source_privileges.go +++ b/internal/provider/data_source_privileges.go @@ -13,7 +13,7 @@ func dataSourcePrivileges() *schema.Resource { return &schema.Resource{ Description: "Privileges data source in the Terraform Googleworkspace provider.", - ReadContext: dataSourcePrivilegeRead, + ReadContext: dataSourcePrivilegesRead, Schema: map[string]*schema.Schema{ "etag": { @@ -59,7 +59,7 @@ func dataSourcePrivileges() *schema.Resource { } } -func dataSourcePrivilegeRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func dataSourcePrivilegesRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*apiClient) directoryService, diags := client.NewDirectoryService() diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7c4945c2..cecec6f1 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -14,6 +14,7 @@ import ( ) var DefaultClientScopes = []string{ + "https://www.googleapis.com/auth/chrome.management.policy", "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/admin.directory.customer", "https://www.googleapis.com/auth/admin.directory.domain", @@ -87,18 +88,20 @@ func New(version string) func() *schema.Provider { }, }, DataSourcesMap: map[string]*schema.Resource{ - "googleworkspace_domain": dataSourceDomain(), - "googleworkspace_domain_alias": dataSourceDomainAlias(), - "googleworkspace_group": dataSourceGroup(), - "googleworkspace_group_member": dataSourceGroupMember(), - "googleworkspace_group_settings": dataSourceGroupSettings(), - "googleworkspace_org_unit": dataSourceOrgUnit(), - "googleworkspace_privileges": dataSourcePrivileges(), - "googleworkspace_role": dataSourceRole(), - "googleworkspace_schema": dataSourceSchema(), - "googleworkspace_user": dataSourceUser(), + "googleworkspace_chrome_policy_schema": dataSourceChromePolicySchema(), + "googleworkspace_domain": dataSourceDomain(), + "googleworkspace_domain_alias": dataSourceDomainAlias(), + "googleworkspace_group": dataSourceGroup(), + "googleworkspace_group_member": dataSourceGroupMember(), + "googleworkspace_group_settings": dataSourceGroupSettings(), + "googleworkspace_org_unit": dataSourceOrgUnit(), + "googleworkspace_privileges": dataSourcePrivileges(), + "googleworkspace_role": dataSourceRole(), + "googleworkspace_schema": dataSourceSchema(), + "googleworkspace_user": dataSourceUser(), }, ResourcesMap: map[string]*schema.Resource{ + "googleworkspace_chrome_policy": resourceChromePolicy(), "googleworkspace_domain": resourceDomain(), "googleworkspace_domain_alias": resourceDomainAlias(), "googleworkspace_group": resourceGroup(), diff --git a/internal/provider/provider_config.go b/internal/provider/provider_config.go index 92ae0bf5..67498e7a 100644 --- a/internal/provider/provider_config.go +++ b/internal/provider/provider_config.go @@ -11,6 +11,7 @@ import ( "golang.org/x/oauth2" googleoauth "golang.org/x/oauth2/google" + "google.golang.org/api/chromepolicy/v1" "google.golang.org/api/option" directory "google.golang.org/api/admin/directory/v1" @@ -73,6 +74,28 @@ func (c *apiClient) loadAndValidate(ctx context.Context) diag.Diagnostics { return diags } +func (c *apiClient) NewChromePolicyService() (*chromepolicy.Service, diag.Diagnostics) { + var diags diag.Diagnostics + + log.Printf("[INFO] Instantiating Google Admin Chrome Policy service") + + chromePolicyService, err := chromepolicy.NewService(context.Background(), option.WithHTTPClient(c.client)) + if err != nil { + return nil, diag.FromErr(err) + } + + if chromePolicyService == nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Directory Service could not be created.", + }) + + return nil, diags + } + + return chromePolicyService, diags +} + func (c *apiClient) NewDirectoryService() (*directory.Service, diag.Diagnostics) { var diags diag.Diagnostics diff --git a/internal/provider/resource_chrome_policy.go b/internal/provider/resource_chrome_policy.go new file mode 100644 index 00000000..2fdd0765 --- /dev/null +++ b/internal/provider/resource_chrome_policy.go @@ -0,0 +1,574 @@ +package googleworkspace + +import ( + "context" + "encoding/json" + "fmt" + "log" + "reflect" + "strconv" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "google.golang.org/api/chromepolicy/v1" +) + +func resourceChromePolicy() *schema.Resource { + return &schema.Resource{ + Description: "Chrome Policy resource in the Terraform Googleworkspace provider. Currently only supports policies not requiring additionalTargetKeys.", + + CreateContext: resourceChromePolicyCreate, + UpdateContext: resourceChromePolicyUpdate, + ReadContext: resourceChromePolicyRead, + DeleteContext: resourceChromePolicyDelete, + + Schema: map[string]*schema.Schema{ + "org_unit_id": { + Description: "The target org unit on which this policy is applied.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: diffSuppressOrgUnitId, + }, + "policies": { + Description: "Policies to set for the org unit", + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "schema_name": { + Description: "The full qualified name of the policy schema.", + Type: schema.TypeString, + Required: true, + }, + "schema_values": { + Description: "JSON encoded map that represents key/value pairs that " + + "correspond to the given schema. ", + Type: schema.TypeMap, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringIsJSON, + ), + }, + }, + }, + }, + }, + }, + } +} + +func resourceChromePolicyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*apiClient) + + chromePolicyService, diags := client.NewChromePolicyService() + if diags.HasError() { + return diags + } + + chromePoliciesService, diags := GetChromePoliciesService(chromePolicyService) + if diags.HasError() { + return diags + } + + orgUnitId := strings.TrimPrefix(d.Get("org_unit_id").(string), "id:") + + log.Printf("[DEBUG] Creating Chrome Policy for org:%s", orgUnitId) + + policyTargetKey := &chromepolicy.GoogleChromePolicyV1PolicyTargetKey{ + TargetResource: "orgunits/" + orgUnitId, + } + + diags = validateChromePolicies(d, client) + if diags.HasError() { + return diags + } + + policies, diags := expandChromePoliciesValues(d.Get("policies").([]interface{})) + if diags.HasError() { + return diags + } + + var modifyRequests []*chromepolicy.GoogleChromePolicyV1ModifyOrgUnitPolicyRequest + for _, p := range policies { + var keys []string + var schemaValues map[string]interface{} + if err := json.Unmarshal(p.Value, &schemaValues); err != nil { + return diag.FromErr(err) + } + for key := range schemaValues { + keys = append(keys, key) + } + modifyRequests = append(modifyRequests, &chromepolicy.GoogleChromePolicyV1ModifyOrgUnitPolicyRequest{ + PolicyTargetKey: policyTargetKey, + PolicyValue: p, + UpdateMask: strings.Join(keys, ","), + }) + } + + _, err := chromePoliciesService.Orgunits.BatchModify(fmt.Sprintf("customers/%s", client.Customer), &chromepolicy.GoogleChromePolicyV1BatchModifyOrgUnitPoliciesRequest{Requests: modifyRequests}).Do() + if err != nil { + return diag.FromErr(err) + } + + log.Printf("[DEBUG] Finished creating Chrome Policy for org:%s", orgUnitId) + d.SetId(orgUnitId) + + return resourceChromePolicyRead(ctx, d, meta) +} + +func resourceChromePolicyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*apiClient) + + chromePolicyService, diags := client.NewChromePolicyService() + if diags.HasError() { + return diags + } + + chromePoliciesService, diags := GetChromePoliciesService(chromePolicyService) + if diags.HasError() { + return diags + } + + log.Printf("[DEBUG] Updating Chrome Policy for org:%s", d.Id()) + + policyTargetKey := &chromepolicy.GoogleChromePolicyV1PolicyTargetKey{ + TargetResource: "orgunits/" + d.Id(), + } + + // Update is achieved by inheriting defaults for the previous policySchemas, and then applying the new set + old, _ := d.GetChange("policies") + + var requests []*chromepolicy.GoogleChromePolicyV1InheritOrgUnitPolicyRequest + for _, p := range old.([]interface{}) { + policy := p.(map[string]interface{}) + schemaName := policy["schema_name"].(string) + + requests = append(requests, &chromepolicy.GoogleChromePolicyV1InheritOrgUnitPolicyRequest{ + PolicyTargetKey: policyTargetKey, + PolicySchema: schemaName, + }) + } + + _, err := chromePoliciesService.Orgunits.BatchInherit(fmt.Sprintf("customers/%s", client.Customer), &chromepolicy.GoogleChromePolicyV1BatchInheritOrgUnitPoliciesRequest{Requests: requests}).Do() + if err != nil { + return diag.FromErr(err) + } + + // run create + diags = resourceChromePolicyCreate(ctx, d, meta) + if diags.HasError() { + return diags + } + + log.Printf("[DEBUG] Finished Updating Chrome Policy for org:%s", d.Id()) + + return diags +} + +func resourceChromePolicyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*apiClient) + + chromePolicyService, diags := client.NewChromePolicyService() + if diags.HasError() { + return diags + } + + chromePoliciesService, diags := GetChromePoliciesService(chromePolicyService) + if diags.HasError() { + return diags + } + + log.Printf("[DEBUG] Getting Chrome Policy for org:%s", d.Id()) + + policyTargetKey := &chromepolicy.GoogleChromePolicyV1PolicyTargetKey{ + TargetResource: "orgunits/" + d.Id(), + } + + policiesObj := []*chromepolicy.GoogleChromePolicyV1PolicyValue{} + for _, p := range d.Get("policies").([]interface{}) { + policy := p.(map[string]interface{}) + schemaName := policy["schema_name"].(string) + + var resp *chromepolicy.GoogleChromePolicyV1ResolveResponse + // the resolve endpoint does not like being called in quick succession + err := retryTimeDuration(ctx, time.Minute, func() error { + var retryErr error + + // we will resolve each individual policySchema by fully qualified name, so the responses should be a single result + resp, retryErr = chromePoliciesService.Resolve(fmt.Sprintf("customers/%s", client.Customer), &chromepolicy.GoogleChromePolicyV1ResolveRequest{ + PolicySchemaFilter: schemaName, + PolicyTargetKey: policyTargetKey, + }).Do() + + return retryErr + }) + if err != nil { + return diag.FromErr(err) + } + + if len(resp.ResolvedPolicies) != 1 { + return diag.Errorf("unexpected number of resolved policies for schema: %s", schemaName) + } + + value := resp.ResolvedPolicies[0].Value + + policiesObj = append(policiesObj, value) + } + + policies, diags := flattenChromePolicies(policiesObj, client) + if diags.HasError() { + return diags + } + + if err := d.Set("policies", policies); err != nil { + return diag.FromErr(err) + } + + log.Printf("[DEBUG] Finished getting Chrome Policy for org:%s", d.Id()) + return nil +} + +func resourceChromePolicyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*apiClient) + + chromePolicyService, diags := client.NewChromePolicyService() + if diags.HasError() { + return diags + } + + chromePoliciesService, diags := GetChromePoliciesService(chromePolicyService) + if diags.HasError() { + return diags + } + + log.Printf("[DEBUG] Deleting Chrome Policy for org:%s", d.Id()) + + policyTargetKey := &chromepolicy.GoogleChromePolicyV1PolicyTargetKey{ + TargetResource: "orgunits/" + d.Id(), + } + + var requests []*chromepolicy.GoogleChromePolicyV1InheritOrgUnitPolicyRequest + for _, p := range d.Get("policies").([]interface{}) { + policy := p.(map[string]interface{}) + schemaName := policy["schema_name"].(string) + + requests = append(requests, &chromepolicy.GoogleChromePolicyV1InheritOrgUnitPolicyRequest{ + PolicyTargetKey: policyTargetKey, + PolicySchema: schemaName, + }) + } + + _, err := chromePoliciesService.Orgunits.BatchInherit(fmt.Sprintf("customers/%s", client.Customer), &chromepolicy.GoogleChromePolicyV1BatchInheritOrgUnitPoliciesRequest{Requests: requests}).Do() + if err != nil { + return diag.FromErr(err) + } + + log.Printf("[DEBUG] Finished deleting Chrome Policy for org:%s", d.Id()) + return nil +} + +// Chrome Policies + +func validateChromePolicies(d *schema.ResourceData, client *apiClient) diag.Diagnostics { + var diags diag.Diagnostics + + new := d.Get("policies") + + chromePolicyService, diags := client.NewChromePolicyService() + if diags.HasError() { + return diags + } + + chromePolicySchemasService, diags := GetChromePolicySchemasService(chromePolicyService) + if diags.HasError() { + return diags + } + + // Validate config against schemas + for _, policy := range new.([]interface{}) { + schemaName := policy.(map[string]interface{})["schema_name"].(string) + + schemaDef, err := chromePolicySchemasService.Get(fmt.Sprintf("customers/%s/policySchemas/%s", client.Customer, schemaName)).Do() + if err != nil { + return diag.FromErr(err) + } + + if schemaDef == nil || schemaDef.Definition == nil || schemaDef.Definition.MessageType == nil { + return append(diags, diag.Diagnostic{ + Summary: fmt.Sprintf("schema definition (%s) is empty", schemaName), + Severity: diag.Error, + }) + } + + schemaFieldMap := map[string][]*chromepolicy.Proto2FieldDescriptorProto{} + for _, schemaField := range schemaDef.Definition.MessageType { + for _, schemaNestedField := range schemaField.Field { + schemaFieldMap[schemaNestedField.Name] = schemaField.Field + } + } + + policyDef := policy.(map[string]interface{})["schema_values"].(map[string]interface{}) + + for polKey, polJsonVal := range policyDef { + if _, ok := schemaFieldMap[polKey]; !ok { + return append(diags, diag.Diagnostic{ + Summary: fmt.Sprintf("field name (%s) is not found in this schema definition (%s)", polKey, schemaName), + Severity: diag.Error, + }) + } + + var polVal interface{} + err := json.Unmarshal([]byte(polJsonVal.(string)), &polVal) + if err != nil { + return diag.FromErr(err) + } + + for _, schemaField := range schemaFieldMap[polKey] { + + if schemaField == nil { + return append(diags, diag.Diagnostic{ + Summary: fmt.Sprintf("field type is not defined for field name (%s)", polKey), + Severity: diag.Warning, + }) + } + + validType := validatePolicyFieldValueType(schemaField.Type, polVal) + if !validType { + return append(diags, diag.Diagnostic{ + Summary: fmt.Sprintf("value provided for %s is of incorrect type (expected type: %s)", schemaField.Name, schemaField.Type), + Severity: diag.Error, + }) + } + } + } + } + + return nil +} + +// This will take a value and validate whether the type is correct +func validatePolicyFieldValueType(fieldType string, fieldValue interface{}) bool { + valid := false + + switch fieldType { + case "TYPE_BOOL": + valid = reflect.ValueOf(fieldValue).Kind() == reflect.Bool + case "TYPE_FLOAT": + fallthrough + case "TYPE_DOUBLE": + valid = reflect.ValueOf(fieldValue).Kind() == reflect.Float64 + case "TYPE_INT64": + fallthrough + case "TYPE_FIXED64": + fallthrough + case "TYPE_SFIXED64": + fallthrough + case "TYPE_SINT64": + fallthrough + case "TYPE_UINT64": + // this is unmarshalled as a float, check that it's an int + if reflect.ValueOf(fieldValue).Kind() == reflect.Float64 && + fieldValue == float64(int(fieldValue.(float64))) { + valid = true + } + case "TYPE_INT32": + fallthrough + case "TYPE_FIXED32": + fallthrough + case "TYPE_SFIXED32": + fallthrough + case "TYPE_SINT32": + fallthrough + case "TYPE_UINT32": + // this is unmarshalled as a float, check that it's an int + if reflect.ValueOf(fieldValue).Kind() == reflect.Float32 && + fieldValue == float32(int(fieldValue.(float32))) { + valid = true + } + case "TYPE_ENUM": + fallthrough + case "TYPE_MESSAGE": + fallthrough + case "TYPE_STRING": + fallthrough + default: + valid = reflect.ValueOf(fieldValue).Kind() == reflect.String + } + + return valid +} + +// The API returns numeric values as strings. This will convert it to the appropriate type +func convertPolicyFieldValueType(fieldType string, fieldValue interface{}) (interface{}, error) { + // If it's not of type string, then we'll assume it's the right type + if reflect.ValueOf(fieldValue).Kind() != reflect.String { + return fieldValue, nil + } + + var err error + var value interface{} + + switch fieldType { + case "TYPE_BOOL": + value, err = strconv.ParseBool(fieldValue.(string)) + case "TYPE_FLOAT": + fallthrough + case "TYPE_DOUBLE": + value, err = strconv.ParseFloat(fieldValue.(string), 64) + case "TYPE_INT64": + fallthrough + case "TYPE_FIXED64": + fallthrough + case "TYPE_SFIXED64": + fallthrough + case "TYPE_SINT64": + fallthrough + case "TYPE_UINT64": + value, err = strconv.ParseInt(fieldValue.(string), 10, 64) + case "TYPE_INT32": + fallthrough + case "TYPE_FIXED32": + fallthrough + case "TYPE_SFIXED32": + fallthrough + case "TYPE_SINT32": + fallthrough + case "TYPE_UINT32": + value, err = strconv.ParseInt(fieldValue.(string), 10, 32) + case "TYPE_ENUM": + fallthrough + case "TYPE_MESSAGE": + fallthrough + case "TYPE_STRING": + fallthrough + default: + value = fieldValue + } + + return value, err +} + +func expandChromePoliciesValues(policies []interface{}) ([]*chromepolicy.GoogleChromePolicyV1PolicyValue, diag.Diagnostics) { + var diags diag.Diagnostics + result := []*chromepolicy.GoogleChromePolicyV1PolicyValue{} + + for _, p := range policies { + policy := p.(map[string]interface{}) + + schemaName := policy["schema_name"].(string) + schemaValues := policy["schema_values"].(map[string]interface{}) + + policyValuesObj := map[string]interface{}{} + + for k, v := range schemaValues { + var polVal interface{} + err := json.Unmarshal([]byte(v.(string)), &polVal) + if err != nil { + return nil, diag.FromErr(err) + } + + policyValuesObj[k] = polVal + } + + // create the json object and assign to the schema + schemaValuesJson, err := json.Marshal(policyValuesObj) + if err != nil { + return nil, diag.FromErr(err) + } + + policyObj := chromepolicy.GoogleChromePolicyV1PolicyValue{ + PolicySchema: schemaName, + Value: schemaValuesJson, + } + + result = append(result, &policyObj) + } + + return result, diags +} + +func flattenChromePolicies(policiesObj []*chromepolicy.GoogleChromePolicyV1PolicyValue, client *apiClient) ([]map[string]interface{}, diag.Diagnostics) { + var policies []map[string]interface{} + + chromePolicyService, diags := client.NewChromePolicyService() + if diags.HasError() { + return nil, diags + } + + schemaService, diags := GetChromePolicySchemasService(chromePolicyService) + if diags.HasError() { + return nil, diags + } + + for _, polObj := range policiesObj { + schemaDef, err := schemaService.Get(fmt.Sprintf("customers/%s/policySchemas/%s", client.Customer, polObj.PolicySchema)).Do() + if err != nil { + return nil, diag.FromErr(err) + } + + if schemaDef == nil || schemaDef.Definition == nil || schemaDef.Definition.MessageType == nil { + return nil, append(diags, diag.Diagnostic{ + Summary: fmt.Sprintf("schema definition (%s) is not defined", polObj.PolicySchema), + Severity: diag.Warning, + }) + } + + schemaFieldMap := map[string][]*chromepolicy.Proto2FieldDescriptorProto{} + for _, schemaField := range schemaDef.Definition.MessageType { + for _, schemaNestedField := range schemaField.Field { + schemaFieldMap[schemaNestedField.Name] = schemaField.Field + } + } + + var schemaValuesObj map[string]interface{} + + err = json.Unmarshal(polObj.Value, &schemaValuesObj) + if err != nil { + return nil, diag.FromErr(err) + } + + schemaValues := map[string]interface{}{} + for k, v := range schemaValuesObj { + if _, ok := schemaFieldMap[k]; !ok { + return nil, append(diags, diag.Diagnostic{ + Summary: fmt.Sprintf("field name (%s) is not found in this schema definition (%s)", k, polObj.PolicySchema), + Severity: diag.Warning, + }) + } + + for _, schemaField := range schemaFieldMap[k] { + + if schemaField == nil { + return nil, append(diags, diag.Diagnostic{ + Summary: fmt.Sprintf("field type is not defined for field name (%s)", k), + Severity: diag.Warning, + }) + } + + val, err := convertPolicyFieldValueType(schemaField.Type, v) + if err != nil { + return nil, diag.FromErr(err) + } + + jsonVal, err := json.Marshal(val) + if err != nil { + return nil, diag.FromErr(err) + } + schemaValues[k] = string(jsonVal) + } + } + + policies = append(policies, map[string]interface{}{ + "schema_name": polObj.PolicySchema, + "schema_values": schemaValues, + }) + } + + return policies, nil +} diff --git a/internal/provider/resource_chrome_policy_test.go b/internal/provider/resource_chrome_policy_test.go new file mode 100644 index 00000000..3b4cd60a --- /dev/null +++ b/internal/provider/resource_chrome_policy_test.go @@ -0,0 +1,252 @@ +package googleworkspace + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "google.golang.org/api/chromepolicy/v1" +) + +func TestAccResourceChromePolicy_basic(t *testing.T) { + t.Parallel() + + ouName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccResourceChromePolicy_basic(ouName, 33), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.#", "1"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_name", "chrome.users.MaxConnectionsPerProxy"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_values.maxConnectionsPerProxy", "33"), + ), + }, + }, + }) +} + +func TestAccResourceChromePolicy_update(t *testing.T) { + t.Parallel() + + ouName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccResourceChromePolicy_basic(ouName, 33), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.#", "1"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_name", "chrome.users.MaxConnectionsPerProxy"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_values.maxConnectionsPerProxy", "33"), + ), + }, + { + Config: testAccResourceChromePolicy_basic(ouName, 34), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.#", "1"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_name", "chrome.users.MaxConnectionsPerProxy"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_values.maxConnectionsPerProxy", "34"), + ), + }, + }, + }) +} + +func TestAccResourceChromePolicy_multiple(t *testing.T) { + t.Parallel() + + ouName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + + // ensures previously set field was reset/removed + // this passing also implies Delete works correctly + // based on the implementation + testCheck := func(s *terraform.State) error { + client, err := googleworkspaceTestClient() + if err != nil { + return err + } + + rs, ok := s.RootModule().Resources["googleworkspace_org_unit.test"] + if !ok { + return fmt.Errorf("Can't find org unit resource: googleworkspace_org_unit.test") + } + + if rs.Primary.ID == "" { + return fmt.Errorf("org unit ID not set") + } + + chromePolicyService, diags := client.NewChromePolicyService() + if diags.HasError() { + return errors.New(diags[0].Summary) + } + + chromePoliciesService, diags := GetChromePoliciesService(chromePolicyService) + if diags.HasError() { + return errors.New(diags[0].Summary) + } + + policyTargetKey := &chromepolicy.GoogleChromePolicyV1PolicyTargetKey{ + TargetResource: "orgunits/" + strings.TrimPrefix(rs.Primary.ID, "id:"), + } + + resp, err := chromePoliciesService.Resolve(fmt.Sprintf("customers/%s", client.Customer), &chromepolicy.GoogleChromePolicyV1ResolveRequest{ + PolicySchemaFilter: "chrome.users.MaxConnectionsPerProxy", + PolicyTargetKey: policyTargetKey, + }).Do() + if err != nil { + return err + } + if len(resp.ResolvedPolicies) > 0 { + return fmt.Errorf("Expected policy to be reset") + } + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccResourceChromePolicy_multiple(ouName, 33, ".*@example"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.#", "2"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_name", "chrome.users.RestrictSigninToPattern"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_values.restrictSigninToPattern", encode(".*@example")), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.1.schema_name", "chrome.users.MaxConnectionsPerProxy"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.1.schema_values.maxConnectionsPerProxy", "33"), + ), + }, + { + Config: testAccResourceChromePolicy_multipleRearranged(ouName, 34, ".*@example.com"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.#", "2"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_name", "chrome.users.MaxConnectionsPerProxy"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_values.maxConnectionsPerProxy", "34"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.1.schema_name", "chrome.users.RestrictSigninToPattern"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.1.schema_values.restrictSigninToPattern", encode(".*@example.com")), + ), + }, + { + Config: testAccResourceChromePolicy_multipleDifferent(ouName, true, ".*@example.com"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.#", "2"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_name", "chrome.users.OnlineRevocationChecks"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.0.schema_values.enableOnlineRevocationChecks", "true"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.1.schema_name", "chrome.users.RestrictSigninToPattern"), + resource.TestCheckResourceAttr("googleworkspace_chrome_policy.test", "policies.1.schema_values.restrictSigninToPattern", encode(".*@example.com")), + testCheck, + ), + }, + }, + }) +} + +func encode(content string) string { + res, _ := json.Marshal(content) + return string(res) +} + +func testAccResourceChromePolicy_multiple(ouName string, conns int, pattern string) string { + return fmt.Sprintf(` +resource "googleworkspace_org_unit" "test" { + name = "%s" + parent_org_unit_path = "/" +} + +resource "googleworkspace_chrome_policy" "test" { + org_unit_id = googleworkspace_org_unit.test.id + policies { + schema_name = "chrome.users.RestrictSigninToPattern" + schema_values = { + restrictSigninToPattern = jsonencode("%s") + } + } + policies { + schema_name = "chrome.users.MaxConnectionsPerProxy" + schema_values = { + maxConnectionsPerProxy = jsonencode(%d) + } + } +} +`, ouName, pattern, conns) +} + +func testAccResourceChromePolicy_multipleRearranged(ouName string, conns int, pattern string) string { + return fmt.Sprintf(` +resource "googleworkspace_org_unit" "test" { + name = "%s" + parent_org_unit_path = "/" +} + +resource "googleworkspace_chrome_policy" "test" { + org_unit_id = googleworkspace_org_unit.test.id + policies { + schema_name = "chrome.users.MaxConnectionsPerProxy" + schema_values = { + maxConnectionsPerProxy = jsonencode(%d) + } + } + policies { + schema_name = "chrome.users.RestrictSigninToPattern" + schema_values = { + restrictSigninToPattern = jsonencode("%s") + } + } +} +`, ouName, conns, pattern) +} + +func testAccResourceChromePolicy_multipleDifferent(ouName string, enabled bool, pattern string) string { + return fmt.Sprintf(` +resource "googleworkspace_org_unit" "test" { + name = "%s" + parent_org_unit_path = "/" +} + +resource "googleworkspace_chrome_policy" "test" { + org_unit_id = googleworkspace_org_unit.test.id + policies { + schema_name = "chrome.users.OnlineRevocationChecks" + schema_values = { + enableOnlineRevocationChecks = jsonencode(%t) + } + } + policies { + schema_name = "chrome.users.RestrictSigninToPattern" + schema_values = { + restrictSigninToPattern = jsonencode("%s") + } + } +} +`, ouName, enabled, pattern) +} + +func testAccResourceChromePolicy_basic(ouName string, conns int) string { + return fmt.Sprintf(` +resource "googleworkspace_org_unit" "test" { + name = "%s" + parent_org_unit_path = "/" +} + +resource "googleworkspace_chrome_policy" "test" { + org_unit_id = googleworkspace_org_unit.test.id + policies { + schema_name = "chrome.users.MaxConnectionsPerProxy" + schema_values = { + maxConnectionsPerProxy = jsonencode(%d) + } + } +} +`, ouName, conns) +} diff --git a/internal/provider/resource_role_assignment.go b/internal/provider/resource_role_assignment.go index 52f600c7..1adf729a 100644 --- a/internal/provider/resource_role_assignment.go +++ b/internal/provider/resource_role_assignment.go @@ -56,19 +56,22 @@ func resourceRoleAssignment() *schema.Resource { ForceNew: true, }, "org_unit_id": { - Description: "If the role is restricted to an organization unit, this contains the ID for the organization unit the exercise of this role is restricted to.", - Type: schema.TypeString, - Optional: true, - ForceNew: true, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - // for some reason the Google API returns org unit ids with a "id:" prefix - return strings.TrimPrefix(old, "id:") == strings.TrimPrefix(new, "id:") - }, + Description: "If the role is restricted to an organization unit, this contains the ID for the organization unit the exercise of this role is restricted to.", + Type: schema.TypeString, + Optional: true, + ForceNew: true, + DiffSuppressFunc: diffSuppressOrgUnitId, }, }, } } +// for some reason the Google API returns org unit ids with a "id:" prefix +// some resources won't accept this prefix when specifying an org unit id +func diffSuppressOrgUnitId(k, old, new string, d *schema.ResourceData) bool { + return strings.TrimPrefix(old, "id:") == strings.TrimPrefix(new, "id:") +} + func resourceRolesAssignmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics @@ -94,9 +97,7 @@ func resourceRolesAssignmentCreate(ctx context.Context, d *schema.ResourceData, } scopeType := strings.ToUpper(d.Get("scope_type").(string)) - orgUnitId := d.Get("org_unit_id").(string) - // for some reason the Google API returns org unit ids with a "id:" prefix - orgUnitId = strings.TrimPrefix(orgUnitId, "id:") + orgUnitId := strings.TrimPrefix(d.Get("org_unit_id").(string), "id:") if scopeType == "ORG_UNIT" && orgUnitId == "" { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, @@ -156,8 +157,7 @@ func resourceRoleAssignmentRead(ctx context.Context, d *schema.ResourceData, met d.Set("etag", ra.Etag) d.Set("assigned_to", ra.AssignedTo) d.Set("scope_type", ra.ScopeType) - // for some reason the Google API returns org unit ids with a "id:" prefix - d.Set("org_unit_id", strings.TrimPrefix(ra.OrgUnitId, "id:")) + d.Set("org_unit_id", ra.OrgUnitId) log.Printf("[DEBUG] Finished getting RoleAssignment %q", d.Id()) diff --git a/internal/provider/retry_utils.go b/internal/provider/retry_utils.go index 5d59a93e..aeb0cc42 100644 --- a/internal/provider/retry_utils.go +++ b/internal/provider/retry_utils.go @@ -26,6 +26,10 @@ func retryTimeDuration(ctx context.Context, duration time.Duration, retryFunc fu return resource.RetryableError(err) } + if IsRateLimitExceeded(err) { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) }) } @@ -53,3 +57,17 @@ func IsTemporarilyUnavailable(err error) bool { return false } + +func IsRateLimitExceeded(err error) bool { + gerr, ok := err.(*googleapi.Error) + if !ok { + return false + } + + if gerr.Code == 429 { + log.Printf("[DEBUG] Dismissed an error as retryable based on error code: %s", err) + return true + } + return false + +} diff --git a/internal/provider/services.go b/internal/provider/services.go index 3215d529..37fa95e0 100644 --- a/internal/provider/services.go +++ b/internal/provider/services.go @@ -6,9 +6,44 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" directory "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/chromepolicy/v1" "google.golang.org/api/groupssettings/v1" ) +func GetChromePoliciesService(chromePolicyService *chromepolicy.Service) (*chromepolicy.CustomersPoliciesService, diag.Diagnostics) { + var diags diag.Diagnostics + + log.Printf("[INFO] Instantiating Google Admin Chrome Policies service") + customersService := chromePolicyService.Customers + if customersService == nil || customersService.Policies == nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Chrome Policies Service could not be created.", + }) + + return nil, diags + } + + return customersService.Policies, diags +} + +func GetChromePolicySchemasService(chromePolicyService *chromepolicy.Service) (*chromepolicy.CustomersPolicySchemasService, diag.Diagnostics) { + var diags diag.Diagnostics + + log.Printf("[INFO] Instantiating Google Admin Chrome Policy Schemas service") + customersService := chromePolicyService.Customers + if customersService == nil || customersService.PolicySchemas == nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Chrome Policy Schemas Service could not be created.", + }) + + return nil, diags + } + + return customersService.PolicySchemas, diags +} + func GetDomainAliasesService(directoryService *directory.Service) (*directory.DomainAliasesService, diag.Diagnostics) { var diags diag.Diagnostics