Use PostgREST as an authorization service
Using postgrest as an authorization service.
Authorization is a very domain-dependent topic, and thus it is harder to
standardize (than e.g. Authn, where solutions like OAuth2 exist) IMO. Given a
more specific context, however, it is rather straightforward to build a
service that can be used within an internal environment.
This project introduces a solution that is to be used internally as a RESTful
service and queried by other internal (micro-)services. It allows other services
to store, manage, and query authorization models like users, resources, and
access lists (ACLs). Since most of these operations are purely CRUDs, there is
no point of writing repetitive HTTP routes, JSON handling logics, or model
mapping logics from/to backend database - the right tool is to use
PostgREST and allow your Postgres skills to do the rest.
Given that, you can build an authz service with just your SQL model, and no
other coding required. It is also flexible enough so that you can change your
schema, logic, etc. within minimal time.
TLDR; all you need is to write the create-model.sql
, and then you’ll
get an up-and-running authorization service within a minute!
In this example, I used a very simple model:
Notice that resources do not have this type of hierarchy, but as mentioned above,
you can easily adjust it to your needs.
If you wonder how the recursive ACL checking is done, there is a very powerful
language struct in Postgres called recursive common table expressions,
with which you can do recursive queries and union their results together.
This can save you a lot of coding (when done in non-pg languages e.g. Java or
Go), and also a lot of roundtrip queries. With proper index setup, this can also
be quite fast. Below is the query plan for the recursive CTE used in this example:
explain with recursive all_subordinates(id) as (
select id from users where id = 1
union
select u.id
from all_subordinates as c inner join users as u on u.supervisor_id = c.id
)
select a.access_type
from all_subordinates as u
inner join access_lists as a on a.user_id = u.id
inner join resources as r on a.resource_id = r.id
where r.id = 2;
Nested Loop (cost=273.90..293.68 rows=17 width=4)
CTE all_subordinates
-> Recursive Union (cost=0.15..259.31 rows=481 width=4)
-> Index Only Scan using users_pkey on users (cost=0.15..8.17 rows=1 width=4)
Index Cond: (id = 1)
-> Hash Join (cost=0.33..24.15 rows=48 width=4)
Hash Cond: (u_1.supervisor_id = c.id)
-> Seq Scan on users u_1 (cost=0.00..19.70 rows=970 width=8)
-> Hash (cost=0.20..0.20 rows=10 width=4)
-> WorkTable Scan on all_subordinates c (cost=0.00..0.20 rows=10 width=4)
-> Index Only Scan using resources_pkey on resources r (cost=0.15..8.17 rows=1 width=4)
Index Cond: (id = 2)
-> Hash Join (cost=14.44..26.03 rows=17 width=8)
Hash Cond: (u.id = a.user_id)
-> CTE Scan on all_subordinates u (cost=0.00..9.62 rows=481 width=4)
-> Hash (cost=14.35..14.35 rows=7 width=12)
-> Bitmap Heap Scan on access_lists a (cost=4.21..14.35 rows=7 width=12)
Recheck Cond: (resource_id = 2)
-> Bitmap Index Scan on resource_id_idx (cost=0.00..4.21 rows=7 width=0)
Index Cond: (resource_id = 2)
You can checkout test/index.js
as a demo for how the APIs will look like. It
is written in Node.js and if you want to run it yourself, make sure you do the
following to setup environment first.
3000
and 8080
ports aren’t taken, otherwise changedocker-compose.yml
docker-compose up
and wait for connection successful log, so that youPostgREST
has successfully connected to the databaseyarn
to install dependencies for testsyarn test
to run the testsIf you run the tests again you will see some errors, because the model ids
conflict. This is the expected behavior, and you can free to change tests and
apply some deletion logic as well.
As mentioned in the docs of PostgREST
you can also use swagger to see the API
documentation. If you run the docker-compose
a swagger UI server is already
available at http://localhost:8080
, make sure you change the docs address tohttp://localhost:3000
in order to see the PostgREST
exposed APIs.