From b94c41777a0e9a78ea7aae0461288d7c606a72ac Mon Sep 17 00:00:00 2001 From: Martin Guibert Date: Fri, 1 Oct 2021 11:02:46 +0200 Subject: [PATCH] implement iam for project and make middleware generics --- pkg/driftctl.go | 4 +- ... => google_iam_binding_tranformer_test.go} | 252 +++++++++++++++++- .../google_iam_binding_transformer.go | 65 +++++ ...o => google_iam_policy_tranformer_test.go} | 198 +++++++++++++- ...er.go => google_iam_policy_transformer.go} | 34 ++- .../google_project_iam_member_enumerator.go | 57 ++++ pkg/remote/google/init.go | 10 + .../google/repository/cloudresourcemanager.go | 57 ++++ .../mock_CloudResourceManagerRepository.go | 33 +++ pkg/remote/google_project_scanner_test.go | 138 ++++++++++ .../results.golden.json | 50 ++++ .../google/google_project_iam_binding.go | 3 + .../google/google_project_iam_member.go | 21 ++ .../google/google_project_iam_member_test.go | 31 +++ .../google/google_project_iam_policy.go | 3 + pkg/resource/google/metadatas.go | 1 + .../google_project_iam_member/terraform.tf | 20 ++ pkg/resource/resource_types.go | 7 + 18 files changed, 966 insertions(+), 18 deletions(-) rename pkg/middlewares/{google_storage_bucket_iam_binding_tranformer_test.go => google_iam_binding_tranformer_test.go} (51%) create mode 100644 pkg/middlewares/google_iam_binding_transformer.go rename pkg/middlewares/{google_storage_bucket_iam_policy_tranformer_test.go => google_iam_policy_tranformer_test.go} (53%) rename pkg/middlewares/{google_storage_bucket_iam_policy_transformer.go => google_iam_policy_transformer.go} (57%) create mode 100644 pkg/remote/google/google_project_iam_member_enumerator.go create mode 100644 pkg/remote/google/repository/cloudresourcemanager.go create mode 100644 pkg/remote/google/repository/mock_CloudResourceManagerRepository.go create mode 100644 pkg/remote/google_project_scanner_test.go create mode 100755 pkg/remote/test/google_project_member_listing_multiple/results.golden.json create mode 100644 pkg/resource/google/google_project_iam_binding.go create mode 100644 pkg/resource/google/google_project_iam_member.go create mode 100644 pkg/resource/google/google_project_iam_member_test.go create mode 100644 pkg/resource/google/google_project_iam_policy.go create mode 100644 pkg/resource/google/testdata/acc/google_project_iam_member/terraform.tf diff --git a/pkg/driftctl.go b/pkg/driftctl.go index f6792c2b..9085c0c7 100644 --- a/pkg/driftctl.go +++ b/pkg/driftctl.go @@ -115,8 +115,8 @@ func (d DriftCTL) Run() (*analyser.Analysis, error) { middlewares.NewAwsApiGatewayRestApiExpander(d.resourceFactory), middlewares.NewAwsApiGatewayRestApiPolicyExpander(d.resourceFactory), - middlewares.NewGoogleStorageBucketIAMBindingTransformer(d.resourceFactory), - middlewares.NewGoogleStorageBucketIAMPolicyTransformer(d.resourceFactory), + middlewares.NewGoogleIAMBindingTransformer(d.resourceFactory), + middlewares.NewGoogleIAMPolicyTransformer(d.resourceFactory), middlewares.NewGoogleLegacyBucketIAMMember(), middlewares.NewAzurermRouteExpander(d.resourceFactory), diff --git a/pkg/middlewares/google_storage_bucket_iam_binding_tranformer_test.go b/pkg/middlewares/google_iam_binding_tranformer_test.go similarity index 51% rename from pkg/middlewares/google_storage_bucket_iam_binding_tranformer_test.go rename to pkg/middlewares/google_iam_binding_tranformer_test.go index 6538143e..8540b90f 100644 --- a/pkg/middlewares/google_storage_bucket_iam_binding_tranformer_test.go +++ b/pkg/middlewares/google_iam_binding_tranformer_test.go @@ -11,6 +11,256 @@ import ( "github.com/r3labs/diff/v2" ) +func TestGoogleProjectIAMBindingTransformer_Execute(t *testing.T) { + tests := []struct { + name string + resourcesFromState []*resource.Resource + expected []*resource.Resource + mock func(factory *terraform.MockResourceFactory) + }{ + { + "Test that project bindings are transformed into member", + []*resource.Resource{ + { + Id: "fake", + Type: google.GoogleStorageBucketResourceType, + Attrs: &resource.Attributes{}, + }, + { + Id: "admin project", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "project": "coucou", + "role": "storage.admin", + "member": "user:elie@cloudskiff.com", + }, + }, + { + Id: "proj/admin", + Type: google.GoogleProjectIamBindingResourceType, + Attrs: &resource.Attributes{ + "role": "storage.admin", + "project": "proj", + "members": []interface{}{ + "user:elie@cloudskiff.com", + "user:william@cloudskiff.com", + }, + }, + }, + + { + Id: "proj/viewer", + Type: google.GoogleProjectIamBindingResourceType, + Attrs: &resource.Attributes{ + "role": "storage.viewer", + "project": "proj", + "members": []interface{}{ + "user:william@cloudskiff.com", + }, + }, + }, + { + Id: "proj2/viewer", + Type: google.GoogleProjectIamBindingResourceType, + Attrs: &resource.Attributes{ + "role": "storage.viewer", + "project": "proj2", + "members": []interface{}{ + "user:william@cloudskiff.com", + }, + }, + }, + }, + []*resource.Resource{ + { + Id: "fake", + Type: google.GoogleStorageBucketResourceType, + Attrs: &resource.Attributes{}, + }, + { + Id: "admin project", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "project": "coucou", + "role": "storage.admin", + "member": "user:elie@cloudskiff.com", + }, + }, + { + Id: "proj/storage.admin/user:elie@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "role": "storage.admin", + "project": "proj", + "member": "user:elie@cloudskiff.com", + }, + }, + { + Id: "proj/storage.admin/user:william@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "role": "storage.admin", + "project": "proj", + "member": "user:william@cloudskiff.com", + }, + }, + { + Id: "proj/storage.viewer/user:william@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "role": "storage.viewer", + "project": "proj", + "member": "user:william@cloudskiff.com", + }, + }, + { + Id: "proj2/storage.viewer/user:william@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "role": "storage.viewer", + "project": "proj2", + "member": "user:william@cloudskiff.com", + }, + }, + }, + func(factory *terraform.MockResourceFactory) { + factory.On( + "CreateAbstractResource", google.GoogleProjectIamMemberResourceType, + "proj/storage.admin/user:elie@cloudskiff.com", + map[string]interface{}{ + "id": "proj/storage.admin/user:elie@cloudskiff.com", + "project": "proj", + "role": "storage.admin", + "member": "user:elie@cloudskiff.com", + }).Return(&resource.Resource{ + Id: "proj/storage.admin/user:elie@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "role": "storage.admin", + "project": "proj", + "member": "user:elie@cloudskiff.com", + }, + }).Once() + + factory.On( + "CreateAbstractResource", google.GoogleProjectIamMemberResourceType, + "proj/storage.admin/user:william@cloudskiff.com", + map[string]interface{}{ + "id": "proj/storage.admin/user:william@cloudskiff.com", + "project": "proj", + "role": "storage.admin", + "member": "user:william@cloudskiff.com", + }).Return(&resource.Resource{ + Id: "proj/storage.admin/user:william@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "role": "storage.admin", + "project": "proj", + "member": "user:william@cloudskiff.com", + }, + }).Once() + + factory.On( + "CreateAbstractResource", google.GoogleProjectIamMemberResourceType, + "proj/storage.viewer/user:william@cloudskiff.com", + map[string]interface{}{ + "id": "proj/storage.viewer/user:william@cloudskiff.com", + "project": "proj", + "role": "storage.viewer", + "member": "user:william@cloudskiff.com", + }).Return(&resource.Resource{ + Id: "proj/storage.viewer/user:william@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "role": "storage.viewer", + "project": "proj", + "member": "user:william@cloudskiff.com", + }, + }).Once() + + factory.On( + "CreateAbstractResource", google.GoogleProjectIamMemberResourceType, + "proj2/storage.viewer/user:william@cloudskiff.com", + map[string]interface{}{ + "id": "proj2/storage.viewer/user:william@cloudskiff.com", + "project": "proj2", + "role": "storage.viewer", + "member": "user:william@cloudskiff.com", + }).Return(&resource.Resource{ + Id: "proj2/storage.viewer/user:william@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "role": "storage.viewer", + "project": "proj2", + "member": "user:william@cloudskiff.com", + }, + }).Once() + }, + }, + { + "test that everything is fine when there is no bindings", + []*resource.Resource{ + { + Id: "fake", + Type: google.GoogleStorageBucketResourceType, + Attrs: &resource.Attributes{}, + }, + { + Id: "admin project", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "project": "coucou", + "role": "storage.admin", + "member": "user:elie@cloudskiff.com", + }, + }, + }, + []*resource.Resource{ + { + Id: "fake", + Type: google.GoogleStorageBucketResourceType, + Attrs: &resource.Attributes{}, + }, + { + Id: "admin project", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "project": "coucou", + "role": "storage.admin", + "member": "user:elie@cloudskiff.com", + }, + }, + }, + func(factory *terraform.MockResourceFactory) { + + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := &terraform.MockResourceFactory{} + if tt.mock != nil { + tt.mock(factory) + } + + m := NewGoogleIAMBindingTransformer(factory) + err := m.Execute(&[]*resource.Resource{}, &tt.resourcesFromState) + if err != nil { + t.Fatal(err) + } + changelog, err := diff.Diff(tt.expected, tt.resourcesFromState) + if err != nil { + t.Fatal(err) + } + if len(changelog) > 0 { + for _, change := range changelog { + t.Errorf("%s got = %v, want %v", strings.Join(change.Path, "."), awsutil.Prettify(change.From), awsutil.Prettify(change.To)) + } + } + }) + } +} + func TestGoogleBucketIAMBindingTransformer_Execute(t *testing.T) { tests := []struct { name string @@ -243,7 +493,7 @@ func TestGoogleBucketIAMBindingTransformer_Execute(t *testing.T) { tt.mock(factory) } - m := NewGoogleStorageBucketIAMBindingTransformer(factory) + m := NewGoogleIAMBindingTransformer(factory) err := m.Execute(&[]*resource.Resource{}, &tt.resourcesFromState) if err != nil { t.Fatal(err) diff --git a/pkg/middlewares/google_iam_binding_transformer.go b/pkg/middlewares/google_iam_binding_transformer.go new file mode 100644 index 00000000..7dcfce63 --- /dev/null +++ b/pkg/middlewares/google_iam_binding_transformer.go @@ -0,0 +1,65 @@ +package middlewares + +import ( + "fmt" + "strings" + + "github.com/cloudskiff/driftctl/pkg/resource" + "github.com/cloudskiff/driftctl/pkg/resource/google" +) + +// GoogleIAMBindingTransformer Transforms Bucket IAM binding in bucket iam member to ease comparison. +type GoogleIAMBindingTransformer struct { + resourceFactory resource.ResourceFactory + resFieldByType map[string]string // map of the field to add to resource attribute for all supported type +} + +func NewGoogleIAMBindingTransformer(resourceFactory resource.ResourceFactory) *GoogleIAMBindingTransformer { + return &GoogleIAMBindingTransformer{ + resourceFactory, + map[string]string{ + google.GoogleStorageBucketIamBindingResourceType: "bucket", + google.GoogleProjectIamBindingResourceType: "project", + }, + } +} + +func (m *GoogleIAMBindingTransformer) Execute(_, resourcesFromState *[]*resource.Resource) error { + + resources := make([]*resource.Resource, 0) + + for _, stateRes := range *resourcesFromState { + // Ignore all resources other than IamBinding + resType := stateRes.ResourceType() + resField, supported := m.resFieldByType[resType] + if !supported { + resources = append(resources, stateRes) + continue + } + + resName := *stateRes.Attrs.GetString(resField) + roleName := *stateRes.Attrs.GetString("role") + members, _ := stateRes.Attrs.Get("members") + + for _, member := range members.([]interface{}) { + id := fmt.Sprintf("%s/%s/%s", resName, roleName, member) + resources = append( + resources, + m.resourceFactory.CreateAbstractResource( + fmt.Sprintf("%s_member", strings.TrimSuffix(resType, "_binding")), + id, + map[string]interface{}{ + "id": id, + resField: resName, + "role": roleName, + "member": member.(string), + }, + ), + ) + } + } + + *resourcesFromState = resources + + return nil +} diff --git a/pkg/middlewares/google_storage_bucket_iam_policy_tranformer_test.go b/pkg/middlewares/google_iam_policy_tranformer_test.go similarity index 53% rename from pkg/middlewares/google_storage_bucket_iam_policy_tranformer_test.go rename to pkg/middlewares/google_iam_policy_tranformer_test.go index 5e259cbb..09e1553a 100644 --- a/pkg/middlewares/google_storage_bucket_iam_policy_tranformer_test.go +++ b/pkg/middlewares/google_iam_policy_tranformer_test.go @@ -11,6 +11,202 @@ import ( "github.com/r3labs/diff/v2" ) +func TestGoogleProjectIAMPolicyTransformer_Execute(t *testing.T) { + tests := []struct { + name string + resourcesFromState []*resource.Resource + expected []*resource.Resource + mock func(factory *terraform.MockResourceFactory) + }{ + { + "Test that project policy are transformed into bindings", + []*resource.Resource{ + { + Id: "b/bucket-1", + Type: google.GoogleStorageBucketResourceType, + Attrs: &resource.Attributes{}, + }, + { + Id: "b/bucket-2", + Type: google.GoogleStorageBucketResourceType, + Attrs: &resource.Attributes{}, + }, + { + Id: "project-1", + Type: google.GoogleProjectIamPolicyResourceType, + Attrs: &resource.Attributes{ + "project": "project-1", + "id": "project-1", + "policy_data": "{\"bindings\":[{\"members\":[\"serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com\"],\"role\":\"roles/storage.admin\"},{\"members\":[\"user:william.beuil@cloudskiff.com\"],\"role\":\"roles/storage.objectViewer\"}]}", + }, + }, + { + Id: "dctlgstorageprojectiambinding-2", + Type: google.GoogleProjectIamPolicyResourceType, + Attrs: &resource.Attributes{ + "project": "project-2", + "etag": "CAU=", + "id": "project-2", + "policy_data": "{\"bindings\":[{\"members\":[\"serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com\"],\"role\":\"roles/storage.admin\"},{\"members\":[\"user:william.beuil@cloudskiff.com\"],\"role\":\"roles/storage.objectViewer\"}]}", + }, + }, + }, + []*resource.Resource{ + { + Id: "b/bucket-1", + Type: google.GoogleStorageBucketResourceType, + Attrs: &resource.Attributes{}, + }, + { + Id: "b/bucket-2", + Type: google.GoogleStorageBucketResourceType, + Attrs: &resource.Attributes{}, + }, + { + Id: "project-1/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "id": "project-1/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "role": "roles/storage.admin", + "project": "project-1", + "member": "serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + }, + }, + { + Id: "project-1/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "id": "project-1/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + "role": "roles/storage.objectViewer", + "project": "project-1", + "member": "user:william.beuil@cloudskiff.com", + }, + }, + { + Id: "project-2/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "id": "project-2/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "role": "roles/storage.admin", + "project": "project-2", + "member": "serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + }, + }, + { + Id: "project-2/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "id": "project-2/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + "role": "roles/storage.objectViewer", + "project": "project-2", + "member": "user:william.beuil@cloudskiff.com", + }, + }, + }, + func(factory *terraform.MockResourceFactory) { + factory.On( + "CreateAbstractResource", google.GoogleProjectIamMemberResourceType, + "project-1/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + map[string]interface{}{ + "id": "project-1/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "project": "project-1", + "role": "roles/storage.admin", + "member": "serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + }).Return(&resource.Resource{ + Id: "project-1/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "id": "project-1/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "role": "roles/storage.admin", + "project": "project-1", + "member": "serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + }, + }).Once() + + factory.On( + "CreateAbstractResource", google.GoogleProjectIamMemberResourceType, + "project-1/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + map[string]interface{}{ + "id": "project-1/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + "project": "project-1", + "role": "roles/storage.objectViewer", + "member": "user:william.beuil@cloudskiff.com", + }).Return(&resource.Resource{ + Id: "project-1/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "id": "project-1/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + "role": "roles/storage.objectViewer", + "project": "project-1", + "member": "user:william.beuil@cloudskiff.com", + }, + }).Once() + + factory.On( + "CreateAbstractResource", google.GoogleProjectIamMemberResourceType, + "project-2/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + map[string]interface{}{ + "id": "project-2/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "project": "project-2", + "role": "roles/storage.admin", + "member": "serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + }).Return(&resource.Resource{ + Id: "project-2/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "id": "project-2/roles/storage.admin/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "role": "roles/storage.admin", + "project": "project-2", + "member": "serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + }, + }).Once() + + factory.On( + "CreateAbstractResource", google.GoogleProjectIamMemberResourceType, + "project-2/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + map[string]interface{}{ + "id": "project-2/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + "project": "project-2", + "role": "roles/storage.objectViewer", + "member": "user:william.beuil@cloudskiff.com", + }).Return(&resource.Resource{ + Id: "project-2/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + Type: google.GoogleProjectIamMemberResourceType, + Attrs: &resource.Attributes{ + "id": "project-2/roles/storage.objectViewer/user:william.beuil@cloudskiff.com", + "role": "roles/storage.objectViewer", + "project": "project-2", + "member": "user:william.beuil@cloudskiff.com", + }, + }).Once() + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := &terraform.MockResourceFactory{} + if tt.mock != nil { + tt.mock(factory) + } + + m := NewGoogleIAMPolicyTransformer(factory) + err := m.Execute(&[]*resource.Resource{}, &tt.resourcesFromState) + if err != nil { + t.Fatal(err) + } + changelog, err := diff.Diff(tt.expected, tt.resourcesFromState) + if err != nil { + t.Fatal(err) + } + if len(changelog) > 0 { + for _, change := range changelog { + t.Errorf("%s got = %v, want %v", strings.Join(change.Path, "."), awsutil.Prettify(change.From), awsutil.Prettify(change.To)) + } + } + }) + } +} + func TestGoogleBucketIAMPolicyTransformer_Execute(t *testing.T) { tests := []struct { name string @@ -189,7 +385,7 @@ func TestGoogleBucketIAMPolicyTransformer_Execute(t *testing.T) { tt.mock(factory) } - m := NewGoogleStorageBucketIAMPolicyTransformer(factory) + m := NewGoogleIAMPolicyTransformer(factory) err := m.Execute(&[]*resource.Resource{}, &tt.resourcesFromState) if err != nil { t.Fatal(err) diff --git a/pkg/middlewares/google_storage_bucket_iam_policy_transformer.go b/pkg/middlewares/google_iam_policy_transformer.go similarity index 57% rename from pkg/middlewares/google_storage_bucket_iam_policy_transformer.go rename to pkg/middlewares/google_iam_policy_transformer.go index ffbfbf50..5146dfb4 100644 --- a/pkg/middlewares/google_storage_bucket_iam_policy_transformer.go +++ b/pkg/middlewares/google_iam_policy_transformer.go @@ -3,6 +3,7 @@ package middlewares import ( "encoding/json" "fmt" + "strings" "github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource/google" @@ -11,10 +12,16 @@ import ( // GoogleStorageBucketIAMPolicyTransformer Transforms Bucket IAM policy in bucket iam binding to ease comparison. type GoogleStorageBucketIAMPolicyTransformer struct { resourceFactory resource.ResourceFactory + resFieldByType map[string]string // map of the field to add to resource attribute for all supported type } -func NewGoogleStorageBucketIAMPolicyTransformer(resourceFactory resource.ResourceFactory) *GoogleStorageBucketIAMPolicyTransformer { - return &GoogleStorageBucketIAMPolicyTransformer{resourceFactory} +func NewGoogleIAMPolicyTransformer(resourceFactory resource.ResourceFactory) *GoogleStorageBucketIAMPolicyTransformer { + return &GoogleStorageBucketIAMPolicyTransformer{ + resourceFactory, + map[string]string{ + google.GoogleStorageBucketIamPolicyResourceType: "bucket", + google.GoogleProjectIamPolicyResourceType: "project", + }} } func (m *GoogleStorageBucketIAMPolicyTransformer) Execute(_, resourcesFromState *[]*resource.Resource) error { @@ -23,12 +30,14 @@ func (m *GoogleStorageBucketIAMPolicyTransformer) Execute(_, resourcesFromState for _, stateRes := range *resourcesFromState { // Ignore all resources other than BucketIamBinding - if stateRes.ResourceType() != google.GoogleStorageBucketIamPolicyResourceType { + resType := stateRes.ResourceType() + resField, supported := m.resFieldByType[resType] + if !supported { resources = append(resources, stateRes) continue } - bucket := *stateRes.Attrs.GetString("bucket") + resName := *stateRes.Attrs.GetString(resField) policyJSON := *stateRes.Attrs.GetString("policy_data") policies := policyDataType{} @@ -38,20 +47,20 @@ func (m *GoogleStorageBucketIAMPolicyTransformer) Execute(_, resourcesFromState } for _, policy := range policies.Bindings { - roleName := policy.Role - members := policy.Members + roleName := policy["role"].(string) + members := policy["members"].([]interface{}) for _, member := range members { - id := fmt.Sprintf("%s/%s/%s", bucket, roleName, member) + id := fmt.Sprintf("%s/%s/%s", resName, roleName, member) resources = append( resources, m.resourceFactory.CreateAbstractResource( - google.GoogleStorageBucketIamMemberResourceType, + fmt.Sprintf("%s_member", strings.TrimSuffix(resType, "_policy")), id, map[string]interface{}{ "id": id, - "bucket": bucket, + resField: resName, "role": roleName, - "member": member, + "member": member.(string), }, ), ) @@ -65,8 +74,5 @@ func (m *GoogleStorageBucketIAMPolicyTransformer) Execute(_, resourcesFromState } type policyDataType struct { - Bindings []struct { - Members []string - Role string - } + Bindings []map[string]interface{} } diff --git a/pkg/remote/google/google_project_iam_member_enumerator.go b/pkg/remote/google/google_project_iam_member_enumerator.go new file mode 100644 index 00000000..906bed4e --- /dev/null +++ b/pkg/remote/google/google_project_iam_member_enumerator.go @@ -0,0 +1,57 @@ +package google + +import ( + "fmt" + + remoteerror "github.com/cloudskiff/driftctl/pkg/remote/error" + "github.com/cloudskiff/driftctl/pkg/remote/google/repository" + "github.com/cloudskiff/driftctl/pkg/resource" + "github.com/cloudskiff/driftctl/pkg/resource/google" +) + +type GoogleProjectIamMemberEnumerator struct { + repository repository.CloudResourceManagerRepository + factory resource.ResourceFactory +} + +func NewGoogleProjectIamMemberEnumerator(repo repository.CloudResourceManagerRepository, factory resource.ResourceFactory) *GoogleProjectIamMemberEnumerator { + return &GoogleProjectIamMemberEnumerator{ + repository: repo, + factory: factory, + } +} + +func (e *GoogleProjectIamMemberEnumerator) SupportedType() resource.ResourceType { + return google.GoogleProjectIamMemberResourceType +} + +func (e *GoogleProjectIamMemberEnumerator) Enumerate() ([]*resource.Resource, error) { + results := make([]*resource.Resource, 0) + + bindingsByProject, err := e.repository.ListProjectsBindings() + if err != nil { + return nil, remoteerror.NewResourceListingError(err, string(e.SupportedType())) + } + for project, bindings := range bindingsByProject { + for roleName, members := range bindings { + for _, member := range members { + id := fmt.Sprintf("%s/%s/%s", project, roleName, member) + results = append( + results, + e.factory.CreateAbstractResource( + string(e.SupportedType()), + id, + map[string]interface{}{ + "id": id, + "project": project, + "role": roleName, + "member": member, + }, + ), + ) + } + } + } + + return results, err +} diff --git a/pkg/remote/google/init.go b/pkg/remote/google/init.go index c14cd52c..09f95187 100644 --- a/pkg/remote/google/init.go +++ b/pkg/remote/google/init.go @@ -13,6 +13,7 @@ import ( "github.com/cloudskiff/driftctl/pkg/resource" "github.com/cloudskiff/driftctl/pkg/resource/google" "github.com/cloudskiff/driftctl/pkg/terraform" + "google.golang.org/api/cloudresourcemanager/v1" ) func Init(version string, alerter *alerter.Alerter, @@ -45,8 +46,14 @@ func Init(version string, alerter *alerter.Alerter, return err } + crmService, err := cloudresourcemanager.NewService(ctx) + if err != nil { + return err + } + assetRepository := repository.NewAssetRepository(assetClient, provider.GetConfig(), repositoryCache) storageRepository := repository.NewStorageRepository(storageClient, repositoryCache) + iamRepository := repository.NewCloudResourceManagerRepository(crmService, provider.GetConfig(), repositoryCache) providerLibrary.AddProvider(terraform.GOOGLE, provider) deserializer := resource.NewDeserializer(factory) @@ -61,6 +68,9 @@ func Init(version string, alerter *alerter.Alerter, remoteLibrary.AddEnumerator(NewGoogleComputeInstanceEnumerator(assetRepository, factory)) + remoteLibrary.AddEnumerator(NewGoogleProjectIamMemberEnumerator(iamRepository, factory)) + remoteLibrary.AddDetailsFetcher(google.GoogleProjectIamMemberResourceType, common.NewGenericDetailsFetcher(google.GoogleProjectIamMemberResourceType, provider, deserializer)) + remoteLibrary.AddEnumerator(NewGoogleStorageBucketIamMemberEnumerator(assetRepository, storageRepository, factory)) remoteLibrary.AddDetailsFetcher(google.GoogleStorageBucketIamMemberResourceType, common.NewGenericDetailsFetcher(google.GoogleStorageBucketIamMemberResourceType, provider, deserializer)) diff --git a/pkg/remote/google/repository/cloudresourcemanager.go b/pkg/remote/google/repository/cloudresourcemanager.go new file mode 100644 index 00000000..94be3443 --- /dev/null +++ b/pkg/remote/google/repository/cloudresourcemanager.go @@ -0,0 +1,57 @@ +package repository + +import ( + "sync" + + "github.com/cloudskiff/driftctl/pkg/remote/cache" + "github.com/cloudskiff/driftctl/pkg/remote/google/config" + "google.golang.org/api/cloudresourcemanager/v1" +) + +type CloudResourceManagerRepository interface { + ListProjectsBindings() (map[string]map[string][]string, error) +} + +type cloudResourceManagerRepository struct { + service *cloudresourcemanager.Service + config config.GCPTerraformConfig + cache cache.Cache + lock sync.Locker +} + +func NewCloudResourceManagerRepository(service *cloudresourcemanager.Service, config config.GCPTerraformConfig, cache cache.Cache) CloudResourceManagerRepository { + return &cloudResourceManagerRepository{ + service: service, + config: config, + cache: cache, + lock: &sync.Mutex{}, + } +} + +func (s *cloudResourceManagerRepository) ListProjectsBindings() (map[string]map[string][]string, error) { + + s.lock.Lock() + defer s.lock.Unlock() + if cachedResults := s.cache.Get("ListProjectsBindings"); cachedResults != nil { + return cachedResults.(map[string]map[string][]string), nil + } + + request := new(cloudresourcemanager.GetIamPolicyRequest) + policy, err := s.service.Projects.GetIamPolicy(s.config.Project, request).Do() + if err != nil { + return nil, err + } + + bindings := make(map[string][]string) + + for _, binding := range policy.Bindings { + bindings[binding.Role] = binding.Members + } + + bindingsByProject := make(map[string]map[string][]string) + bindingsByProject[s.config.Project] = bindings + + s.cache.Put("ListProjectsBindings", bindingsByProject) + + return bindingsByProject, nil +} diff --git a/pkg/remote/google/repository/mock_CloudResourceManagerRepository.go b/pkg/remote/google/repository/mock_CloudResourceManagerRepository.go new file mode 100644 index 00000000..4b2a7658 --- /dev/null +++ b/pkg/remote/google/repository/mock_CloudResourceManagerRepository.go @@ -0,0 +1,33 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package repository + +import mock "github.com/stretchr/testify/mock" + +// MockCloudResourceManagerRepository is an autogenerated mock type for the CloudResourceManagerRepository type +type MockCloudResourceManagerRepository struct { + mock.Mock +} + +// ListProjectsBindings provides a mock function with given fields: +func (_m *MockCloudResourceManagerRepository) ListProjectsBindings() (map[string]map[string][]string, error) { + ret := _m.Called() + + var r0 map[string]map[string][]string + if rf, ok := ret.Get(0).(func() map[string]map[string][]string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]map[string][]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/remote/google_project_scanner_test.go b/pkg/remote/google_project_scanner_test.go new file mode 100644 index 00000000..ce692cc5 --- /dev/null +++ b/pkg/remote/google_project_scanner_test.go @@ -0,0 +1,138 @@ +package remote + +import ( + "testing" + + "github.com/cloudskiff/driftctl/mocks" + "github.com/cloudskiff/driftctl/pkg/filter" + "github.com/cloudskiff/driftctl/pkg/remote/alerts" + "github.com/cloudskiff/driftctl/pkg/remote/common" + remoteerr "github.com/cloudskiff/driftctl/pkg/remote/error" + "github.com/cloudskiff/driftctl/pkg/remote/google" + "github.com/cloudskiff/driftctl/pkg/remote/google/repository" + "github.com/cloudskiff/driftctl/pkg/resource" + googleresource "github.com/cloudskiff/driftctl/pkg/resource/google" + "github.com/cloudskiff/driftctl/pkg/terraform" + "github.com/cloudskiff/driftctl/test" + "github.com/cloudskiff/driftctl/test/goldenfile" + testresource "github.com/cloudskiff/driftctl/test/resource" + terraform2 "github.com/cloudskiff/driftctl/test/terraform" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGoogleProjectIAMMember(t *testing.T) { + + cases := []struct { + test string + dirName string + repositoryMock func(repository *repository.MockCloudResourceManagerRepository) + responseErr error + setupAlerterMock func(alerter *mocks.AlerterInterface) + wantErr error + }{ + { + test: "no bindings", + dirName: "google_project_member_empty", + repositoryMock: func(repository *repository.MockCloudResourceManagerRepository) { + repository.On("ListProjectsBindings").Return(map[string]map[string][]string{}, nil) + }, + wantErr: nil, + }, + { + test: "Cannot list bindings", + dirName: "google_project_member_listing_error", + repositoryMock: func(repository *repository.MockCloudResourceManagerRepository) { + repository.On("ListProjectsBindings").Return( + map[string]map[string][]string{}, + errors.New("googleapi: Error 403: driftctl-acc-circle@driftctl-qa-1.iam.gserviceaccount.com does not have project.getIamPolicy access., forbidden")) + }, + setupAlerterMock: func(alerter *mocks.AlerterInterface) { + alerter.On( + "SendAlert", + "google_project_iam_member", + alerts.NewRemoteAccessDeniedAlert( + common.RemoteGoogleTerraform, + remoteerr.NewResourceListingError( + errors.New("googleapi: Error 403: driftctl-acc-circle@driftctl-qa-1.iam.gserviceaccount.com does not have project.getIamPolicy access., forbidden"), + "google_project_iam_member", + ), + alerts.EnumerationPhase, + ), + ).Once() + }, + wantErr: nil, + }, + { + test: "multiples storage buckets, multiple bindings", + dirName: "google_project_member_listing_multiple", + repositoryMock: func(repository *repository.MockCloudResourceManagerRepository) { + repository.On("ListProjectsBindings").Return(map[string]map[string][]string{ + "": { + "roles/editor": { + "user:martin.guibert@cloudskiff.com", + "serviceAccount:drifctl-admin@cloudskiff-dev-martin.iam.gserviceaccount.com", + }, + "roles/storage.admin": {"user:martin.guibert@cloudskiff.com"}, + "roles/viewer": {"serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com"}, + "roles/cloudasset.viewer": {"serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com"}, + "roles/iam.securityReviewer": {"serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com"}, + }, + }, nil) + }, + wantErr: nil, + }, + } + + providerVersion := "3.78.0" + resType := resource.ResourceType(googleresource.GoogleProjectIamMemberResourceType) + schemaRepository := testresource.InitFakeSchemaRepository("google", providerVersion) + googleresource.InitResourcesMetadata(schemaRepository) + factory := terraform.NewTerraformResourceFactory(schemaRepository) + deserializer := resource.NewDeserializer(factory) + + for _, c := range cases { + t.Run(c.test, func(tt *testing.T) { + + shouldUpdate := c.dirName == *goldenfile.Update + + scanOptions := ScannerOptions{Deep: true} + providerLibrary := terraform.NewProviderLibrary() + remoteLibrary := common.NewRemoteLibrary() + + // Initialize mocks + alerter := &mocks.AlerterInterface{} + if c.setupAlerterMock != nil { + c.setupAlerterMock(alerter) + } + + realProvider, err := terraform2.InitTestGoogleProvider(providerLibrary, providerVersion) + if err != nil { + tt.Fatal(err) + } + provider := terraform2.NewFakeTerraformProvider(realProvider) + provider.WithResponse(c.dirName) + + managerRepository := &repository.MockCloudResourceManagerRepository{} + if c.repositoryMock != nil { + c.repositoryMock(managerRepository) + } + + remoteLibrary.AddEnumerator(google.NewGoogleProjectIamMemberEnumerator(managerRepository, factory)) + + testFilter := &filter.MockFilter{} + testFilter.On("IsTypeIgnored", mock.Anything).Return(false) + + s := NewScanner(remoteLibrary, alerter, scanOptions, testFilter) + got, err := s.Resources() + assert.Equal(tt, c.wantErr, err) + if err != nil { + return + } + alerter.AssertExpectations(tt) + testFilter.AssertExpectations(tt) + test.TestAgainstGoldenFile(got, resType.String(), c.dirName, provider, deserializer, shouldUpdate, tt) + }) + } +} diff --git a/pkg/remote/test/google_project_member_listing_multiple/results.golden.json b/pkg/remote/test/google_project_member_listing_multiple/results.golden.json new file mode 100755 index 00000000..5c7b2ffc --- /dev/null +++ b/pkg/remote/test/google_project_member_listing_multiple/results.golden.json @@ -0,0 +1,50 @@ +[ + { + "condition": null, + "etag": null, + "id": "/roles/storage.admin/user:martin.guibert@cloudskiff.com", + "member": "user:martin.guibert@cloudskiff.com", + "project": "", + "role": "roles/storage.admin" + }, + { + "condition": null, + "etag": null, + "id": "/roles/viewer/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "member": "serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "project": "", + "role": "roles/viewer" + }, + { + "condition": null, + "etag": null, + "id": "/roles/cloudasset.viewer/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "member": "serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "project": "", + "role": "roles/cloudasset.viewer" + }, + { + "condition": null, + "etag": null, + "id": "/roles/iam.securityReviewer/serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "member": "serviceAccount:driftctl@cloudskiff-dev-martin.iam.gserviceaccount.com", + "project": "", + "role": "roles/iam.securityReviewer" + }, + { + "condition": null, + "etag": null, + "id": "/roles/editor/serviceAccount:drifctl-admin@cloudskiff-dev-martin.iam.gserviceaccount.com", + "member": "serviceAccount:drifctl-admin@cloudskiff-dev-martin.iam.gserviceaccount.com", + "project": "", + "role": "roles/editor" + }, + { + "condition": null, + "etag": null, + "id": "/roles/editor/user:martin.guibert@cloudskiff.com", + "member": "user:martin.guibert@cloudskiff.com", + "project": "", + "role": "roles/editor" + } +] \ No newline at end of file diff --git a/pkg/resource/google/google_project_iam_binding.go b/pkg/resource/google/google_project_iam_binding.go new file mode 100644 index 00000000..096e9e01 --- /dev/null +++ b/pkg/resource/google/google_project_iam_binding.go @@ -0,0 +1,3 @@ +package google + +const GoogleProjectIamBindingResourceType = "google_project_iam_binding" diff --git a/pkg/resource/google/google_project_iam_member.go b/pkg/resource/google/google_project_iam_member.go new file mode 100644 index 00000000..cb9f6813 --- /dev/null +++ b/pkg/resource/google/google_project_iam_member.go @@ -0,0 +1,21 @@ +package google + +import "github.com/cloudskiff/driftctl/pkg/resource" + +const GoogleProjectIamMemberResourceType = "google_project_iam_member" + +func initGoogleProjectIAMMemberMetadata(resourceSchemaRepository resource.SchemaRepositoryInterface) { + resourceSchemaRepository.SetNormalizeFunc(GoogleProjectIamMemberResourceType, func(res *resource.Resource) { + res.Attributes().SafeDelete([]string{"force_destroy"}) + res.Attributes().SafeDelete([]string{"etag"}) + }) + resourceSchemaRepository.SetResolveReadAttributesFunc(GoogleProjectIamMemberResourceType, func(res *resource.Resource) map[string]string { + return map[string]string{ + "project": *res.Attrs.GetString("project"), + "role": *res.Attrs.GetString("role"), + "member": *res.Attrs.GetString("member"), + } + }) + resourceSchemaRepository.SetFlags(GoogleProjectIamMemberResourceType, resource.FlagDeepMode) + +} diff --git a/pkg/resource/google/google_project_iam_member_test.go b/pkg/resource/google/google_project_iam_member_test.go new file mode 100644 index 00000000..b4f2fca5 --- /dev/null +++ b/pkg/resource/google/google_project_iam_member_test.go @@ -0,0 +1,31 @@ +package google_test + +import ( + "testing" + + "github.com/cloudskiff/driftctl/test" + "github.com/cloudskiff/driftctl/test/acceptance" +) + +func TestAcc_Google_ProjectIAMMember(t *testing.T) { + acceptance.Run(t, acceptance.AccTestCase{ + TerraformVersion: "0.15.5", + Paths: []string{"./testdata/acc/google_project_iam_member"}, + Args: []string{ + "scan", + "--to", "gcp+tf", + "--filter", "Type=='google_project_iam_member'", + }, + Checks: []acceptance.AccCheck{ + { + Check: func(result *test.ScanResult, stdout string, err error) { + if err != nil { + t.Fatal(err) + } + result.AssertInfrastructureIsInSync() + result.AssertManagedCount(2) + }, + }, + }, + }) +} diff --git a/pkg/resource/google/google_project_iam_policy.go b/pkg/resource/google/google_project_iam_policy.go new file mode 100644 index 00000000..96db80af --- /dev/null +++ b/pkg/resource/google/google_project_iam_policy.go @@ -0,0 +1,3 @@ +package google + +const GoogleProjectIamPolicyResourceType = "google_project_iam_policy" diff --git a/pkg/resource/google/metadatas.go b/pkg/resource/google/metadatas.go index 7bbe24e5..b41d8ceb 100644 --- a/pkg/resource/google/metadatas.go +++ b/pkg/resource/google/metadatas.go @@ -10,4 +10,5 @@ func InitResourcesMetadata(resourceSchemaRepository resource.SchemaRepositoryInt initGoogleStorageBucketIamBMemberMetadata(resourceSchemaRepository) initGoogleComputeInstanceGroupMetadata(resourceSchemaRepository) initGoogleBigqueryDatasetMetadata(resourceSchemaRepository) + initGoogleProjectIAMMemberMetadata(resourceSchemaRepository) } diff --git a/pkg/resource/google/testdata/acc/google_project_iam_member/terraform.tf b/pkg/resource/google/testdata/acc/google_project_iam_member/terraform.tf new file mode 100644 index 00000000..786cef94 --- /dev/null +++ b/pkg/resource/google/testdata/acc/google_project_iam_member/terraform.tf @@ -0,0 +1,20 @@ +provider "google" {} + +terraform { + required_version = "~> 0.15.0" + required_providers { + google = { + version = "3.78.0" + } + } +} + +resource "google_project_iam_member" "elie1" { + role = "roles/editor" + member = "user:elie.charra@cloudskiff.com" +} + +resource "google_project_iam_member" "will1" { + role = "roles/viewer" + member = "user:william.beuil@cloudskiff.com" +} diff --git a/pkg/resource/resource_types.go b/pkg/resource/resource_types.go index b8a0242c..01f6f0e8 100644 --- a/pkg/resource/resource_types.go +++ b/pkg/resource/resource_types.go @@ -145,6 +145,13 @@ var supportedTypes = map[string]ResourceTypeMeta{ "google_dns_managed_zone": {}, "google_compute_instance_group": {}, "google_bigquery_dataset": {}, + "google_project_iam_member": {}, + "google_project_iam_binding": {children: []ResourceType{ + "google_project_iam_member", + }}, + "google_project_iam_policy": {children: []ResourceType{ + "google_project_iam_member", + }}, "azurerm_storage_account": {}, "azurerm_storage_container": {},