From 2687ad63ddd9b19a4cd5889d19f9e4410a3b7b09 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Thu, 5 Nov 2015 19:06:10 -0800 Subject: [PATCH 01/21] Basic tables/procs for basic auth --- schema-templates/basic-auth.sql | 206 ++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 schema-templates/basic-auth.sql diff --git a/schema-templates/basic-auth.sql b/schema-templates/basic-auth.sql new file mode 100644 index 000000000..4f828606b --- /dev/null +++ b/schema-templates/basic-auth.sql @@ -0,0 +1,206 @@ +------------------------------------------------------------------------------- +-- Adapted from https://github.com/robconery/pg-auth + +begin; + +drop schema if exists basic_auth cascade; +create schema if not exists basic_auth; +set search_path to basic_auth; + +------------------------------------------------------------------------------- +-- Utility functions + +create function random_value(len int, out result varchar(32)) as +$$ +BEGIN +SELECT substring(md5(random()::text),0, len) into result; +END +$$ LANGUAGE plpgsql; + +------------------------------------------------------------------------------- +-- Login storage and constraints + +create table logins ( + username character varying not null, + pass character(60) not null, + role name not null, + email character varying not null unique, + active boolean not null default false, + more JSON, + constraint l_pkey primary key (username) +); + +create function 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 +$$; + +create constraint trigger ensure_login_role_exists + after insert or update on logins + for each row + execute procedure check_role_exists(); + +create function 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 +$$; + +create trigger protect_passwords + before insert or update on logins + for each row + execute procedure encrypt_pass(); + +create function send_validation() returns trigger + language plpgsql + as $$ +declare + tok character varying; +begin + select basic_auth.random_value(64) into tok; + insert into basic_auth.tokens (token, token_type, username) + values (tok, 'validation', new.username); + perform pg_notify('validate', + json_build_object( + 'email', new.email, + 'username', new.username, + 'token', tok + )::text + ); + return new; +end +$$; + +create trigger send_validation_t + after insert on logins + for each row + execute procedure send_validation(); + +------------------------------------------------------------------------------- +-- Email Validation and Password Reset + +create table tokens ( + token character varying unique, + token_type varchar(64) not null, + username character varying not null, + created_at timestamptz not null default current_date, + constraint t_pk primary key (token), + constraint t_login_fk foreign key (username) references logins + on delete cascade on update cascade +); + +------------------------------------------------------------------------------- +-- Passwording + +create function +login_role(username text, pass text) returns text + language plpgsql + as $$ +begin + return ( + select role from basic_auth.logins + where logins.username = login_role.username + and logins.pass = crypt(login_role.pass, logins.pass) + ); +end; +$$; + +create function request_password_reset(username text) returns void + language plpgsql + as $$ +declare + tok character varying; +begin + delete from basic_auth.tokens + where token_type = 'reset' + and tokens.username = request_password_reset.username; + + select basic_auth.random_value(64) into tok; + insert into basic_auth.tokens (token, token_type, username) + values (tok, 'reset', request_password_reset.username); + perform pg_notify('reset', + json_build_object( + 'email', (select email + from basic_auth.logins + where logins.username = request_password_reset.username), + 'username', request_password_reset.username, + 'token', tok + )::text + ); +end; +$$; + +create or replace function basic_auth.reset_password(username text, token text, pass text) + returns void + language plpgsql + as $$ +declare + tok character varying; +begin + if exists(select 1 from basic_auth.tokens + where tokens.username = reset_password.username + and tokens.token = reset_password.token + and token_type = 'reset') then + update basic_auth.logins set pass=reset_password.pass + where logins.username = reset_password.username; + + delete from basic_auth.tokens + where tokens.username = reset_password.username + 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.username = reset_password.username; + + select basic_auth.random_value(64) into tok; + insert into basic_auth.tokens (token, token_type, username) + values (tok, 'reset', reset_password.username); + perform pg_notify('reset', + json_build_object( + 'email', (select email + from basic_auth.logins + where logins.username = reset_password.username), + 'username', reset_password.username, + 'token', tok + )::text + ); +end; +$$; + +create type jwt_claims AS (role text, username text); + +create function +obtain_auth_token(username text, pass text) returns jwt_claims + language plpgsql + as $$ +declare + _role character varying; + result jwt_claims; +begin + select basic_auth.login_role(username, pass) into _role; + if _role is null then + raise invalid_password using message = 'invalid user or password'; + end if; + select _role as role, obtain_auth_token.username as username into result; + return result; +end; +$$; + +commit; From d4ef343b8d42ad49beee3b7c7a4f660168d952eb Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 6 Nov 2015 14:41:53 -0800 Subject: [PATCH 02/21] Leave public functions in current schema Put supporting things into basic_auth schema Also tested from a clean db --- schema-templates/basic-auth.sql | 71 ++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/schema-templates/basic-auth.sql b/schema-templates/basic-auth.sql index 4f828606b..d62c0e23f 100644 --- a/schema-templates/basic-auth.sql +++ b/schema-templates/basic-auth.sql @@ -3,14 +3,14 @@ begin; -drop schema if exists basic_auth cascade; +create extension if not exists pgcrypto; create schema if not exists basic_auth; -set search_path to basic_auth; ------------------------------------------------------------------------------- -- Utility functions -create function random_value(len int, out result varchar(32)) as +create or replace function +basic_auth.random_value(len int, out result varchar(32)) as $$ BEGIN SELECT substring(md5(random()::text),0, len) into result; @@ -20,7 +20,8 @@ $$ LANGUAGE plpgsql; ------------------------------------------------------------------------------- -- Login storage and constraints -create table logins ( +create table if not exists +basic_auth.logins ( username character varying not null, pass character(60) not null, role name not null, @@ -30,7 +31,8 @@ create table logins ( constraint l_pkey primary key (username) ); -create function check_role_exists() returns trigger +create or replace function +basic_auth.check_role_exists() returns trigger language plpgsql as $$ begin @@ -43,12 +45,14 @@ begin end $$; +drop trigger if exists ensure_login_role_exists on basic_auth.logins; create constraint trigger ensure_login_role_exists - after insert or update on logins + after insert or update on basic_auth.logins for each row - execute procedure check_role_exists(); + execute procedure basic_auth.check_role_exists(); -create function encrypt_pass() returns trigger +create or replace function +basic_auth.encrypt_pass() returns trigger language plpgsql as $$ begin @@ -59,12 +63,14 @@ begin end $$; +drop trigger if exists protect_passwords on basic_auth.logins; create trigger protect_passwords - before insert or update on logins + before insert or update on basic_auth.logins for each row - execute procedure encrypt_pass(); + execute procedure basic_auth.encrypt_pass(); -create function send_validation() returns trigger +create or replace function +basic_auth.send_validation() returns trigger language plpgsql as $$ declare @@ -77,36 +83,39 @@ begin json_build_object( 'email', new.email, 'username', new.username, - 'token', tok + 'token', tok, + 'token_type', 'validation' )::text ); return new; end $$; +drop trigger if exists send_validation_t on basic_auth.logins; create trigger send_validation_t - after insert on logins + after insert on basic_auth.logins for each row - execute procedure send_validation(); + execute procedure basic_auth.send_validation(); ------------------------------------------------------------------------------- -- Email Validation and Password Reset -create table tokens ( +create table if not exists +basic_auth.tokens ( token character varying unique, token_type varchar(64) not null, username character varying not null, created_at timestamptz not null default current_date, constraint t_pk primary key (token), - constraint t_login_fk foreign key (username) references logins + constraint t_login_fk foreign key (username) references basic_auth.logins on delete cascade on update cascade ); ------------------------------------------------------------------------------- --- Passwording +-- Login helper -create function -login_role(username text, pass text) returns text +create or replace function +basic_auth.login_role(username text, pass text) returns text language plpgsql as $$ begin @@ -118,7 +127,11 @@ begin end; $$; -create function request_password_reset(username text) returns void +------------------------------------------------------------------------------- +-- Public functions (in current schema, not basic_auth) + +create or replace function +request_password_reset(username text) returns void language plpgsql as $$ declare @@ -137,13 +150,15 @@ begin from basic_auth.logins where logins.username = request_password_reset.username), 'username', request_password_reset.username, - 'token', tok + 'token', tok, + 'token_type', 'reset' )::text ); end; $$; -create or replace function basic_auth.reset_password(username text, token text, pass text) +create or replace function +reset_password(username text, token text, pass text) returns void language plpgsql as $$ @@ -184,21 +199,23 @@ begin end; $$; -create type jwt_claims AS (role text, username text); +drop type if exists basic_auth.jwt_claims cascade; +create type +basic_auth.jwt_claims AS (role text, username text); -create function -obtain_auth_token(username text, pass text) returns jwt_claims +create or replace function +create_auth_token(username text, pass text) returns basic_auth.jwt_claims language plpgsql as $$ declare _role character varying; - result jwt_claims; + result basic_auth.jwt_claims; begin select basic_auth.login_role(username, pass) into _role; if _role is null then raise invalid_password using message = 'invalid user or password'; end if; - select _role as role, obtain_auth_token.username as username into result; + select _role as role, create_auth_token.username as username into result; return result; end; $$; From 1524a4fe743e99e3f359bcbd9dc75a585352d3bf Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 6 Nov 2015 22:35:34 -0800 Subject: [PATCH 03/21] User management view and trigger --- schema-templates/basic-auth.sql | 75 +++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/schema-templates/basic-auth.sql b/schema-templates/basic-auth.sql index d62c0e23f..ff1aaea77 100644 --- a/schema-templates/basic-auth.sql +++ b/schema-templates/basic-auth.sql @@ -17,6 +17,26 @@ SELECT substring(md5(random()::text),0, len) into result; END $$ LANGUAGE plpgsql; + +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; + ------------------------------------------------------------------------------- -- Login storage and constraints @@ -220,4 +240,59 @@ begin end; $$; +------------------------------------------------------------------------------- +-- User management + +create or replace view logins as +select actual.username as username, + actual.role as role, + '***'::text as pass, + actual.email as email, + actual.active as active, + actual.more as more +from basic_auth.logins as actual, + (select rolname + from pg_authid + where pg_has_role(current_user, oid, 'member') + ) as member_of +where actual.role = member_of.rolname; + +create or replace function +update_logins() returns trigger +language plpgsql +AS $$ +begin + if tg_op = 'INSERT' then + perform basic_auth.clearance_for_role(new.role); + + insert into basic_auth.logins + (username, role, pass, email, active, more) values + (new.username, new.role, new.pass, new.email, + new.active, new.more); + return new; + elsif tg_op = 'UPDATE' then + -- no need to check clearance for old.role because + -- an ineligible row would not even available to update (http 404) + perform basic_auth.clearance_for_role(new.role); + + update basic_auth.logins set + username = new.username, role = new.role, + pass = new.pass, email = new.email, + active = new.active, more = new.more + where username = old.username; + return new; + elsif tg_op = 'DELETE' then + -- no need to check clearance for old.role (see previous case) + + delete from basic_auth.logins + where basic_auth.username = old.username; + return null; + end if; +end +$$; + +create trigger update_logins_t + instead of insert or update or delete on + logins for each row execute procedure update_logins(); + commit; From 1e017b86d35a4711ff48fafd0cde37ff5659660a Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 8 Nov 2015 09:46:51 -0800 Subject: [PATCH 04/21] Drop trigger before (re)creating it --- schema-templates/basic-auth.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/schema-templates/basic-auth.sql b/schema-templates/basic-auth.sql index ff1aaea77..d938d2266 100644 --- a/schema-templates/basic-auth.sql +++ b/schema-templates/basic-auth.sql @@ -291,6 +291,7 @@ begin end $$; +drop trigger if exists update_logins_t on logins; create trigger update_logins_t instead of insert or update or delete on logins for each row execute procedure update_logins(); From c25cd1b97eb850b9d862d0bf95f7b52b764681bc Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 8 Nov 2015 09:47:10 -0800 Subject: [PATCH 05/21] Use uuid-ossp extension rather than custom function --- schema-templates/basic-auth.sql | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/schema-templates/basic-auth.sql b/schema-templates/basic-auth.sql index d938d2266..c17e071f4 100644 --- a/schema-templates/basic-auth.sql +++ b/schema-templates/basic-auth.sql @@ -4,20 +4,12 @@ begin; create extension if not exists pgcrypto; +create extension if not exists "uuid-ossp"; create schema if not exists basic_auth; ------------------------------------------------------------------------------- -- Utility functions -create or replace function -basic_auth.random_value(len int, out result varchar(32)) as -$$ -BEGIN -SELECT substring(md5(random()::text),0, len) into result; -END -$$ LANGUAGE plpgsql; - - create or replace function basic_auth.clearance_for_role(u name) returns void as $$ @@ -96,7 +88,7 @@ basic_auth.send_validation() returns trigger declare tok character varying; begin - select basic_auth.random_value(64) into tok; + select uuid_generate_v4() into tok; insert into basic_auth.tokens (token, token_type, username) values (tok, 'validation', new.username); perform pg_notify('validate', @@ -161,7 +153,7 @@ begin where token_type = 'reset' and tokens.username = request_password_reset.username; - select basic_auth.random_value(64) into tok; + select uuid_generate_v4() into tok; insert into basic_auth.tokens (token, token_type, username) values (tok, 'reset', request_password_reset.username); perform pg_notify('reset', @@ -204,7 +196,7 @@ begin where token_type = 'reset' and tokens.username = reset_password.username; - select basic_auth.random_value(64) into tok; + select uuid_generate_v4() into tok; insert into basic_auth.tokens (token, token_type, username) values (tok, 'reset', reset_password.username); perform pg_notify('reset', From 9fa16e205359646e1d02663f64e440dac6191b57 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 8 Nov 2015 10:47:30 -0800 Subject: [PATCH 06/21] Handle missing "active" key in logins update The default value of underlying column gets clobbered by triggers --- schema-templates/basic-auth.sql | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/schema-templates/basic-auth.sql b/schema-templates/basic-auth.sql index c17e071f4..d76e0d22c 100644 --- a/schema-templates/basic-auth.sql +++ b/schema-templates/basic-auth.sql @@ -260,7 +260,7 @@ begin insert into basic_auth.logins (username, role, pass, email, active, more) values (new.username, new.role, new.pass, new.email, - new.active, new.more); + coalesce(new.active, false), new.more); return new; elsif tg_op = 'UPDATE' then -- no need to check clearance for old.role because @@ -270,7 +270,8 @@ begin update basic_auth.logins set username = new.username, role = new.role, pass = new.pass, email = new.email, - active = new.active, more = new.more + active = coalesce(new.active, old.active, false), + more = new.more where username = old.username; return new; elsif tg_op = 'DELETE' then From 3a659843d20ceae5fe9867817ed92a0de319bd64 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 8 Nov 2015 11:12:56 -0800 Subject: [PATCH 07/21] Limit field lengths in logins It is open to the public so people could abuse the storage Also switch to text type everywhere --- schema-templates/basic-auth.sql | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/schema-templates/basic-auth.sql b/schema-templates/basic-auth.sql index d76e0d22c..9026c0d3f 100644 --- a/schema-templates/basic-auth.sql +++ b/schema-templates/basic-auth.sql @@ -34,13 +34,16 @@ $$ LANGUAGE plpgsql; create table if not exists basic_auth.logins ( - username character varying not null, - pass character(60) not null, + username text not null, + pass text not null, role name not null, - email character varying not null unique, + email text not null unique, active boolean not null default false, more JSON, - constraint l_pkey primary key (username) + constraint l_pkey primary key (username), + constraint login_field_length_limits check ( + length(username::text) < 512 AND length(pass) < 512 AND + length(email::text) < 512 AND length(more::text) < 1024) ); create or replace function @@ -86,7 +89,7 @@ basic_auth.send_validation() returns trigger language plpgsql as $$ declare - tok character varying; + tok text; begin select uuid_generate_v4() into tok; insert into basic_auth.tokens (token, token_type, username) @@ -114,9 +117,9 @@ create trigger send_validation_t create table if not exists basic_auth.tokens ( - token character varying unique, - token_type varchar(64) not null, - username character varying not null, + token text unique, + token_type text not null, + username text not null, created_at timestamptz not null default current_date, constraint t_pk primary key (token), constraint t_login_fk foreign key (username) references basic_auth.logins @@ -147,7 +150,7 @@ request_password_reset(username text) returns void language plpgsql as $$ declare - tok character varying; + tok text; begin delete from basic_auth.tokens where token_type = 'reset' @@ -175,7 +178,7 @@ reset_password(username text, token text, pass text) language plpgsql as $$ declare - tok character varying; + tok text; begin if exists(select 1 from basic_auth.tokens where tokens.username = reset_password.username @@ -220,7 +223,7 @@ create_auth_token(username text, pass text) returns basic_auth.jwt_claims language plpgsql as $$ declare - _role character varying; + _role text; result basic_auth.jwt_claims; begin select basic_auth.login_role(username, pass) into _role; From 64172873e41ac418fad7bcbd4494a7fa1553de24 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 8 Nov 2015 12:39:49 -0800 Subject: [PATCH 08/21] Explanation about split schemas --- schema-templates/basic-auth.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/schema-templates/basic-auth.sql b/schema-templates/basic-auth.sql index 9026c0d3f..b2cbc8181 100644 --- a/schema-templates/basic-auth.sql +++ b/schema-templates/basic-auth.sql @@ -5,6 +5,10 @@ begin; create extension if not exists pgcrypto; create extension if not exists "uuid-ossp"; + +-- 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; ------------------------------------------------------------------------------- From 9daaf6ba708067725852bdc1e66c2595520d4841 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 8 Nov 2015 12:42:36 -0800 Subject: [PATCH 09/21] s/logins/users --- schema-templates/basic-auth.sql | 64 ++++++++++++++++----------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/schema-templates/basic-auth.sql b/schema-templates/basic-auth.sql index b2cbc8181..fdbe3f4ee 100644 --- a/schema-templates/basic-auth.sql +++ b/schema-templates/basic-auth.sql @@ -34,18 +34,18 @@ end $$ LANGUAGE plpgsql; ------------------------------------------------------------------------------- --- Login storage and constraints +-- Users storage and constraints create table if not exists -basic_auth.logins ( +basic_auth.users ( username text not null, pass text not null, role name not null, email text not null unique, active boolean not null default false, more JSON, - constraint l_pkey primary key (username), - constraint login_field_length_limits check ( + constraint user_pkey primary key (username), + constraint user_field_length_limits check ( length(username::text) < 512 AND length(pass) < 512 AND length(email::text) < 512 AND length(more::text) < 1024) ); @@ -64,9 +64,9 @@ begin end $$; -drop trigger if exists ensure_login_role_exists on basic_auth.logins; -create constraint trigger ensure_login_role_exists - after insert or update on basic_auth.logins +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(); @@ -82,9 +82,9 @@ begin end $$; -drop trigger if exists protect_passwords on basic_auth.logins; +drop trigger if exists protect_passwords on basic_auth.users; create trigger protect_passwords - before insert or update on basic_auth.logins + before insert or update on basic_auth.users for each row execute procedure basic_auth.encrypt_pass(); @@ -110,9 +110,9 @@ begin end $$; -drop trigger if exists send_validation_t on basic_auth.logins; +drop trigger if exists send_validation_t on basic_auth.users; create trigger send_validation_t - after insert on basic_auth.logins + after insert on basic_auth.users for each row execute procedure basic_auth.send_validation(); @@ -126,7 +126,7 @@ basic_auth.tokens ( username text not null, created_at timestamptz not null default current_date, constraint t_pk primary key (token), - constraint t_login_fk foreign key (username) references basic_auth.logins + constraint t_user_fk foreign key (username) references basic_auth.users on delete cascade on update cascade ); @@ -134,14 +134,14 @@ basic_auth.tokens ( -- Login helper create or replace function -basic_auth.login_role(username text, pass text) returns text +basic_auth.user_role(username text, pass text) returns text language plpgsql as $$ begin return ( - select role from basic_auth.logins - where logins.username = login_role.username - and logins.pass = crypt(login_role.pass, logins.pass) + select role from basic_auth.users + where users.username = user_role.username + and users.pass = crypt(user_role.pass, users.pass) ); end; $$; @@ -166,8 +166,8 @@ begin perform pg_notify('reset', json_build_object( 'email', (select email - from basic_auth.logins - where logins.username = request_password_reset.username), + from basic_auth.users + where users.username = request_password_reset.username), 'username', request_password_reset.username, 'token', tok, 'token_type', 'reset' @@ -188,8 +188,8 @@ begin where tokens.username = reset_password.username and tokens.token = reset_password.token and token_type = 'reset') then - update basic_auth.logins set pass=reset_password.pass - where logins.username = reset_password.username; + update basic_auth.users set pass=reset_password.pass + where users.username = reset_password.username; delete from basic_auth.tokens where tokens.username = reset_password.username @@ -209,8 +209,8 @@ begin perform pg_notify('reset', json_build_object( 'email', (select email - from basic_auth.logins - where logins.username = reset_password.username), + from basic_auth.users + where users.username = reset_password.username), 'username', reset_password.username, 'token', tok )::text @@ -230,7 +230,7 @@ declare _role text; result basic_auth.jwt_claims; begin - select basic_auth.login_role(username, pass) into _role; + select basic_auth.user_role(username, pass) into _role; if _role is null then raise invalid_password using message = 'invalid user or password'; end if; @@ -242,14 +242,14 @@ $$; ------------------------------------------------------------------------------- -- User management -create or replace view logins as +create or replace view users as select actual.username as username, actual.role as role, '***'::text as pass, actual.email as email, actual.active as active, actual.more as more -from basic_auth.logins as actual, +from basic_auth.users as actual, (select rolname from pg_authid where pg_has_role(current_user, oid, 'member') @@ -257,14 +257,14 @@ from basic_auth.logins as actual, where actual.role = member_of.rolname; create or replace function -update_logins() returns trigger +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.logins + insert into basic_auth.users (username, role, pass, email, active, more) values (new.username, new.role, new.pass, new.email, coalesce(new.active, false), new.more); @@ -274,7 +274,7 @@ begin -- an ineligible row would not even available to update (http 404) perform basic_auth.clearance_for_role(new.role); - update basic_auth.logins set + update basic_auth.users set username = new.username, role = new.role, pass = new.pass, email = new.email, active = coalesce(new.active, old.active, false), @@ -284,16 +284,16 @@ begin elsif tg_op = 'DELETE' then -- no need to check clearance for old.role (see previous case) - delete from basic_auth.logins + delete from basic_auth.users where basic_auth.username = old.username; return null; end if; end $$; -drop trigger if exists update_logins_t on logins; -create trigger update_logins_t +drop trigger if exists update_users_t on users; +create trigger update_users_t instead of insert or update or delete on - logins for each row execute procedure update_logins(); + users for each row execute procedure update_users(); commit; From 22fb13b30a6427559ae6427d3781f4d871e7cb3a Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 8 Nov 2015 12:47:48 -0800 Subject: [PATCH 10/21] Shorten login function to login --- schema-templates/basic-auth.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schema-templates/basic-auth.sql b/schema-templates/basic-auth.sql index fdbe3f4ee..c2cc02c25 100644 --- a/schema-templates/basic-auth.sql +++ b/schema-templates/basic-auth.sql @@ -223,7 +223,7 @@ create type basic_auth.jwt_claims AS (role text, username text); create or replace function -create_auth_token(username text, pass text) returns basic_auth.jwt_claims +login(username text, pass text) returns basic_auth.jwt_claims language plpgsql as $$ declare @@ -234,7 +234,7 @@ begin if _role is null then raise invalid_password using message = 'invalid user or password'; end if; - select _role as role, create_auth_token.username as username into result; + select _role as role, login.username as username into result; return result; end; $$; From c8479e792f33c766066f10427e50a5ccc6548b85 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 8 Nov 2015 22:41:41 -0800 Subject: [PATCH 11/21] Actual blog and row level security! --- schema-templates/{basic-auth.sql => blog.sql} | 84 ++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) rename schema-templates/{basic-auth.sql => blog.sql} (76%) diff --git a/schema-templates/basic-auth.sql b/schema-templates/blog.sql similarity index 76% rename from schema-templates/basic-auth.sql rename to schema-templates/blog.sql index c2cc02c25..74c190f9c 100644 --- a/schema-templates/basic-auth.sql +++ b/schema-templates/blog.sql @@ -239,6 +239,16 @@ begin end; $$; +create or replace function +signup(username text, email text, pass text) returns void + language plpgsql + as $$ +begin + insert into basic_auth.users (username, email, pass, role) values + (signup.username, signup.email, signup.pass, 'author'); +end; +$$; + ------------------------------------------------------------------------------- -- User management @@ -266,8 +276,8 @@ begin insert into basic_auth.users (username, role, pass, email, active, more) values - (new.username, new.role, new.pass, new.email, - coalesce(new.active, false), new.more); + (new.username, coalesce(new.role, 'author'), new.pass, + new.email, coalesce(new.active, false), new.more); return new; elsif tg_op = 'UPDATE' then -- no need to check clearance for old.role because @@ -296,4 +306,74 @@ create trigger update_users_t instead of insert or update or delete on users for each row execute procedure update_users(); +------------------------------------------------------------------------------- +-- Blogging stuff! + +create table if not exists +posts ( + id bigserial not null, + title text not null, + body text not null, + author text not null, + created_at timestamptz not null default current_date, + constraint post_pk primary key (id), + constraint post_user_fk foreign key (author) + references basic_auth.users + on delete restrict on update cascade +); + +create table if not exists +comments ( + id bigserial not null, + body text not null, + author text not null, + post bigint not null, + created_at timestamptz not null default current_date, + constraint comment_pk primary key (id), + constraint comment_user_fk foreign key (author) + references basic_auth.users + on delete restrict on update cascade, + constraint comment_post_fk foreign key (post) references posts + on delete cascade on update cascade +); + +------------------------------------------------------------------------------- +-- Permissions + +--create role anon noinherit; +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,text,text), + signup(text, text, text) + to anon; + +--create role author; +grant author to anon; +grant select, insert, update, delete + on table basic_auth.users, users, posts, comments to author; +grant usage, select on sequence posts_id_seq, comments_id_seq to author; + +grant usage on schema public, basic_auth to anon, author; + +ALTER TABLE posts ENABLE ROW LEVEL SECURITY; +drop policy if exists authors_eigenedit on posts; +create policy authors_eigenedit on posts + for all + using (true) + with check ( + author = current_setting('postgrest.claims.username') + ); + +ALTER TABLE comments ENABLE ROW LEVEL SECURITY; +drop policy if exists authors_eigenedit on comments; +create policy authors_eigenedit on comments + for all + using (true) + with check ( + author = current_setting('postgrest.claims.username') + ); + commit; From 1b28f77854a0e666445dc123d0c39f805ec9dc4e Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 9 Nov 2015 12:26:30 -0800 Subject: [PATCH 12/21] Allow authors to see only themselves in /users --- schema-templates/blog.sql | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index 74c190f9c..1bafa7d07 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -264,7 +264,11 @@ from basic_auth.users as actual, from pg_authid where pg_has_role(current_user, oid, 'member') ) as member_of -where actual.role = member_of.rolname; +where actual.role = member_of.rolname + and ( + actual.role <> 'author' + or username = current_setting('postgrest.claims.username') + ); create or replace function update_users() returns trigger @@ -361,7 +365,6 @@ grant usage on schema public, basic_auth to anon, author; ALTER TABLE posts ENABLE ROW LEVEL SECURITY; drop policy if exists authors_eigenedit on posts; create policy authors_eigenedit on posts - for all using (true) with check ( author = current_setting('postgrest.claims.username') @@ -370,7 +373,6 @@ create policy authors_eigenedit on posts ALTER TABLE comments ENABLE ROW LEVEL SECURITY; drop policy if exists authors_eigenedit on comments; create policy authors_eigenedit on comments - for all using (true) with check ( author = current_setting('postgrest.claims.username') From a508f8df375546d6d699f42b016ce20d3e52cf15 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Tue, 10 Nov 2015 15:12:26 -0800 Subject: [PATCH 13/21] Remove usernames, just use email --- schema-templates/blog.sql | 123 ++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index 1bafa7d07..8f9e8746b 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -38,16 +38,15 @@ $$ LANGUAGE plpgsql; create table if not exists basic_auth.users ( - username text not null, + email text not null, pass text not null, role name not null, - email text not null unique, active boolean not null default false, - more JSON, - constraint user_pkey primary key (username), + -- If you like add more columns, or a json column + constraint user_pkey primary key (email), constraint user_field_length_limits check ( - length(username::text) < 512 AND length(pass) < 512 AND - length(email::text) < 512 AND length(more::text) < 1024) + length(pass) < 512 + AND length(email::text) < 512) ); create or replace function @@ -96,12 +95,11 @@ declare tok text; begin select uuid_generate_v4() into tok; - insert into basic_auth.tokens (token, token_type, username) - values (tok, 'validation', new.username); + insert into basic_auth.tokens (token, token_type, email) + values (tok, 'validation', new.email); perform pg_notify('validate', json_build_object( 'email', new.email, - 'username', new.username, 'token', tok, 'token_type', 'validation' )::text @@ -123,10 +121,10 @@ create table if not exists basic_auth.tokens ( token text unique, token_type text not null, - username text not null, + email text not null, created_at timestamptz not null default current_date, constraint t_pk primary key (token), - constraint t_user_fk foreign key (username) references basic_auth.users + constraint t_user_fk foreign key (email) references basic_auth.users on delete cascade on update cascade ); @@ -134,23 +132,36 @@ basic_auth.tokens ( -- Login helper create or replace function -basic_auth.user_role(username text, pass text) returns text +basic_auth.user_role(email text, pass text) returns text language plpgsql as $$ begin return ( select role from basic_auth.users - where users.username = user_role.username + where users.email = user_role.email and users.pass = crypt(user_role.pass, users.pass) ); end; $$; +create or replace function +basic_auth.current_email() returns text + language plpgsql + as $$ +begin + return current_setting('postgrest.claims.email'); +exception + -- handle unrecognized configuration parameter error + when undefined_object then return ''; +end; +$$; + + ------------------------------------------------------------------------------- -- Public functions (in current schema, not basic_auth) create or replace function -request_password_reset(username text) returns void +request_password_reset(email text) returns void language plpgsql as $$ declare @@ -158,17 +169,14 @@ declare begin delete from basic_auth.tokens where token_type = 'reset' - and tokens.username = request_password_reset.username; + and tokens.email = request_password_reset.email; select uuid_generate_v4() into tok; - insert into basic_auth.tokens (token, token_type, username) - values (tok, 'reset', request_password_reset.username); + insert into basic_auth.tokens (token, token_type, email) + values (tok, 'reset', request_password_reset.email); perform pg_notify('reset', json_build_object( - 'email', (select email - from basic_auth.users - where users.username = request_password_reset.username), - 'username', request_password_reset.username, + 'email', request_password_reset.email, 'token', tok, 'token_type', 'reset' )::text @@ -177,7 +185,7 @@ end; $$; create or replace function -reset_password(username text, token text, pass text) +reset_password(email text, token text, pass text) returns void language plpgsql as $$ @@ -185,14 +193,14 @@ declare tok text; begin if exists(select 1 from basic_auth.tokens - where tokens.username = reset_password.username + 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.username = reset_password.username; + where users.email = reset_password.email; delete from basic_auth.tokens - where tokens.username = reset_password.username + where tokens.email = reset_password.email and tokens.token = reset_password.token and token_type = 'reset'; else @@ -201,17 +209,14 @@ begin end if; delete from basic_auth.tokens where token_type = 'reset' - and tokens.username = reset_password.username; + and tokens.email = reset_password.email; select uuid_generate_v4() into tok; - insert into basic_auth.tokens (token, token_type, username) - values (tok, 'reset', reset_password.username); + insert into basic_auth.tokens (token, token_type, email) + values (tok, 'reset', reset_password.email); perform pg_notify('reset', json_build_object( - 'email', (select email - from basic_auth.users - where users.username = reset_password.username), - 'username', reset_password.username, + 'email', reset_password.email, 'token', tok )::text ); @@ -220,32 +225,34 @@ $$; drop type if exists basic_auth.jwt_claims cascade; create type -basic_auth.jwt_claims AS (role text, username text); +basic_auth.jwt_claims AS (role text, email text); create or replace function -login(username text, pass text) returns basic_auth.jwt_claims +login(email text, pass text) returns basic_auth.jwt_claims language plpgsql as $$ declare _role text; result basic_auth.jwt_claims; begin - select basic_auth.user_role(username, pass) into _role; + 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; - select _role as role, login.username as username into result; + -- TODO; check active flag if you care whether users + -- have validated their emails + select _role as role, login.email as email into result; return result; end; $$; create or replace function -signup(username text, email text, pass text) returns void +signup(email text, pass text) returns void language plpgsql as $$ begin - insert into basic_auth.users (username, email, pass, role) values - (signup.username, signup.email, signup.pass, 'author'); + insert into basic_auth.users (email, pass, role) values + (signup.email, signup.pass, 'author'); end; $$; @@ -253,12 +260,10 @@ $$; -- User management create or replace view users as -select actual.username as username, - actual.role as role, +select actual.role as role, '***'::text as pass, actual.email as email, - actual.active as active, - actual.more as more + actual.active as active from basic_auth.users as actual, (select rolname from pg_authid @@ -267,7 +272,7 @@ from basic_auth.users as actual, where actual.role = member_of.rolname and ( actual.role <> 'author' - or username = current_setting('postgrest.claims.username') + or email = basic_auth.current_email() ); create or replace function @@ -279,9 +284,9 @@ begin perform basic_auth.clearance_for_role(new.role); insert into basic_auth.users - (username, role, pass, email, active, more) values - (new.username, coalesce(new.role, 'author'), new.pass, - new.email, coalesce(new.active, false), new.more); + (role, pass, email, active) values + (coalesce(new.role, 'author'), new.pass, + new.email, coalesce(new.active, false)); return new; elsif tg_op = 'UPDATE' then -- no need to check clearance for old.role because @@ -289,17 +294,17 @@ begin perform basic_auth.clearance_for_role(new.role); update basic_auth.users set - username = new.username, role = new.role, - pass = new.pass, email = new.email, - active = coalesce(new.active, old.active, false), - more = new.more - where username = old.username; + email = new.email, + role = new.role, + pass = new.pass, + active = coalesce(new.active, old.active, 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.username = old.username; + where basic_auth.email = old.email; return null; end if; end @@ -344,20 +349,20 @@ comments ( ------------------------------------------------------------------------------- -- Permissions ---create role anon noinherit; 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,text,text), - signup(text, text, text) + signup(text, text) to anon; ---create role author; grant author to anon; grant select, insert, update, delete - on table basic_auth.users, users, posts, comments to author; + 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; grant usage on schema public, basic_auth to anon, author; @@ -367,7 +372,7 @@ drop policy if exists authors_eigenedit on posts; create policy authors_eigenedit on posts using (true) with check ( - author = current_setting('postgrest.claims.username') + author = basic_auth.current_email() ); ALTER TABLE comments ENABLE ROW LEVEL SECURITY; @@ -375,7 +380,7 @@ drop policy if exists authors_eigenedit on comments; create policy authors_eigenedit on comments using (true) with check ( - author = current_setting('postgrest.claims.username') + author = basic_auth.current_email() ); commit; From 86d992c21f51ca0d926730873dd0118e7cc21dd5 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Tue, 10 Nov 2015 15:12:57 -0800 Subject: [PATCH 14/21] Roles --- schema-templates/blog.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index 8f9e8746b..3958238ed 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -3,6 +3,9 @@ begin; +create role anon noinherit; +create role author; + create extension if not exists pgcrypto; create extension if not exists "uuid-ossp"; From bda5f0a1766d39bbd2b2528502a9a16a2d045046 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Tue, 10 Nov 2015 19:48:48 -0800 Subject: [PATCH 15/21] Match trigger names with their functions --- schema-templates/blog.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index 3958238ed..bf9f1cbcd 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -84,8 +84,8 @@ begin end $$; -drop trigger if exists protect_passwords on basic_auth.users; -create trigger protect_passwords +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(); @@ -111,8 +111,8 @@ begin end $$; -drop trigger if exists send_validation_t on basic_auth.users; -create trigger send_validation_t +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(); @@ -313,8 +313,8 @@ begin end $$; -drop trigger if exists update_users_t on users; -create trigger update_users_t +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(); From 91f15aa0e3b81e4172b5dc000dee34d5fc48bf42 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Tue, 10 Nov 2015 21:00:13 -0800 Subject: [PATCH 16/21] Use uuid type for token and name for role --- schema-templates/blog.sql | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index bf9f1cbcd..be3896ae9 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -95,7 +95,7 @@ basic_auth.send_validation() returns trigger language plpgsql as $$ declare - tok text; + tok uuid; begin select uuid_generate_v4() into tok; insert into basic_auth.tokens (token, token_type, email) @@ -122,7 +122,7 @@ create trigger send_validation create table if not exists basic_auth.tokens ( - token text unique, + token uuid unique, token_type text not null, email text not null, created_at timestamptz not null default current_date, @@ -135,7 +135,7 @@ basic_auth.tokens ( -- Login helper create or replace function -basic_auth.user_role(email text, pass text) returns text +basic_auth.user_role(email text, pass text) returns name language plpgsql as $$ begin @@ -168,7 +168,7 @@ request_password_reset(email text) returns void language plpgsql as $$ declare - tok text; + tok uuid; begin delete from basic_auth.tokens where token_type = 'reset' @@ -188,12 +188,12 @@ end; $$; create or replace function -reset_password(email text, token text, pass text) +reset_password(email text, token uuid, pass text) returns void language plpgsql as $$ declare - tok text; + tok uuid; begin if exists(select 1 from basic_auth.tokens where tokens.email = reset_password.email @@ -235,7 +235,7 @@ login(email text, pass text) returns basic_auth.jwt_claims language plpgsql as $$ declare - _role text; + _role name; result basic_auth.jwt_claims; begin select basic_auth.user_role(email, pass) into _role; @@ -357,7 +357,7 @@ 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,text,text), + reset_password(text,uuid,text), signup(text, text) to anon; From 807e4b7787d4fc3c3aff54f757718e25e278fd09 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Tue, 10 Nov 2015 21:07:14 -0800 Subject: [PATCH 17/21] Turn signup() into a plain sql function --- schema-templates/blog.sql | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index be3896ae9..ffdf18797 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -251,13 +251,10 @@ $$; create or replace function signup(email text, pass text) returns void - language plpgsql - as $$ -begin +as $$ insert into basic_auth.users (email, pass, role) values (signup.email, signup.pass, 'author'); -end; -$$; +$$ language sql; ------------------------------------------------------------------------------- -- User management From ad316841f248a79d32dcce6bc2573622dd15ccd0 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Tue, 10 Nov 2015 21:09:56 -0800 Subject: [PATCH 18/21] Restrict possible values of token_type --- schema-templates/blog.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index ffdf18797..82dab8a73 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -120,10 +120,12 @@ create trigger send_validation ------------------------------------------------------------------------------- -- Email Validation and Password Reset +create type token_type_enum as enum ('validation', 'reset'); + create table if not exists basic_auth.tokens ( token uuid unique, - token_type text not null, + token_type token_type_enum not null, email text not null, created_at timestamptz not null default current_date, constraint t_pk primary key (token), From 80928535b00fcf871a627e276538c37469b64691 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Wed, 11 Nov 2015 08:15:06 -0800 Subject: [PATCH 19/21] Active flag is more accurately called verified --- schema-templates/blog.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index 82dab8a73..65cde81da 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -44,7 +44,7 @@ basic_auth.users ( email text not null, pass text not null, role name not null, - active boolean not null default false, + verified boolean not null default false, -- If you like add more columns, or a json column constraint user_pkey primary key (email), constraint user_field_length_limits check ( @@ -244,7 +244,7 @@ begin if _role is null then raise invalid_password using message = 'invalid user or password'; end if; - -- TODO; check active flag if you care whether users + -- TODO; check verified flag if you care whether users -- have validated their emails select _role as role, login.email as email into result; return result; @@ -265,7 +265,7 @@ create or replace view users as select actual.role as role, '***'::text as pass, actual.email as email, - actual.active as active + actual.verified as verified from basic_auth.users as actual, (select rolname from pg_authid @@ -286,9 +286,9 @@ begin perform basic_auth.clearance_for_role(new.role); insert into basic_auth.users - (role, pass, email, active) values + (role, pass, email, verified) values (coalesce(new.role, 'author'), new.pass, - new.email, coalesce(new.active, false)); + new.email, coalesce(new.verified, false)); return new; elsif tg_op = 'UPDATE' then -- no need to check clearance for old.role because @@ -299,7 +299,7 @@ begin email = new.email, role = new.role, pass = new.pass, - active = coalesce(new.active, old.active, false) + verified = coalesce(new.verified, old.verified, false) where email = old.email; return new; elsif tg_op = 'DELETE' then From 71e6d0414d8a353c2967098c3611c2bb68760e59 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 13 Nov 2015 12:59:45 -0800 Subject: [PATCH 20/21] Inline constraints --- schema-templates/blog.sql | 52 ++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index 65cde81da..4a23794bf 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -3,6 +3,8 @@ begin; +-- comment out the role creation statements if +-- you want to run this script more than once create role anon noinherit; create role author; @@ -41,15 +43,11 @@ $$ LANGUAGE plpgsql; create table if not exists basic_auth.users ( - email text not null, - pass text not null, - role name not null, - verified boolean not null default false, + email text primary key, + 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 - constraint user_pkey primary key (email), - constraint user_field_length_limits check ( - length(pass) < 512 - AND length(email::text) < 512) ); create or replace function @@ -120,17 +118,16 @@ create trigger send_validation ------------------------------------------------------------------------------- -- Email Validation and Password Reset +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 unique, + token uuid primary key, token_type token_type_enum not null, - email text not null, - created_at timestamptz not null default current_date, - constraint t_pk primary key (token), - constraint t_user_fk foreign key (email) references basic_auth.users - on delete cascade on update cascade + email text not null references basic_auth.users (email) + on delete cascade on update cascade, + created_at timestamptz not null default current_date ); ------------------------------------------------------------------------------- @@ -322,30 +319,23 @@ create trigger update_users create table if not exists posts ( - id bigserial not null, + id bigserial primary key, title text not null, body text not null, - author text not null, - created_at timestamptz not null default current_date, - constraint post_pk primary key (id), - constraint post_user_fk foreign key (author) - references basic_auth.users - on delete restrict on update cascade + author text not null references basic_auth.users (email) + on delete restrict on update cascade, + created_at timestamptz not null default current_date ); create table if not exists comments ( - id bigserial not null, + id bigserial primary key, body text not null, - author text not null, - post bigint not null, - created_at timestamptz not null default current_date, - constraint comment_pk primary key (id), - constraint comment_user_fk foreign key (author) - references basic_auth.users - on delete restrict on update cascade, - constraint comment_post_fk foreign key (post) references posts - on delete cascade on update cascade + author text not null references basic_auth.users (email) + on delete restrict on update cascade, + post bigint not null references posts (id) + on delete cascade on update cascade, + created_at timestamptz not null default current_date ); ------------------------------------------------------------------------------- From 98caf9e091b190c4c02ad60e7044d910634e2a6e Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 13 Nov 2015 13:16:40 -0800 Subject: [PATCH 21/21] Light validation on email column --- schema-templates/blog.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema-templates/blog.sql b/schema-templates/blog.sql index 4a23794bf..0788a6280 100644 --- a/schema-templates/blog.sql +++ b/schema-templates/blog.sql @@ -43,7 +43,7 @@ $$ LANGUAGE plpgsql; create table if not exists basic_auth.users ( - email text primary key, + 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