Modeling Google Cloud IAM in SpiceDB

Modeling Google Cloud IAM in SpiceDB

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:

  1. Instances
  2. Databases
  3. Sessions
  4. Database Roles
  5. 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):

Permissions Hierarchy

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!

Recent Blog Posts
blog-image
blog-image
blog-image
blog-image