Introduction
We often get asked about how you would model Infrastructure as a Service (IaaS) permissions in our SpiceDB Schema Language. Since we know that Google Cloud IAM uses Zanzibar internally, it should be possible to use relationship based access control to get the desired effect. It might seem like the most interesting challenge would be modeling the hierarchical organization of Google Cloud into cloud services, and the projects into which they’re deployed. It turns out it’s actually the complete separation of permissions and roles.
Often when we think about role-based access control within the context of a service, the service owner has some idea of what roles are relevant to their service. For example: in Google Docs, there are the owner, editor, commented, and viewer roles, in order of most-to-least privileges. Having explored user-defined roles in a previous post, I wanted to take this opportunity to extend that idea into a more concrete example that separates out permissions completely, and allows for things like gaining access to a whole class of object, not just a particular instance.
I have recently been playing with Google’s Cloud Spanner service as part of turning-up customer SpiceDB Dedicated clusters in Google Cloud Platform, so I’ll use that as an example here. The approach should be fully generalizable to modeling other GCP services as well.
Cloud Spanner Permissions Model
In order to model Cloud Spanner’s permissions model, we first need to understand exactly what the model is. If we look at the full list of spanner.*
permissions in Google Cloud IAM, we can see that there are five explicit types of entities referenced:
- Instances
- Databases
- Sessions
- Database Roles
- Database Operations, such as backup and restore operations
From a hierarchy perspective, the objects relate to one another as follows (arrows point from a sub-resource to the parent):
We can see from the full list of permissions for Cloud Spanner that each level of the Spanner hierarchy has specific permissions that cover one or more of the basic CRUDL operations, as well as some specific verbs for each object type. There are also a few additional rules we must incorporate into our Cloud Spanner model:
- GCP IAM roles are granted to a user within the context of a database, instance, or project.
- Databases inherit all permissions from the instances they belong to.
- Instances inherit permissions from the projects they belong to.
Without further ado, let’s get to modeling!
Modeling
We are going to model in several phases. The first phase will be setting out the skeleton for the objects and permissions.
Permissions Skeleton
First we want to define the object types and how they are going to relate to one another in the hierarchy.
definition user {}
definition project {}
definition spanner_instance {
relation project: project
}
definition spanner_database {
relation instance: spanner_instance
}
Next we’ll stub out support for the various operations that we can do:
definition spanner_instance {
relation project: project
permission get = nil
permission getiampolicy = nil
permission list = nil
}
definition spanner_database {
relation instance: spanner_instance
// Database
permission beginorrollbackreadwritetransaction = nil
permission beginpartitioneddmltransaction = nil
permission beginreadonlytransaction = nil
permission create = nil
permission drop = nil
permission get = nil
permission get_ddl = nil
permission getiampolicy = nil
permission list = nil
permission partitionquery = nil
permission partitionread = nil
permission read = nil
permission select = nil
permission setiampolicy = nil
permission update = nil
permission updateddl = nil
permission userolebasedaccess = nil
permission write = nil
// Sessions
permission create_session = nil
permission delete_session = nil
permission get_session = nil
permission list_sessions = nil
// Database Operations
permission cancel_operation = nil
permission delete_operation = nil
permission get_operation = nil
permission list_operations = nil
// Database Roles
permission list_roles = nil
permission use_role = nil
}
Note: You’ll notice that we listed session, operations, and roles under the database. We could have modeled them explicitly but as they are somewhat more inextricably linked to the database, and ephemeral in the case of operations and sessions, we can handle their permissions on their parent object.
Next, we need to define how we’re going to do custom roles.
Roles and Role Bindings
When modeling our roles and role bindings, we need to be able to answer a few questions:
What are all of the permissions that can be bound to a role? Who is the role going to be bound to? At what layer in the hierarchy are we going to bind it?
First let’s start with the role itself. We’re going to enumerate all of the possible permissions that can be held by the role.
We’re also going to set the subject type for adding a permission to a role to user:*
.
This will allow us to express the concept: “any user who holds this role will have this permission”.
definition role {
relation spanner_databaseoperations_cancel: user:*
relation spanner_databaseoperations_delete: user:*
relation spanner_databaseoperations_get: user:*
relation spanner_databaseoperations_list: user:*
relation spanner_databaseroles_list: user:*
relation spanner_databaseroles_use: user:*
relation spanner_databases_beginorrollbackreadwritetransaction: user:*
relation spanner_databases_beginpartitioneddmltransaction: user:*
relation spanner_databases_beginreadonlytransaction: user:*
relation spanner_databases_create: user:*
relation spanner_databases_drop: user:*
relation spanner_databases_get: user:*
relation spanner_databases_getddl: user:*
relation spanner_databases_getiampolicy: user:*
relation spanner_databases_list: user:*
relation spanner_databases_partitionquery: user:*
relation spanner_databases_partitionread: user:*
relation spanner_databases_read: user:*
relation spanner_databases_select: user:*
relation spanner_databases_setiampolicy: user:*
relation spanner_databases_update: user:*
relation spanner_databases_updateddl: user:*
relation spanner_databases_userolebasedaccess: user:*
relation spanner_databases_write: user:*
relation spanner_instances_get: user:*
relation spanner_instances_getiampolicy: user:*
relation spanner_instances_list: user:*
relation spanner_sessions_create: user:*
relation spanner_sessions_delete: user:*
relation spanner_sessions_get: user:*
relation spanner_sessions_list: user:*
}
Forgive the formatting, I generated this out of the list of permissions in the Cloud IAM console using find and replace. This definition answers the question: “What are all of the permissions that can be bound to a role?” At runtime we will bind a permission to a role by writing a relationship like the following:
role:database_reader#spanner_databases_read@user:*
Next we’ll add an object to bind a role to one or more specific users:
definition role_binding {
relation user: user
relation role: role
permission spanner_databaseoperations_cancel = user & role->spanner_databaseoperations_cancel
permission spanner_databaseoperations_delete = user & role->spanner_databaseoperations_delete
permission spanner_databaseoperations_get = user & role->spanner_databaseoperations_get
permission spanner_databaseoperations_list = user & role->spanner_databaseoperations_list
permission spanner_databaseroles_list = user & role->spanner_databaseroles_list
permission spanner_databaseroles_use = user & role->spanner_databaseroles_use
permission spanner_databases_beginorrollbackreadwritetransaction = user & role->spanner_databases_beginorrollbackreadwritetransaction
permission spanner_databases_beginpartitioneddmltransaction = user & role->spanner_databases_beginpartitioneddmltransaction
permission spanner_databases_beginreadonlytransaction = user & role->spanner_databases_beginreadonlytransaction
permission spanner_databases_create = user & role->spanner_databases_create
permission spanner_databases_drop = user & role->spanner_databases_drop
permission spanner_databases_get = user & role->spanner_databases_get
permission spanner_databases_getddl = user & role->spanner_databases_getddl
permission spanner_databases_getiampolicy = user & role->spanner_databases_getiampolicy
permission spanner_databases_list = user & role->spanner_databases_list
permission spanner_databases_partitionquery = user & role->spanner_databases_partitionquery
permission spanner_databases_partitionread = user & role->spanner_databases_partitionread
permission spanner_databases_read = user & role->spanner_databases_read
permission spanner_databases_select = user & role->spanner_databases_select
permission spanner_databases_setiampolicy = user & role->spanner_databases_setiampolicy
permission spanner_databases_update = user & role->spanner_databases_update
permission spanner_databases_updateddl = user & role->spanner_databases_updateddl
permission spanner_databases_userolebasedaccess = user & role->spanner_databases_userolebasedaccess
permission spanner_databases_write = user & role->spanner_databases_write
permission spanner_instances_get = user & role->spanner_instances_get
permission spanner_instances_getiampolicy = user & role->spanner_instances_getiampolicy
permission spanner_instances_list = user & role->spanner_instances_list
permission spanner_sessions_create = user & role->spanner_sessions_create
permission spanner_sessions_delete = user & role->spanner_sessions_delete
permission spanner_sessions_get = user & role->spanner_sessions_get
permission spanner_sessions_list = user & role->spanner_sessions_list
}
Now we can bind the role to a user by writing a pair of relationships, atomically, in a transaction:
role_binding:jake_is_reader#user@user:jake role_binding:jake_is_reader#role@role:database_reader
Notice the use of the intersection operator &
. This operator is what lets us check both conditions, that the role has the permission, and that the user has the role.
We’ve answered our first two questions, now we need to bind it to our hierarchy to answer our final question:
definition project {
relation granted: role_binding
}
definition spanner_instance {
relation project: project
relation granted: role_binding
…
}
definition spanner_database {
relation instance: spanner_instance
relation granted: role_binding
…
}
Let's put it all together!
Using Roles to Make Decisions
Now that we have our user-defined roles with explicit permissions, our objects, and our permissions defined, the last step is to use those bindings to make our permissions decisions. We’re going to start explicitly referencing role binding permissions (which are acting as synthetic relations) in our specific instance and database permissions computations.
Warning: this is going to be VERY verbose, as we need to thread through the permissions in the hierarchy manually. We hope to address this in the future by allowing for nested arrow ->
syntax.
definition project {
relation granted: role_binding
// Synthetic Instance Relations
permission granted_spanner_instances_get = granted->spanner_instances_get
permission granted_spanner_instances_getiampolicy = granted->spanner_instances_getiampolicy
permission granted_spanner_instances_list = granted->spanner_instances_list
// Synthetic Database Relations
permission granted_spanner_databases_beginorrollbackreadwritetransaction = granted->spanner_databases_beginorrollbackreadwritetransaction
permission granted_spanner_databases_beginpartitioneddmltransaction = granted->spanner_databases_beginpartitioneddmltransaction
permission granted_spanner_databases_beginreadonlytransaction = granted->spanner_databases_beginreadonlytransaction
permission granted_spanner_databases_create = granted->spanner_databases_create
permission granted_spanner_databases_drop = granted->spanner_databases_drop
permission granted_spanner_databases_get = granted->spanner_databases_get
permission granted_spanner_databases_getddl = granted->spanner_databases_getddl
permission granted_spanner_databases_getiampolicy = granted->spanner_databases_getiampolicy
permission granted_spanner_databases_list = granted->spanner_databases_list
permission granted_spanner_databases_partitionquery = granted->spanner_databases_partitionquery
permission granted_spanner_databases_partitionread = granted->spanner_databases_partitionread
permission granted_spanner_databases_read = granted->spanner_databases_read
permission granted_spanner_databases_select = granted->spanner_databases_select
permission granted_spanner_databases_setiampolicy = granted->spanner_databases_setiampolicy
permission granted_spanner_databases_update = granted->spanner_databases_update
permission granted_spanner_databases_updateddl = granted->spanner_databases_updateddl
permission granted_spanner_databases_userolebasedaccess = granted->spanner_databases_userolebasedaccess
permission granted_spanner_databases_write = granted->spanner_databases_write
// Synthetic Sessions Relations
permission granted_spanner_sessions_create = granted->spanner_sessions_create
permission granted_spanner_sessions_delete = granted->spanner_sessions_delete
permission granted_spanner_sessions_get = granted->spanner_sessions_get
permission granted_spanner_sessions_list = granted->spanner_sessions_list
// Synthetic Database Operations Relations
permission granted_spanner_databaseoperations_cancel = granted->spanner_databaseoperations_cancel
permission granted_spanner_databaseoperations_delete = granted->spanner_databaseoperations_delete
permission granted_spanner_databaseoperations_get = granted->spanner_databaseoperations_get
permission granted_spanner_databaseoperations_list = granted->spanner_databaseoperations_list
// Synthetic Database Roles Relations
permission granted_spanner_databaseroles_list = granted->spanner_databaseroles_list
permission granted_spanner_databaseroles_use = granted->spanner_databaseroles_use
}
definition spanner_instance {
relation project: project
relation granted: role_binding
permission get = granted->spanner_instances_get + project->granted_spanner_instances_get
permission getiampolicy = granted->spanner_instances_getiampolicy + project->granted_spanner_instances_getiampolicy
permission list = granted->spanner_instances_list + project->granted_spanner_instances_list
// Synthetic Database Relations
permission granted_spanner_databases_beginorrollbackreadwritetransaction = granted->spanner_databases_beginorrollbackreadwritetransaction + project->granted_spanner_databases_beginorrollbackreadwritetransaction
permission granted_spanner_databases_beginpartitioneddmltransaction = granted->spanner_databases_beginpartitioneddmltransaction + project->granted_spanner_databases_beginpartitioneddmltransaction
permission granted_spanner_databases_beginreadonlytransaction = granted->spanner_databases_beginreadonlytransaction + project->granted_spanner_databases_beginreadonlytransaction
permission granted_spanner_databases_create = granted->spanner_databases_create + project->granted_spanner_databases_create
permission granted_spanner_databases_drop = granted->spanner_databases_drop + project->granted_spanner_databases_drop
permission granted_spanner_databases_get = granted->spanner_databases_get + project->granted_spanner_databases_get
permission granted_spanner_databases_getddl = granted->spanner_databases_getddl + project->granted_spanner_databases_getddl
permission granted_spanner_databases_getiampolicy = granted->spanner_databases_getiampolicy + project->granted_spanner_databases_getiampolicy
permission granted_spanner_databases_list = granted->spanner_databases_list + project->granted_spanner_databases_list
permission granted_spanner_databases_partitionquery = granted->spanner_databases_partitionquery + project->granted_spanner_databases_partitionquery
permission granted_spanner_databases_partitionread = granted->spanner_databases_partitionread + project->granted_spanner_databases_partitionread
permission granted_spanner_databases_read = granted->spanner_databases_read + project->granted_spanner_databases_read
permission granted_spanner_databases_select = granted->spanner_databases_select + project->granted_spanner_databases_select
permission granted_spanner_databases_setiampolicy = granted->spanner_databases_setiampolicy + project->granted_spanner_databases_setiampolicy
permission granted_spanner_databases_update = granted->spanner_databases_update + project->granted_spanner_databases_update
permission granted_spanner_databases_updateddl = granted->spanner_databases_updateddl + project->granted_spanner_databases_updateddl
permission granted_spanner_databases_userolebasedaccess = granted->spanner_databases_userolebasedaccess + project->granted_spanner_databases_userolebasedaccess
permission granted_spanner_databases_write = granted->spanner_databases_write + project->granted_spanner_databases_write
// Synthetic Sessions Relations
permission granted_spanner_sessions_create = granted->spanner_sessions_create + project->granted_spanner_sessions_create
permission granted_spanner_sessions_delete = granted->spanner_sessions_delete + project->granted_spanner_sessions_delete
permission granted_spanner_sessions_get = granted->spanner_sessions_get + project->granted_spanner_sessions_get
permission granted_spanner_sessions_list = granted->spanner_sessions_list + project->granted_spanner_sessions_list
// Synthetic Database Operations Relations
permission granted_spanner_databaseoperations_cancel = granted->spanner_databaseoperations_cancel + project->granted_spanner_databaseoperations_cancel
permission granted_spanner_databaseoperations_delete = granted->spanner_databaseoperations_delete + project->granted_spanner_databaseoperations_delete
permission granted_spanner_databaseoperations_get = granted->spanner_databaseoperations_get + project->granted_spanner_databaseoperations_get
permission granted_spanner_databaseoperations_list = granted->spanner_databaseoperations_list + project->granted_spanner_databaseoperations_list
// Synthetic Database Roles Relations
permission granted_spanner_databaseroles_list = granted->spanner_databaseroles_list + project->granted_spanner_databaseroles_list
permission granted_spanner_databaseroles_use = granted->spanner_databaseroles_use + project->granted_spanner_databaseroles_use
}
definition spanner_database {
relation instance: spanner_instance
relation granted: role_binding
// Database
permission beginorrollbackreadwritetransaction = granted->spanner_databases_beginorrollbackreadwritetransaction + instance->granted_spanner_databases_beginorrollbackreadwritetransaction
permission beginpartitioneddmltransaction = granted->spanner_databases_beginpartitioneddmltransaction + instance->granted_spanner_databases_beginpartitioneddmltransaction
permission beginreadonlytransaction = granted->spanner_databases_beginreadonlytransaction + instance->granted_spanner_databases_beginreadonlytransaction
permission create = granted->spanner_databases_create + instance->granted_spanner_databases_create
permission drop = granted->spanner_databases_drop + instance->granted_spanner_databases_drop
permission get = granted->spanner_databases_get + instance->granted_spanner_databases_get
permission get_ddl = granted->spanner_databases_getddl + instance->granted_spanner_databases_getddl
permission getiampolicy = granted->spanner_databases_getiampolicy + instance->granted_spanner_databases_getiampolicy
permission list = granted->spanner_databases_list + instance->granted_spanner_databases_list
permission partitionquery = granted->spanner_databases_partitionquery + instance->granted_spanner_databases_partitionquery
permission partitionread = granted->spanner_databases_partitionread + instance->granted_spanner_databases_partitionread
permission read = granted->spanner_databases_read + instance->granted_spanner_databases_read
permission select = granted->spanner_databases_select + instance->granted_spanner_databases_select
permission setiampolicy = granted->spanner_databases_setiampolicy + instance->granted_spanner_databases_setiampolicy
permission update = granted->spanner_databases_update + instance->granted_spanner_databases_update
permission updateddl = granted->spanner_databases_updateddl + instance->granted_spanner_databases_updateddl
permission userolebasedaccess = granted->spanner_databases_userolebasedaccess + instance->granted_spanner_databases_userolebasedaccess
permission write = granted->spanner_databases_write + instance->granted_spanner_databases_write
// Sessions
permission create_session = granted->spanner_sessions_create + instance->granted_spanner_sessions_create
permission delete_session = granted->spanner_sessions_delete + instance->granted_spanner_sessions_delete
permission get_session = granted->spanner_sessions_get + instance->granted_spanner_sessions_get
permission list_sessions = granted->spanner_sessions_list + instance->granted_spanner_sessions_list
// Database Operations
permission cancel_operation = granted->spanner_databaseoperations_cancel + instance->granted_spanner_databaseoperations_cancel
permission delete_operation = granted->spanner_databaseoperations_delete + instance->granted_spanner_databaseoperations_delete
permission get_operation = granted->spanner_databaseoperations_get + instance->granted_spanner_databaseoperations_get
permission list_operations = granted->spanner_databaseoperations_list + instance->granted_spanner_databaseoperations_list
// Database Roles
permission list_roles = granted->spanner_databaseroles_list + instance->granted_spanner_databaseroles_list
permission use_role = granted->spanner_databaseroles_use + instance->granted_spanner_databaseroles_use
}
Putting It All Together
I’ve included a playground share link with the complete example that you can hopefully use to adapt and expand for your own use. You can also look forward to a future post where I will cover how to use our brand new caveated relationships feature to model the Google Cloud IAM conditions feature for even more fine-grained access! If this kind of modeling is relevant to your organization’s use case, but you’re still struggling a bit, I highly recommend joining our Discord or scheduling a call!
Additional Reading
If you’re interested in learning more about Authorization and Google Zanzibar, we recommend reading the following posts: