Access Policies

This type is only available in EdgeDB 2.0 or later.

Object types can contain security policies that restrict the set of objects that can be selected, inserted, updated, or deleted by a particular query. This is known as object-level security.

Let’s start with a simple schema.

  1. type User {
  2. required property email -> str { constraint exclusive; };
  3. }
  4. type BlogPost {
  5. required property title -> str;
  6. required link author -> User;
  7. }

When no access policies are defined, object-level security is not activated. Any properly authenticated client can select or modify any object in the database.

⚠️ Once a policy is added to a particular object type, all operations (select, insert, delete, and update etc.) on any object of that type are now disallowed by default unless specifically allowed by an access policy!

Defining a global

To start, we’ll add a global variable to our schema. We’ll use this global to represent the identity of the user executing the query.

  1. global current_user -> uuid;
  2. type User {
  3. required property email -> str { constraint exclusive; };
  4. }
  5. type BlogPost {
  6. required property title -> str;
  7. required link author -> User;
  8. }

Global variables are a generic mechanism for providing context to a query. Most commonly, they are used in the context of access policies.

The value of these variables is attached to the client you use to execute queries. The exact API depends on which client library you’re using:

TypeScript

Python

Go

  1. import createClient from 'edgedb';
  2. const client = createClient().withGlobals({
  3. current_user: '2141a5b4-5634-4ccc-b835-437863534c51',
  4. });
  5. await client.query(`select global current_user;`);
  1. from edgedb import create_client
  2. client = create_client().with_globals({
  3. 'current_user': '580cc652-8ab8-4a20-8db9-4c79a4b1fd81'
  4. })
  5. result = client.query("""
  6. select global current_user;
  7. """)
  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "github.com/edgedb/edgedb-go"
  7. )
  8. func main() {
  9. ctx := context.Background()
  10. client, err := edgedb.CreateClient(ctx, edgedb.Options{})
  11. if err != nil {
  12. log.Fatal(err)
  13. }
  14. defer client.Close()
  15. id, err := edgedb.ParseUUID("2141a5b4-5634-4ccc-b835-437863534c51")
  16. if err != nil {
  17. log.Fatal(err)
  18. }
  19. var result edgedb.UUID
  20. err = client.
  21. WithGlobals(map[string]interface{}{"current_user": id}).
  22. QuerySingle(ctx, "SELECT global current_user;", &result)
  23. if err != nil {
  24. log.Fatal(err)
  25. }
  26. fmt.Println(result)
  27. }

Defining a policy

Let’s add a policy to our sample schema.

  1. global current_user -> uuid;
  2. type User {
  3. required property email -> str { constraint exclusive; };
  4. }
  5. type BlogPost {
  6. required property title -> str;
  7. required link author -> User;
  8. access policy author_has_full_access
  9. allow all
  10. using (global current_user ?= .author.id);
  11. }

Let’s break down the access policy syntax piece-by-piece. This policy grants full read-write access (all) to the author of each BlogPost. No one else will be able to edit, delete, or view this post.

We’re using the coalescing equality operator ?= which returns false even if one of its arguments is an empty set.

  • access policy: The keyword used to declare a policy inside an object type.

  • own_posts: The name of this policy; could be any string.

  • allow: The kind of policy; could be allow or deny

  • all: The set of operations being allowed/denied; a comma-separated list of the following: all, select, insert, delete, update, update read, update write.

  • using (<expr>): A boolean expression. Think of this as a filter expression that defined the set of objects to which the policy applies.

Let’s do some experiments.

  1. db>
  1. insert User { email := "test@edgedb.com" };
  1. {default::User {id: be44b326-03db-11ed-b346-7f1594474966}}
  1. db>
  1. set global current_user := <uuid>"be44b326-03db-11ed-b346-7f1594474966";
  1. OK: SET GLOBAL
  1. db>
  2. ...
  3. ...
  4. ...
  1. insert BlogPost {
  2. title := "My post",
  3. author := (select User filter .id = global current_user)
  4. };
  1. {default::BlogPost {id: e76afeae-03db-11ed-b346-fbb81f537ca6}}

We’ve created a User, set the value of current_user to its id, and created a new BlogPost. When we try to select all BlogPost objects, we’ll see the post we just created.

  1. db>
  1. select BlogPost;
  1. {default::BlogPost {id: e76afeae-03db-11ed-b346-fbb81f537ca6}}
  1. db>
  1. select count(BlogPost);
  1. {1}

Now let’s unset current_user and see what happens.

  1. db>
  1. set global current_user := {};
  1. OK: SET GLOBAL
  1. db>
  1. select BlogPost;
  1. {}
  1. db>
  1. select count(BlogPost);
  1. {0}

Now select BlogPost returns zero results. We can only select the posts written by the user specified by current_user. When current_user has no value, we can’t read any posts.

The access policies use global variables to define a “subgraph” of data that is visible to a particular query.

Policy types

For the most part, the policy types correspond to EdgeQL’s statement types:

  • select: Applies to all queries; objects without a select permission cannot be modified either.

  • insert: Applies to insert queries; executed post-insert. If an inserted object violates the policy, the query will fail.

  • delete: Applies to delete queries.

  • update: Applies to update queries.

Additionally, the update operation can broken down into two sub-policies: update read and update write.

  • update read: This policy restricts which objects can be updated. It runs pre-update; that is, this policy is executed before the updates have been applied.

  • update write: This policy restricts how you update the objects; you can think of it as a post-update validity check. This could be used to prevent a User from transferring a BlogPost to another User.

Finally, there’s an umbrella policy that can be used as a shorthand for all the others.

  • all: A shorthand policy that can be used to allow or deny full read/ write permissions. Exactly equivalent to select, insert, update, delete.

Resolution order

An object type can contain an arbitrary number of access policies, including several conflicting allow and deny policies. EdgeDB uses a particular algorithm for resolving these policies.

Access Policies New - 图1

The access policy resolution algorithm, explained with Venn diagrams.

  1. When no policies are defined on a given object type, object-level security is all objects of that type can be read or modified by any appropriately authenticated connection.

  2. EdgeDB then applies all allow policies. Each policy grants a permission that is scoped to a particular set of objects as defined by the using clause. Conceptually, these permissions are merged with the union / or operator to determine the set of allowable actions.

  3. After the allow policies are resolved, the deny policies can be used to carve out exceptions to the allow rules. Deny rules supersede allow rules! As before, the set of objects targeted by the policy is defined by the using clause.

  4. This results in the final access level: a set of objects targetable by each of select, insert, update read, update write, and delete.

Examples

Blog posts are publicly visible if published but only writable by the author.

  1. global current_user -> uuid;
  2. type User {
  3. required property email -> str { constraint exclusive; };
  4. }
  5. type BlogPost {
  6. required property title -> str;
  7. required link author -> User;
  8. required property published -> bool { default := false }
  9. access policy author_has_full_access
  10. allow all
  11. using (global current_user ?= .author.id);
  12. access policy visible_if_published
  13. allow select
  14. using (.published);
  15. }

Blog posts are visible to friends but only modifiable by the author.

  1. global current_user -> uuid;
  2. type User {
  3. required property email -> str { constraint exclusive; };
  4. multi link friends -> User;
  5. }
  6. type BlogPost {
  7. required property title -> str;
  8. required link author -> User;
  9. access policy author_has_full_access
  10. allow all
  11. using (global current_user ?= .author.id);
  12. access policy friends_can_read
  13. allow select
  14. using ((global current_user in .author.friends.id) ?? false);
  15. }

Blog posts are publicly visible except to users that have been blocked by the author.

  1. type User {
  2. required property email -> str { constraint exclusive; };
  3. multi link blocked -> User;
  4. }
  5. type BlogPost {
  6. required property title -> str;
  7. required link author -> User;
  8. access policy author_has_full_access
  9. allow all
  10. using (global current_user ?= .author.id);
  11. access policy anyone_can_read
  12. allow select;
  13. access policy exclude_blocked
  14. deny select
  15. using ((global current_user in .author.blocked.id) ?? false);
  16. }

“Disappearing” posts that become invisible after 24 hours.

Blog posts are publicly visible except to users that have been blocked by the author.

  1. type User {
  2. required property email -> str { constraint exclusive; };
  3. }
  4. type BlogPost {
  5. required property title -> str;
  6. required link author -> User;
  7. required property created_at -> datetime {
  8. default := datetime_of_statement() # non-volatile
  9. }
  10. access policy author_has_full_access
  11. allow all
  12. using (global current_user ?= .author.id);
  13. access policy hide_after_24hrs
  14. allow select
  15. using (datetime_of_statement() - .created_at < <duration>'24 hours');
  16. }

Super constraints

Access policies support arbitrary EdgeQL and can be used to define “super constraints”. Policies on insert and update write can be thought of as post-write “validity checks”; if the check fails, the write will be rolled back.

Due to an underlying Postgres limitation, constraints on object types can only reference properties, not links.

Here’s a policy that limits the number of blog posts a User can post.

  1. type User {
  2. required property email -> str { constraint exclusive; };
  3. multi link posts := .<author[is BlogPost]
  4. }
  5. type BlogPost {
  6. required property title -> str;
  7. required link author -> User;
  8. access policy author_has_full_access
  9. allow all
  10. using (global current_user ?= .author.id);
  11. access policy max_posts_limit
  12. deny insert
  13. using (count(.author.posts) > 500);
  14. }

See also

SDL > Access policies

DDL > Access policies