├── LICENSE ├── README.md └── zanzibar.sql /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Joseph Glanville 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zanzibar PG 2 | 3 | This is a proof of concept implementation of the [Zanzibar](https://research.google/pubs/pub48190/) ACL language in pure PL/pgSQL. 4 | 5 | The goal is only to replicate the relation tuple resolution API. 6 | 7 | Much of Zanzibar's special features are actually implementation details of the distributed caching and consistency model which this project makes not attempt to replicate. 8 | 9 | My motivation for creating this was to ensure I fully understand how the resolution works when using naive depth first search and explore how simply it can be done if you don't need a globally available version of this for smaller projects that still have complex authorization needs. 10 | 11 | ## Functions 12 | 13 | ### Expand 14 | Expand allows users to compute the list of subjects that satisfy a relation on an object. 15 | ```sql 16 | SELECT zanzibar_expand('view', 'videos', '/cats/1.mp4'); 17 | ``` 18 | 19 | ### Check 20 | Check is used to check if a specific subject has a relation on a object either directly or via any subject sets. 21 | ```sql 22 | SELECT zanzibar_check('*', 'view', 'videos', '/cats/1.mp4'); 23 | ``` 24 | 25 | ### Enumerate 26 | Not part of the Zanzibar API. This performs the reverse lookup of expand and finds all resources a subject has access to in a given namespace. 27 | ```sql 28 | SELECT zanzibar_enumerate('cat lady', 'view', 'videos'); 29 | ``` 30 | 31 | ## Limitations 32 | 33 | No subject-set rewrites meaning that all tuples to be evaluated need to be materialised. 34 | 35 | ## Related projects/services 36 | 37 | * [Ory Keto](https://www.ory.sh/keto/) - An open-source Go implemenation of Zanzibar. 38 | * [access-controller](https://github.com/authorizer-tech/access-controller) - Another Go implementation of Zanzibar, seems further along than Keto w.r.t distributed operation in particular. 39 | * [authzed](https://authzed.com/) - SaaS that implements the Zanzibar API with slight tweaks for better multi-tenancy. 40 | * [SpiceDB](https://github.com/authzed/spicedb) - Open-source implementation of Zanzibar behind [authzed](https://authzed.com/), looks to be the most production ready and supports PostgreSQL and CockroachDB for storage. 41 | -------------------------------------------------------------------------------- /zanzibar.sql: -------------------------------------------------------------------------------- 1 | -- Main tuples table 2 | DROP TABLE IF EXISTS tuples; 3 | 4 | CREATE TABLE tuples ( 5 | object text NOT NULL, 6 | object_namespace text NOT NULL, 7 | relation text NOT NULL, 8 | subject text NOT NULL, 9 | subject_namespace text, -- nullable because only valid if subject set 10 | subject_relation text -- again only applicable for subject sets 11 | ); 12 | 13 | -- Test dataset 14 | INSERT INTO tuples (object, object_namespace, relation, subject, subject_namespace, subject_relation) VALUES 15 | ('/cats', 'videos', 'owner', 'cat lady', NULL, NULL), 16 | ('/cats', 'videos', 'view', '/cats', 'videos', 'owner'), 17 | ('/cats/2.mp4', 'videos', 'owner', '/cats', 'videos', 'owner'), 18 | ('/cats/2.mp4', 'videos', 'view', '/cats', 'videos', 'owner'), 19 | ('/cats/1.mp4', 'videos', 'view', '*', NULL, NULL), 20 | ('/cats/1.mp4', 'videos', 'owner', '/cats', 'videos', 'owner'), 21 | ('/cats/1.mp4', 'videos', 'view', '/cats/1.mp4', 'videos', 'owner'); 22 | 23 | CREATE OR REPLACE FUNCTION zanzibar_expand (p_relation text, p_object_namespace text, p_object text, p_seen text[] DEFAULT '{}' ::text[]) 24 | RETURNS TABLE ( 25 | r_object_namespace text, 26 | r_object text, 27 | r_relation text, 28 | r_subject text) 29 | LANGUAGE plpgsql 30 | AS $$ 31 | DECLARE 32 | var_r record; 33 | BEGIN 34 | FOR var_r IN ( 35 | SELECT 36 | object, 37 | object_namespace, 38 | relation, 39 | subject, 40 | subject_namespace, 41 | subject_relation 42 | FROM 43 | tuples 44 | WHERE 45 | object_namespace = p_object_namespace 46 | AND object = p_object 47 | AND relation = p_relation 48 | ORDER BY 49 | subject_relation NULLS FIRST) 50 | LOOP 51 | IF array_position(p_seen, var_r.subject) IS NULL THEN 52 | p_seen := array_append(p_seen, var_r.subject); 53 | IF var_r.subject_namespace IS NULL AND var_r.subject_relation IS NULL THEN 54 | r_object_namespace := var_r.object_namespace; 55 | r_object := var_r.object; 56 | r_relation := var_r.relation; 57 | r_subject := var_r.subject; 58 | RETURN NEXT; 59 | ELSE 60 | RETURN QUERY 61 | SELECT * FROM zanzibar_expand (var_r.subject_relation, var_r.subject_namespace, var_r.subject, p_seen); 62 | END IF; 63 | END IF; 64 | END LOOP; 65 | END; 66 | $$; 67 | 68 | CREATE OR REPLACE FUNCTION zanzibar_check (p_subject text, p_relation text, p_object_namespace text, p_object text) 69 | RETURNS boolean 70 | LANGUAGE plpgsql 71 | AS $$ 72 | DECLARE 73 | var_r record; 74 | var_b boolean; 75 | BEGIN 76 | FOR var_r IN ( 77 | SELECT 78 | object, 79 | object_namespace, 80 | relation, 81 | subject, 82 | subject_namespace, 83 | subject_relation 84 | FROM 85 | tuples 86 | WHERE 87 | object_namespace = p_object_namespace 88 | AND object = p_object 89 | AND relation = p_relation 90 | ORDER BY 91 | subject_relation NULLS FIRST) 92 | LOOP 93 | IF var_r.subject = p_subject THEN 94 | RETURN TRUE; 95 | END IF; 96 | IF var_r.subject_namespace IS NOT NULL AND var_r.subject_relation IS NOT NULL THEN 97 | EXECUTE 'SELECT zanzibar_check($1, $2, $3, $4)' 98 | USING p_subject, var_r.subject_relation, var_r.subject_namespace, var_r.subject INTO var_b; 99 | IF var_b = TRUE THEN 100 | RETURN TRUE; 101 | END IF; 102 | END IF; 103 | END LOOP; 104 | RETURN FALSE; 105 | END; 106 | $$; 107 | 108 | CREATE OR REPLACE FUNCTION zanzibar_enumerate (p_subject text, p_relation text, p_object_namespace text, p_seen text[] DEFAULT '{}' ::text[]) 109 | RETURNS TABLE ( 110 | r_object_namespace text, 111 | r_object text, 112 | r_relation text, 113 | r_subject text) 114 | LANGUAGE plpgsql 115 | AS $$ 116 | DECLARE 117 | rel text; 118 | var_r record; 119 | BEGIN 120 | FOR var_r IN ( 121 | SELECT 122 | object, 123 | object_namespace, 124 | relation, 125 | subject, 126 | subject_namespace, 127 | subject_relation 128 | FROM 129 | tuples 130 | WHERE 131 | subject = p_subject 132 | ORDER BY 133 | subject_relation NULLS FIRST) 134 | LOOP 135 | rel := var_r.object || '#' || var_r.relation; 136 | IF array_position(p_seen, rel) IS NULL THEN 137 | p_seen := array_append(p_seen, rel); 138 | IF var_r.object_namespace = p_object_namespace AND var_r.relation = p_relation THEN 139 | r_object_namespace := var_r.object_namespace; 140 | r_object := var_r.object; 141 | r_relation := var_r.relation; 142 | r_subject := var_r.subject; 143 | RETURN NEXT; 144 | ELSE 145 | RETURN QUERY SELECT * FROM zanzibar_enumerate (var_r.object, p_relation, p_object_namespace, p_seen); 146 | END IF; 147 | END IF; 148 | END LOOP; 149 | END; 150 | $$; 151 | 152 | SELECT zanzibar_expand('view', 'videos', '/cats/1.mp4'); 153 | SELECT zanzibar_check('*', 'view', 'videos', '/cats/1.mp4'); 154 | SELECT zanzibar_check('*', 'view', 'videos', '/cats/2.mp4'); 155 | SELECT zanzibar_check('cat lady', 'view', 'videos', '/cats/2.mp4'); 156 | SELECT zanzibar_enumerate('cat lady', 'owner', 'videos'); 157 | --------------------------------------------------------------------------------