add-customer and introducing JpaAttempt test helper
This commit is contained in:
parent
f58a68d1cc
commit
03ee2cfd62
18
README.md
18
README.md
@ -227,3 +227,21 @@ You can explore the prototype as follows:
|
|||||||
(the example tables are currently not compatible with RBAC),
|
(the example tables are currently not compatible with RBAC),
|
||||||
- then run `historization.sql` in the database,
|
- then run `historization.sql` in the database,
|
||||||
- finally run `examples.sql` in the database.
|
- finally run `examples.sql` in the database.
|
||||||
|
|
||||||
|
## How To
|
||||||
|
|
||||||
|
### How to Use a Persistent Database for Integration Tests?
|
||||||
|
|
||||||
|
Usually, the `DataJpaTest` integration tests run against a database in a temporary docker container.
|
||||||
|
As soon as the test ends, the database is gone; this might make debugging difficult.
|
||||||
|
|
||||||
|
Alternatively
|
||||||
|
|
||||||
|
If the persistent database and the temporary database show different results, one of these reasons could be the cause:
|
||||||
|
|
||||||
|
1. You might have some changesets only running in either context,
|
||||||
|
check the `context: ...` in the changeset control lines.
|
||||||
|
2. You might have changes in the database which interfere with the tests,
|
||||||
|
e.g. from a previous run of tests or manually applied.
|
||||||
|
It's best to run `pg-sql-reset && gw bootRun` before each test run, to have a clean database.
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
--liquibase formatted sql
|
--liquibase formatted sql
|
||||||
|
|
||||||
--changeset rbac-base-reference:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-base-REFERENCE:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -27,7 +29,9 @@ end; $$;
|
|||||||
|
|
||||||
--//
|
--//
|
||||||
|
|
||||||
--changeset rbac-base-user:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-base-USER:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -86,7 +90,9 @@ $$;
|
|||||||
|
|
||||||
--//
|
--//
|
||||||
|
|
||||||
--changeset rbac-base-object:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-base-OBJECT:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -119,7 +125,9 @@ end; $$;
|
|||||||
|
|
||||||
--//
|
--//
|
||||||
|
|
||||||
--changeset rbac-base-role:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-base-ROLE:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -203,7 +211,9 @@ begin
|
|||||||
end;
|
end;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
--changeset rbac-base-permission:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-base-PERMISSION:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -217,7 +227,6 @@ create domain RbacOp as varchar(67)
|
|||||||
or VALUE ~ '^add-[a-z]+$'
|
or VALUE ~ '^add-[a-z]+$'
|
||||||
);
|
);
|
||||||
|
|
||||||
-- DROP TABLE IF EXISTS RbacPermission;
|
|
||||||
create table RbacPermission
|
create table RbacPermission
|
||||||
(
|
(
|
||||||
uuid uuid primary key references RbacReference (uuid) on delete cascade,
|
uuid uuid primary key references RbacReference (uuid) on delete cascade,
|
||||||
@ -226,11 +235,7 @@ create table RbacPermission
|
|||||||
unique (objectUuid, op)
|
unique (objectUuid, op)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- SET SESSION SESSION AUTHORIZATION DEFAULT;
|
create or replace function permissionExists(forObjectUuid uuid, forOp RbacOp)
|
||||||
-- alter table rbacpermission add constraint rbacpermission_objectuuid_fkey foreign key (objectUuid) references rbacobject(uuid);
|
|
||||||
-- alter table rbacpermission drop constraint rbacpermission_objectuuid;
|
|
||||||
|
|
||||||
create or replace function hasPermission(forObjectUuid uuid, forOp RbacOp)
|
|
||||||
returns bool
|
returns bool
|
||||||
language sql as $$
|
language sql as $$
|
||||||
select exists(
|
select exists(
|
||||||
@ -291,7 +296,9 @@ $$;
|
|||||||
|
|
||||||
--//
|
--//
|
||||||
|
|
||||||
--changeset rbac-base-grants:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-base-GRANTS:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -306,8 +313,6 @@ create index on RbacGrants (ascendantUuid);
|
|||||||
create index on RbacGrants (descendantUuid);
|
create index on RbacGrants (descendantUuid);
|
||||||
|
|
||||||
|
|
||||||
--//
|
|
||||||
|
|
||||||
create or replace function findGrantees(grantedId uuid)
|
create or replace function findGrantees(grantedId uuid)
|
||||||
returns setof RbacReference
|
returns setof RbacReference
|
||||||
returns null on null input
|
returns null on null input
|
||||||
@ -377,7 +382,8 @@ begin
|
|||||||
|
|
||||||
insert
|
insert
|
||||||
into RbacGrants (ascendantUuid, descendantUuid, follow)
|
into RbacGrants (ascendantUuid, descendantUuid, follow)
|
||||||
values (roleUuid, permissionIds[i], true);
|
values (roleUuid, permissionIds[i], true)
|
||||||
|
on conflict do nothing; -- allow granting multiple times
|
||||||
end loop;
|
end loop;
|
||||||
end;
|
end;
|
||||||
$$;
|
$$;
|
||||||
@ -395,7 +401,7 @@ begin
|
|||||||
insert
|
insert
|
||||||
into RbacGrants (ascendantUuid, descendantUuid, follow)
|
into RbacGrants (ascendantUuid, descendantUuid, follow)
|
||||||
values (superRoleId, subRoleId, doFollow)
|
values (superRoleId, subRoleId, doFollow)
|
||||||
on conflict do nothing; -- TODO: remove?
|
on conflict do nothing; -- allow granting multiple times
|
||||||
end; $$;
|
end; $$;
|
||||||
|
|
||||||
create or replace procedure revokeRoleFromRole(subRoleId uuid, superRoleId uuid)
|
create or replace procedure revokeRoleFromRole(subRoleId uuid, superRoleId uuid)
|
||||||
@ -418,11 +424,13 @@ begin
|
|||||||
insert
|
insert
|
||||||
into RbacGrants (ascendantUuid, descendantUuid, follow)
|
into RbacGrants (ascendantUuid, descendantUuid, follow)
|
||||||
values (userId, roleId, true)
|
values (userId, roleId, true)
|
||||||
on conflict do nothing; -- TODO: remove?
|
on conflict do nothing; -- allow granting multiple times
|
||||||
end; $$;
|
end; $$;
|
||||||
--//
|
--//
|
||||||
|
|
||||||
--changeset rbac-base-query-accessible-object-uuids:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -467,7 +475,9 @@ $$;
|
|||||||
|
|
||||||
--//
|
--//
|
||||||
|
|
||||||
--changeset rbac-base-query-granted-permissions:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-base-QUERY-GRANTED-PERMISSIONS:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -494,7 +504,9 @@ $$;
|
|||||||
|
|
||||||
--//
|
--//
|
||||||
|
|
||||||
--changeset rbac-base-query-users-with-permission-for-object:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-base-QUERY-USERS-WITH-PERMISSION-FOR-OBJECT:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -520,7 +532,9 @@ $$;
|
|||||||
|
|
||||||
--//
|
--//
|
||||||
|
|
||||||
--changeset rbac-current-user:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-CURRENT-USER:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -553,13 +567,16 @@ declare
|
|||||||
begin
|
begin
|
||||||
currentUser := currentUser();
|
currentUser := currentUser();
|
||||||
currentUserId = (select uuid from RbacUser where name = currentUser);
|
currentUserId = (select uuid from RbacUser where name = currentUser);
|
||||||
|
if currentUserId is null then
|
||||||
|
raise exception 'hsadminng.currentUser defined as %, but does not exists', currentUser;
|
||||||
|
end if;
|
||||||
return currentUserId;
|
return currentUserId;
|
||||||
end; $$;
|
end; $$;
|
||||||
|
|
||||||
|
|
||||||
--//
|
--//
|
||||||
|
|
||||||
--changeset rbac-assumed-roles:1 endDelimiter:--//
|
-- ============================================================================
|
||||||
|
--changeset rbac-ASSUMED-ROLES:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -595,7 +612,7 @@ create or replace function findUuidByIdName(objectTable varchar, objectIdName va
|
|||||||
returns null on null input
|
returns null on null input
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
sql varchar;
|
sql varchar;
|
||||||
uuid uuid;
|
uuid uuid;
|
||||||
begin
|
begin
|
||||||
objectTable := pureIdentifier(objectTable);
|
objectTable := pureIdentifier(objectTable);
|
||||||
@ -604,10 +621,26 @@ begin
|
|||||||
begin
|
begin
|
||||||
raise notice 'sql: %', sql;
|
raise notice 'sql: %', sql;
|
||||||
execute sql into uuid;
|
execute sql into uuid;
|
||||||
exception when OTHERS then
|
exception
|
||||||
raise exception 'function %UuidByIdName(...) not found, add identity view support for table %', objectTable, objectTable;
|
when others then
|
||||||
|
raise exception 'function %UuidByIdName(...) not found, add identity view support for table %', objectTable, objectTable;
|
||||||
end;
|
end;
|
||||||
return uuid;
|
return uuid;
|
||||||
|
end ; $$;
|
||||||
|
|
||||||
|
create or replace function currentSubjects()
|
||||||
|
returns varchar(63)[]
|
||||||
|
stable leakproof
|
||||||
|
language plpgsql as $$
|
||||||
|
declare
|
||||||
|
assumedRoles varchar(63)[];
|
||||||
|
begin
|
||||||
|
assumedRoles := assumedRoles();
|
||||||
|
if array_length(assumedRoles(), 1) > 0 then
|
||||||
|
return assumedRoles();
|
||||||
|
else
|
||||||
|
return array[currentUser()]::varchar(63)[];
|
||||||
|
end if;
|
||||||
end; $$;
|
end; $$;
|
||||||
|
|
||||||
create or replace function currentSubjectIds()
|
create or replace function currentSubjectIds()
|
||||||
@ -664,9 +697,8 @@ end; $$;
|
|||||||
|
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- PGSQL-ROLES
|
--changeset rbac-base-PGSQL-ROLES:1 endDelimiter:--//
|
||||||
--changeset rbac-base-pgsql-roles:1 endDelimiter:--//
|
-- ----------------------------------------------------------------------------
|
||||||
-- ------------------------------------------------------------------
|
|
||||||
|
|
||||||
create role admin;
|
create role admin;
|
||||||
grant all privileges on all tables in schema public to admin;
|
grant all privileges on all tables in schema public to admin;
|
||||||
@ -675,3 +707,4 @@ create role restricted;
|
|||||||
grant all privileges on all tables in schema public to restricted;
|
grant all privileges on all tables in schema public to restricted;
|
||||||
|
|
||||||
--//
|
--//
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ create table Global
|
|||||||
);
|
);
|
||||||
create unique index Global_Singleton on Global ((0));
|
create unique index Global_Singleton on Global ((0));
|
||||||
|
|
||||||
|
grant select on global to restricted;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
A single row to be referenced as a global object.
|
A single row to be referenced as a global object.
|
||||||
*/
|
*/
|
||||||
@ -25,6 +27,23 @@ insert
|
|||||||
into Global (uuid, name) values ((select uuid from RbacObject where objectTable = 'global'), 'hostsharing');
|
into Global (uuid, name) values ((select uuid from RbacObject where objectTable = 'global'), 'hostsharing');
|
||||||
--//
|
--//
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
--changeset rhs-base-HAS-GLOBAL-PERMISSION:1 endDelimiter:--//
|
||||||
|
-- ------------------------------------------------------------------
|
||||||
|
|
||||||
|
create or replace function hasGlobalPermission(op RbacOp)
|
||||||
|
returns boolean
|
||||||
|
language sql as
|
||||||
|
$$
|
||||||
|
-- TODO: this could to be optimized
|
||||||
|
select (select uuid from global) in
|
||||||
|
(select queryAccessibleObjectUuidsOfSubjectIds(
|
||||||
|
op, 'global', currentSubjectIds()));
|
||||||
|
$$;
|
||||||
|
--//
|
||||||
|
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset hs-base-GLOBAL-IDENTITY-VIEW:1 endDelimiter:--//
|
--changeset hs-base-GLOBAL-IDENTITY-VIEW:1 endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
@ -34,7 +53,7 @@ insert
|
|||||||
*/
|
*/
|
||||||
drop view if exists global_iv;
|
drop view if exists global_iv;
|
||||||
create or replace view global_iv as
|
create or replace view global_iv as
|
||||||
select distinct target.uuid, target.name as idName
|
select target.uuid, target.name as idName
|
||||||
from global as target;
|
from global as target;
|
||||||
grant all privileges on global_iv to restricted;
|
grant all privileges on global_iv to restricted;
|
||||||
|
|
||||||
@ -65,7 +84,7 @@ $$;
|
|||||||
select createRole(hostsharingAdmin());
|
select createRole(hostsharingAdmin());
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset hs-base-ADMIN-USERS:1 context:dev,test,tc endDelimiter:--//
|
--changeset hs-base-ADMIN-USERS:1 context:dev,tc endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
Create two users and assign both to the administrators role.
|
Create two users and assign both to the administrators role.
|
||||||
@ -83,7 +102,7 @@ $$;
|
|||||||
|
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset hs-base-hostsharing-TEST:1 context:dev,test,tc runAlways:true endDelimiter:--//
|
--changeset hs-base-hostsharing-TEST:1 context:dev,tc runAlways:true endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -149,7 +149,7 @@ execute procedure deleteRbacRulesForCustomer();
|
|||||||
*/
|
*/
|
||||||
drop view if exists customer_iv;
|
drop view if exists customer_iv;
|
||||||
create or replace view customer_iv as
|
create or replace view customer_iv as
|
||||||
select distinct target.uuid, target.prefix as idName
|
select target.uuid, target.prefix as idName
|
||||||
from customer as target;
|
from customer as target;
|
||||||
-- TODO: Is it ok that everybody has access to this information?
|
-- TODO: Is it ok that everybody has access to this information?
|
||||||
grant all privileges on customer_iv to restricted;
|
grant all privileges on customer_iv to restricted;
|
||||||
@ -176,8 +176,51 @@ $$;
|
|||||||
set session session authorization default;
|
set session session authorization default;
|
||||||
drop view if exists customer_rv;
|
drop view if exists customer_rv;
|
||||||
create or replace view customer_rv as
|
create or replace view customer_rv as
|
||||||
select distinct target.*
|
select target.*
|
||||||
from customer as target
|
from customer as target
|
||||||
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'customer', currentSubjectIds()));
|
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'customer', currentSubjectIds()));
|
||||||
grant all privileges on customer_rv to restricted;
|
grant all privileges on customer_rv to restricted;
|
||||||
--//
|
--//
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
--changeset hs-customer-rbac-ADD-CUSTOMER:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
Creates a global permission for add-customer and assigns it to the hostsharing admins role.
|
||||||
|
*/
|
||||||
|
do language plpgsql $$
|
||||||
|
declare
|
||||||
|
addCustomerPermissions uuid[];
|
||||||
|
hostsharingObjectUuid uuid;
|
||||||
|
hsAdminRoleUuid uuid ;
|
||||||
|
begin
|
||||||
|
hsAdminRoleUuid := findRoleId(hostsharingAdmin());
|
||||||
|
hostsharingObjectUuid := (select uuid from global);
|
||||||
|
addCustomerPermissions := createPermissions(hostsharingObjectUuid, array ['add-customer']);
|
||||||
|
call grantPermissionsToRole(hsAdminRoleUuid, addCustomerPermissions);
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Used by the trigger to prevent the add-customer to current user respectively assumed roles.
|
||||||
|
*/
|
||||||
|
create or replace function addCustomerNotAllowedForCurrentSubjects()
|
||||||
|
returns trigger
|
||||||
|
language PLPGSQL
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
raise exception 'add-customer not permitted for %', array_to_string(currentSubjects());
|
||||||
|
end; $$;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Checks if the user or assumed roles are allowed to add a new customer.
|
||||||
|
*/
|
||||||
|
create trigger customer_insert_trigger
|
||||||
|
before insert
|
||||||
|
on customer
|
||||||
|
for each row
|
||||||
|
when ( currentUser() <> 'mike@hostsharing.net' or not hasGlobalPermission('add-customer') )
|
||||||
|
execute procedure addCustomerNotAllowedForCurrentSubjects();
|
||||||
|
--//
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ $$;
|
|||||||
*/
|
*/
|
||||||
drop view if exists package_rv;
|
drop view if exists package_rv;
|
||||||
create or replace view package_rv as
|
create or replace view package_rv as
|
||||||
select distinct target.*
|
select target.*
|
||||||
from package as target
|
from package as target
|
||||||
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'package', currentSubjectIds()));
|
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'package', currentSubjectIds()));
|
||||||
grant all privileges on package_rv to restricted;
|
grant all privileges on package_rv to restricted;
|
||||||
|
@ -115,7 +115,7 @@ set session session authorization default;
|
|||||||
-- ALTER TABLE unixuser ENABLE ROW LEVEL SECURITY;
|
-- ALTER TABLE unixuser ENABLE ROW LEVEL SECURITY;
|
||||||
drop view if exists unixuser_rv;
|
drop view if exists unixuser_rv;
|
||||||
create or replace view unixuser_rv as
|
create or replace view unixuser_rv as
|
||||||
select distinct target.*
|
select target.*
|
||||||
from unixuser as target
|
from unixuser as target
|
||||||
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'unixuser', currentSubjectIds()));
|
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'unixuser', currentSubjectIds()));
|
||||||
grant all privileges on unixuser_rv to restricted;
|
grant all privileges on unixuser_rv to restricted;
|
||||||
|
@ -100,7 +100,7 @@ set session session authorization default;
|
|||||||
-- ALTER TABLE Domain ENABLE ROW LEVEL SECURITY;
|
-- ALTER TABLE Domain ENABLE ROW LEVEL SECURITY;
|
||||||
drop view if exists domain_rv;
|
drop view if exists domain_rv;
|
||||||
create or replace view domain_rv as
|
create or replace view domain_rv as
|
||||||
select distinct target.*
|
select target.*
|
||||||
from Domain as target
|
from Domain as target
|
||||||
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'domain', currentSubjectIds()));
|
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'domain', currentSubjectIds()));
|
||||||
grant all privileges on domain_rv to restricted;
|
grant all privileges on domain_rv to restricted;
|
||||||
|
@ -85,7 +85,7 @@ set session session authorization default;
|
|||||||
-- ALTER TABLE EMailAddress ENABLE ROW LEVEL SECURITY;
|
-- ALTER TABLE EMailAddress ENABLE ROW LEVEL SECURITY;
|
||||||
drop view if exists EMailAddress_rv;
|
drop view if exists EMailAddress_rv;
|
||||||
create or replace view EMailAddress_rv as
|
create or replace view EMailAddress_rv as
|
||||||
select distinct target.*
|
select target.*
|
||||||
from EMailAddress as target
|
from EMailAddress as target
|
||||||
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'emailaddress', currentSubjectIds()));
|
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'emailaddress', currentSubjectIds()));
|
||||||
grant all privileges on EMailAddress_rv to restricted;
|
grant all privileges on EMailAddress_rv to restricted;
|
||||||
|
@ -6,15 +6,15 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
import org.springframework.core.NestedRuntimeException;
|
|
||||||
import org.springframework.orm.jpa.JpaSystemException;
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
|
import javax.persistence.PersistenceException;
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.UUID;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
|
import static net.hostsharing.test.JpaAttempt.attempt;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
@ -30,153 +30,179 @@ class CustomerRepositoryIntegrationTest {
|
|||||||
@Autowired EntityManager em;
|
@Autowired EntityManager em;
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class FindAll {
|
class CreateCustomer {
|
||||||
|
|
||||||
private final Given given = new Given();
|
|
||||||
private When<List<CustomerEntity>> when;
|
|
||||||
private final Then then = new Then();
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void hostsharingAdminWithoutAssumedRoleCanViewAllCustomers() {
|
public void hostsharingAdmin_withoutAssumedRole_canCreateNewCustomer() {
|
||||||
given.currentUser("mike@hostsharing.net");
|
// given
|
||||||
|
currentUser("mike@hostsharing.net");
|
||||||
|
|
||||||
when(() -> customerRepository.findAll());
|
// when
|
||||||
|
final var newCustomer = new CustomerEntity(
|
||||||
|
UUID.randomUUID(), "xxx", 90001, "admin@xxx.example.com");
|
||||||
|
final var result = customerRepository.save(newCustomer);
|
||||||
|
|
||||||
then.exactlyTheseCustomersAreReturned("aaa", "aab", "aac");
|
// then
|
||||||
|
assertThat(result).isNotNull().extracting(CustomerEntity::getUuid).isNotNull();
|
||||||
|
assertThatCustomerIsPersisted(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void hostsharingAdminWithAssumedHostsharingAdminRoleCanViewAllCustomers() {
|
public void hostsharingAdmin_withAssumedCustomerRole_cannotCreateNewCustomer() {
|
||||||
given.currentUser("mike@hostsharing.net").
|
// given
|
||||||
and().assumedRoles("global#hostsharing.admin");
|
currentUser("mike@hostsharing.net");
|
||||||
|
assumedRoles("customer#aaa.admin");
|
||||||
|
|
||||||
when(() -> customerRepository.findAll());
|
// when
|
||||||
|
final var attempt = attempt(em, () -> {
|
||||||
|
final var newCustomer = new CustomerEntity(
|
||||||
|
UUID.randomUUID(), "xxx", 90001, "admin@xxx.example.com");
|
||||||
|
return customerRepository.save(newCustomer);
|
||||||
|
});
|
||||||
|
|
||||||
then.exactlyTheseCustomersAreReturned("aaa", "aab", "aac");
|
// then
|
||||||
|
attempt.assertExceptionWithRootCauseMessage(
|
||||||
|
PersistenceException.class,
|
||||||
|
"add-customer not permitted for customer#aaa.admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void customerAdminWithoutAssumedRoleCanViewOnlyItsOwnCustomer() {
|
public void customerAdmin_withoutAssumedRole_cannotCreateNewCustomer() {
|
||||||
given.currentUser("admin@aaa.example.com");
|
// given
|
||||||
|
currentUser("admin@aaa.example.com");
|
||||||
|
|
||||||
when(() -> customerRepository.findAll());
|
// when
|
||||||
|
final var attempt = attempt(em, () -> {
|
||||||
|
final var newCustomer = new CustomerEntity(
|
||||||
|
UUID.randomUUID(), "yyy", 90002, "admin@yyy.example.com");
|
||||||
|
return customerRepository.save(newCustomer);
|
||||||
|
});
|
||||||
|
|
||||||
then.exactlyTheseCustomersAreReturned("aaa");
|
// then
|
||||||
|
attempt.assertExceptionWithRootCauseMessage(
|
||||||
|
PersistenceException.class,
|
||||||
|
"add-customer not permitted for admin@aaa.example.com");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertThatCustomerIsPersisted(final CustomerEntity saved) {
|
||||||
|
final var found = customerRepository.findById(saved.getUuid());
|
||||||
|
assertThat(found).hasValue(saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class FindAllCustomers {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void hostsharingAdmin_withoutAssumedRole_canViewAllCustomers() {
|
||||||
|
// given
|
||||||
|
currentUser("mike@hostsharing.net");
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var result = customerRepository.findAll();
|
||||||
|
|
||||||
|
// then
|
||||||
|
exactlyTheseCustomersAreReturned(result, "aaa", "aab", "aac");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void customerAdminWithAssumedOwnedPackageAdminRoleCanViewOnlyItsOwnCustomer() {
|
public void hostsharingAdmin_withAssumedHostsharingAdminRole_canViewAllCustomers() {
|
||||||
given.currentUser("admin@aaa.example.com").
|
given:
|
||||||
and().assumedRoles("package#aaa00.admin");
|
currentUser("mike@hostsharing.net");
|
||||||
|
assumedRoles("global#hostsharing.admin");
|
||||||
|
|
||||||
when(() -> customerRepository.findAll());
|
// when
|
||||||
|
final var result = customerRepository.findAll();
|
||||||
|
|
||||||
then.exactlyTheseCustomersAreReturned("aaa");
|
then:
|
||||||
|
exactlyTheseCustomersAreReturned(result, "aaa", "aab", "aac");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void customerAdmin_withoutAssumedRole_canViewOnlyItsOwnCustomer() {
|
||||||
|
// given:
|
||||||
|
currentUser("admin@aaa.example.com");
|
||||||
|
|
||||||
|
// when:
|
||||||
|
final var result = customerRepository.findAll();
|
||||||
|
|
||||||
|
// then:
|
||||||
|
exactlyTheseCustomersAreReturned(result, "aaa");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer() {
|
||||||
|
currentUser("admin@aaa.example.com");
|
||||||
|
assumedRoles("package#aaa00.admin");
|
||||||
|
|
||||||
|
final var result = customerRepository.findAll();
|
||||||
|
|
||||||
|
exactlyTheseCustomersAreReturned(result, "aaa");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void customerAdmin_withAssumedAlienPackageAdminRole_cannotViewAnyCustomer() {
|
public void customerAdmin_withAssumedAlienPackageAdminRole_cannotViewAnyCustomer() {
|
||||||
given.currentUser("admin@aaa.example.com").
|
// given:
|
||||||
and().assumedRoles("package#aab00.admin");
|
currentUser("admin@aaa.example.com");
|
||||||
|
assumedRoles("package#aab00.admin");
|
||||||
|
|
||||||
when(() -> customerRepository.findAll());
|
// when
|
||||||
|
final var attempt = attempt(
|
||||||
|
em,
|
||||||
|
() -> customerRepository.findAll());
|
||||||
|
|
||||||
then.expectJpaSystemExceptionHasBeenThrown().
|
// then
|
||||||
and()
|
attempt.assertExceptionWithRootCauseMessage(
|
||||||
.expectRootCauseMessageMatches(
|
JpaSystemException.class,
|
||||||
".* user admin@aaa.example.com .* has no permission to assume role package#aab00#admin .*");
|
"user admin@aaa.example.com .* has no permission to assume role package#aab00#admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void unknownUser_withoutAssumedRole_cannotViewAnyCustomers() {
|
void unknownUser_withoutAssumedRole_cannotViewAnyCustomers() {
|
||||||
given.currentUser("unknown@example.org");
|
currentUser("unknown@example.org");
|
||||||
|
|
||||||
when(() -> customerRepository.findAll());
|
final var attempt = attempt(
|
||||||
|
em,
|
||||||
|
() -> customerRepository.findAll());
|
||||||
|
|
||||||
then.expectJpaSystemExceptionHasBeenThrown().
|
attempt.assertExceptionWithRootCauseMessage(
|
||||||
and().expectRootCauseMessageMatches(".* user unknown@example.org does not exist.*");
|
JpaSystemException.class,
|
||||||
|
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Transactional
|
@Transactional
|
||||||
void unknownUserWithAssumedCustomerRoleCannotViewAnyCustomers() {
|
void unknownUser_withAssumedCustomerRole_cannotViewAnyCustomers() {
|
||||||
given.currentUser("unknown@example.org").
|
currentUser("unknown@example.org");
|
||||||
and().assumedRoles("customer#aaa.admin");
|
assumedRoles("customer#aaa.admin");
|
||||||
|
|
||||||
when(() -> customerRepository.findAll());
|
final var attempt = attempt(
|
||||||
|
em,
|
||||||
|
() -> customerRepository.findAll());
|
||||||
|
|
||||||
then.expectJpaSystemExceptionHasBeenThrown().
|
attempt.assertExceptionWithRootCauseMessage(
|
||||||
and().expectRootCauseMessageMatches(".* user unknown@example.org does not exist.*");
|
JpaSystemException.class,
|
||||||
|
"hsadminng.currentUser defined as unknown@example.org, but does not exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
void when(final Supplier<List<CustomerEntity>> code) {
|
|
||||||
try {
|
|
||||||
when = new When<>(code.get());
|
|
||||||
} catch (final NestedRuntimeException exc) {
|
|
||||||
when = new When<>(exc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Then {
|
|
||||||
|
|
||||||
Then and() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void exactlyTheseCustomersAreReturned(final String... customerPrefixes) {
|
|
||||||
assertThat(when.actualResult)
|
|
||||||
.hasSize(customerPrefixes.length)
|
|
||||||
.extracting(CustomerEntity::getPrefix)
|
|
||||||
.containsExactlyInAnyOrder(customerPrefixes);
|
|
||||||
}
|
|
||||||
|
|
||||||
Then expectJpaSystemExceptionHasBeenThrown() {
|
|
||||||
assertThat(when.actualException).isInstanceOf(JpaSystemException.class);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void expectRootCauseMessageMatches(final String expectedMessage) {
|
|
||||||
assertThat(firstRootCauseMessageLineOf(when.actualException)).matches(expectedMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String firstRootCauseMessageLineOf(final NestedRuntimeException exception) {
|
void currentUser(final String currentUser) {
|
||||||
return Optional.ofNullable(exception.getRootCause())
|
context.setCurrentUser(currentUser);
|
||||||
.map(Throwable::getMessage)
|
assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser);
|
||||||
.map(message -> message.split("\\r|\\n|\\r\\n", 0)[0])
|
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Given {
|
void assumedRoles(final String assumedRoles) {
|
||||||
|
context.assumeRoles(assumedRoles);
|
||||||
Given and() {
|
assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";"));
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Given currentUser(final String currentUser) {
|
|
||||||
context.setCurrentUser(currentUser);
|
|
||||||
assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void assumedRoles(final String assumedRoles) {
|
|
||||||
context.assumeRoles(assumedRoles);
|
|
||||||
assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class When<T> {
|
void exactlyTheseCustomersAreReturned(final List<CustomerEntity> actualResult, final String... customerPrefixes) {
|
||||||
|
assertThat(actualResult)
|
||||||
T actualResult;
|
.hasSize(customerPrefixes.length)
|
||||||
NestedRuntimeException actualException;
|
.extracting(CustomerEntity::getPrefix)
|
||||||
|
.containsExactlyInAnyOrder(customerPrefixes);
|
||||||
When(final T actualResult) {
|
|
||||||
this.actualResult = actualResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
When(final NestedRuntimeException exception) {
|
|
||||||
this.actualException = exception;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
79
src/test/java/net/hostsharing/test/JpaAttempt.java
Normal file
79
src/test/java/net/hostsharing/test/JpaAttempt.java
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package net.hostsharing.test;
|
||||||
|
|
||||||
|
import junit.framework.AssertionFailedError;
|
||||||
|
import org.springframework.core.NestedExceptionUtils;
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the 'when' part of a DataJpaTest to improve readability of tests.
|
||||||
|
* <p>
|
||||||
|
* It
|
||||||
|
* - makes sure that the SQL code is actually performed (em.flush()),
|
||||||
|
* - if any exception is throw, it's caught and stored,
|
||||||
|
* - makes the result available for assertions,
|
||||||
|
* - cleans the JPA first level cache to force assertions read from the database, not just cache,
|
||||||
|
* - offers some assertions based on the exception.
|
||||||
|
* *
|
||||||
|
*
|
||||||
|
* @param <T> success result type
|
||||||
|
*/
|
||||||
|
public class JpaAttempt<T> {
|
||||||
|
|
||||||
|
private T result = null;
|
||||||
|
private RuntimeException exception = null;
|
||||||
|
|
||||||
|
private String firstRootCauseMessageLineOf(final RuntimeException exception) {
|
||||||
|
final var rootCause = NestedExceptionUtils.getRootCause(exception);
|
||||||
|
return Optional.ofNullable(rootCause)
|
||||||
|
.map(Throwable::getMessage)
|
||||||
|
.map(message -> message.split("\\r|\\n|\\r\\n", 0)[0])
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> JpaAttempt<T> attempt(final EntityManager em, final Supplier<T> code) {
|
||||||
|
return new JpaAttempt<>(em, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JpaAttempt(final EntityManager em, final Supplier<T> code) {
|
||||||
|
try {
|
||||||
|
result = code.get();
|
||||||
|
em.flush();
|
||||||
|
em.clear();
|
||||||
|
} catch (RuntimeException exc) {
|
||||||
|
exception = exc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean wasSuccessful() {
|
||||||
|
return exception == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T returnedResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeException caughtException() {
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <E extends RuntimeException> E caughtException(final Class<E> expectedExceptionClass) {
|
||||||
|
if (expectedExceptionClass.isAssignableFrom(exception.getClass())) {
|
||||||
|
return (E) exception;
|
||||||
|
}
|
||||||
|
throw new AssertionFailedError("expected " + expectedExceptionClass + " but got " + exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertExceptionWithRootCauseMessage(
|
||||||
|
final Class<? extends RuntimeException> expectedExceptionClass,
|
||||||
|
final String expectedRootCauseMessage) {
|
||||||
|
assertThat(
|
||||||
|
firstRootCauseMessageLineOf(caughtException(expectedExceptionClass)))
|
||||||
|
.matches(".*" + expectedRootCauseMessage + ".*");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user