/* ---------- * veil2--0.9.1.sql * * Create the veil2 extension for Veil2 version 0.9.1 * * Copyright (c) 2020 Marc Munro * Author: Marc Munro * License: GPL V3 * * ---------- */ -- Although technically it is evil to create roles within extensions, -- the veil_user role is so intrinsic to veil2 that it is a necessity. -- Please don't hate me for it. do $$ declare _result integer; begin select 1 into _result from pg_roles where rolname = 'veil_user'; if not found then execute 'create role veil_user'; end if; end; $$; comment on role veil_user is 'This role will have read access to all veil2 tables. That is not to say that an assignee of the role will be able to see the data in those tables, but that they will be allowed to try.'; \echo ...veil2 schema... create schema if not exists veil2; comment on schema veil2 is 'Schema containing veil2 database objects.'; revoke all on schema veil2 from public; grant usage on schema veil2 to veil_user; -- Create the VEIL2 schema tables \echo ......scope_types... create table veil2.scope_types ( scope_type_id integer not null, scope_type_name text not null, description text not null ); alter table veil2.scope_types add constraint scope_type__pk primary key(scope_type_id); alter table veil2.scope_types add constraint scope__name_uq unique(scope_type_name); comment on table veil2.scope_types is 'Identifies the types of security scope for your VPD. This can be thought of as the ''level'' of a scope. Insert one record into this table for each type of scope that you wish to implement. Veil2 comes with 2 built-in scope types: for global and personal scopes.'; revoke all on veil2.scope_types from public; grant select on veil2.scope_types to veil_user; \echo ......scopes... create table veil2.scopes ( scope_type_id integer not null, scope_id integer not null ); alter table veil2.scopes add constraint scope__pk primary key(scope_type_id, scope_id); alter table veil2.scopes add constraint scope__type_fk foreign key(scope_type_id) references veil2.scope_types(scope_type_id); comment on table veil2.scopes is 'A scope, or context, identifies a limit to access. It is a scope_type applied to a specific instance. For example, if access controls are placed in project scopes, there will be one scope record for each project that we wish to manage access to. So for three projects A, B and C, there would be 3 scopes with scope_types of project. This table as created by the Veil2 database creation scripts is incomplete. It needs additional columns to link itself with the scopes it is protecting. Your implementation must link this scopes table to the tables in your database that provide your scopes. For instance a users table or a projects table. The approved method for linking your tables to the veil2 scopes table is by defining your own veil2 table that inherits from scopes. Your inherited table will provide foreign key relationships back to your protected database. There are a number of ways to do this. Probably the simplest is to add nullable columns to this table for each type of relational context key and then add appropriate foreign key and check constraints. For example to implement a corp context with a foreign key back to your corporations table: create table veil2.scope_corps ( column corp_id integer ) inherits (veil2.scopes); -- create pk and fks for the new table based on those for veil2.scopes alter table veil2.scope_corps_link add constraint scope_corps__corp_fk foreign key (corp_id) references my_schema.corporations(corp_id); -- Ensure that for corp context types we have a corp_id -- (assume corp_context has scope_type_id = 3) alter table veil2.scope_corps add constraint scope_corp__corp_chk check ((scope_type_id != 3) or ((scope_type_id = 3) and (corp_id is not null))); You will, of course, also need to ensure that the corp_id field is populated. Note that global scope uses scope_id 0. Ideally it would be null, since it does not relate directly to any other entity but that makes defining foreign key relationships (to this table) difficult. Using a reserved value of zero is just simpler (though suckier).'; comment on column veil2.scopes.scope_type_id is 'Identifies the type of scope that we are describing.'; comment on column veil2.scopes.scope_id is 'This, in conjunction with the scope_type_id, fully identifies a scope or context. For global scope, this id is 0: ideally it would be null but as it needs to be part of the primary key of this table, that is not possible. The scope_id provides a link back to the database we are protecting, and will usually be the key to some entity that can be said to ''own'' data. This might be a party, or a project, or a department.'; revoke all on veil2.scopes from public; grant select on veil2.scopes to veil_user; \echo ......context_exists_chk() (function)... create or replace 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; revoke all on function veil2.context_exists_chk() from public; comment on function veil2.context_exists_chk() is '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.'; \echo ......privileges... create table veil2.privileges ( privilege_id integer not null, privilege_name text not null, promotion_scope_type_id integer, description text ); comment on table veil2.privileges is 'This provides all privileges used by our VPD. There should be no need for anyone other than administrators to have any access to this table. A privilege is the lowest level of access control. It should be used to allow the holder of that privilege to do exactly one thing, for example ''select privileges'' should be used to allow the privilege holder to select from the privileges table. It should not be used for any other purpose. Note that the name of the privilege is only a clue to its usage. We use the privilege ids and not the names to manage access. It is the responsibility of the implementor to ensure that a privilege''s name matches the purpose to which it is put.'; comment on column veil2.privileges.privilege_id is 'Primary key for privilege. This is the integer that will be used as a key into our privilege bitmaps. It is not generated from a sequence as we want to have very tight control of the privilege_ids. The range of privilege_ids in use should be kept as small as possible. If privileges become deprecated, you should (once you have ensured that the old privilege_id is not in use *anywhere*) try to re-use the old privilege_ids rather than extending the range of privilege_ids by allocating new ones. This will keep your privilege bitmaps smaller, which should in turn improve performance.'; comment on column veil2.privileges.privilege_name is 'A descriptive name for a privilege. This should generally be enough to figure out the purpose of the privilege.'; comment on column veil2.privileges.promotion_scope_type_id is 'Identfies a security scope type to which this privileges scope should be promoted if possible. This allows roles which will be assigned in a restricted security context to contain privileges which necessarily must apply in a superior scope (ie as if they had been assigned in a superior context). For example a hypothetical ''select lookup'' privilege may be assigned in a team context (via a hypothetical ''team member'' role). But if the lookups table is not in any way team-specific it makes no sense to apply that privilege in that scope. Instead, we will promote that privilege to a scope where it does make sense. See the Veil2 docs for more on privilege promotion and on the use of the terms scope and context.'; comment on column veil2.privileges.description is 'For any privilege whose purpose cannot easily be determined from the name, a description of the privilege should appear here.'; alter table veil2.privileges add constraint privilege__pk primary key(privilege_id); alter table veil2.privileges add constraint privilege__promotion_scope_type_fk foreign key(promotion_scope_type_id) references veil2.scope_types(scope_type_id); revoke all on veil2.privileges from public; grant select on veil2.privileges to veil_user; \echo ......role_types... create table veil2.role_types ( role_type_id integer not null, role_type_name text not null, description text ); comment on table veil2.role_types is 'A role type is used to classify roles so that they may be shown and used in different ways. This is mostly a VPD implementation choice. For instance you may choose to distinguish between user and function-level roles so that you can prevent role assignments to user-level roles. In such a case you might add columns to this table to identify specific properties of specific role_types.'; alter table veil2.role_types add constraint role_type__pk primary key(role_type_id); alter table veil2.role_types add constraint role_type__name_uk unique(role_type_name); revoke all on veil2.role_types from public; grant select on veil2.role_types to veil_user; \echo ......roles... create sequence veil2.role_id_seq; create table veil2.roles ( role_id integer not null, role_type_id integer not null default(1), role_name text not null, implicit boolean not null default false, immutable boolean not null default false, description text ); comment on table veil2.roles is 'A role is a way of collecting privileges (and other roles) into groups for easier management.'; comment on column veil2.roles.role_id is 'Primary key for role.'; comment on column veil2.roles.role_name is 'A descriptive name for a role. This should generally be enough to figure out the purpose of the role.'; comment on column veil2.roles.implicit is 'Whether this role is implicitly assigned to all accessors. Such roles may not be explicitly assigned.'; comment on column veil2.roles.immutable is 'Whether this role is considered unmodifiable. Such roles may not be the primary role in a role_role assignment, ie you cannot assign other roles to them.'; comment on column veil2.roles.description is 'For any role whose purpose cannot easily be determined from the name, a description of the role should appear here.'; alter table veil2.roles add constraint role__pk primary key(role_id); alter table veil2.roles add constraint role__name_uk unique(role_name); alter table veil2.roles add constraint role__type_fk foreign key(role_type_id) references veil2.role_types(role_type_id); revoke all on veil2.roles from public; grant select on veil2.roles to veil_user; \echo ......context_roles... create table veil2.context_roles ( role_id integer not null, role_name text not null, context_type_id integer not null, context_id integer not null ); comment on table veil2.context_roles is 'This provides a context-based role-name for a role. The purpose of this is to allow certain security contexts to name their own roles. This, coupled with role_roles, allows for role definitions to be different in different contexts. It is primarily aimed at VPDs where there are completely independent sets of accessors. For example in a SaaS implementation where each corporate customer gets their virtual private database and no customer can see any data for any other customer. In such a case it is likely that roles will be different, will have different names, and different sets of roles will exist. If this makes no sense to you, you probably have no need for it, so don''t use it. If do choose to use, do so sparingly as it could lead to great confusion.'; alter table veil2.context_roles add constraint context_role__pk primary key(role_id, context_type_id, context_id); alter table veil2.context_roles add constraint context_role__name_uk unique(role_name, context_type_id, context_id); alter table veil2.context_roles add constraint context_role__role_fk foreign key(role_id) references veil2.roles(role_id); 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'); comment on trigger context_role__context_fk on veil2.context_roles is '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.'; revoke all on veil2.context_roles from public; grant select on veil2.context_roles to veil_user; \echo ......role_privileges... create table veil2.role_privileges ( role_id integer not null, privilege_id integer not null ); comment on table veil2.role_privileges is 'Records the mapping of privileges to roles. Roles will be assigned to parties in various contexts; privileges are only assigned indirectly through roles. Note that role privileges should not be managed by anyone other than a developer or administrator that understands the requirements of system access controls. Getting this wrong is the best route to your system having poor database security. There should be no need for anyone other than administrators to have any access to this table. User management of roles should be done through user visible role->role mappings. While this may seem an odd concept, the use of roles in databases provides a good model for how this can work. Note that the assignment of role to role may be something that is done within a specific security context: consider that the database may be storing data for separate groups of parties (eg companies) and the role->role assignment may therefore need to be specific to those groups (eg a customer liaison role in one company may need different privileges from a similar role in another company).'; alter table veil2.role_privileges add constraint role_privilege__pk primary key(role_id, privilege_id); alter table veil2.role_privileges add constraint role_privilege__role_fk foreign key(role_id) references veil2.roles(role_id); alter table veil2.role_privileges add constraint role_privilege__privilege_fk foreign key(privilege_id) references veil2.privileges(privilege_id); revoke all on veil2.role_privileges from public; grant select on veil2.role_privileges to veil_user; \echo ......role_roles... create table veil2.role_roles ( primary_role_id integer not null, assigned_role_id integer not null, context_type_id integer not null, context_id integer not null ); comment on table veil2.role_roles is 'This table shows the mapping of roles to roles in various contexts. The purpose of context-specific role mappings is to enable custom role mappings in different situations. An example of when this may be useful is when creating a SaaS application for multiple corporate customers. Each corporation can have their own role mappings, unaffected and unseen by other corporations. This means that a CSR role at one corporation may have different privileges from a CSR at another.'; alter table veil2.role_roles add constraint role_role__pk primary key(primary_role_id, assigned_role_id, context_type_id, context_id); alter table veil2.role_roles add constraint role_role__primary_role_fk foreign key(primary_role_id) references veil2.roles(role_id); alter table veil2.role_roles add constraint role_role__assigned_role_fk foreign key(assigned_role_id) references veil2.roles(role_id); 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'); comment on trigger role_role__context_fk on veil2.role_roles is '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.'; revoke all on veil2.role_roles from public; grant select on veil2.role_roles to veil_user; \echo .....accessors... create table veil2.accessors ( accessor_id integer not null, username text, notes text ); comment on table veil2.accessors is 'Identifies parties that may access our database. If this is a party that should have direct database access (ie they are a database user), we record their username here. This allows our security functions to associate the connected database user with their assigned privileges. VPD Implementation Notes: You are likely to want to implement a foreign-key relationship back to your users table in your protected database (each accessor is a user). It is likely that your accessor_id can simply be the same as the user_id (or party_id, or person_id...). If this is not the case, you can add columns to this table as needed and define FKs as needed. In the simple case you will do something like this: alter table veil2.accessors add constraint accessor__user_fk foreign key(accessor_id) references my_schema.users(user_id); In the event that you have multiple types of accessors, with overlapping ranges of keys, you may have to extend this table to add an accessor_type, and other columns to provide the actual foreign-key values. As accessor_id is heavily used by Veil2 you *must* ensure that this value is truly unique.'; comment on column veil2.accessors.username is 'If this is provided, it should match a database username. This allows a database user to be associated with the accessor_id, and for their privileges to be determined.'; comment on column veil2.accessors.accessor_id is 'The id of the database accessor. This is the id used throughout Veil2 for determining access rights. Ideally this will be the id of the user from the protected database'; alter table veil2.accessors add constraint accessor__pk primary key(accessor_id); alter table veil2.accessors add constraint accessor__username_uk unique (username); revoke all on veil2.accessors from public; grant select on veil2.accessors to veil_user; \echo ......authentication_types... create table veil2.authentication_types ( shortname text not null, enabled boolean not null, description text not null, authent_fn text not null, supplemental_fn text, user_defined boolean ); comment on table veil2.authentication_types is 'Types of authentication supported by this VPD.'; comment on column veil2.authentication_types.shortname is 'A short textual identifier for this type of authentication. This acts as the primary key.'; comment on column veil2.authentication_types.enabled is 'Whether this authentication type is currently enabled. If it is not, you will not be able to authenticate using this method.'; comment on column veil2.authentication_types.description is 'A description of this authentication type.'; comment on column veil2.authentication_types.authent_fn is 'The name of a function that will determine whether a supplied authentication token is correct. The signature for this function is: fn(accessor_id integer, token text) returns boolean; It will return true if the supplied token is what is expected.'; comment on column veil2.authentication_types.supplemental_fn is 'The name of a function that will return session_supplemental values for create_session. The signature for this function is: fn(accessor_id in integer, session_token in out text, session_supplemental out text) returns record; The provided session_token is a random value, that may be returned untouched or may be modified. The session_supplemental result is supplemental data for the chosen authentication protocol. This is where you might return the base and modulus selection for a Diffie-Hellman exchange, should you wish to implement such a thing.'; comment on column veil2.authentication_types.user_defined is 'Whether this parameter value was modified by the user. This is needed for exports using pg_dump.'; alter table veil2.authentication_types add constraint authentication_type__pk primary key(shortname); revoke all on veil2.authentication_types from public; grant select on veil2.authentication_types to veil_user; \echo ......authentication_details... create table veil2.authentication_details ( accessor_id integer not null, authentication_type text not null, authent_token text not null ); comment on table veil2.authentication_details is 'Types of authentication available for individual parties, along with whatever authentication tokens are needed for that form of authentication. Because this table stores authentication tables, access to it must be as thoroughly locked down as possible.'; comment on column veil2.authentication_details.authentication_type is 'Identifies a specific authentication type. More than 1 authentication type may be available to some parties.'; comment on column veil2.authentication_details.authent_token is 'An authentication token for the party for the given authentication type. If we were using plaintext passwords (do not do this), this would be where the password would be stored.'; alter table veil2.authentication_details add constraint authentication_detail__pk primary key(accessor_id, authentication_type); alter table veil2.authentication_details add constraint authentication_detail__authent_type_fk foreign key(authentication_type) references veil2.authentication_types(shortname); alter table veil2.authentication_details add constraint authentication_detail__accessor_fk foreign key(accessor_id) references veil2.accessors(accessor_id) on delete cascade on update cascade; comment on constraint authentication_detail__accessor_fk on veil2.authentication_details is 'Since accessors may be updated or deleted as a result of transactions in our secured database, we must allow such updates or deletions to cascade to this table as well. The point of this is that the application need not know about fk relationships that are internal to Veil2.'; revoke all on veil2.authentication_details from public; grant select on veil2.authentication_details to veil_user; \echo ......accessor_roles... create table veil2.accessor_roles ( accessor_id integer not null, role_id integer not null, context_type_id integer not null, context_id integer not null ); comment on table veil2.accessor_roles is 'This records the assignment of roles to accessors in various contexts. A role assigned to a party here, grants that accessor all of the privileges that that role has been assigned, whether directly or indirectly.'; alter table veil2.accessor_roles add constraint accessor_role__pk primary key(accessor_id, role_id, context_type_id, context_id); alter table veil2.accessor_roles add constraint accessor_role__accessor_fk foreign key(accessor_id) references veil2.accessors(accessor_id) on delete cascade on update cascade; comment on constraint accessor_role__accessor_fk on veil2.accessor_roles is 'Since accessors may be updated or deleted as a result of transactions in our secured database, we must allow such updates or deletions to cascade to this table as well. The point of this is that the application need not know about fk relationships that are internal to Veil2.'; 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'); comment on trigger accessor_role__context_fk on veil2.accessor_roles is '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.'; revoke all on veil2.accessor_roles from public; grant select on veil2.accessor_roles to veil_user; \echo ......sessions... create sequence veil2.session_id_seq minvalue 1 maxvalue 2000000000 cycle; create unlogged table veil2.sessions ( session_id integer not null default nextval('veil2.session_id_seq'), accessor_id integer not null, login_context_type_id integer not null, login_context_id integer not null, session_context_type_id integer not null, session_context_id integer not null, mapping_context_type_id integer not null, mapping_context_id integer not null, authent_type text not null, expires timestamp with time zone, token text not null, has_authenticated boolean not null, session_supplemental text, nonces bitmap, parent_session_id integer ); comment on table veil2.sessions is 'Records active sessions. There should be a background task to delete expired sessions and keep this table vacuumed. Note that for performance reasons we may want to disable any foreign key constraints on this table. Note that access to this table should not be granted to normal users. This table can be used to determine whether a create_session() call successfully created a session, and so can aid in username fishing.'; comment on column veil2.sessions.login_context_type_id is 'This, along with the login_context_id column describes the context used for authentication of this session. This allows users to log in in specific contexts (eg for dept a, rather than dept b), within which role mappings may differ. This context information allows the session to determine which role mappings to apply.'; comment on column veil2.sessions.login_context_id is 'See comment on veil2.sessions.login_context_type_id'; comment on column veil2.sessions.mapping_context_type_id is 'This, along with the mapping_context_id column describes the context used for role->role mapping by this session.'; comment on column veil2.sessions.mapping_context_id is 'See comment on veil2.sessions.mapping_context_type_id'; comment on column veil2.sessions.parent_session_id is 'Used by become-user sessions to record their parent session_id'; alter table veil2.sessions add constraint session__pk primary key(session_id); /* * For performance reasons we will not create FK constraints on this * table. alter table veil2.sessions add constraint session__accessor_fk foreign key(accessor_id) references veil2.accessors(accessor_id); */ revoke all on veil2.sessions from public; grant select on veil2.sessions to veil_user; \echo ......system_parameters... create table veil2.system_parameters ( parameter_name text not null, parameter_value text not null, user_defined boolean ); alter table veil2.system_parameters add constraint system_parameter__pk primary key(parameter_name); comment on table veil2.system_parameters is 'Provides values for various parameters.'; comment on column veil2.system_parameters.user_defined is 'Whether this parameter value was modified by the user. This is needed for exports using pg_dump.'; revoke all on veil2.system_parameters from public; grant select on veil2.system_parameters to veil_user; \echo ......deferred_install... create table veil2.deferred_install ( install_time timestamp with time zone not null); comment on table veil2.deferred_install is 'This table is used solely to provide a hook for a trigger. By inserting into this table, a trigger is fired which will cause any user-provided veil2 objects to replace their equivalent system-provided ones.'; revoke all on veil2.deferred_install from public; grant select on veil2.deferred_install to veil_user; \echo ......session_privileges_t... create type veil2.session_privileges_t as ( scope_type_id integer, scope_id integer, roles bitmap, privs bitmap ); comment on type veil2.session_privileges_t is 'Records the privileges for active sessions in each assigned context. This type is used for the generation of a veil2_session_privileges temporary table which is populated by Veil2''s session management functions.'; \echo ......session_context_t(type)... create type veil2.session_context_t as ( accessor_id integer, session_id integer, login_context_type_id integer, login_context_id integer, session_context_type_id integer, session_context_id integer, mapping_context_type_id integer, mapping_context_id integer, parent_session_id integer ); comment on type veil2.session_context_t is 'Records context for the current session. This type is used for the generation of a veil2_session_context temporary table which is populated by Veil2''s session management functions.'; comment on column veil2.session_context_t.accessor_id is 'The id of the accessor whose session this is.'; comment on column veil2.session_context_t.accessor_id is '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.'; comment on column veil2.session_context_t.login_context_type_id is '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.'; comment on column veil2.session_context_t.login_context_id is '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.'; comment on column veil2.session_context_t.session_context_type_id is '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.'; \echo ......accessor_privileges_cache create table veil2.accessor_privileges_cache ( accessor_id integer not null, login_context_type_id integer not null, login_context_id integer not null, session_context_type_id integer not null, session_context_id integer not null, mapping_context_type_id integer not null, mapping_context_id integer not null, scope_type_id integer not null, scope_id integer not null, roles bitmap not null, privs bitmap not null ); comment on table veil2.accessor_privileges_cache is 'Table used to cache accessor_privileges returned by veil2.session_privileges_v. This is automatically populated by the Veil2 session management functions for any combination of accessor and session context for which it contains no data. It should be truncated whenever any underlying role, privilege or context data is updated, and records for individual accessors should be deleted whenever their role assignments are updated.'; create index accessor_privileges_cache__accessor_idx on veil2.accessor_privileges_cache(accessor_id); comment on index veil2.accessor_privileges_cache__accessor_idx is 'There is no need for a PK on this cache table. The way this table is used is: - retrieve the scopes, roles and privs for a given accessor and login context: extending the index to include login_context will have very little impact on performance; - truncating the whole thing when roles, role_roles or privileges are modified; - removing entries for a single accessor when an accessor''s roles have changed: an index solely on accessor_id is perfect for this.'; -- Create the VEIL2 schema views, including matviews -- \echo ......all_role_roles... create or replace view veil2.all_role_roles ( primary_role_id, assigned_role_id, context_type_id, context_id) as with recursive assigned_roles ( primary_role_id, assigned_role_id, context_type_id, context_id) as ( -- get all role->role assignments, both direct and indirect, in all contexts select primary_role_id, assigned_role_id, context_type_id, context_id, bitmap(primary_role_id) + assigned_role_id as roles_encountered from veil2.role_roles union all select ar.primary_role_id, rr.assigned_role_id, ar.context_type_id, ar.context_id, ar.roles_encountered + rr.assigned_role_id from assigned_roles ar inner join veil2.role_roles rr on rr.primary_role_id = ar.assigned_role_id and rr.context_type_id = ar.context_type_id and rr.context_id = ar.context_id and not ar.roles_encountered ? rr.assigned_role_id and rr.primary_role_id != 1 -- Superuser role is handled below ), superuser_roles (primary_role_id, assigned_role_id) as ( select 1, role_id from veil2.roles where role_id not in (1, 0) -- not connect and not superuser and not implicit -- and not implicitly assigned roles ) select primary_role_id, assigned_role_id, context_type_id, context_id from assigned_roles union all select primary_role_id, assigned_role_id, null, null from superuser_roles; comment on view veil2.all_role_roles is 'Show all role to role mappings in all contexts. If the context is null, the mapping applies in all contexts, taking into account role mappings that occur indirectly through other role mappings. Indirect mappings occur through other mappings (ie mappings are transitive). Eg if a is assigned to b and b to c, then by transitivity a is assigned (indirectly) to c. Note that the superuser role is implicitly assigned all non-implicit roles except connect.'; revoke all on veil2.all_role_roles from public; grant select on veil2.all_role_roles to veil_user; \echo ......all_role_privileges... create or replace view veil2.all_role_privileges_v as with superuser_privs as ( -- Superuser role has implied assignments of all privileges except -- connect. select bitmap_of(privilege_id) as privileges from veil2.privileges where privilege_id != 0 ) select r.role_id as role_id, rr.context_type_id as mapping_context_type_id, rr.context_id as mapping_context_id, coalesce(bitmap_of(rr.assigned_role_id) + r.role_id, bitmap(r.role_id)) as roles, case when r.role_id = 1 then (select privileges from superuser_privs) else coalesce(bitmap_of(rp.privilege_id), bitmap()) end as privileges from veil2.roles r left outer join veil2.all_role_roles rr on rr.primary_role_id = r.role_id left join veil2.role_privileges rp on rp.role_id = r.role_id or rp.role_id = rr.assigned_role_id group by r.role_id, rr.context_type_id, rr.context_id; comment on view veil2.all_role_privileges_v is 'Provides all role to role mappings, with their resulting privileges in all mapping contexts. If the mapping context is null, the mapping applies in all mapping contexts. For performance reasons a materialized view veil2.all_role_privileges, has been created. This must be refreshed whenever data underlying this view is updated.'; create or replace view veil2.all_role_privileges_info as select role_id, mapping_context_type_id, mapping_context_id, to_array(roles) as roles, to_array(privileges) as privileges from veil2.all_role_privileges_v; comment on view veil2.all_role_privileges_info is 'Developer view on all_role_privileges showing roles and privileges as arrays of integers for easier comprehension.'; create materialized view veil2.all_role_privileges as select * from veil2.all_role_privileges_v; comment on materialized view veil2.all_role_privileges is 'Materialized view on veil2.all_role_privileges_v. This exists to improve the performance of veil2.session_privileges_v. Any time that the data underlying all_role_privileges_v is modified, this materialized view should be refreshed.'; revoke all on veil2.all_role_privileges_v from public; grant select on veil2.all_role_privileges_v to veil_user; revoke all on veil2.all_role_privileges from public; grant select on veil2.all_role_privileges to veil_user; revoke all on veil2.all_role_privileges_info from public; grant select on veil2.all_role_privileges_info to veil_user; \echo ......accessor_contexts... create or replace view veil2.accessor_contexts ( accessor_id, context_type_id, context_id ) as select accessor_id, 1, 0 from veil2.accessors; comment on view veil2.accessor_contexts is 'This view lists the allowed session (and login, where different) contexts for accessors. The system-provided version of this view may be overridden by providing an equivalent view called veil2.my_accessor_contexts. When an accessor opens a session, they choose a session context. This session context determines which set of role to role mappings are in play. Typically, there will only be one such set, as provided by the default implementation of this view. If however, your application requires separate contexts to have different role to role mappings, you should modify this view to map your accessors with that context. Typically this will be used in a situation where your application serves a number of different clients, each of which have their own role definitions. Each accessor will belong to one of those clients and this view should be modified to make that mapping apparent. A typical view definition might be: select party_id, 3, client_id from app_schema.parties union all select party_id, 1, 0 from mycorp_schema.superusers; which would allow those defined in the superusers table to connect in the global scope, and those defined in the parties table to connect in the context of the client that they work for. Note that any change to the underlying data of this view (ie one that changes what the view will show) *must* cause a full refresh of all Veil2 materialized views and caches.'; \echo ......superior_scopes... create or replace view veil2.superior_scopes ( scope_type_id, scope_id, superior_scope_type_id, superior_scope_id ) as select null::integer, null::integer, null::integer, null::integer where false; comment on view veil2.superior_scopes is 'This view identifies superior scopes for determining the scope hierarchy. This is used for determing how to promote privileges when privilege promotion is needed, which happens when a role that is assigned in a restricted security context has privileges that must be applied in a less restricted scope. Note that promotion to global scope is always possible and is not managed through this view. If you have restricted scopes which are descendant scopes of less restricted ones, and you need privileges assigned in the restricted context to be promoted to the less restricted one, you must override this view to show which scopes may be promoted to which other scopes. For example if you have a corp scope type and a dept scope type which is a sub-scope of it, and your departments table identifies the corp_id for each department, you would define your over-riding view something like this: create or replace view veil2.my_superior_scopes ( scope_type_id, scope_id, superior_scope_type_id, superior_scope_id ) as select 96, -- dept scope type id department_id, 95, -- corp scope type id corp_id from departments; Multi-level context promotions (eg to grandparent or great-grandparent scopes) will be handled by veil2.all_superior_scopes which you should have no need to modify. Note that any change to the underlying data of this view (ie one that changes what the view will show) *must* cause a full refresh of all Veil2 materialized views and caches.'; revoke all on veil2.superior_scopes from public; grant select on veil2.superior_scopes to veil_user; \echo ......all_superior_scopes... create or replace view veil2.all_superior_scopes_v ( scope_type_id, scope_id, superior_scope_type_id, superior_scope_id, is_type_promotion ) as with recursive recursive_superior_scopes as ( select scope_type_id, scope_id, superior_scope_type_id, superior_scope_id, scope_type_id != superior_scope_type_id from veil2.superior_scopes union select rsp.scope_type_id, rsp.scope_id, sp.superior_scope_type_id, sp.superior_scope_id, sp.scope_type_id != sp.superior_scope_type_id from recursive_superior_scopes rsp inner join veil2.superior_scopes sp on sp.scope_type_id = rsp.superior_scope_type_id and sp.scope_id = rsp.superior_scope_id where not ( sp.superior_scope_type_id = rsp.superior_scope_type_id and sp.superior_scope_id = rsp.superior_scope_id) ) select * from recursive_superior_scopes; comment on view veil2.all_superior_scopes_v is 'This takes the simple user-provided view veil2.superior_scopes and makes it recursive so that if context a contains scope b and scope b contains scope c, then this view will return rows for scope c promoting to both scope b and scope a. You should not need to modify this view when creating your custom VPD implementation. Note that for performance reasons a materialized version of this view, veil2.all_superior_scopes, has been created. Any change to the data underlying this view must result in the materialized view being refreshed.'; create materialized view veil2.all_superior_scopes as select * from veil2.all_superior_scopes_v; comment on materialized view veil2.all_superior_scopes is 'This is a materialized view on veil2.all_superior_scopes_v. It exists in order to improve the performance of veil2.session_privileges_v. It must be fully refreshed whenever the underlying data for veil2.superior_scopes is updated.'; revoke all on veil2.all_superior_scopes from public; grant select on veil2.all_superior_scopes to veil_user; revoke all on veil2.all_superior_scopes_v from public; grant select on veil2.all_superior_scopes_v to veil_user; \echo ......scope_tree... create or replace view veil2.scope_tree (scope_tree) as with recursive top_scopes as ( select distinct sp.superior_scope_id as root_scope_id, sp.superior_scope_type_id as root_scope_type_id, st.scope_type_name as root_scope_type_name, st.scope_type_id || ' (' || st.scope_type_name || ').' || sp.superior_scope_id as root_full_name from veil2.superior_scopes sp inner join veil2.scope_types st on st.scope_type_id = sp.superior_scope_type_id where (sp.superior_scope_type_id, sp.superior_scope_id) not in ( select sp2.scope_type_id, sp2.scope_id from veil2.superior_scopes sp2) ), recursive_part as ( select 1 as depth, root_scope_id as scope_id, root_scope_type_id as scope_type_id, root_full_name as full_name, '(' || root_scope_type_id || '.' || root_scope_id || ')' as path, length(root_full_name) as path_length from top_scopes union all select rp.depth + 1, sp.scope_id, sp.scope_type_id, st.scope_type_id || ' (' || st.scope_type_name || ').' || sp.scope_id, rp.path || '(' || sp.scope_type_id || '.' || sp.scope_id || ')', length(st.scope_type_name || '.' || sp.scope_id) + path_length from recursive_part rp inner join veil2.superior_scopes sp on sp.superior_scope_id = rp.scope_id and sp.superior_scope_type_id = rp.scope_type_id inner join veil2.scope_types st on st.scope_type_id = sp.scope_type_id ) select format('%' || ((depth * 4) - 2) || 's', '+ ') || full_name from recursive_part order by path; comment on view veil2.scope_tree is 'Provides a simple ascii-formatted tree representation of our scope promotions tree. This is an aid to data visualisation for data designers and administrators and is not used elsewhere in Veil2.'; revoke all on veil2.scope_tree from public; grant select on veil2.scope_tree to veil_user; \echo ......promotable_privileges... create view veil2.promotable_privileges ( scope_type_id, privilege_ids) as select p.promotion_scope_type_id, bitmap_of(p.privilege_id) from veil2.privileges p where p.promotion_scope_type_id is not null group by p.promotion_scope_type_id; comment on view veil2.promotable_privileges is 'Provide bitmaps of those privileges that may be promoted, mapped to the context types to which they should promote.'; create view veil2.promotable_privileges_info ( scope_type_id, privilege_ids) as select scope_type_id, to_array(privilege_ids) from veil2.promotable_privileges; comment on view veil2.promotable_privileges_info is 'As veil2.promotable_privileges with bitmaps shown as arrays. Info views are intended as developer-readable versions of the non-info views.'; revoke all on veil2.promotable_privileges from public; grant select on veil2.promotable_privileges to veil_user; revoke all on veil2.promotable_privileges_info from public; grant select on veil2.promotable_privileges_info to veil_user; \echo ......all_accessor_roles... create or replace view veil2.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; comment on view veil2.all_accessor_roles is 'Provides all of an accessor''s explicit role assignments, ie it does not provide the personal context role. If you have any explicitly assigned roles that are not granted through the veil2.accessor_role table, you must provide your own definition of this view (called veil2.my_all_accessor_roles). For example if you have a project context that is dependent on an accessor being assigned to a project you might redefine the view as follows: 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 party_id, role_id, 99, -- id for project context_type project_id from project_parties; Note that any change to the underlying data of this view (ie one that changes what the view will show) *must* cause a full or partial refresh of all Veil2 materialized views and caches.'; revoke all on veil2.all_accessor_roles from public; grant select on veil2.all_accessor_roles to veil_user; \echo ......all_accessor_roles_plus... create or replace view veil2.all_accessor_roles_plus as select accessor_id, role_id, context_type_id, context_id from veil2.all_accessor_roles union all select accessor_id, 2, 2, accessor_id from veil2.accessors; comment on view veil2.all_accessor_roles_plus is 'As all_accessor_roles but also providing the implicitly assigned personal context role for each accessor. '; revoke all on veil2.all_accessor_roles_plus from public; grant select on veil2.all_accessor_roles_plus to veil_user; \echo ......role_chains... create or replace view veil2.role_chains as with recursive role_chains as ( select rr.primary_role_id, rr.assigned_role_id, rr.primary_role_id::text || '->' || rr.assigned_role_id::text as id_chain, r1.role_name || '->' || r2.role_name as name_chain, rr.context_type_id, rr.context_id, bitmap(rr.primary_role_id) + rr.assigned_role_id as roles_bitmap from veil2.role_roles rr inner join veil2.roles r1 on r1.role_id = rr.primary_role_id inner join veil2.roles r2 on r2.role_id = rr.assigned_role_id union all select rc.primary_role_id, rr.assigned_role_id, rc.id_chain || '->' || rr.assigned_role_id::text, rc.name_chain || '->' || r.role_name, rc.context_type_id, rc.context_id, rc.roles_bitmap + rr.assigned_role_id from role_chains rc inner join veil2.role_roles rr on rr.primary_role_id = rc.assigned_role_id and rr.context_type_id = rc.context_type_id and rr.context_id = rc.context_id inner join veil2.roles r on r.role_id = rr.assigned_role_id where not rc.roles_bitmap ? rr.assigned_role_id ), all_contexts as ( select distinct context_type_id, context_id from role_chains ), base_roles as ( select r.role_id as primary_role_id, r.role_id as assigned_role_id, r.role_id::text as id_chain, r.role_name as name_chain, ac.context_type_id, ac.context_id from veil2.roles r cross join all_contexts ac ) select primary_role_id, assigned_role_id, context_type_id, context_id, id_chain, name_chain from role_chains union all select primary_role_id, assigned_role_id, context_type_id, context_id, id_chain, name_chain from base_roles order by 3, 4, 1, 2; comment on view veil2.role_chains is 'This is a developer view. It is intended for development and debugging, and provides a way to view role mappings in a simple but complete way. Try it, it should immediately make sense.'; revoke all on veil2.role_chains from public; grant select on veil2.role_chains to veil_user; \echo ......privilege_assignments... create or replace view veil2.privilege_assignments as select aar.accessor_id, rp.privilege_id, aar.context_type_id as ass_cntxt_type_id, aar.context_id as ass_cntxt_id, coalesce(p.promotion_scope_type_id, aar.context_type_id) as scope_type_id, coalesce(asp.superior_scope_id, aar.context_id) as scope_id, rc.primary_role_id as ass_role_id, rc.assigned_role_id as priv_bearing_role_id, rc.id_chain as role_id_mapping, rc.name_chain as role_name_mapping, rc.context_type_id as map_cntxt_type_id, rc.context_id as map_cntxt_id from ( select role_id, privilege_id from veil2.role_privileges union all select 1, privilege_id from veil2.privileges ) rp inner join veil2.privileges p on p.privilege_id = rp.privilege_id inner join veil2.role_chains rc on rc.assigned_role_id = rp.role_id inner join veil2.all_accessor_roles_plus aar on aar.role_id = rc.primary_role_id left outer join veil2.all_superior_scopes asp on asp.scope_type_id = aar.context_type_id and asp.scope_id = aar.context_id and asp.superior_scope_type_id = p.promotion_scope_type_id and asp.is_type_promotion; comment on view veil2.privilege_assignments is 'Developer view that shows how accessors get privileges. It shows the roles that the user is assigned, and the context in which they are assigned, as well as the mappings from role to role to privilege which give that resulting privilege to the accessor. If you are uncertain how accessor 999 has privilege 333, then simply run: select * from veil2.privilege_assignments where accessor_id = 999 and privilege_id = 333;'; revoke all on veil2.privilege_assignments from public; grant select on veil2.privilege_assignments to veil_user; \echo ......session_context()... create or replace 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; revoke all on function veil2.session_context() from public; grant execute on function veil2.session_context() to veil_user; comment on function veil2.session_context() is '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.'; \echo ......session_assignment_contexts... create or replace view veil2.session_assignment_contexts as select login_context_type_id as context_type_id, login_context_id as context_id from veil2.session_context() sc union select session_context_type_id as context_type_id, session_context_id as context_id from veil2.session_context() sc union select ass.superior_scope_type_id, ass.superior_scope_id from veil2.session_context() sc inner join veil2.all_superior_scopes ass on ( ass.scope_type_id = sc.login_context_type_id and ass.scope_id = sc.login_context_id) or ( ass.scope_type_id = sc.session_context_type_id and ass.scope_id = sc.session_context_id) union select ass.scope_type_id, ass.scope_id from veil2.session_context() sc inner join veil2.all_superior_scopes ass on ( ass.superior_scope_type_id = sc.login_context_type_id and ass.superior_scope_id = sc.login_context_id) or ( ass.superior_scope_type_id = sc.session_context_type_id and ass.superior_scope_id = sc.session_context_id) union select 1, 0 union select 2, accessor_id from veil2.session_context(); comment on view veil2.session_assignment_contexts is 'Provides the set of security contexts which are valid for role assignments within the current session. The purpose of this is to filter out any role assignments which should not apply to the current session, as those roles could contain privileges which could be promoted to global_scope. The situation this prevents is for users that are allowed to login in different contexts with different roles in those contexts. We do not want the roles provided in one context to provide privileges that have not been assigned when we are logged-in in a different context.'; revoke all on veil2.session_assignment_contexts from public; grant select on veil2.session_assignment_contexts to veil_user; \echo ......all_accessor_roles(function)... create or replace 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; revoke all on function veil2.all_accessor_roles( integer, integer, integer) from public; comment on function veil2.all_accessor_roles(integer, integer, integer) is 'Return all roles for the given accessor in the given session context.'; \echo ......base_accessor_roleprivs(function)... create or replace 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; revoke all on function veil2.base_accessor_roleprivs( integer, integer, integer, integer, integer) from public; comment on function veil2.base_accessor_roleprivs( integer, integer, integer, integer, integer) is 'Give the set of base (ignoring privilege promotion) roles and privileges that apply to a given accessor in given mapping and session contexts.'; \echo ......session_privileges_v... create or replace view veil2.session_privileges_v as with session_context as ( select * from veil2.session_context() sc ), 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 session_context sc -- 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( sc.accessor_id, sc.session_context_type_id, sc.session_context_id, sc.mapping_context_type_id, sc.mapping_context_id) barp on barp.accessor_id = sc.accessor_id ), 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 accessor_id, scope_type_id, scope_id, union_of(roles) as roles, union_of(privileges) as privileges from all_role_privs group by accessor_id, scope_type_id, scope_id ), have_global_connect as ( select exists ( select null from grouped_role_privs where scope_type_id = 1 and scope_id = 0 and privileges ? 0) as have_global_connect ), have_session_connect as ( select exists ( select null from session_context sc cross join grouped_role_privs grp where grp.privileges ? 0 and ( ( grp.scope_type_id = sc.session_context_type_id and grp.scope_id = sc.session_context_id) or (grp.scope_type_id, grp.scope_id) in ( select ass.superior_scope_type_id, ass.superior_scope_id from veil2.all_superior_scopes ass where ass.scope_type_id = sc.session_context_type_id and ass.scope_id = sc.session_context_id))) as have_session_connect ), have_login_connect as ( select exists ( select null from session_context sc cross join grouped_role_privs grp where grp.privileges ? 0 and ( ( grp.scope_type_id = sc.login_context_type_id and grp.scope_id = sc.login_context_id) or (grp.scope_type_id, grp.scope_id) in ( select ass.superior_scope_type_id, ass.superior_scope_id from veil2.all_superior_scopes ass where ass.scope_type_id = sc.login_context_type_id and ass.scope_id = sc.login_context_id))) as have_login_connect ), have_connect as ( select true as have_connect from have_global_connect where have_global_connect union select have_login_connect and have_session_connect from have_global_connect cross join have_login_connect cross join have_session_connect where not have_global_connect ) select scope_type_id, scope_id, roles, privileges from grouped_role_privs where exists (select null from have_connect where have_connect); comment on view veil2.session_privileges_v is 'View used to dynamically figure out the roles and privileges in all contexts for the current session. If the accessor for the session does not have connect privilege in both the authentication and login contexts, then no rows are returned. This view is used as the basis for loading session privileges into the veil2_session_privileges temporary table and into the veil2.accessor_privileges_cache table. Note that the cache table is used in preference to querying from this view if it has records for the session''s accessor and session context.'; revoke all on veil2.session_privileges_v from public; \echo ...creating materialized view refresh functions... \echo ......refresh_all_matviews()... create or replace 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; revoke all on function veil2.refresh_all_matviews() from public; comment on function veil2.refresh_all_matviews() is 'Clear all matviews and caches unconditionally.'; \echo ......refresh_scopes_matviews()... create or replace function veil2.refresh_scopes_matviews() returns trigger as $$ begin perform veil2.refresh_all_matviews(); return new; end; $$ language plpgsql security definer; revoke all on function veil2.refresh_scopes_matviews() from public; comment on function veil2.refresh_scopes_matviews() is 'Trigger function to refresh all materialized views and caches that depend on the scopes hierarchy.'; \echo ......refresh_privs_matviews()... create or replace 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; revoke all on function veil2.refresh_privs_matviews() from public; comment on function veil2.refresh_privs_matviews() is 'Trigger function to refresh all materialized views and caches that depend on privileges.'; \echo ......refresh_roles_matviews()... create or replace 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; revoke all on function veil2.refresh_roles_matviews() from public; comment on function veil2.refresh_roles_matviews() is 'Trigger function to refresh all materialized views and caches that depend on roles.'; \echo ......clear_accessor_privs_cache()... create or replace function veil2.clear_accessor_privs_cache() returns trigger as $$ begin truncate table veil2.accessor_privileges_cache; return new; end; $$ language plpgsql security definer volatile; revoke all on function veil2.clear_accessor_privs_cache() from public; comment on function veil2.clear_accessor_privs_cache() is 'Clear cached role and privileges information for all accessors.'; \echo ......clear_accessor_privs_cache_entry()... create or replace 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; revoke all on function veil2.clear_accessor_privs_cache_entry() from public; comment on function veil2.clear_accessor_privs_cache_entry() is 'Clear the cached role and privileges information for the affected accessor.'; \echo ...creating materialized view refresh triggers... \echo ......on scopes... create trigger scopes__aiudt after insert or update or delete or truncate on veil2.scopes for each statement execute procedure veil2.refresh_scopes_matviews(); comment on trigger scopes__aiudt on veil2.scopes is '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.'; \echo ......on privileges... create trigger privileges__aiudt after insert or update or delete or truncate on veil2.privileges for each statement execute procedure veil2.refresh_privs_matviews(); comment on trigger privileges__aiudt on veil2.privileges is 'Refresh materialized views that are constructed from the privileges table.'; \echo ......on roles... create trigger roles__aiudt after insert or update or delete or truncate on veil2.roles for each statement execute procedure veil2.refresh_roles_matviews(); comment on trigger roles__aiudt on veil2.roles is 'Refresh materialized views that are constructed from the roles table.'; \echo ......on role_roles... create trigger role_roles__aiudt after insert or update or delete or truncate on veil2.role_roles for each statement execute procedure veil2.refresh_roles_matviews(); comment on trigger role_roles__aiudt on veil2.role_roles is 'Refresh materialized views that are constructed from the role_roles table.'; \echo ......on role_privileges... create trigger role_privileges__aiudt after insert or update or delete or truncate on veil2.role_privileges for each statement execute procedure veil2.refresh_roles_matviews(); comment on trigger role_privileges__aiudt on veil2.role_privileges is 'Refresh materialized views that are constructed from the role_privileges table.'; \echo ......on accessor_roles... create trigger accessor_roles__at after truncate on veil2.accessor_roles for each statement execute procedure veil2.clear_accessor_privs_cache_entry(); comment on trigger accessor_roles__at on veil2.accessor_roles is 'Clear all cached accessor role and privilege data.'; create trigger accessor_roles__aiud after insert or update or delete on veil2.accessor_roles for each row execute procedure veil2.clear_accessor_privs_cache_entry(); comment on trigger accessor_roles__aiud on veil2.accessor_roles is 'Clear cached accessor role and privilege data for a given accessor.'; \echo ...creating veil2 user-provided object handling functions... \echo ......docpath()... create or replace function veil2.docpath() returns text as '$libdir/veil2', 'veil2_docpath' language C stable strict; revoke all on function veil2.docpath() from public; grant execute on function veil2.docpath() to veil_user; comment on function veil2.docpath() is 'Return the path to the directory under which Veil2 documents will be installed.'; \echo ......datapath()... create or replace function veil2.datapath() returns text as '$libdir/veil2', 'veil2_datapath' language C stable strict; revoke all on function veil2.datapath() from public; grant execute on function veil2.datapath() to veil_user; comment on function veil2.datapath() is 'Return the path to the directory under which Veil2 scripts will be installed.'; \echo ......function_definition()... create or replace 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; revoke execute on function veil2.function_definition(text, oid) from public; comment on function veil2.function_definition(text, oid) is '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()'; \echo ......replace_function()... create or replace 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; revoke all on function veil2.replace_function(text, oid) from public; comment on function veil2.replace_function(text, oid) is '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.'; \echo ......restore_system_functions()... create or replace 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; revoke all on function veil2.restore_system_functions() from public; comment on function veil2.restore_system_functions() is '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()'; \echo ......install_user_functions()... create or replace 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; revoke all on function veil2.install_user_functions() from public; comment on function veil2.install_user_functions() is '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.'; \echo ......function_exists()... create or replace 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; revoke all on function veil2.function_exists(text) from public; comment on function veil2.function_exists(text) is 'Predicate returning true if a function named fn_name exists in the veil2 schema.'; \echo ......view_exists()... create or replace 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; revoke all on function veil2.view_exists(text) from public; comment on function veil2.view_exists(text) is 'Predicate returning true if a view named vw_name exists in the veil2 schema.'; \echo ......replace_view()... create or replace 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; revoke all on function veil2.replace_view(text, oid) from public; comment on function veil2.replace_view(text, oid) is '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.'; \echo ......restore_system_views()... create or replace 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; revoke all on function veil2.restore_system_views() from public; comment on function veil2.restore_system_views() is '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()'; \echo ......install_user_views()... create or replace 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; revoke all on function veil2.install_user_views() from public; comment on function veil2.install_user_views() is '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.'; \echo ......init()... create or replace 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; revoke all on function veil2.init() from public; comment on function veil2.init() is '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.'; \echo ......deferred_install()... create or replace function veil2.deferred_install_fn() returns trigger as $$ begin perform veil2.init(); return new; end; $$ language plpgsql security definer volatile; revoke all on function veil2.deferred_install_fn() from public; comment on function veil2.deferred_install_fn() is '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.'; create trigger deferred_install_trg after insert on veil2.deferred_install for each statement execute function veil2.deferred_install_fn(); comment on trigger deferred_install_trg on veil2.deferred_install is '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.'; \echo ...creating veil2 authentication functions... \echo ......authenticate_false()... create or replace function veil2.authenticate_false( accessor_id integer, token text) returns boolean as $$ select false; $$ language sql security definer stable; revoke all on function veil2.authenticate_false(integer, text) from public; comment on function veil2.authenticate_false(integer, text) is 'Authentication predicate for unimplemented or disabled authentication types. This function always returns false, causing authentication to fail.'; \echo ......authenticate_plaintext()... create or replace 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; revoke all on function veil2.authenticate_plaintext(integer, text) from public; comment on function veil2.authenticate_plaintext(integer, text) is '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!'; \echo ......authenticate_bcrypt()... create or replace 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; revoke all on function veil2.authenticate_bcrypt(integer, text) from public; comment on function veil2.authenticate_bcrypt(integer, text) is '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.'; \echo ......authenticate()... create or replace 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; revoke all on function veil2.authenticate(integer, text, text) from public; comment on function veil2.authenticate(integer, text, text) is 'For the given accessor_id and authentication_type check whether token is an appropriate authentication.'; \echo ...creating veil2 session functions... \echo ...session_ready()... create or replace function veil2.session_ready() returns boolean as '$libdir/veil2', 'veil2_session_ready' language C stable strict; revoke all on function veil2.session_ready() from public; comment on function veil2.session_ready() is '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.'; \echo ......reset_session()... create or replace function veil2.reset_session() returns void as '$libdir/veil2', 'veil2_reset_session' language C volatile strict security definer; revoke all on function veil2.reset_session() from public; comment on function veil2.reset_session() is '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.'; \echo ......reset_session_privs()... create or replace function veil2.reset_session_privs() returns void as '$libdir/veil2', 'veil2_reset_session_privs' language C volatile strict security definer; revoke all on function veil2.reset_session_privs() from public; comment on function veil2.reset_session_privs() is 'Safely clear the contents of the veil2_session_privileges temporary table.'; \echo ......get_accessor()... create or replace 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; revoke all on function veil2.get_accessor(text, integer, integer) from public; comment on function veil2.get_accessor(text, integer, integer) is '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().'; \echo ......new_session_context()... create or replace 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; revoke all on function veil2.new_session_context( integer, integer, integer, integer, integer, integer) from public; comment on function veil2.new_session_context( integer, integer, integer, integer, integer, integer) is 'Create a veil2_session_context record for the given parameters, returning session_id and mapping context.'; \echo ......have_accessor_context()... create or replace 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; revoke all on function veil2.have_accessor_context( integer, integer, integer) from public; comment on function veil2.have_accessor_context( integer, integer, integer) is 'Predicate to determine whether an accessor has the right to use the given session context.'; \echo ......create_accessor_session()... create or replace 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'; revoke all on function veil2.create_accessor_session( integer, text, integer, integer, integer, integer) from public; comment on function veil2.create_accessor_session( integer, text, integer, integer, integer, integer) is '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().'; \echo ......create_session()... create or replace 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'; revoke all on function veil2.create_session( text, text, integer, integer, integer, integer) from public; grant execute on function veil2.create_session( text, text, integer, integer, integer, integer) to veil_user; comment on function veil2.create_session( text, text, integer, integer, integer, integer) is '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.'; \echo ......check_nonce()... create or replace 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; revoke all on function veil2.check_nonce(integer, bitmap) from public; comment on function veil2.check_nonce(integer, bitmap) is 'Check that nonce has not already been used and is within the range of acceptable values, returning true if all is well.'; \echo ......update_nonces()... create or replace 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; revoke all on function veil2.update_nonces(integer, bitmap) from public; comment on function veil2.update_nonces(integer, bitmap) is 'Add nonce to the list of used nonces, slimming the bitmap down when it gets too large.'; \echo ......filter_privs()... create or replace 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; revoke all on function veil2.filter_privs() from public; comment on function veil2.filter_privs() is '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.'; \echo ......load_ancestor_privs(parent_session_id)... create or replace 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; revoke all on function veil2.load_ancestor_privs(integer) from public; comment on function veil2.load_ancestor_privs(integer) is '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).'; \echo ......filter_session_privs(parent_session_id)... create or replace 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; revoke all on function veil2.filter_session_privs(integer) from public; comment on function veil2.filter_session_privs(integer) is 'Remove from veil2_session_privileges any roles and privileges not held by the ancestor session(s).'; \echo ......session_privileges()... create or replace 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; revoke all on function veil2.session_privileges() from public; grant execute on function veil2.session_privileges() to veil_user; comment on function veil2.session_privileges() is '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.'; \echo ......session_privileges_info (view)... create or replace view veil2.session_privileges_info as select * from veil2.session_privileges(); comment on view veil2.session_privileges_info is 'Provides a user-readable view of the veil2.session_privileges temporary table.'; grant select on veil2.session_privileges_info to veil_user; \echo ......load_session_privs()... create or replace 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; revoke all on function veil2.load_session_privs() from public; comment on function veil2.load_session_privs() is '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.'; \echo ......check_continuation()... create or replace 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; revoke all on function veil2.check_continuation(integer, text, text) from public; comment on function veil2.check_continuation(integer, text, text) is '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.'; \echo ......load_cached_privs()... create or replace 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; revoke all on function veil2.load_cached_privs() from public; comment on function veil2.load_cached_privs() is 'Reload cached session privileges for the session''s accessor into our current session.'; \echo ......update_session()... create or replace 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; revoke all on function veil2.update_session( integer, bitmap, boolean) from public; comment on function veil2.update_session(integer, bitmap, boolean) is '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.'; \echo ......load_connection_privs()... create or replace 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; revoke all on function veil2.load_connection_privs(integer) from public; comment on function veil2.load_connection_privs(integer) is 'Load veil_session_privileges temp table for a session, filtering the privileges if neccesary for a become_user() session.'; \echo ......reload_session_context()... create or replace 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; revoke all on function veil2.reload_session_context(integer) from public; comment on function veil2.reload_session_context(integer) is '(Re)load veil2_session_context for a given session_id.'; \echo ......reload_connection_privs()... create or replace 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; revoke all on function veil2.reload_connection_privs() from public; grant execute on function veil2.reload_connection_privs() to veil_user; comment on function veil2.reload_connection_privs() is 'Reload a session''s privileges. This would be done in the event that cached privileges need to be reloaded.'; \echo ......open_connection()... create or replace 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'; revoke all on function veil2.open_connection(integer, integer, text) from public; grant execute on function veil2.open_connection(integer, integer, text) to veil_user; comment on function veil2.open_connection(integer, integer, text) is '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.'; \echo ......close_connection()... create or replace 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; revoke all on function veil2.close_connection() from public; grant execute on function veil2.close_connection() to veil_user; comment on function veil2.close_connection() is '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.'; \echo ......hello()... create or replace 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; revoke all on function veil2.hello(integer, integer) from public; grant execute on function veil2.hello(integer, integer) to veil_user; comment on function veil2.hello(integer, integer) is 'This is used to begin a veil2 session for a database user, ie someone who can directly access the database.'; \echo ......check_become_user_priv()... create or replace 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; revoke all on function veil2.check_become_user_priv( text, integer, integer, integer) from public; comment on function veil2.check_become_user_priv( text, integer, integer, integer) is 'Determine whether the accessor has become_user privilege for the supplied context (including superior and global). Return ''NOPRIV'' to the caller if not.'; \echo ......check_accessor_context()... create or replace 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; revoke all on function veil2.check_accessor_context( text, integer, integer, integer) from public; comment on function veil2.check_accessor_context( text, integer, integer, integer) is 'Determine whether the geven accessor has rights to the given session context. Return ''AUTHFAIL'' if not.'; \echo ......become_accessor()... create or replace 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'; revoke all on function veil2.become_accessor( integer, integer, integer, integer, integer) from public; comment on function veil2.become_accessor( integer, integer, integer, integer, integer) is '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.'; \echo ......become_user()... create or replace 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; revoke all on function veil2.become_user( text, integer, integer, integer, integer) from public; grant execute on function veil2.become_user( text, integer, integer, integer, integer) to veil_user; comment on function veil2.become_user( text, integer, integer, integer, integer) is 'See comments for become_accessor(). This is the same but takes a username rather than accessor_id.'; \echo ...creating veil2 privilege testing functions... -- Ensure the veil2_session_context and session _privileges temp tables -- exist as they are needed in order to compile the following functions. select veil2.reset_session(); \echo ......true()... create or replace function veil2.always_true(integer) returns boolean as '$libdir/veil2', 'veil2_true' language C security definer stable leakproof; comment on function veil2.always_true(integer) is 'Performance testing function - always returns true. Used to establish the minimum overhead of security policies for tables.'; \echo ......i_have_global_priv()... create or replace function veil2.i_have_global_priv(integer) returns boolean as '$libdir/veil2', 'veil2_i_have_global_priv' language C security definer stable leakproof; comment on function veil2.i_have_global_priv(integer) is 'Predicate to determine whether the connected user has the given privilege in the global scope.'; \echo ......i_have_personal_priv()... create or replace function veil2.i_have_personal_priv(integer, integer) returns boolean as '$libdir/veil2', 'veil2_i_have_personal_priv' language C security definer stable leakproof; comment on function veil2.i_have_personal_priv(integer, integer) is 'Predicate to determine whether the connected user has the given privilege in the personal scope.'; \echo ......i_have_priv_in_scope()... create or replace 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; comment on function veil2.i_have_priv_in_scope(integer, integer, integer) is 'Predicate to determine whether the connected user has the given privilege in the given scope.'; \echo ......i_have_priv_in_scope_or_global()... create or replace 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; comment on function veil2.i_have_priv_in_scope(integer, integer, integer) is 'Predicate to determine whether the connected user has the given privilege in the given scope, or in global scope.'; \echo ......i_have_priv_in_superior_scope()... create or replace 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; comment on function veil2.i_have_priv_in_superior_scope( integer, integer, integer) is '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.'; \echo ......i_have_priv_in_scope_or_superior()... create or replace 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; comment on function veil2.i_have_priv_in_scope_or_superior( integer, integer, integer) is '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.'; \echo ......i_have_priv_in_scope_or_superior_or_global()... create or replace 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; comment on function veil2.i_have_priv_in_scope_or_superior_or_global( integer, integer, integer) is '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.'; \echo ......result_counts()... create or replace 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; comment on function veil2.result_counts() is 'Return record of how many false and how many true results have been returned by the i_have_privi_xxx() functions in this session'; revoke all on function veil2.result_counts() from public; \echo ...creating veil2 admin and helper functions... \echo ......delete_expired_sessions()... create or replace function veil2.delete_expired_sessions() returns void as $$ delete from veil2.sessions s where expires <= now(); $$ language 'sql' security definer volatile; comment on function veil2.delete_expired_sessions() is 'Utility function to clean-up session data. This should be run periodically from a batch job.'; \echo ......bcrypt()... create or replace function veil2.bcrypt(passwd text) returns text as $$ select crypt(passwd, gen_salt('bf')); $$ language 'sql' security definer volatile; comment on function veil2.bcrypt(text) is '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.'; -- Create base meta-data for veil2 schema insert into veil2.scope_types (scope_type_id, scope_type_name, description) values (1, 'global scope', 'Assignments made in the global context apply globally (in ' || 'global scope): that is there are no limitions based on data ' || 'ownership applied to these assignments'), (2, 'personal scope', 'Privileges assigned in personal context apply to the personal ' || 'data of the user. If they have the ''select_parties'' ' || 'privilege assigned only in personal context, they will be ' || 'able to see only their own party record. All parties are ' || 'expected to have the same rights to their own data, so we ' || 'do not explicitly assign rights in personal context, instead ' || 'we assume that the ''personal_context'' role has been ' || 'assigned to every party. This role is special in that it ' || 'should not be assigned in any other context, and so ' || 'is defined as not enabled.'); insert into veil2.scopes (scope_type_id, scope_id) values (1, 0); insert into veil2.authentication_types (shortname, enabled, description, authent_fn) values ('bcrypt', true, 'Bcrypted password from the user.', 'veil2.authenticate_bcrypt'), ('plaintext', false, 'Plaintext password - enable for development only', 'veil2.authenticate_plaintext'), ('dedicated', false, 'Dedicated Session. Authentication by database session username', 'veil2.authenticate_false'), ('become', false, 'From become_user. Session supplemental identifies the originating session', 'veil2.authenticate_false'), ('oath2', false, -- Placeholder. An exercise for the reader 'Openid authentication.', 'veil2.authenticate_false'); insert into veil2.privileges (privilege_id, privilege_name, promotion_scope_type_id, description) values (0, 'connect', null, 'May connect to the database to execute queries.'), (1, 'become user', null, 'May execute the become_user function. This should only ' || 'be available to superusers'), (2, 'select scope_types', 1, 'May select from scope_types.'), (3, 'select scopes', null, 'May select from scopes.'), (4, 'select privileges', 1, 'May select from privileges.'), (5, 'select role_types', 1, 'May select from role_types.'), (6, 'select roles', 1, 'May select from roles.'), (7, 'select context_roles', null, 'May select from context_roles.'), (8, 'select role_privileges', 1, 'May select from role_privileges.'), (9, 'select role_roles', null, 'May select from role_roles.'), (10, 'select accessors', null, 'May select from accessors.'), (11, 'select authentication_types', 1, 'May select from authentication_types.'), (12, 'select authentication_details', null, 'May select from authentication_details.'), (13, 'select accessor_roles', null, 'May select from accessor_roles.'), (14, 'select sessions', null, 'May select from sessions.'), (15, 'select system_parameters', null, 'May select from system_parameters.'); insert into veil2.role_types (role_type_id, role_type_name, description) values (1, 'default', 'A general-purpose, unspecific role type'), (2, 'veil2', 'A Veil2-specific role type, used for access to veil2 data'); insert into veil2.roles (role_id, role_name, implicit, immutable, description) values (0, 'connect', false, true, 'Allow minimal access to the system.'), (1, 'superuser', false, true, 'An all-encompassing role.'), (2, 'personal context', true, true, 'An implicitly assigned, to all users, role that allows ' || 'access to a user''s own information'); -- Veil-specific roles insert into veil2.roles (role_id, role_type_id, role_name, implicit, immutable, description) values (3, 2, 'veil2_viewer', false, true, 'Allow read-access to veil data'); -- Set up basic access rights. insert into veil2.role_privileges (role_id, privilege_id) values (0, 0), (2, 10) -- personal_scope gives select to accessors table ; -- Set up veil2_viewer rights insert into veil2.role_privileges (role_id, privilege_id) values (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (3, 10), (3, 11), (3, 12), (3, 13), (3, 14), (3, 15); -- system parameters insert into veil2.system_parameters (parameter_name, parameter_value) values ('shared session timeout', '20 mins'), ('mapping context target scope type', '1'), ('error on uninitialized session', true); -- Create security for vpd tables. -- This consists of enabling row-level security and only allowing -- select access to users with the approrpiate veil privileges. \echo ......save_session_privs()... select veil2.reset_session(); create or replace 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; comment on function veil2.save_session_privs() is '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.'; \echo ......scope_types... alter table veil2.scope_types enable row level security; -- Prevent modifications to scope_types - the database owner -- should be the only user doing this. create policy scope_type__select on veil2.scope_types for select using (veil2.i_have_global_priv(2)); \echo ......scopes... alter table veil2.scopes enable row level security; create policy scope__select on veil2.scopes for select using ( veil2.i_have_global_priv(3) or veil2.i_have_priv_in_scope(3, scope_type_id, scope_id)); comment on policy scope__select on veil2.scopes is 'Require privilege ''select scopes'' in global scope (assigned in global scope), in order to see the data in this table.'; \echo ......privileges... alter table veil2.privileges enable row level security; create policy privilege__select on veil2.privileges for select using (veil2.i_have_global_priv(4)); comment on policy privilege__select on veil2.privileges is 'Require privilege ''select privilege'' in global scope (assigned in global scope), in order to see the data in this table.'; \echo ......role_types... alter table veil2.role_types enable row level security; create policy role_type__select on veil2.role_types for select using (veil2.i_have_global_priv(5)); comment on policy role_type__select on veil2.role_types is 'Require privilege ''select role_type'' in global scope (assigned in global scope), in order to see the data in this table.'; \echo ......roles... alter table veil2.roles enable row level security; create policy role__select on veil2.roles for select using (veil2.i_have_global_priv(6)); comment on policy role__select on veil2.roles is 'Require privilege ''select roles'' in global scope (assigned in global scope), in order to see the data in this table.'; \echo ......context_roles... alter table veil2.context_roles enable row level security; -- We should be able to view this if we have select_context_role -- privilege in a suitable scope. create policy context_role__select on veil2.context_roles for select using ( veil2.i_have_global_priv(7) or veil2.i_have_priv_in_scope(7, context_type_id, context_id) or veil2.i_have_priv_in_superior_scope(7, context_type_id, context_id) ); comment on policy context_role__select on veil2.context_roles is 'Require privilege ''select context_roles'' in an appropriate scope in order to see the data in this table.'; \echo ......role_privileges... alter table veil2.role_privileges enable row level security; -- We should be able to view this if we have select_role_privilege -- privilege in a suitable scope. create policy role_privilege__select on veil2.role_privileges for select using (veil2.i_have_global_priv(8)); comment on policy role_privilege__select on veil2.role_privileges is 'Require privilege ''select role_privileges'' in global scope (assigned in global scope), in order to see the data in this table.'; \echo ......role_roles... alter table veil2.role_roles enable row level security; -- We should be able to view this if we have select_role_role -- privilege in a suitable scope. create policy role_role__select on veil2.role_roles using ( veil2.i_have_global_priv(9) or veil2.i_have_priv_in_scope(9, context_type_id, context_id) or veil2.i_have_priv_in_superior_scope(9, context_type_id, context_id) ); comment on policy role_role__select on veil2.role_roles is 'Require privilege ''select role_roles'' in an appropriate scope in order to see the data in this table.'; \echo ......accessors... alter table veil2.accessors enable row level security; -- We should be able to view this if we have select_accessor -- privilege in a suitable scope. create policy accessor__select on veil2.accessors for select using ( veil2.i_have_global_priv(10) or veil2.i_have_personal_priv(10, accessor_id)); comment on policy accessor__select on veil2.accessors is 'Require privilege ''select accessors'' in global scope (assigned in global scope) or personal scope, in order to see the data in this table.'; \echo ......authentication_types... alter table veil2.authentication_types enable row level security; -- We should be able to view this if we have select_authentication_type -- privilege in a suitable scope. create policy authentication_type__select on veil2.authentication_types for select using (veil2.i_have_global_priv(11)); comment on policy authentication_type__select on veil2.authentication_types is 'Require privilege ''select authentication_types'' in global scope (assigned in global scope) in order to see the data in this table.'; \echo ......authentication_details... alter table veil2.authentication_details enable row level security; -- We should be able to view this if we have select_authentication_detail -- privilege in a suitable scope. create policy authentication_detail__select on veil2.authentication_details for select using (veil2.i_have_global_priv(12)); comment on policy authentication_detail__select on veil2.authentication_details is 'Require privilege ''select authentication_details'' in global scope (assigned in global scope) in order to see the data in this table.'; \echo ......accessor_roles... alter table veil2.accessor_roles enable row level security; -- We should be able to view this if we have select_accessor_role -- privilege in a suitable scope. create policy accessor_role__select on veil2.accessor_roles for select using (veil2.i_have_global_priv(13) or veil2.i_have_priv_in_scope(13, context_type_id, context_id)); comment on policy accessor_role__select on veil2.accessor_roles is 'Require privilege ''select accessor_roles'' in global scope (assigned in global scope) in order to see the data in this table.'; \echo ......sessions... alter table veil2.sessions enable row level security; -- We should be able to view this if we have select_session -- privilege in a suitable scope. create policy session__select on veil2.sessions for select using (veil2.i_have_global_priv(14)); comment on policy session__select on veil2.sessions is 'Require privilege ''select sessions'' in global scope (assigned in global scope) or personal scope, in order to see the data in this table.'; \echo ......system_parameters... alter table veil2.system_parameters enable row level security; -- We should be able to view this if we have select_system_parameter -- privilege in a suitable scope. create policy system_parameter__select on veil2.system_parameters for select using (veil2.i_have_global_priv(15)); comment on policy system_parameter__select on veil2.system_parameters is 'Require privilege ''select system_parameters'' in global scope (assigned in global scope) in order to see the data in this table.'; \echo ......accessor_privileges_cache... alter table veil2.accessor_privileges_cache enable row level security; create policy accessor_privileges_cache__all on veil2.accessor_privileges_cache; comment on policy accessor_privileges_cache__all on veil2.accessor_privileges_cache is 'No access to this table should be given to normal users.'; revoke all on veil2.accessor_privileges_cache from public; \echo ......deferred_install... alter table veil2.deferred_install enable row level security; create policy deferred_install__all on veil2.deferred_install; comment on policy deferred_install__all on veil2.deferred_install is 'No access to this table should be given to normal users'; revoke all on veil2.deferred_install from public; -- Deal with tables that implementors and administrators are expected -- to update. \echo ...handling for user-defined data in pg_dump... \echo ......scope_types... select pg_catalog.pg_extension_config_dump( 'veil2.scope_types', 'where not scope_type_id in (1,2)'); \echo ......system_parameters... create or replace 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; comment on function veil2.system_parameters_check() is '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.'; create trigger system_parameters_biu before insert or update on veil2.system_parameters for each row execute function veil2.system_parameters_check(); select pg_catalog.pg_extension_config_dump( 'veil2.system_parameters', 'where user_defined'); \echo ......authentication_types... create or replace 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; create trigger authentication_types_biu before insert or update on veil2.authentication_types for each row execute function veil2.make_user_defined(); select pg_catalog.pg_extension_config_dump( 'veil2.authentication_types', 'where user_defined'); \echo ......privileges... select pg_catalog.pg_extension_config_dump( 'veil2.privileges', 'where privilege_id >= 20 or privilege_id < 0'); \echo ......roles... select pg_catalog.pg_extension_config_dump( 'veil2.roles', 'where role_id > 4 or role_id < 0'); select pg_catalog.pg_extension_config_dump( 'veil2.role_types', 'where role_type_id not in (1, 2)'); \echo ......role_privileges... select pg_catalog.pg_extension_config_dump( 'veil2.role_privileges', 'where role_id > 4 or role_id < 0'); \echo ......role_roles... select pg_catalog.pg_extension_config_dump( 'veil2.role_roles', ''); \echo ......scopes... select pg_catalog.pg_extension_config_dump( 'veil2.scopes', 'where scope_type_id != 1 or scope_id != 0'); \echo ......accessors... select pg_catalog.pg_extension_config_dump( 'veil2.accessors', ''); \echo ......authentication_details... select pg_catalog.pg_extension_config_dump( 'veil2.authentication_details', ''); -- Functions for checking implementation status. These are to help -- security model implementors. \echo ...Functions for checking implementation status... \echo ......have_user_scope_types()... create or replace 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; comment on function veil2.have_user_scope_types() is 'Predicate used to determine whether user-defined scope_types have been added to the implementation.'; \echo ......have_user_user_privileges()... create or replace 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; comment on function veil2.have_user_privileges() is 'Predicate used to determine whether any user-defined privileges have been created.'; \echo ......have_user_user_roles()... create or replace 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; comment on function veil2.have_user_roles() is 'Predicate used to determine whether any user-defined roles have been created.'; \echo ......have_role_privileges()... create or replace 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; comment on function veil2.have_role_privileges() is 'Predicate used to determine whether any user-defined role_privileges have been created.'; \echo ......have_role_roles()... create or replace function veil2.have_role_roles() returns boolean as $$ select exists ( select null from veil2.role_roles); $$ language sql security definer volatile; comment on function veil2.have_role_roles() is 'Predicate used to determine whether any user-defined role_roles (role to role mappings) have been created.'; \echo ......have_accessors()... create or replace function veil2.have_accessors() returns boolean as $$ select exists ( select null from veil2.accessors); $$ language sql security definer volatile; comment on function veil2.have_accessors() is 'Predicate used to determine whether any accessors have been defined.'; \echo ......have_user_scopes()... create or replace 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; comment on function veil2.have_user_scopes() is 'Predicate used to determine whether any user-defined scopes have been created.'; \echo ......check_table_security()... create or replace 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; comment on function veil2.check_table_security() is 'Predicate used to determine whether all user-defined tables have security policies in place.'; \echo ......implementation_status()... create or replace 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; comment on function veil2.implementation_status() is '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.'; create or replace view veil2.docs(file, purpose) as values (veil2.docpath() || '/html/index.html', 'Complete html documentation for Veil2'); comment on view veil2.docs is 'Show where local Veil2 documentation can be found.'; create or replace view veil2.sql_files(file, purpose) as values (veil2.datapath() || '/demo.sql', 'Install demo and run test'), (veil2.datapath() || '/demo_test.sql', 'Run simple tests against demo'), (veil2.datapath() || '/demo_bulk_data.sql', 'Install some bulk role and priv data'), (veil2.datapath() || '/perf.sql', 'Run session management performance check'), (veil2.datapath() || '/veil2_demo--0.9.1.sql', 'Veil2 demo creation script'), (veil2.datapath() || '/veil2_minimal_demo.sql', 'Veil2 minimal-demo creation script'), (veil2.datapath() || '/veil2_template.sql', 'Veil2 implementation template.'), (veil2.datapath() || '/veil2--0.9.1.sql', 'Veil2 extension creation script.'); comment on view veil2.sql_files is 'Show where copies of useful Veil2 sql files can be found.';