add CustomerRepositoryIntegrationTest, fix testcontainers liquibase context and improve error messages
This commit is contained in:
parent
5d4fb85383
commit
27c5699c36
@ -8,6 +8,7 @@ public class PostgreSQL95CustomDialect extends PostgreSQL95Dialect {
|
|||||||
|
|
||||||
public PostgreSQL95CustomDialect() {
|
public PostgreSQL95CustomDialect() {
|
||||||
this.registerHibernateType(2003, StringArrayType.class.getName());
|
this.registerHibernateType(2003, StringArrayType.class.getName());
|
||||||
|
this.registerHibernateType(1111, "pg-uuid");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package net.hostsharing.hsadminng.hscustomer;
|
package net.hostsharing.hsadminng.hscustomer;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface CustomerRepository extends JpaRepository<CustomerEntity, UUID> {
|
public interface CustomerRepository extends JpaRepository<CustomerEntity, UUID> {
|
||||||
|
|
||||||
|
List<CustomerEntity> findByPrefixLike(final String prefix);
|
||||||
}
|
}
|
||||||
|
@ -601,8 +601,12 @@ begin
|
|||||||
objectTable := pureIdentifier(objectTable);
|
objectTable := pureIdentifier(objectTable);
|
||||||
objectIdName := pureIdentifier(objectIdName);
|
objectIdName := pureIdentifier(objectIdName);
|
||||||
sql := format('select * from %sUuidByIdName(%L);', objectTable, objectIdName);
|
sql := format('select * from %sUuidByIdName(%L);', objectTable, objectIdName);
|
||||||
raise notice 'sql: %', sql;
|
begin
|
||||||
execute sql into uuid;
|
raise notice 'sql: %', sql;
|
||||||
|
execute sql into uuid;
|
||||||
|
exception when OTHERS then
|
||||||
|
raise exception 'function %UuidByIdName(...) not found, add identity view support for table %', objectTable, objectTable;
|
||||||
|
end;
|
||||||
return uuid;
|
return uuid;
|
||||||
end; $$;
|
end; $$;
|
||||||
|
|
||||||
@ -622,8 +626,12 @@ declare
|
|||||||
roleUuidToAssume uuid;
|
roleUuidToAssume uuid;
|
||||||
begin
|
begin
|
||||||
currentUserId := currentUserId();
|
currentUserId := currentUserId();
|
||||||
|
if currentUserId is null then
|
||||||
|
raise exception 'user % does not exist', currentUser();
|
||||||
|
end if;
|
||||||
|
|
||||||
roleNames := assumedRoles();
|
roleNames := assumedRoles();
|
||||||
if (cardinality(roleNames) = 0) then
|
if cardinality(roleNames) = 0 then
|
||||||
return array [currentUserId];
|
return array [currentUserId];
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
@ -645,7 +653,7 @@ begin
|
|||||||
and r.roleType = roleTypeToAssume
|
and r.roleType = roleTypeToAssume
|
||||||
into roleUuidToAssume;
|
into roleUuidToAssume;
|
||||||
if (not isGranted(currentUserId, roleUuidToAssume)) then
|
if (not isGranted(currentUserId, roleUuidToAssume)) then
|
||||||
raise exception 'user % has no permission to assume role %', currentUser(), roleUuidToAssume;
|
raise exception 'user % (%) has no permission to assume role % (%)', currentUser(), currentUserId, roleName, roleUuidToAssume;
|
||||||
end if;
|
end if;
|
||||||
roleIdsToAssume := roleIdsToAssume || roleUuidToAssume;
|
roleIdsToAssume := roleIdsToAssume || roleUuidToAssume;
|
||||||
end loop;
|
end loop;
|
||||||
|
@ -9,19 +9,44 @@
|
|||||||
Otherwise these columns needed to be nullable and
|
Otherwise these columns needed to be nullable and
|
||||||
many queries would be more complicated.
|
many queries would be more complicated.
|
||||||
*/
|
*/
|
||||||
create table Hostsharing
|
create table Global
|
||||||
(
|
(
|
||||||
uuid uuid primary key references RbacObject (uuid)
|
uuid uuid primary key references RbacObject (uuid),
|
||||||
|
name varchar(63)
|
||||||
);
|
);
|
||||||
create unique index Hostsharing_Singleton on Hostsharing ((0));
|
create unique index Global_Singleton on Global ((0));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
A single row to be referenced as a global object.
|
A single row to be referenced as a global object.
|
||||||
*/
|
*/
|
||||||
insert
|
insert
|
||||||
into RbacObject (objecttable) values ('hostsharing');
|
into RbacObject (objecttable) values ('global');
|
||||||
insert
|
insert
|
||||||
into Hostsharing (uuid) values ((select uuid from RbacObject where objectTable = 'hostsharing'));
|
into Global (uuid, name) values ((select uuid from RbacObject where objectTable = 'global'), 'hostsharing');
|
||||||
|
--//
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
--changeset hs-base-GLOBAL-IDENTITY-VIEW:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/*
|
||||||
|
Creates a view to the global object table which maps the identifying name to the objectUuid.
|
||||||
|
*/
|
||||||
|
drop view if exists global_iv;
|
||||||
|
create or replace view global_iv as
|
||||||
|
select distinct target.uuid, target.name as idName
|
||||||
|
from global as target;
|
||||||
|
grant all privileges on global_iv to restricted;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Returns the objectUuid for a given identifying name (in this case the prefix).
|
||||||
|
*/
|
||||||
|
create or replace function globalUuidByIdName(idName varchar)
|
||||||
|
returns uuid
|
||||||
|
language sql
|
||||||
|
strict as $$
|
||||||
|
select uuid from global_iv iv where iv.idName = globalUuidByIdName.idName;
|
||||||
|
$$;
|
||||||
--//
|
--//
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
@ -35,12 +60,12 @@ create or replace function hostsharingAdmin()
|
|||||||
returns null on null input
|
returns null on null input
|
||||||
stable leakproof
|
stable leakproof
|
||||||
language sql as $$
|
language sql as $$
|
||||||
select 'global', (select uuid from RbacObject where objectTable = 'hostsharing'), 'admin'::RbacRoleType;
|
select 'global', (select uuid from RbacObject where objectTable = 'global'), 'admin'::RbacRoleType;
|
||||||
$$;
|
$$;
|
||||||
select createRole(hostsharingAdmin());
|
select createRole(hostsharingAdmin());
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset hs-base-ADMIN-USERS:1 context:dev endDelimiter:--//
|
--changeset hs-base-ADMIN-USERS:1 context:dev,test,tc endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
Create two users and assign both to the administrators role.
|
Create two users and assign both to the administrators role.
|
||||||
@ -58,7 +83,7 @@ $$;
|
|||||||
|
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset hs-base-hostsharing-TEST:1 context:dev runAlways:true endDelimiter:--//
|
--changeset hs-base-hostsharing-TEST:1 context:dev,test,tc runAlways:true endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -69,16 +94,16 @@ do language plpgsql $$
|
|||||||
declare
|
declare
|
||||||
userName varchar;
|
userName varchar;
|
||||||
begin
|
begin
|
||||||
set local hsadminng.currentUser = 'mike@hostsharing.net';
|
|
||||||
select userName from RbacUser where uuid = currentUserId() into userName;
|
|
||||||
if userName <> 'mike@hostsharing.net' then
|
|
||||||
raise exception 'fetching initial currentUser failed';
|
|
||||||
end if;
|
|
||||||
|
|
||||||
set local hsadminng.currentUser = 'sven@hostsharing.net';
|
set local hsadminng.currentUser = 'sven@hostsharing.net';
|
||||||
select userName from RbacUser where uuid = currentUserId() into userName;
|
select userName from RbacUser where uuid = currentUserId() into userName;
|
||||||
if userName <> 'sven@hostsharing.net' then
|
if userName <> 'sven@hostsharing.net' then
|
||||||
raise exception 'fetching changed currentUser failed';
|
raise exception 'setting or fetching initial currentUser failed, got: %', userName;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
set local hsadminng.currentUser = 'mike@hostsharing.net';
|
||||||
|
select userName from RbacUser where uuid = currentUserId() into userName;
|
||||||
|
if userName = 'mike@hostsharing.net' then
|
||||||
|
raise exception 'currentUser should not change in one transaction, but did change, got: %', userName;
|
||||||
end if;
|
end if;
|
||||||
end; $$;
|
end; $$;
|
||||||
--//
|
--//
|
||||||
|
@ -0,0 +1,155 @@
|
|||||||
|
package net.hostsharing.hsadminng.hscustomer;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
@DataJpaTest
|
||||||
|
@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class })
|
||||||
|
class CustomerRepositoryIntegrationTest {
|
||||||
|
|
||||||
|
final static String adminUser = "mike@hostsharing.net";
|
||||||
|
final static String customerAaa = "admin@aaa.example.com";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Context context;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
CustomerRepository customerRepository;
|
||||||
|
|
||||||
|
@Autowired EntityManager em;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void hostsharingAdminWithoutAssumedRoleCanViewAllCustomers() {
|
||||||
|
// given
|
||||||
|
context.setCurrentUser(adminUser);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var actual = customerRepository.findAll();
|
||||||
|
|
||||||
|
// then
|
||||||
|
|
||||||
|
assertThat(actual).hasSize(3)
|
||||||
|
.extracting(CustomerEntity::getPrefix)
|
||||||
|
.containsExactlyInAnyOrder("aaa", "aab", "aac");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void hostsharingAdminWithAssumedHostsharingAdminRoleCanViewAllCustomers() {
|
||||||
|
// given
|
||||||
|
context.setCurrentUser(adminUser);
|
||||||
|
context.assumeRoles("global#hostsharing.admin");
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var actual = customerRepository.findAll();
|
||||||
|
|
||||||
|
// then
|
||||||
|
|
||||||
|
assertThat(actual).hasSize(3)
|
||||||
|
.extracting(CustomerEntity::getPrefix)
|
||||||
|
.containsExactlyInAnyOrder("aaa", "aab", "aac");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void customerAdminWithoutAssumedRoleCanViewItsOwnCustomer() {
|
||||||
|
// given
|
||||||
|
context.setCurrentUser(customerAaa);
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var actual = customerRepository.findAll();
|
||||||
|
|
||||||
|
// then
|
||||||
|
|
||||||
|
assertThat(actual).hasSize(1)
|
||||||
|
.extracting(CustomerEntity::getPrefix)
|
||||||
|
.containsExactly("aaa");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void customerAdminWithAssumedOwnedPackageAdminRoleCanViewItsOwnCustomer() {
|
||||||
|
// given
|
||||||
|
context.setCurrentUser(customerAaa);
|
||||||
|
context.assumeRoles("package#aaa00.admin");
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var actual = customerRepository.findAll();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(actual).hasSize(1)
|
||||||
|
.extracting(CustomerEntity::getPrefix)
|
||||||
|
.containsExactly("aaa");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void customerAdminWithAssumedAlienPackageAdminRoleCanViewItsOwnCustomer() {
|
||||||
|
// given
|
||||||
|
context.setCurrentUser(customerAaa);
|
||||||
|
context.assumeRoles("package#aab00.admin");
|
||||||
|
|
||||||
|
// when
|
||||||
|
final JpaSystemException thrown =
|
||||||
|
assertThrows(JpaSystemException.class, () -> customerRepository.findAll());
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(firstRootCauseMessageLineOf(thrown)).matches(
|
||||||
|
".* user admin@aaa.example.com .* has no permission to assume role package#aab00#admin .*"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void unknownUserWithoutAssumedRoleCannotViewAnyCustomers() {
|
||||||
|
// given
|
||||||
|
context.setCurrentUser("unknown@example.org");
|
||||||
|
|
||||||
|
// when
|
||||||
|
final JpaSystemException thrown =
|
||||||
|
assertThrows(JpaSystemException.class, () -> customerRepository.findAll());
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(firstRootCauseMessageLineOf(thrown)).matches(
|
||||||
|
".* user unknown@example.org does not exist.*"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional
|
||||||
|
void unknownUserWithAssumedRoleCannotViewAnyCustomers() {
|
||||||
|
// given
|
||||||
|
context.setCurrentUser("unknown@example.org");
|
||||||
|
assertThat(context.getCurrentUser()).isEqualTo("unknown@example.org");
|
||||||
|
context.assumeRoles("customer#aaa.admin");
|
||||||
|
|
||||||
|
|
||||||
|
// when
|
||||||
|
final JpaSystemException thrown =
|
||||||
|
assertThrows(JpaSystemException.class, () -> customerRepository.findAll());
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(firstRootCauseMessageLineOf(thrown)).matches(
|
||||||
|
".* user unknown@example.org does not exist.*"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstRootCauseMessageLineOf(final JpaSystemException throwable) {
|
||||||
|
return Optional.ofNullable(throwable.getRootCause())
|
||||||
|
.map(Throwable::getMessage)
|
||||||
|
.map( message -> message.split("\\r|\\n|\\r\\n", 0)[0])
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,8 @@ spring:
|
|||||||
platform: postgres
|
platform: postgres
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:tc:postgresql:12.9-alpine:///spring_boot_testcontainers
|
url: jdbc:tc:postgresql:13.7-bullseye:///spring_boot_testcontainers
|
||||||
|
url-local: jdbc:postgresql://localhost:5432/postgres
|
||||||
username: postgres
|
username: postgres
|
||||||
password: password
|
password: password
|
||||||
|
|
||||||
@ -23,11 +24,8 @@ spring:
|
|||||||
|
|
||||||
liquibase:
|
liquibase:
|
||||||
change-log: classpath:/db/changelog/db.changelog-master.yaml
|
change-log: classpath:/db/changelog/db.changelog-master.yaml
|
||||||
contexts: test
|
contexts: tc,test,dev
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
liquibase: INFO
|
liquibase: INFO
|
||||||
|
|
||||||
liquibase:
|
|
||||||
contexts: dev,tc
|
|
||||||
|
Loading…
Reference in New Issue
Block a user