add SSHA+Argon2 hashed password to accounts profile and validate profile activation (#203)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/203 Reviewed-by: Marc Sandlus <hsh-marcsandlus@noreply.dev.hostsharing.net>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package net.hostsharing.hsadminng.hash;
|
||||
|
||||
import lombok.val;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class Base64UtilsUnitTest {
|
||||
|
||||
@Test
|
||||
void testNullInput() {
|
||||
val given = (String) null;
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Null input should not be valid Base64").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyInput() {
|
||||
val given = "";
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Empty string should not be valid Base64").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidBase64WithoutPadding() {
|
||||
val given = "U29tZU5vbmRLZXk"; // 'SomeNonKey' in Base64
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Valid Base64 string without padding should be identified as valid").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidBase64WithPadding() {
|
||||
val given = "U29tZSBLZXk="; // 'Some Key' in Base64
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Valid Base64 string with padding should be identified as valid").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidBase64WithDoublePadding() {
|
||||
val given = "U29tZQ=="; // 'Some' in Base64
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Valid Base64 string with double padding should be identified as valid").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidBase64LengthNotDivisibleByFour() {
|
||||
val given = "U29tZQ="; // Invalid length for Base64 with padding
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Input with invalid length not divisible by 4 should not be valid Base64").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidBase64WithSpecialCharacters() {
|
||||
val given = "U29tZQ$$"; // Contains invalid characters
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Input containing invalid characters should not be valid Base64").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidBase64WithWhitespace() {
|
||||
val given = "U29tZ SBs"; // Contains whitespace
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Input containing whitespace should not be valid Base64").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidBase64WithExcessivePadding() {
|
||||
val given = "U29tZQ==="; // Too many padding characters
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Input with excessive padding should not be valid Base64").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEdgeCaseBase64SingleCharacter() {
|
||||
val given = "U"; // Single valid Base64 character
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Single-character input should not be valid Base64").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidBase64EdgeCaseMinimalLengthWithPadding() {
|
||||
val given = "U2="; // Minimal valid Base64 with padding – maps to one byte
|
||||
val actual = Base64Utils.isBase64(given);
|
||||
assertThat(actual).as("Base64 input length not divisible by 4 should fail even if contains padding.").isFalse();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
package net.hostsharing.hsadminng.hash;
|
||||
|
||||
import lombok.val;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Base64;
|
||||
|
||||
import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LDAP_ARGON2;
|
||||
import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LDAP_SSHA;
|
||||
import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LINUX_SHA512;
|
||||
import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LINUX_YESCRYPT;
|
||||
import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.MYSQL_NATIVE;
|
||||
@@ -12,6 +17,7 @@ import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.SCRAM_SHA25
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HashGeneratorUnitTest {
|
||||
|
||||
final String GIVEN_PASSWORD = "given password";
|
||||
@@ -27,6 +33,89 @@ class HashGeneratorUnitTest {
|
||||
// SELECT rolname, rolpassword FROM pg_authid WHERE rolname = 'test';
|
||||
final String GIVEN_POSTGRESQL_GENERATED_SCRAM_SHA256_HASH = "SCRAM-SHA-256$4096:m8M12fdSTsKH+ywthTx1Zw==$4vsB1OddRNdsej9NPAFh91MPdtbOPjkQ85LQZS5lV0Q=:NsVpQNx4Ic/8Sqj1dxfBzUAxyF4FCTMpIsI+bOZCTfA=";
|
||||
|
||||
@Test
|
||||
void fetchesHashGeneratorFromEnvVarDefault() {
|
||||
{
|
||||
val hash = HashGenerator.fromEnv("NON_EXISTING_ENV_VAR", "{SSHA}").withRandomSalt().hash(GIVEN_PASSWORD);
|
||||
LdapSshaHash.verifyHash(hash, GIVEN_PASSWORD); // throws exception if wrong
|
||||
}
|
||||
|
||||
{
|
||||
val hash = HashGenerator.fromEnv("NON_EXISTING_ENV_VAR", "{ARGON2}").withRandomSalt().hash(GIVEN_PASSWORD);
|
||||
LdapArgon2Hash.verifyHash(hash, GIVEN_PASSWORD); // throws exception if wrong
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifiesPasswordAgainstGeneratedArgon2Hash() {
|
||||
val hash = HashGenerator.using(LDAP_ARGON2).withSalt(null).hash(GIVEN_PASSWORD);
|
||||
LdapArgon2Hash.verifyHash(hash, GIVEN_PASSWORD); // throws exception if wrong
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidPasswordAgainstGeneratedArgon2Hash() {
|
||||
val hash = HashGenerator.using(LDAP_ARGON2).withSalt(null).hash(GIVEN_PASSWORD);
|
||||
final var throwable = catchThrowable(() ->
|
||||
LdapArgon2Hash.verifyHash(hash, GIVEN_PASSWORD+"x") // throws exception if wrong
|
||||
);
|
||||
assertThat(throwable).hasMessage("invalid password");
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentArgon2AdapterIgnoresExplicitSalt() {
|
||||
val hash = HashGenerator.using(LDAP_ARGON2).withRandomSalt().hash(GIVEN_PASSWORD);
|
||||
LdapArgon2Hash.verifyHash(hash, GIVEN_PASSWORD); // throws exception if wrong
|
||||
}
|
||||
|
||||
@Test
|
||||
void avoidsDoubleHashingArgon2AHashPassword() {
|
||||
val hashedPassword = "{ARGON2}$argon2id$v=19$m=65536,t=3,p=1$pEabRksh7EJQV+OwPR5n7Q$83qQtZe2J8+fteWm7g/uvXksfhJKGsipZFsuAaJtBjs";
|
||||
val hash = HashGenerator.using(LDAP_ARGON2).hash(hashedPassword);
|
||||
assertThat(hash).isEqualTo(hashedPassword);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hashesPasswordWhichLooksLikeArgon2AHashButIsNot() {
|
||||
val password = "{ARGON2}$argon2id$das-ist-kein-base64-hash";
|
||||
val hash = HashGenerator.using(LDAP_ARGON2).hash(password);
|
||||
LdapArgon2Hash.verifyHash(hash, password); // throws exception if wrong
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifiesPasswordAgainstGeneratedSshaHash() {
|
||||
val hash = HashGenerator.using(LDAP_SSHA).withRandomSalt().hash(GIVEN_PASSWORD);
|
||||
LdapSshaHash.verifyHash(hash, GIVEN_PASSWORD); // throws exception if wrong
|
||||
}
|
||||
|
||||
@Test
|
||||
void avoidsDoubleHashingSshaHashPassword() {
|
||||
val hashedPassword = "{SSHA}SNBnIh5QomfgrvDLDwBR+JOcc8Y17H+4";
|
||||
val hash = HashGenerator.using(LDAP_SSHA).withRandomSalt().hash(hashedPassword);
|
||||
assertThat(hash).isEqualTo(hashedPassword);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hashesPasswordWhichLooksLikeSshaHashButIsNot() {
|
||||
val password = "{SSHA}das-ist-kein-base64-hash";
|
||||
val hash = HashGenerator.using(LDAP_SSHA).withRandomSalt().hash(password);
|
||||
LdapSshaHash.verifyHash(hash, password); // throws exception if wrong
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifiesPasswordAgainstRawSshaHashFromOpenLdap() {
|
||||
val sha512HashFromOpenLdap = "{SSHA}SNBnIh5QomfgrvDLDwBR+JOcc8Y17H+4";
|
||||
LdapSshaHash.verifyHash(sha512HashFromOpenLdap, "QpoGyCeuC1m5X6ew"); // throws exception if wrong
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidPasswordAgainstGeneratedSshaHash() {
|
||||
val hash = HashGenerator.using(LDAP_SSHA).withRandomSalt().hash(GIVEN_PASSWORD);
|
||||
final var throwable = catchThrowable(() ->
|
||||
LdapSshaHash.verifyHash(hash, GIVEN_PASSWORD+"x") // throws exception if wrong
|
||||
);
|
||||
assertThat(throwable).hasMessage("invalid password");
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifiesLinuxPasswordAgainstSha512HashFromMkpasswd() {
|
||||
LinuxEtcShadowHashGenerator.verify(GIVEN_LINUX_GENERATED_SHA512_HASH, GIVEN_PASSWORD); // throws exception if wrong
|
||||
|
||||
+57
-5
@@ -9,6 +9,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RealSubjectEntity;
|
||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -52,7 +53,7 @@ class HsProfileControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
||||
Context context;
|
||||
|
||||
@Autowired
|
||||
RbacSubjectRepository subjectRepo;
|
||||
RbacSubjectRepository rbacSubjectRepo;
|
||||
|
||||
@Autowired
|
||||
HsOfficePersonRealRepository realPersonRepo;
|
||||
@@ -361,9 +362,59 @@ class HsProfileControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
||||
.body("message", containsString("die eigenen hsadmin-Profile dürfen nicht entfernt werden"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldRejectActivatingProfileForNormalUser() {
|
||||
// given
|
||||
context.define("selfregistered-user-drew@hostsharing.org");
|
||||
val drewProfile = profileRepo.findByCurrentSubject().stream().findFirst().orElseThrow();
|
||||
val inactiveProfileUuid = createNewInactiveProfile(drewProfile.getPerson()).getSubject().getUuid();
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("Authorization", bearer("selfregistered-user-drew@hostsharing.org"))
|
||||
.header("Accept-Language", "de")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"active": true
|
||||
}
|
||||
""")
|
||||
.port(port)
|
||||
.when()
|
||||
.patch("http://localhost/api/hs/accounts/profiles/" + inactiveProfileUuid)
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(403)
|
||||
.contentType("application/json")
|
||||
.body("message", containsString("Only global admins are allowed to activate an inactive profile"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private HsProfileEntity createNewInactiveProfile(final HsOfficePersonRealEntity person) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
// only RbacSubject entities can be created
|
||||
val rbacSubjectEntity = rbacSubjectRepo.create(RbacSubjectEntity.builder()
|
||||
.name("some-inactive-profile")
|
||||
.build());
|
||||
// but we need the RealSubjectEntity to be attached to the profile entity
|
||||
val realSubjectEntity = em.find(RealSubjectEntity.class, rbacSubjectEntity.getUuid());
|
||||
|
||||
val inactiveCopy = HsProfileEntity.builder()
|
||||
.person(person)
|
||||
.subject(realSubjectEntity)
|
||||
.active(false).build();
|
||||
em.persist(inactiveCopy);
|
||||
em.flush();
|
||||
return toCleanup(inactiveCopy);
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
private HsOfficePersonRealEntity givenLegalPerson(final String executingSubjectName) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define(executingSubjectName);
|
||||
@@ -418,16 +469,17 @@ class HsProfileControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
||||
) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
context.define(executingSubjectName);
|
||||
final RbacSubjectEntity rbacSubjectEntity = RbacSubjectEntity.builder()
|
||||
|
||||
// only RbacSubject entities can be created
|
||||
val subject = rbacSubjectRepo.create(RbacSubjectEntity.builder()
|
||||
.name(newSubjectName)
|
||||
.build();
|
||||
val subject = subjectRepo.create(rbacSubjectEntity);
|
||||
.build());
|
||||
|
||||
context.define(subject.getName());
|
||||
val attachedPerson = em.find(HsOfficePersonRealEntity.class, person.getUuid());
|
||||
val profileBuilder = HsProfileEntity.builder()
|
||||
.person(attachedPerson)
|
||||
.subject(subjectRepo.findByUuid(subject.getUuid()))
|
||||
.subject(em.find(RealSubjectEntity.class, subject.getUuid()))
|
||||
.scopes(Set.of());
|
||||
modifier.accept(profileBuilder);
|
||||
return toCleanup(profileRepo.save(profileBuilder.build()));
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import lombok.val;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RealSubjectEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class HsProfileEntityUnitTest {
|
||||
|
||||
static final HsProfileEntity GIVEN_PROFILE_ENTITY = HsProfileEntity.builder()
|
||||
.uuid(UUID.fromString("11111111-1111-1111-1111-111111111111"))
|
||||
.subject(
|
||||
RealSubjectEntity.builder().uuid(UUID.randomUUID()).name("testSubject").build())
|
||||
.person(
|
||||
HsOfficePersonRealEntity.builder()
|
||||
.personType(HsOfficePersonType.NATURAL_PERSON)
|
||||
.familyName("Miller")
|
||||
.givenName("John")
|
||||
.build()
|
||||
)
|
||||
.emailAddress("john.miller@example.com")
|
||||
.smsNumber("+49 1234567890")
|
||||
.globalUid(10001)
|
||||
.globalUid(20002)
|
||||
.phonePassword("hello world")
|
||||
.totpSecrets(List.of("secret1", "secret2"))
|
||||
.active(true)
|
||||
.build();
|
||||
|
||||
@Test
|
||||
void toShortStringContainsJustTypeAndQualifier() {
|
||||
assertThat(GIVEN_PROFILE_ENTITY.toShortString()).isEqualTo("true:john.miller@example.com:20002");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringContainsAllPropertiesExceptUuidAndPasswordHash() {
|
||||
assertThat(GIVEN_PROFILE_ENTITY.toString()).isEqualTo("profile(true, john.miller@example.com, [secret1, secret2], hello world, +49 1234567890)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setPasswordSetsPasswordHash() {
|
||||
val profile = HsProfileEntity.builder().build();
|
||||
profile.setPassword("my password");
|
||||
assertThat(profile.getPasswordHash()).startsWith("{SSHA}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsValidSshaPasswordHash() {
|
||||
val givenSshaHash = "{SSHA}SNBnIh5QomfgrvDLDwBR+JOcc8Y17H+4";
|
||||
val profile = HsProfileEntity.builder().build();
|
||||
profile.setPasswordHash(givenSshaHash);
|
||||
assertThat(profile.getPasswordHash()).isEqualTo(givenSshaHash);
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsValidArgon2PasswordHash() {
|
||||
val givenArgon2Hash = "{ARGON2}$argon2id$v=19$m=65536,t=3,p=1$pEabRksh7EJQV+OwPR5n7Q$83qQtZe2J8+fteWm7g/uvXksfhJKGsipZFsuAaJtBjs";
|
||||
val profile = HsProfileEntity.builder().build();
|
||||
profile.setPasswordHash(givenArgon2Hash);
|
||||
assertThat(profile.getPasswordHash()).isEqualTo(givenArgon2Hash);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectInvalidPasswordHash() {
|
||||
val profile = HsProfileEntity.builder().build();
|
||||
val throwable = assertThrows(
|
||||
ValidationException.class,
|
||||
() -> profile.setPasswordHash("{whatever} but not a valid hash"));
|
||||
assertThat(throwable.getMessage()).isEqualTo("passwordHash must be SSHA or ARGON2 hash valid for LDAP");
|
||||
}
|
||||
}
|
||||
+42
-16
@@ -1,5 +1,6 @@
|
||||
package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import lombok.val;
|
||||
import net.hostsharing.hsadminng.rbac.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||
@@ -7,6 +8,8 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
||||
import net.hostsharing.hsadminng.rbac.subject.RealSubjectEntity;
|
||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.hibernate.TransientObjectException;
|
||||
@@ -48,6 +51,9 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
@MockitoBean
|
||||
HttpServletRequest request;
|
||||
|
||||
@Autowired
|
||||
private RbacSubjectRepository rbacSubjectRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePersonRealRepository personRepo;
|
||||
|
||||
@@ -58,9 +64,9 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
private HsProfileScopeRealRepository scopeRealRepo;
|
||||
|
||||
// fetched UUIDs from test-data
|
||||
private RbacSubjectEntity alexSubject;
|
||||
private RbacSubjectEntity drewSubject;
|
||||
private RbacSubjectEntity testUserSubject;
|
||||
private RealSubjectEntity alexSubject;
|
||||
private RealSubjectEntity drewSubject;
|
||||
private RealSubjectEntity testUserSubject;
|
||||
private HsOfficePersonRealEntity drewPerson;
|
||||
private HsOfficePersonRealEntity testUserPerson;
|
||||
|
||||
@@ -106,11 +112,13 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
givenRelation(REPRESENTATIVE)
|
||||
.withAnchorPersonLike(firstGmbHPerson)
|
||||
.withHolder(drewPerson)
|
||||
.withContact("some test contact");
|
||||
.withContact("some test contact")
|
||||
.inDatabase();
|
||||
givenProfile()
|
||||
.forSubject("first-gmbh")
|
||||
.forPerson(firstGmbHPerson)
|
||||
.withEMailAddress("first-gmbh@example.com");
|
||||
.withEMailAddress("first-gmbh@example.com")
|
||||
.inDatabase();
|
||||
|
||||
// when
|
||||
final var foundProfile = attempt(
|
||||
@@ -263,13 +271,13 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
}
|
||||
|
||||
|
||||
private RbacSubjectEntity fetchSubjectByName(final String name) {
|
||||
final String jpql = "SELECT s FROM RbacSubjectEntity s WHERE s.name = :name";
|
||||
final Query query = em.createQuery(jpql, RbacSubjectEntity.class);
|
||||
private RealSubjectEntity fetchSubjectByName(final String name) {
|
||||
final String jpql = "SELECT s FROM RealSubjectEntity s WHERE s.name = :name";
|
||||
final Query query = em.createQuery(jpql, RealSubjectEntity.class);
|
||||
query.setParameter("name", name);
|
||||
try {
|
||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||
return notNull((RbacSubjectEntity) query.getSingleResult());
|
||||
return notNull((RealSubjectEntity) query.getSingleResult());
|
||||
} catch (final NoResultException e) {
|
||||
throw new AssertionError(
|
||||
"Failed to find subject with name '" + name + "'. Ensure test data is present.", e);
|
||||
@@ -331,10 +339,14 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
return this;
|
||||
}
|
||||
|
||||
public HsOfficeRelationRealEntity withContact(String caption) {
|
||||
public RelationBuilder withContact(String caption) {
|
||||
this.contact = HsOfficeContactRealEntity.builder()
|
||||
.caption(caption)
|
||||
.build();
|
||||
return this;
|
||||
}
|
||||
|
||||
public HsOfficeRelationRealEntity inDatabase() {
|
||||
em.persist(contact);
|
||||
|
||||
final var relation = HsOfficeRelationRealEntity.builder()
|
||||
@@ -350,15 +362,17 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
}
|
||||
|
||||
private class ProfileBuilder {
|
||||
private RbacSubjectEntity subject;
|
||||
private RealSubjectEntity subject;
|
||||
private HsOfficePersonRealEntity person;
|
||||
private String emailAddress;
|
||||
|
||||
public ProfileBuilder forSubject(String subjectName) {
|
||||
this.subject = RbacSubjectEntity.builder()
|
||||
// only the RbacSubject can be created
|
||||
val rbacSubject = toCleanup(rbacSubjectRepo.create(RbacSubjectEntity.builder()
|
||||
.name(subjectName)
|
||||
.build();
|
||||
em.persist(subject);
|
||||
toCleanup(subject);
|
||||
.build()));
|
||||
// but we need the RealSubject
|
||||
this.subject = em.find(RealSubjectEntity.class, rbacSubject.getUuid());
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -367,7 +381,19 @@ class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||
return this;
|
||||
}
|
||||
|
||||
public HsProfileEntity withEMailAddress(String emailAddress) {
|
||||
public ProfileBuilder withEMailAddress(String emailAddress) {
|
||||
this.emailAddress = emailAddress;
|
||||
final var profile = HsProfileEntity.builder()
|
||||
.uuid(subject.getUuid())
|
||||
.subject(subject)
|
||||
.person(em.find(HsOfficePersonRealEntity.class, person.getUuid()))
|
||||
.emailAddress(emailAddress)
|
||||
.active(true)
|
||||
.build();
|
||||
return this;
|
||||
}
|
||||
|
||||
public HsProfileEntity inDatabase() {
|
||||
|
||||
final var profile = HsProfileEntity.builder()
|
||||
.uuid(subject.getUuid())
|
||||
|
||||
@@ -35,13 +35,14 @@ public class CreateProfile extends BaseProfileUseCase<CreateProfile> {
|
||||
{
|
||||
"person.uuid": ${Person: %{personGivenName} %{personFamilyName}},
|
||||
"nickname": ${nickname},
|
||||
"active": %{active},
|
||||
"totpSecrets": @{totpSecrets},
|
||||
"emailAddress": ${emailAddress},
|
||||
"phonePassword": ${phonePassword},
|
||||
"smsNumber": ${smsNumber},
|
||||
"password": ${password},
|
||||
"totpSecrets": @{totpSecrets},
|
||||
"phonePassword": ${phonePassword},
|
||||
"globalUid": %{globalUid},
|
||||
"globalGid": %{globalGid},
|
||||
"active": %{active},
|
||||
"scopes": @{resolvedScopes}
|
||||
}
|
||||
"""))
|
||||
|
||||
+5
-3
@@ -74,13 +74,14 @@ class ProfileScenarioTests extends ScenarioTest {
|
||||
// a login name, to be stored in the new RBAC subject
|
||||
.given("nickname", "firby-susan")
|
||||
// initial profile
|
||||
.given("active", true)
|
||||
.given("totpSecrets", Array.of("initialSecret"))
|
||||
.given("emailAddress", "susan.firby@example.com")
|
||||
.given("phonePassword", "securePass123")
|
||||
.given("smsNumber", "+49123456789")
|
||||
.given("password", "my raw password")
|
||||
.given("totpSecrets", Array.of("initialSecret"))
|
||||
.given("phonePassword", "securePass123")
|
||||
.given("globalUid", 21011)
|
||||
.given("globalGid", 21011)
|
||||
.given("active", true)
|
||||
.given(
|
||||
"scopes", Array.of(
|
||||
Pair.of("HSADMIN", "prod")
|
||||
@@ -100,6 +101,7 @@ class ProfileScenarioTests extends ScenarioTest {
|
||||
.given("active", false)
|
||||
.given("totpSecrets", Array.of("initialSecret", "additionalSecret"))
|
||||
.given("emailAddress", "susan.firby@example.org")
|
||||
.given("password", "my new raw password")
|
||||
.given("phonePassword", "securePass987")
|
||||
.given("smsNumber", "+49987654321")
|
||||
.given(
|
||||
|
||||
+3
-2
@@ -24,8 +24,8 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.UUID.randomUUID;
|
||||
import static net.hostsharing.hsadminng.config.JwtFakeBearer.bearer;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
@@ -491,7 +491,8 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
final String newUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
|
||||
context(null);
|
||||
return rbacSubjectRepository.create(new RbacSubjectEntity(UUID.randomUUID(), newUserName));
|
||||
return rbacSubjectRepository.create(
|
||||
RbacSubjectEntity.builder().uuid(randomUUID()).name(newUserName).build());
|
||||
}).returnedValue();
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -322,13 +322,13 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
final var newUserName = "test-user-" + System.currentTimeMillis() + "@example.com";
|
||||
context(null);
|
||||
return rbacSubjectRepository.create(new RbacSubjectEntity(null, newUserName));
|
||||
return rbacSubjectRepository.create(RbacSubjectEntity.builder().name(newUserName).build());
|
||||
}).assumeSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
private RbacSubjectEntity createNewUser() {
|
||||
return rbacSubjectRepository.create(
|
||||
new RbacSubjectEntity(null, "test-user-" + System.currentTimeMillis() + "@example.com"));
|
||||
RbacSubjectEntity.builder().name("test-user-" + System.currentTimeMillis() + "@example.com").build());
|
||||
}
|
||||
|
||||
void exactlyTheseRbacGrantsAreReturned(final List<RbacGrantEntity> actualResult, final String... expectedGrant) {
|
||||
|
||||
+3
-1
@@ -16,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.UUID.randomUUID;
|
||||
import static net.hostsharing.hsadminng.config.JwtFakeBearer.bearer;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
@@ -454,7 +455,8 @@ class RbacSubjectControllerAcceptanceTest {
|
||||
final var givenUserName = "test-user-" + System.currentTimeMillis() + "@example.com";
|
||||
final var givenUser = jpaAttempt.transacted(() -> {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
return rbacSubjectRepository.create(new RbacSubjectEntity(UUID.randomUUID(), givenUserName));
|
||||
return rbacSubjectRepository.create(
|
||||
RbacSubjectEntity.builder().uuid(randomUUID()).name(givenUserName).build());
|
||||
}).assumeSuccessful().returnedValue();
|
||||
assertThat(rbacSubjectRepository.findByName(givenUser.getName())).isNotNull();
|
||||
return givenUser;
|
||||
|
||||
@@ -4,14 +4,14 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.UUID.randomUUID;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class RbacSubjectEntityUnitTest {
|
||||
|
||||
RbacSubjectEntity givenUser = new RbacSubjectEntity(UUID.randomUUID(), "test@example.org");
|
||||
RbacSubjectEntity givenUser = RbacSubjectEntity.builder().uuid(randomUUID()).name("test@example.org").build();
|
||||
|
||||
@Test
|
||||
void generatedAccessCodeMatchesDefinedPattern() {
|
||||
|
||||
+3
-2
@@ -56,7 +56,7 @@ class RbacSubjectRepositoryIntegrationTest extends ContextBasedTest {
|
||||
// when:
|
||||
final var result = jpaAttempt.transacted(() -> {
|
||||
context(null);
|
||||
return rbacSubjectRepository.create(new RbacSubjectEntity(givenUuid, newUserName));
|
||||
return rbacSubjectRepository.create(RbacSubjectEntity.builder().uuid(givenUuid).name(newUserName).build());
|
||||
});
|
||||
|
||||
// then:
|
||||
@@ -395,7 +395,8 @@ class RbacSubjectRepositoryIntegrationTest extends ContextBasedTest {
|
||||
final var givenUserName = "test-user-" + System.currentTimeMillis() + "@example.com";
|
||||
final var givenUser = jpaAttempt.transacted(() -> {
|
||||
context(null);
|
||||
return rbacSubjectRepository.create(new RbacSubjectEntity(UUID.randomUUID(), givenUserName));
|
||||
return rbacSubjectRepository.create(
|
||||
RbacSubjectEntity.builder().uuid(UUID.randomUUID()).name(givenUserName).build());
|
||||
}).assumeSuccessful().returnedValue();
|
||||
assertThat(rbacSubjectRepository.findByName(givenUser.getName())).isNotNull();
|
||||
return givenUser;
|
||||
|
||||
@@ -9,6 +9,6 @@ public class TestRbacSubject {
|
||||
static final RbacSubjectEntity userBbb = rbacRole("customer-admin@bbb.example.com");
|
||||
|
||||
static public RbacSubjectEntity rbacRole(final String userName) {
|
||||
return new RbacSubjectEntity(randomUUID(), userName);
|
||||
return RbacSubjectEntity.builder().uuid(randomUUID()).name(userName).build();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user