From d21120962dea55af55fb97ed985e0f193644d4d5 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Thu, 3 Dec 2015 21:58:55 -0800 Subject: [PATCH] Blog example --- docs/examples/blog.md | 163 ++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + schema-templates/blog.sql | 6 +- 3 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 docs/examples/blog.md diff --git a/docs/examples/blog.md b/docs/examples/blog.md new file mode 100644 index 000000000..54a4ed233 --- /dev/null +++ b/docs/examples/blog.md @@ -0,0 +1,163 @@ +## Multi-Tenant Blog + +In our blog app there will be anonymous users and authors. Each +author can create and edit their own posts, and read (but not edit) +the posts of other authors. Anonymous users cannot edit anything +but can sign up for author accounts. Authors can also post comments +on articles. + +This example builds off the previous one. We had previously created +a signup and login system on top of JWT. We'll use this auth system +for the blog. **Run the SQL in the previous example** first, before +continuing with this example. + +For your convenience, the complete sql for the blog demo is +[here](https://github.com/begriffs/postgrest/blob/master/schema-templates/blog.sql). +You can try it out in this [vagrant +image](https://github.com/ruslantalpa/blogdemo) as well. + +### Adding Blog-Specific Tables + +Storing the posts and comments is this simple. The comments do not +form a tree, they are linear under a post. + +```sql +create table if not exists +posts ( + id bigserial primary key, + title text not null, + body text not null, + author text not null references basic_auth.users (email) + on delete restrict on update cascade + default basic_auth.current_email(), + created_at timestamptz not null default current_date +); + +create table if not exists +comments ( + id bigserial primary key, + body text not null, + author text not null references basic_auth.users (email) + on delete restrict on update cascade + default basic_auth.current_email(), + post bigint not null references posts (id) + on delete cascade on update cascade, + created_at timestamptz not null default current_date +); +``` + +### Permissions + +Basic table-level permissions. We'll add an the `authenticator` +role which can't do anything itself other than switch into other +roles as directed by JWT. + +```sql +create role anon; +create role author; +create role authenticator noinherit; +grant anon, author to authenticator; + +grant usage on schema public, basic_auth to anon, author; + +-- anon can create new logins and can read comments/posts +grant insert on table basic_auth.users, basic_auth.tokens to anon; +grant select on table pg_authid, basic_auth.users, posts, comments to anon; +grant execute on function + login(text,text), + request_password_reset(text), + reset_password(text,uuid,text), + signup(text, text) + to anon; + +-- authors can edit comments/posts +grant select, insert, update, delete + on basic_auth.tokens, basic_auth.users to anon, author; +grant select, insert, update, delete + on table users, posts, comments to author; +grant usage, select on sequence posts_id_seq, comments_id_seq to author; +``` + +To ensure that authors cannot edit each others' posts and comments +we'll use [row-level +security](http://www.postgresql.org/docs/9.5/static/ddl-rowsecurity.html). +Note that it requires PostgreSQL 9.5 or later. + +```sql +ALTER TABLE posts ENABLE ROW LEVEL SECURITY; +drop policy if exists authors_eigenedit on posts; +create policy authors_eigenedit on posts + using (true) + with check ( + author = basic_auth.current_email() + ); + +ALTER TABLE comments ENABLE ROW LEVEL SECURITY; +drop policy if exists authors_eigenedit on comments; +create policy authors_eigenedit on comments + using (true) + with check ( + author = basic_auth.current_email() + ); +``` + +Finally we need to modify the `users` view from the previous example. +This is because all authors share a single db role. We could have +chosen to assign a new role for every author (all inheriting from +`author`) but we choose to tell them apart by their email addresses. +The addition below prevents authors from seeing each others' info +in the `users` view. + + +```diff + create or replace view users as + select actual.role as role, + '***'::text as pass, + actual.email as email, + actual.verified as verified + from basic_auth.users as actual, + (select rolname + from pg_authid + where pg_has_role(current_user, oid, 'member') + ) as member_of + where actual.role = member_of.rolname ++ and ( ++ actual.role <> 'author' ++ or email = basic_auth.current_email() ++ ); +``` + +### Example client queries + +* Top ten most recent posts + +```HTTP + GET /posts?order=created_at.desc + Range: 0-9 +``` + +* Single post (randomly chose id=1) with its comments + +```HTTP + GET /posts?id=eq.1&select=*,comments{*} +``` + +* Add a new post + +```HTTP + POST /posts + Authorization: Bearer [JWT TOKEN] + + { + "title": "My first post", + "body": "Meh, forgot what I wanted to say." + } +``` + +### Conclusion + +VoilĂ , a blog API. Most of the code ended up being for defining +security. Once you have set up an authentication system, the code +to do application specific things like blog posts and comments is +short. All the front-end routes and verbs are created automatically +for you. diff --git a/mkdocs.yml b/mkdocs.yml index 1778a5d66..1957209d3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,3 +23,4 @@ pages: - Examples: - Getting Started: examples/start.md - User Management: examples/users.md + - Multi-Tenant Blog: examples/blog.md diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index c001786a0..05adffc6e 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -325,7 +325,8 @@ posts ( title text not null, body text not null, author text not null references basic_auth.users (email) - on delete restrict on update cascade, + on delete restrict on update cascade + default basic_auth.current_email(), created_at timestamptz not null default current_date ); @@ -334,7 +335,8 @@ comments ( id bigserial primary key, body text not null, author text not null references basic_auth.users (email) - on delete restrict on update cascade, + on delete restrict on update cascade + default basic_auth.current_email(), post bigint not null references posts (id) on delete cascade on update cascade, created_at timestamptz not null default current_date