Implementing Notion Authorization Model

In this article, we will examine Notion authorization model and implement it using Permify.


Introduction

This is the second article of the series implementing real world authorization models. Previously we cover Facebook Groups. The motivation behind this series is we frequently receive questions about how to model different types of permissions or policies using Permify Schema Language. For those who may not be familiar, the schema language is an authorization language that allows you to define arbitrary relations between users and objects to model your authorization logic. And its offered by Permify, an open-source relationship-based authorization service. Just to let you know, I'm one of the maintainers and co-founders of Permify :)

We already have documentation, examples, analogies, and other resources that demonstrate how our schema language works and its different aspects. Yet, when it comes to complex cases, there may be multiple approaches that can achieve the same results. So it's quite normal for users to seek our take and best practices for such scenarios.

Therefore we have started to search and implement "real life" examples that held with Permify to our resources, we believe this approach could be the engine to drive more adoption to our schema language.

So in this article, we’ll examine Notion’s authorization model and implement it using Permify.

Notion and Its Access Control Structure

Notion is a versatile productivity tool that allows individuals and teams to organize and manage their projects, documents, and information. It provides a flexible and customizable platform for creating and sharing content, combining features of note-taking, task management, and database organization.

In Notion, a workspace is the top-level organizational unit where users collaborate and manage their content. Workspaces can be created for personal use, teams, or organizations. The workspace owner has full administrative control and can invite others to join the workspace.

Within a workspace, members can be assigned different roles that determine their level of access and capabilities. These roles include admin, bot, guest.

Notion's content is organized into pages and databases. Pages contains blocks, which are individual units of content within a page, such as text, images, headings, checkboxes, bulleted lists, code snippets, and more. Blocks can be organized, rearranged, and nested to create structured and dynamic content. Databases are structured collections of information. Permissions can be set on individual pages or databases, allowing granular control over who can access and modify specific content.

Notion also has templates, which are pre-designed structures or layouts that can be used as a starting point for creating new pages or databases. Templates help streamline workflows and provide consistency by offering predefined structures, content formats, and organization.

When it comes to authorization approach, most of the entities inherit the permissions of the workspace or the parent entity where they are used. In other words, in Notion the access permissions are determined by the permissions set for the workspace or the parent in which the entity is being utilized.

Implementation with Permify

Let's implement the Notion authorization with Permify schema language.

Defining Entities & Relation Between Resources

The very first step to build authorization model with Permify schema language is creating your entities.

So according to our Notion example we have several entities:

user: Represents a user in the system.

workspace: Represents a workspace in which users can collaborate. Each workspace has an owner, members, guests, and bots associated with it. The owner and admin users have permission to manage the workspace. Permissions are defined for creating pages, inviting members, viewing the workspace, and managing the workspace. The read and write permissions can be inherited by child entities.

page: Represents a page within a workspace. Each page is associated with a workspace and has a writer and readers. The read and write permissions are defined based on the writer and readers of the page and can be inherited from the workspace.

database: Represents a database within a workspace. Each database is associated with a workspace and has an editor and viewers. The read and write permissions are defined based on the editor and viewers of the database and can be inherited from the workspace. Permissions are also defined for creating and deleting databases.

block: Represents a block within a page or database. Each block is associated with a page or database and has an editor and commenters. The read and write permissions are defined based on the editor and commenters of the block and can be inherited from the database. Commenters are users who have permission to comment on the block.

comment: Represents a comment on a block. Each comment is associated with a block and has an author. The read and write permissions are defined based on the author of the comment and can be inherited from the block.

template: Represents a template within a workspace. Each template is associated with a workspace and has a creator and viewers. The read and write permissions are defined based on the creator and viewers of the template and can be inherited from the workspace. Permissions are also defined for creating and deleting templates.

integration: Represents an integration within a workspace. Each integration is associated with a workspace and has an owner. Permissions are defined for reading and writing to the integration.

Lets put those entities and their relations to our schema language,

entity user {}

entity workspace {
    // The owner of the workspace
    relation owner @user
    // Members of the workspace
    relation member @user
    // Guests (users with read-only access) of the workspace
    relation guest @user
    // Bots associated with the workspace
    relation bot @user
    // Admin users who have permission to manage the workspace
    relation admin @user
}

entity page {
    // The workspace associated with the page
    relation workspace @workspace
     // The user who can write to the page
    relation writer @user
     // The user(s) who can read the page (members of the workspace or guests)
    relation reader @user @workspace#member @workspace#guest
}

entity database {
    // The workspace associated with the database
    relation workspace @workspace
    // The user who can edit the database
    relation editor @user
    // The user(s) who can view the database (members of the workspace or guests)
    relation viewer @user @workspace#member @workspace#guest
}

entity block {
    // The page associated with the block
    relation page @page
    // The database associated with the block

    relation database @database
    // The user who can edit the block
    relation editor @user
    // The user(s) who can comment on the block (readers of the parent object)
    relation commenter @user @page#reader
}

entity template {
   // The workspace associated with the template
    relation workspace @workspace
    // The user who creates the template
    relation creator @user

    // The user(s) who can view the page (members of the workspace or guests)
    relation viewer @user @workspace#member @workspace#guest

}

entity integration {
    // The workspace associated with the integration
    relation workspace @workspace

    // The owner of the integration
    relation owner @user

    // Define permissions for integration actions

}

Defining Permissions

We have several permissions attached with the entities.

Permify Schema language supports and, or and not operators to achieve permission intersection and exclusion. The keywords action or permission can be used with those operators to form rules for your authorization logic.

For the sake of simplicity I won’t define all of the permissions, but to give a comprehensive understanding let’s define a read permission for the page.

This permission will specify who can read the contents of a specific page at Notion.

entity page {

    // The workspace associated with the page
    relation workspace @workspace
    
    ..
    ..
    
    // The user(s) who can read the page (members of the workspace or guests)
    relation reader @user @workspace#member @workspace#guest

    ..
    ..

    // Define permissions for page actions
    permission read = reader or workspace.read

    ..
    ..
}

The reader relation specifies the users who are members of the workspace associated with the page (workspace#member) or guests of the workspace (workspace#guest).

The other condition workspace.read is inherited from the read permission of the workspace entity, see below.‍

entity workspace {
    // The owner of the workspace
    relation owner @user
    // Members of the workspace
    relation member @user
    // Guests (users with read-only access) of the workspace
    relation guest @user
    // Bots associated with the workspace
    relation bot @user
    // Admin users who have permission to manage the workspace
    relation admin @user

    // Define permissions for workspace actions

    ..
    ..

    // Define permissions that can be inherited by child entities
    permission read = member or guest or bot or admin
    ..
}

So this permission specifies that any user who has been granted read access to the workspace object (i.e., the workspace that the page belongs to) can also read the page.

In summary, any user who is a member or guest of the workspace and has been granted read access to the page through the reader relation, as well as any user who has been granted read access to the workspace itself, can read the contents of the page.‍

Complete Schema

So let’s add the remaining permissions and complete our schema.

entity user {}

entity workspace {
    // The owner of the workspace
    relation owner @user
    // Members of the workspace
    relation member @user
    // Guests (users with read-only access) of the workspace
    relation guest @user
    // Bots associated with the workspace
    relation bot @user
    // Admin users who have permission to manage the workspace
    relation admin @user

    // Define permissions for workspace actions
    permission create_page = owner or member or admin
    permission invite_member = owner or admin
    permission view_workspace = owner or member or guest or bot
    permission manage_workspace = owner or admin

    // Define permissions that can be inherited by child entities
    permission read = member or guest or bot or admin
    permission write = owner or admin
}

entity page {
    // The workspace associated with the page
    relation workspace @workspace
     // The user who can write to the page
    relation writer @user
     // The user(s) who can read the page (members of the workspace or guests)
    relation reader @user @workspace#member @workspace#guest

    // Define permissions for page actions
    permission read = reader or workspace.read
    permission write = writer or workspace.write
}

entity database {
    // The workspace associated with the database
    relation workspace @workspace
    // The user who can edit the database
    relation editor @user
    // The user(s) who can view the database (members of the workspace or guests)
    relation viewer @user @workspace#member @workspace#guest

    // Define permissions for database actions
    permission read = viewer or workspace.read
    permission write = editor or workspace.write
    permission create = editor or workspace.write
    permission delete = editor or workspace.write
}

entity block {
    // The page associated with the block
    relation page @page
    // The database associated with the block

    relation database @database
    // The user who can edit the block
    relation editor @user
    // The user(s) who can comment on the block (readers of the parent object)
    relation commenter @user @page#reader

    // Define permissions for block actions
    permission read = database.read or commenter
    permission write = editor or database.write
    permission comment = commenter
}

entity template {
   // The workspace associated with the template
    relation workspace @workspace
    // The user who creates the template
    relation creator @user

    // The user(s) who can view the page (members of the workspace or guests)
    relation viewer @user @workspace#member @workspace#guest

    // Define permissions for template actions
    permission read = viewer or workspace.read
    permission write = creator or workspace.write
    permission create = creator or workspace.write
    permission delete = creator or workspace.write
}

entity integration {
    // The workspace associated with the integration
    relation workspace @workspace

    // The owner of the integration
    relation owner @user

    // Define permissions for integration actions
    permission read = workspace.read
    permission write = owner or workspace.write
}

‍Creating Sample Authorization Data

To test authorization model that created above, we need to have related data. Each data should represent relationships between entities, objects, and users and builds up a collection of access control lists (ACLs).

In Permify, we use a specific form to represents object-to-object and object-to-subject relations. It’s called relational tuples.

the simplest form of relational tuple structured as: entity # relation @ user. Each relational tuple represents an action that a specific user or user set can do on a resource and takes form of user U has relation R to object O, where user U could be a simple user or a user set such as group X members.

Here are some relational tuples according to our Notion example,

// Assign users to different workspaces:
workspace:engineering_team#owner@user:alice
workspace:engineering_team#member@user:bob
workspace:engineering_team#guest@user:charlie
workspace:engineering_team#admin@user:alice
workspace:sales_team#owner@user:david
workspace:sales_team#member@user:eve
workspace:sales_team#guest@user:frank
workspace:sales_team#admin@user:david

// Connect pages, databases, and templates to workspaces:
page:project_plan#workspace@workspace:engineering_team
page:product_spec#workspace@workspace:engineering_team
database:task_list#workspace@workspace:engineering_team
template:weekly_report#workspace@workspace:sales_team
database:customer_list#workspace@workspace:sales_team
template:marketing_campaign#workspace@workspace:sales_team

// Set permissions for pages, databases, and templates:
page:project_plan#writer@user:frank
page:project_plan#reader@user:bob

database:task_list#editor@user:alice
database:task_list#viewer@user:bob 

template:weekly_report#creator@user:alice
template:weekly_report#viewer@user:bob 

page:product_spec#writer@user:david
page:product_spec#reader@user:eve 

database:customer_list#editor@user:david
database:customer_list#viewer@user:eve 

template:marketing_campaign#creator@user:david
template:marketing_campaign#viewer@user:eve 

// Set relationships for blocks and comments:
block:task_list_1#database@database:task_list
block:task_list_1#editor@user:alice
block:task_list_1#commenter@user:bob 
block:task_list_2#database@database:task_list
block:task_list_2#editor@user:alice
block:task_list_2#commenter@user:bob

‍Test & Validation

Finally, we be able to check some permissions and test our authorization logic.

Lets start with the simple access check for the database entity,

can user:alice write database:task_list ?

entity database {
        // The workspace associated with the database
        relation workspace @workspace
        // The user who can edit the database
        relation editor @user

        ..
        ..

        // Define permissions for database actions
        ..
        ..

        permission write = editor or workspace.write

        ..
        ..
    }

According to what we have defined for the 'write' permission, users who are either;

The editor in task list database (database:task_list)

Have a write permission in the engineering team workspace, which is the only workspace that task list is associated (database:task_list#workspace@workspace:engineering_team) can edit the task list database (database:task_list) Based on the relation tuples we created, user:alice doesn't have the editor relationship with the database:task_list.

Since user:alice is the owner and admin in the engineering team workspace (workspace:engineering_team#admin@user:alice) it has a write permission defined in the workspace entity, as you can see below:‍

entity workspace {
    // The owner of the workspace
    relation owner @user
    ..
    ..
    // Admin users who have permission to manage the workspace
    relation admin @user

    ..
    ..

    // Define permissions that can be inherited by child entities
    ..
    permission write = owner or admin
}

And as we mentioned the engineering team workspace is the only workspace that task list is associated (database:task_list#workspace@workspace:engineering_team). Therefore, the user:alice write database:task_list check request should yield a 'true' response. ‍ Lets move to another access check, ‍

can user:charlie write page:product_spec ?‍

entity page {
    // The workspace associated with the page
    relation workspace @workspace
    // The user who can write to the page
    relation writer @user

    ..
    ..

    permission write = writer or workspace.write
}

‍user:charlie is guest in the workspace (workspace:engineering_team#guest@user:charlie) and the engineering team workspace is the only workspace that page:product_spec belongs to.

As we defined, guests doesn't have write permission in a workspace.‍

entity workspace {
   // The owner of the workspace
   relation owner @user
   // Admin users who have permission to manage the workspace
   relation admin @user

   ..
   ..

   permission write = owner or admin
}

So that, user:charlie doesn't have a write relationship in the workspace. And ultimately, the user:charlie write page:product_spec check request should yield a 'false' response.

Let's test these two access checks in our local with using permify validator. We'll use the below schema for the validation yaml file.

schema: >-
  entity user {}

  entity workspace {
      // The owner of the workspace
      relation owner @user
      // Members of the workspace
      relation member @user
      // Guests (users with read-only access) of the workspace
      relation guest @user
      // Bots associated with the workspace
      relation bot @user
      // Admin users who have permission to manage the workspace
      relation admin @user

      // Define permissions for workspace actions
      permission create_page = owner or member or admin
      permission invite_member = owner or admin
      permission view_workspace = owner or member or guest or bot
      permission manage_workspace = owner or admin

      // Define permissions that can be inherited by child entities
      permission read = member or guest or bot or admin
      permission write = owner or admin
  }

  entity page {
      // The workspace associated with the page
      relation workspace @workspace
      // The user who can write to the page
      relation writer @user
      // The user(s) who can read the page (members of the workspace or guests)
      relation reader @user @workspace#member @workspace#guest

      // Define permissions for page actions
      permission read = reader or workspace.read
      permission write = writer or workspace.write
  }

  entity database {
      // The workspace associated with the database
      relation workspace @workspace
      // The user who can edit the database
      relation editor @user
      // The user(s) who can view the database (members of the workspace or guests)
      relation viewer @user @workspace#member @workspace#guest

      // Define permissions for database actions
      permission read = viewer or workspace.read
      permission write = editor or workspace.write
      permission create = editor or workspace.write
      permission delete = editor or workspace.write
  }

  entity block {
      // The page associated with the block
      relation page @page
      // The database associated with the block

      relation database @database
      // The user who can edit the block
      relation editor @user
      // The user(s) who can comment on the block (readers of the parent object)
      relation commenter @user @page#reader

      // Define permissions for block actions
      permission read = database.read or commenter
      permission write = editor or database.write
      permission comment = commenter
  }

  entity comment {
      // The block associated with the comment
      relation block @block

      // The author of the comment
      relation author @user

      // Define permissions for comment actions
      permission read = block.read
      permission write = author
  }

  entity template {
  // The workspace associated with the template
      relation workspace @workspace
      // The user who creates the template
      relation creator @user

      // The user(s) who can view the page (members of the workspace or guests)
      relation viewer @user @workspace#member @workspace#guest

      // Define permissions for template actions
      permission read = viewer or workspace.read
      permission write = creator or workspace.write
      permission create = creator or workspace.write
      permission delete = creator or workspace.write
  }

  entity integration {
      // The workspace associated with the integration
      relation workspace @workspace

      // The owner of the integration
      relation owner @user

      // Define permissions for integration actions
      permission read = workspace.read
      permission write = owner or workspace.write
  }

relationships:
  - workspace:engineering_team#owner@user:alice
  - workspace:engineering_team#member@user:bob
  - workspace:engineering_team#guest@user:charlie
  - workspace:engineering_team#admin@user:alice
  - workspace:sales_team#owner@user:david
  - workspace:sales_team#member@user:eve
  - workspace:sales_team#guest@user:frank
  - workspace:sales_team#admin@user:david
  - page:project_plan#workspace@workspace:engineering_team
  - page:product_spec#workspace@workspace:engineering_team
  - database:task_list#workspace@workspace:engineering_team
  - template:weekly_report#workspace@workspace:sales_team
  - database:customer_list#workspace@workspace:sales_team
  - template:marketing_campaign#workspace@workspace:sales_team
  - page:project_plan#writer@user:frank
  - page:project_plan#reader@user:bob
  - database:task_list#editor@user:alice
  - database:task_list#viewer@user:bob
  - template:weekly_report#creator@user:alice
  - template:weekly_report#viewer@user:bob
  - page:product_spec#writer@user:david
  - page:product_spec#reader@user:eve
  - database:customer_list#editor@user:david
  - database:customer_list#viewer@user:eve
  - template:marketing_campaign#creator@user:david
  - template:marketing_campaign#viewer@user:eve
  - block:task_list_1#database@database:task_list
  - block:task_list_1#editor@user:alice
  - block:task_list_1#commenter@user:bob
  - block:task_list_2#database@database:task_list
  - block:task_list_2#editor@user:alice
  - block:task_list_2#commenter@user:bob

scenarios:
  - name: "scenario 1"
    description: "test description"
    checks:
      - entity: "database:task_list"
        subject: "user:alice"
        assertions:
          write: true
      - entity: "page:product_spec"
        subject: "user:charlie"
        assertions:
          write: false

To use permify validate function clone the Permify from GitHub and open up a new file and copy the schema yaml file content inside. Then, build and run Permify instance using the command

make serve

permify-terminal

Then to start the test process run.

permify validate {`path of your schema validation file`}

The validation result according to our example schema validation file:

permify-validate

Conclusion‍

This is the end of demonstration and implementation of the authorization structure for Notion with using Permify. You can find this example on our playground to examine it on your browser.

If you are interested in learning more about our solution or believe that it may be beneficial to your organization, please don't hesitate to join our community on discord. We welcome the opportunity to chat and discuss.