Skip to content

Commit b708032

Browse files
authored
Add service account IAM support (#840)
* Add service account iam resources * Add documentation * Add import functionality to iam resources * Add import documentation
1 parent 961ebf1 commit b708032

5 files changed

+372
-0
lines changed

google/iam_service_account.go

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
"github.com/hashicorp/terraform/helper/schema"
6+
"google.golang.org/api/cloudresourcemanager/v1"
7+
"google.golang.org/api/iam/v1"
8+
)
9+
10+
var IamServiceAccountSchema = map[string]*schema.Schema{
11+
"service_account_id": &schema.Schema{
12+
Type: schema.TypeString,
13+
Required: true,
14+
ForceNew: true,
15+
ValidateFunc: validateRegexp(ServiceAccountLinkRegex),
16+
},
17+
}
18+
19+
type ServiceAccountIamUpdater struct {
20+
serviceAccountId string
21+
Config *Config
22+
}
23+
24+
func NewServiceAccountIamUpdater(d *schema.ResourceData, config *Config) (ResourceIamUpdater, error) {
25+
return &ServiceAccountIamUpdater{
26+
serviceAccountId: d.Get("service_account_id").(string),
27+
Config: config,
28+
}, nil
29+
}
30+
31+
func ServiceAccountIdParseFunc(d *schema.ResourceData, _ *Config) error {
32+
d.Set("service_account_id", d.Id())
33+
return nil
34+
}
35+
36+
func (u *ServiceAccountIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) {
37+
p, err := u.Config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(u.serviceAccountId).Do()
38+
39+
if err != nil {
40+
return nil, fmt.Errorf("Error retrieving IAM policy for %s: %s", u.DescribeResource(), err)
41+
}
42+
43+
cloudResourcePolicy, err := iamToResourceManagerPolicy(p)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
return cloudResourcePolicy, nil
49+
}
50+
51+
func (u *ServiceAccountIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error {
52+
iamPolicy, err := resourceManagerToIamPolicy(policy)
53+
if err != nil {
54+
return err
55+
}
56+
57+
_, err = u.Config.clientIAM.Projects.ServiceAccounts.SetIamPolicy(u.GetResourceId(), &iam.SetIamPolicyRequest{
58+
Policy: iamPolicy,
59+
}).Do()
60+
61+
if err != nil {
62+
return fmt.Errorf("Error setting IAM policy for %s: %s", u.DescribeResource(), err)
63+
}
64+
65+
return nil
66+
}
67+
68+
func (u *ServiceAccountIamUpdater) GetResourceId() string {
69+
return u.serviceAccountId
70+
}
71+
72+
func (u *ServiceAccountIamUpdater) GetMutexKey() string {
73+
return fmt.Sprintf("iam-service-account-%s", u.serviceAccountId)
74+
}
75+
76+
func (u *ServiceAccountIamUpdater) DescribeResource() string {
77+
return fmt.Sprintf("service account '%s'", u.serviceAccountId)
78+
}
79+
80+
func resourceManagerToIamPolicy(p *cloudresourcemanager.Policy) (policy *iam.Policy, err error) {
81+
policy = &iam.Policy{}
82+
83+
err = Convert(p, policy)
84+
85+
return
86+
}
87+
88+
func iamToResourceManagerPolicy(p *iam.Policy) (policy *cloudresourcemanager.Policy, err error) {
89+
policy = &cloudresourcemanager.Policy{}
90+
91+
err = Convert(p, policy)
92+
93+
return
94+
}

google/provider.go

+3
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ func Provider() terraform.ResourceProvider {
164164
"google_runtimeconfig_config": resourceRuntimeconfigConfig(),
165165
"google_runtimeconfig_variable": resourceRuntimeconfigVariable(),
166166
"google_service_account": resourceGoogleServiceAccount(),
167+
"google_service_account_iam_binding": ResourceIamBindingWithImport(IamServiceAccountSchema, NewServiceAccountIamUpdater, ServiceAccountIdParseFunc),
168+
"google_service_account_iam_member": ResourceIamMemberWithImport(IamServiceAccountSchema, NewServiceAccountIamUpdater, ServiceAccountIdParseFunc),
169+
"google_service_account_iam_policy": ResourceIamPolicyWithImport(IamServiceAccountSchema, NewServiceAccountIamUpdater, ServiceAccountIdParseFunc),
167170
"google_service_account_key": resourceGoogleServiceAccountKey(),
168171
"google_storage_bucket": resourceStorageBucket(),
169172
"google_storage_bucket_acl": resourceStorageBucketAcl(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
"github.com/hashicorp/terraform/helper/acctest"
6+
"github.com/hashicorp/terraform/helper/resource"
7+
"github.com/hashicorp/terraform/terraform"
8+
"reflect"
9+
"sort"
10+
"testing"
11+
)
12+
13+
func TestAccGoogleServiceAccountIamBinding(t *testing.T) {
14+
t.Parallel()
15+
16+
account := acctest.RandomWithPrefix("tf-test")
17+
18+
resource.Test(t, resource.TestCase{
19+
PreCheck: func() { testAccPreCheck(t) },
20+
Providers: testAccProviders,
21+
Steps: []resource.TestStep{
22+
{
23+
Config: testAccGoogleServiceAccountIamBinding_basic(account),
24+
Check: testAccCheckGoogleServiceAccountIam(account, "roles/viewer", []string{
25+
fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()),
26+
}),
27+
},
28+
{
29+
ResourceName: "google_service_account_iam_binding.foo",
30+
ImportStateId: fmt.Sprintf("%s %s", getServiceAccountCanonicalId(account), "roles/viewer"),
31+
ImportState: true,
32+
},
33+
},
34+
})
35+
}
36+
37+
func TestAccGoogleServiceAccountIamMember(t *testing.T) {
38+
t.Parallel()
39+
40+
account := acctest.RandomWithPrefix("tf-test")
41+
identity := fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv())
42+
43+
resource.Test(t, resource.TestCase{
44+
PreCheck: func() { testAccPreCheck(t) },
45+
Providers: testAccProviders,
46+
Steps: []resource.TestStep{
47+
{
48+
Config: testAccGoogleServiceAccountIamMember_basic(account),
49+
Check: testAccCheckGoogleServiceAccountIam(account, "roles/editor", []string{identity}),
50+
},
51+
{
52+
ResourceName: "google_service_account_iam_member.foo",
53+
ImportStateId: fmt.Sprintf("%s %s %s", getServiceAccountCanonicalId(account), "roles/editor", identity),
54+
ImportState: true,
55+
},
56+
},
57+
})
58+
}
59+
60+
func TestAccGoogleServiceAccountIamPolicy(t *testing.T) {
61+
t.Parallel()
62+
63+
account := acctest.RandomWithPrefix("tf-test")
64+
65+
resource.Test(t, resource.TestCase{
66+
PreCheck: func() { testAccPreCheck(t) },
67+
Providers: testAccProviders,
68+
Steps: []resource.TestStep{
69+
{
70+
Config: testAccGoogleServiceAccountIamPolicy_basic(account),
71+
Check: testAccCheckGoogleServiceAccountIam(account, "roles/owner", []string{
72+
fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()),
73+
}),
74+
},
75+
{
76+
ResourceName: "google_service_account_iam_policy.foo",
77+
ImportStateId: getServiceAccountCanonicalId(account),
78+
ImportState: true,
79+
},
80+
},
81+
})
82+
}
83+
84+
func testAccCheckGoogleServiceAccountIam(account, role string, members []string) resource.TestCheckFunc {
85+
return func(s *terraform.State) error {
86+
config := testAccProvider.Meta().(*Config)
87+
p, err := config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(getServiceAccountCanonicalId(account)).Do()
88+
if err != nil {
89+
return err
90+
}
91+
92+
for _, binding := range p.Bindings {
93+
if binding.Role == role {
94+
sort.Strings(members)
95+
sort.Strings(binding.Members)
96+
97+
if reflect.DeepEqual(members, binding.Members) {
98+
return nil
99+
}
100+
101+
return fmt.Errorf("Binding found but expected members is %v, got %v", members, binding.Members)
102+
}
103+
}
104+
105+
return fmt.Errorf("No binding for role %q", role)
106+
}
107+
}
108+
109+
func getServiceAccountCanonicalId(account string) string {
110+
return fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", getTestProjectFromEnv(), account, getTestProjectFromEnv())
111+
}
112+
113+
func testAccGoogleServiceAccountIamBinding_basic(account string) string {
114+
return fmt.Sprintf(`
115+
resource "google_service_account" "test_account" {
116+
account_id = "%s"
117+
display_name = "Iam Testing Account"
118+
}
119+
120+
resource "google_service_account_iam_binding" "foo" {
121+
service_account_id = "${google_service_account.test_account.id}"
122+
role = "roles/viewer"
123+
members = ["serviceAccount:${google_service_account.test_account.email}"]
124+
}
125+
`, account)
126+
}
127+
128+
func testAccGoogleServiceAccountIamMember_basic(account string) string {
129+
return fmt.Sprintf(`
130+
resource "google_service_account" "test_account" {
131+
account_id = "%s"
132+
display_name = "Iam Testing Account"
133+
}
134+
135+
resource "google_service_account_iam_member" "foo" {
136+
service_account_id = "${google_service_account.test_account.id}"
137+
role = "roles/editor"
138+
member = "serviceAccount:${google_service_account.test_account.email}"
139+
}
140+
`, account)
141+
}
142+
143+
func testAccGoogleServiceAccountIamPolicy_basic(account string) string {
144+
return fmt.Sprintf(`
145+
resource "google_service_account" "test_account" {
146+
account_id = "%s"
147+
display_name = "Iam Testing Account"
148+
}
149+
150+
data "google_iam_policy" "foo" {
151+
binding {
152+
role = "roles/owner"
153+
154+
members = ["serviceAccount:${google_service_account.test_account.email}"]
155+
}
156+
}
157+
158+
resource "google_service_account_iam_policy" "foo" {
159+
service_account_id = "${google_service_account.test_account.id}"
160+
policy_data = "${data.google_iam_policy.foo.policy_data}"
161+
}
162+
`, account)
163+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
layout: "google"
3+
page_title: "Google: google_service_account_iam"
4+
sidebar_current: "docs-google-service-account-iam"
5+
description: |-
6+
Collection of resources to manage IAM policy for a service account.
7+
---
8+
9+
# IAM policy for service account
10+
11+
When managing IAM roles, you can treat a service account either as a resource or as an identity. This resource is to add iam policy bindings to a service account resource.
12+
13+
Three different resources help you manage your IAM policy for a service account. Each of these resources serves a different use case:
14+
15+
* `google_service_account_iam_policy`: Authoritative. Sets the IAM policy for the service account and replaces any existing policy already attached.
16+
* `google_service_account_iam_binding`: Authoritative for a given role. Updates the IAM policy to grant a role to a list of members. Other roles within the IAM policy for the service account are preserved.
17+
* `google_service_account_iam_member`: Non-authoritative. Updates the IAM policy to grant a role to a new member. Other members for the role for the service account are preserved.
18+
19+
~> **Note:** `google_service_account_iam_policy` **cannot** be used in conjunction with `google_service_account_iam_binding` and `google_service_account_iam_member` or they will fight over what your policy should be.
20+
21+
~> **Note:** `google_service_account_iam_binding` resources **can be** used in conjunction with `google_service_account_iam_member` resources **only if** they do not grant privilege to the same role.
22+
23+
## google\_service\_account\_iam\_policy
24+
25+
```hcl
26+
data "google_iam_policy" "admin" {
27+
binding {
28+
role = "roles/editor"
29+
30+
members = [
31+
32+
]
33+
}
34+
}
35+
36+
resource "google_service_account_iam_policy" "admin-account-iam" {
37+
service_account_id = "your-service-account-id"
38+
policy_data = "${data.google_iam_policy.admin.policy_data}"
39+
}
40+
```
41+
42+
## google\_service\_account\_iam\_binding
43+
44+
```hcl
45+
resource "google_service_account_iam_binding" "admin-account-iam" {
46+
service_account_id = "your-service-account-id"
47+
role = "roles/editor"
48+
49+
members = [
50+
51+
]
52+
}
53+
```
54+
55+
## google\_service\_account\_iam\_member
56+
57+
```hcl
58+
resource "google_service_account_iam_member" "admin-account-iam" {
59+
service_account_id = "your-service-account-id"
60+
role = "roles/editor"
61+
member = "user:[email protected]"
62+
}
63+
```
64+
65+
## Argument Reference
66+
67+
The following arguments are supported:
68+
69+
* `service_account_id` - (Required) The service account id to apply policy to.
70+
71+
* `member/members` - (Required) Identities that will be granted the privilege in `role`.
72+
Each entry can have one of the following values:
73+
* **allUsers**: A special identifier that represents anyone who is on the internet; with or without a Google account.
74+
* **allAuthenticatedUsers**: A special identifier that represents anyone who is authenticated with a Google account or a service account.
75+
* **user:{emailid}**: An email address that represents a specific Google account. For example, [email protected] or [email protected].
76+
* **serviceAccount:{emailid}**: An email address that represents a service account. For example, [email protected].
77+
* **group:{emailid}**: An email address that represents a Google group. For example, [email protected].
78+
* **domain:{domain}**: A Google Apps domain name that represents all the users of that domain. For example, google.com or example.com.
79+
80+
* `role` - (Required) The role that should be applied. Only one
81+
`google_service_account_iam_binding` can be used per role.
82+
83+
* `policy_data` - (Required only by `google_service_account_iam_policy`) The policy data generated by
84+
a `google_iam_policy` data source.
85+
86+
## Attributes Reference
87+
88+
In addition to the arguments listed above, the following computed attributes are
89+
exported:
90+
91+
* `etag` - (Computed) The etag of the service account IAM policy.
92+
93+
## Import
94+
95+
Service account IAM resources can be imported using the project, service account email, role and member.
96+
97+
```
98+
$ terraform import google_service_account_iam_policy.admin-account-iam projects/{your-project-id}/serviceAccounts/{your-service-account-email}
99+
100+
$ terraform import google_service_account_iam_binding.admin-account-iam "projects/{your-project-id}/serviceAccounts/{your-service-account-email} roles/editor"
101+
102+
$ terraform import google_service_account_iam_member.admin-account-iam "projects/{your-project-id}/serviceAccounts/{your-service-account-email} roles/editor [email protected]"
103+
```

website/google.erb

+9
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@
130130
</li>
131131
<li<%= sidebar_current("docs-google-service-account") %>>
132132
<a href="/docs/providers/google/r/google_service_account.html">google_service_account</a>
133+
</li>
134+
<li<%= sidebar_current("docs-google-service-account-iam") %>>
135+
<a href="/docs/providers/google/r/google_service_account_iam.html">google_service_account_iam_binding</a>
136+
</li>
137+
<li<%= sidebar_current("docs-google-service-account-iam") %>>
138+
<a href="/docs/providers/google/r/google_service_account_iam.html">google_service_account_iam_member</a>
139+
</li>
140+
<li<%= sidebar_current("docs-google-service-account-iam") %>>
141+
<a href="/docs/providers/google/r/google_service_account_iam.html">google_service_account_iam_policy</a>
133142
</li>
134143
<li<%= sidebar_current("docs-google-service-account-key") %>>
135144
<a href="/docs/providers/google/r/google_service_account_key.html">google_service_account_key</a>

0 commit comments

Comments
 (0)