From 5d0861824abc9d7899cf7a8aaa7e8124e1aadd1b Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Sat, 22 Jul 2023 01:59:12 +0200 Subject: [PATCH] Add authentication relations, views, and functions for PostgreSQL Most of them are exactly the same as we use for Numerus, but with the main application schema changed to camper. Closes #1 --- deploy/available_languages.sql | 13 +++ deploy/build_cookie.sql | 25 +++++ deploy/change_password.sql | 31 ++++++ deploy/check_cookie.sql | 48 +++++++++ deploy/current_user_cookie.sql | 24 +++++ deploy/current_user_email.sql | 24 +++++ deploy/email.sql | 15 +++ deploy/encrypt_password.sql | 33 +++++++ deploy/ensure_role_exists.sql | 32 ++++++ deploy/extension_citext.sql | 8 ++ deploy/extension_pgcrypto.sql | 8 ++ deploy/language.sql | 32 ++++++ deploy/login.sql | 61 ++++++++++++ deploy/login_attempt.sql | 16 +++ deploy/logout.sql | 34 +++++++ deploy/roles.sql | 28 ++++++ deploy/schema_auth.sql | 10 ++ deploy/schema_camper.sql | 14 +++ deploy/schema_public.sql | 14 +++ deploy/set_cookie.sql | 23 +++++ deploy/user.sql | 23 +++++ deploy/user_profile.sql | 71 ++++++++++++++ revert/available_languages.sql | 8 ++ revert/build_cookie.sql | 7 ++ revert/change_password.sql | 7 ++ revert/check_cookie.sql | 7 ++ revert/current_user_cookie.sql | 7 ++ revert/current_user_email.sql | 7 ++ revert/email.sql | 7 ++ revert/encrypt_password.sql | 8 ++ revert/ensure_role_exists.sql | 8 ++ revert/extension_citext.sql | 7 ++ revert/extension_pgcrypto.sql | 7 ++ revert/language.sql | 7 ++ revert/login.sql | 7 ++ revert/login_attempt.sql | 7 ++ revert/logout.sql | 7 ++ revert/roles.sql | 10 ++ revert/schema_auth.sql | 7 ++ revert/schema_camper.sql | 7 ++ revert/schema_public.sql | 13 +++ revert/set_cookie.sql | 7 ++ revert/user.sql | 7 ++ revert/user_profile.sql | 9 ++ sqitch.plan | 22 +++++ test/build_cookie.sql | 72 ++++++++++++++ test/change_password.sql | 57 +++++++++++ test/check_cookie.sql | 113 ++++++++++++++++++++++ test/current_user_cookie.sql | 70 ++++++++++++++ test/current_user_email.sql | 63 ++++++++++++ test/email.sql | 33 +++++++ test/encrypt_password.sql | 40 ++++++++ test/ensure_role_exists.sql | 54 +++++++++++ test/extensions.sql | 20 ++++ test/language.sql | 48 +++++++++ test/login.sql | 111 +++++++++++++++++++++ test/login_attempt.sql | 58 +++++++++++ test/logout.sql | 88 +++++++++++++++++ test/roles.sql | 23 +++++ test/schemas.sql | 44 +++++++++ test/set_cookie.sql | 76 +++++++++++++++ test/user.sql | 84 ++++++++++++++++ test/user_profile.sql | 171 +++++++++++++++++++++++++++++++++ verify/available_languages.sql | 31 ++++++ verify/build_cookie.sql | 7 ++ verify/change_password.sql | 7 ++ verify/check_cookie.sql | 7 ++ verify/current_user_cookie.sql | 7 ++ verify/current_user_email.sql | 7 ++ verify/email.sql | 7 ++ verify/encrypt_password.sql | 22 +++++ verify/ensure_role_exists.sql | 22 +++++ verify/extension_citext.sql | 10 ++ verify/extension_pgcrypto.sql | 10 ++ verify/language.sql | 13 +++ verify/login.sql | 7 ++ verify/login_attempt.sql | 13 +++ verify/logout.sql | 7 ++ verify/roles.sql | 10 ++ verify/schema_auth.sql | 7 ++ verify/schema_camper.sql | 7 ++ verify/schema_public.sql | 7 ++ verify/set_cookie.sql | 7 ++ verify/user.sql | 17 ++++ verify/user_profile.sql | 45 +++++++++ 85 files changed, 2279 insertions(+) create mode 100644 deploy/available_languages.sql create mode 100644 deploy/build_cookie.sql create mode 100644 deploy/change_password.sql create mode 100644 deploy/check_cookie.sql create mode 100644 deploy/current_user_cookie.sql create mode 100644 deploy/current_user_email.sql create mode 100644 deploy/email.sql create mode 100644 deploy/encrypt_password.sql create mode 100644 deploy/ensure_role_exists.sql create mode 100644 deploy/extension_citext.sql create mode 100644 deploy/extension_pgcrypto.sql create mode 100644 deploy/language.sql create mode 100644 deploy/login.sql create mode 100644 deploy/login_attempt.sql create mode 100644 deploy/logout.sql create mode 100644 deploy/roles.sql create mode 100644 deploy/schema_auth.sql create mode 100644 deploy/schema_camper.sql create mode 100644 deploy/schema_public.sql create mode 100644 deploy/set_cookie.sql create mode 100644 deploy/user.sql create mode 100644 deploy/user_profile.sql create mode 100644 revert/available_languages.sql create mode 100644 revert/build_cookie.sql create mode 100644 revert/change_password.sql create mode 100644 revert/check_cookie.sql create mode 100644 revert/current_user_cookie.sql create mode 100644 revert/current_user_email.sql create mode 100644 revert/email.sql create mode 100644 revert/encrypt_password.sql create mode 100644 revert/ensure_role_exists.sql create mode 100644 revert/extension_citext.sql create mode 100644 revert/extension_pgcrypto.sql create mode 100644 revert/language.sql create mode 100644 revert/login.sql create mode 100644 revert/login_attempt.sql create mode 100644 revert/logout.sql create mode 100644 revert/roles.sql create mode 100644 revert/schema_auth.sql create mode 100644 revert/schema_camper.sql create mode 100644 revert/schema_public.sql create mode 100644 revert/set_cookie.sql create mode 100644 revert/user.sql create mode 100644 revert/user_profile.sql create mode 100644 test/build_cookie.sql create mode 100644 test/change_password.sql create mode 100644 test/check_cookie.sql create mode 100644 test/current_user_cookie.sql create mode 100644 test/current_user_email.sql create mode 100644 test/email.sql create mode 100644 test/encrypt_password.sql create mode 100644 test/ensure_role_exists.sql create mode 100644 test/extensions.sql create mode 100644 test/language.sql create mode 100644 test/login.sql create mode 100644 test/login_attempt.sql create mode 100644 test/logout.sql create mode 100644 test/roles.sql create mode 100644 test/schemas.sql create mode 100644 test/set_cookie.sql create mode 100644 test/user.sql create mode 100644 test/user_profile.sql create mode 100644 verify/available_languages.sql create mode 100644 verify/build_cookie.sql create mode 100644 verify/change_password.sql create mode 100644 verify/check_cookie.sql create mode 100644 verify/current_user_cookie.sql create mode 100644 verify/current_user_email.sql create mode 100644 verify/email.sql create mode 100644 verify/encrypt_password.sql create mode 100644 verify/ensure_role_exists.sql create mode 100644 verify/extension_citext.sql create mode 100644 verify/extension_pgcrypto.sql create mode 100644 verify/language.sql create mode 100644 verify/login.sql create mode 100644 verify/login_attempt.sql create mode 100644 verify/logout.sql create mode 100644 verify/roles.sql create mode 100644 verify/schema_auth.sql create mode 100644 verify/schema_camper.sql create mode 100644 verify/schema_public.sql create mode 100644 verify/set_cookie.sql create mode 100644 verify/user.sql create mode 100644 verify/user_profile.sql diff --git a/deploy/available_languages.sql b/deploy/available_languages.sql new file mode 100644 index 0000000..d8ea088 --- /dev/null +++ b/deploy/available_languages.sql @@ -0,0 +1,13 @@ +-- Deploy camper:available_languages to pg +-- requires: schema_public +-- requires: language + +begin; + +insert into public.language (lang_tag, name, endonym, selectable, currency_pattern) +values ('und', 'Undefined', 'Undefined', false, '%[3]s%.[1]*[2]f') + , ('ca', 'Catalan', 'català', true, '%.[1]*[2]f %[3]s') + , ('es', 'Spanish', 'español', true, '%.[1]*[2]f %[3]s') +; + +commit; diff --git a/deploy/build_cookie.sql b/deploy/build_cookie.sql new file mode 100644 index 0000000..1d0035c --- /dev/null +++ b/deploy/build_cookie.sql @@ -0,0 +1,25 @@ +-- Deploy camper:build_cookie to pg +-- requires: roles +-- requires: schema_camper +-- requires: current_user_email +-- requires: current_user_cookie + +begin; + +set search_path to camper, public; + +create or replace function build_cookie(user_email email default null, user_cookie text default null) returns text as +$$ +select coalesce(user_cookie, current_user_cookie()) || '/' || coalesce(user_email, current_user_email()); +$$ +language sql +stable; + +revoke execute on function build_cookie(email, text) from public; +grant execute on function build_cookie(email, text) to employee; +grant execute on function build_cookie(email, text) to admin; + +comment on function build_cookie(email, text) is +'Build the cookie to send to the user’s browser, either for the given values or for the current user.'; + +commit; diff --git a/deploy/change_password.sql b/deploy/change_password.sql new file mode 100644 index 0000000..8b1e072 --- /dev/null +++ b/deploy/change_password.sql @@ -0,0 +1,31 @@ +-- Deploy camper:change_password to pg +-- requires: roles +-- requires: schema_auth +-- requires: schema_camper +-- requires: user + +begin; + +set search_path to camper, auth, public; + +create or replace function change_password(new_password text) returns void as +$$ +update "user" +set password = new_password +where email = current_user_email() + and cookie = current_user_cookie() + and cookie_expires_at > current_timestamp + and length(cookie) > 30 +$$ +language sql +security definer +set search_path to auth, camper, pg_temp; + +revoke execute on function change_password(text) from public; +grant execute on function change_password(text) to employee; +grant execute on function change_password(text) to admin; + +comment on function change_password(text) is +'Changes the password for the current app user'; + +commit; diff --git a/deploy/check_cookie.sql b/deploy/check_cookie.sql new file mode 100644 index 0000000..522ba45 --- /dev/null +++ b/deploy/check_cookie.sql @@ -0,0 +1,48 @@ +-- Deploy camper:check_cookie to pg +-- requires: roles +-- requires: schema_public +-- requires: schema_auth +-- requires: user + +begin; + +set search_path to public, auth; + +create or replace function check_cookie(input_cookie text) returns name as +$$ +declare + uid text; + user_email text; + user_role name; + user_cookie text; +begin + select user_id::text, email::text, role, cookie + into uid, user_email, user_role, user_cookie + from "user" + where email = split_part(input_cookie, '/', 2) + and cookie_expires_at > current_timestamp + and length(password) > 0 + and cookie = split_part(input_cookie, '/', 1); + if user_role is null then + uid := '0'; + user_email := ''; + user_cookie := ''; + user_role := 'guest'::name; + end if; + perform set_config('request.user.email', user_email, false); + perform set_config('request.user.cookie', user_cookie, false); + return user_role; +end; +$$ +language plpgsql +security definer +stable +set search_path = auth, pg_temp; + +comment on function check_cookie(text) is +'Checks whether a given cookie is for a valid users, returning their role, and setting current_user_email and current_user_cookie'; + +revoke execute on function check_cookie(text) from public; +grant execute on function check_cookie(text) to authenticator; + +commit; diff --git a/deploy/current_user_cookie.sql b/deploy/current_user_cookie.sql new file mode 100644 index 0000000..9d214e3 --- /dev/null +++ b/deploy/current_user_cookie.sql @@ -0,0 +1,24 @@ +-- Deploy camper:current_user_cookie to pg +-- requires: roles +-- requires: schema_camper + +begin; + +set search_path to camper; + +create or replace function current_user_cookie() returns text as +$$ +select current_setting('request.user.cookie', true); +$$ +language sql +stable; + +comment on function current_user_cookie() is +'Returns the cookie of the current Camper user'; + +revoke execute on function current_user_cookie() from public; +grant execute on function current_user_cookie() to guest; +grant execute on function current_user_cookie() to employee; +grant execute on function current_user_cookie() to admin; + +commit; diff --git a/deploy/current_user_email.sql b/deploy/current_user_email.sql new file mode 100644 index 0000000..2a49ebc --- /dev/null +++ b/deploy/current_user_email.sql @@ -0,0 +1,24 @@ +-- Deploy camper:current_user_email to pg +-- requires: roles +-- requires: schema_camper + +begin; + +set search_path to camper; + +create or replace function current_user_email() returns text as +$$ +select current_setting('request.user.email', true); +$$ +language sql +stable; + +comment on function current_user_email() is +'Returns the email of the current Camper user'; + +revoke execute on function current_user_email() from public; +grant execute on function current_user_email() to guest; +grant execute on function current_user_email() to employee; +grant execute on function current_user_email() to admin; + +commit; diff --git a/deploy/email.sql b/deploy/email.sql new file mode 100644 index 0000000..a40e4d5 --- /dev/null +++ b/deploy/email.sql @@ -0,0 +1,15 @@ +-- Deploy camper:email to pg +-- requires: schema_camper +-- requires: extension_citext + +begin; + +set search_path to camper, public; + +-- regular expression from https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address +create domain email as citext +check ( value ~ '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$' ); + +comment on domain email is 'A valid email address according to HTML5 spec.'; + +commit; diff --git a/deploy/encrypt_password.sql b/deploy/encrypt_password.sql new file mode 100644 index 0000000..3c4250f --- /dev/null +++ b/deploy/encrypt_password.sql @@ -0,0 +1,33 @@ +-- Deploy camper:encrypt_password to pg +-- requires: schema_auth +-- requires: user +-- requires: extension_pgcrypto + +begin; + +set search_path to auth, public; + +create or replace function encrypt_password() returns trigger as +$$ +begin + if tg_op = 'INSERT' or new.password <> old.password then + new.password = crypt(new.password, gen_salt('bf')); + end if; + return new; +end; +$$ +language plpgsql +set search_path = auth, pg_temp; + +comment on function encrypt_password() is +'Encrypts and salts the input password with the blowfish encryption algorithm'; + +revoke execute on function encrypt_password() from public; + +create trigger encrypt_password +before insert or update +on "user" +for each row +execute procedure encrypt_password(); + +commit; diff --git a/deploy/ensure_role_exists.sql b/deploy/ensure_role_exists.sql new file mode 100644 index 0000000..b8bbe1e --- /dev/null +++ b/deploy/ensure_role_exists.sql @@ -0,0 +1,32 @@ +-- Deploy camper:ensure_role_exists to pg +-- requires: schema_auth +-- requires: user + +begin; + +set search_path to auth, public; + +create or replace function ensure_role_exists() returns trigger as +$$ +begin + if not exists (select 1 from pg_roles where rolname = new.role) then + raise foreign_key_violation using message = 'role not found: ' || new.role; + end if; + return new; +end; +$$ +language plpgsql; + +comment on function ensure_role_exists() is +'Makes sure that a role given to a user is a valid, existing role in the cluster.'; + +revoke execute on function ensure_role_exists() from public; + +create trigger ensure_role_exists +after insert or update +on "user" +for each row +execute procedure ensure_role_exists(); + + +commit; diff --git a/deploy/extension_citext.sql b/deploy/extension_citext.sql new file mode 100644 index 0000000..6a9b88f --- /dev/null +++ b/deploy/extension_citext.sql @@ -0,0 +1,8 @@ +-- Deploy camper:extension_citext to pg +-- requires: schema_public + +begin; + +create extension if not exists citext; + +commit; diff --git a/deploy/extension_pgcrypto.sql b/deploy/extension_pgcrypto.sql new file mode 100644 index 0000000..89aaacf --- /dev/null +++ b/deploy/extension_pgcrypto.sql @@ -0,0 +1,8 @@ +-- Deploy camper:extension_pgcrypto to pg +-- requires: schema_auth + +begin; + +create extension if not exists pgcrypto with schema auth; + +commit; diff --git a/deploy/language.sql b/deploy/language.sql new file mode 100644 index 0000000..8bb76c8 --- /dev/null +++ b/deploy/language.sql @@ -0,0 +1,32 @@ +-- Deploy camper:language to pg +-- requires: roles +-- requires: schema_public + +begin; + +set search_path to public; + +create table language +( + lang_tag text primary key check (length(lang_tag) < 36), -- RFC5646 recommends 35 at least + name text not null, + endonym text not null, + selectable boolean not null, + currency_pattern text not null +); + +grant select on table language to guest; +grant select on table language to employee; +grant select on table language to admin; +grant select on table language to authenticator; + +comment on table language is +'Languages/locales available in Camper'; + +comment on column language.lang_tag is +'BCP-47 language tag'; + +comment on column language.selectable is +'Whether the language should be a option in a user-facing select control.'; + +commit; diff --git a/deploy/login.sql b/deploy/login.sql new file mode 100644 index 0000000..99c18b7 --- /dev/null +++ b/deploy/login.sql @@ -0,0 +1,61 @@ +-- Deploy camper:login to pg +-- requires: roles +-- requires: schema_auth +-- requires: schema_camper +-- requires: extension_pgcrypto +-- requires: email +-- requires: user +-- requires: login_attempt +-- requires: build_cookie + +begin; + +set search_path to camper, auth; + +create or replace function login(email email, password text, ip_address inet default null) returns text as +$$ +declare + user_cookie text; +begin + if not exists (select * + from "user" + where "user".email = login.email + and "user".password = crypt(login.password, "user".password)) then + insert into login_attempt (user_name, ip_address, success) + values (login.email, login.ip_address, false); + return ''; + end if; + + select cookie + into user_cookie + from "user" + where "user".email = login.email + and cookie_expires_at > current_timestamp + and length(cookie) > 30; + + if user_cookie is null then + select encode(gen_random_bytes(25), 'hex') into user_cookie; + end if; + + update "user" + set cookie = user_cookie + , cookie_expires_at = current_timestamp + interval '1 year' + where "user".email = login.email; + + insert into login_attempt (user_name, ip_address, success) + values (login.email, login.ip_address, true); + + return build_cookie(email, user_cookie); +end; +$$ +language plpgsql +security definer +set search_path = auth, camper, pg_temp; + +comment on function login(email, text, inet) is +'Tries to logs a user in, recording the attempt, and returns the cookie to send back to the user if the authentication was successfull.'; + +revoke execute on function login(email, text, inet) from public; +grant execute on function login(email, text, inet) to guest; + +commit; diff --git a/deploy/login_attempt.sql b/deploy/login_attempt.sql new file mode 100644 index 0000000..80bd4f4 --- /dev/null +++ b/deploy/login_attempt.sql @@ -0,0 +1,16 @@ +-- Deploy camper:login_attempt to pg +-- requires: schema_auth + +begin; + +set search_path to auth, public; + +create table login_attempt ( + attempt_id bigserial primary key, + user_name text not null, + ip_address inet, -- nullable just in case we login from outside the web application + success boolean not null, + attempted_at timestamptz not null default current_timestamp +); + +commit; diff --git a/deploy/logout.sql b/deploy/logout.sql new file mode 100644 index 0000000..3bd7984 --- /dev/null +++ b/deploy/logout.sql @@ -0,0 +1,34 @@ +-- Deploy camper:logout to pg +-- requires: roles +-- requires: schema_auth +-- requires: schema_camper +-- requires: current_user_email +-- requires: current_user_cookie +-- requires: user + +begin; + +set search_path to camper, auth, public; + +create or replace function logout() returns void as +$$ +update "user" +set cookie = default + , cookie_expires_at = default +where email = current_user_email() + and cookie = current_user_cookie() + and cookie_expires_at > current_timestamp + and length(cookie) > 30 +$$ +language sql +security definer +set search_path to auth, camper, pg_temp; + +comment on function logout() is +'Removes the cookie and its expiry data from the current user, as returned by current_user_email and current_user_cookie'; + +revoke execute on function logout() from public; +grant execute on function logout() to employee; +grant execute on function logout() to admin; + +commit; diff --git a/deploy/roles.sql b/deploy/roles.sql new file mode 100644 index 0000000..940a764 --- /dev/null +++ b/deploy/roles.sql @@ -0,0 +1,28 @@ +-- Deploy camper:roles to pg + +begin; + +do +$$ + declare + role name; + roles name[] := array ['guest', 'employee', 'admin', 'authenticator']; + begin + foreach role in array roles + loop + begin + execute 'create role ' || role || ' noinherit nologin'; + exception + when duplicate_object then + raise notice '%, skipping', sqlerrm using errcode = sqlstate; + end; + end loop; + end +$$; + +grant guest to authenticator; +grant employee to authenticator; +grant admin to authenticator; + + +commit; diff --git a/deploy/schema_auth.sql b/deploy/schema_auth.sql new file mode 100644 index 0000000..afbb4f5 --- /dev/null +++ b/deploy/schema_auth.sql @@ -0,0 +1,10 @@ +-- Deploy camper:schema_auth to pg +-- requires: roles + +begin; + +create schema auth; + +comment on schema auth is 'To keep user’s passwords safe.'; + +commit; diff --git a/deploy/schema_camper.sql b/deploy/schema_camper.sql new file mode 100644 index 0000000..d013c73 --- /dev/null +++ b/deploy/schema_camper.sql @@ -0,0 +1,14 @@ +-- Deploy camper:schema_camper to pg +-- requires: roles + +begin; + +create schema camper; + +comment on schema camper is 'The main application schema'; + +grant usage on schema camper to guest; +grant usage on schema camper to employee; +grant usage on schema camper to admin; + +commit; diff --git a/deploy/schema_public.sql b/deploy/schema_public.sql new file mode 100644 index 0000000..e0a3f13 --- /dev/null +++ b/deploy/schema_public.sql @@ -0,0 +1,14 @@ +-- Deploy camper:schema_public to pg +-- requires: roles + +begin; + +revoke create on schema public from public; +revoke usage on schema public from public; + +grant usage on schema public to authenticator; +grant usage on schema public to guest; +grant usage on schema public to employee; +grant usage on schema public to admin; + +commit; diff --git a/deploy/set_cookie.sql b/deploy/set_cookie.sql new file mode 100644 index 0000000..70a25d9 --- /dev/null +++ b/deploy/set_cookie.sql @@ -0,0 +1,23 @@ +-- Deploy camper:set_cookie to pg +-- requires: roles +-- requires: schema_public +-- requires: check_cookie + +begin; + +set search_path to public; + +create or replace function set_cookie(input_cookie text) returns void as +$$ +select set_config('role', check_cookie(input_cookie), false); +$$ +language sql +stable; + +comment on function set_cookie(text) is +'Sets the user information for the cookie and switches to its role'; + +revoke execute on function set_cookie(text) from public; +grant execute on function set_cookie(text) to authenticator; + +commit; diff --git a/deploy/user.sql b/deploy/user.sql new file mode 100644 index 0000000..a04b811 --- /dev/null +++ b/deploy/user.sql @@ -0,0 +1,23 @@ +-- Deploy camper:user to pg +-- requires: roles +-- requires: schema_auth +-- requires: email +-- requires: language + +begin; + +set search_path to auth, camper, public; + +create table "user" ( + user_id serial primary key, + email email not null unique, + name text not null, + password text not null check (length(password) < 512), + role name not null check (length(role) < 512), + lang_tag text not null default 'und' references language, + cookie text not null default '', + cookie_expires_at timestamptz not null default '-infinity'::timestamp, + created_at timestamptz not null default current_timestamp +); + +commit; diff --git a/deploy/user_profile.sql b/deploy/user_profile.sql new file mode 100644 index 0000000..ea2b3c4 --- /dev/null +++ b/deploy/user_profile.sql @@ -0,0 +1,71 @@ +-- Deploy camper:user_profile to pg +-- requires: roles +-- requires: schema_camper +-- requires: user +-- requires: current_user_email +-- requires: current_user_cookie + +begin; + +set search_path to camper, public; + +create or replace view user_profile with (security_barrier) as + select user_id + , email + , name + , role + , lang_tag + , left(cookie, 10) as csrf_token + from auth."user" + where email = current_user_email() + and cookie = current_user_cookie() + and cookie_expires_at > current_timestamp + and length(cookie) > 30 +union all + select 0 + , null::email + , '' + , 'guest'::name + , 'und' + , '' + where not exists (select 1 + from auth."user" + where email = current_user_email() + and cookie = current_user_cookie() + and cookie_expires_at > current_timestamp + and length(cookie) > 30) +; + +grant select on table user_profile to guest; +grant select, update (email, name, lang_tag) on table user_profile to employee; +grant select, update (email, name, lang_tag) on table user_profile to admin; + +create or replace function update_user_profile() returns trigger as +$$ +begin + update auth."user" + set email = new.email + , name = new.name + , lang_tag = new.lang_tag + where email = current_user_email() + and cookie = current_user_cookie() + and cookie_expires_at > current_timestamp + and length(cookie) > 30 + ; + + perform set_config('request.user.email', new.email, false); + + return new; +end; +$$ +language plpgsql +security definer +set search_path to auth, camper, pg_temp; + +create trigger update_user_profile +instead of update +on user_profile +for each row +execute procedure update_user_profile(); + +commit; diff --git a/revert/available_languages.sql b/revert/available_languages.sql new file mode 100644 index 0000000..037d231 --- /dev/null +++ b/revert/available_languages.sql @@ -0,0 +1,8 @@ +-- Revert camper:available_languages from pg + +begin; + +delete +from public.language; + +commit; diff --git a/revert/build_cookie.sql b/revert/build_cookie.sql new file mode 100644 index 0000000..c43b5b1 --- /dev/null +++ b/revert/build_cookie.sql @@ -0,0 +1,7 @@ +-- Revert camper:build_cookie from pg + +begin; + +drop function if exists camper.build_cookie(camper.email, text); + +commit; diff --git a/revert/change_password.sql b/revert/change_password.sql new file mode 100644 index 0000000..7ab90dd --- /dev/null +++ b/revert/change_password.sql @@ -0,0 +1,7 @@ +-- Revert camper:change_password from pg + +begin; + +drop function if exists camper.change_password(text); + +commit; diff --git a/revert/check_cookie.sql b/revert/check_cookie.sql new file mode 100644 index 0000000..e7e946c --- /dev/null +++ b/revert/check_cookie.sql @@ -0,0 +1,7 @@ +-- Revert camper:check_cookie from pg + +begin; + +drop function if exists public.check_cookie(text); + +commit; diff --git a/revert/current_user_cookie.sql b/revert/current_user_cookie.sql new file mode 100644 index 0000000..064680d --- /dev/null +++ b/revert/current_user_cookie.sql @@ -0,0 +1,7 @@ +-- Revert camper:current_user_cookie from pg + +begin; + +drop function if exists camper.current_user_cookie(); + +commit; diff --git a/revert/current_user_email.sql b/revert/current_user_email.sql new file mode 100644 index 0000000..ef8d95b --- /dev/null +++ b/revert/current_user_email.sql @@ -0,0 +1,7 @@ +-- Revert camper:current_user_email from pg + +begin; + +drop function if exists camper.current_user_email(); + +commit; diff --git a/revert/email.sql b/revert/email.sql new file mode 100644 index 0000000..12a17e6 --- /dev/null +++ b/revert/email.sql @@ -0,0 +1,7 @@ +-- Revert camper:email from pg + +begin; + +drop domain if exists camper.email; + +commit; diff --git a/revert/encrypt_password.sql b/revert/encrypt_password.sql new file mode 100644 index 0000000..c194bf7 --- /dev/null +++ b/revert/encrypt_password.sql @@ -0,0 +1,8 @@ +-- Revert camper:encrypt_password from pg + +begin; + +drop trigger if exists encrypt_password on auth."user"; +drop function if exists auth.encrypt_password(); + +commit; diff --git a/revert/ensure_role_exists.sql b/revert/ensure_role_exists.sql new file mode 100644 index 0000000..0409459 --- /dev/null +++ b/revert/ensure_role_exists.sql @@ -0,0 +1,8 @@ +-- Revert camper:ensure_role_exists from pg + +begin; + +drop trigger if exists ensure_role_exists on auth."user"; +drop function if exists auth.ensure_role_exists(); + +commit; diff --git a/revert/extension_citext.sql b/revert/extension_citext.sql new file mode 100644 index 0000000..e14b441 --- /dev/null +++ b/revert/extension_citext.sql @@ -0,0 +1,7 @@ +-- Revert camper:extension_citext from pg + +begin; + +drop extension if exists citext; + +commit; diff --git a/revert/extension_pgcrypto.sql b/revert/extension_pgcrypto.sql new file mode 100644 index 0000000..b452799 --- /dev/null +++ b/revert/extension_pgcrypto.sql @@ -0,0 +1,7 @@ +-- Revert camper:extension_pgcrypto from pg + +begin; + +drop extension if exists pgcrypto; + +commit; diff --git a/revert/language.sql b/revert/language.sql new file mode 100644 index 0000000..f7ba172 --- /dev/null +++ b/revert/language.sql @@ -0,0 +1,7 @@ +-- Revert camper:language from pg + +begin; + +drop table if exists public.language; + +commit; diff --git a/revert/login.sql b/revert/login.sql new file mode 100644 index 0000000..799c3c4 --- /dev/null +++ b/revert/login.sql @@ -0,0 +1,7 @@ +-- Revert camper:login from pg + +begin; + +drop function if exists camper.login(camper.email, text, inet); + +commit; diff --git a/revert/login_attempt.sql b/revert/login_attempt.sql new file mode 100644 index 0000000..545dec3 --- /dev/null +++ b/revert/login_attempt.sql @@ -0,0 +1,7 @@ +-- Revert camper:login_attempt from pg + +begin; + +drop table if exists auth.login_attempt; + +commit; diff --git a/revert/logout.sql b/revert/logout.sql new file mode 100644 index 0000000..41e878a --- /dev/null +++ b/revert/logout.sql @@ -0,0 +1,7 @@ +-- Revert camper:logout from pg + +begin; + +drop function if exists camper.logout(); + +commit; diff --git a/revert/roles.sql b/revert/roles.sql new file mode 100644 index 0000000..b7e4f3c --- /dev/null +++ b/revert/roles.sql @@ -0,0 +1,10 @@ +-- Revert camper:roles from pg + +begin; + +drop role authenticator; +drop role admin; +drop role employee; +drop role guest; + +commit; diff --git a/revert/schema_auth.sql b/revert/schema_auth.sql new file mode 100644 index 0000000..95e550e --- /dev/null +++ b/revert/schema_auth.sql @@ -0,0 +1,7 @@ +-- Revert camper:schema_auth from pg + +begin; + +drop schema if exists auth; + +commit; diff --git a/revert/schema_camper.sql b/revert/schema_camper.sql new file mode 100644 index 0000000..70d749c --- /dev/null +++ b/revert/schema_camper.sql @@ -0,0 +1,7 @@ +-- Revert camper:schema_camper from pg + +begin; + +drop schema if exists camper; + +commit; diff --git a/revert/schema_public.sql b/revert/schema_public.sql new file mode 100644 index 0000000..f2eee45 --- /dev/null +++ b/revert/schema_public.sql @@ -0,0 +1,13 @@ +-- Revert camper:schema_public from pg + +begin; + +revoke usage on schema public from admin; +revoke usage on schema public from employee; +revoke usage on schema public from guest; +revoke usage on schema public from authenticator; + +grant usage on schema public to public; +grant create on schema public to public; + +commit; diff --git a/revert/set_cookie.sql b/revert/set_cookie.sql new file mode 100644 index 0000000..a71ccb3 --- /dev/null +++ b/revert/set_cookie.sql @@ -0,0 +1,7 @@ +-- Revert camper:set_cookie from pg + +begin; + +drop function if exists public.set_cookie(text); + +commit; diff --git a/revert/user.sql b/revert/user.sql new file mode 100644 index 0000000..0ebfffa --- /dev/null +++ b/revert/user.sql @@ -0,0 +1,7 @@ +-- Revert camper:user from pg + +begin; + +drop table if exists auth."user"; + +commit; diff --git a/revert/user_profile.sql b/revert/user_profile.sql new file mode 100644 index 0000000..625f84e --- /dev/null +++ b/revert/user_profile.sql @@ -0,0 +1,9 @@ +-- Revert camper:user_profile from pg + +begin; + +drop trigger if exists update_user_profile on camper.user_profile; +drop function if exists camper.update_user_profile(); +drop view if exists camper.user_profile; + +commit; diff --git a/sqitch.plan b/sqitch.plan index 96b6265..87aed9b 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -2,3 +2,25 @@ %project=camper %uri=https://dev.tandem.ws/tandem/camper +roles 2023-07-21T21:25:06Z jordi fita mas # Add database roles +schema_public [roles] 2023-07-21T21:39:58Z jordi fita mas # Set privileges to the public schema +language [roles schema_public] 2023-07-21T22:00:31Z jordi fita mas # Add relation of available languages +available_languages [schema_public language] 2023-07-21T22:03:24Z jordi fita mas # Add the initially available languages +extension_citext [schema_public] 2023-07-21T22:06:24Z jordi fita mas # Add citext extension +schema_camper [roles] 2023-07-21T22:08:39Z jordi fita mas # Add application schema +email [schema_camper extension_citext] 2023-07-21T22:11:13Z jordi fita mas # Add email domain +schema_auth [roles] 2023-07-21T22:13:23Z jordi fita mas # Add authentication schema +user [roles schema_auth email language] 2023-07-21T22:37:20Z jordi fita mas # Add user relation +ensure_role_exists [schema_auth user] 2023-07-21T22:48:56Z jordi fita mas # Add trigger to ensure users’ role exists +extension_pgcrypto [schema_auth] 2023-07-21T22:53:25Z jordi fita mas # Add pgcrypto extension +encrypt_password [schema_auth user extension_pgcrypto] 2023-07-21T22:56:40Z jordi fita mas # Add function to encrypt users’ password +current_user_cookie [roles schema_camper] 2023-07-21T23:05:26Z jordi fita mas # Add function to get the cookie of the current user +current_user_email [roles schema_camper] 2023-07-21T23:09:34Z jordi fita mas # Add function to ge the email of the current user +build_cookie [roles schema_camper current_user_email current_user_cookie] 2023-07-21T23:14:35Z jordi fita mas # Add function to build the cookie for the current user +login_attempt [schema_auth] 2023-07-21T23:22:17Z jordi fita mas # Add relation of log in attempts +login [roles schema_auth schema_camper extension_pgcrypto email user login_attempt build_cookie] 2023-07-21T23:29:18Z jordi fita mas # Add function to login +logout [roles schema_auth schema_camper current_user_email current_user_cookie user] 2023-07-21T23:36:12Z jordi fita mas # Add function to logout +check_cookie [roles schema_public schema_auth user] 2023-07-21T23:40:55Z jordi fita mas # Add function to check if a user cookie is valid +set_cookie [roles schema_public check_cookie] 2023-07-21T23:44:30Z jordi fita mas # Add function to set the role base don the cookie +user_profile [roles schema_camper user current_user_email current_user_cookie] 2023-07-21T23:47:36Z jordi fita mas # Add view for user profile +change_password [roles schema_auth schema_camper user] 2023-07-21T23:54:52Z jordi fita mas # Add function to change the current user’s password diff --git a/test/build_cookie.sql b/test/build_cookie.sql new file mode 100644 index 0000000..a51c157 --- /dev/null +++ b/test/build_cookie.sql @@ -0,0 +1,72 @@ +-- Test build_cookie +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(13); + +set search_path to camper, auth, public; + +select has_function('camper', 'build_cookie', array ['email', 'text']); +select function_lang_is('camper', 'build_cookie', array ['email', 'text'], 'sql'); +select function_returns('camper', 'build_cookie', array ['email', 'text'], 'text'); +select isnt_definer('camper', 'build_cookie', array ['email', 'text']); +select volatility_is('camper', 'build_cookie', array ['email', 'text'], 'stable'); +select function_privs_are('camper', 'build_cookie', array ['email', 'text'], 'guest', array []::text[]); +select function_privs_are('camper', 'build_cookie', array ['email', 'text'], 'employee', array ['EXECUTE']); +select function_privs_are('camper', 'build_cookie', array ['email', 'text'], 'admin', array ['EXECUTE']); +select function_privs_are('camper', 'build_cookie', array ['email', 'text'], 'authenticator', array []::text[]); + +set client_min_messages to warning; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', 'employee', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', + current_timestamp + interval '1 month') + , (9, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', + current_timestamp + interval '1 month') +; + +select is( + build_cookie('test@example.com'::email, '123abc'), + '123abc/test@example.com', + 'Should build the cookie with the given user and cookie value' +); + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +reset role; + +select is( + build_cookie(), + '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', + 'Should build the cookie for the logged in user' +); + + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +reset role; + +select is( + build_cookie(), + '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog', + 'Should build the cookie for the other logged in user' +); + + +select set_cookie('ashtasth'); +reset role; + +select is( + build_cookie(), + '/', + 'Should build the cookie for the guest user' +); + + +select * +from finish(); + +rollback; diff --git a/test/change_password.sql b/test/change_password.sql new file mode 100644 index 0000000..1ee82e3 --- /dev/null +++ b/test/change_password.sql @@ -0,0 +1,57 @@ +-- Test change_password +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to camper, auth, public; + +select has_function('camper', 'change_password', array ['text']); +select function_lang_is('camper', 'change_password', array ['text'], 'sql'); +select function_returns('camper', 'change_password', array ['text'], 'void'); +select is_definer('camper', 'change_password', array ['text']); +select volatility_is('camper', 'change_password', array ['text'], 'volatile'); +select function_privs_are('camper', 'change_password', array ['text'], 'guest', array []::text[]); +select function_privs_are('camper', 'change_password', array ['text'], 'employee', array ['EXECUTE']); +select function_privs_are('camper', 'change_password', array ['text'], 'admin', array ['EXECUTE']); +select function_privs_are('camper', 'change_password', array ['text'], 'authenticator', array []::text[]); + +set client_min_messages to warning; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', 'employee', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (9, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +select lives_ok($$ select change_password('another') $$, 'Should run even without current user'); + +select isnt_empty( + $$ select * from auth."user" where password = crypt('test', password) $$, + 'Should not have changed any password' +); + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); + +select lives_ok($$ select change_password('another') $$, 'Should run with the correct user'); + +reset role; + +select isnt_empty( + $$ select * from auth."user" where email = 'demo@tandem.blog' and password = crypt('another', password) $$, + 'Should have changed the password of the current user' +); + +select isnt_empty( + $$ select * from auth."user" where email = 'admin@tandem.blog' and password = crypt('test', password) $$, + 'Should not have changed any other password' +); + +select * +from finish(); + +rollback; diff --git a/test/check_cookie.sql b/test/check_cookie.sql new file mode 100644 index 0000000..4d456da --- /dev/null +++ b/test/check_cookie.sql @@ -0,0 +1,113 @@ +-- Test check_cookie +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(21); + +set search_path to auth, camper, public; + +select has_function('public', 'check_cookie', array ['text']); +select function_lang_is('public', 'check_cookie', array ['text'], 'plpgsql'); +select function_returns('public', 'check_cookie', array ['text'], 'name'); +select is_definer('public', 'check_cookie', array ['text']); +select volatility_is('public', 'check_cookie', array ['text'], 'stable'); +select function_privs_are('public', 'check_cookie', array ['text'], 'guest', array []::text[]); +select function_privs_are('public', 'check_cookie', array ['text'], 'employee', array []::text[]); +select function_privs_are('public', 'check_cookie', array ['text'], 'admin', array []::text[]); +select function_privs_are('public', 'check_cookie', array ['text'], 'authenticator', array ['EXECUTE']); + +set client_min_messages to warning; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', 'employee', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (9, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +prepare user_info as +select current_user_email(), current_user_cookie(); + +select is( + check_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'), + 'employee'::name, + 'Should validate the cookie for the first user' +); + +select results_eq( + 'user_info', + $$ values ('demo@tandem.blog', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e') $$, + 'Should have updated the settings with the user info' +); + +select is( + check_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'), + 'admin'::name, + 'Should validate the cookie for the second user' +); + +select results_eq( + 'user_info', + $$ values ('admin@tandem.blog', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524') $$, + 'Should have updated the settings with the other user info' +); + +select is( + check_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/admin@tandem.blog'), + 'guest'::name, + 'Should only match with the correct email' +); + +select results_eq( + 'user_info', + $$ values ('', '') $$, + 'Should have updated the settings with a guest user' +); + +select is( + check_cookie('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/admin@tandem.blog'), + 'guest'::name, + 'Should only match with the correct cookie value' +); + +select results_eq( + 'user_info', + $$ values ('', '') $$, + 'Should have left the settings with a guest user' +); + +update "user" +set cookie_expires_at = current_timestamp - interval '1 minute'; + +select is( + check_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'), + 'guest'::name, + 'Should not allow expired cookies' +); + +select results_eq( + 'user_info', + $$ values ('', '') $$, + 'Should have left the settings with a guest user' +); + +select is( + check_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'), + 'guest'::name, + 'Should not allow expired cookied for the other user as well' +); + +select results_eq( + 'user_info', + $$ values ('', '') $$, + 'Should have left the settings with a guest user' +); + + +select * +from finish(); + +rollback; diff --git a/test/current_user_cookie.sql b/test/current_user_cookie.sql new file mode 100644 index 0000000..6955b01 --- /dev/null +++ b/test/current_user_cookie.sql @@ -0,0 +1,70 @@ +-- Test current_user_cookie +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to camper, auth, public; + +select plan(15); + +select has_function('camper', 'current_user_cookie', array []::name[]); +select function_lang_is('camper', 'current_user_cookie', array []::name[], 'sql'); +select function_returns('camper', 'current_user_cookie', array []::name[], 'text'); +select isnt_definer('camper', 'current_user_cookie', array []::name[]); +select volatility_is('camper', 'current_user_cookie', array []::name[], 'stable'); +select function_privs_are('camper', 'current_user_cookie', array []::name[], 'guest', array ['EXECUTE']); +select function_privs_are('camper', 'current_user_cookie', array []::name[], 'employee', array ['EXECUTE']); +select function_privs_are('camper', 'current_user_cookie', array []::name[], 'admin', array ['EXECUTE']); +select function_privs_are('camper', 'current_user_cookie', array []::name[], 'authenticator', array []::text[]); + +set client_min_messages to warning; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', 'employee', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +select lives_ok( + $$ select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog') $$, + 'Should change ok for the first user' +); + +select is( + current_user_cookie(), + '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', + 'Should return the cookie of the first user' +); + +reset role; + + +select lives_ok( + $$ select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog') $$, + 'Should change ok for the second user' +); + +select is( + current_user_cookie(), + '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', + 'Should return the cookie of the second user' +); + +reset role; + +select lives_ok( + $$ select set_cookie('') $$, + 'Should change ok for a guest user' +); + +select is(current_user_cookie(), '', 'Should return an empty string'); + +reset role; + +select * +from finish(); + +rollback; diff --git a/test/current_user_email.sql b/test/current_user_email.sql new file mode 100644 index 0000000..6718796 --- /dev/null +++ b/test/current_user_email.sql @@ -0,0 +1,63 @@ +-- Test current_user_email +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to camper, auth, public; + +select plan(15); + +select has_function('camper', 'current_user_email', array []::name[]); +select function_lang_is('camper', 'current_user_email', array []::name[], 'sql'); +select function_returns('camper', 'current_user_email', array []::name[], 'text'); +select isnt_definer('camper', 'current_user_email', array []::name[]); +select volatility_is('camper', 'current_user_email', array []::name[], 'stable'); +select function_privs_are('camper', 'current_user_email', array []::name[], 'guest', array ['EXECUTE']); +select function_privs_are('camper', 'current_user_email', array []::name[], 'employee', array ['EXECUTE']); +select function_privs_are('camper', 'current_user_email', array []::name[], 'admin', array ['EXECUTE']); +select function_privs_are('camper', 'current_user_email', array []::name[], 'authenticator', array []::text[]); + +set client_min_messages to warning; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', 'employee', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +select lives_ok( + $$ select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog') $$, + 'Should change ok for the first user' +); + +select is(current_user_email(), 'demo@tandem.blog', 'Should return the email of the first user'); + +reset role; + + +select lives_ok( + $$ select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog') $$, + 'Should change ok for the second user' +); + +select is(current_user_email(), 'admin@tandem.blog', 'Should return the email of the second user'); + +reset role; + +select lives_ok( + $$ select set_cookie('') $$, + 'Should change ok for a guest user' +); + +select is(current_user_email(), '', 'Should return an empty string'); + +reset role; + + +select * +from finish(); + +rollback; diff --git a/test/email.sql b/test/email.sql new file mode 100644 index 0000000..b6aba5b --- /dev/null +++ b/test/email.sql @@ -0,0 +1,33 @@ +-- Test email +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(5); + +set search_path to camper, public; + +select has_domain('email'); +select domain_type_is('email', 'citext'); + +select lives_ok($$ select 'test@tandem.com'::email $$, 'Should be able to cast strings to email'); + +select throws_ok( + $$ SELECT 'test@tandem,,co.uk'::email $$, + 23514, null, + 'Should reject email addresses with wrong domain' +); + +select throws_ok( + $$ SELECT 'test@a@tandem.com'::email $$, + 23514, null, + 'Should reject email address with two @ signs' +); + + +select * +from finish(); + +rollback; diff --git a/test/encrypt_password.sql b/test/encrypt_password.sql new file mode 100644 index 0000000..e3a29d8 --- /dev/null +++ b/test/encrypt_password.sql @@ -0,0 +1,40 @@ +-- Test encrypt_password +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(11); + +set search_path to auth, camper, public; + +select has_function('auth', 'encrypt_password', array []::name[]); +select function_lang_is('auth', 'encrypt_password', array []::name[], 'plpgsql'); +select function_returns('auth', 'encrypt_password', array []::name[], 'trigger'); +select isnt_definer('auth', 'encrypt_password', array []::name[]); +select volatility_is('auth', 'encrypt_password', array []::name[], 'volatile'); +select function_privs_are('auth', 'encrypt_password', array []::name[], 'guest', array []::text[]); +select function_privs_are('auth', 'encrypt_password', array []::name[], 'employee', array []::text[]); +select function_privs_are('auth', 'encrypt_password', array []::name[], 'admin', array []::text[]); +select function_privs_are('auth', 'encrypt_password', array []::name[], 'authenticator', array []::text[]); + +select trigger_is('user', 'encrypt_password', 'encrypt_password'); + +set client_min_messages to warning; +truncate "user" cascade; +reset client_min_messages; + +insert into "user" (email, name, password, role) +values ('info@tandem.blog', 'Perita', 'test', 'guest'); + +select row_eq( + $$ select email from "user" where password = crypt('test', password) $$, + row ('info@tandem.blog'::email), + 'Should find the new user using its encrypted password' +); + +select * +from finish(); + +rollback; diff --git a/test/ensure_role_exists.sql b/test/ensure_role_exists.sql new file mode 100644 index 0000000..2f5f401 --- /dev/null +++ b/test/ensure_role_exists.sql @@ -0,0 +1,54 @@ +-- Test ensure_role_exists +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to auth, public; + +select has_function('auth', 'ensure_role_exists', array []::name[]); +select function_lang_is('auth', 'ensure_role_exists', array []::name[], 'plpgsql'); +select function_returns('auth', 'ensure_role_exists', array []::name[], 'trigger'); +select isnt_definer('auth', 'ensure_role_exists', array []::name[]); +select volatility_is('auth', 'ensure_role_exists', array []::name[], 'volatile'); +select function_privs_are('auth', 'ensure_role_exists', array []::name[], 'guest', array []::text[]); +select function_privs_are('auth', 'ensure_role_exists', array []::name[], 'employee', array []::text[]); +select function_privs_are('auth', 'ensure_role_exists', array []::name[], 'admin', array []::text[]); +select function_privs_are('auth', 'ensure_role_exists', array []::name[], 'authenticator', array []::text[]); + +select trigger_is('user', 'ensure_role_exists', 'ensure_role_exists'); + +set client_min_messages to warning; +truncate "user" cascade; +reset client_min_messages; + +select lives_ok( + $$ insert into "user" (email, name, password, role) values ('info@tandem.blog', 'Factura', 'test', 'guest') $$, + 'Should be able to insert a user with a valid role' +); + +select throws_ok( + $$ insert into "user" (email, name, password, role) values ('nope@tandem.blog', 'Factura', 'test', 'non-existing-role') $$, + '23503', + 'role not found: non-existing-role', + 'Should not allow insert users with invalid roles' +); + +select lives_ok($$ update "user" set role = 'employee' where email = 'info@tandem.blog' $$, + 'Should be able to change the role of a user to another valid role' +); + +select throws_ok($$ update "user" set role = 'usurer' where email = 'info@tandem.blog' $$, + '23503', + 'role not found: usurer', + 'Should not allow update users to invalid roles' +); + + +select * +from finish(); + +rollback; diff --git a/test/extensions.sql b/test/extensions.sql new file mode 100644 index 0000000..a9e1e62 --- /dev/null +++ b/test/extensions.sql @@ -0,0 +1,20 @@ +-- Test extension_citext +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(1); + +select extensions_are(array [ + 'citext' + , 'pgtap' + , 'pgcrypto' + , 'plpgsql' +]); + +select * +from finish(); + +rollback; diff --git a/test/language.sql b/test/language.sql new file mode 100644 index 0000000..a53ab09 --- /dev/null +++ b/test/language.sql @@ -0,0 +1,48 @@ +-- Test language +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +set search_path to public; + +select plan(27); + +select has_table('language'); +select has_pk('language'); +select table_privs_are('language', 'guest', array ['SELECT']); +select table_privs_are('language', 'employee', array ['SELECT']); +select table_privs_are('language', 'admin', array ['SELECT']); +select table_privs_are('language', 'authenticator', array ['SELECT']::text[]); + +select has_column('language', 'lang_tag'); +select col_is_pk('language', 'lang_tag'); +select col_type_is('language', 'lang_tag', 'text'); +select col_not_null('language', 'lang_tag'); +select col_hasnt_default('language', 'lang_tag'); + +select has_column('language', 'name'); +select col_type_is('language', 'name', 'text'); +select col_not_null('language', 'name'); +select col_hasnt_default('language', 'name'); + +select has_column('language', 'endonym'); +select col_type_is('language', 'endonym', 'text'); +select col_not_null('language', 'endonym'); +select col_hasnt_default('language', 'endonym'); + +select has_column('language', 'selectable'); +select col_type_is('language', 'selectable', 'boolean'); +select col_not_null('language', 'selectable'); +select col_hasnt_default('language', 'selectable'); + +select has_column('language', 'currency_pattern'); +select col_type_is('language', 'currency_pattern', 'text'); +select col_not_null('language', 'currency_pattern'); +select col_hasnt_default('language', 'currency_pattern'); + +select * +from finish(); + +rollback; diff --git a/test/login.sql b/test/login.sql new file mode 100644 index 0000000..286311c --- /dev/null +++ b/test/login.sql @@ -0,0 +1,111 @@ +-- Test login +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(20); + +set search_path to auth, camper, public; + +select has_function('camper', 'login', array ['email', 'text', 'inet']); +select function_lang_is('camper', 'login', array ['email', 'text', 'inet'], 'plpgsql'); +select function_returns('camper', 'login', array ['email', 'text', 'inet'], 'text'); +select is_definer('camper', 'login', array ['email', 'text', 'inet']); +select volatility_is('camper', 'login', array ['email', 'text', 'inet'], 'volatile'); +select function_privs_are('camper', 'login', array ['email', 'text', 'inet'], 'guest', array ['EXECUTE']); +select function_privs_are('camper', 'login', array ['email', 'text', 'inet'], 'employee', array []::text[]); +select function_privs_are('camper', 'login', array ['email', 'text', 'inet'], 'admin', array []::text[]); +select function_privs_are('camper', 'login', array ['email', 'text', 'inet'], 'authenticator', array []::text[]); + +set client_min_messages to warning; +truncate auth."user" cascade; +truncate auth.login_attempt cascade; +reset client_min_messages; + +insert into auth."user" (email, name, password, role) +values ('info@tandem.blog', 'Tandem', 'test', 'employee'); + +create temp table _login_test +( + result_num integer, + cookie text not null +); + +select lives_ok( + $$ insert into _login_test select 1, split_part(login('info@tandem.blog', 'test', '::1'::inet), '/', 1) $$, + 'Should login with a correct user and password' +); + +select isnt_empty( + $$ select cookie from _login_test join "user" using (cookie) where email = 'info@tandem.blog' $$, + 'Should have returned the cookie that wrote to the user relation.' +); + +select results_eq( + $$ select cookie_expires_at > current_timestamp from "user" where email = 'info@tandem.blog' $$, + $$ values (true) $$, + 'Should have set an expiry date in the future.' +); + +select isnt_empty( + $$ select cookie from _login_test where cookie in (select split_part(login('info@tandem.blog', 'test', '192.168.0.1'::inet), '/', 1)) $$, + 'Should return the same cookie if not expired yet.' +); + +update "user" +set cookie_expires_at = current_timestamp - interval '1 hour' +where email = 'info@tandem.blog'; + +select lives_ok( + $$ insert into _login_test select 2, split_part(login('info@tandem.blog', 'test', '::1'::inet), '/', 1) $$, + 'Should login with a correct user and password even with an expired cookie' +); + + +select results_eq( + $$ select count(distinct cookie)::integer from _login_test $$, + $$ values (2) $$, + 'Should have returned a new cookie' +); + +select isnt_empty( + $$ select cookie from _login_test join "user" using (cookie) where email = 'info@tandem.blog' and result_num = 2 $$, + 'Should have updated the user’s cookie.' +); + +select results_eq( + $$ select cookie_expires_at > current_timestamp from "user" where email = 'info@tandem.blog' $$, + $$ values(true) $$, + 'Should have set an expiry date in the future, again.' +); + +select is( + login('info@tandem.blog'::email, 'mah password', '127.0.0.1'::inet), + ''::text, + 'Should not find any role with an invalid password' +); + +select is( + login('nope@tandem.blog'::email, 'test'), + ''::text, + 'Should not find any role with an invalid email' +); + +select results_eq( + 'select user_name, ip_address, success, attempted_at from login_attempt order by attempt_id', + $$ values ('info@tandem.blog', '::1'::inet, true, current_timestamp) + , ('info@tandem.blog', '192.168.0.1'::inet, true, current_timestamp) + , ('info@tandem.blog', '::1'::inet, true, current_timestamp) + , ('info@tandem.blog', '127.0.0.1'::inet, false, current_timestamp) + , ('nope@tandem.blog', null, false, current_timestamp) + $$, + 'Should have recorded all login attempts.' +); + + +select * +from finish(); + +rollback; diff --git a/test/login_attempt.sql b/test/login_attempt.sql new file mode 100644 index 0000000..b92d16e --- /dev/null +++ b/test/login_attempt.sql @@ -0,0 +1,58 @@ +-- Test login_attempt +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(34); + +set search_path to auth, public; + +select has_table('login_attempt'); +select has_pk('login_attempt'); +select table_privs_are('login_attempt', 'guest', array []::text[]); +select table_privs_are('login_attempt', 'employee', array []::text[]); +select table_privs_are('login_attempt', 'admin', array []::text[]); +select table_privs_are('login_attempt', 'authenticator', array []::text[]); + +select has_sequence('login_attempt_attempt_id_seq'); +select sequence_privs_are('login_attempt_attempt_id_seq', 'guest', array[]::text[]); +select sequence_privs_are('login_attempt_attempt_id_seq', 'employee', array[]::text[]); +select sequence_privs_are('login_attempt_attempt_id_seq', 'admin', array[]::text[]); +select sequence_privs_are('login_attempt_attempt_id_seq', 'authenticator', array[]::text[]); + +select has_column('login_attempt', 'attempt_id'); +select col_is_pk('login_attempt', 'attempt_id'); +select col_type_is('login_attempt', 'attempt_id', 'bigint'); +select col_not_null('login_attempt', 'attempt_id'); +select col_has_default('login_attempt', 'attempt_id'); +select col_default_is('login_attempt', 'attempt_id', 'nextval(''login_attempt_attempt_id_seq''::regclass)'); + +select has_column('login_attempt', 'user_name'); +select col_type_is('login_attempt', 'user_name', 'text'); +select col_not_null('login_attempt', 'user_name'); +select col_hasnt_default('login_attempt', 'user_name'); + +select has_column('login_attempt', 'ip_address'); +select col_type_is('login_attempt', 'ip_address', 'inet'); +select col_is_null('login_attempt', 'ip_address'); +select col_hasnt_default('login_attempt', 'ip_address'); + +select has_column('login_attempt', 'success'); +select col_type_is('login_attempt', 'success', 'boolean'); +select col_not_null('login_attempt', 'success'); +select col_hasnt_default('login_attempt', 'success'); + +select has_column('login_attempt', 'attempted_at'); +select col_type_is('login_attempt', 'attempted_at', 'timestamp with time zone'); +select col_not_null('login_attempt', 'attempted_at'); +select col_has_default('login_attempt', 'attempted_at'); +select col_default_is('login_attempt', 'attempted_at', 'CURRENT_TIMESTAMP'); + + +select * +from finish(); + +rollback; + diff --git a/test/logout.sql b/test/logout.sql new file mode 100644 index 0000000..4611955 --- /dev/null +++ b/test/logout.sql @@ -0,0 +1,88 @@ +-- Test logout +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(17); + +set search_path to auth, camper, public; + +select has_function('camper', 'logout', array []::name[]); +select function_lang_is('camper', 'logout', array []::name[], 'sql'); +select function_returns('camper', 'logout', array []::name[], 'void'); +select is_definer('camper', 'logout', array []::name[]); +select volatility_is('camper', 'logout', array []::name[], 'volatile'); +select function_privs_are('camper', 'logout', array []::name[], 'guest', array []::text[]); +select function_privs_are('camper', 'logout', array []::name[], 'employee', array ['EXECUTE']); +select function_privs_are('camper', 'logout', array []::name[], 'admin', array ['EXECUTE']); +select function_privs_are('camper', 'logout', array []::name[], 'authenticator', array []::text[]); + +set client_min_messages to warning; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at) +values (1, 'info@tandem.blog', 'Tandem', 'test', 'employee', '8c23d4a8d777775f8fc507676a0d99d3dfa54b03b1b257c838', current_timestamp + interval '1 day') + , (12, 'admin@tandem.blog', 'Admin', 'test', 'admin', '0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', current_timestamp + interval '2 day') +; + +prepare user_cookies as +select cookie, cookie_expires_at +from "user" +order by user_id +; + +select set_config('request.user.cookie', '', false); +select set_config('request.user.email', '', false); +select lives_ok($$ select * from logout() $$, 'Can logout “nobody”'); + +select results_eq( + 'user_cookies', + $$ values ('8c23d4a8d777775f8fc507676a0d99d3dfa54b03b1b257c838', current_timestamp + interval '1 day') + , ('0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', current_timestamp + interval '2 day') + $$, + 'Should have changed nothing' +); + +select set_config('request.user.cookie', '0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', false); +select set_config('request.user.email', 'info@tandem.blog', false); +select lives_ok($$ select * from logout() $$, 'Can logout even if the email and cookie does not match'); + +select results_eq( + 'user_cookies', + $$ values ('8c23d4a8d777775f8fc507676a0d99d3dfa54b03b1b257c838', current_timestamp + interval '1 day') + , ('0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', current_timestamp + interval '2 day') + $$, + 'Should have changed nothing' +); + +select set_config('request.user.cookie', '8c23d4a8d777775f8fc507676a0d99d3dfa54b03b1b257c838', false); +select set_config('request.user.email', 'info@tandem.blog', false); +select lives_ok($$ select * from logout() $$, 'Can logout the first user'); + +select results_eq( + 'user_cookies', + $$ values ('', '-infinity'::timestamptz) + , ('0169e5f668eec1e6749fd25388b057997358efa8dfd697961a'::text, current_timestamp + interval '2 day') + $$, + 'The first user logged out' +); + +select set_config('request.user.cookie', '0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', false); +select set_config('request.user.email', 'admin@tandem.blog', false); +select lives_ok($$ select * from logout() $$, 'Can logout the second user'); + +select results_eq( + 'user_cookies', + $$ values ('', '-infinity'::timestamptz) + , ('', '-infinity'::timestamptz) + $$, + 'The second user logged out' +); + +select * +from finish(); + +rollback; diff --git a/test/roles.sql b/test/roles.sql new file mode 100644 index 0000000..9a54244 --- /dev/null +++ b/test/roles.sql @@ -0,0 +1,23 @@ +-- Test roles +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(8); + +select has_role('guest'); +select has_role('employee'); +select has_role('admin'); +select has_role('authenticator'); + +select is_member_of('guest', 'authenticator'); +select is_member_of('employee', 'authenticator'); +select is_member_of('admin', 'authenticator'); +select is_member_of('authenticator', array[]::text[]); + +select * +from finish(); + +rollback; diff --git a/test/schemas.sql b/test/schemas.sql new file mode 100644 index 0000000..f4e5114 --- /dev/null +++ b/test/schemas.sql @@ -0,0 +1,44 @@ +-- Test schemas +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(21); + +select schemas_are(array [ + 'auth', + 'camper', + 'public', + 'sqitch' +]); + +select schema_privs_are('auth', 'guest', array []::text[]); +select schema_privs_are('auth', 'employee', array []::text[]); +select schema_privs_are('auth', 'admin', array []::text[]); +select schema_privs_are('auth', 'authenticator', array []::text[]); +select schema_privs_are('auth', 'public', array []::text[]); + +select schema_privs_are('camper', 'guest', array ['USAGE']); +select schema_privs_are('camper', 'employee', array ['USAGE']); +select schema_privs_are('camper', 'admin', array ['USAGE']); +select schema_privs_are('camper', 'authenticator', array []::text[]); +select schema_privs_are('camper', 'public', array []::text[]); + +select schema_privs_are('public', 'guest', array ['USAGE']); +select schema_privs_are('public', 'employee', array ['USAGE']); +select schema_privs_are('public', 'admin', array ['USAGE']); +select schema_privs_are('public', 'authenticator', array ['USAGE']); +select schema_privs_are('public', 'public', array []::text[]); + +select schema_privs_are('sqitch', 'guest', array []::text[]); +select schema_privs_are('sqitch', 'employee', array []::text[]); +select schema_privs_are('sqitch', 'admin', array []::text[]); +select schema_privs_are('sqitch', 'authenticator', array []::text[]); +select schema_privs_are('sqitch', 'public', array []::text[]); + +select * +from finish(); + +rollback; diff --git a/test/set_cookie.sql b/test/set_cookie.sql new file mode 100644 index 0000000..a701d31 --- /dev/null +++ b/test/set_cookie.sql @@ -0,0 +1,76 @@ +-- Test set_cookie +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(15); + +set search_path to auth, camper, public; + +select has_function('public', 'set_cookie', array ['text']); +select function_lang_is('public', 'set_cookie', array ['text'], 'sql'); +select function_returns('public', 'set_cookie', array ['text'], 'void'); +select isnt_definer('public', 'set_cookie', array ['text']); +select volatility_is('public', 'set_cookie', array ['text'], 'stable'); +select function_privs_are('public', 'set_cookie', array ['text'], 'guest', array []::text[]); +select function_privs_are('public', 'set_cookie', array ['text'], 'employee', array []::text[]); +select function_privs_are('public', 'set_cookie', array ['text'], 'admin', array []::text[]); +select function_privs_are('public', 'set_cookie', array ['text'], 'authenticator', array ['EXECUTE']); + +set client_min_messages to warning; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', 'employee', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +prepare user_info as +select current_user_email(), current_user_cookie(), current_user; + +select lives_ok( + $$ select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog') $$, + 'Should run ok for a valid cookie' +); + +select results_eq( + 'user_info', + $$ values ('demo@tandem.blog', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', 'employee'::name) $$, + 'Should have updated the info with the correct user' +); + +reset role; + +select lives_ok( + $$ select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog') $$, + 'Should run ok for a different cookie' +); + +select results_eq( + 'user_info', + $$ values ('admin@tandem.blog', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', 'admin'::name) $$, + 'Should have updated the info with the other user' +); + +reset role; + +select lives_ok( + $$ select set_cookie('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/admin@tandem.blog') $$, + 'Should run ok even for an invalid cookie' +); + +select results_eq( + 'user_info', + $$ values ('', '', 'guest'::name) $$, + 'Should have updated the info as a guest user' +); + +reset role; + +select * +from finish(); + +rollback; diff --git a/test/user.sql b/test/user.sql new file mode 100644 index 0000000..e830dac --- /dev/null +++ b/test/user.sql @@ -0,0 +1,84 @@ +-- Test user +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(56); + +set search_path to auth, public; + +select has_table('user'); +select has_pk('user'); +select table_privs_are('user', 'guest', array []::text[]); +select table_privs_are('user', 'employee', array []::text[]); +select table_privs_are('user', 'admin', array []::text[]); +select table_privs_are('user', 'authenticator', array []::text[]); + +select has_sequence('user_user_id_seq'); +select sequence_privs_are('user_user_id_seq', 'guest', array[]::text[]); +select sequence_privs_are('user_user_id_seq', 'employee', array[]::text[]); +select sequence_privs_are('user_user_id_seq', 'admin', array[]::text[]); +select sequence_privs_are('user_user_id_seq', 'authenticator', array[]::text[]); + +select has_column('user', 'user_id'); +select col_is_pk('user', 'user_id'); +select col_type_is('user', 'user_id', 'integer'); +select col_not_null('user', 'user_id'); +select col_has_default('user', 'user_id'); +select col_default_is('user', 'user_id', 'nextval(''user_user_id_seq''::regclass)'); + +select has_column('user', 'email'); +select col_is_unique('user', 'email'); +select col_type_is('user', 'email', 'camper.email'); +select col_not_null('user', 'email'); +select col_hasnt_default('user', 'email'); + +select has_column('user', 'name'); +select col_type_is('user', 'name', 'text'); +select col_not_null('user', 'name'); +select col_hasnt_default('user', 'name'); + +select has_column('user', 'password'); +select col_type_is('user', 'password', 'text'); +select col_not_null('user', 'password'); +select col_hasnt_default('user', 'password'); + +select has_column('user', 'role'); +select col_type_is('user', 'role', 'name'); +select col_not_null('user', 'role'); +select col_hasnt_default('user', 'role'); + +select has_column('user', 'lang_tag'); +select col_is_fk('user', 'lang_tag'); +select fk_ok('user', 'lang_tag', 'language', 'lang_tag'); +select col_type_is('user', 'lang_tag', 'text'); +select col_not_null('user', 'lang_tag'); +select col_has_default('user', 'lang_tag'); +select col_default_is('user', 'lang_tag', 'und'); + +select has_column('user', 'cookie'); +select col_type_is('user', 'cookie', 'text'); +select col_not_null('user', 'cookie'); +select col_has_default('user', 'cookie'); +select col_default_is('user', 'cookie', ''); + +select has_column('user', 'cookie_expires_at'); +select col_type_is('user', 'cookie_expires_at', 'timestamp with time zone'); +select col_not_null('user', 'cookie_expires_at'); +select col_has_default('user', 'cookie_expires_at'); +select col_default_is('user', 'cookie_expires_at', '-infinity'::timestamp); + +select has_column('user', 'created_at'); +select col_type_is('user', 'created_at', 'timestamp with time zone'); +select col_not_null('user', 'created_at'); +select col_has_default('user', 'created_at'); +select col_default_is('user', 'created_at', 'CURRENT_TIMESTAMP'); + + +select * +from finish(); + +rollback; + diff --git a/test/user_profile.sql b/test/user_profile.sql new file mode 100644 index 0000000..425d909 --- /dev/null +++ b/test/user_profile.sql @@ -0,0 +1,171 @@ +-- Test user_profile +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(53); + +set search_path to camper, auth, public; + +select has_view('user_profile'); +select table_privs_are('user_profile', 'guest', array ['SELECT']); +select table_privs_are('user_profile', 'employee', array ['SELECT']); +select table_privs_are('user_profile', 'admin', array ['SELECT']); +select table_privs_are('user_profile', 'authenticator', array []::text[]); + +select has_column('user_profile', 'user_id'); +select col_type_is('user_profile', 'user_id', 'integer'); +select column_privs_are('user_profile', 'user_id', 'guest', array ['SELECT']); +select column_privs_are('user_profile', 'user_id', 'employee', array ['SELECT']); +select column_privs_are('user_profile', 'user_id', 'admin', array ['SELECT']); +select column_privs_are('user_profile', 'user_id', 'authenticator', array []::text[]); + +select has_column('user_profile', 'email'); +select col_type_is('user_profile', 'email', 'email'); +select column_privs_are('user_profile', 'email', 'guest', array ['SELECT']); +select column_privs_are('user_profile', 'email', 'employee', array ['SELECT', 'UPDATE']); +select column_privs_are('user_profile', 'email', 'admin', array ['SELECT', 'UPDATE']); +select column_privs_are('user_profile', 'email', 'authenticator', array []::text[]); + +select has_column('user_profile', 'name'); +select col_type_is('user_profile', 'name', 'text'); +select column_privs_are('user_profile', 'name', 'guest', array ['SELECT']); +select column_privs_are('user_profile', 'name', 'employee', array ['SELECT', 'UPDATE']); +select column_privs_are('user_profile', 'name', 'admin', array ['SELECT', 'UPDATE']); +select column_privs_are('user_profile', 'name', 'authenticator', array []::text[]); + +select has_column('user_profile', 'role'); +select col_type_is('user_profile', 'role', 'name'); +select column_privs_are('user_profile', 'role', 'guest', array ['SELECT']); +select column_privs_are('user_profile', 'role', 'employee', array ['SELECT']); +select column_privs_are('user_profile', 'role', 'admin', array ['SELECT']); +select column_privs_are('user_profile', 'role', 'authenticator', array []::text[]); + +select has_column('user_profile', 'lang_tag'); +select col_type_is('user_profile', 'lang_tag', 'text'); +select column_privs_are('user_profile', 'lang_tag', 'guest', array ['SELECT']); +select column_privs_are('user_profile', 'lang_tag', 'employee', array ['SELECT', 'UPDATE']); +select column_privs_are('user_profile', 'lang_tag', 'admin', array ['SELECT', 'UPDATE']); +select column_privs_are('user_profile', 'lang_tag', 'authenticator', array []::text[]); + +select has_column('user_profile', 'csrf_token'); +select col_type_is('user_profile', 'csrf_token', 'text'); +select column_privs_are('user_profile', 'csrf_token', 'guest', array ['SELECT']); +select column_privs_are('user_profile', 'csrf_token', 'employee', array ['SELECT']); +select column_privs_are('user_profile', 'csrf_token', 'admin', array ['SELECT']); +select column_privs_are('user_profile', 'csrf_token', 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate auth."user" cascade; +reset client_min_messages; + +insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at, lang_tag) +values (1, 'demo@tandem.blog', 'Demo', 'test', 'employee', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', + current_timestamp + interval '1 month', 'ca') +, (5, 'admin@tandem.blog', 'Admin', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', + current_timestamp + interval '1 month', 'es') +, (7, 'another@tandem.blog', 'Another Admin', 'test', 'admin', default, default, default) +; + +prepare profile as +select user_id, email, name, role, lang_tag, csrf_token +from user_profile; + +select set_config('request.user.cookie', '', false); + +select results_eq( + 'profile', + $$ values (0, null::email, '', 'guest'::name, 'und', '') $$, + 'Should be set up with the guest user when no user logged in yet.' +); + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); + +select results_eq( + 'profile', + $$ values (1, 'demo@tandem.blog'::email, 'Demo', 'employee'::name, 'ca', '44facbb30d') $$, + 'Should only see the profile of the first user' +); + +select lives_ok($$ + update user_profile + set email = 'demo+update@tandem.blog' + , name = 'Demo Update' + , lang_tag = 'es'; + $$, + 'Should be able to update the first profile' +); + +select throws_ok( + $$ update user_profile set user_id = 123 $$, + '42501', 'permission denied for view user_profile', + 'Should not be able to change the ID' +); + +select throws_ok( + $$ update user_profile set role = 'admin' $$, + '42501', 'permission denied for view user_profile', + 'Should not be able to change the ID' +); + +select results_eq( + 'profile', + $$ values (1, 'demo+update@tandem.blog'::email, 'Demo Update', 'employee'::name, 'es', '44facbb30d') $$, + 'Should see the changed profile of the first user' +); + +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); + +select results_eq( + 'profile', + $$ values (5, 'admin@tandem.blog'::email, 'Admin', 'admin'::name, 'es', '12af4c88b5') $$, + 'Should only see the profile of the second user' +); + +select lives_ok($$ + update user_profile + set email = 'admin+update@tandem.blog' + , name = 'Admin Update' + , lang_tag = 'ca'; + $$, + 'Should be able to update the second profile' +); + +select throws_ok( + $$ update user_profile set user_id = 123 $$, + '42501', 'permission denied for view user_profile', + 'Should not be able to change the ID' +); + +select throws_ok( + $$ update user_profile set role = 'employee' $$, + '42501', 'permission denied for view user_profile', + 'Should not be able to change the ID' +); + +select results_eq( + 'profile', + $$ values (5, 'admin+update@tandem.blog'::email, 'Admin Update', 'admin'::name, 'ca', '12af4c88b5') $$, + 'Should see the changed profile of the first user' +); + +reset role; + +select results_eq( + $$ select user_id, email, name, lang_tag from auth."user" order by user_id $$, + $$ values (1, 'demo+update@tandem.blog'::email, 'Demo Update', 'es') + , (5, 'admin+update@tandem.blog'::email, 'Admin Update', 'ca') + , (7, 'another@tandem.blog'::email, 'Another Admin', 'und') + $$, + 'Should have updated the base table’s data' +); + +select * +from finish(); + +rollback; diff --git a/verify/available_languages.sql b/verify/available_languages.sql new file mode 100644 index 0000000..e6dccdf --- /dev/null +++ b/verify/available_languages.sql @@ -0,0 +1,31 @@ +-- Verify camper:available_languages on pg + +begin; + +set search_path to public; + +select 1 / count(*) +from language +where lang_tag = 'und' + and name = 'Undefined' + and endonym = 'Undefined' + and not selectable + and currency_pattern = '%[3]s%.[1]*[2]f'; + +select 1 / count(*) +from language +where lang_tag = 'ca' + and name = 'Catalan' + and endonym = 'català' + and selectable + and currency_pattern = '%.[1]*[2]f %[3]s'; + +select 1 / count(*) +from language +where lang_tag = 'es' + and name = 'Spanish' + and endonym = 'español' + and selectable + and currency_pattern = '%.[1]*[2]f %[3]s'; + +rollback; diff --git a/verify/build_cookie.sql b/verify/build_cookie.sql new file mode 100644 index 0000000..db566e3 --- /dev/null +++ b/verify/build_cookie.sql @@ -0,0 +1,7 @@ +-- Verify camper:build_cookie on pg + +begin; + +select has_function_privilege('camper.build_cookie(camper.email, text)', 'execute'); + +rollback; diff --git a/verify/change_password.sql b/verify/change_password.sql new file mode 100644 index 0000000..a100f66 --- /dev/null +++ b/verify/change_password.sql @@ -0,0 +1,7 @@ +-- Verify camper:change_password on pg + +begin; + +select has_function_privilege('camper.change_password(text)', 'execute'); + +rollback; diff --git a/verify/check_cookie.sql b/verify/check_cookie.sql new file mode 100644 index 0000000..116a7bd --- /dev/null +++ b/verify/check_cookie.sql @@ -0,0 +1,7 @@ +-- Verify camper:check_cookie on pg + +begin; + +select has_function_privilege('public.check_cookie(text)', 'execute'); + +rollback; diff --git a/verify/current_user_cookie.sql b/verify/current_user_cookie.sql new file mode 100644 index 0000000..a606497 --- /dev/null +++ b/verify/current_user_cookie.sql @@ -0,0 +1,7 @@ +-- Verify camper:current_user_cookie on pg + +begin; + +select has_function_privilege('camper.current_user_cookie()', 'execute'); + +rollback; diff --git a/verify/current_user_email.sql b/verify/current_user_email.sql new file mode 100644 index 0000000..22f8268 --- /dev/null +++ b/verify/current_user_email.sql @@ -0,0 +1,7 @@ +-- Verify camper:current_user_email on pg + +begin; + +select has_function_privilege('camper.current_user_email()', 'execute'); + +rollback; diff --git a/verify/email.sql b/verify/email.sql new file mode 100644 index 0000000..3af4934 --- /dev/null +++ b/verify/email.sql @@ -0,0 +1,7 @@ +-- Verify camper:email on pg + +begin; + +select pg_catalog.has_type_privilege('camper.email', 'usage'); + +rollback; diff --git a/verify/encrypt_password.sql b/verify/encrypt_password.sql new file mode 100644 index 0000000..8e0c5ed --- /dev/null +++ b/verify/encrypt_password.sql @@ -0,0 +1,22 @@ +-- Verify camper:encrypt_password on pg + +begin; + +select has_function_privilege('auth.encrypt_password()', 'execute'); + +select 1 / count(*) +from pg_trigger +where not tgisinternal + and tgname = 'encrypt_password' + and tgrelid = 'auth.user'::regclass + and tgtype = b'00010111'::int; +-- │││││││ +-- ││││││└─> row +-- │││││└──> before +-- ││││└───> insert +-- │││└────> delete +-- ││└─────> update +-- │└──────> truncate +-- └───────> instead + +rollback; diff --git a/verify/ensure_role_exists.sql b/verify/ensure_role_exists.sql new file mode 100644 index 0000000..404f27b --- /dev/null +++ b/verify/ensure_role_exists.sql @@ -0,0 +1,22 @@ +-- Verify camper:ensure_role_exists on pg + +begin; + +select has_function_privilege('auth.ensure_role_exists()', 'execute'); + +select 1 / count(*) +from pg_trigger +where not tgisinternal + and tgname = 'ensure_role_exists' + and tgrelid = 'auth.user'::regclass + and tgtype = b'00010101'::int; +-- │││││││ +-- ││││││└─> row +-- │││││└──> before +-- ││││└───> insert +-- │││└────> delete +-- ││└─────> update +-- │└──────> truncate +-- └───────> instead + +rollback; diff --git a/verify/extension_citext.sql b/verify/extension_citext.sql new file mode 100644 index 0000000..91f2be8 --- /dev/null +++ b/verify/extension_citext.sql @@ -0,0 +1,10 @@ +-- Verify camper:extension_citext on pg + +begin; + +select 1 / count(*) +from pg_extension +where extname = 'citext' +; + +rollback; diff --git a/verify/extension_pgcrypto.sql b/verify/extension_pgcrypto.sql new file mode 100644 index 0000000..153c801 --- /dev/null +++ b/verify/extension_pgcrypto.sql @@ -0,0 +1,10 @@ +-- Verify camper:extension_pgcrypto on pg + +begin; + +select 1 / count(*) +from pg_extension +where extname = 'pgcrypto' +; + +rollback; diff --git a/verify/language.sql b/verify/language.sql new file mode 100644 index 0000000..2a74912 --- /dev/null +++ b/verify/language.sql @@ -0,0 +1,13 @@ +-- Verify camper:language on pg + +begin; + +select lang_tag + , name + , endonym + , selectable + , currency_pattern +from public.language +where false; + +rollback; diff --git a/verify/login.sql b/verify/login.sql new file mode 100644 index 0000000..ac43141 --- /dev/null +++ b/verify/login.sql @@ -0,0 +1,7 @@ +-- Verify camper:login on pg + +begin; + +select has_function_privilege('camper.login(camper.email, text, inet)', 'execute'); + +rollback; diff --git a/verify/login_attempt.sql b/verify/login_attempt.sql new file mode 100644 index 0000000..bf51507 --- /dev/null +++ b/verify/login_attempt.sql @@ -0,0 +1,13 @@ +-- Verify camper:login_attempt on pg + +begin; + +select attempt_id + , user_name + , ip_address + , success + , attempted_at +from auth.login_attempt +where false; + +rollback; diff --git a/verify/logout.sql b/verify/logout.sql new file mode 100644 index 0000000..2278e08 --- /dev/null +++ b/verify/logout.sql @@ -0,0 +1,7 @@ +-- Verify camper:logout on pg + +begin; + +select has_function_privilege('camper.logout()', 'execute'); + +rollback; diff --git a/verify/roles.sql b/verify/roles.sql new file mode 100644 index 0000000..5388668 --- /dev/null +++ b/verify/roles.sql @@ -0,0 +1,10 @@ +-- Verify camper:roles on pg + +begin; + +select pg_catalog.pg_has_role('guest', 'usage'); +select pg_catalog.pg_has_role('employee', 'usage'); +select pg_catalog.pg_has_role('admin', 'usage'); +select pg_catalog.pg_has_role('authenticator', 'usage'); + +rollback; diff --git a/verify/schema_auth.sql b/verify/schema_auth.sql new file mode 100644 index 0000000..fe80a6e --- /dev/null +++ b/verify/schema_auth.sql @@ -0,0 +1,7 @@ +-- Verify camper:schema_auth on pg + +begin; + +select pg_catalog.has_schema_privilege('auth', 'usage'); + +rollback; diff --git a/verify/schema_camper.sql b/verify/schema_camper.sql new file mode 100644 index 0000000..c6701bf --- /dev/null +++ b/verify/schema_camper.sql @@ -0,0 +1,7 @@ +-- Verify camper:schema_camper on pg + +begin; + +select pg_catalog.has_schema_privilege('camper', 'usage'); + +rollback; diff --git a/verify/schema_public.sql b/verify/schema_public.sql new file mode 100644 index 0000000..1964908 --- /dev/null +++ b/verify/schema_public.sql @@ -0,0 +1,7 @@ +-- Verify camper:schema_public on pg + +begin; + +select pg_catalog.has_schema_privilege('public', 'usage'); + +rollback; diff --git a/verify/set_cookie.sql b/verify/set_cookie.sql new file mode 100644 index 0000000..8e8b140 --- /dev/null +++ b/verify/set_cookie.sql @@ -0,0 +1,7 @@ +-- Verify camper:set_cookie on pg + +begin; + +select has_function_privilege('public.set_cookie(text)', 'execute'); + +rollback; diff --git a/verify/user.sql b/verify/user.sql new file mode 100644 index 0000000..c2606f6 --- /dev/null +++ b/verify/user.sql @@ -0,0 +1,17 @@ +-- Verify camper:user on pg + +begin; + +select user_id + , email + , name + , password + , role + , lang_tag + , cookie + , cookie_expires_at + , created_at +from auth."user" +where false; + +rollback; diff --git a/verify/user_profile.sql b/verify/user_profile.sql new file mode 100644 index 0000000..f17cbf1 --- /dev/null +++ b/verify/user_profile.sql @@ -0,0 +1,45 @@ +-- Verify camper:user_profile on pg + +begin; + +select user_id + , email + , name + , role + , lang_tag + , csrf_token +from camper.user_profile +where false; + +select has_function_privilege('camper.update_user_profile()', 'execute'); + +select 1 / count(*) +from pg_trigger +where not tgisinternal + and tgname = 'update_user_profile' + and tgrelid = 'camper.user_profile'::regclass + and tgtype = b'01010001'::int; +-- │││││││ +-- ││││││└─> row +-- │││││└──> before +-- ││││└───> insert +-- │││└────> delete +-- ││└─────> update +-- │└──────> truncate +-- └───────> instead +select 1 / count(*) +from pg_trigger +where not tgisinternal + and tgname = 'encrypt_password' + and tgrelid = 'auth.user'::regclass + and tgtype = b'00010111'::int; +-- │││││││ +-- ││││││└─> row +-- │││││└──> before +-- ││││└───> insert +-- │││└────> delete +-- ││└─────> update +-- │└──────> truncate +-- └───────> instead + +rollback;