Setup authentication schema and user relation
User authentication is based on PostgREST’s[0]: There is a noninherit
role, authenticator, whose function is only to switch to a different
role according to the application’s session. Accordingly, this role has
no permission for anything.
The roles that this authentication can switch to are guest, invoicer, or
admin. Guest is for anonymous users, when they need to login or
register; invoicers are regular users; and admin are application’s
administrators, that can change other user’s status, when they have to
be removed or have they password changed, for example.
The user relation is actually inaccessible to all roles and can only be
used through a security definer function, login, so that passwords are
not accessible from the application.
I hesitated on what to use as the user’s primary key. The email seemed
a good candiate, because it will be used for login. But something rubs
me the wrong way.
It is not that they can change because, despite what people on the
Internet keeps parroting, they do not need to be “immutable”, PostgreSQL
can cascade updates to foreign keys, and people do **not** change email
addresses that ofter.
What i **do** know is that email addresses should be unique in order to
be used for login and password, hovewer i had to decide what “unique”
means here, because the domain part is case insensitive, but the local
part who knows? I made the arbitrary decision of assuming that the
whole address is case sensitive.
I have the feeling that this will bite me harder in the ass than using
it as the primary key.
[0]: https://postgrest.org/en/stable/auth.html
2023-01-13 00:43:20 +00:00
|
|
|
-- Test user
|
|
|
|
set client_min_messages to warning;
|
|
|
|
create extension if not exists pgtap;
|
|
|
|
reset client_min_messages;
|
|
|
|
|
|
|
|
begin;
|
|
|
|
|
Add user_profile view to update the profile with form
Since users do not have access to the auth scheme, i had to add a view
that selects only the data that they can see of themselves (i.e., no
password or cookie).
I wanted to use the `request.user.id` setting that i set in
check_cookie, but this would be bad because anyone can change that
parameter and, since the view is created by the owner, could see and
*change* the values of everyone just by knowing their id. Thus, now i
use the cookie instead, because it is way harder to figure out, and if
you already have it you can just set to your browser and the user is
fucked anyway; the database can not help here.
I **am** going to use the user id in row level security policies, but
not the value coming for the setting but instaed the one in the
`user_profile`, since it already is “derived” from the cookie, that’s
why i added that column to the view.
The profile includes the language, that i do not use it yet to switch
the locale, so i had to add a relation of the available languages, for
constraint purposes. There is no NULL language, and instead i added the
“Undefined” language, with ‘und’ tag’, to represent “do not know/use
content negotiation”.
The languages in that relation are the same i used to have inside
locale.go, because there is no point on having options for languages i
do not have the translation for, so i now configure the list of
available languages user in content negotiation from that relation.
Finally, i have added all font from RemixIcon because that’s what we
used in the design and i am going to use quite a lot of them.
There is duplication in the views; i will address that in a different
commit.
2023-01-22 01:23:09 +00:00
|
|
|
select plan(51);
|
Setup authentication schema and user relation
User authentication is based on PostgREST’s[0]: There is a noninherit
role, authenticator, whose function is only to switch to a different
role according to the application’s session. Accordingly, this role has
no permission for anything.
The roles that this authentication can switch to are guest, invoicer, or
admin. Guest is for anonymous users, when they need to login or
register; invoicers are regular users; and admin are application’s
administrators, that can change other user’s status, when they have to
be removed or have they password changed, for example.
The user relation is actually inaccessible to all roles and can only be
used through a security definer function, login, so that passwords are
not accessible from the application.
I hesitated on what to use as the user’s primary key. The email seemed
a good candiate, because it will be used for login. But something rubs
me the wrong way.
It is not that they can change because, despite what people on the
Internet keeps parroting, they do not need to be “immutable”, PostgreSQL
can cascade updates to foreign keys, and people do **not** change email
addresses that ofter.
What i **do** know is that email addresses should be unique in order to
be used for login and password, hovewer i had to decide what “unique”
means here, because the domain part is case insensitive, but the local
part who knows? I made the arbitrary decision of assuming that the
whole address is case sensitive.
I have the feeling that this will bite me harder in the ass than using
it as the primary key.
[0]: https://postgrest.org/en/stable/auth.html
2023-01-13 00:43:20 +00:00
|
|
|
|
|
|
|
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', 'invoicer', array []::text[]);
|
|
|
|
select table_privs_are('user', 'admin', array []::text[]);
|
|
|
|
select table_privs_are('user', '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', 'numerus.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');
|
|
|
|
|
Add user_profile view to update the profile with form
Since users do not have access to the auth scheme, i had to add a view
that selects only the data that they can see of themselves (i.e., no
password or cookie).
I wanted to use the `request.user.id` setting that i set in
check_cookie, but this would be bad because anyone can change that
parameter and, since the view is created by the owner, could see and
*change* the values of everyone just by knowing their id. Thus, now i
use the cookie instead, because it is way harder to figure out, and if
you already have it you can just set to your browser and the user is
fucked anyway; the database can not help here.
I **am** going to use the user id in row level security policies, but
not the value coming for the setting but instaed the one in the
`user_profile`, since it already is “derived” from the cookie, that’s
why i added that column to the view.
The profile includes the language, that i do not use it yet to switch
the locale, so i had to add a relation of the available languages, for
constraint purposes. There is no NULL language, and instead i added the
“Undefined” language, with ‘und’ tag’, to represent “do not know/use
content negotiation”.
The languages in that relation are the same i used to have inside
locale.go, because there is no point on having options for languages i
do not have the translation for, so i now configure the list of
available languages user in content negotiation from that relation.
Finally, i have added all font from RemixIcon because that’s what we
used in the design and i am going to use quite a lot of them.
There is duplication in the views; i will address that in a different
commit.
2023-01-22 01:23:09 +00:00
|
|
|
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');
|
|
|
|
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
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);
|
|
|
|
|
Setup authentication schema and user relation
User authentication is based on PostgREST’s[0]: There is a noninherit
role, authenticator, whose function is only to switch to a different
role according to the application’s session. Accordingly, this role has
no permission for anything.
The roles that this authentication can switch to are guest, invoicer, or
admin. Guest is for anonymous users, when they need to login or
register; invoicers are regular users; and admin are application’s
administrators, that can change other user’s status, when they have to
be removed or have they password changed, for example.
The user relation is actually inaccessible to all roles and can only be
used through a security definer function, login, so that passwords are
not accessible from the application.
I hesitated on what to use as the user’s primary key. The email seemed
a good candiate, because it will be used for login. But something rubs
me the wrong way.
It is not that they can change because, despite what people on the
Internet keeps parroting, they do not need to be “immutable”, PostgreSQL
can cascade updates to foreign keys, and people do **not** change email
addresses that ofter.
What i **do** know is that email addresses should be unique in order to
be used for login and password, hovewer i had to decide what “unique”
means here, because the domain part is case insensitive, but the local
part who knows? I made the arbitrary decision of assuming that the
whole address is case sensitive.
I have the feeling that this will bite me harder in the ass than using
it as the primary key.
[0]: https://postgrest.org/en/stable/auth.html
2023-01-13 00:43:20 +00:00
|
|
|
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;
|