/* ----------
 * minimal_demo.sql
 *
 *      A minimal demo of veil2, build using ../sql/veil2_template.sql
 *
 *      Copyright (c) 2020 Marc Munro
 *      Author:  Marc Munro
 *	License: GPL V3
 *
 * ----------
 */

-- STEP 0
-- Define your database objects.  These will intially be entirely
-- independent of Veil2 unless you choose to use some of the Veil2
-- tables, such as roles, directly.

-- This is a simple database that allows users to post stuff, that
-- other users may or may not be able to see.

do
$$
declare
  _result integer;
begin
  select 1 into _result from pg_roles where rolname = 'demouser';
  if not found then
    execute 'create role demouser with login password ''pass''';
  end if;
end;
$$;

-- Trigger function usable by various triggers.
create or replace
function nocando() returns trigger as
$$
begin
  -- Prevent whatever was being attempted.  This should really take a
  -- parameter for an error message and cause the transaction to fail,
  -- but this is good enough for now.
  return null;
end;
$$
language plpgsql security definer volatile;


create table users (
  accessor_id integer not null primary key,
  username    text not null unique,
  password    text not null
);

insert
  into users
       (accessor_id, username, password)
values (1, 'Alice', 'passwd_alice'),
       (2, 'Bob', 'passwd_bob'),
       (3, 'Carol', 'passwd_carol'),
       (4, 'Dave', 'passwd_dave'),
       (5, 'Eve', 'passwd_eve'),
       (6, 'Sue', 'passwd_sue'),
       (7, 'Simon', 'passwd_simon');

create table follower_types (
  follower_type_id	    integer not null primary key,
  follower_type_name	    text not null unique,
  description		    text
);

insert
  into follower_types
       (follower_type_id, follower_type_name, description)
values (1, 'friend_request',
        'This requests that follows becomes a friend to follower'),
       (2, 'friend_acceptance',
        'This identifies that follower has become a friend to follows'),
       (3, 'follower',
        'This identifies that follower anonymously follows follows');

create table followers (
  follower_type_id     integer not null,
  follower	       integer not null,
  follows	       integer not null
);

alter table followers add constraint follower__pk
  primary key(follower, follows, follower_type_id);

alter table followers add constraint follower__type_fk
  foreign key(follower_type_id)
  references follower_types(follower_type_id);

alter table followers add constraint follower__follower_fk
  foreign key(follower)
  references users(accessor_id);

alter table followers add constraint follower__follows_fk
  foreign key(follows)
  references users(accessor_id);

create or replace
function follower_friend_check() returns trigger as
$$
declare
  _ok boolean;
begin
  -- Ensure that there is a friend request matching the friend
  -- acceptance.
  select true
    into _ok
    from followers f
   where f.follower_type_id = 1
     and f.follower = new.follows
     and f.follows = new.follower;
  if found then
    return new;
  else
    -- We could maybe raise an error here but the important thing is
    -- that the friend request fails.
    return null;
  end if;
end;
$$
language plpgsql security definer volatile;

create trigger follower__bi_trg
  before insert on followers
  for each row
  when (new.follower_type_id = 2)
  execute procedure follower_friend_check();

-- Prevent updates, even by db owner.
create trigger follower__bu_trg
  before update on followers
  for each row
  execute procedure nocando();

create or replace
function follower__bd() returns trigger as
$$
begin
  -- Deleting the friend request, means that the friendship gets
  -- cancelled.  All other deletions need no action
  delete
    from followers f
   where f.follower_type_id = 2
     and f.follower = old.follows
     and f.follows = old.follower;
  return old;
end;
$$
language plpgsql security definer volatile;

create trigger follower__bd_trg
  before delete on followers
  for each row
  when (old.follower_type_id = 1)
  execute procedure follower__bd();

-- TODO: we ought to have a constraint that you can't create a friend
-- without first having a friend request.

insert
  into followers
       (follower_type_id, follower, follows)
values (1, 4, 2),
       (2, 2, 4);

create table post_types (
  post_type_id		    integer not null primary key,
  post_type_name	    text not null unique,
  description		    text
);

insert
  into post_types
       (post_type_id, post_type_name, description)
values (11, 'public',
        'Anyone can view this'),
       (12, 'private',
        'Only poster can view this'),
       (13, 'for-friends',
        'Only friends can view this');

create sequence post_id_seq start with 900;
grant all on sequence post_id_seq to veil_user;
create table posts (
  post_id	       integer not null primary key
  		           default nextval('post_id_seq'),
  post_type_id	       integer not null,
  poster	       integer not null,
  post		       text not null
);

alter table posts add constraint post__type_fk
  foreign key(post_type_id)
  references post_types(post_type_id);

alter table posts add constraint post__poster_fk
  foreign key(poster)
  references users(accessor_id);


-- STEP 1: Install Veil2

-- If you haven't installed the extension on your server, and have the
-- pgxn client installed:
-- $ pgxn install veil2

create extension if not exists veil2 cascade; 


-- STEP 2:
-- Define scopes

-- 2.1 Create new scopes by inserting records into veil2.scope_types.
-- 
 
insert 
  into veil2.scope_types
       (scope_type_id, scope_type_name,
        description)
values (3, 'friend', 'Friends get to see more than anonymous followers');


-- STEP 3:
-- Authentication stuff.

-- The Veil2 system defaults work for authentication.

-- Define this just to stop veil2.implementation_status() from
-- complaining that it is not done.  This is just the same as the
-- system-supplied version.

create or replace
view veil2.my_accessor_contexts (
  accessor_id, context_type_id, context_id
) as
select accessor_id, 1, 0
  from veil2.accessors;

-- 3.3 get_accessor()
-- Create a my_get_accessor() function that will take a username and
-- context, and return an accessor_id

create or replace
function veil2.my_get_accessor(
    username in text,
    context_type_id in integer,
    context_id in integer)
  returns integer as
$$
declare
  result integer;
  _username text;
begin
  _username = username;
  select accessor_id
    into result
    from users u
   where u.username = _username;
  return result;
end;
$$
language 'plpgsql' security definer stable leakproof;


-- STEP 4:
-- Link Veil2 accessors to your database's users.

-- 4.1 Create a mapping table
-- Note that you may have multiple users tables and so you may need 
-- multiple FK fields back to your database.  If so, you may want to
-- add a check constraint to ensure that only 1 of them is not null.


create table veil2.accessor_users_map (
  accessor_id		integer not null
);

alter table veil2.accessor_users_map add constraint accessor_users__pk 
  primary key (accessor_id);

alter table veil2.accessor_users_map add constraint accessor__accessor_fk
  foreign key (accessor_id)
  references veil2.accessors(accessor_id);

-- And you must create FKs
alter table veil2.accessor_users_map add constraint accessor__user_fk
  foreign key (accessor_id)
  references users(accessor_id);

-- 4.2 Copy Existing User Records
-- 

insert
  into veil2.accessors
      (accessor_id, notes)
select accessor_id, 'Created as part of initial migration'
  from users;

-- 4.3 Copy existing authentication records
-- This is going to be tricky as it is unlikely that the
-- authentication tokens in your database are going to match those
-- exepected by Veil2.  See the Veil2 documentation for Step 4 for
-- more information.

insert
  into veil2.authentication_details
      (accessor_id, authentication_type, authent_token)
select accessor_id, 'bcrypt', veil2.bcrypt(password)
  from users;

-- Clear out the original password fields, as we don't need them anymore.
update users
  set password = '[REDACTED]

-- 4.4 Create Referential Integrity Triggers
-- On insert and on delete triggers are needed as a minimum.  If
-- updates are allowed to change the user id fields, then we will also
-- need to deal with updates.  Far better though to simply have an
-- on-update trigger that fails if such an attempt is made. 

-- Can't do a before insert here as we need the users record to
-- exist before we can create the map table entry.
--
create or replace
function user__ai() returns trigger as
$$
begin
  -- A new users record has been created.  Create the
  -- appropriate records in veil2
  insert 
    into veil2.accessors
         (accessor_id)
  values (new.accessor_id);
  insert
    into veil2.accessor_users_map
         (accessor_id)
  values (new.accessor_id);

  insert
   into veil2.authentication_details
        (accessor_id, authentication_type, authent_token)
  select new.accessor_id, 'bcrypt', veil2.bcrypt(new.password);

  update users
     set password = '[REDACTED]
   where accessor_id = new.accessor_id;
  return new;
end;
$$
language plpgsql security definer volatile;

create trigger user__ai_trg
  after insert on users
  for each row
  execute procedure user__ai();


create or replace
function user__bu() returns trigger as
$$
begin
  if new.accessor_id != old.accessor_id or
     new.username != old.username then
    -- You can't do that.
    return null;
  end if;
  if new.password != 'xxxx' then
      update veil2.authentication_details
         set authent_token = veil2.bcrypt(new.password)
       where accessor_id = new.accessor_id;
  
  end if;
  new.password = '[REDACTED]
  return new;
end;
$$
language plpgsql security definer volatile;

create trigger user__bu_trg
  before update on users
  for each row
  execute procedure user__bu();

-- Do not allow deletions.  Ours is not to reason why.
create trigger user__bd_trg
  before delete on users
  for each row
  execute procedure nocando();


-- STEP 5: Link your scopes

-- 5.1 Extend the veil2.scopes table to link to the tables that define
-- your scopes.

-- No need to link scopes as all of our relational scopes are based on
-- accessor_id.  This means that we do not need to map between Veil2's
-- scope_ids and the ids used in our demo.  So, we can completely
-- ignore the veil2.scopes table.  Yay!

-- Insert a dummy record to quiten veil2.implementation_status();
insert into veil2.scopes values (3, -1);

-- 5.5 Update the all_accessor_roles view

-- Having a friend gives us an implied 'friend' role in the friend's
-- context.

create or replace
view veil2.my_all_accessor_roles (
  accessor_id, role_id, context_type_id, context_id
) as
select accessor_id, role_id,
       context_type_id, context_id
  from veil2.accessor_roles
 union all
select follows, 5,   -- The friend role
       3, follower   -- follows has friend role in friend context of follower
  from followers
 where follower_type_id = 2
 union all
select follower, 5,
       3, follows   -- follower has friend role in friend context of follows
  from followers
 where follower_type_id = 2;
  
-- 5.6 Create triggers on updates to accessor roles
-- There is no need to do this on the veil2.accessor_roles table as
-- this is already done.

-- For each table providing data to my_all_accessor_roles...

-- Clearing the accessor_privileges_cache is slightly more complex
-- than the standard clear_accessor_privs_cache_entry can handle, so
-- we have to create our own trigger function.
--
create or replace
function veil2.clear_accessor_privs_for_follower()
  returns trigger as
$$
begin
  if tg_op = 'INSERT' or tg_op = 'UPDATE' then
    delete
      from veil2.accessor_privileges_cache
     where accessor_id = new.follower
        or accessor_id = new.follows;
    if (tg_op = 'UPDATE') and
       (old.follower != new.follower) then
      delete
        from veil2.accessor_privileges_cache
       where accessor_id = old.follower
          or accessor_id = old.follows;
    end if;
    return new;
  elsif tg_op = 'DELETE' then
    delete
      from veil2.accessor_privileges_cache
     where accessor_id = old.follower
        or accessor_id = old.follows;
    return old;
  end if;
end;
$$
language plpgsql security definer volatile;



create trigger follower__ad_trg
  after delete on followers
  for each row
  when (old.follower_type_id = 2)
  execute procedure veil2.clear_accessor_privs_for_follower();

create trigger follower__ai_trg
  after insert on followers
  for each row
  when (new.follower_type_id = 2)
  execute procedure veil2.clear_accessor_privs_for_follower();

-- STEP 6: Define Scope Hierarchy

-- We have no scope hierarchy, so this is a bit of a no-op.

-- 6.1
-- Define the my_superior_scopes view
create or replace
view veil2.my_superior_scopes (
  scope_type_id, scope_id,
  superior_scope_type_id, superior_scope_id
) as
select 0, 0, 0, 0
 where false;


-- STEP 7:
-- Define privileges.  Note that priv_ids below 20 are reserved for Veil2
-- objects

insert into veil2.privileges
       (privilege_id, privilege_name,
        promotion_scope_type_id, description)
values (20, 'select users',
        null, 'select from the users table'),
       (21, 'insert users',
        null, 'insert into the users table'),
       (22, 'update users',
        null, 'update the users table'),
       (23, 'delete users',
        null, 'delete the users table'),
       (24, 'select follower_types',
        1, 'select from the follower_types table'), -- applies in global scope
       (25, 'select followers',
        null, 'select from the followers table'),
       (26, 'insert followers',
        null, 'insert into the followers table'),
       (27, 'delete followers',
        null, 'delete from the followers table'),
       (28, 'select post_types',
        1, 'select from the post_types table'), -- applies in global scope
       (29, 'select posts',
        null, 'select from the posts table'),
       (30, 'insert posts',
        null, 'insert into the posts table');
	

-- STEP 8:
-- Define/integrate roles

-- We use only veil2's roles so no integration needed.

-- 8.2 Create new roles and mappings
--

-- Use of roles
-- - viewer is assigned implicitly for all of your friends and allows
--   you to view friend posts.
-- - personal_context gives you the ability to post
-- - admin must be assigned explicitly
-- - anonymous is the role assigned to anonymous users


insert
  into veil2.roles
       (role_id, role_type_id, role_name,
        implicit, immutable, description)
values (5, 1, 'viewer', false, true, 'can view all appropriate data'),
       (6, 1, 'admin', true, false, 'can administer the system');

-- The admin role can be assigned through accessor_roles in global
-- context.
-- The user role is implied for all users and applies in personal
-- context.

insert into veil2.role_roles
       (primary_role_id, assigned_role_id,
        context_type_id, context_id)
values (6, 5, 1, 0),
       (6, 6, 1, 0);

insert into veil2.role_privileges
       (role_id, privilege_id)
values -- Viewer role privs
       (5, 20),
       (5, 24),
       (5, 25),
       (5, 29),
       -- Admin role privs
       (6, 20),
       (6, 21),
       (6, 25),
       (6, 29),
          -- Personal context role privs
       (2, 20),
       (2, 22),
       (2, 24),
       (2, 25),
       (2, 26),
       (2, 29),
       (2, 30);


-- STEP 9:
-- Secure Tables
-- For each table

grant veil_user to demouser;

alter table users enable row level security;

create policy users__select
    on users
   for select
 using (veil2.i_have_personal_priv(20, accessor_id) or
        veil2.i_have_priv_in_scope_or_global(20, 3, accessor_id));

create policy users__insert
    on users
   for insert
  with check (veil2.i_have_personal_priv(21, accessor_id) or
              veil2.i_have_global_priv(21));

create policy users__update
    on users
   for update
  using (veil2.i_have_personal_priv(22, accessor_id) or
         veil2.i_have_global_priv(22));

revoke all on users from public;
grant select, insert, update on users to demouser;


alter table follower_types enable row level security;

create policy follower_types__select
    on follower_types
   for select
 using (veil2.i_have_global_priv(24));

revoke all on follower_types from public;
grant select on follower_types to demouser;


alter table followers enable row level security;

create policy followers__select
    on followers
   for select
 using (veil2.i_have_global_priv(25) or
        veil2.i_have_personal_priv(25, follower) or -- Anyone I follow
        veil2.i_have_personal_priv(25, follows));   -- Anyone following me

create policy followers__insert
    on followers
   for insert
  with check (veil2.i_have_personal_priv(26, follower) or
	      veil2.i_have_global_priv(26));

revoke all on followers from public;
grant select, insert, delete on followers to demouser;


alter table post_types enable row level security;

create policy post_types__select
    on post_types
   for select
 using (veil2.i_have_global_priv(28));

revoke all on post_types from public;
grant select, insert, delete on post_types to demouser;


alter table posts enable row level security;

create policy posts__select
    on posts
   for select
 using (veil2.i_have_personal_priv(29, poster) or
        veil2.i_have_global_priv(29) or
        (    post_type_id = 12 -- friend posts
	 and veil2.i_have_priv_in_scope(29, 3, poster)) or
        (    post_type_id = 11  -- Public posts require connect privilege.
	 and veil2.i_have_global_priv(0)));

-- The following checks *only* for personal priv as that will
-- ensure that we are the poster (ie we are not faking the id)
create policy posts__insert
    on posts
   for insert
  with check (veil2.i_have_personal_priv(30, poster));

revoke all on post_types from public;
grant select, insert, delete on post_types to demouser;

revoke all on posts from public;
grant select, insert, delete on posts to demouser;


-- STEP 10:
-- Secure Views

/*
-- For each view

create or replace
view view_name as
select ....
 where veil2.i_have_xxxx(<priv>,??);

-- Create appropriate instead-of triggers if the view is to be
-- updatable.

*/


-- Step 11:
-- Assign roles to accessors

-- ...

insert
  into veil2.accessor_roles
       (accessor_id, role_id, context_type_id, context_id)
values (1, 0, 1, 0),    -- Alice gets connect
       (1, 6, 1, 0),	-- Alice gets admin
       (2, 0, 1, 0),    -- Bob gets connect
       (3, 0, 1, 0),    -- Carol gets connect
       (3, 1, 1, 0),	-- Carol gets superuser
       (5, 0, 1, 0);	-- Eve gets connect

update veil2.system_parameters
   set parameter_value = 'false'
 where parameter_name = 'error on uninitialized session';

