Move docs out of this repo

They live in a separate repo now
This commit is contained in:
Joe Nelson
2016-06-24 13:05:27 -07:00
parent acd75a3997
commit 99d9984111
21 changed files with 3 additions and 2481 deletions
-1
View File
@@ -1 +0,0 @@
postgrest.com
+3
View File
@@ -0,0 +1,3 @@
The docs have moved to their own repo:
[begriffs/postgrest-docs](https://github.com/begriffs/postgrest-docs)
-9
View File
@@ -1,9 +0,0 @@
## Deployment
### Heroku
#### Getting Started
#### Using Amazon RDS
### Debian
-9
View File
@@ -1,9 +0,0 @@
## Data Migration
### Sqitch
### Test-Driven Migrations
#### Structural Tests
#### Value Tests with pgTAP
-9
View File
@@ -1,9 +0,0 @@
## Performance
### Benchmarks
### Caching
### Quality of Service
### Tips
-82
View File
@@ -1,82 +0,0 @@
## Security
PostgREST is designed to keep the database at the center of API
security. All authorization happens through database roles and
permissions. It is PostgREST's job to *authenticate* requests --
i.e. verify that a client is who they say they are -- and then let
the database *authorize* client actions.
We use [JSON Web Tokens](http://jwt.io/) to authenticate API requests.
As you'll recall a JWT contains a list of cryptographically signed
claims. PostgREST cares specifically about a claim called `role`.
When request contains a valid JWT with a role claim PostgREST will
switch to the database role with that name for the duration of the
HTTP request. If the client included no (or an invalid) JWT then
PostgREST selects the "anonymous role" which is specified by a
command line arguments to the server on startup.
```js
{
"role": "jdoe123"
}
// Encoded as JWT with a secret of "secret" this becomes
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZTEyMyJ9.X_ZeWSS9qsKDCDczv8C-GE2fccrPQjOh_ALMZJa5jsU
```
Using JWT allows us to authenticate with external services. A login
service needs merely to share a JWT encryption secret with the
PostgREST server. The secret is also a server command line option.
It is even possible to generate JWT from inside a stored procedure
in your database. Any SQL stored procedure that returns a type whose
name ends in `jwt_claims` will have its return value encoded into
JWT. See the [User Management](http://postgrest.com/examples/users/)
example for details.
### Database Roles
Suppose you start the server like this:
```bash
postgrest postgres://foo@localhost:5432/mydb --anonymous anon
```
This means that `foo` is the so-called *authenticator role* and
`anon` is the anonymous role. When a new HTTP request arrives at the
server the latter is connected to the database as user `foo`. If
no JWT is present, or if it is invalid, or if it does not contain
the role claim then the server changes to the anonymous role with
the query
```sql
SET LOCAL ROLE anon;
```
Otherwise it sets the role to that specified by JWT. For security
your authenticator role should have access to nothing except the
ability to become other users. Supposing you have three roles, one
for anonymous users, one for authors, and another for the authenticator,
you would set it up like this
```sql
CREATE ROLE authenticator NOINHERIT LOGIN;
CREATE ROLE anon;
CREATE ROLE author;
GRANT anon, author TO authenticator;
```
### Row-Level Security
#### Simulated - PostgreSQL <9.5
#### Real - PostgreSQL >=9.5
### Building Auth on top of JWT
#### Basic Auth
#### Github Sign-in
### SSL
-9
View File
@@ -1,9 +0,0 @@
## API Versioning
### Schema Search Path
### Changing a Resource
### Removing a Resource
### Avoiding DB and Client Coupling
-390
View File
@@ -1,390 +0,0 @@
## Requesting Information
### Tables and Views
* ✅ Cacheable, prefetchable
* ✅ Idempotent
The list of accessible tables and views is provided at
```HTTP
GET /
```
Every view and table accessible by the active db role is exposed
in a one-level deep route. For instance the full contents of a table
`people` is returned at
```HTTP
GET /people
```
There are no `deeply/nested/routes`. Each route provides `OPTIONS`,
`GET`, `POST`, `PATCH`, and `DELETE` verbs depending entirely
on database permissions.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>Why not provide nested routes? Many APIs allow nesting to
retrieve related information, such as <code>/films/1/director</code>.
We offer a more flexible mechanism (inspired by GraphQL) to embed
related information. It can handle one-to-many and many-to-many
relationships. This is covered in the section about Embedding.</p>
</div>
### Stored Procedures
* ❌ Cannot necessarily be cached or prefetched
* ❌ Not necessarily idempotent
Every stored procedure is accessible under the `/rpc` prefix. The
API endpoint supports only POST which executes the function.
```HTTP
POST /rpc/proc_name
```
PostgREST supports calling procedures with [named
arguments](http://www.postgresql.org/docs/9.4/static/sql-syntax-calling-funcs.html#SQL-SYNTAX-CALLING-FUNCS-NAMED).
Include a JSON object in the request payload and each
key/value of the object will become an argument.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>Why the /rpc prefix? One reason is to avoid name collisions
between views and procedures. It also helps emphasize to API
consumers that these functions are not normal restful things.
The functions can have arbitrary and surprising behavior, not
the standard "post creates a resource" thing that users expect
from the other routes.</p>
<p>We considered allowing GET requests for functions that are
marked non-volatile but could not reconcile how to pass in
parameters. Query string arguments are reserved for shaping/filtering
the output, not providing input.</p>
</div>
### Filtering
#### Filtering Rows
You can filter result rows by adding conditions on columns, each
condition a query string parameter. For instance, to return people
aged under 13 years old:
```HTTP
GET /people?age=lt.13
```
Adding multiple parameters conjoins the conditions:
```HTTP
GET /people?age=gte.18&student=is.true
```
These operators are available:
abbreviation | meaning
------------ | -------
eq | equals
gte | greater than or equal
gt | greater than
lte | less than or equal
lt | less than
neq | not equal
like | LIKE operator (use * in place of %)
ilike | ILIKE operator (use * in place of %)
in | one of a list of values e.g. `?a=in.1,2,3`
notin | not one of a list of values e.g. `?a=notin.1,2,3`
is | checking for exact equality (null,true,false)
isnot | checking for exact inequality (null,true,false)
@@ | full-text search using to_tsquery
@> | contains e.g. `?tags=@>.{example, new}`
<@ | contained in e.g. `values=<@{1,2,3}`
not | negates another operator, see below
To negate any operator, prefix it with `not` like `?a=not.eq.2`.
For more complicated filters (such as those involving condition 1
*OR* condition 2) you will have to create a new view in the database.
Filters may be applied to [computed
columns](http://www.postgresql.org/docs/current/interactive/xfunc-sql.html#XFUNC-SQL-COMPOSITE-FUNCTIONS)
as well as actual table/view columns, even though the computed
columns will not appear in the output.
#### Filtering Columns
You can customize which columns are returned by using the `select`
parameter:
```HTTP
GET /people?select=age,height,weight
```
To cast the column types, add a double colon
```HTTP
GET /people?select=age::text,height,weight
```
Not all type coercions are possible, and you will get an error
describing any problems from selection or type casting.
The `select` keyword is reserved. You thus cannot filter rows based
on a column named select. Then again it is a reserved SQL keyword
too, hence an unlikely column name.
#### Inside JSONB
PostgreSQL >=9.4.2 supports native JSON columns and can even index
them by internal keys using the `jsonb` column type. PostgREST
allows you to filter results by internal JSON object values. Use
the single- and double-arrows to path into and obtain values, e.g.
```HTTP
GET /stuff?json_col->a->>b=eq.2
```
This query finds rows in `stuff` where `json_col->'a'->>'b'` is
equal to 2 (or "2" -- it coerces as needed). The final arrow must
be the double kind, `->>`, or else PostgREST will not attempt to
look inside the JSON.
### Ordering
The reserved word `order` reorders the response rows. It uses a
comma-separated list of columns and directions:
```HTTP
GET /people?order=age.desc,height.asc
```
If no direction is specified it defaults to ascending order:
```HTTP
GET /people?order=age
```
If you care where nulls are sorted, add `nullsfirst` or `nullslast`:
```HTTP
GET /people?order=age.nullsfirst
GET /people?order=age.desc.nullslast
```
To order the embedded items, you need to specify the tree path for the order param like so.
```HTTP
GET /projects?select=id,name,tasks{id,name}&order=id.asc&tasks.order=name.asc
```
You can also use [computed
columns](http://www.postgresql.org/docs/current/interactive/xfunc-sql.html#XFUNC-SQL-COMPOSITE-FUNCTIONS)
to order the results, even though the computed
columns will not appear in the output.
### Limiting and Pagination
#### Pagination by Limit-Offset
PostgREST uses HTTP range headers for limiting and describing the
size of results. Every response contains the current range and total
results:
```
Range-Unit: items
Content-Range → 0-14/15
```
This means items zero through fourteen are returned out of a total
of fifteen -- i.e. all of them. This information is available in
every response and can help you render pagination controls on the
client. This is a RFC7233-compliant solution that keeps the response
JSON cleaner.
The client can set the limit and offset of a request by setting the
`Range` header. Translate the limit and offset into a range. To
request the first five elements, include these request headers:
```
Range-Unit: items
Range: 0-4
```
You can also use open-ended ranges for an offset with no limit:
`Range: 10-`.
In addition to the `Range` header, you can use `&limit` and `&offset` parameters
to achieve the same result.
You can also set a limit (but not offset) for the embedded items like so
```HTTP
/posts?select=id,title,body,comments{id,email,body}&limit=10&comments.limit=3
```
The above request will return the first 10 posts and for each of the posts, 3 comments at most
#### Suppressing Counts
Sometimes knowing the total row count of a query is unnecessary and
only adds extra cost to the database query. So you can skip the
count total using a ```Prefer``` header as:
```
Prefer: count=none
```
With count suppressed the PostgREST response will look like:
```
Range-Unit: items
Content-Range → 0-14/*
```
### Embedding Foreign Entities
To help you make fewer requests, PostgREST allows the embedding of
traditional SQL relationships into a response. Suppose you have a
`projects` table which references `clients` through a foreign key
called `client_id`. When listing projects through the API you can
have it embed the client within each project response. For example,
```HTTP
GET /projects?id=eq.1&select=id, name, clients{*}
```
Notice this is the same `select` keyword which is used to choose
which columns to include. When a column name is followed by parentheses
that means to fetch the entire record and nest it. You include a
list of columns inside the parens, or asterisk to request all
columns.
The embedding works for 1-N, N-1, and N-N relationships. That means
you could also ask for a client and all their projects:
```HTTP
GET /clients?id=eq.42&select=id, name, projects{*}
```
In the examples above we asked for all columns in the embedded resource
but the the select query is recursive. You could for instance specify
```HTTP
GET /foo?select=x, y, bar{z, w, baz{*}}
```
You can select not only using table names, but also foreign key column names!
This is especially needed when you have a table with two foreign keys pointing to the same table, for example billing_address_id and shipping_address_id.
To embed the same foreign key row from our client example earlier
you could do the following:
```HTTP
GET /projects?id=eq.1&select=id, name, client_id{*}
```
In the response there will be a `client_id` object containing all
the data for that row.
However, a `client_id` object doesn't make a lot of sense, so you
could do one of two things. Tell PostgREST that you want the key renamed by using the `alias` feature like so `client:client_id{*}`, or just try `client{*}`
in the select parameter! PostgREST supports smart ducktype checking
for common foreign key names, so if your column name ends with
`_id`, `_fk`, or any variation of the two (including camelcase)
you can embed a row with just the name's beginning.
So for a complete example:
```HTTP
GET /projects?id=eq.1&select=id, name, client{*}
```
Would embed in the `client` key the row referenced with `client_id`.
The `alias` feature works for embedded entities and also for regular columns. This is useful in situations where for example you use different naming conventions in the database and frontend.
The following request will produce the output below:
```HTTP
GET /orders?id=eq.1&select=orderId:id, customer:customer_id{customerId:id, customerName:name}
```
```json
[
{
"orderId": 1,
"customer": {
"customerId": 1,
"customerName": "John Smith"
}
}
]
```
If you want to apply filters to the embedded items, you can do that like so:
```HTTP
GET /clients?id=eq.42&select=id,name,projects{id,name,is_active}&projects.is_active=eq.true
```
The above request will return the client with id=42 and all the projects for that client that are still active
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>In order for this feature to work as expected after a schema change, PostgREST currently requires to be restarted.</p>
</div>
### Response Format
Query responses default to JSON but you can get them in CSV as well. Just make your request with the header
```HTTP
Accept: text/csv
```
### Singular vs Plural
Many APIs distinguish plural and singular resources, e.g.`/stories`
vs `/stories/1`. Why do we use `/stories?id=eq.1`? It is because a
single resource is for us a row determined by a primary key, and
primary keys can be *compound* (meaning defined across more than
one column). The common urls come from a degenerate case of simple
(and overwhelmingly numeric) primary keys often introduced automatically
be Object Relational Mapping.
For consistency's sake all these endpoints return a JSON array,
`/stories`, `/stories?genre=eq.mystery`, `/stories?id=eq.1`. They
are all filtering a bigger array. However you might want the
last one to return a single JSON object, not an array with one
element. To request a singular response send the header
`Prefer: plurality=singular`.
### Data Schema
As well as issuing a `GET /` to obtain a list of the tables, views,
and stored procedures available, you can get more information about
any particular endpoint.
```HTTP
OPTIONS /my_view
```
This will include the row names, their types, primary key
information, and foreign keys for the given table or view.
<div class="admonition warning">
<p class="admonition-title">Schema Changes</p>
<p>Note that when the schema of your database changes PostgREST will not reflect
the change. You have to either restart PostgREST or send its running process
a HUP signal:
<pre><code>killall -HUP postgrest</code></pre>
</div>
### CORS
PostgREST sets highly permissive cross origin resource sharing. It
accepts Ajax requests from any domain.
-210
View File
@@ -1,210 +0,0 @@
## Updating Data
### Record Creation
* ❌ Cannot be cached or prefetched
* ❌ Not idempotent
To create a row in a database table post a JSON object whose keys
are the names of the columns you would like to create. Missing keys
will be set to default values when applicable.
```HTTP
POST /table_name
{ "col1": "value1", "col2": "value2" }
```
The response will include a `Location` header describing where to
find the new object. If you would like to get the full object back
in the response to your request, include the header `Prefer:
return=representation`. That way you won't have to make another
HTTP call to discover properties that may have been filled in on
the server side.
### Bulk Insertion
* ❌ Cannot be cached or prefetched
* ❌ Not idempotent
You can POST a JSON array or CSV to insert multiple rows in a single
HTTP request. Note that using CSV requires less parsing on the server
and is **much faster**.
Example of CSV bulk insert. Simply post to a table route with
`Content-Type: text/csv` and include the names of the columns as
the first row. For instance
```HTTP
POST /people
name,age,height
J Doe,62,70
Jonas,10,55
```
An empty field (`,,`) is coerced to an empty string and the reserved
word `NULL` is mapped to the SQL null value. Note that there should
be no spaces between the column names and commas.
Example of JSON bulk insert. Send an array:
```HTTP
POST /people
[
{ "name": "J Doe", "age": 62, "height": 70 },
{ "name": "Janus", "age": 10, "height": 55 }
]
```
If you would like to get the full object back in the response to
your request, include the header `Prefer: return=representation`.
Chances are you only want certain information back, though, like
created ids. You can pass a `select` parameter to affect the shape
of the response (further documented in the [reading](/api/reading/)
page). For instance
```HTTP
POST /people?select=id
[...]
```
returns something like
```json
[ { "id": 1 }, { "id": 2 } ]
```
### Multiple Tables Insertion or Update
The cleanest way to insert or update data into multiple tables using only one POST/PATCH request
is to create a view that will join all target tables and present a single endpoint.
In our example let's assume one users table and one companies table.
In this case, we want a signup endpoint to create the first user within a company.
And for this endpoint we want to insert with one request both user and company.
```SQL
CREATE TABLE companies (
id serial primary key,
name text unique
);
CREATE TABLE users (
id serial primary key,
name text not null,
pass text,
company_id integer not null references companies
);
```
Having both tables created we create a view that joins them to be used
as a ```/signup``` endpoint.
```SQL
CREATE VIEW signup AS
SELECT
c.name AS company_name,
u.name AS user_name,
u.pass
FROM
public.users u
JOIN public.companies c ON c.id = u.company_id;
```
After the signup view creation, we can issue ```GET``` requests to read data
from users and companies, but any atempt to ```POST``` or ```PATCH``` data will fail.
PostgreSQL won't allow any data change on views that have a ```JOIN```
clause in their ```FROM``` without a proper ```INSTEAD OF``` trigger.
So in the example bellow we create a trigger to allow insertion of data in the signup view.
The trigger is a simple PL/pgSQL function that first inserts into the companies table and
uses the newly create company_id to create its first user.
```SQL
CREATE FUNCTION signup()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
vcompany_id int;
BEGIN
INSERT INTO companies (name) VALUES (new.company_name) RETURNING id INTO vcompany_id;
INSERT INTO users (name, pass, company_id) VALUES (new.user_name, new.pass, vcompany_id);
RETURN new;
END;
$$;
CREATE TRIGGER signup
INSTEAD OF INSERT ON signup
FOR EACH ROW
EXECUTE PROCEDURE signup();
```
After the trigger creation we can issue a normal ```POST``` request to our signup endpoint:
```HTTP
POST /signup
{ "company_name": "foo", "user_name": "bar" }
```
For an endpoint such as signup its usually not desirable to have a ```PATCH``` route for updates,
and we will skip this example for the sake of brevity. But it would be implemented in a very
similar way to our ```POST``` example.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>It's advisable to create a separate trigger for <code>UPDATE</code> and <code>INSERT</code>
avoiding conditionals that decide which is the trigger current operation.
This makes it easier to change code for (or even disable) one operation without interfering with others while
improving readability.
</p>
</div>
### Bulk Updates
* ❌ Cannot be cached or prefetched
* ❌ Not idempotent
To change parts of a resource or resources use the `PATCH` verb.
For instance, here is how to mark all young people as children.
```HTTP
PATCH /people?age=lt.13
{
"person_type": "child"
}
```
This affects any rows matched by the url param filters, overwrites
any fields specified in in the payload JSON and leaves the other
fields unaffected. Note that although the payload is not in the
JSON patch format specified by
[RFC6902](https://tools.ietf.org/html/rfc6902), HTTP does not specify
which patch format to use. Our format is more pleasant, meant for
basic field replacements, and not at all "incorrect."
### Deletion
* ❌ Cannot be cached or prefetched
* ✅ Idempotent
Simply use the `DELETE` verb. All records that match your filter
will be removed. For instance deleting inactive users:
```HTTP
DELETE /user?active=is.false
```
### Protecting Dangerous Actions
Notice that it is very easy to delete or update many records at
once. In fact forgetting a filter will affect an entire table!
<div class="admonition warning">
<p class="admonition-title">Invitation to Contribute</p>
<p>We would like to investigate nginx rules to guard dangerous
actions, perhaps requiring a confirmation header or query param
to perform the action.</p>
<p>You're invited to research this option and contribute to
this documentation.</p>
</div>
-181
View File
@@ -1,181 +0,0 @@
## 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 previous [User Management](users/)
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
On top of the `authenticator` and `anon` access granted in the
previous example, blogs have an `author` role with extra permissions.
```sql
create role author;
grant author to authenticator;
grant usage on schema public, basic_auth to author;
-- authors can edit comments/posts
grant select, insert, update, delete
on basic_auth.tokens, basic_auth.users to 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
grant select on posts, comments to anon;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
drop policy if exists posts_select_unsecure on posts;
create policy posts_select_unsecure on posts for select
using (true);
drop policy if exists comments_select_unsecure on comments;
create policy comments_select_unsecure on comments for select
using (true);
drop policy if exists authors_eigencreate on posts;
create policy authors_eigencreate on posts for insert
with check (
author = basic_auth.current_email()
);
drop policy if exists authors_eigencreate on comments;
create policy authors_eigencreate on comments for insert
with check (
author = basic_auth.current_email()
);
drop policy if exists authors_eigenedit on posts;
create policy authors_eigenedit on posts for update
using (author = basic_auth.current_email())
with check (
author = basic_auth.current_email()
);
drop policy if exists authors_eigenedit on comments;
create policy authors_eigenedit on comments for update
using (author = basic_auth.current_email())
with check (
author = basic_auth.current_email()
);
drop policy if exists authors_eigendelete on posts;
create policy authors_eigendelete on posts for delete
using (author = basic_auth.current_email());
drop policy if exists authors_eigendelete on comments;
create policy authors_eigendelete on comments for delete
using (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.
-193
View File
@@ -1,193 +0,0 @@
## External Authentication
API clients authenticate with [JSON Web Tokens](http://jwt.io).
PostgREST does not support any other authentication mechanism
directly, but they can be built on top. In this demo we will build
a system that works with an external authentication server
and integrates with a PostgREST server by sharing the same JWT secret.
For a better understanding of JWT and PostgREST authentication system you should read
the [User Management](users/) example as well.
I'll use a [Rails](http://rubyonrails.org) application using [Devise](https://github.com/plataformatec/devise)
just to make the example more concrete, but this could be replicated for
any other external authentication system using the same principles.
In case Rails is not your cup of tea you can continue reading and
just skip the Ruby code samples. I'll also assume
the use of JQuery for some client-side code samples for the sake of simplicity.
I won't delve into Devise authentication details, for this would require a tutorial on its own,
so I'm assuming that the reader's authentication system is already working.
### Sharing the JWT Secret
Allowing a third party to generate valid JWTs for your PostgREST API
is just a matter of sharing a secret. So you need to give your authenticator
software the same secret that was used in your API server under the ```--jwt-secret```
parameter.
This could be done easly using environment variables. You set a ```JWT_SECRET``` variable
in the environment where you run your rails app and it will be accessible in the global
variable ```ENV['JWT_SECRET']```.
### User Model
We will map each user in this example to two database roles.
So our application users are either ```admin``` or ```customer```.
If they are just visitors (not logged in) to our website they will be ```anonymous```.
One way of mapping users is to add a field in our users table indicating their database role.
I'll add a text field called role to my users table:
```sql
ALTER TABLE users ADD role text NOT NULL DEFAULT 'customer';
```
Besides the main user that PostgREST uses to connect to PostgreSQL
and the anonymous user, we will need two additional roles for our example:
* admin - to be used by users that access all the system rows.
* customer - to be used when user has restricted access to database rows.
Bellow we have the commands to create all roles that will be used:
```sql
CREATE USER authenticator NOINHERIT;
CREATE ROLE anonymous;
CREATE ROLE admin;
CREATE ROLE customer;
GRANT customer, admin, anonymous TO authenticator;
```
### Generating a JWT
Several libraries are available to generate JWT, you will find a very handy list in [their website](http://jwt.io)
under **Libraries**.
To continue our Rails example I'll use the ruby library [json_web_token](https://github.com/garyf/json_web_token).
In order to make the gem available in my Rails project I add the following line to my Gemfile:
```
gem 'json_web_token'
```
Then we create a Rails controller to serve JWTs for my authenticated users.
For this I just open a file ```app/controllers/api_tokens_controller.rb``` with the content:
```ruby
class ApiTokensController < ApplicationController
TOKEN_TTL = 1.hour
def show
unless ENV['JWT_SECRET'].present?
return render json: {error: "you need to have JWT_SECRET configured to get an API token"}, status: 500
end
unless current_user.present?
return render json: {error: "only authenticated users can request the API token"}, status: 401
end
expires_in TOKEN_TTL, public: false
render json: {token: jwt}, status: 200
end
private
def jwt
JsonWebToken.sign(claims, key: ENV['JWT_SECRET'])
end
def claims
# This token will expire 1 hour after being issued
{
role: current_user.role,
user_id: current_user.id.to_s,
exp: (Time.now + TOKEN_TTL).to_i
}
end
end
```
<div class="admonition note">
<p class="admonition-title">Token Time to Live</p>
<p>
In the code above we leverage the HTTP time based cache headers to expire the
endpoint cache at the same time as the token. In this example we have a token
that will be refresh one hour after its issuing time.
That's why both are based on the <code>TOKEN_TTL</code> constant.
</p>
</div>
We also need to create a route in the ```config/routes.rb``` file:
```ruby
resource :api_token, only: [:show]
```
Now, any authenticated user in our rails application can request an api_token making a GET
request to ```/api_token```. This endpoint will return a json object with one property
whose value is the token the API requests should use.
### Orders Endpoint
Here is how to create a view to generate an endpoint ```/orders``` filtered by
the logged in user:
```sql
ALTER DATABASE mydb SET postgrest.claims.user_id TO '';
CREATE OR REPLACE FUNCTION current_user_id()
RETURNS integer
STABLE
LANGUAGE SQL
AS $$
SELECT nullif(current_setting('postgrest.claims.user_id'), '')::integer;
$$;
CREATE SCHEMA private;
CREATE TABLE private.orders (
id serial primary key,
user_id int references users,
created_at timestamp not null default current_timestamp,
updated_at timestamp not null default current_timestamp
);
CREATE VIEW orders AS
SELECT
id, user_id, created_at, updated_at
FROM
private.orders o
WHERE
current_user = 'admin' OR o.user_id = current_user_id();
```
<div class="admonition note">
<p class="admonition-title">DRY priviledge checking conditions</p>
<p>
You can encapsulate conditions that will be commonly used to check for privileges while reading a database row.
We used a function <code>current_user_id()</code> but we could add more conditions to functions
as the system becomes more complex.<br/>
Remeber to mark your functions as <code>STABLE</code> so that PostgreSQL can inline then while planning the query.
</p>
</div>
### Using the JWT
Now whenever you are authenticated in your Rails application you can use some Javascript
code to get the token and use it:
```javascript
$.getJSON('/api_token').done(function(data){
$.ajax('/orders', {'Authorization': 'Bearer ' + data.token}).done(function(data){
console.log('Visible Orders: ', data);
})
}).fail(function(){
console.log('Error fetching API token');
})
```
We could also store the token to avoid having to fetch it again in the same page.
### Conclusion
This section explained the implementation details for building an
external authentication system working with PostgREST.
With the previous [User Management](users/) example this should give a clearer
idea of how to set up authentication for your API.
-40
View File
@@ -1,40 +0,0 @@
## Python Client for PostgREST API
### Setup PostgreSQL
This code relies on setting up the PostgreSQL auth functions and grants correctly first. Follow [these instructions](http://postgrest.com/examples/users/).
After completing the PostgreSQL configuration, be sure to create a user with email, password, role, and verified flag. We'll use that user to login in the code below.
### Setup PostgREST
Next, setup PostgREST according to the documentation [http://postgrest.com/install/server/](here).
### Setup Python Client
Finally, we'll install and configure the python client. Follow the instructions in the [README](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/README.md). Be sure to set the [credentials](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/config.in#L3-L5) and [urls](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/config.in#L7-L9) in config.py.
### Python Client Functions
There are four primary functions to the python client:
* login
* construct_jwt_auth
* get_result_size
* get_range
The *login* and *construct_jwt_auth* functions will be required for any REST client using a PostgREST server, since a JWT auth instance is presumed.
The *get_result_size* and *get_range* functions are designed specifically for result sets where pagination is required. You can certainly use them for a single page result set that does not require pagination, but that may be overkill.
### Login
The [login function](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/client.py#L12-L17) takes email and password strings (credentials.email and credentials.password, respectively from the config.py) and return the response.
### Construct JWT Auth
The [construct_jwt_auth](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/client.py#L20-L23) function takes the auth response returned by the login function, retrieves the token in the response, and returns a JWT auth instance to the caller. The JWT auth instance can then be used for successive calls to the same PostgREST service.
### Get Result Size
The [get_result_size](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/client.py#L26-L30) function takes a JWT auth instance calls the URL at urls.data, extracts the size of the result set from the response object and returns the size.
### Get Range
The [get_range](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/client.py#L26-L30) function takes a beginning range, ending range, page size, and JWT auth instance, gets only that range of the available result set and returns JSON for that result set.
-509
View File
@@ -1,509 +0,0 @@
## Getting Started
### Your First (simple) API
Let's start with the simplest thing possible. We will expose some tables directly for reading and writing by anyone.
Start by making a database
```sh
createdb demo1
```
We'll set it up with a film example (courtesy of [Jonathan Harrington](http://blog.jonharrington.org/postgrest-introduction/)). Copy the following into your clipboard:
```sql
BEGIN;
CREATE TABLE director
(
name text NOT NULL PRIMARY KEY
);
CREATE TABLE film
(
id serial PRIMARY KEY,
title text NOT NULL,
year date NOT NULL,
director text REFERENCES director (name)
ON UPDATE CASCADE ON DELETE CASCADE,
rating real NOT NULL DEFAULT 0,
language text NOT NULL
);
CREATE TABLE festival
(
name text NOT NULL PRIMARY KEY
);
CREATE TABLE competition
(
id serial PRIMARY KEY,
name text NOT NULL,
festival text NOT NULL REFERENCES festival (name)
ON UPDATE CASCADE ON DELETE CASCADE,
year date NOT NULL
);
CREATE TABLE film_nomination
(
id serial PRIMARY KEY,
competition integer NOT NULL REFERENCES competition (id)
ON UPDATE NO ACTION ON DELETE NO ACTION,
film integer NOT NULL REFERENCES film (id)
ON UPDATE CASCADE ON DELETE CASCADE,
won boolean NOT NULL DEFAULT true
);
COMMIT;
```
Apply it to your new database by running
```sh
# On OS X
pbpaste | psql demo1
# Or Linux
# xclip -selection clipboard -o | psql demo1
```
Start the PostgREST server and point it at the new database. (See the [installation instructions](/install/server/).)
```sh
postgrest postgres://postgres:@localhost:5432/demo1 -a postgres --schema public
```
<div class="admonition note">
<p class="admonition-title">Note about database users</p>
<p>If you installed PostgreSQL with Homebrew on Mac then the
database username may be your own login rather than
<code>postgres</code>.</p>
</div>
### Populating Data
Let's use PostgREST to populate the database. Install a REST client such as [Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en). Now let's insert some data as a bulk post in CSV format:
```HTTP
POST http://localhost:3000/festival
Content-Type: text/csv
name
Venice Film Festival
Cannes Film Festival
```
In Postman it will look like this
![Festival bulk insert in postman](/img/post-festivals.png)
Notice that the post type is `raw` and that `Content-Type: text/csv` set in the Headers tab.
The server returns HTTP 201 Created. Because we inserted more than one item at once there is no `Location` header in the response. However sometimes you want to learn more about items which you just inserted. To have the server include the full results, include the header `Prefer: return=representation`.
At this point if you send a GET request to `/festival` it should return
```json
[
{
"name": "Venice Film Festival"
},
{
"name": "Cannes Film Festival"
}
]
```
Now that you've seen how to do a bulk insert, let's do some more and fully populate the database.
Post the following to `/competition`:
```csv
name,festival,year
Golden Lion,Venice Film Festival,2014-01-01
Palme d'Or,Cannes Film Festival,2014-01-01
```
Now `/director`:
```csv
name
Bertrand Bonello
Atom Egoyan
David Gordon Green
Andrey Konchalovskiy
Mario Martone
Mike Leigh
Roy Andersson
Saverio Costanzo
Alix Delaporte
Jean-Pierre Dardenne
Xiaoshuai Wang
Kaan Müjdeci
Tommy Lee Jones
Nuri Bilge Ceylan
Michel Hazanavicius
Xavier Dolan
Ramin Bahrani
Alice Rohrwacher
Andrew Niccol
Rakhshan Bani-Etemad
David Oelhoffen
Bennett Miller
David Cronenberg
Shin'ya Tsukamoto
Joshua Oppenheimer
Olivier Assayas
Jean-Luc Godard
Alejandro González Iñárritu
Benoît Jacquot
Fatih Akin
Francesco Munzi
Ken Loach
Abel Ferrara
Xavier Beauvois
Naomi Kawase
```
And `/film`:
```csv
title,year,director,rating,language
Chuang ru zhe,2014-01-01,Xiaoshuai Wang,6.19999981,english
The Look of Silence,2014-01-01,Joshua Oppenheimer,8.30000019,Indonesian
Fires on the Plain,2014-01-01,Shin'ya Tsukamoto,5.80000019,Japanese
Far from Men,2014-01-01,David Oelhoffen,7.5,english
Good Kill,2014-01-01,Andrew Niccol,6.0999999,english
Leopardi,2014-01-01,Mario Martone,6.9000001,english
Sivas,2014-01-01,Kaan Müjdeci,7.69999981,english
Black Souls,2014-01-01,Francesco Munzi,7.0999999,english
Three Hearts,2014-01-01,Benoît Jacquot,5.80000019,French
Pasolini,2014-01-01,Abel Ferrara,5.80000019,english
Le dernier coup de marteau,2014-01-01,Alix Delaporte,6.5,english
Manglehorn,2014-01-01,David Gordon Green,7.0999999,english
Hungry Hearts,2014-01-01,Saverio Costanzo,6.4000001,English
Belye nochi pochtalona Alekseya Tryapitsyna,2014-01-01,Andrey Konchalovskiy,6.9000001,Russian
99 Homes,2014-01-01,Ramin Bahrani,7.30000019,english
The Cut,2014-01-01,Fatih Akin,6,Armenian
Birdman: Or (The Unexpected Virtue of Ignorance),2014-01-01,Alejandro González Iñárritu,8,English
La rançon de la gloire,2014-01-01,Xavier Beauvois,5.69999981,French
A Pigeon Sat on a Branch Reflecting on Existence,2014-01-01,Roy Andersson,7.19999981,english
Tales,2014-01-01,Rakhshan Bani-Etemad,6.80000019,english
The Wonders,2014-01-01,Alice Rohrwacher,6.80000019,Italian
Foxcatcher,2014-01-01,Bennett Miller,7.19999981,English
Mr. Turner,2014-01-01,Mike Leigh,7,English
Jimmy's Hall,2014-01-01,Ken Loach,6.69999981,English
The Homesman,2014-01-01,Tommy Lee Jones,6.5999999,English
The Captive,2014-01-01,Atom Egoyan,5.9000001,english
Goodbye to Language,2014-01-01,Jean-Luc Godard,6.19999981,French
The Search,2014-01-01,Michel Hazanavicius,6.9000001,French
Still the Water,2014-01-01,Naomi Kawase,6.9000001,Japanese
Mommy,2014-01-01,Xavier Dolan,8.30000019,French
"Two Days, One Night",2014-01-01,Jean-Pierre Dardenne,7.4000001,French
Maps to the Stars,2014-01-01,David Cronenberg,6.4000001,English
Saint Laurent,2014-01-01,Bertrand Bonello,6.5,French
Clouds of Sils Maria,2014-01-01,Olivier Assayas,6.9000001,english
Winter Sleep,2014-01-01,Nuri Bilge Ceylan,8.5,Turkish
```
Finally `/film_nomination`:
```csv
competition,film,won
1,1,f
1,2,f
1,3,f
1,4,f
1,5,f
1,6,f
1,7,f
1,8,f
1,9,f
1,10,f
1,11,f
1,12,f
1,13,f
1,14,f
1,15,f
1,16,f
1,17,f
1,18,f
1,19,f
1,20,f
2,21,f
2,22,f
2,23,f
2,24,f
2,25,f
2,26,f
2,27,f
2,28,f
2,29,f
2,30,f
2,31,f
2,32,f
2,33,f
2,34,f
2,35,f
```
### Getting and Embedding Data
First let's review which films are stored in the database:
```http
GET http://localhost:3000/film
```
It gives us back a list of JSON objects. What if we care only about the film titles? Use `select` to shape the output:
```http
GET http://localhost:3000/film?select=title
```
```json
[
{
"title": "Chuang ru zhe"
},
{
"title": "The Look of Silence"
},
{
"title": "Fires on the Plain"
},
...
]
```
Here is where it gets cool. PostgREST can embed objects in its response through foreign key relationships. Earlier we created a join table called `film_nomination`. It joins films and competitions. We can ask the server about the structure of this table:
```
OPTIONS http://localhost:3000/film_nomination
```
```json
{
"pkey": [
"id"
],
"columns": [
{
"references": null,
"default": "nextval('film_nomination_id_seq'::regclass)",
"precision": 32,
"updatable": true,
"schema": "public",
"name": "id",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 1
},
{
"references": {
"schema": "public",
"column": "id",
"table": "competition"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "public",
"name": "competition",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 2
},
{
"references": {
"schema": "public",
"column": "id",
"table": "film"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "public",
"name": "film",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 3
},
{
"references": null,
"default": "true",
"precision": null,
"updatable": true,
"schema": "public",
"name": "won",
"type": "boolean",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 4
}
]
}
```
From this you can see that the columns `film` and `competition` reference their eponymous tables. Let's ask the server for each film along with names of the competitions it entered. You don't have to do any custom coding. Send this query:
```http
GET http://localhost:3000/film?select=title,competition{name}
```
```json
[
{
"title": "Chuang ru zhe",
"competition": [
{
"name": "Golden Lion"
}
]
},
{
"title": "The Look of Silence",
"competition": [
{
"name": "Golden Lion"
}
]
},
...
]
```
The relation flows both ways. Here is how to get the name of each competition's name and the movies shown at it.
```http
GET http://localhost:3000/competition?select=name,film{title}
```
```json
[
{
"name": "Golden Lion",
"film": [
{
"title": "Chuang ru zhe"
},
{
"title": "The Look of Silence"
},
...
]
},
{
"name": "Palme d'Or",
"film": [
{
"title": "The Wonders"
},
{
"title": "Foxcatcher"
},
...
]
}
]
```
Why not learn about the directors too? There is a many-to-one relation directly between films and directors. We can alter our previous query to include directors in its results.
```http
GET http://localhost:3000/competition?select=name,film{title,director{*}}
```
```json
[
{
"name": "Golden Lion",
"film": [
{
"title": "Manglehorn",
"director": {
"name": "David Gordon Green"
}
},
{
"title": "Belye nochi pochtalona Alekseya Tryapitsyna",
"director": {
"name": "Andrey Konchalovskiy"
}
},
...
]
},
...
]
```
### Singular Responses
How do we ask for a single film, for instance the second one we inserted?
```http
GET http://localhost:3000/film?id=eq.2
```
It returns
```json
[
{
"id": 2,
"title": "The Look of Silence",
"year": "2014-01-01",
"director": "Joshua Oppenheimer",
"rating": 8.3,
"language": "Indonesian"
}
]
```
Like any query, it gives us a result *set*, in this case an array with one element. However you and I know that `id` is a primary key, it will never return more than one result. We might want it returned as a JSON object, not an array. To express this preference include the header `Prefer: plurality=singular`. It will respond with
```json
{
"id": 2,
"title": "The Look of Silence",
"year": "2014-01-01",
"director": "Joshua Oppenheimer",
"rating": 8.3,
"language": "Indonesian"
}
```
<div class="admonition note">
<p class="admonition-title">Why this approach to singular responses?</p>
<p>
PostgREST knows which columns comprise a primary key for a
table, so why not automatically choose plurality=singular when
these column filters are present? The fact is it could come as a
shock to a client that by adding one more filter condition it can
change the entire response format.
</p>
<p>
Then why not expose another kind of route such as /film/2 to indicate
one particular film? Because this does not accommodate compound keys.
The convention complects a plurality preference with table key
assumptions. We should separate concerns.
</p>
<p>
It turns out you can still have routes like /film/2. Use a
proxy such as Nginx. It can rewrite routes such as /films/2
into /films?id=eq.2 and add the Prefer header to make the results
singular.
</p>
</div>
### Conclusion
This tutorial showed how to create a database with a basic schema, run PostgREST, and interact with the API. The next tutorial will show how to enable security for a multi-tenant blogging API.
-513
View File
@@ -1,513 +0,0 @@
## User Management
API clients authenticate with [JSON Web Tokens](http://jwt.io).
PostgREST does not support any other authentication mechanism
directly, but they can be built on top. In this demo we will build
a username and password system on top of JWT using only plpgsql.
Future examples such as the multi-tenant blogging platform will use
the results from this example for their auth. We will build a system
for users to sign up, log in, manage their accounts, and for admins
to manage other people's accounts. We will also see how to trigger
outside events like sending password reset emails.
Before jumping into the code, a little more about how the tokens
work. Every JWT contains cryptographically signed *claims*. PostgREST
cares specifically about a claim called `role`. When a client includes
a `role` claim PostgREST executes their request using that database
role.
How would a client include a role claim, or claims in general?
Without knowing the server JWT secret a client cannot create a
claim. The only place to get a JWT is from the PostgREST server or
from another service sharing the secret and acting on its behalf.
We'll use a stored procedure returning type `jwt_claims` which is
a special type causing the server to encrypt and sign the return
value.
### Storing Users and Passwords
We create a database schema especially for auth information. We'll
also need the postgres extension
[pgcrypto](http://www.postgresql.org/docs/current/static/pgcrypto.html).
```sql
create extension if not exists pgcrypto;
-- We put things inside the basic_auth schema to hide
-- them from public view. Certain public procs/views will
-- refer to helpers and tables inside.
create schema if not exists basic_auth;
```
Next a table to store the mapping from usernames and passwords to
database roles. The code below includes triggers and functions to
encrypt the password and ensure the role exists.
```sql
create table if not exists
basic_auth.users (
email text primary key check ( email ~* '^.+@.+\..+$' ),
pass text not null check (length(pass) < 512),
role name not null check (length(role) < 512),
verified boolean not null default false
-- If you like add more columns, or a json column
);
create or replace function
basic_auth.check_role_exists() returns trigger
language plpgsql
as $$
begin
if not exists (select 1 from pg_roles as r where r.rolname = new.role) then
raise foreign_key_violation using message =
'unknown database role: ' || new.role;
return null;
end if;
return new;
end
$$;
drop trigger if exists ensure_user_role_exists on basic_auth.users;
create constraint trigger ensure_user_role_exists
after insert or update on basic_auth.users
for each row
execute procedure basic_auth.check_role_exists();
create or replace function
basic_auth.encrypt_pass() returns trigger
language plpgsql
as $$
begin
if tg_op = 'INSERT' or new.pass <> old.pass then
new.pass = crypt(new.pass, gen_salt('bf'));
end if;
return new;
end
$$;
drop trigger if exists encrypt_pass on basic_auth.users;
create trigger encrypt_pass
before insert or update on basic_auth.users
for each row
execute procedure basic_auth.encrypt_pass();
```
With the table in place we can make a helper to check passwords.
It returns the database role for a user if the email and password
are correct.
```sql
create or replace function
basic_auth.user_role(email text, pass text) returns name
language plpgsql
as $$
begin
return (
select role from basic_auth.users
where users.email = user_role.email
and users.pass = crypt(user_role.pass, users.pass)
);
end;
$$;
```
### Password Reset
When a user requests a password reset or signs up we create a token
they will use later to prove their identity. The tokens go in this
table.
```sql
drop type if exists token_type_enum cascade;
create type token_type_enum as enum ('validation', 'reset');
create table if not exists
basic_auth.tokens (
token uuid primary key,
token_type token_type_enum not null,
email text not null references basic_auth.users (email)
on delete cascade on update cascade,
created_at timestamptz not null default current_date
);
```
In the main schema (as opposed to the `basic_auth` schema) we expose
a password reset request function. HTTP clients will call it. The
function takes the email address of the user.
```sql
create or replace function
request_password_reset(email text) returns void
language plpgsql
as $$
declare
tok uuid;
begin
delete from basic_auth.tokens
where token_type = 'reset'
and tokens.email = request_password_reset.email;
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'reset', request_password_reset.email);
perform pg_notify('reset',
json_build_object(
'email', request_password_reset.email,
'token', tok,
'token_type', 'reset'
)::text
);
end;
$$;
```
This function does not send any emails. It sends a postgres
[NOTIFY](http://www.postgresql.org/docs/current/static/sql-notify.html)
command. External programs such as a mailer listen for this event
and do the work. The most robust way to process these signals is
by pushing them onto work queues. Here are two programs to do that:
1. [aweber/pgsql-listen-exchange](https://github.com/aweber/pgsql-listen-exchange) for RabbitMQ
2. [SpiderOak/skeeter](https://github.com/SpiderOak/skeeter) for ZeroMQ
For experimentation you don't need that though. Here's a sample
Node program that listens for the events and logs them to stdout.
```js
var PS = require('pg-pubsub');
if(process.argv.length !== 3) {
console.log("USAGE: DB_URL");
process.exit(2);
}
var url = process.argv[2],
ps = new PS(url);
// password reset request events
ps.addChannel('reset', console.log);
// email validation required event
ps.addChannel('validate', console.log);
// modify me to send emails
```
Once the user has a reset token they can use it as an argument to
the password reset function, calling it through the PostgREST RPC
interface.
```sql
create or replace function
reset_password(email text, token uuid, pass text)
returns void
language plpgsql
as $$
declare
tok uuid;
begin
if exists(select 1 from basic_auth.tokens
where tokens.email = reset_password.email
and tokens.token = reset_password.token
and token_type = 'reset') then
update basic_auth.users set pass=reset_password.pass
where users.email = reset_password.email;
delete from basic_auth.tokens
where tokens.email = reset_password.email
and tokens.token = reset_password.token
and token_type = 'reset';
else
raise invalid_password using message =
'invalid user or token';
end if;
delete from basic_auth.tokens
where token_type = 'reset'
and tokens.email = reset_password.email;
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'reset', reset_password.email);
perform pg_notify('reset',
json_build_object(
'email', reset_password.email,
'token', tok
)::text
);
end;
$$;
```
### Email Validation
This is similar to password resets. Once again we generate a token.
It differs in that there is a trigger to send validations when a
new login is added to the users table.
```sql
create or replace function
basic_auth.send_validation() returns trigger
language plpgsql
as $$
declare
tok uuid;
begin
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'validation', new.email);
perform pg_notify('validate',
json_build_object(
'email', new.email,
'token', tok,
'token_type', 'validation'
)::text
);
return new;
end
$$;
drop trigger if exists send_validation on basic_auth.users;
create trigger send_validation
after insert on basic_auth.users
for each row
execute procedure basic_auth.send_validation();
```
### Editing Own User
We'll construct a redacted view for users. It hides passwords and
shows only those users whose roles the currently logged in user has
db permission to access.
```sql
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;
-- can also add restriction that current_setting('postgrest.claims.email')
-- is equal to email so that user can only see themselves
```
Using this view clients can see themselves and any other users with
the right db roles. This view does not yet support inserts or updates
because not all the columns refer directly to underlying columns.
Nor do we want it to be auto-updatable because it would allow an escalation
of privileges. Someone could update their own row and change their
role to become more powerful.
We'll handle updates with a trigger, but we'll need a helper function
to prevent an escalation of privileges.
```sql
create or replace function
basic_auth.clearance_for_role(u name) returns void as
$$
declare
ok boolean;
begin
select exists (
select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
and rolname = u
) into ok;
if not ok then
raise invalid_password using message =
'current user not member of role ' || u;
end if;
end
$$ LANGUAGE plpgsql;
```
With the above function we can now make a safe trigger to allow
user updates.
```sql
create or replace function
update_users() returns trigger
language plpgsql
AS $$
begin
if tg_op = 'INSERT' then
perform basic_auth.clearance_for_role(new.role);
insert into basic_auth.users
(role, pass, email, verified)
values
(new.role, new.pass, new.email,
coalesce(new.verified, false));
return new;
elsif tg_op = 'UPDATE' then
-- no need to check clearance for old.role because
-- an ineligible row would not have been available to update (http 404)
perform basic_auth.clearance_for_role(new.role);
update basic_auth.users set
email = new.email,
role = new.role,
pass = new.pass,
verified = coalesce(new.verified, old.verified, false)
where email = old.email;
return new;
elsif tg_op = 'DELETE' then
-- no need to check clearance for old.role (see previous case)
delete from basic_auth.users
where basic_auth.email = old.email;
return null;
end if;
end
$$;
drop trigger if exists update_users on users;
create trigger update_users
instead of insert or update or delete on
users for each row execute procedure update_users();
```
Finally add a public function people can use to sign up. You can
hard code a default db role in it. It alters the underlying
`basic_auth.users` so you can set whatever role you want without
restriction.
```sql
create or replace function
signup(email text, pass text) returns void
as $$
insert into basic_auth.users (email, pass, role) values
(signup.email, signup.pass, 'hardcoded-role-here');
$$ language sql;
```
### Generating JWT
As mentioned at the start, clients authenticate with JWT. PostgREST
has a special convention to allow your sql functions to return JWT.
Any function that returns a type whose name ends in `jwt_claims` will
have its return value encoded. For instance, let's make a login function
which consults our users table.
First create a return type:
```sql
drop type if exists basic_auth.jwt_claims cascade;
create type basic_auth.jwt_claims AS (role text, email text);
```
And now the function:
```sql
create or replace function
login(email text, pass text) returns basic_auth.jwt_claims
language plpgsql
as $$
declare
_role name;
_verified boolean;
_email text;
result basic_auth.jwt_claims;
begin
-- check email and password
select basic_auth.user_role(email, pass) into _role;
if _role is null then
raise invalid_password using message = 'invalid user or password';
end if;
-- check verified flag whether users
-- have validated their emails
_email := email;
select verified from basic_auth.users as u where u.email=_email limit 1 into _verified;
if not _verified then
raise invalid_authorization_specification using message = 'user is not verified';
end if;
select _role as role, login.email as email into result;
return result;
end;
$$;
```
An API request to login would look like this.
```HTTP
POST /rpc/login
{ "email": "foo@bar.com", "pass": "foobar" }
```
Response
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicm9sZSI6ImF1dGhvciJ9.KHwYdK9dAMAg-MGCQXuDiFuvbmW-y8FjfYIcMrETnto"
}
```
Try decoding the token at [jwt.io](http://jwt.io/). (It was encoded
with a secret of `secret` which is the default.) To use this token
in a future API request include it in an `Authorization` request
header.
```HTTP
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicm9sZSI6ImF1dGhvciJ9.KHwYdK9dAMAg-MGCQXuDiFuvbmW-y8FjfYIcMrETnto
```
### Same-Role Users
You may not want a separate db role for every user. You can distinguish
one user from another in SQL by examining the JWT claims which
PostgREST makes available in the SQL variable `postgrest.claims`.
Here's a function to get the email of the currently authenticated
user.
```sql
-- Prevent current_setting('postgrest.claims.email') from raising
-- an exception if the setting is not present. Default it to ''.
ALTER DATABASE your_db_name SET postgrest.claims.email TO '';
create or replace function
basic_auth.current_email() returns text
language plpgsql
as $$
begin
return current_setting('postgrest.claims.email');
end;
$$;
```
Remember that the `login` function set the claims `email` and `role`.
You can modify `login` to set other claims as well if they are
useful for your other SQL functions to reference later.
### 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 authenticator noinherit;
grant anon to authenticator;
grant usage on schema public, basic_auth to anon;
-- anon can create new logins
grant insert on table basic_auth.users, basic_auth.tokens to anon;
grant select on table pg_authid, basic_auth.users to anon;
grant execute on function
login(text,text),
request_password_reset(text),
reset_password(text,uuid,text),
signup(text, text)
to anon;
```
### Conclusion
This section explained the implementation details for building a
password based authentication system in pure sql. The next example
will put it to work in a multi-tenant blogging API.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

-91
View File
@@ -1,91 +0,0 @@
<style>
.videoWrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
padding-top: 25px;
height: 0;
}
.videoWrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
![PostgREST logo](img/logo.png)
## Introduction
PostgREST is a standalone web server that turns your database directly into a RESTful API. The structural constraints and permissions in the database determine the API endpoints and operations.
This guide explains how to install the software and provides practical examples of its use. You'll learn how to build a fast, versioned, secure API and how to deploy it to production.
The project has a friendly and growing community. Here are some ways to get help or get involved:
* The project [chat room](https://gitter.im/begriffs/postgrest)
* Report or search [issues](https://github.com/begriffs/postgrest/issues)
### Motivation
Using PostgREST is an alternative to manual CRUD programming. Custom API servers suffer problems. Writing business logic often duplicates, ignores or hobbles database structure. Object-relational mapping is a leaky abstraction leading to slow imperative code. The PostgREST philosophy establishes a single declarative source of truth: the data itself.
#### Declarative Programming
It's easier to ask Postgres to join data for you and let its query planner figure out the details than to loop through rows yourself. It's easier to assign permissions to db objects than to add guards in controllers. (This is especially true for cascading permissions in data dependencies.) It's easier set constraints than to litter code with sanity checks.
#### Leakproof Abstraction
There is no ORM involved. Creating new views happens in SQL with known performance implications. A database administrator can now create an API from scratch with no custom programming.
#### Embracing the Relational Model
In 1970 E. F. Codd criticized the then-dominant hierarchical model of databases in his article <a href="https://www.seas.upenn.edu/~zives/03f/cis550/codd.pdf">A Relational Model of Data for Large Shared Data Banks</a>. Reading the article reveals a striking similarity between hierarchical databases and nested http routes. With PostgREST we attempt to use flexible filtering and embedding rather than nested routes.
#### One Thing Well
PostgREST has a focused scope. It works well with other tools like Nginx. This forces you to cleanly separate the data-centric CRUD operations from other concerns. Use a collection of sharp tools rather than building a big ball of mud.
#### Shared Improvements
As with any open source project, we all gain from features and fixes in the tool. It's more beneficial than improvements locked inextricably within custom codebases.
### Intro Video
Some things have changed since this video was created but the basics are the same. Learn the big vision behind automating APIs.
<div class="videoWrapper">
<iframe src="https://player.vimeo.com/video/115668217" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
</div>
### Myths
#### You have to make tons of stored procs and triggers
Modern PostgreSQL features like auto-updatable views and computed columns make this mostly unnecessary. Triggers do play a part, but generally not for irksome boilerplate. When they are required triggers are preferable to ad-hoc app code anyway, since the former work reliably for any codepath.
#### Exposing the database destroys encapsulation
PostgREST does versioning through database schemas. This allows you to expose tables and views without making the app brittle. Underlying tables can be superseded and hidden behind public facing views. The chapter about versioning shows how to do this.
### Conventions
This guide contains highlighted notes and tangential information interspersed with the text.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>Contains history which informed the current design. Sometimes it discusses unavoidable tradeoffs or a point of theory.</p>
</div>
<div class="admonition warning">
<p class="admonition-title">Invitation to Contribute</p>
<p>Points out things we know we want to add or improve. They might give you ideas for ways to contribute to the project.</p>
</div>
<div class="admonition danger">
<p class="admonition-title">Deprecation Warning</p>
<p>Alerts you to features which will be removed in the next major (breaking) release.</p>
</div>
-26
View File
@@ -1,26 +0,0 @@
## Ecosystem
### Client-Side Libraries
* [calebmer/postgrest-client](https://github.com/calebmer/postgrest-client) - Advanced JS client for the PostgREST API
* [mithril.postgrest](https://github.com/catarse/mithril.postgrest) - Mithril plugin to create and authenticate requests
* [lewisjared/postgrest-request](https://github.com/lewisjared/postgrest-request) - node interface to postgrest instances
* [JarvusInnovations/jarvus-postgrest-apikit](https://github.com/JarvusInnovations/jarvus-postgrest-apikit) - Sencha framework package for binding models/stores/proxies to PostgREST tables
* [davidthewatson/postgrest_python_requests_client](https://github.com/davidthewatson/postgrest_python_requests_client) - python client featuring JWT auth and pagination of result sets
### Extensions
* [srid/spas](https://github.com/srid/spas) - allow file uploads and basic auth
### Example Apps
* [ruslantalpa/blogdemo](https://github.com/ruslantalpa/blogdemo) - blog api demo in a vagrant image
* [timwis/ext-postgrest-crud](https://github.com/timwis/ext-postgrest-crud) - browser-based spreadsheet
* [srid/chronicle](https://github.com/srid/chronicle#deploying-to-heroku) - tracking a tree of personal memories
* [begriffs/postgrest-example](https://github.com/begriffs/postgrest-example) - how to configure a db for use as an API
* [marmelab/ng-admin-postgrest](https://github.com/marmelab/ng-admin-postgrest) - automatic database admin panel
* [tyrchen/goodfilm](https://github.com/tyrchen/goodfilm) - example film api
### In Production
* [Catarse](https://www.catarse.me/)
-181
View File
@@ -1,181 +0,0 @@
## Installation
### Installing from Pre-Built Release
The [release page](https://github.com/begriffs/postgrest/releases/latest)
has precompiled binaries for Mac OS X, Windows, and several Linux
distros. Extract the tarball and run the binary inside with no
arguments to see usage instructions:
```sh
# Untar the release (available at https://github.com/begriffs/postgrest/releases/latest)
$ tar zxf postgrest-[version]-[platform].tar.xz
# Try running it
$ ./postgrest
# You should see a usage help message
```
<div class="admonition warning">
<p class="admonition-title">Invitation to Contribute</p>
<p>I currently build the binaries manually for each architecture.
It would be nice to set up an automated build matrix for various
architectures. It should support Mac, Windows and 32- and 64-bit
versions of
<ul><li>Scientific Linux 6</li><li>CentOS</li><li>RHEL 6</li></ul></p>
</div>
### Building from Source
When a prebuilt binary does not exist for your system you can build
the project from source. You'll also need to do this if you want
to help with development.
[Stack](https://github.com/commercialhaskell/stack) makes it easy.
It will install any necessary Haskell dependencies on your system.
* [Install Stack](http://docs.haskellstack.org/en/stable/README.html#how-to-install) for your platform
```bash
#ubuntu example
#See the link above for other operating systems
wget -q -O- https://s3.amazonaws.com/download.fpcomplete.com/ubuntu/fpco.key | sudo apt-key add -
echo 'deb http://download.fpcomplete.com/ubuntu/trusty stable main'|sudo tee /etc/apt/sources.list.d/fpco.list
sudo apt-get update && sudo apt-get install stack -y
```
* Install libpq-dev
```
sudo apt-get install -y libpq-dev
```
* Build & install in one step
```bash
git clone https://github.com/begriffs/postgrest.git
cd postgrest
stack build --install-ghc
sudo stack install --allow-different-user --local-bin-path /usr/local/bin
```
* Run the server
If you want to run the test suite, stack can do that too: `stack test`.
### Running the Server
```bash
postgrest postgres://user:pass@host:port/db -a anon_user [other flags]
```
The user in the connection string is the "authenticator role," i.e.
a role which is used temporarily to switch into other roles depending
on the authentication request JWT. For simple API's you can use the
same role for authenticator and anonymous.
The complete list of options:
<dl>
<dt>-p, --port</dt>
<dd>The port on which the server will listen for HTTP requests.
Defaults to 3000.</dd>
<dt>-a, --anonymous (required)</dt>
<dd>The database role used to execute commands for those requests
which provide no JWT authorization.</dd>
<dt>-s, --schema</dt>
<dd>The db schema which you want to expose as an API. For historical
reasons it defaults to <code>1</code>, but you're more likely
to want to choose a value of <code>public</code>.</dd>
<dt>-j, --jwt-secret</dt>
<dd>The secret passphrase used to encrypt JWT tokens. Defaults to
<code>secret</code> but do not use the default in production!
Load-balanced PostgREST servers should share the same secret.</dd>
<dt>-o, --pool</dt>
<dd>Max connections to use in db pool. Defaults to to 10, but you
should find an optimal value for your db by running the SQL
command <code>show max_connections;</code></dd>
<dt>-m, --max-rows</dt>
<dd>Max number of rows to return in a read request. The default is
no limit.</dd>
</dl>
<div class="admonition note">
<p class="admonition-title">Hiding Password from Process List</p>
<p>Passing the database password and JWT secret as naked
parameters might not be a good idea because the parameters are
visible in a <code>ps</code> listing. One solution is to set
environment variables such as PASS and use <code>$PASS</code>
in the connection string. Another is to use a user-specific
<a
href="http://www.postgresql.org/docs/current/static/libpq-pgpass.html">.pgpass</a>
file.</p>
</div>
When running `postgrest` on the same machine as PostgreSQL, it is also
possible to connect to the database using the [Unix socket]
(https://en.wikipedia.org/wiki/Unix_domain_socket) and the
[Peer Authentication method]
(http://www.postgresql.org/docs/current/static/auth-methods.html#AUTH-PEER)
as an alternative to TCP/IP communication and authentication with a password.
The Peer Authentication grants access to the database to any Unix user
who connects as a user of the same name in the database.
Since the empty host resolves to the Unix socket]
(http://www.postgresql.org/docs/current/static/libpq-connect.html#AEN42494)
and the password can be omitted in this case,
the command line is reduced to:
```sh
sudo -u user postgrest postgres://user@/db [flags]
```
where the `sudo -u user` command runs the following command as given `user`.
If you create a Unix user `postgrest` and a database user `postgrest`
for example, the command becomes:
```sh
sudo -u postgrest postgrest postgres://postgrest@/db [flags]
```
The first `postgrest` is the Unix user name, the second `postgrest`
is the name of the executable, the third `postgrest` is the name
of the database user.
### Install via Homebrew (Mac OS X)
You can use the Homebrew package manager to install PostgREST on Mac
```bash
# Ensure brew is up to date
brew update
# Check for any problems with brew's setup
brew doctor
# Install the postgrest package
brew install postgrest
```
This will automatically install PostgreSQL as a dependency (see the [Installing PostgreSQL](#installing-postgresql) section for setup instructions). The process tends to take up to 15 minutes to install the package and its dependencies.
After installation completes, the tool is added to your $PATH and can be used from anywhere with:
```bash
postgrest --help
```
### Installing PostgreSQL
To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or greater is required). You can use something like Amazon [RDS](https://aws.amazon.com/rds/) but installing your own locally is cheaper and more convenient for development.
* [Instructions for OS X](http://exponential.io/blog/2015/02/21/install-postgresql-on-mac-os-x-via-brew/)
* [Instructions for Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-14-04)
* [Installer for Windows](http://www.enterprisedb.com/products-services-training/pgdownload#windows)
-28
View File
@@ -1,28 +0,0 @@
site_name: PostgREST
site_url: http://postgrest.com
site_description: Building declarative APIs
site_author: Joe Nelson
site_favicon: favicon.ico
repo_url: https://github.com/begriffs/postgrest
pages:
- Home: index.md
- Install:
- The Server: install/server.md
- Ecosystem: install/ecosystem.md
- API:
- Reading: api/reading.md
- Writing: api/writing.md
- Admin:
- Security: admin/security.md
- Versioning: admin/versioning.md
- Migration: admin/migration.md
- Deployment: admin/deployment.md
- Performance: admin/performance.md
- Examples:
- Getting Started: examples/start.md
- User Management: examples/users.md
- Multi-Tenant Blog: examples/blog.md
- External Authentication: examples/external_auth.md
- Python Client: examples/python-requests-jwt.md