Skip to content

Commit 86c16ee

Browse files
feat: Add support for predefined roles as a basis for custom roles (#118)
1 parent 1df0f6a commit 86c16ee

File tree

9 files changed

+122
-31
lines changed

9 files changed

+122
-31
lines changed

examples/custom_role_org/main.tf

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ resource "random_id" "rand_custom_id" {
3535
module "custom-roles-org" {
3636
source = "../../modules/custom_role_iam/"
3737

38-
target_level = "org"
39-
target_id = var.org_id
40-
role_id = "iamDeleter_${random_id.rand_custom_id.hex}"
41-
permissions = ["iam.roles.list", "iam.roles.delete"]
42-
description = "This is an organization level custom role."
43-
members = ["group:test-gcp-org-admins@test.infra.cft.tips", "group:test-gcp-billing-admins@test.infra.cft.tips"]
38+
target_level = "org"
39+
target_id = var.org_id
40+
role_id = "iamDeleter_${random_id.rand_custom_id.hex}"
41+
base_roles = ["roles/iam.serviceAccountAdmin"]
42+
permissions = ["iam.roles.list", "iam.roles.create", "iam.roles.delete"]
43+
excluded_permissions = ["iam.serviceAccounts.setIamPolicy"]
44+
description = "This is an organization level custom role."
45+
members = ["group:test-gcp-org-admins@test.infra.cft.tips", "group:test-gcp-billing-admins@test.infra.cft.tips"]
4446
}

examples/custom_role_project/main.tf

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ provider "google-beta" {
3131
module "custom-role-project" {
3232
source = "../../modules/custom_role_iam/"
3333

34-
target_level = "project"
35-
target_id = var.project_id
36-
role_id = "iamDeleter"
37-
permissions = ["iam.roles.list", "iam.roles.delete"]
38-
description = "This is a project level custom role."
39-
members = ["serviceAccount:custom-role-account-01@${var.project_id}.iam.gserviceaccount.com", "serviceAccount:custom-role-account-02@${var.project_id}.iam.gserviceaccount.com"]
34+
target_level = "project"
35+
target_id = var.project_id
36+
role_id = "iamDeleter"
37+
base_roles = ["roles/iam.serviceAccountAdmin"]
38+
permissions = ["iam.roles.list", "iam.roles.create", "iam.roles.delete"]
39+
excluded_permissions = ["iam.serviceAccounts.setIamPolicy", "resourcemanager.projects.get", "resourcemanager.projects.list"]
40+
description = "This is a project level custom role."
41+
members = ["serviceAccount:custom-role-account-01@${var.project_id}.iam.gserviceaccount.com", "serviceAccount:custom-role-account-02@${var.project_id}.iam.gserviceaccount.com"]
4042
}
4143

4244
/******************************************

modules/custom_role_iam/README.md

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
# Module Custom Role IAM
22

3-
This optional module is used to create custom roles at organization or project level.
3+
This optional module is used to create custom roles at organization or project level. The module supports creating custom rules optionally using predefined roles as a base, with additional permissions or excluded permissions.
4+
5+
Permissions that are [unsupported](https://cloud.google.com/iam/docs/custom-roles-permissions-support) from custom roles are automatically excluded.
46

57
## Usage - Custom Role at Organization Level
68

79
```hcl
810
module "custom-roles" {
911
source = "terraform-google-modules/iam/google//modules/custom_role_iam"
1012
11-
target_level = "org"
12-
target_id = "123456789"
13-
role_id = "custom_role_id"
14-
title = "Custom Role Unique Title"
15-
description = "Custom Role Description"
16-
permissions = ["iam.roles.list", "iam.roles.create", "iam.roles.delete"]
17-
members = ["user:user01@domain.com", "group:group01@domain.com"]
13+
target_level = "org"
14+
target_id = "123456789"
15+
role_id = "custom_role_id"
16+
title = "Custom Role Unique Title"
17+
description = "Custom Role Description"
18+
base_roles = ["roles/iam.serviceAccountAdmin"]
19+
permissions = ["iam.roles.list", "iam.roles.create", "iam.roles.delete"]
20+
excluded_permissions = ["iam.serviceAccounts.setIamPolicy"]
21+
members = ["user:user01@domain.com", "group:group01@domain.com"]
1822
}
1923
```
2024

@@ -24,13 +28,15 @@ module "custom-roles" {
2428
module "custom-roles" {
2529
source = "terraform-google-modules/iam/google//modules/custom_role_iam"
2630
27-
target_level = "project"
28-
target_id = "project_id_123"
29-
role_id = "custom_role_id"
30-
title = "Custom Role Unique Title"
31-
description = "Custom Role Description"
32-
permissions = ["iam.roles.list", "iam.roles.create", "iam.roles.delete"]
33-
members = ["serviceAccount:member01@${var.target_id}.iam.gserviceaccount.com", "serviceAccount:member02@${var.target_id}.iam.gserviceaccount.com"]
31+
target_level = "project"
32+
target_id = "project_id_123"
33+
role_id = "custom_role_id"
34+
title = "Custom Role Unique Title"
35+
description = "Custom Role Description"
36+
base_roles = ["roles/iam.serviceAccountAdmin"]
37+
permissions = ["iam.roles.list", "iam.roles.create", "iam.roles.delete"]
38+
excluded_permissions = ["iam.serviceAccounts.setIamPolicy"]
39+
members = ["serviceAccount:member01@${var.target_id}.iam.gserviceaccount.com", "serviceAccount:member02@${var.target_id}.iam.gserviceaccount.com"]
3440
}
3541
```
3642

@@ -39,7 +45,9 @@ module "custom-roles" {
3945

4046
| Name | Description | Type | Default | Required |
4147
|------|-------------|:----:|:-----:|:-----:|
48+
| base\_roles | List of base predefined roles to use to compose custom role. | list(string) | `<list>` | no |
4249
| description | Description of Custom role. | string | `""` | no |
50+
| excluded\_permissions | List of permissions to exclude from custom role. | list(string) | `<list>` | no |
4351
| members | List of members to be added to custom role. | list(string) | n/a | yes |
4452
| permissions | IAM permissions assigned to Custom Role. | list(string) | n/a | yes |
4553
| role\_id | ID of the Custom Role. | string | n/a | yes |

modules/custom_role_iam/main.tf

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2019 Google LLC
2+
* Copyright 2020 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,7 +15,26 @@
1515
*/
1616

1717
locals {
18-
custom-role-output = (var.target_level == "project") ? google_project_iam_custom_role.project-custom-role[0].role_id : google_organization_iam_custom_role.org-custom-role[0].role_id
18+
excluded_permissions = concat(data.google_iam_testable_permissions.unsupported_permissions.permissions[*].name, var.excluded_permissions)
19+
included_permissions = concat(flatten(values(data.google_iam_role.role_permissions)[*].included_permissions), var.permissions)
20+
permissions = [for permission in local.included_permissions : permission if ! contains(local.excluded_permissions, permission)]
21+
custom-role-output = (var.target_level == "project") ? google_project_iam_custom_role.project-custom-role[0].role_id : google_organization_iam_custom_role.org-custom-role[0].role_id
22+
}
23+
24+
/******************************************
25+
Permissions from predefined roles
26+
*****************************************/
27+
data "google_iam_role" "role_permissions" {
28+
for_each = toset(var.base_roles)
29+
name = "${each.value}"
30+
}
31+
32+
/******************************************
33+
Permissions unsupported for custom roles
34+
*****************************************/
35+
data "google_iam_testable_permissions" "unsupported_permissions" {
36+
full_resource_name = var.target_level == "org" ? "//cloudresourcemanager.googleapis.com/organizations/${var.target_id}" : "//cloudresourcemanager.googleapis.com/projects/${var.target_id}"
37+
custom_support_level = "NOT_SUPPORTED"
1938
}
2039

2140
/******************************************
@@ -28,7 +47,7 @@ resource "google_organization_iam_custom_role" "org-custom-role" {
2847
role_id = var.role_id
2948
title = var.title == "" ? var.role_id : var.title
3049
description = var.description
31-
permissions = var.permissions
50+
permissions = local.permissions
3251
}
3352

3453
/******************************************
@@ -52,7 +71,7 @@ resource "google_project_iam_custom_role" "project-custom-role" {
5271
role_id = var.role_id
5372
title = var.title == "" ? var.role_id : var.title
5473
description = var.description
55-
permissions = var.permissions
74+
permissions = local.permissions
5675
}
5776

5877
/******************************************

modules/custom_role_iam/variables.tf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,23 @@ variable "title" {
2525
default = ""
2626
}
2727

28+
variable "base_roles" {
29+
type = list(string)
30+
description = "List of base predefined roles to use to compose custom role."
31+
default = []
32+
}
33+
2834
variable "permissions" {
2935
type = list(string)
3036
description = "IAM permissions assigned to Custom Role."
3137
}
3238

39+
variable "excluded_permissions" {
40+
type = list(string)
41+
description = "List of permissions to exclude from custom role."
42+
default = []
43+
}
44+
3345
variable "description" {
3446
type = string
3547
description = "Description of Custom role."

test/fixtures/custom-role/main.tf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,17 @@ module "create_custom_role_org" {
2323
source = "../../../examples/custom_role_org"
2424
org_id = var.org_id
2525
}
26+
27+
module "create_custom_role_unsupported_permissions_org" {
28+
source = "../../../modules/custom_role_iam"
29+
target_level = "org"
30+
target_id = var.org_id
31+
role_id = "customDatastoreViewer_${random_id.rand_custom_id.hex}"
32+
base_roles = ["roles/datastore.viewer"] # https://cloud.google.com/iam/docs/custom-roles-permissions-support
33+
permissions = []
34+
members = []
35+
}
36+
37+
resource "random_id" "rand_custom_id" {
38+
byte_length = 2
39+
}

test/fixtures/custom-role/outputs.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,8 @@ output "custom_role_id_org" {
3333
value = module.create_custom_role_org.role_id
3434
description = "ID of the custom role created at organization level."
3535
}
36+
37+
output "custom_role_id_org_unsupported" {
38+
value = module.create_custom_role_unsupported_permissions_org.custom_role_id
39+
description = "ID of the custom role created formed from base role with unsupported permissions"
40+
}

test/integration/custom-role/controls/custom-role.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
custom_role_id_project = attribute('custom_role_id_project')
1818
custom_role_id_org = attribute('custom_role_id_org')
19+
custom_role_id_org_unsupported = attribute('custom_role_id_org_unsupported')
1920
project_id = attribute('project_id')
2021
org_id = attribute('org_id')
2122

@@ -39,6 +40,9 @@
3940
expect(data["description"]).to include("This is a project level custom role.")
4041
expect(data["includedPermissions"]).to include("iam.roles.list")
4142
expect(data["includedPermissions"]).to include("iam.roles.delete")
43+
expect(data["includedPermissions"]).to include("iam.serviceAccounts.list")
44+
expect(data["includedPermissions"]).to include("iam.serviceAccounts.delete")
45+
expect(data["includedPermissions"]).not_to include("iam.serviceAccounts.setIamPolicy")
4246
end
4347
end
4448
end
@@ -60,6 +64,28 @@
6064
expect(data["description"]).to include("This is an organization level custom role.")
6165
expect(data["includedPermissions"]).to include("iam.roles.list")
6266
expect(data["includedPermissions"]).to include("iam.roles.delete")
67+
expect(data["includedPermissions"]).to include("iam.serviceAccounts.list")
68+
expect(data["includedPermissions"]).to include("iam.serviceAccounts.delete")
69+
expect(data["includedPermissions"]).not_to include("iam.serviceAccounts.setIamPolicy")
70+
end
71+
end
72+
end
73+
74+
describe command ("gcloud iam roles describe #{custom_role_id_org_unsupported} --organization #{org_id} --format=json") do
75+
its(:exit_status) { should eq 0 }
76+
its(:stderr) { should eq '' }
77+
78+
let!(:data) do
79+
if subject.exit_status == 0
80+
JSON.parse(subject.stdout)
81+
else
82+
{}
83+
end
84+
end
85+
86+
describe "project_unsupported_custom_role" do
87+
it "does not have permissions" do
88+
expect(data["includedPermissions"]).not_to include("datastore.databases.get")
6389
end
6490
end
6591
end

test/integration/custom-role/inspec.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ attributes:
2020
- name: custom_role_id_org
2121
required: true
2222
type: string
23+
- name: custom_role_id_org_unsupported
24+
required: true
25+
type: string
2326
- name: project_id
2427
required: true
2528
type: string

0 commit comments

Comments
 (0)