Appendix C. Veil2 Functions and Triggers

The following functions, defined in sql/veil2--0.9.1.sql, are for managing Referential Integrity:

These functions are for managing user-provided database objects (functions and views):

These functions are for authentication handling:

These functions are for session handling:

These functions are for testing session privileges:

Utility and administration functions:

Functions for refreshing materialized views:

Functions for checking the status of your Veil2 secured database implementation:

Other trigger functions:

And these triggers exist:

C.1. Functions for managing Referential Integrity

C.1.1. context_exists_chk()

function veil2.context_exists_chk()
  returns trigger as
$$
begin
  if not exists (
      select null
        from veil2.scopes a
       where a.scope_type_id = new.context_type_id
         and a.scope_id = new.context_id)
  then
    -- Pseudo Integrity Constraint Violation
    raise exception using
            message = TG_OP || ' on table "' ||
	    	      TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME ||
		      '" violates foreign key constraint "' ||
		      coalesce(TG_ARGV[0], 'null') || '"',
		detail = 'Key (scope_type_id, scope_id)=(' ||
		         coalesce(new.context_type_id::text, 'null') || ', ' ||
			 coalesce(new.context_id::text, 'null') || 
			 ') is not present in table "veil2.scopes"',
		errcode = '23503';
  end if;
  return new;
end;
$$
language plpgsql security definer stable;

Trigger to be used instead of FK-constraints against the scopes table. This is because we expect to use inheritence to extend the scopes table to contain references to user-provided tables, and inheritence does not work well with foreign-key constraints.

C.2. Functions for Management of User-Provided Database Objects

C.2.1. function_definition()

function veil2.function_definition(fn_name text, fn_oid oid)
  returns text as
$$
declare
  rec record;
  _result text;
begin
  -- Query (modified) from \df+ in psql
  select 
    pg_catalog.pg_get_function_arguments(p.oid) as arg_types,
    pg_catalog.pg_get_function_result(p.oid) as result_type,
    p.prosrc as source,
    l.lanname as language,
    case when prosecdef then 'definer' else 'invoker' end as security,
    case
      when p.provolatile = 'i' then 'immutable'
      when p.provolatile = 's' then 'stable'
      when p.provolatile = 'v' then 'volatile'
    end as volatility,
    case
      when p.proparallel = 'r' then 'restricted'
      when p.proparallel = 's' then 'safe'
      when p.proparallel = 'u' then 'unsafe'
    end as parallel
  into rec
  from pg_catalog.pg_proc p
       left join pg_catalog.pg_language l on l.oid = p.prolang
  where p.oid = fn_oid;
  if found then
    _result := 'create or replace function veil2.' || fn_name ||
              '(' || rec.arg_types || ') returns ' ||
	      rec.result_type || ' as $xyzzy$' ||
	      rec.source || '$xyzzy$ language ''' ||
	      rec.language || ''' security ' ||
	      rec.security || ' ' || rec.volatility ||
	      ' parallel ' || rec.parallel;
  end if;
  return _result;
end;
$$
language plpgsql security definer stable;

Returns the text to create a function named fn_name, based on the function definition provided by fn_oid. This is used by veil2.install_user_functions() and veil2.restore_system_functions()

C.2.2. replace_function()

function veil2.replace_function(fn_name text, from_fn oid)
  returns void as
$$
declare
  fn_defn text;
begin
  fn_defn := veil2.function_definition(fn_name, from_fn);
  --raise warning '%', fn_defn;
  execute fn_defn;
end;
$$
language plpgsql security definer volatile;

Create or replace the function named fn_name based on the definition given in fn_oid. Used for installing user-provided functions in place of the veil2 system-provided ones.

C.2.3. restore_system_functions()

function veil2.restore_system_functions()
  returns void as
$$
declare
  rec record;
begin
  for rec in
    select po.proname, pb.oid backup_oid, po.oid as old_oid
      from pg_catalog.pg_namespace n
     inner join pg_catalog.pg_proc pb -- backup proc
        on pb.pronamespace = n.oid
       and pb.proname like 'backup_%'
      inner join pg_catalog.pg_proc po -- original proc
        on po.pronamespace = n.oid
       and pb.proname = 'backup_' || po.proname
     where n.nspname = 'veil2'
  loop
    perform veil2.replace_function(rec.proname, rec.backup_oid);
  end loop;
end;
$$
language plpgsql security definer volatile;

Restore system-provided functions that have been replaced by user-provided ones. The originals for the system-provided functions will have been saved as backups by veil2.install_user_functions()

C.2.4. install_user_functions()

function veil2.install_user_functions()
  returns void as
$$
declare
  rec record;
begin
  for rec in
    select po.proname, p.oid new_oid, po.oid as old_oid,
    	   case when pb.proname is null then false
	   else true end as have_backup
      from pg_catalog.pg_namespace n
     inner join pg_catalog.pg_proc p -- replacement proc
        on p.pronamespace = n.oid
       and p.proname like 'my%'
      inner join pg_catalog.pg_proc po -- original proc
        on po.pronamespace = n.oid
       and p.proname = 'my_' || po.proname
      left outer join pg_catalog.pg_proc pb -- backup of original proc
        on pb.pronamespace = n.oid
       and pb.proname = 'backup_' || po.proname
     where n.nspname = 'veil2'
  loop
    if not rec.have_backup then
      perform veil2.replace_function('backup_' || rec.proname,
	     			     rec.old_oid);
    end if;
    perform veil2.replace_function(rec.proname, rec.new_oid);
  end loop;
end;
$$
language plpgsql security definer volatile;

Install any user-provided functions that are to replace system-provided ones. The original versions of the system-provided functions will be saved as backups.

C.2.5. function_exists()

function veil2.function_exists(fn_name text)
  returns boolean as
$$
declare
  _result boolean;
begin
  select true
    into _result
     from pg_catalog.pg_namespace n
    inner join pg_catalog.pg_proc pn -- new proc
       on pn.pronamespace = n.oid
      and pn.proname = fn_name
     where n.nspname = 'veil2';
  return found;
end;
$$
language plpgsql security definer volatile;

Predicate returning true if a function named fn_name exists in the veil2 schema.

C.2.6. view_exists()

function veil2.view_exists(vw_name text)
  returns boolean as
$$
declare
  _result boolean;
begin
  select true
    into _result
     from pg_catalog.pg_namespace n
    inner join pg_catalog.pg_class rn -- new reln
       on rn.relnamespace = n.oid
      and rn.relname = vw_name
      and rn.relkind = 'v'
     where n.nspname = 'veil2';
  return found;
end;
$$
language plpgsql security definer volatile;

Predicate returning true if a view named vw_name exists in the veil2 schema.

C.2.7. replace_view()

function veil2.replace_view(view_name text, from_view oid)
  returns void as
$$
declare
  view_defn text;
begin
  view_defn := 'create or replace view veil2.' ||
  	       view_name || ' as ' ||
  	       pg_catalog.pg_get_viewdef(from_view, true);
  --raise warning '%', view_defn;
  execute view_defn;
end;
$$
language plpgsql security definer volatile;

Create or replace the view named view_name based on the definition given in view_oid. Used for installing user-provided views in place of the veil2 system-provided ones.

C.2.8. restore_system_views()

function veil2.restore_system_views()
  returns void as
$$
declare
  rec record;
begin
  for rec in
    select vo.relname, vb.oid backup_oid, vo.oid as old_oid
      from pg_catalog.pg_namespace n
     inner join pg_catalog.pg_class vb -- backup view
        on vb.relnamespace = n.oid
       and vb.relkind = 'v'
       and vb.relname like 'backup_%'
     inner join pg_catalog.pg_class vo -- original view
        on vo.relnamespace = n.oid
       and vo.relkind = 'v'
       and vb.relname = 'backup_' || vo.relname
     where n.nspname = 'veil2'
  loop
    perform veil2.replace_view(rec.relname, rec.backup_oid);
  end loop;
end;
$$
language plpgsql security definer volatile;

Restore system-provided views that have been replaced by user-provided ones. The originals for the system-provided view will have been saved as backups by veil2.install_user_views()

C.2.9. install_user_views()

function veil2.install_user_views()
  returns void as
$$
declare
  rec record;
begin
  for rec in
    select vo.relname, v.oid new_oid, vo.oid as old_oid,
           case when vb.relname is null then false
           else true end as have_backup
      from pg_catalog.pg_namespace n
     inner join pg_catalog.pg_class v -- Replacement view
        on v.relnamespace = n.oid
       and v.relkind = 'v'
     inner join pg_catalog.pg_class vo -- original view
        on vo.relnamespace = n.oid
       and vo.relkind = 'v'
       and v.relname = 'my_' || vo.relname
      left outer join pg_catalog.pg_class vb -- backup of original view
        on vb.relnamespace = n.oid
       and vb.relkind = 'v'
       and vb.relname = 'backup_' || vo.relname
     where n.nspname = 'veil2'
  loop
    if not rec.have_backup then
      perform veil2.replace_view('backup_' || rec.relname,
	     			 rec.old_oid);
    end if;
    perform veil2.replace_view(rec.relname, rec.new_oid);
  end loop;
end;
$$
language plpgsql security definer volatile;

Install any user-provided views that are to replace system-provided ones. The original versions of the system-provided views will be saved as backups.

C.2.10. init()

function veil2.init() returns void as
$$
begin
  perform veil2.install_user_functions();
  perform veil2.install_user_views();
  refresh materialized view veil2.all_superior_scopes;
  refresh materialized view veil2.all_role_privileges;
end;
$$
language plpgsql security definer volatile;

Perform some basic setup and reset tasks. This creates user-modifiable views that are not already defined and refreshes all materialized views. You should call it any time you have unexpected results. If it fixes your problem then you have a problem with the automatic refresh of one of the materialized views. If not, no harm will have been done.

C.2.11. deferred_install_fn()

function veil2.deferred_install_fn() returns trigger as
$$
begin
  perform veil2.init();
  return new;
end;
$$
language plpgsql security definer volatile;

Install user-provided functions and views. This is called from an after statement trigger so that we do not install the new versions of functions until the current versions, which we may be replacing, have completed. This may be overly prudent but it does no harm.

C.3. Authentication Functions

C.3.1. authenticate_false()

function veil2.authenticate_false(
    accessor_id integer,
    token text)
  returns boolean as
$$
select false;
$$
language sql security definer stable;

Authentication predicate for unimplemented or disabled authentication types. This function always returns false, causing authentication to fail.

C.3.2. authenticate_plaintext()

function veil2.authenticate_plaintext(
    accessor_id integer,
    token text)
  returns boolean as
$$
select coalesce(
    (select authent_token = authenticate_plaintext.token
       from veil2.authentication_details
      where accessor_id = authenticate_plaintext.accessor_id
        and authentication_type = 'plaintext'),
    false);
$$
language sql security definer stable;

Authentication predicate for plaintext authentication. Return true if the supplied token matches the stored authentication token for the accessor. This authentication mechanism exists primarily for demo purposes. DO NOT USE IT IN A REAL APPLICATION!

C.3.3. authenticate_bcrypt()

function veil2.authenticate_bcrypt(
    accessor_id integer,
    token text)
  returns boolean as
$$
select coalesce(
    (select authent_token = crypt(token, authent_token)
       from veil2.authentication_details
      where accessor_id = authenticate_bcrypt.accessor_id
        and authentication_type = 'bcrypt'),
    false);
$$
language sql security definer stable;

Authentication predicate for bcrypt authentication. Return true if running bcrypt on the supplied token, using the salt from the stored authentication token for the accessor, matches that stored authentication token.

Bcrypt is generally considered a step up from traditional hash-based password authentication, though it is essentially the same thing. In a hash-based authentication system, a user's password is stored as a, possibly salted, hash on the plaintext. Since hashes are one-way algorithms it is impossible to retrieve the original password from the hash. However, as computers have become more powerful, brute-force approaches have become more feasible. With a simple hash, it is is now possible to try every possible password until one matches the hash, whether salted or not in a matter of hours. Bcrypt makes brute-forcing difficult by using a compuatationally inefficient hash algorithm, which makes brute force attacks a very expensive proposition. Note that in attacks on hash-based passwords it is assumed that the hashed password has been compromised. Whether this is likely in database protected by Veil2 is moot, however there may be more likely avenues for attack as the hashed passwords can be pretty well locked down.

The current bcrypt implementation's biggest down-side, in common with traditional hash-based approaches, is that the user's password is sent to the server in plaintext before it is tested by bcrypting it. A better authentication method would avoid this.

C.3.4. authenticate()

function veil2.authenticate(
    accessor_id integer,
    authent_type text,
    token text)
  returns boolean as
$$
declare
  success boolean;
  authent_fn text;
  enabled boolean;
begin
  select t.enabled, t.authent_fn
    into enabled, authent_fn
    from veil2.authentication_types t
   where t.shortname = authent_type;
  if found and enabled then
    execute format('select * from %s(%s, %L)',
                   authent_fn, accessor_id, token)
       into success;
    return success;
  end if;
  return false;
end;
$$
language plpgsql security definer stable;

For the given accessor_id and authentication_type check whether token is an appropriate authentication.

C.4. Session Functions

C.4.1. session_ready()

function veil2.session_ready() returns boolean
     as '$libdir/veil2', 'veil2_session_ready'
     language C stable strict;

Predicate to indicate whether veil2.reset_session() has been successfully called for this session. If not, none of the C language i_have_privilege functions will return true.

This exists as a stand-alone function so that it may be used by user-defined functions.

The Doxygen documentation for this can be found here.

C.4.2. reset_session()

function veil2.reset_session() returns void
     as '$libdir/veil2', 'veil2_reset_session'
     language C volatile strict security definer;

Ensure our temp tables exist, are of the expected type (temporary tables); that the session user has no unexpected access rights on them; and clear them.

The Doxygen documentation for this can be found here.

C.4.3. reset_session_privs()

function veil2.reset_session_privs() returns void
     as '$libdir/veil2', 'veil2_reset_session_privs'
     language C volatile strict security definer;

Safely clear the contents of the veil2_session_privileges temporary table.

The Doxygen documentation for this can be found here.

C.4.4. get_accessor()

function veil2.get_accessor(
    username in text,
    context_type_id in integer,
    context_id in integer)
  returns integer as
$$
begin
  -- Cause any user-provided versions of veil2 functions to be
  -- installed before the current statement completes.
  insert into veil2.deferred_install values (now());
  
  if veil2.function_exists('my_get_accessor') then
    -- If we have a user-provided version of this function, we call it
    -- now.  The next time we are called, this, the system-provided
    -- version of the function, will have been replaced by the
    -- user-provided copy.  This version can be restored by calling
    -- veil2.restore_base_system().
    return veil2.my_get_accessor(username, context_type_id, context_id);
  end if;
  
  return 0;
end;
$$
language plpgsql security definer volatile;

Retrieve accessor_id based on username and context. A user-provided version of this, named my_get_accessor() should be created specifically for your application. It will be automatically installed when it is first needed. If you modify your version, you can update the system version by calling veil2.init().

C.4.5. new_session_context()

function veil2.new_session_context(
    accessor_id in integer,
    login_context_type_id in integer,
    login_context_id in integer,
    session_context_type_id in integer,
    session_context_id in integer,
    parent_session_id in integer default null,
    session_id out integer,
    mapping_context_type_id out integer,
    mapping_context_id out integer)
  returns record as
$$
  insert
    into veil2_session_context
        (accessor_id, session_id,
	 login_context_type_id, login_context_id,
	 session_context_type_id, session_context_id,
	 mapping_context_type_id, mapping_context_id,
	 parent_session_id)
  select new_session_context.accessor_id,  nextval('veil2.session_id_seq'),
	 new_session_context.login_context_type_id,
	   new_session_context.login_context_id,
	 new_session_context.session_context_type_id,
	   new_session_context.session_context_id,
         case when sp.parameter_value = '1' then 1
         else coalesce(asp.superior_scope_type_id,
	               new_session_context.session_context_type_id) end,
           case when sp.parameter_value = '1' then 0
           else coalesce(asp.superior_scope_id,
	                 new_session_context.session_context_id) end,
	 new_session_context.parent_session_id
    from veil2.system_parameters sp
    left outer join veil2.all_superior_scopes asp
      on asp.scope_type_id = new_session_context.session_context_type_id
     and asp.scope_id = new_session_context.session_context_id
     and asp.superior_scope_type_id = sp.parameter_value::integer
     and asp.is_type_promotion
   where sp.parameter_name = 'mapping context target scope type'
  returning veil2_session_context.session_id,
            veil2_session_context.mapping_context_type_id,
            veil2_session_context.mapping_context_id;
$$
language sql security definer volatile;

Create a veil2_session_context record for the given parameters, returning session_id and mapping context.

C.4.6. have_accessor_context()

function veil2.have_accessor_context(
    _accessor_id integer,
    _context_type_id integer,
    _context_id integer)
  returns boolean as
$$
declare
  result boolean;
begin
    -- Whether the combination of accessor_id and context is valid.
    -- Generate session_supplemental if authentication method supports it.
  select exists (
    into result
    select null
      from veil2.accessor_contexts ac
     where ac.accessor_id = _accessor_id
       and ac.context_type_id = _context_type_id
       and ac.context_id = _context_id);
  
  return result;
end;
$$
language plpgsql security definer volatile;

Predicate to determine whether an accessor has the right to use the given session context.

C.4.7. create_accessor_session()

function veil2.create_accessor_session(
    accessor_id in integer,
    authent_type in text,
    login_context_type_id in integer,
    login_context_id in integer,
    session_context_type_id in integer,
    session_context_id in integer,
    session_id out integer,
    session_token out text,
    session_supplemental out text)
  returns record as
$$
declare
  _mapping_context_type_id integer;
  _mapping_context_id integer;
  supplemental_fn text;
begin
  execute veil2.reset_session();

  -- Regardless of validity of accessor_id, we create a
  -- veil2_session_context record.  This is to prevent fishing for
  -- valid accessor_ids.
  select *
    into session_id, _mapping_context_type_id,
	 _mapping_context_id
     from veil2.new_session_context(
  	     accessor_id,
	     login_context_type_id, login_context_id,
	     session_context_type_id, session_context_id) x;

  -- Figure out the session tokens.  This must succeed regardless of
  -- the validity of our parameters.
  select t.supplemental_fn
    into supplemental_fn
    from veil2.authentication_types t
   where shortname = authent_type;

  if supplemental_fn is not null then
    execute format('select * from %s(%s, %L)',
                   supplemental_fn, _accessor_id, session_token)
       into session_token, session_supplemental;
  else
    session_token := encode(digest(random()::text || now()::text, 'sha256'),
		            'base64');
  end if;

  if veil2.have_accessor_context(accessor_id, login_context_type_id,
     				 login_context_id)
  then
    insert
      into veil2.sessions
          (accessor_id, session_id,
	   login_context_type_id, login_context_id,
	   session_context_type_id, session_context_id,
	   mapping_context_type_id, mapping_context_id,
	   authent_type, has_authenticated,
	   session_supplemental, expires,
	   token)
    select create_accessor_session.accessor_id,
	     create_accessor_session.session_id, 
    	   create_accessor_session.login_context_type_id,
	     create_accessor_session.login_context_id,
	   create_accessor_session.session_context_type_id,
	     create_accessor_session.session_context_id,
    	   _mapping_context_type_id, _mapping_context_id,
	   authent_type, false,
	   session_supplemental, now() + sp.parameter_value::interval,
	   session_token
      from veil2.system_parameters sp
     where sp.parameter_name = 'shared session timeout';
  end if;
end;
$$
language plpgsql security definer volatile
set client_min_messages = 'error';

Create a new session based on an accessor_id rather than username. This is an internal function to veil2. It does the hard work for create_session().

C.4.8. create_session()

function veil2.create_session(
    username in text,
    authent_type in text,
    context_type_id in integer default 1,
    context_id in integer default 0,
    session_context_type_id in integer default null,
    session_context_id in integer default null,
    session_id out integer,
    session_token out text,
    session_supplemental out text)
  returns record as
$$
declare
  _accessor_id integer;
begin
  -- Generate session_id and session_token and establish whether
  -- username was valid.
  _accessor_id := veil2.get_accessor(username, context_type_id, context_id);

  select cas.session_id, cas.session_token,
         cas.session_supplemental
    into create_session.session_id, create_session.session_token,
         create_session.session_supplemental
    from veil2.create_accessor_session(
             _accessor_id, authent_type,
	     context_type_id, context_id,
	     coalesce(session_context_type_id, context_type_id),
	     coalesce(session_context_id, context_id)) cas;
end;
$$
language plpgsql security definer volatile
set client_min_messages = 'error';

Get session credentials for a new session.

Returns session_id, authent_token and session_supplemental.

session_id is used to uniquely identify this user's session. It will be needed for subsequent open_connection() calls.

session_token is randomly generated. Depending on the authentication method chosen, the client may need to use this when generating their authentication token for the subsequent open_connection() call. session_supplemental is an authentication method specific set of data. Depending upon the authentication method, the client may need to use this in generating subsequent authentication tokens,

If username is not valid the function will appear to work but subsequent attempts to open the session will fail and no privileges will be loaded. This makes it harder to fish for valid usernames.

The authent_type parameter identifies what type of authentication will be used, and therefore determines the authentication protocol. All authentication types will make use of a session_id and session_token, some may also require additional fields. These will be provided in session_supplemental. For example, if we were to define a Diffie-Hellman key exchange protocol, the session_supplemental field would provide modulus, base and public transport values.

C.4.9. check_nonce()

function veil2.check_nonce(
    nonce integer,
    nonces bitmap)
  returns boolean as
$$
  select case
         when nonces is null then true
         when (nonces ? nonce) then false
	 when nonce < bitmin(nonces) then false
	 when nonce > (bitmax(nonces) + 64) then false
	 else true end;
$$
language sql security definer stable;

Check that nonce has not already been used and is within the range of acceptable values, returning true if all is well.

C.4.10. update_nonces()

function veil2.update_nonces(
    nonce integer,
    nonces bitmap)
  returns bitmap as
$$
declare
  reslt bitmap;
  i integer;
  target_bitmin integer;
begin
  reslt := coalesce(nonces, bitmap()) + nonce;
  if (bitmax(reslt) - bitmin(reslt)) > 192 then
    -- If there are 3 64-bit groups in the bitmap, let's lose the
    -- lowest one.  We keep 2 groups active, allowing for some slop in
    -- the arrival of consecutive integers without allowing the bitmaps
    -- to become unreasonably large.  I don't see any attack vector
    -- here as it should be impossible to get past the checks in
    -- check_nonce() by attempting to re-use (in a replay attack) a
    -- nonce from a group that we have dropped.
    target_bitmin = (bitmin(reslt) + 64) & ~63;
    reslt := bitmap_setmin(reslt, target_bitmin);
  end if;
  return reslt;
end;
$$
language plpgsql security definer stable;

Add nonce to the list of used nonces, slimming the bitmap down when it gets too large.

C.4.11. filter_privs()

function veil2.filter_privs()
  returns void as
$$
begin
  -- We are going to update veil2_session_privileges to remove any
  -- roles and privileges that do not exist in
  -- veil2_ancestor_privileges.  This is part of the become user
  -- process, to ensure that become user cannot lead to privilege
  -- escalation.
  with updatable_privs as
    (
      select sp.scope_type_id, sp.scope_id
        from veil2_session_privileges sp
       where scope_type_id != 2
    ),
  superior_scopes as
    (
      -- For each scope, other than personal, in
      -- veil2_session_privileges, identify all scopes that could give
      -- us equivalent privileges.
      select up.scope_type_id, up.scope_id,
      	     asp.scope_type_id as test_scope_type_id,
	       asp.scope_id as test_scope_id
        from updatable_privs up
       inner join veil2.all_superior_scopes asp
          on asp.scope_type_id = up.scope_type_id
         and asp.scope_id = up.scope_id
       union all
      select scope_type_id, scope_id, scope_type_id, scope_id
        from updatable_privs
       union all
      select scope_type_id, scope_id, 1, 0  -- global scope is superior
        from updatable_privs
    ),
  effective_ancestor_privs as
    (
      -- The set of privileges our ancestor effectively has at each
      -- scope level.  These privileges may be from global or other
      -- superior scopes.
      select ss.scope_type_id, ss.scope_id,
      	     union_of(coalesce(vap.roles, bitmap())) as roles,
	       union_of(coalesce(vap.privs, bitmap())) as privs
        from superior_scopes ss
        left outer join veil2_ancestor_privileges vap
          on vap.scope_type_id = ss.test_scope_type_id
	 and vap.scope_id = ss.test_scope_id
       group by ss.scope_type_id, ss.scope_id
    ),
  final_privs as
    (
      -- What the become-user session's roles and privs should be
      -- after filtering any that the ancestor session did not
      -- effectively have.
      select sp.scope_type_id, sp.scope_id,
             sp.roles * eap.roles as roles,
             sp.privs * eap.privs as privs
        from veil2_session_privileges sp
       inner join effective_ancestor_privs eap
          on eap.scope_type_id = sp.scope_type_id
         and eap.scope_id = sp.scope_id
    )
  update veil2_session_privileges sp
     set roles = fp.roles,
         privs = fp.privs
    from final_privs fp
   where sp.scope_type_id = fp.scope_type_id
     and sp.scope_id = fp.scope_id;
end;
$$
language plpgsql security definer volatile;

Remove any privileges from veil2_session_privileges that would not be provided by veil2_ancestor_privileges. This is part of the become user functionality. We perform this filtering in order to ensure that a user cannot increase their privileges using become user.

C.4.12. load_ancestor_privs()

function veil2.load_ancestor_privs(parent_session_id integer)
  returns void as
$$
begin
  with recursive ancestors as
    (
      select *
        from veil2.sessions
       where session_id = load_ancestor_privs.parent_session_id
       union all
      select s.*
        from ancestors a
       inner join veil2.sessions s
          on s.session_id = a.parent_session_id
    ),
   base_accessor_privs as
    (
      select barp.accessor_id, barp.role_id,
      	     barp.assignment_context_type_id,
                 barp.assignment_context_id,
             barp.mapping_context_type_id, barp.mapping_context_id,
	     barp.roles, barp.privileges
        from ancestors a
       -- Making the following an inner join (rather than cross-join)
       -- even though the condition is redundant improves performance
       -- (Postgres 12.5).
       inner join veil2.base_accessor_roleprivs(
       	     	            a.accessor_id, a.session_context_type_id,
			    a.session_context_id, a.mapping_context_type_id,
			    a.mapping_context_id) barp
          on barp.accessor_id = a.accessor_id
       where assignment_context_type_id != 2 -- ignore personal context
    ),
  promoted_privs as
    (
      select bap.accessor_id, bap.role_id,
      	     bap.mapping_context_type_id, bap.mapping_context_id,
	     pp.scope_type_id, ss.superior_scope_id as scope_id,
  	     bap.privileges * pp.privilege_ids as privileges
        from base_accessor_privs bap
       inner join veil2.promotable_privileges pp
          on not is_empty(bap.privileges * pp.privilege_ids)
         and pp.scope_type_id != 1
       inner join veil2.all_superior_scopes ss
          on ss.scope_type_id = bap.assignment_context_type_id
         and ss.scope_id = bap.assignment_context_id
         and ss.superior_scope_type_id = pp.scope_type_id
	 and ss.is_type_promotion
    ),
  global_privs as
    (
      select bap.accessor_id, bap.role_id,
      	     bap.mapping_context_type_id, bap.mapping_context_id,
	     pp.scope_type_id, 0 as scope_id,
  	     bap.privileges * pp.privilege_ids as privileges
        from base_accessor_privs bap
       inner join veil2.promotable_privileges pp
          on not is_empty(bap.privileges * pp.privilege_ids)
         and pp.scope_type_id = 1
    ),  
  all_role_privs as
    (
      select accessor_id, 
      	     mapping_context_type_id, mapping_context_id,
  	     assignment_context_type_id as scope_type_id,
             assignment_context_id as scope_id,
             roles + role_id as roles,  privileges
        from base_accessor_privs
       union all
      select accessor_id, 
             mapping_context_type_id, mapping_context_id,
  	     scope_type_id, scope_id,
             bitmap() as roles, privileges
        from promoted_privs
       union all
      select accessor_id, 
             mapping_context_type_id, mapping_context_id,
  	     scope_type_id, scope_id,
             bitmap() as roles, privileges
        from global_privs
    ),
  grouped_role_privs as
    (
      select scope_type_id, scope_id,
             union_of(roles) as roles, union_of(privileges) as privs
        from all_role_privs
       group by scope_type_id, scope_id
    )
  insert
    into veil2_ancestor_privileges
        (scope_type_id, scope_id, roles, privs)
  select scope_type_id, scope_id, roles, privs
    from grouped_role_privs;
end;
$$
language plpgsql security definer volatile;

Load the privileges for all ancestor sessions into veil2_ancestor_privileges. We will use this to ensure that a become_user() session does not gain privileges that the parent session did not have (ie we aim to stop it from being used as a mechanism for privilege escalation).

C.4.13. filter_session_privs()

function veil2.filter_session_privs(parent_session_id integer)
  returns void as
$$
begin
  perform veil2.load_ancestor_privs(parent_session_id);
  perform veil2.filter_privs();
end;
$$
language plpgsql security definer volatile;

Remove from veil2_session_privileges any roles and privileges not held by the ancestor session(s).

C.4.14. save_privs_as_orig()

C.4.15. all_accessor_roles()

function veil2.all_accessor_roles(
   accessor_id in out integer,
   session_context_type_id in integer,
   session_context_id in integer,
   role_id out integer,
   context_type_id out integer,
   context_id out integer)
  returns setof record as
$$
  select -- All roles without filtering if our session context is global
         -- context.
         aar.accessor_id, aar.role_id,
	 aar.context_type_id, aar.context_id
    from veil2.all_accessor_roles_plus aar
   where aar.accessor_id = all_accessor_roles.accessor_id
     and all_accessor_roles.session_context_type_id = 1
   union all
  select -- Globally assigned roles, if our session context is non-global
         -- context.
         aar.accessor_id, aar.role_id,
	 aar.context_type_id, aar.context_id
    from veil2.all_accessor_roles_plus aar
   inner join veil2.session_assignment_contexts sac
      on -- Matching login context and assignment context
         -- Also session_context and assignment context
         aar.context_type_id = 1
      or (    sac.context_type_id = aar.context_type_id
          and sac.context_id = aar.context_id
 	  and aar.context_type_id != 1)
   where aar.accessor_id = all_accessor_roles.accessor_id
     and all_accessor_roles.session_context_type_id != 1
   union all
  select accessor_id, 2, 2, accessor_id;
$$
language sql security definer stable;

Return all roles for the given accessor in the given session context.

C.4.16. base_accessor_roleprivs()

function veil2.base_accessor_roleprivs(
    accessor_id in out integer,
    session_context_type_id in integer,
    session_context_id in integer,
    mapping_context_type_id in out integer,
    mapping_context_id in out integer,
    assignment_context_type_id out integer,
    assignment_context_id out integer,
    role_id out integer,
    roles out bitmap,
    privileges out bitmap)
  returns setof record as
$$
  select aar.accessor_id, arp.mapping_context_type_id,
         arp.mapping_context_id,  aar.context_type_id,
         aar.context_id, aar.role_id,
	 coalesce(arp.roles, bitmap(aar.role_id)),
	 coalesce(arp.privileges, bitmap())
    from veil2.all_accessor_roles(
                        base_accessor_roleprivs.accessor_id,
			base_accessor_roleprivs.session_context_type_id,
			base_accessor_roleprivs.session_context_id) aar
    left outer join veil2.all_role_privileges arp
      on arp.role_id = aar.role_id
     and (   (    arp.mapping_context_type_id =
     	              base_accessor_roleprivs.mapping_context_type_id
              and arp.mapping_context_id =
	      	      base_accessor_roleprivs.mapping_context_id)
          or (    arp.mapping_context_type_id = 1
              and arp.mapping_context_id = 0)
          or (    arp.mapping_context_type_id is null
              and arp.mapping_context_id is null));
$$
language sql security definer stable;

Give the set of base (ignoring privilege promotion) roles and privileges that apply to a given accessor in given mapping and session contexts.

C.4.17. session_context()

function veil2.session_context(
    accessor_id out integer,
    session_id out integer,
    login_context_type_id out integer,
    login_context_id out integer,
    session_context_type_id out integer,
    session_context_id out integer,
    mapping_context_type_id out integer,
    mapping_context_id out integer
    )
  returns record as
$$
begin
  select sc.accessor_id, sc.session_id,
         sc.login_context_type_id, sc.login_context_id,
         sc.session_context_type_id, sc.session_context_id,
         sc.mapping_context_type_id, sc.mapping_context_id
    into session_context.accessor_id, session_context.session_id,
         session_context.login_context_type_id,
	   session_context.login_context_id,
         session_context.session_context_type_id,
	   session_context.session_context_id,
         session_context.mapping_context_type_id,
	   session_context.mapping_context_id
    from veil2_session_context sc;
exception
  when sqlstate '42P01' then
    -- Temp table does not exist
    return;
  when others then
    raise;
end;
$$
language plpgsql security definer volatile;

Column accessor_id: The id of the accessor whose session this is.

Column accessor_id: The id of the accessor whose access rights (mostly) are being used by this session. If this is not the same as the accessor_id, then the session_user has assumed the access rights of this accessor using the become_user() function.

Column login_context_type_id: This is the context_type_id for the context within which our accessor has authenticated. This will have been the context_type_id provided to the create_session() or hello() function that began this session.

Column login_context_id: This is the context_id for the context within which our accessor has authenticated. This will have been the context_id provided to the create_session() or hello() function that began this session.

Column session_context_type_id: This is the context_type_id to be used for limiting our session's assigned roles and from which is determined our mapping_context_type_id. Ordinarily, this will be the same as our login_context_type_id, but if create_session() has been provided with session_context parameters, this will be different. Note that for an accessor to create such a session they must have connect privilege in both their login context and their requested session context.

Safe function to return the context of the current session. If no session exists, returns nulls. We use a function in this context because we cannot create a view on the veil2_session_context table as it is a temporary table and does not always exist.

C.4.18. session_privileges()

function veil2.session_privileges(
    scope_type_id out integer,
    scope_id out integer,
    roles out integer[],
    privs out integer[]
    )
  returns setof record as
$$
begin
  for session_privileges.scope_type_id,
      session_privileges.scope_id,
      session_privileges.roles,
      session_privileges.privs
  in select sp.scope_type_id, sp.scope_id,
     	    to_array(sp.roles), to_array(sp.privs)
       from veil2_session_privileges sp
  loop
    return next;
  end loop;
exception
  when sqlstate '42P01' then
    return;
  when others then
    raise;
end;
$$
language plpgsql security definer volatile;

Safe function to return a user-readable version of the privileges for the current session. If no session exists, returns nulls. We use a function in this context because we cannot create a view on the veil2_session_privileges table as it is a temporary table and does not always exist.

C.4.19. load_session_privs()

function veil2.load_session_privs()
  returns boolean as
$$
begin
  insert
    into veil2_session_privileges
        (scope_type_id, scope_id,
  	 roles, privs)
  select scope_type_id, scope_id,
         roles, privileges
    from veil2.session_privileges_v;

  if found then
    perform veil2.save_session_privs();
    return true;
  else
    return false;
  end if;
end;
$$
language plpgsql security definer volatile;

Load the temporary table veil2_session_privileges for session_id, with the privileges for the current session. The temporary table is queried by security functions in order to determine what access rights the connected user has.

C.4.20. save_session_privs()

function veil2.save_session_privs()
  returns void as
$$
delete
  from veil2.accessor_privileges_cache
 where (accessor_id, login_context_type_id,
        login_context_id) = (
    select accessor_id, login_context_type_id,
        login_context_id
      from veil2_session_context);
insert
  into veil2.accessor_privileges_cache
      (accessor_id, login_context_type_id,
       login_context_id, session_context_type_id,
       session_context_id, mapping_context_type_id,
       mapping_context_id, scope_type_id,
       scope_id, roles,
       privs)
select sc.accessor_id, sc.login_context_type_id,
       sc.login_context_id, sc.session_context_type_id,
       sc.session_context_id, sc.mapping_context_type_id,
       sc.mapping_context_id, sp.scope_type_id,
       sp.scope_id, sp.roles,
       sp.privs
  from veil2_session_context sc
 cross join veil2_session_privileges sp;
$$
language 'sql' security definer volatile;

Save the current contents of the veil2_session_privileges temporary table into veil2.session_privileges after ensuring that there is no existing data present for the session. This saves our session_privileges data for future use in the session.

C.4.21. load_cached_privs()

function veil2.load_cached_privs()
  returns boolean as
$$
begin
  insert
    into veil2_session_privileges
        (scope_type_id,	 scope_id,
	 roles, privs)
  select apc.scope_type_id, apc.scope_id,
  	 apc.roles, apc.privs
    from veil2_session_context sc
   inner join veil2.accessor_privileges_cache apc
      on apc.accessor_id = sc.accessor_id
     and apc.login_context_type_id = sc.login_context_type_id
     and apc.login_context_id = sc.login_context_id
     and apc.session_context_type_id = sc.session_context_type_id
     and apc.session_context_id = sc.session_context_id
     and apc.mapping_context_type_id = sc.mapping_context_type_id
     and apc.mapping_context_id = sc.mapping_context_id;
  return found;
end;
$$
language 'plpgsql' security definer volatile;

Reload cached session privileges for the session's accessor into our current session.

C.4.22. update_session()

function veil2.update_session(
    _session_id integer,
    _nonces bitmap,
    _authenticated boolean)
  returns void as
$$
  with recursive sessions as
    (
      select parent_session_id
        from upd_cur_session
       union all
      select s2.parent_session_id
        from sessions s1
       inner join veil2.sessions s2
          on s1.parent_session_id is not null
	 and s2.session_id = s1.parent_session_id
	 and s2.parent_session_id is not null
    ),
  timeout as
    (
      select parameter_value::interval as increment
        from veil2.system_parameters
       where parameter_name = 'shared session timeout'
    ),
  upd_cur_session as
    (
      update veil2.sessions s
	 set expires =
	     case when _authenticated
	       then now() + t.increment
	       else s.expires
	       end,
	     nonces = coalesce(_nonces, nonces),
	     has_authenticated = has_authenticated or _authenticated
        from timeout t
       where s.session_id = _session_id
      returning parent_session_id
    )
  update veil2.sessions s
     set expires =
	   case when _authenticated
	     then now() + t.increment
	     else s.expires
	     end
   from timeout t
  where s.session_id in (select parent_session_id from sessions);
$$
language sql security definer volatile;

Update the veil2.sessions records associated with the current session to show a new expiry date, record new nonces, and ongoing session validity. This updates the identified sessions record with nonces, validity and expiry, and also updates any ancestor sessions with expiry. Although this might be more clearly expressed in plpgsql, being able to do it in pure sql is cool and has the potential to be faster as it requires only a single statement.

C.4.23. load_connection_privs()

function veil2.load_connection_privs(
    parent_session_id integer)
  returns boolean as
$$
begin
  if not veil2.load_cached_privs() then
    if not veil2.load_session_privs() then
      return false;
    end if;
  end if;

  if parent_session_id is not null then
    perform veil2.filter_session_privs(parent_session_id);
  end if;
  return true;
end;
$$
language plpgsql security definer volatile;

Load veil_session_privileges temp table for a session, filtering the privileges if neccesary for a become_user() session.

C.4.24. reload_session_context()

function veil2.reload_session_context(_session_id integer)
  returns integer as
$$
    insert
      into veil2_session_context
          (accessor_id, session_id,
           login_context_type_id, login_context_id,
           session_context_type_id, session_context_id,
  	   mapping_context_type_id, mapping_context_id,
	   parent_session_id)
    select s.accessor_id, s.session_id,
           s.login_context_type_id, s.login_context_id,
           s.session_context_type_id, s.session_context_id,
           s.mapping_context_type_id, s.mapping_context_id,
	   s.parent_session_id
      from veil2.sessions s
     where s.session_id = _session_id
    returning parent_session_id;
$$
language sql security definer volatile;

(Re)load veil2_session_context for a given session_id.

C.4.25. reload_connection_privs()

function veil2.reload_connection_privs()
  returns boolean as
$$
declare
  _parent_session_id integer;
begin
  perform veil2.reset_session_privs();
  select parent_session_id
    into _parent_session_id
    from veil2_session_context;
  return veil2.load_connection_privs(_parent_session_id);
end;
$$
language plpgsql security definer volatile;

Reload a session's privileges. This would be done in the event that cached privileges need to be reloaded.

C.4.26. check_continuation()

function veil2.check_continuation(
    nonce integer,
    session_token text,
    authent_token text)
  returns boolean as
$$
select encode(digest(session_token || to_hex(nonce), 'sha1'),
              'base64') = authent_token;
$$
language sql security definer stable;

Checks whether the combination of nonce, session_token and authent_token is valid. This is used to continue sessions that have already been authenticated. It ensures that new tokens are used on each call, and that the caller has access to the session_token returned from the original (subsequently authenticated) create session() call.

C.4.27. open_connection()

function veil2.open_connection(
    session_id in integer,
    nonce in integer,
    authent_token in text,
    success out boolean,
    errmsg out text)
  returns record as
$$
declare
  _accessor_id integer;
  _nonces bitmap;
  nonce_ok boolean;
  _has_authenticated boolean;
  _session_token text;
  _context_type_id integer;
  authent_type text;
  expired boolean;
  parent_session_id integer;
begin
  success := false;
  select s.accessor_id, s.expires < now(),
         veil2.check_nonce(nonce, s.nonces), s.nonces,
	 s.authent_type,
	 ac.context_type_id, s.has_authenticated,
	 s.token
    into _accessor_id, expired,
         nonce_ok, _nonces,
	 authent_type, _context_type_id, 
	 _has_authenticated, _session_token
    from veil2.sessions s
    left outer join veil2.accessor_contexts ac
      on ac.accessor_id = s.accessor_id
     and ac.context_type_id = s.login_context_type_id
     and ac.context_id = s.login_context_id
   where s.session_id = open_connection.session_id;

  if not found then
    raise warning 'SECURITY: Connection attempt with no session: %',
        session_id;
    errmsg := 'AUTHFAIL';
  elsif _context_type_id is null then
    raise warning 'SECURITY: Connection attempt for invalid context';
    errmsg := 'AUTHFAIL';
  elsif expired then
    errmsg := 'EXPIRED';
  elsif not nonce_ok then
    -- Since this could be the result of an attempt to replay a past
    -- authentication token, we log this failure
    raise warning 'SECURITY: Nonce failure.  Nonce %, Nonces %',
                   nonce, to_array(_nonces);
    errmsg := 'NONCEFAIL';
  else
    success := true;
      
    if _has_authenticated then
      -- The session has already been opened.  From here on we 
      -- use different authentication tokens for each open_connection()
      -- call in order to avoid replay attacks.
      if not veil2.check_continuation(nonce, _session_token,
	       			      authent_token) then
        raise warning 'SECURITY: incorrect continuation token for %, %',
                     _accessor_id, session_id;
        errmsg := 'AUTHFAIL';
	success := false;
      end if;
    else 
      if not veil2.authenticate(_accessor_id, authent_type,
				authent_token) then
        raise warning 'SECURITY: incorrect % authentication token for %, %',
                      authent_type, _accessor_id, session_id;
        errmsg := 'AUTHFAIL';
	success := false;
      end if;
    end if;
  end if;
  
  if success then
    perform veil2.reset_session();
    -- Reload session context
    parent_session_id := veil2.reload_session_context(session_id);

    if not veil2.load_connection_privs(parent_session_id) then
      raise warning 'SECURITY: Accessor % has no connect privilege.',
                    _accessor_id;
      errmsg := 'AUTHFAIL';
      success := false;
    end if;
  end if;

  if not success then
    perform veil2.reset_session();
  end if;
  
  -- Regardless of the success of the preceding checks we record the
  -- use of the latest nonce.  If all validations succeeded, we
  -- extend the expiry time of the session.
  perform veil2.update_session(session_id,
			       veil2.update_nonces(nonce, _nonces),
			       success);
end;
$$
language plpgsql security definer volatile
set client_min_messages = 'error';

Attempt to open or re-open a session. This is used to authenticate or re-authenticate a connection, and until this is done a session cannot be used.

Failures may be for several reasons with errmsg as described below:

- non-existence of session [errmsg: 'AUTHFAIL'];

- expiry of session (while session record still exists - has not been cleaned away) [errmsg: 'EXPIRED'];

- incorrect credentials being used [errmsg: 'AUTHFAIL'];

- invalid nonce being provided [errmsg: 'NONCEFAIL'];

- the user has no connect privilege [errmsg: 'AUTHFAIL'].

The _nonce is a number that may only be used once per session, and is used to prevent replay attacks. Each open_connection() call should provide a new nonce ascending in value from the last. As connections may be asynchronous, we do not require a strictly ascending order but nonces may not be out of sequence by a value of more than 64. This allows us to keep track of used nonces without excess overhead while still allowing an application to have multiple database connections per user session.

The value of _authent_token depends upon the authentication method chosen. See the authentication function for your session's authentication method (identified in table veil2.authentication_types) for details.

Note that warning messages will be sent to the log but not to the client, even if client_min_messages is modified for the session. This is deliberate, for security reasons.

C.4.28. close_connection()

function veil2.close_connection() returns boolean as
$$
begin
  perform veil2.reset_session();
  delete from veil2_session_privileges;
  return true;
end;
$$
language 'plpgsql' security definer volatile;

Close the current session. We use this to ensure that a shared database connection cannot be used with our privileges once we have finished with it. There is no authentication or verification done to ensure that the session owner is the one doing this, because there is no perceived need. If this is a problem then, given that you can achieve the same thing by deliberately failing a veil2.open() call, there are other, more complex areas of the session management protocol that will need to be reconsidered.

C.4.29. hello()

function veil2.hello(
    context_type_id in integer default 1,
    context_id in integer default 0)
  returns boolean as
$$
declare
  _accessor_id integer;
  _session_id integer;
  success boolean;
begin
  success := false;
  execute veil2.reset_session();
  
  select accessor_id
    into _accessor_id
    from veil2.accessors
   where username = session_user;
  if found then

    select cas.session_id
      into _session_id
    from veil2.create_accessor_session(
             _accessor_id, 'dedicated',
	     context_type_id, context_id,
	     context_type_id, context_id) cas;

    -- TODO: CHECK IF REFACTORING IS NEEDED HERE - MAYBE UNUSED VARS?
    success := veil2.load_session_privs();

    if not success then
      raise exception 'SECURITY: user % has no connect privilege.',
      	    	      session_user;
    else
      -- Update the permanent session record to show that we have
      -- authenticated and give a reasonable expiry time.
      update veil2.sessions
         set expires = now() + '1 day'::interval,
 	    has_authenticated = true
       where session_id = _session_id;
    end if;
  end if;
  return success;
end;
$$
language plpgsql security definer volatile;

This is used to begin a veil2 session for a database user, ie someone who can directly access the database.

C.4.30. check_become_user_priv()

function veil2.check_become_user_priv(
    label text,
    accessor_id integer,
    context_type_id integer,
    context_id integer)
  returns text as
$$
begin
  if veil2.i_have_priv_in_scope_or_superior_or_global(
  	       1, context_type_id, context_id)
  then
    return null;
  else
    raise warning 'SECURITY: become_user() (%): no privilege for % in '
    	          'context %,%', label, accessor_id,
		  context_type_id, context_id;
    return 'NOPRIV';
  end if;
end;
$$
language plpgsql security definer volatile;

Determine whether the accessor has become_user privilege for the supplied context (including superior and global). Return 'NOPRIV' to the caller if not.

C.4.31. check_accessor_context()

function veil2.check_accessor_context(
    label text,
    accessor_id integer,
    context_type_id integer,
    context_id integer)
  returns text as
$$
begin
  if veil2.have_accessor_context(accessor_id, context_type_id,
		         	 context_id) 
  then
    return null;
  else
    raise warning 'SECURITY: become_user() (%): invalid context for %'
    	          ' - %,%', label, accessor_id, context_type_id, context_id;
    return 'AUTHFAIL';
  end if;
end;
$$
language plpgsql security definer volatile;

Determine whether the geven accessor has rights to the given session context. Return 'AUTHFAIL' if not.

C.4.32. become_accessor()

function veil2.become_accessor(
    accessor_id in integer,
    login_context_type_id in integer,
    login_context_id in integer,
    session_context_type_id in integer,
    session_context_id in integer,
    session_id out integer,
    session_token out text,
    success out boolean,
    errmsg out text)
  returns record as
$$
declare
  orig_session_id integer;
  orig_accessor_id integer;
  _mapping_context_type_id integer;
  _mapping_context_id integer;
begin
  select sc.session_id, sc.accessor_id
    into orig_session_id, orig_accessor_id
    from veil2_session_context sc;

  -- We must check that login context is valid for both the current
  -- and target accessors.
  errmsg := veil2.check_become_user_priv('AAA', orig_accessor_id, 
  	        login_context_type_id, login_context_id);

  if errmsg is null then
    errmsg := veil2.check_accessor_context('BBB', orig_accessor_id, 
  	          login_context_type_id, login_context_id);
  end if;

  if errmsg is null then
    errmsg := veil2.check_accessor_context('CCC', accessor_id, 
  	          login_context_type_id, login_context_id);
  end if;
  
  if errmsg is null then
    -- Create new session_context record
    perform veil2.reset_session();

    select *
      into session_id, _mapping_context_type_id,
	   _mapping_context_id
      from veil2.new_session_context(accessor_id,
		     login_context_type_id, login_context_id,
	             session_context_type_id, session_context_id,
		     orig_session_id) x;

    -- Create new session record.
    insert
      into veil2.sessions
          (accessor_id, session_id,
	   login_context_type_id, login_context_id,
	   session_context_type_id, session_context_id,
	   mapping_context_type_id, mapping_context_id,
	   authent_type, has_authenticated,
	   session_supplemental, expires,
	   token, parent_session_id)
    select become_accessor.accessor_id, become_accessor.session_id,
	   become_accessor.login_context_type_id,
	     become_accessor.login_context_id,
	   become_accessor.session_context_type_id,
	     become_accessor.session_context_id,
    	   _mapping_context_type_id,
	     _mapping_context_id,
	   'become', true,
	   null, now() + sp.parameter_value::interval,
	   encode(digest(random()::text || now()::text, 'sha256'),
		  'base64'),
	     orig_session_id
      from veil2.system_parameters sp
     where sp.parameter_name = 'shared session timeout'
    returning token into session_token;

    -- Update expiry of parent session.
    perform veil2.update_session(orig_session_id, null::bitmap, true);

    if not veil2.load_session_privs() then
      raise warning 'SECURITY: Accessor % has no connect privilege.',
                     _accessor_id;
      errmsg := 'AUTHFAIL';
    else
      raise warning 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
      perform veil2.filter_session_privs(orig_session_id);
    end if;
  end if;

  if errmsg is null then
     success := true;
  else
     success := false;
  end if;
end;
$$
language plpgsql security definer volatile
set client_min_messages = 'error';

Create a new opened session for the given accessor_id and context. This allows a suitably privileged accessor to emulate another user. The intended use-case for this is in testing and debugging access rights. Note that the new session will not give the connected user more privileges than they already have, so the usage of this should probably be confined to superusers. Any other user is likely to get a set of privileges that may be less than the user they have become would normally get.

C.4.33. become_user()

function veil2.become_user(
    username in text,
    login_context_type_id in integer,
    login_context_id in integer,
    session_context_type_id in integer default null,
    session_context_id in integer default null,
    session_id out integer,
    session_token out text,
    success out boolean,
    errmsg out text)
  returns record as
$$
declare
  _accessor_id integer;
begin
  _accessor_id := veil2.get_accessor(username, login_context_type_id,
				     login_context_id);

  select ba.session_id, ba.session_token,
  	 ba.success, ba.errmsg
    into become_user.session_id, become_user.session_token,
         become_user.success, become_user.errmsg
    from veil2.become_accessor(
             _accessor_id,
	     login_context_type_id, login_context_id,
	     coalesce(session_context_type_id, login_context_type_id), 
	     coalesce(session_context_id, login_context_id)) ba;
end;
$$
language plpgsql security definer volatile;

See comments for become_accessor(). This is the same but takes a username rather than accessor_id.

C.5. Privilege Testing Functions

C.5.1. always_true()

function veil2.always_true(integer) returns boolean
     as '$libdir/veil2', 'veil2_true'
     language C security definer stable leakproof;

Performance testing function - always returns true. Used to establish the minimum overhead of security policies for tables.

The Doxygen documentation for this can be found here.

C.5.2. i_have_global_priv()

function veil2.i_have_global_priv(integer) returns boolean
     as '$libdir/veil2', 'veil2_i_have_global_priv'
     language C security definer stable leakproof;

Predicate to determine whether the connected user has the given privilege in the global scope.

The Doxygen documentation for this can be found here.

C.5.3. i_have_personal_priv()

function veil2.i_have_personal_priv(integer, integer) returns boolean
     as '$libdir/veil2', 'veil2_i_have_personal_priv'
     language C security definer stable leakproof;

Predicate to determine whether the connected user has the given privilege in the personal scope.

The Doxygen documentation for this can be found here.

C.5.4. i_have_priv_in_scope()

function veil2.i_have_priv_in_scope(integer, integer, integer) returns boolean
     as '$libdir/veil2', 'veil2_i_have_priv_in_scope'
     language C security definer stable leakproof;

Predicate to determine whether the connected user has the given privilege in the given scope.

Predicate to determine whether the connected user has the given privilege in the given scope, or in global scope.

The Doxygen documentation for this can be found here.

C.5.5. i_have_priv_in_scope_or_global()

function veil2.i_have_priv_in_scope_or_global(
	     integer, integer, integer) returns boolean
     as '$libdir/veil2', 'veil2_i_have_priv_in_scope_or_global'
     language C security definer stable leakproof;

The Doxygen documentation for this can be found here.

C.5.6. i_have_priv_in_superior_scope()

function veil2.i_have_priv_in_superior_scope(integer, integer, integer) 
     returns boolean
     as '$libdir/veil2', 'veil2_i_have_priv_in_superior_scope'
     language C security definer stable leakproof;

Predicate to determine whether the connected user has the given privilege in a scope that is superior to the given scope. This does not check for the privilege in a global scope as it is assumed that such a test will have already been performed. Note that due to the join on all_superior_scopes this function may incur some small measurable overhead.

The Doxygen documentation for this can be found here.

C.5.7. i_have_priv_in_scope_or_superior()

function veil2.i_have_priv_in_scope_or_superior(integer, integer, integer) 
     returns boolean
     as '$libdir/veil2', 'veil2_i_have_priv_in_scope_or_superior'
     language C security definer stable leakproof;

Predicate to determine whether the connected user has the given privilege in a scope that is, or is superior to the given scope. This does not check for the privilege in a global scope as it is assumed that such a test will have already been performed. Note that due to the join on all_superior_scopes this function may incur some small measurable overhead.

The Doxygen documentation for this can be found here.

C.5.8. i_have_priv_in_scope_or_superior_or_global()

function veil2.i_have_priv_in_scope_or_superior_or_global(
	     integer, integer, integer) 
     returns boolean
     as '$libdir/veil2', 'veil2_i_have_priv_in_scope_or_superior_or_global'
     language C security definer stable leakproof;

Predicate to determine whether the connected user has the given privilege in a scope that is, or is superior to the given scope, or in the global scope. This does not check for the privilege in a global scope as it is assumed that such a test will have already been performed. Note that due to the join on all_superior_scopes this function may incur some small measurable overhead.

The Doxygen documentation for this can be found here.

C.6. Utility and Administration Functions

C.6.1. result_counts()

function veil2.result_counts(false_count out integer, true_count out integer)
     returns record
     as '$libdir/veil2', 'veil2_result_counts'
     language C security definer stable leakproof;

Return record of how many false and how many true results have been returned by the i_have_privi_xxx() functions in this session

The Doxygen documentation for this can be found here.

C.6.2. delete_expired_sessions()

function veil2.delete_expired_sessions() returns void as
$$
delete
  from veil2.sessions s
     where expires <= now();
$$
language 'sql' security definer volatile;

Utility function to clean-up session data. This should be run periodically from a batch job.

C.6.3. func_docpath()

function veil2.docpath() returns text
     as '$libdir/veil2', 'veil2_docpath'
     language C stable strict;

Return the path to the directory under which Veil2 documents will be installed.

C.6.4. func_datapath()

function veil2.datapath() returns text
     as '$libdir/veil2', 'veil2_datapath'
     language C stable strict;

Return the path to the directory under which Veil2 scripts will be installed.

C.6.5. func_bcrypt()

function veil2.bcrypt(passwd text) returns text as
$$
select crypt(passwd, gen_salt('bf'));
$$
language 'sql' security definer volatile;

Create a bcrypted password from plaintext. It creates a value that can be stored in veil2.authentication_details for use by the authenticate_bcrypt() function.

C.7. Materialized View Functions

C.7.1. Refresh All Matviews Function

function veil2.refresh_all_matviews()
    returns void as
$$
  refresh materialized view veil2.all_superior_scopes;
  refresh materialized view veil2.all_role_privileges;
  truncate table veil2.accessor_privileges_cache;
$$
language sql security definer volatile;

Clear all matviews and caches unconditionally.

C.7.2. Refresh Scopes Matviews Function

function veil2.refresh_scopes_matviews()
  returns trigger
as
$$
begin
  perform veil2.refresh_all_matviews();
  return new;
end;
$$
language plpgsql security definer;

Trigger function to refresh all materialized views and caches that depend on the scopes hierarchy.

C.7.3. Refresh Roles Matviews Function

function veil2.refresh_roles_matviews()
  returns trigger
as
$$
begin
  refresh materialized view veil2.all_role_privileges;
  truncate table veil2.accessor_privileges_cache;
  return new;
end;
$$
language plpgsql security definer volatile;

Trigger function to refresh all materialized views and caches that depend on roles.

C.7.4. Refresh Privs Matviews Function

function veil2.refresh_privs_matviews()
  returns trigger
as
$$
begin
  refresh materialized view veil2.all_role_privileges;
  truncate table veil2.accessor_privileges_cache;
  return new;
end;
$$
language plpgsql security definer volatile;

Trigger function to refresh all materialized views and caches that depend on privileges.

C.7.5. Clear Accessor Privs Cache Function

function veil2.clear_accessor_privs_cache()
  returns trigger as
$$
begin
  truncate table veil2.accessor_privileges_cache;
  return new;
end;
$$
language plpgsql security definer volatile;

Clear cached role and privileges information for all accessors.

C.7.6. Clear Accessor Privs Cache Entry Function

function veil2.clear_accessor_privs_cache_entry()
  returns trigger as
$$
begin
  if tg_op = 'INSERT' or tg_op = 'UPDATE' then
    delete
      from veil2.accessor_privileges_cache
     where accessor_id = new.accessor_id;
    if (tg_op = 'UPDATE') and
       (old.accessor_id != new.accessor_id) then
      delete
        from veil2.accessor_privileges_cache
       where accessor_id = old.accessor_id;
    end if;
    return new;
  elsif tg_op = 'DELETE' then
    delete
      from veil2.accessor_privileges_cache
     where accessor_id = old.accessor_id;
    return old;
  end if;
end;
$$
language plpgsql security definer volatile;

Clear the cached role and privileges information for the affected accessor.

C.8. Functions For Checking Your Implementation

C.8.1. func_have_user_scope_types()

function veil2.have_user_scope_types()
  returns boolean as
$$
-- Have we defined new scope_types:
select exists (
  select null
    from veil2.scope_types
   where scope_type_id not in (1, 2));
$$
language sql security definer stable;

Predicate used to determine whether user-defined scope_types have been added to the implementation.

C.8.2. have_user_privileges()

function veil2.have_user_privileges()
  returns boolean as
$$
select exists (
  select null
    from veil2.privileges
   where privilege_id > 15
      or privilege_id < 0);
$$
language sql security definer volatile;

Predicate used to determine whether any user-defined privileges have been created.

C.8.3. have_user_roles()

function veil2.have_user_roles()
  returns boolean as
$$
select exists (
  select null
    from veil2.roles
   where role_id > 4
      or role_id < 0);
$$
language sql security definer volatile;

Predicate used to determine whether any user-defined roles have been created.

C.8.4. have_role_privileges()

function veil2.have_role_privileges()
  returns boolean as
$$
select exists (
  select null
    from veil2.role_privileges
   where role_id < 0
      or role_id > 4);
$$
language sql security definer volatile;

Predicate used to determine whether any user-defined role_privileges have been created.

C.8.5. have_role_roles()

function veil2.have_role_roles()
  returns boolean as
$$
select exists (
  select null
    from veil2.role_roles);
$$
language sql security definer volatile;

Predicate used to determine whether any user-defined role_roles (role to role mappings) have been created.

C.8.6. have_accessors()

function veil2.have_accessors()
  returns boolean as
$$
select exists (
  select null
    from veil2.accessors);
$$
language sql security definer volatile;

Predicate used to determine whether any accessors have been defined.

C.8.7. have_user_scopes()

function veil2.have_user_scopes ()
  returns boolean as
$$
select exists (
  select null
    from veil2.scopes
   where scope_type_id != 1
     and scope_id != 0);
$$
language sql security definer volatile;

Predicate used to determine whether any user-defined scopes have been created.

C.8.8. check_table_security()

function veil2.check_table_security()
  returns setof text as
$$
declare
  tbl text;
  header_returned boolean := false;
begin
  for tbl in
    select n.nspname || '.' || c.relname
      from pg_catalog.pg_class c
     inner join pg_catalog.pg_namespace n
        on n.oid = c.relnamespace
     where c.relkind = 'r'
       and n.nspname not in ('pg_catalog', 'information_schema')
       and c.relpersistence = 'p'
       and not relrowsecurity
  loop
    if not header_returned then
      header_returned := true;
      return next 'The following tables have no security policies:';
    end if;
    return next '    - ' || tbl;
  end loop;
  if not header_returned then
    return next 'All tables appear to have security policies.';
  end if;  
end;
$$
language plpgsql security definer stable;

Predicate used to determine whether all user-defined tables have security policies in place.

C.8.9. implementation_status()

function veil2.implementation_status()
  returns setof text as
$$
declare
  ok boolean := true;
  line text;
begin
  perform veil2.init();
  if not veil2.have_user_scope_types() then
    ok := false;
    return next 'You need to define some scope types (step 2)';
  end if;
  if not veil2.view_exists('my_accessor_contexts') then
    ok := false;
    return next 'You need to redefine the accessor_contexts view (step 3)';
  end if;
  if not veil2.function_exists('my_get_accessor') then
    ok := false;
    return next 'You need to define a get_accessor() function (step 3)';
  end if;
  if not veil2.have_accessors() then
    ok := false;
    return next 'You need to create accessors (and maybe FK links) (step 4)';
  end if;
  if not veil2.have_user_scopes() then
    ok := false;
    return next 'You need to create user scopes (step 5)';
  end if;
  if not veil2.view_exists('my_superior_scopes') then
    ok := false;
    return next 'You need to redefine the superior_scopes view (step 6)';
  else
    execute('refresh materialized view veil2.all_superior_scopes');
  end if;
  if not veil2.have_user_privileges() then
    ok := false;
    return next 'You need to define some privileges (step 7)';
  end if;
  if not veil2.have_user_roles() then
    ok := false;
    return next 'You need to define some roles (step 8)';
  end if;
  if not veil2.have_role_privileges() then
    ok := false;
    return next 'You need to create entries in role_privileges (step 8)';
  end if;
  if not veil2.have_role_roles() then
    ok := false;
    return next 'You need to create entries in role_roles (step 8)';
  end if;
  if ok then
    return next 'Your Veil2 basic implemementation seems to be complete.';
  end if;
  for line in select * from veil2.check_table_security()
  loop
    return next line;
  end loop;
  if ok then
    return next 'Have you secured your views (I have no way of knowing)?';
  end if;
end;
$$
language plpgsql security definer volatile;

Set returning function that identifies incomplete user-implementations.

Call this using select * from veil2.implementation_status();

and it will return a list of things to implement or consider implementing.

C.9. Other Trigger Functions

C.9.1. system_parameters_check()

function veil2.system_parameters_check()
  returns trigger
as
$$
begin
  if tg_op = 'INSERT' then
    -- Check that the insert will not result in a key collision.  If
    -- it will, do an update instead.  The insert may come from a
    -- backup from pg_dump which is why we have to handle it like
    -- this.
    if exists (
        select null
	  from veil2.system_parameters
	 where parameter_name = new.parameter_name)
    then
      update veil2.system_parameters
         set parameter_value = new.parameter_value
       where parameter_name = new.parameter_name;
      return null;
    end if;
  end if;
  new.user_defined := true;
  return new;
end;
$$
language 'plpgsql' security definer volatile leakproof;

Trigger function to allow pg_dump to dump and restore user-defined system parameters, and to ensure all inserted and updated rows are identfied as user_defined.

C.9.2. make_user_defined()

function veil2.make_user_defined()
  returns trigger
as
$$
begin
  if tg_op = 'INSERT' then
    -- Check that the insert will not result in a key collision.  If
    -- it will, do an update instead.  The insert may come from a
    -- backup from pg_dump which is why we have to handle it like
    -- this.
    if exists (
        select null
	  from veil2.authentication_types
	 where shortname = new.shortname)
    then
      update veil2.authentication_types
         set enabled = new.enabled,
	     description = new.description,
	     authent_fn = new.authent_fn,
	     supplemental_fn = new.supplemental_fn
       where shortname = new.shortname;
      return null;
    end if;
  end if;
  new.user_defined := true;
  return new;
end;
$$
language 'plpgsql' security definer volatile leakproof;

C.10. Built-in Triggers

C.10.1. context_role__context_fk

create trigger context_role__context_fk
  before insert or update on veil2.context_roles
  for each row execute function
    veil2.context_exists_chk('context_role__context_fk');

This trigger is in place of a foreign-key constraint against the scopes table. We use this rather than the FK as we expect the scopes table to be extended through inheritence which does not play nicely with FK-constraints.

C.10.2. role_role__context_fk

create trigger role_role__context_fk
  before insert or update on veil2.role_roles
  for each row execute function
    veil2.context_exists_chk('role_role__context_fk');

This trigger is in place of a foreign-key constraint against the scopes table. We use this rather than the FK as we expect the scopes table to be extended through inheritence which does not play nicely with FK-constraints.

C.10.3. accessor_role__context_fk

create trigger accessor_role__context_fk
  before insert or update on veil2.accessor_roles
  for each row execute function
    veil2.context_exists_chk('accessor_role__context_fk');

This trigger is in place of a foreign-key constraint against the scopes table. We use this rather than the FK as we expect the scopes table to be extended through inheritence which does not play nicely with FK-constraints.

C.10.4. scopes__aiudt

create trigger scopes__aiudt
  after insert or update or delete or truncate
  on veil2.scopes
  for each statement
  execute procedure veil2.refresh_scopes_matviews();

Refresh materialized views and clear caches that are constructed from the scopes table.

VPD Implementation Notes: Although we expect that scopes will be modified relatively infrequently, this may not be the case in your application. If the overhead of this trigger proves to be too significant it should be dropped, and other mechanisms used to refresh the affected materialized views. Note that this will mean that the materialized views will not always be up to date, so this is a trade-off that must be evaluated.

C.10.5. deferred_install_trg

create trigger deferred_install_trg
  after insert
  on veil2.deferred_install
  for each statement
  execute function veil2.deferred_install_fn();

This trigger exists to allow inserts into the deferred install table to cause user-provided functions and views to be installed after the current system-provided functions have completed running. This is to prevent the function that inserts into the table from being overwritten while it is still running. PostgreSQL may handle this well, I don't know - but I see no reason to stress the implementation any further than I must.

C.10.6. system_parameters_biu

create trigger system_parameters_biu before insert or update
  on veil2.system_parameters
  for each row execute function veil2.system_parameters_check();

C.10.7. authentication_types_biu

create trigger authentication_types_biu before insert or update
  on veil2.authentication_types
  for each row execute function veil2.make_user_defined();