sequential transaction-id (#178)
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/178
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
classDiagram
|
classDiagram
|
||||||
direction LR
|
direction LR
|
||||||
|
|
||||||
OfficePerson o.. "*" LoginCredentials
|
OfficePerson o.. "*" Credentials
|
||||||
LoginCredentials "1" o-- "1" RbacSubject
|
Credentials "1" o-- "1" RbacSubject
|
||||||
|
|
||||||
LoginContext "1..n" --o "1" LoginContextMapping
|
CredentialsContext "1..n" --o "1" CredentialsContextMapping
|
||||||
LoginCredentials "1..n" --o "1" LoginContextMapping
|
Credentials "1..n" --o "1" CredentialsContextMapping
|
||||||
|
|
||||||
class LoginCredentials{
|
class Credentials{
|
||||||
+twoFactorAuth: text
|
+twoFactorAuth: text
|
||||||
+telephonePassword: text
|
+telephonePassword: text
|
||||||
+emailAdress: text
|
+emailAdress: text
|
||||||
@@ -18,14 +18,14 @@ classDiagram
|
|||||||
-onboardingToken: text [w/o]
|
-onboardingToken: text [w/o]
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoginContext{
|
class CredentialsContext{
|
||||||
-type: Enum [SSH, Matrix, Mastodon, ...]
|
-type: Enum [SSH, Matrix, Mastodon, ...]
|
||||||
-qualifier: text
|
-qualifier: text
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoginContextMapping{
|
class CredentialsContextMapping{
|
||||||
}
|
}
|
||||||
note for LoginContextMapping "Assigns LoginCredentials to LoginContexts"
|
note for CredentialsContextMapping "Assigns Credentials to CredentialsContexts"
|
||||||
|
|
||||||
class RbacSubject{
|
class RbacSubject{
|
||||||
+uuid: uuid
|
+uuid: uuid
|
||||||
@@ -41,9 +41,9 @@ classDiagram
|
|||||||
+salutation: text
|
+salutation: text
|
||||||
}
|
}
|
||||||
|
|
||||||
style LoginContext fill:#00f,color:#fff
|
style CredentialsContext fill:#00f,color:#fff
|
||||||
style LoginContextMapping fill:#00f,color:#fff
|
style CredentialsContextMapping fill:#00f,color:#fff
|
||||||
style LoginCredentials fill:#00f,color:#fff
|
style Credentials fill:#00f,color:#fff
|
||||||
|
|
||||||
style RbacSubject fill:#f96,color:#fff
|
style RbacSubject fill:#f96,color:#fff
|
||||||
style OfficePerson fill:#f66,color:#000
|
style OfficePerson fill:#f66,color:#000
|
||||||
|
@@ -2,16 +2,19 @@ package net.hostsharing.hsadminng.credentials;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import net.hostsharing.hsadminng.config.NoSecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.api.LoginContextsApi;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.api.ContextsApi;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginContextResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.ContextResource;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class HsCredentialsContextsController implements LoginContextsApi {
|
@NoSecurityRequirement
|
||||||
|
public class HsCredentialsContextsController implements ContextsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
@@ -23,11 +26,12 @@ public class HsCredentialsContextsController implements LoginContextsApi {
|
|||||||
private HsCredentialsContextRbacRepository contextRepo;
|
private HsCredentialsContextRbacRepository contextRepo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<List<LoginContextResource>> getListOfLoginContexts(final String assumedRoles) {
|
@Timed("app.credentials.contexts.getListOfLoginContexts")
|
||||||
|
public ResponseEntity<List<ContextResource>> getListOfContexts(final String assumedRoles) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
final var loginContexts = contextRepo.findAll();
|
final var loginContexts = contextRepo.findAll();
|
||||||
final var result = mapper.mapList(loginContexts, LoginContextResource.class);
|
final var result = mapper.mapList(loginContexts, ContextResource.class);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,11 +2,15 @@ package net.hostsharing.hsadminng.credentials;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.api.LoginCredentialsApi;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.api.CredentialsApi;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsInsertResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsInsertResource;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsPatchResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsPatchResource;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
@@ -14,8 +18,11 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class HsCredentialsController implements LoginCredentialsApi {
|
@SecurityRequirement(name = "casTicket")
|
||||||
|
public class HsCredentialsController implements CredentialsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
@@ -26,71 +33,84 @@ public class HsCredentialsController implements LoginCredentialsApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private StrictMapper mapper;
|
private StrictMapper mapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageTranslator messageTranslator;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficePersonRbacRepository personRepo;
|
private HsOfficePersonRbacRepository personRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsRepository loginCredentialsRepo;
|
private HsCredentialsRepository credentialsRepo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<LoginCredentialsResource> getSingleLoginCredentialsByUuid(
|
@Timed("app.credentials.credentials.getSingleCredentialsByUuid")
|
||||||
|
public ResponseEntity<CredentialsResource> getSingleCredentialsByUuid(
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final UUID loginCredentialsUuid) {
|
final UUID credentialsUuid) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
final var credentials = loginCredentialsRepo.findByUuid(loginCredentialsUuid);
|
final var credentials = credentialsRepo.findByUuid(credentialsUuid);
|
||||||
final var result = mapper.map(credentials, LoginCredentialsResource.class);
|
final var result = mapper.map(credentials, CredentialsResource.class);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<List<LoginCredentialsResource>> getListOfLoginCredentialsByPersonUuid(
|
@Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid")
|
||||||
|
public ResponseEntity<List<CredentialsResource>> getListOfCredentialsByPersonUuid(
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final UUID personUuid
|
final UUID personUuid
|
||||||
) {
|
) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
final var person = personRepo.findByUuid(personUuid).orElseThrow(); // FIXME: use proper exception
|
final var person = personRepo.findByUuid(personUuid).orElseThrow(
|
||||||
final var credentials = loginCredentialsRepo.findByPerson(person);
|
() -> new EntityNotFoundException(
|
||||||
final var result = mapper.mapList(credentials, LoginCredentialsResource.class);
|
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid)
|
||||||
|
)
|
||||||
|
|
||||||
|
); // FIXME: use proper exception
|
||||||
|
final var credentials = credentialsRepo.findByPerson(person);
|
||||||
|
final var result = mapper.mapList(credentials, CredentialsResource.class);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<LoginCredentialsResource> postNewLoginCredentials(
|
@Timed("app.credentials.credentials.postNewCredentials")
|
||||||
|
public ResponseEntity<CredentialsResource> postNewCredentials(
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final LoginCredentialsInsertResource body
|
final CredentialsInsertResource body
|
||||||
) {
|
) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
final var newLoginCredentialsEntity = mapper.map(body, HsCredentialsEntity.class);
|
final var newCredentialsEntity = mapper.map(body, HsCredentialsEntity.class);
|
||||||
final var savedLoginCredentialsEntity = loginCredentialsRepo.save(newLoginCredentialsEntity);
|
final var savedCredentialsEntity = credentialsRepo.save(newCredentialsEntity);
|
||||||
final var newLoginCredentialsResource = mapper.map(savedLoginCredentialsEntity, LoginCredentialsResource.class);
|
final var newCredentialsResource = mapper.map(savedCredentialsEntity, CredentialsResource.class);
|
||||||
return ResponseEntity.ok(newLoginCredentialsResource);
|
return ResponseEntity.ok(newCredentialsResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<Void> deleteLoginCredentialsByUuid(final String assumedRoles, final UUID loginCredentialsUuid) {
|
@Timed("app.credentials.credentials.deleteCredentialsByUuid")
|
||||||
|
public ResponseEntity<Void> deleteCredentialsByUuid(final String assumedRoles, final UUID credentialsUuid) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
final var loginCredentialsEntity = em.getReference(HsCredentialsEntity.class, loginCredentialsUuid);
|
final var credentialsEntity = em.getReference(HsCredentialsEntity.class, credentialsUuid);
|
||||||
em.remove(loginCredentialsEntity);
|
em.remove(credentialsEntity);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<LoginCredentialsResource> patchLoginCredentials(
|
@Timed("app.credentials.credentials.patchCredentials")
|
||||||
|
public ResponseEntity<CredentialsResource> patchCredentials(
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final UUID loginCredentialsUuid,
|
final UUID credentialsUuid,
|
||||||
final LoginCredentialsPatchResource body
|
final CredentialsPatchResource body
|
||||||
) {
|
) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
final var current = loginCredentialsRepo.findByUuid(loginCredentialsUuid).orElseThrow();
|
final var current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
|
||||||
|
|
||||||
new HsCredentialsEntityPatcher(em, current).apply(body);
|
new HsCredentialsEntityPatcher(em, messageTranslator, current).apply(body);
|
||||||
|
|
||||||
final var saved = loginCredentialsRepo.save(current);
|
final var saved = credentialsRepo.save(current);
|
||||||
final var mapped = mapper.map(saved, LoginCredentialsResource.class);
|
final var mapped = mapper.map(saved, CredentialsResource.class);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Stringifyable {
|
public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Stringifyable {
|
||||||
|
|
||||||
protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "loginCredentials")
|
protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "credentials")
|
||||||
.withProp(HsCredentialsEntity::isActive)
|
.withProp(HsCredentialsEntity::isActive)
|
||||||
.withProp(HsCredentialsEntity::getEmailAddress)
|
.withProp(HsCredentialsEntity::getEmailAddress)
|
||||||
.withProp(HsCredentialsEntity::getTwoFactorAuth)
|
.withProp(HsCredentialsEntity::getTwoFactorAuth)
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
package net.hostsharing.hsadminng.credentials;
|
package net.hostsharing.hsadminng.credentials;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginContextResource;
|
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsPatchResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.ContextResource;
|
||||||
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsPatchResource;
|
||||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||||
|
|
||||||
@@ -11,18 +12,20 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class HsCredentialsEntityPatcher implements EntityPatcher<LoginCredentialsPatchResource> {
|
public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatchResource> {
|
||||||
|
|
||||||
private final EntityManager em;
|
private final EntityManager em;
|
||||||
|
private MessageTranslator messageTranslator;
|
||||||
private final HsCredentialsEntity entity;
|
private final HsCredentialsEntity entity;
|
||||||
|
|
||||||
public HsCredentialsEntityPatcher(final EntityManager em, final HsCredentialsEntity entity) {
|
public HsCredentialsEntityPatcher(final EntityManager em, MessageTranslator messageTranslator, final HsCredentialsEntity entity) {
|
||||||
this.em = em;
|
this.em = em;
|
||||||
|
this.messageTranslator = messageTranslator;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(final LoginCredentialsPatchResource resource) {
|
public void apply(final CredentialsPatchResource resource) {
|
||||||
if ( resource.getActive() != null ) {
|
if ( resource.getActive() != null ) {
|
||||||
entity.setActive(resource.getActive());
|
entity.setActive(resource.getActive());
|
||||||
}
|
}
|
||||||
@@ -40,11 +43,11 @@ public class HsCredentialsEntityPatcher implements EntityPatcher<LoginCredential
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void syncLoginContextEntities(
|
public void syncLoginContextEntities(
|
||||||
List<LoginContextResource> resources,
|
List<ContextResource> resources,
|
||||||
Set<HsCredentialsContextRealEntity> entities
|
Set<HsCredentialsContextRealEntity> entities
|
||||||
) {
|
) {
|
||||||
final var resourceUuids = resources.stream()
|
final var resourceUuids = resources.stream()
|
||||||
.map(LoginContextResource::getUuid)
|
.map(ContextResource::getUuid)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
final var entityUuids = entities.stream()
|
final var entityUuids = entities.stream()
|
||||||
@@ -57,14 +60,15 @@ public class HsCredentialsEntityPatcher implements EntityPatcher<LoginCredential
|
|||||||
if (!entityUuids.contains(resource.getUuid())) {
|
if (!entityUuids.contains(resource.getUuid())) {
|
||||||
final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid());
|
final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid());
|
||||||
if ( existingContextEntity == null ) {
|
if ( existingContextEntity == null ) {
|
||||||
// FIXME: i18n
|
|
||||||
throw new EntityNotFoundException(
|
throw new EntityNotFoundException(
|
||||||
HsCredentialsContextRealEntity.class.getName() + " with uuid " + resource.getUuid() + " not found.");
|
messageTranslator.translate("{0} \"{1}\" not found or not accessible",
|
||||||
|
"credentials uuid", resource.getUuid()));
|
||||||
}
|
}
|
||||||
if (!existingContextEntity.getType().equals(resource.getType().name()) &&
|
if (!existingContextEntity.getType().equals(resource.getType()) &&
|
||||||
!existingContextEntity.getQualifier().equals(resource.getQualifier())) {
|
!existingContextEntity.getQualifier().equals(resource.getQualifier())) {
|
||||||
// FIXME: i18n
|
throw new EntityNotFoundException(
|
||||||
throw new EntityNotFoundException("existing " + existingContextEntity + " does not match given resource " + resource);
|
messageTranslator.translate("existing {0} does not match given resource {1}",
|
||||||
|
existingContextEntity, resource));
|
||||||
}
|
}
|
||||||
entities.add(existingContextEntity);
|
entities.add(existingContextEntity);
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ paths:
|
|||||||
# Credentials
|
# Credentials
|
||||||
|
|
||||||
/api/hs/credentials/credentials:
|
/api/hs/credentials/credentials:
|
||||||
$ref: "api-paths.yaml"
|
$ref: "credentials.yaml"
|
||||||
|
|
||||||
/api/hs/credentials/credentials/{credentialsUuid}:
|
/api/hs/credentials/credentials/{credentialsUuid}:
|
||||||
$ref: "credentials-with-uuid.yaml"
|
$ref: "credentials-with-uuid.yaml"
|
||||||
|
@@ -28,7 +28,7 @@ components:
|
|||||||
contexts:
|
contexts:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '-context-schemas.yaml#/components/schemas/Context'
|
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
||||||
required:
|
required:
|
||||||
- uuid
|
- uuid
|
||||||
- active
|
- active
|
||||||
@@ -55,7 +55,7 @@ components:
|
|||||||
contexts:
|
contexts:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '-context-schemas.yaml#/components/schemas/Context'
|
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
||||||
CredentialsInsert:
|
CredentialsInsert:
|
||||||
@@ -83,7 +83,7 @@ components:
|
|||||||
contexts:
|
contexts:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '-context-schemas.yaml#/components/schemas/Context'
|
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
||||||
required:
|
required:
|
||||||
- uuid
|
- uuid
|
||||||
- active
|
- active
|
||||||
|
@@ -34,6 +34,33 @@ create table base.tx_context
|
|||||||
create index on base.tx_context using brin (txTimestamp);
|
create index on base.tx_context using brin (txTimestamp);
|
||||||
--//
|
--//
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
--changeset michael.hoennig:audit-TX-CONTEXT-TABLE-COLUMN-SEQUENTIAL-TX-ID endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
Adds a column to base.tx_context which keeps a strictly sequentially ordered tx-id.
|
||||||
|
*/
|
||||||
|
|
||||||
|
alter table base.tx_context
|
||||||
|
add column seqTxId BIGINT;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION set_next_sequential_txid()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
LOCK TABLE base.tx_context IN EXCLUSIVE MODE;
|
||||||
|
SELECT COALESCE(MAX(seqTxId)+1, 0) INTO NEW.seqTxId FROM base.tx_context;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER set_commit_order_trigger
|
||||||
|
BEFORE INSERT ON base.tx_context
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_next_sequential_txid();
|
||||||
|
--//
|
||||||
|
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset michael.hoennig:audit-TX-JOURNAL-TABLE endDelimiter:--//
|
--changeset michael.hoennig:audit-TX-JOURNAL-TABLE endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
@@ -53,13 +80,24 @@ create index on base.tx_journal (targetTable, targetUuid);
|
|||||||
--//
|
--//
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset michael.hoennig:audit-TX-JOURNAL-VIEW endDelimiter:--//
|
--changeset michael.hoennig:audit-TX-JOURNAL-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
/*
|
/*
|
||||||
A view combining base.tx_journal with base.tx_context.
|
A view combining base.tx_journal with base.tx_context.
|
||||||
*/
|
*/
|
||||||
|
drop view if exists base.tx_journal_v;
|
||||||
create view base.tx_journal_v as
|
create view base.tx_journal_v as
|
||||||
select txc.*, txj.targettable, txj.targetop, txj.targetuuid, txj.targetdelta
|
select txc.seqTxId,
|
||||||
|
txc.txId,
|
||||||
|
txc.txTimeStamp,
|
||||||
|
txc.currentSubject,
|
||||||
|
txc.assumedRoles,
|
||||||
|
txc.currentTask,
|
||||||
|
txc.currentRequest,
|
||||||
|
txj.targetTable,
|
||||||
|
txj.targeTop,
|
||||||
|
txj.targetUuid,
|
||||||
|
txj.targetDelta
|
||||||
from base.tx_journal txj
|
from base.tx_journal txj
|
||||||
left join base.tx_context txc using (txId)
|
left join base.tx_context txc using (txId)
|
||||||
order by txc.txtimestamp;
|
order by txc.txtimestamp;
|
||||||
|
@@ -12,6 +12,9 @@ unknown\ authorization\ ticket=unbekanntes Autorisierungs-Ticket
|
|||||||
{0}\ "{1}"\ not\ found\ or\ not\ accessible={0} "{1}" nicht gefunden oder nicht zugänglich
|
{0}\ "{1}"\ not\ found\ or\ not\ accessible={0} "{1}" nicht gefunden oder nicht zugänglich
|
||||||
but\ is=ist aber
|
but\ is=ist aber
|
||||||
|
|
||||||
|
# credentials validations
|
||||||
|
existing\ {0}\ does\ not\ match\ given\ resource\ {1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1}
|
||||||
|
|
||||||
# office.coop-shares
|
# office.coop-shares
|
||||||
for\ transactionType\={0},\ shareCount\ must\ be\ positive\ but\ is\ {1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1}
|
for\ transactionType\={0},\ shareCount\ must\ be\ positive\ but\ is\ {1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1}
|
||||||
for\ transactionType\={0},\ shareCount\ must\ be\ negative\ but\ is\ {1}=für transactionType={0}, muss shareCount negativ sein, ist aber {1}
|
for\ transactionType\={0},\ shareCount\ must\ be\ negative\ but\ is\ {1}=für transactionType={0}, muss shareCount negativ sein, ist aber {1}
|
||||||
|
@@ -52,6 +52,7 @@ public class ArchitectureTest {
|
|||||||
"..credentials",
|
"..credentials",
|
||||||
"..hash",
|
"..hash",
|
||||||
"..lambda",
|
"..lambda",
|
||||||
|
"..journal",
|
||||||
"..generated..",
|
"..generated..",
|
||||||
"..persistence..",
|
"..persistence..",
|
||||||
"..reflection",
|
"..reflection",
|
||||||
@@ -155,14 +156,14 @@ public class ArchitectureTest {
|
|||||||
public static final ArchRule testPackagesRule = classes()
|
public static final ArchRule testPackagesRule = classes()
|
||||||
.that().resideInAPackage("..test.(*)..")
|
.that().resideInAPackage("..test.(*)..")
|
||||||
.should().onlyBeAccessed().byClassesThat()
|
.should().onlyBeAccessed().byClassesThat()
|
||||||
.resideInAnyPackage("..test.(*)..");
|
.resideInAnyPackage("..test.(*)..", "..journal..");
|
||||||
|
|
||||||
@ArchTest
|
@ArchTest
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static final ArchRule testPackagePackageRule = classes()
|
public static final ArchRule testPackagePackageRule = classes()
|
||||||
.that().resideInAPackage("..test.pac..")
|
.that().resideInAPackage("..test.pac..")
|
||||||
.should().onlyBeAccessed().byClassesThat()
|
.should().onlyBeAccessed().byClassesThat()
|
||||||
.resideInAnyPackage("..test.pac..");
|
.resideInAnyPackage("..test.pac..", "..journal..");
|
||||||
|
|
||||||
@ArchTest
|
@ArchTest
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@@ -174,6 +175,7 @@ public class ArchitectureTest {
|
|||||||
"..hs.office.(*)..",
|
"..hs.office.(*)..",
|
||||||
"..hs.booking.(*)..",
|
"..hs.booking.(*)..",
|
||||||
"..hs.hosting.(*)..",
|
"..hs.hosting.(*)..",
|
||||||
|
"..credentials..",
|
||||||
"..hs.scenarios",
|
"..hs.scenarios",
|
||||||
"..hs.migration",
|
"..hs.migration",
|
||||||
"..rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest
|
"..rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest
|
||||||
|
@@ -11,10 +11,10 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
|||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import jakarta.persistence.PersistenceException;
|
import jakarta.persistence.PersistenceException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
@@ -97,7 +97,7 @@ class HsCredentialsContextsControllerRestTest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
.get("/api/login/contexts")
|
.get("/api/hs/credentials/contexts")
|
||||||
.header("Authorization", "Bearer superuser-alex@hostsharing.net")
|
.header("Authorization", "Bearer superuser-alex@hostsharing.net")
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
.andDo(print())
|
.andDo(print())
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
package net.hostsharing.hsadminng.credentials;
|
package net.hostsharing.hsadminng.credentials;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginContextResource;
|
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginContextTypeResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.ContextResource;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsPatchResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsPatchResource;
|
||||||
import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase;
|
import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.TestInstance;
|
import org.junit.jupiter.api.TestInstance;
|
||||||
@@ -20,11 +20,12 @@ import java.util.stream.Stream;
|
|||||||
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
|
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
@TestInstance(PER_CLASS)
|
@TestInstance(PER_CLASS)
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
||||||
LoginCredentialsPatchResource,
|
CredentialsPatchResource,
|
||||||
HsCredentialsEntity
|
HsCredentialsEntity
|
||||||
> {
|
> {
|
||||||
|
|
||||||
@@ -58,8 +59,8 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
|||||||
.qualifier("dev")
|
.qualifier("dev")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
private LoginContextResource patchContextResource2;
|
private ContextResource patchContextResource2;
|
||||||
private LoginContextResource patchContextResource3;
|
private ContextResource patchContextResource3;
|
||||||
|
|
||||||
// This is what em.find should return for CONTEXT_UUID_3
|
// This is what em.find should return for CONTEXT_UUID_3
|
||||||
private final HsCredentialsContextRealEntity newContextEntity3 = HsCredentialsContextRealEntity.builder()
|
private final HsCredentialsContextRealEntity newContextEntity3 = HsCredentialsContextRealEntity.builder()
|
||||||
@@ -69,7 +70,7 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
private final Set<HsCredentialsContextRealEntity> initialContextEntities = Set.of(initialContextEntity1, initialContextEntity2);
|
private final Set<HsCredentialsContextRealEntity> initialContextEntities = Set.of(initialContextEntity1, initialContextEntity2);
|
||||||
private List<LoginContextResource> patchedContextResources;
|
private List<ContextResource> patchedContextResources;
|
||||||
private final Set<HsCredentialsContextRealEntity> expectedPatchedContextEntities = Set.of(initialContextEntity2, newContextEntity3);
|
private final Set<HsCredentialsContextRealEntity> expectedPatchedContextEntities = Set.of(initialContextEntity2, newContextEntity3);
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
@@ -82,14 +83,14 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
|||||||
lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_2))).thenReturn(initialContextEntity2);
|
lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_2))).thenReturn(initialContextEntity2);
|
||||||
lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_3))).thenReturn(newContextEntity3);
|
lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_3))).thenReturn(newContextEntity3);
|
||||||
|
|
||||||
patchContextResource2 = new LoginContextResource();
|
patchContextResource2 = new ContextResource();
|
||||||
patchContextResource2.setUuid(CONTEXT_UUID_2);
|
patchContextResource2.setUuid(CONTEXT_UUID_2);
|
||||||
patchContextResource2.setType(LoginContextTypeResource.SSH);
|
patchContextResource2.setType("SSH");
|
||||||
patchContextResource2.setQualifier("dev");
|
patchContextResource2.setQualifier("dev");
|
||||||
|
|
||||||
patchContextResource3 = new LoginContextResource();
|
patchContextResource3 = new ContextResource();
|
||||||
patchContextResource3.setUuid(CONTEXT_UUID_3);
|
patchContextResource3.setUuid(CONTEXT_UUID_3);
|
||||||
patchContextResource3.setType(LoginContextTypeResource.HSADMIN);
|
patchContextResource3.setType("HSADMIN");
|
||||||
patchContextResource3.setQualifier("test");
|
patchContextResource3.setQualifier("test");
|
||||||
|
|
||||||
patchedContextResources = List.of(patchContextResource2, patchContextResource3);
|
patchedContextResources = List.of(patchContextResource2, patchContextResource3);
|
||||||
@@ -110,13 +111,13 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected LoginCredentialsPatchResource newPatchResource() {
|
protected CredentialsPatchResource newPatchResource() {
|
||||||
return new LoginCredentialsPatchResource();
|
return new CredentialsPatchResource();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected HsCredentialsEntityPatcher createPatcher(final HsCredentialsEntity entity) {
|
protected HsCredentialsEntityPatcher createPatcher(final HsCredentialsEntity entity) {
|
||||||
return new HsCredentialsEntityPatcher(em, entity);
|
return new HsCredentialsEntityPatcher(em, mock(MessageTranslator.class), entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -124,38 +125,38 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
|||||||
return Stream.of(
|
return Stream.of(
|
||||||
new SimpleProperty<>(
|
new SimpleProperty<>(
|
||||||
"active",
|
"active",
|
||||||
LoginCredentialsPatchResource::setActive,
|
CredentialsPatchResource::setActive,
|
||||||
PATCHED_ACTIVE,
|
PATCHED_ACTIVE,
|
||||||
HsCredentialsEntity::setActive,
|
HsCredentialsEntity::setActive,
|
||||||
PATCHED_ACTIVE)
|
PATCHED_ACTIVE)
|
||||||
.notNullable(),
|
.notNullable(),
|
||||||
new JsonNullableProperty<>(
|
new JsonNullableProperty<>(
|
||||||
"emailAddress",
|
"emailAddress",
|
||||||
LoginCredentialsPatchResource::setEmailAddress,
|
CredentialsPatchResource::setEmailAddress,
|
||||||
PATCHED_EMAIL_ADDRESS,
|
PATCHED_EMAIL_ADDRESS,
|
||||||
HsCredentialsEntity::setEmailAddress,
|
HsCredentialsEntity::setEmailAddress,
|
||||||
PATCHED_EMAIL_ADDRESS),
|
PATCHED_EMAIL_ADDRESS),
|
||||||
new JsonNullableProperty<>(
|
new JsonNullableProperty<>(
|
||||||
"twoFactorAuth",
|
"twoFactorAuth",
|
||||||
LoginCredentialsPatchResource::setTwoFactorAuth,
|
CredentialsPatchResource::setTwoFactorAuth,
|
||||||
PATCHED_TWO_FACTOR_AUTH,
|
PATCHED_TWO_FACTOR_AUTH,
|
||||||
HsCredentialsEntity::setTwoFactorAuth,
|
HsCredentialsEntity::setTwoFactorAuth,
|
||||||
PATCHED_TWO_FACTOR_AUTH),
|
PATCHED_TWO_FACTOR_AUTH),
|
||||||
new JsonNullableProperty<>(
|
new JsonNullableProperty<>(
|
||||||
"smsNumber",
|
"smsNumber",
|
||||||
LoginCredentialsPatchResource::setSmsNumber,
|
CredentialsPatchResource::setSmsNumber,
|
||||||
PATCHED_SMS_NUMBER,
|
PATCHED_SMS_NUMBER,
|
||||||
HsCredentialsEntity::setSmsNumber,
|
HsCredentialsEntity::setSmsNumber,
|
||||||
PATCHED_SMS_NUMBER),
|
PATCHED_SMS_NUMBER),
|
||||||
new JsonNullableProperty<>(
|
new JsonNullableProperty<>(
|
||||||
"phonePassword",
|
"phonePassword",
|
||||||
LoginCredentialsPatchResource::setPhonePassword,
|
CredentialsPatchResource::setPhonePassword,
|
||||||
PATCHED_PHONE_PASSWORD,
|
PATCHED_PHONE_PASSWORD,
|
||||||
HsCredentialsEntity::setPhonePassword,
|
HsCredentialsEntity::setPhonePassword,
|
||||||
PATCHED_PHONE_PASSWORD),
|
PATCHED_PHONE_PASSWORD),
|
||||||
new SimpleProperty<>(
|
new SimpleProperty<>(
|
||||||
"contexts",
|
"contexts",
|
||||||
LoginCredentialsPatchResource::setContexts,
|
CredentialsPatchResource::setContexts,
|
||||||
patchedContextResources,
|
patchedContextResources,
|
||||||
HsCredentialsEntity::setLoginContexts,
|
HsCredentialsEntity::setLoginContexts,
|
||||||
expectedPatchedContextEntities)
|
expectedPatchedContextEntities)
|
||||||
|
@@ -42,7 +42,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsRepository loginCredentialsRepository;
|
private HsCredentialsRepository credentialsRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsContextRealRepository loginContextRealRepo;
|
private HsCredentialsContextRealRepository loginContextRealRepo;
|
||||||
@@ -88,7 +88,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldFindByUuidUsingTestData() {
|
void shouldFindByUuidUsingTestData() {
|
||||||
// when
|
// when
|
||||||
final var foundEntityOptional = loginCredentialsRepository.findByUuid(alexSubject.getUuid());
|
final var foundEntityOptional = credentialsRepository.findByUuid(alexSubject.getUuid());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(foundEntityOptional).isPresent()
|
assertThat(foundEntityOptional).isPresent()
|
||||||
@@ -96,7 +96,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSaveLoginCredentialsWithExistingContext() {
|
void shouldSaveCredentialsWithExistingContext() {
|
||||||
// given
|
// given
|
||||||
final var existingContext = loginContextRealRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
final var existingContext = loginContextRealRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
@@ -111,12 +111,12 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
loginCredentialsRepository.save(newCredentials);
|
credentialsRepository.save(newCredentials);
|
||||||
em.flush();
|
em.flush();
|
||||||
em.clear();
|
em.clear();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
final var foundEntityOptional = loginCredentialsRepository.findByUuid(drewSubject.getUuid());
|
final var foundEntityOptional = credentialsRepository.findByUuid(drewSubject.getUuid());
|
||||||
assertThat(foundEntityOptional).isPresent();
|
assertThat(foundEntityOptional).isPresent();
|
||||||
final var foundEntity = foundEntityOptional.get();
|
final var foundEntity = foundEntityOptional.get();
|
||||||
assertThat(foundEntity.getEmailAddress()).isEqualTo("drew.new@example.com");
|
assertThat(foundEntity.getEmailAddress()).isEqualTo("drew.new@example.com");
|
||||||
@@ -129,7 +129,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotSaveLoginCredentialsWithNewContext() {
|
void shouldNotSaveCredentialsWithNewContext() {
|
||||||
// given
|
// given
|
||||||
final var newContext = HsCredentialsContextRealEntity.builder()
|
final var newContext = HsCredentialsContextRealEntity.builder()
|
||||||
.type("MATRIX")
|
.type("MATRIX")
|
||||||
@@ -146,7 +146,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
final var exception = catchThrowable(() -> {
|
final var exception = catchThrowable(() -> {
|
||||||
loginCredentialsRepository.save(newCredentials);
|
credentialsRepository.save(newCredentials);
|
||||||
em.flush();
|
em.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSaveNewLoginCredentialsWithoutContext() {
|
void shouldSaveNewCredentialsWithoutContext() {
|
||||||
// given
|
// given
|
||||||
final var newCredentials = HsCredentialsEntity.builder()
|
final var newCredentials = HsCredentialsEntity.builder()
|
||||||
.subject(testUserSubject)
|
.subject(testUserSubject)
|
||||||
@@ -167,12 +167,12 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
loginCredentialsRepository.save(newCredentials);
|
credentialsRepository.save(newCredentials);
|
||||||
em.flush();
|
em.flush();
|
||||||
em.clear();
|
em.clear();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
final var foundEntityOptional = loginCredentialsRepository.findByUuid(testUserSubject.getUuid());
|
final var foundEntityOptional = credentialsRepository.findByUuid(testUserSubject.getUuid());
|
||||||
assertThat(foundEntityOptional).isPresent();
|
assertThat(foundEntityOptional).isPresent();
|
||||||
final var foundEntity = foundEntityOptional.get();
|
final var foundEntity = foundEntityOptional.get();
|
||||||
assertThat(foundEntity.getEmailAddress()).isEqualTo("test.user.new@example.com");
|
assertThat(foundEntity.getEmailAddress()).isEqualTo("test.user.new@example.com");
|
||||||
@@ -183,21 +183,21 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUpdateExistingLoginCredentials() {
|
void shouldUpdateExistingCredentials() {
|
||||||
// given
|
// given
|
||||||
final var entityToUpdate = loginCredentialsRepository.findByUuid(alexSubject.getUuid()).orElseThrow();
|
final var entityToUpdate = credentialsRepository.findByUuid(alexSubject.getUuid()).orElseThrow();
|
||||||
final var initialVersion = entityToUpdate.getVersion();
|
final var initialVersion = entityToUpdate.getVersion();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
entityToUpdate.setActive(false);
|
entityToUpdate.setActive(false);
|
||||||
entityToUpdate.setEmailAddress("updated.user1@example.com");
|
entityToUpdate.setEmailAddress("updated.user1@example.com");
|
||||||
final var savedEntity = loginCredentialsRepository.save(entityToUpdate);
|
final var savedEntity = credentialsRepository.save(entityToUpdate);
|
||||||
em.flush();
|
em.flush();
|
||||||
em.clear();
|
em.clear();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(savedEntity.getVersion()).isGreaterThan(initialVersion);
|
assertThat(savedEntity.getVersion()).isGreaterThan(initialVersion);
|
||||||
final var updatedEntityOptional = loginCredentialsRepository.findByUuid(alexSubject.getUuid());
|
final var updatedEntityOptional = credentialsRepository.findByUuid(alexSubject.getUuid());
|
||||||
assertThat(updatedEntityOptional).isPresent();
|
assertThat(updatedEntityOptional).isPresent();
|
||||||
final var updatedEntity = updatedEntityOptional.get();
|
final var updatedEntity = updatedEntityOptional.get();
|
||||||
assertThat(updatedEntity.isActive()).isFalse();
|
assertThat(updatedEntity.isActive()).isFalse();
|
||||||
|
@@ -0,0 +1,122 @@
|
|||||||
|
package net.hostsharing.hsadminng.journal;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||||
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
|
import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerEntity;
|
||||||
|
import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerRepository;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
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.Import;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.transaction.annotation.Propagation.NEVER;
|
||||||
|
|
||||||
|
@DataJpaTest
|
||||||
|
@Import({ Context.class, JpaAttempt.class })
|
||||||
|
@Tag("generalIntegrationTest")
|
||||||
|
class TransactionContextIntegrationTest extends ContextBasedTestWithCleanup {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PlatformTransactionManager transactionManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
JpaAttempt jpaAttempt;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TestCustomerRepository repository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional(propagation = NEVER)
|
||||||
|
void testConcurrentCommitOrder() {
|
||||||
|
|
||||||
|
// determine initial row count
|
||||||
|
final var rowCount = jpaAttempt.transacted(() -> {
|
||||||
|
context("superuser-alex@hostsharing.net");
|
||||||
|
return em.createQuery("SELECT e FROM TestCustomerEntity e", TestCustomerEntity.class).getResultList();
|
||||||
|
}).assertSuccessful().returnedValue().size();
|
||||||
|
|
||||||
|
// when 3 transactions with different runtime run concurrently
|
||||||
|
runThreads(
|
||||||
|
// starts first, ends last (because it's slow)
|
||||||
|
createTransactionThread("t01", 91001, 500),
|
||||||
|
|
||||||
|
// starts second, ends first (because it's faster than the one that got started first)
|
||||||
|
createTransactionThread("t02", 91002, 0),
|
||||||
|
|
||||||
|
// starts third, ends second
|
||||||
|
createTransactionThread("t03", 91003, 100)
|
||||||
|
);
|
||||||
|
|
||||||
|
// then all 3 threads did insert one row each
|
||||||
|
jpaAttempt.transacted(() -> {
|
||||||
|
context("superuser-alex@hostsharing.net");
|
||||||
|
var all = em.createQuery("SELECT e FROM TestCustomerEntity e", TestCustomerEntity.class).getResultList();
|
||||||
|
assertThat(all).hasSize(rowCount + 3);
|
||||||
|
}).assertSuccessful();
|
||||||
|
|
||||||
|
// and seqTxId order is in correct order
|
||||||
|
final var txContextsX = em.createNativeQuery(
|
||||||
|
"select concat(c.txId, ':', c.currentTask) from base.tx_context c order by c.seqTxId"
|
||||||
|
).getResultList();
|
||||||
|
final var txContextTasks = last(3, txContextsX).stream().map(Object::toString).toList();
|
||||||
|
assertThat(txContextTasks.get(0)).endsWith(
|
||||||
|
":TestCustomerEntity(uuid=null, version=0, prefix=t02, reference=91002, adminUserName=null)");
|
||||||
|
assertThat(txContextTasks.get(1)).endsWith(
|
||||||
|
"TestCustomerEntity(uuid=null, version=0, prefix=t03, reference=91003, adminUserName=null)");
|
||||||
|
assertThat(txContextTasks.get(2)).endsWith(
|
||||||
|
"TestCustomerEntity(uuid=null, version=0, prefix=t01, reference=91001, adminUserName=null)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull Thread createTransactionThread(final String t01, final int reference, final int millis) {
|
||||||
|
return new Thread(() -> {
|
||||||
|
jpaAttempt.transacted(() -> {
|
||||||
|
final var entity1 = new TestCustomerEntity();
|
||||||
|
entity1.setPrefix(t01);
|
||||||
|
entity1.setReference(reference);
|
||||||
|
|
||||||
|
context.define(entity1.toString(), null, "superuser-alex@hostsharing.net", null);
|
||||||
|
entity1.setReference(80000 + toInt(em.createNativeQuery("SELECT txid_current()").getSingleResult()));
|
||||||
|
repository.save(entity1);
|
||||||
|
sleep(millis); // simulate a delay
|
||||||
|
}).assertSuccessful();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private int toInt(final Object singleResult) {
|
||||||
|
return ((Long)singleResult).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private void sleep(final int millis) {
|
||||||
|
Thread.sleep(millis);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private void runThreads(final Thread... threads) {
|
||||||
|
for (final Thread thread : threads) {
|
||||||
|
thread.start();
|
||||||
|
sleep(100);
|
||||||
|
}
|
||||||
|
for (final Thread thread : threads) {
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
private List<?> last(final int n, final List<?> list) {
|
||||||
|
return list.subList(Math.max(list.size() - n, 0), list.size());
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user