Adding context-aware conditions to GCP IAM policies lets you grant permissions based on dynamic attributes of the request, rather than just the identity of the principal.
Let’s say you have a team that needs to access sensitive data in a GCS bucket, but only from within your corporate network during business hours. Without conditions, you’d either grant broad access and hope for the best, or create complex, brittle policies that are hard to manage. With conditions, you can achieve this granular control elegantly.
Here’s how it works in practice. Imagine you want to grant a user developer@example.com read access to a GCS bucket my-sensitive-data but only if the request originates from a specific IP range and the request is made on a weekday between 9 AM and 5 PM in UTC.
First, you’d define a custom IAM role (or use an existing one like roles/storage.objectViewer) that has the storage.objects.get permission. Then, you’d create an IAM policy binding for that role on the my-sensitive-data bucket. The crucial part is adding the condition:
{
"bindings": [
{
"role": "roles/storage.objectViewer",
"members": [
"user:developer@example.com"
],
"condition": {
"title": "Corporate Network and Business Hours Access",
"description": "Grants access only from corporate IPs during business hours.",
"expression": "request.time >= timestamp(\"2023-10-27T09:00:00Z\") && request.time < timestamp(\"2023-10-27T17:00:00Z\") && request.ip.is_in_ip_range(\"192.168.1.0/24\")"
}
}
]
}
In this example, request.time is a built-in attribute representing the timestamp of the request. timestamp("2023-10-27T09:00:00Z") converts a string into a comparable timestamp. We’re checking if the request time is within a specific window. request.ip is another built-in attribute, and is_in_ip_range is a CEL (Common Expression Language) function that checks if the request’s IP address falls within the specified CIDR block.
The expression field uses CEL, a powerful and flexible expression language. You can combine multiple conditions using logical operators like && (AND), || (OR), and ! (NOT). GCP provides a rich set of predefined attributes like request.time, request.ip, resource.type, resource.name, resource.service, and many more, that you can use in your conditions.
The real power comes from the resource attributes. You can create policies that are sensitive to the specific resource being accessed. For instance, you could grant a user the ability to delete GCS objects, but only if the object’s name starts with a specific prefix, or only if the object is tagged with a certain label.
{
"bindings": [
{
"role": "roles/storage.objectAdmin",
"members": [
"serviceAccount:my-cleanup-sa@my-project.iam.gserviceaccount.com"
],
"condition": {
"title": "Delete only staging objects",
"description": "Allows deletion only for objects prefixed with 'staging/'.",
"expression": "resource.name.startsWith(\"projects/_/buckets/my-bucket/objects/staging/\")"
}
}
]
}
Here, resource.name is evaluated against the full resource path. The startsWith function checks if the object’s name begins with the specified prefix. This prevents the service account from accidentally deleting production data.
You can also use conditions based on the resource.type or resource.service to apply policies broadly across certain resource types or services. For example, to allow a specific group to manage all Compute Engine instances but not disks:
{
"bindings": [
{
"role": "roles/compute.admin",
"members": [
"group:compute-admins@example.com"
],
"condition": {
"title": "Compute Admin - Instances Only",
"description": "Grants Compute Engine admin access, but only to instances.",
"expression": "resource.type == \"compute.googleapis.com/Instance\""
}
}
]
}
This ensures that even though the compute.admin role might have permissions for other Compute Engine resources, this policy binding will only grant those permissions when the target resource is an instance.
When a request comes in, GCP evaluates the IAM policy. If a binding has a condition, the condition is evaluated before the permission is granted. If the condition evaluates to true, the permission is granted (assuming other policies also allow it). If it evaluates to false, the permission is denied for that specific request, even if the principal is otherwise authorized by that binding. This evaluation happens at the edge, before the request even hits the service.
A common pitfall is using time-based conditions without considering time zones. request.time is always UTC. So, if you want to restrict access to "business hours" in a specific local time zone, you need to perform the conversion within your CEL expression. For example, to restrict access to PST (UTC-8) business hours (9 AM to 5 PM):
"request.time.getHours(\"-08:00\") >= 9 && request.time.getHours(\"-08:00\") < 17"
The getHours() function can take a time zone offset. This allows for precise temporal control without relying on user-specific clock settings or complex external logic.
The next logical step after mastering context-aware conditions is exploring how to use them with organizational policies to enforce consistent security postures across your GCP environment.