From 139c851c372ad48852a58da88fa15a0d44e491f0 Mon Sep 17 00:00:00 2001 From: Paddy Date: Fri, 21 Dec 2018 05:20:57 +0000 Subject: [PATCH] Add support for Audit Configs --- google/data_source_google_iam_policy.go | 94 +++++++-- google/iam.go | 51 +++++ google/resource_google_project_iam_policy.go | 95 +++++++-- ...resource_google_project_iam_policy_test.go | 189 ++++++++++++++++-- .../docs/d/google_iam_policy.html.markdown | 25 +++ 5 files changed, 397 insertions(+), 57 deletions(-) diff --git a/google/data_source_google_iam_policy.go b/google/data_source_google_iam_policy.go index d917c42f1b6..3a8bac5ed1a 100644 --- a/google/data_source_google_iam_policy.go +++ b/google/data_source_google_iam_policy.go @@ -9,25 +9,6 @@ import ( "google.golang.org/api/cloudresourcemanager/v1" ) -var iamBinding *schema.Schema = &schema.Schema{ - Type: schema.TypeSet, - Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "role": { - Type: schema.TypeString, - Required: true, - }, - "members": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, - }, - }, - }, -} - // dataSourceGoogleIamPolicy returns a *schema.Resource that allows a customer // to express a Google Cloud IAM policy in a data resource. This is an example // of how the schema would be used in a config: @@ -44,11 +25,57 @@ func dataSourceGoogleIamPolicy() *schema.Resource { return &schema.Resource{ Read: dataSourceGoogleIamPolicyRead, Schema: map[string]*schema.Schema{ - "binding": iamBinding, + "binding": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "role": { + Type: schema.TypeString, + Required: true, + }, + "members": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + }, + }, "policy_data": { Type: schema.TypeString, Computed: true, }, + "audit_config": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "service": { + Type: schema.TypeString, + Required: true, + }, + "audit_log_configs": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "log_type": { + Type: schema.TypeString, + Required: true, + }, + "exempted_members": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + }, + }, + }, + }, + }, + }, }, } } @@ -61,6 +88,7 @@ func dataSourceGoogleIamPolicyRead(d *schema.ResourceData, meta interface{}) err // The schema supports multiple binding{} blocks bset := d.Get("binding").(*schema.Set) + aset := d.Get("audit_config").(*schema.Set) // All binding{} blocks will be converted and stored in an array bindings = make([]*cloudresourcemanager.Binding, bset.Len()) @@ -75,6 +103,9 @@ func dataSourceGoogleIamPolicyRead(d *schema.ResourceData, meta interface{}) err } } + // Convert each audit_config into a cloudresourcemanager.AuditConfig + policy.AuditConfigs = expandAuditConfig(aset) + // Marshal cloudresourcemanager.Policy to JSON suitable for storing in state pjson, err := json.Marshal(&policy) if err != nil { @@ -88,3 +119,26 @@ func dataSourceGoogleIamPolicyRead(d *schema.ResourceData, meta interface{}) err return nil } + +func expandAuditConfig(set *schema.Set) []*cloudresourcemanager.AuditConfig { + auditConfigs := make([]*cloudresourcemanager.AuditConfig, 0, set.Len()) + for _, v := range set.List() { + config := v.(map[string]interface{}) + // build list of audit configs first + auditLogConfigSet := config["audit_log_configs"].(*schema.Set) + // the array we're going to add to the outgoing resource + auditLogConfigs := make([]*cloudresourcemanager.AuditLogConfig, 0, auditLogConfigSet.Len()) + for _, y := range auditLogConfigSet.List() { + logConfig := y.(map[string]interface{}) + auditLogConfigs = append(auditLogConfigs, &cloudresourcemanager.AuditLogConfig{ + LogType: logConfig["log_type"].(string), + ExemptedMembers: convertStringArr(logConfig["exempted_members"].(*schema.Set).List()), + }) + } + auditConfigs = append(auditConfigs, &cloudresourcemanager.AuditConfig{ + Service: config["service"].(string), + AuditLogConfigs: auditLogConfigs, + }) + } + return auditConfigs +} diff --git a/google/iam.go b/google/iam.go index 7af9a1cf8d6..e80d907c78f 100644 --- a/google/iam.go +++ b/google/iam.go @@ -161,3 +161,54 @@ func rolesToMembersMap(bindings []*cloudresourcemanager.Binding) map[string]map[ } return bm } + +// Merge multiple Audit Configs such that configs with the same service result in +// a single exemption list with combined members +func mergeAuditConfigs(auditConfigs []*cloudresourcemanager.AuditConfig) []*cloudresourcemanager.AuditConfig { + am := auditConfigToServiceMap(auditConfigs) + var ac []*cloudresourcemanager.AuditConfig + for service, auditLogConfigs := range am { + var a cloudresourcemanager.AuditConfig + a.Service = service + a.AuditLogConfigs = make([]*cloudresourcemanager.AuditLogConfig, 0, len(auditLogConfigs)) + for k, v := range auditLogConfigs { + var alc cloudresourcemanager.AuditLogConfig + alc.LogType = k + for member := range v { + alc.ExemptedMembers = append(alc.ExemptedMembers, member) + } + a.AuditLogConfigs = append(a.AuditLogConfigs, &alc) + } + if len(a.AuditLogConfigs) > 0 { + ac = append(ac, &a) + } + } + return ac +} + +// Build a service map with the log_type and bindings below it +func auditConfigToServiceMap(auditConfig []*cloudresourcemanager.AuditConfig) map[string]map[string]map[string]bool { + ac := make(map[string]map[string]map[string]bool) + // Get each config + for _, c := range auditConfig { + // Initialize service map + if _, ok := ac[c.Service]; !ok { + ac[c.Service] = map[string]map[string]bool{} + } + // loop through audit log configs + for _, lc := range c.AuditLogConfigs { + // Initialize service map + if _, ok := ac[c.Service][lc.LogType]; !ok { + ac[c.Service][lc.LogType] = map[string]bool{} + } + // Get each member (user/principal) for the binding + for _, m := range lc.ExemptedMembers { + // Add the member + if _, ok := ac[c.Service][lc.LogType][m]; !ok { + ac[c.Service][lc.LogType][m] = true + } + } + } + } + return ac +} diff --git a/google/resource_google_project_iam_policy.go b/google/resource_google_project_iam_policy.go index 4dbd6e37b66..3f2b78742db 100644 --- a/google/resource_google_project_iam_policy.go +++ b/google/resource_google_project_iam_policy.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log" + "reflect" "sort" "github.com/hashicorp/errwrap" @@ -88,8 +89,7 @@ func resourceGoogleProjectIamPolicyRead(d *schema.ResourceData, meta interface{} return err } - // we only marshal the bindings, because only the bindings get set in the config - policyBytes, err := json.Marshal(&cloudresourcemanager.Policy{Bindings: policy.Bindings}) + policyBytes, err := json.Marshal(&cloudresourcemanager.Policy{Bindings: policy.Bindings, AuditConfigs: policy.AuditConfigs}) if err != nil { return fmt.Errorf("Error marshaling IAM policy: %v", err) } @@ -157,7 +157,7 @@ func setProjectIamPolicy(policy *cloudresourcemanager.Policy, config *Config, pi pbytes, _ := json.Marshal(policy) log.Printf("[DEBUG] Setting policy %#v for project: %s", string(pbytes), pid) _, err := config.clientResourceManager.Projects.SetIamPolicy(pid, - &cloudresourcemanager.SetIamPolicyRequest{Policy: policy}).Do() + &cloudresourcemanager.SetIamPolicyRequest{Policy: policy, UpdateMask: "bindings,etag,auditConfigs"}).Do() if err != nil { return errwrap.Wrapf(fmt.Sprintf("Error applying IAM policy for project %q. Policy is %#v, error is {{err}}", pid, policy), err) @@ -197,34 +197,67 @@ func jsonPolicyDiffSuppress(k, old, new string, d *schema.ResourceData) bool { log.Printf("[ERROR] Could not unmarshal new policy %s: %v", new, err) return false } - oldPolicy.Bindings = mergeBindings(oldPolicy.Bindings) - newPolicy.Bindings = mergeBindings(newPolicy.Bindings) if newPolicy.Etag != oldPolicy.Etag { return false } if newPolicy.Version != oldPolicy.Version { return false } - if len(newPolicy.Bindings) != len(oldPolicy.Bindings) { + if !compareBindings(oldPolicy.Bindings, newPolicy.Bindings) { return false } - sort.Sort(sortableBindings(newPolicy.Bindings)) - sort.Sort(sortableBindings(oldPolicy.Bindings)) - for pos, newBinding := range newPolicy.Bindings { - oldBinding := oldPolicy.Bindings[pos] - if oldBinding.Role != newBinding.Role { - return false - } - if len(oldBinding.Members) != len(newBinding.Members) { + if !compareAuditConfigs(oldPolicy.AuditConfigs, newPolicy.AuditConfigs) { + return false + } + return true +} + +func derefBindings(b []*cloudresourcemanager.Binding) []cloudresourcemanager.Binding { + db := make([]cloudresourcemanager.Binding, len(b)) + + for i, v := range b { + db[i] = *v + sort.Strings(db[i].Members) + } + return db +} + +func compareBindings(a, b []*cloudresourcemanager.Binding) bool { + a = mergeBindings(a) + b = mergeBindings(b) + sort.Sort(sortableBindings(a)) + sort.Sort(sortableBindings(b)) + return reflect.DeepEqual(derefBindings(a), derefBindings(b)) +} + +func compareAuditConfigs(a, b []*cloudresourcemanager.AuditConfig) bool { + a = mergeAuditConfigs(a) + b = mergeAuditConfigs(b) + sort.Sort(sortableAuditConfigs(a)) + sort.Sort(sortableAuditConfigs(b)) + if len(a) != len(b) { + return false + } + for i, v := range a { + if len(v.AuditLogConfigs) != len(b[i].AuditLogConfigs) { return false } - sort.Strings(oldBinding.Members) - sort.Strings(newBinding.Members) - for i, newMember := range newBinding.Members { - oldMember := oldBinding.Members[i] - if newMember != oldMember { + sort.Sort(sortableAuditLogConfigs(v.AuditLogConfigs)) + sort.Sort(sortableAuditLogConfigs(b[i].AuditLogConfigs)) + for x, logConfig := range v.AuditLogConfigs { + if b[i].AuditLogConfigs[x].LogType != logConfig.LogType { + return false + } + sort.Strings(logConfig.ExemptedMembers) + sort.Strings(b[i].AuditLogConfigs[x].ExemptedMembers) + if len(logConfig.ExemptedMembers) != len(b[i].AuditLogConfigs[x].ExemptedMembers) { return false } + for pos, exemptedMember := range logConfig.ExemptedMembers { + if b[i].AuditLogConfigs[x].ExemptedMembers[pos] != exemptedMember { + return false + } + } } } return true @@ -242,6 +275,30 @@ func (b sortableBindings) Less(i, j int) bool { return b[i].Role < b[j].Role } +type sortableAuditConfigs []*cloudresourcemanager.AuditConfig + +func (b sortableAuditConfigs) Len() int { + return len(b) +} +func (b sortableAuditConfigs) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} +func (b sortableAuditConfigs) Less(i, j int) bool { + return b[i].Service < b[j].Service +} + +type sortableAuditLogConfigs []*cloudresourcemanager.AuditLogConfig + +func (b sortableAuditLogConfigs) Len() int { + return len(b) +} +func (b sortableAuditLogConfigs) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} +func (b sortableAuditLogConfigs) Less(i, j int) bool { + return b[i].LogType < b[j].LogType +} + func getProjectIamPolicyMutexKey(pid string) string { return fmt.Sprintf("iam-project-%s", pid) } diff --git a/google/resource_google_project_iam_policy_test.go b/google/resource_google_project_iam_policy_test.go index 024762d1b92..5d572406571 100644 --- a/google/resource_google_project_iam_policy_test.go +++ b/google/resource_google_project_iam_policy_test.go @@ -63,6 +63,56 @@ func TestAccProjectIamPolicy_expanded(t *testing.T) { }) } +// Test that an IAM policy with an audit config can be applied to a project +func TestAccProjectIamPolicy_basicAuditConfig(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "tf-acctest-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + { + Config: testAccProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccProjectExistingPolicy(pid), + ), + }, + // Apply an IAM policy from a data source. The application + // merges policies, so we validate the expected state. + { + Config: testAccProjectAssociatePolicyAuditConfigBasic(pid, pname, org), + }, + { + ResourceName: "google_project_iam_policy.acceptance", + ImportState: true, + }, + }, + }) +} + +// Test that a non-collapsed IAM policy with AuditConfig doesn't perpetually diff +func TestAccProjectIamPolicy_expandedAuditConfig(t *testing.T) { + t.Parallel() + + org := getTestOrgFromEnv(t) + pid := "tf-acctest-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccProjectAssociatePolicyAuditConfigExpanded(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectIamPolicyExists("google_project_iam_policy.acceptance", "data.google_iam_policy.expanded", pid), + ), + }, + }, + }) +} + func getStatePrimaryResource(s *terraform.State, res, expectedID string) (*terraform.InstanceState, error) { // Get the project resource resource, ok := s.RootModule().Resources[res] @@ -95,14 +145,6 @@ func getGoogleProjectIamPolicyFromState(s *terraform.State, res, expectedID stri return getGoogleProjectIamPolicyFromResource(project) } -func compareBindings(a, b []*cloudresourcemanager.Binding) bool { - a = mergeBindings(a) - b = mergeBindings(b) - sort.Sort(sortableBindings(a)) - sort.Sort(sortableBindings(b)) - return reflect.DeepEqual(derefBindings(a), derefBindings(b)) -} - func testAccCheckGoogleProjectIamPolicyExists(projectRes, policyRes, pid string) resource.TestCheckFunc { return func(s *terraform.State) error { projectPolicy, err := getGoogleProjectIamPolicyFromState(s, projectRes, pid) @@ -118,6 +160,11 @@ func testAccCheckGoogleProjectIamPolicyExists(projectRes, policyRes, pid string) if !compareBindings(projectPolicy.Bindings, policyPolicy.Bindings) { return fmt.Errorf("Project and data source policies do not match: project policy is %+v, data resource policy is %+v", derefBindings(projectPolicy.Bindings), derefBindings(policyPolicy.Bindings)) } + + // The audit configs in both policies should be identical + if !compareAuditConfigs(projectPolicy.AuditConfigs, policyPolicy.AuditConfigs) { + return fmt.Errorf("Project and data source policies do not match: project policy is %+v, data resource policy is %+v", projectPolicy.AuditConfigs, policyPolicy.AuditConfigs) + } return nil } } @@ -235,16 +282,6 @@ func TestIamMergeBindings(t *testing.T) { } } -func derefBindings(b []*cloudresourcemanager.Binding) []cloudresourcemanager.Binding { - db := make([]cloudresourcemanager.Binding, len(b)) - - for i, v := range b { - db[i] = *v - sort.Strings(db[i].Members) - } - return db -} - // Confirm that a project has an IAM policy with at least 1 binding func testAccProjectExistingPolicy(pid string) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -292,6 +329,65 @@ data "google_iam_policy" "admin" { `, pid, name, org) } +func testAccProjectAssociatePolicyAuditConfigBasic(pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} + +resource "google_project_iam_policy" "acceptance" { + project = "${google_project.acceptance.id}" + policy_data = "${data.google_iam_policy.admin.policy_data}" +} + +data "google_iam_policy" "admin" { + binding { + role = "roles/storage.objectViewer" + members = [ + "user:evanbrown@google.com", + ] + } + binding { + role = "roles/compute.instanceAdmin" + members = [ + "user:evanbrown@google.com", + "user:evandbrown@gmail.com", + ] + } + audit_config { + service = "cloudkms.googleapis.com" + audit_log_configs = [ + { + log_type = "DATA_READ" + exempted_members = [ + "user:paddy@hashicorp.com", + ] + }, + { + log_type = "DATA_WRITE" + } + ] + } + audit_config { + service = "cloudsql.googleapis.com" + audit_log_configs = [ + { + log_type = "DATA_READ" + exempted_members = [ + "user:paddy@hashicorp.com", + ] + }, + { + log_type = "DATA_WRITE" + } + ] + } +} +`, pid, name, org) +} + func testAccProject_create(pid, name, org string) string { return fmt.Sprintf(` resource "google_project" "acceptance" { @@ -329,3 +425,60 @@ data "google_iam_policy" "expanded" { } }`, pid, name, org) } + +func testAccProjectAssociatePolicyAuditConfigExpanded(pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} +resource "google_project_iam_policy" "acceptance" { + project = "${google_project.acceptance.id}" + policy_data = "${data.google_iam_policy.expanded.policy_data}" +} + +data "google_iam_policy" "expanded" { + binding { + role = "roles/storage.objectViewer" + members = [ + "user:evanbrown@google.com", + ] + } + binding { + role = "roles/compute.instanceAdmin" + members = [ + "user:evanbrown@google.com", + "user:evandbrown@gmail.com", + ] + } + audit_config { + service = "cloudkms.googleapis.com" + audit_log_configs = [ + { + log_type = "DATA_READ" + exempted_members = [ + "user:paddy@hashicorp.com", + ] + }, + { + log_type = "DATA_WRITE" + } + ] + } + audit_config { + service = "cloudkms.googleapis.com" + audit_log_configs = [ + { + log_type = "DATA_READ" + exempted_members = [ + "user:paddy@carvers.co", + ] + }, + { + log_type = "ADMIN_READ" + } + ] + } +}`, pid, name, org) +} diff --git a/website/docs/d/google_iam_policy.html.markdown b/website/docs/d/google_iam_policy.html.markdown index a852ef1baf8..f3a454ef816 100644 --- a/website/docs/d/google_iam_policy.html.markdown +++ b/website/docs/d/google_iam_policy.html.markdown @@ -29,6 +29,24 @@ data "google_iam_policy" "admin" { "user:jane@example.com", ] } + + audit_config { + service = "cloudkms.googleapis.com" + audit_log_configs = [ + { + log_type = "DATA_READ", + exempted_members = [ + "user:you@domain.com", + ] + }, + { + "logType": "DATA_WRITE", + }, + { + "logType": "ADMIN_READ", + } + ] + } } ``` @@ -64,6 +82,13 @@ each accept the following arguments: * **group:{emailid}**: An email address that represents a Google group. For example, admins@example.com. * **domain:{domain}**: A G Suite domain (primary, instead of alias) name that represents all the users of that domain. For example, google.com or example.com. +* `audit_config` (Optional) - A nested configuration block that defines logging additional configuration for your project. + * `service` (Required) Defines a service that will be enabled for audit logging. For example, `storage.googleapis.com`, `cloudsql.googleapis.com`. `allServices` is a special value that covers all services. + * `audit_log_configs` (Required) A nested block that defines the operations you'd like to log. + * `log_type` (Required) Defines the logging level. `DATA_READ`, `DATA_WRITE` and `ADMIN_READ` capture different types of events. See [the audit configuration documentation](https://cloud.google.com/resource-manager/reference/rest/Shared.Types/AuditConfig) for more details. + * `exempted_members` (Optional) Specifies the identities that are exempt from these types of logging operations. Follows the same format of the `members` array for `binding`. + + ## Attributes Reference The following attribute is exported: