diff --git a/src/main/java/net/hostsharing/hsadminng/config/PostgreSQL95CustomDialect.java b/src/main/java/net/hostsharing/hsadminng/config/PostgreSQL95CustomDialect.java index 70781854..5b86aba9 100644 --- a/src/main/java/net/hostsharing/hsadminng/config/PostgreSQL95CustomDialect.java +++ b/src/main/java/net/hostsharing/hsadminng/config/PostgreSQL95CustomDialect.java @@ -8,6 +8,7 @@ public class PostgreSQL95CustomDialect extends PostgreSQL95Dialect { public PostgreSQL95CustomDialect() { this.registerHibernateType(2003, StringArrayType.class.getName()); + this.registerHibernateType(1111, "pg-uuid"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerRepository.java b/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerRepository.java index 293cbef8..dca4dfb8 100644 --- a/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerRepository.java @@ -1,9 +1,12 @@ package net.hostsharing.hsadminng.hscustomer; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import java.util.List; import java.util.UUID; public interface CustomerRepository extends JpaRepository { + List findByPrefixLike(final String prefix); } diff --git a/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql b/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql index 1e5c77b3..8255ca2b 100644 --- a/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql +++ b/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql @@ -601,8 +601,12 @@ begin objectTable := pureIdentifier(objectTable); objectIdName := pureIdentifier(objectIdName); sql := format('select * from %sUuidByIdName(%L);', objectTable, objectIdName); - raise notice 'sql: %', sql; - execute sql into uuid; + begin + 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; end; $$; @@ -622,8 +626,12 @@ declare roleUuidToAssume uuid; begin currentUserId := currentUserId(); + if currentUserId is null then + raise exception 'user % does not exist', currentUser(); + end if; + roleNames := assumedRoles(); - if (cardinality(roleNames) = 0) then + if cardinality(roleNames) = 0 then return array [currentUserId]; end if; @@ -645,7 +653,7 @@ begin and r.roleType = roleTypeToAssume into roleUuidToAssume; 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; roleIdsToAssume := roleIdsToAssume || roleUuidToAssume; end loop; diff --git a/src/main/resources/db/changelog/2022-07-29-050-hs-base.sql b/src/main/resources/db/changelog/2022-07-29-050-hs-base.sql index 043a995d..d1b42878 100644 --- a/src/main/resources/db/changelog/2022-07-29-050-hs-base.sql +++ b/src/main/resources/db/changelog/2022-07-29-050-hs-base.sql @@ -9,19 +9,44 @@ Otherwise these columns needed to be nullable and 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. */ insert - into RbacObject (objecttable) values ('hostsharing'); + into RbacObject (objecttable) values ('global'); 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 stable leakproof 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()); -- ============================================================================ ---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. @@ -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 userName varchar; 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'; select userName from RbacUser where uuid = currentUserId() into userName; 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; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/hscustomer/CustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hscustomer/CustomerRepositoryIntegrationTest.java new file mode 100644 index 00000000..370b2ad0 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hscustomer/CustomerRepositoryIntegrationTest.java @@ -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); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 0629162b..694e2d74 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -4,7 +4,8 @@ spring: platform: postgres 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 password: password @@ -23,11 +24,8 @@ spring: liquibase: change-log: classpath:/db/changelog/db.changelog-master.yaml - contexts: test + contexts: tc,test,dev logging: level: liquibase: INFO - -liquibase: - contexts: dev,tc