Remove signups and user editing

Too complicated for this section
This commit is contained in:
Joe Nelson
2016-10-31 22:43:41 -07:00
parent 2c53cb4fae
commit 5919a37197
+4 -218
View File
@@ -231,8 +231,6 @@ First we'll need a table to keep track of our 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
);
We would like the role to be a foreign key to actual database roles, however PostgreSQL does not support these constraints against the `pg_roles` table. We'll use a trigger to manually enforce it.
@@ -300,36 +298,13 @@ With the table in place we can make a helper to check a password against the enc
end;
$$;
Finally we want a helper function to check whether the database user for the current API request has access to see or change a given role. This will become useful in the next section.
.. code:: postgres
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;
Public User Interface
---------------------
In the previous section we created an internal place to store user information. Here we create views and functions in a public schema that clients will access through the HTTP API. These public relations allow users view or edit their own information, log in, sign up, etc.
In the previous section we created an internal table to store user information. Here we create a login function which takes an email address and password and returns JWT if the credentials match a user in the internal table.
Logins and Signup
~~~~~~~~~~~~~~~~~
Logins
~~~~~~
As described in `JWT from SQL`_, we'll create a JWT inside our login function. Note that you'll need to adjust the secret key which is hardcoded in this example to a secure secret of your choosing.
@@ -341,8 +316,6 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N
as $$
declare
_role name;
_verified boolean;
_email text;
result basic_auth.jwt_claims;
begin
-- check email and password
@@ -350,13 +323,6 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N
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 jwt.sign(
row_to_json(r), 'mysecret'
@@ -386,72 +352,6 @@ The response would look like the snippet below. Try decoding the token at `jwt.i
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicm9sZSI6ImF1dGhvciJ9.fpf3_ERi5qbWOE5NPzvauJgvulm0zkIG9xSm2w5zmdw"
}
Editing User Info
~~~~~~~~~~~~~~~~~
By creating a public wrapper around the internal users table we can allow people to safely edit it through the same auto-generated API that apply to other tables and views. The following view redacts sensitive information. It hides passwords and shows only those users whose roles the currently logged in user has database permission to access.
.. code:: postgres
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('request.jwt.claim.email')
-- is equal to email so that user can only see themselves
Using this view a client can see their role and any other users to whose roles the client belongs. 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:
.. code:: plpgsql
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();
Permissions
~~~~~~~~~~~
@@ -466,121 +366,7 @@ Your database roles need access to the schema, tables, views and functions in or
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),
signup(text, text)
to anon;
grant execute on function login(text,text) to anon;
You may be worried from the above that anonymous users can read everything from the `basic_auth.users` table. However this table is not available for direct queries because it lives in a separate schema. The anonymous role needs access because the public `users` view reads the underlying table with the permissions of the calling user. But we have made sure the view properly restricts access to sensitive information.
Interacting with Email
----------------------
External actions like sending an email or calling 3rd-party services are possible in PostgREST but must be handled with care. Even if there are PostgreSQL extensions to make network requests it is bad practice to do this in SQL. Blocking on the outside world is unhealthy in a database and holds open long-running transactions. The proper approach is for the database to signal an external program to perform the required action and then not block on the result.
One way to do this is using a table to implement a job queue for external programs. However this approach is `dangerous <https://brandur.org/postgres-queues>`_ because of its potential interactions with unrelated long-running queries. However things are improving with PostgreSQL 9.5 which introduces SKIP LOCKED to build reliable work queues, see `this article <http://blog.2ndquadrant.com/what-is-select-skip-locked-for-in-postgresql-9-5/>`_.
Another way to queue tasks for external processing is by bridging PostgreSQL's `LISTEN <https://www.postgresql.org/docs/9.6/static/sql-listen.html>`_/`NOTIFY <https://www.postgresql.org/docs/9.6/static/sql-notify.html>`_ pubsub with a dedicated external queue system. Two programs to listen for database events and queue them are
* `aweber/pgsql-listen-exchange <https://github.com/aweber/pgsql-listen-exchange>`_ for RabbitMQ
* `SpiderOak/skeeter <https://github.com/SpiderOak/skeeter>`_ for ZeroMQ
For experimentation purposes you can also have external programs LISTEN directly for PostgreSQL events. It's less robust than a queuing system but an example Node program might look like this:
.. code:: 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
To use this LISTEN/NOTIFY approach (with or without a real queue hooked up) we can make our SQL functions issue a NOTIFY to perform external actions. Two such such functions are those to confirm an email address or send a password reset token. Both will use nonces and need a place to store them, so we'll start there.
.. code:: postgres
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
);
Here is a password reset function to make public for API requests. The function takes a user email address.
.. code:: plpgsql
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;
$$;
Notice the use of `pg_notify` above. It notifies a channel called `reset` with a JSON object containing details of the email address and token. A worker process would directly LISTEN for this event or would pull it off a queue and do the work to send an email with a friendly human readable message.
Similar to the password reset request, an email validation function creates a token and then defers to external processing. This one won't be publicly accessible, but rather can be triggered on user account creation.
.. code:: plpgsql
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();