Google Cloud Identity and Access Management (IAM) has come a long way since the inception of Google Cloud Platform (GCP) nearly 9 years ago. In the early days of IAM, users could only select from 3 roles, referred to today as the “primitive” roles (Viewer, Editor, and Owner). These roles provided very broad access and it was all or nothing, you either had permissions to all the resources and services in a project or none of them.
Over the past 4 years Google Cloud has significantly improved IAM through the addition of predefined and custom role types. These new role types allow GCP customers to define permissions scoped more closely to the services being used. However, these new role types still provide access to all of the resources within a project that the role provides access to. For example, binding a user to the Compute Instance Admin Role provides them access to ALL of the instances in the project.
So what happens when a user only needs access to a handful of the instances within the project? The previous recommendation in this scenario was to break the instances out into different projects, creating isolation through the project boundary. This approach works for most cases, but did not provide the flexibility some users desired. To meet this demand, Google released functionality to define a logic-based condition for a role binding to allow or deny access. IAM Conditions were made GA in late February 2020 and new features have slowly been making their way through the product release cycle.
In this blog, we will take a look at some of the intricacies present when using IAM Conditions and highlight a few gotchas we’ve encountered when working with customers to implement IAM conditions into their environment.
Common Expression Language (CEL)
CEL is the language used to write IAM Conditions and is common across other products in the Google Cloud Suite (Firebase, IAP, etc). CEL is not a traditional programming language but follows many of the same principles which should allow anyone with some programming experience a shallow learning curve. The language is also simple enough that even those without prior programming experience will be able to learn how to write expressions with relative ease.
Google has provided a fantastic overview of the IAM Conditions topic in the IAM Conditions Overview and Configuring Resource Based Access pages, even including examples for common use cases. Unfortunately, some of the details which help users connect the dots on how Google arrived at the example condition, seem to have been left out.
Deep Dive
Let’s take a look at two examples and provide some clarity on the finer details of an IAM Condition. Some of these are obvious but others not so much!
Example 1: Granting access to a group of resources based on resource name prefixes
To begin with, let’s assume the below condition is being attached to the Compute Admin Role.
(resource.type == "compute.googleapis.com/Disk" &&
resource.name.startsWith("projects/project-123/regions/us-
central1/disks/dev-access")) ||
(resource.type == "compute.googleapis.com/Instance" &&
resource.name.startsWith("projects/project-123/zones/us-central1-
a/instances/dev-access")) ||
(resource.type != "compute.googleapis.com/Disk" &&
resource.type != "compute.googleapis.com/Instance")
First statement
CEL | Translation |
---|---|
(resource.type == "compute.googleapis.com/Disk" && resource.name.startsWith("projects/project-123/regions/us-central1/disks/dev-access")) || | “The resource we are making a request on is a Google Compute Engine Disk AND the disk’s name starts with "projects/project-123/regions/us-central1/disks/dev-access". |
It should be fairly straightforward that the statement enforces the disk name to starts with dev-access*
, but what may not be obvious initially is that it also enforces the project and zone of the disk. In the above example, in addition to the instance name, the condition enforces that the disk lives in project-123 and is deployed in the us-central1 region. For cases where you still want to enforce the name “dev-access”, but don’t want to specify a project or zone, use the extract() function.
The Resource Types (resource.type) and Resource Names (resource.name) follow a particular syntax and can be found in the linked documentation. Be sure to reference these documents for the correct syntax and to ensure the resource type is supported by IAM Conditions. CEL also supports functions and the startWith() function (see CEL above for example) is commonly used with IAM conditions to check for a prefix in the resource name.
Notice the “||
” at the end of the statement. This represents a logical operator OR, meaning the larger expression will evaluate to TRUE if this statement OR the proceeding statement evaluates to TRUE.
Second Statement
CEL | Translation |
---|---|
(resource.type == "compute.googleapis.com/Instance" && resource.name.startsWith("projects/project-123/zones/us-central1-a/instances/dev-access")) || | “The resource we are making a request on is a Google Compute Instance AND the disk’s name starts with "projects/project-123/zones/us-central1-a/instances/dev-access". |
Similar to the first statement, we are enforcing that the user can only use project-123 and the us-central1-a zone in addition to the instance name being prefixed with “dev-access”.
Third Statement
CEL | Translation |
---|---|
(resource.type != "compute.googleapis.com/Disk" && resource.type != "compute.googleapis.com/Instance") | “The resource we are making a request on is not a Compute Engine Disk AND is not a Compute Engine Instance". |
The need for this statement in the condition is the least clear to those who are just beginning to work with IAM Conditions. There are two main cases for why this statement would be included:
- The IAM Role attached to this condition provides permissions to other resource types than the ones which are defined in the condition.
- The API method the user calls can interact with multiple resources types.
For the first case, recall that we have attached this condition to the Compute Admin Role. The Compute Admin Role has permissions to interact with not only disks and instances, but a variety of other compute resources such as VPCs, Cloud NAT, external IPs. If we were to attach this condition without the third statement, the binding would not provide access to these other resource types.
Example 2: Interacting with GCP Firewall Rules
Another common use case for IAM conditions arises when needing to provide access to a subset of Google Cloud firewalls. This need is especially relevant when using a Shared VPC, where instances are in decentralized projects but the firewall rules are centralized in the host project. Providing certain users access to a subset of the firewall rules, requires the use of an IAM Condition.
Let’s assume we are using the Compute Security Admin Role with the following IAM condition:
(resource.type == “compute.googleapis.com/Firewall &&
(resource.name.extract(‘/firewalls/{name}’).startsWith(‘dev-
access’)) ||
resource.type != “compute.googleapis.com/Firewall”
First Statement
CEL | Translation |
---|---|
(resource.type == “compute.googleapis.com/Firewall && (resource.name.extract(‘/firewalls/{name}’).startsWith(‘dev-access’)) || | “The resource we are making a request on is a Google Compute Firewall and the name must start with dev-access”. |
This statement is fairly straightforward and follows the same logic we saw in example 1.
Second Statement
CEL | Translation |
---|---|
resource.type != “compute.googleapis.com/Firewall” | “The resource we are making a request on is not a firewall rule". |
Similar to the first example, the last statement in the expression is not as straightforward.
If you were to leave off this statement from the condition and attempt to create a Firewall rule named “dev-access-2”, you would be returned with the following error:
Creating firewall...failed.
ERROR: (gcloud.compute.firewall-rules.create) Could not fetch resource:
- Required 'compute.networks.updatePolicy' permission for
'projects/my-project/global/networks/default'
The reason for this failure is that the compute.firewalls.insert()
method does not interact with only the firewall resource, it also interacts with the network resource! This not-so obvious interaction can be seen in the audit log for a firewall rule:
With the removal of the statement 2 from the expression, the condition only allowed interactions with the resource.type = compute.firewalls. However, we can see from the above audit log that the compute.firewalls.insert()
method also interacts with the resource.type = compute.network. Statement 2 allows this, providing access to resources which are not Google Compute Firewalls and to which the role provides permissions to.
Unfortunately, it is not always obvious which resources a method call interacts with. For example, the API reference documentation for the compute.firewalls.insert()
method does not specify that the compute.networks.updatePolicy is required for a successful firewall creation. It is therefore recommended that you test out IAM conditions before implementing them, ensuring that you are able to catch any of these edge cases.
Conclusion
IAM Conditions are a powerful and much appreciated addition to the GCP Cloud IAM ecosystem. These conditions allow users to limit access to a subset of resources within GCP based on an attribute. We are hopeful that as the service matures, new attributes such as labels will be supported by IAM Conditions.