feature/credentials-schema-updates (#180)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/180 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
@@ -674,13 +674,13 @@ howto
|
|||||||
Add `--args='--spring.profiles.active=...` with the wanted profile selector:
|
Add `--args='--spring.profiles.active=...` with the wanted profile selector:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gw bootRun --args='--spring.profiles.active=fakeCasAuthenticator,external-db,only-office,without-test-data'
|
gw bootRun --args='--spring.profiles.active=fakeCasAuthenticator,external-db,only-prod-schema,without-test-data'
|
||||||
```
|
```
|
||||||
|
|
||||||
These profiles mean:
|
These profiles mean:
|
||||||
|
|
||||||
- **external-db**: an external PostgreSQL database is used with the PostgreSQL users already created as specified in the environment
|
- **external-db**: an external PostgreSQL database is used with the PostgreSQL users already created as specified in the environment
|
||||||
- **only-office**: only the Office module is started, but neither the Booking nor the Hosting modules
|
- **only-prod-schema**: only the Office module is started, but neither the Booking nor the Hosting modules
|
||||||
- **without-test-data**: no test-data is inserted
|
- **without-test-data**: no test-data is inserted
|
||||||
|
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ classDiagram
|
|||||||
Credentials "1..n" --o "1" CredentialsContextMapping
|
Credentials "1..n" --o "1" CredentialsContextMapping
|
||||||
|
|
||||||
class Credentials{
|
class Credentials{
|
||||||
+twoFactorAuth: text
|
+totpSecret: text
|
||||||
+telephonePassword: text
|
+telephonePassword: text
|
||||||
+emailAdress: text
|
+emailAdress: text
|
||||||
+smsNumber: text
|
+smsNumber: text
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
package net.hostsharing.hsadminng.credentials;
|
package net.hostsharing.hsadminng.credentials;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
@@ -24,6 +28,8 @@ import jakarta.persistence.EntityNotFoundException;
|
|||||||
@SecurityRequirement(name = "casTicket")
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsCredentialsController implements CredentialsApi {
|
public class HsCredentialsController implements CredentialsApi {
|
||||||
|
|
||||||
|
private static DateTimeFormatter FULL_TIMESTAMP_FORMAT = DateTimeFormatter.BASIC_ISO_DATE;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@@ -67,7 +73,7 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid)
|
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid)
|
||||||
)
|
)
|
||||||
|
|
||||||
); // FIXME: use proper exception
|
);
|
||||||
final var credentials = credentialsRepo.findByPerson(person);
|
final var credentials = credentialsRepo.findByPerson(person);
|
||||||
final var result = mapper.mapList(credentials, CredentialsResource.class);
|
final var result = mapper.mapList(credentials, CredentialsResource.class);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
@@ -113,4 +119,25 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
final var mapped = mapper.map(saved, CredentialsResource.class);
|
final var mapped = mapper.map(saved, CredentialsResource.class);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Timed("app.credentials.credentials.credentialsUsed")
|
||||||
|
public ResponseEntity<CredentialsResource> credentialsUsed(
|
||||||
|
final String assumedRoles,
|
||||||
|
final UUID credentialsUuid) {
|
||||||
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
|
final var current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
|
||||||
|
|
||||||
|
current.setOnboardingToken(null);
|
||||||
|
current.setLastUsed(LocalDateTime.now());
|
||||||
|
|
||||||
|
final var saved = credentialsRepo.save(current);
|
||||||
|
final var mapped = mapper.map(saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
|
return ResponseEntity.ok(mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
final BiConsumer<HsCredentialsEntity, CredentialsResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||||
|
resource.setLastUsed(entity.getLastUsed().atOffset(ZoneOffset.UTC));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import net.hostsharing.hsadminng.repr.Stringify;
|
|||||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||||
// import net.hostsharing.hsadminng.rbac.RbacSubjectEntity; // Assuming RbacSubjectEntity exists for the FK relationship
|
// import net.hostsharing.hsadminng.rbac.RbacSubjectEntity; // Assuming RbacSubjectEntity exists for the FK relationship
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -29,7 +30,7 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
|||||||
protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "credentials")
|
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::getTotpSecret)
|
||||||
.withProp(HsCredentialsEntity::getPhonePassword)
|
.withProp(HsCredentialsEntity::getPhonePassword)
|
||||||
.withProp(HsCredentialsEntity::getSmsNumber)
|
.withProp(HsCredentialsEntity::getSmsNumber)
|
||||||
.quotedValues(false);
|
.quotedValues(false);
|
||||||
@@ -49,6 +50,9 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
|||||||
@Version
|
@Version
|
||||||
private int version;
|
private int version;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private LocalDateTime lastUsed;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private boolean active;
|
private boolean active;
|
||||||
|
|
||||||
@@ -62,7 +66,7 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
|||||||
private String onboardingToken;
|
private String onboardingToken;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private String twoFactorAuth;
|
private String totpSecret;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private String phonePassword;
|
private String phonePassword;
|
||||||
|
@@ -31,8 +31,8 @@ public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatc
|
|||||||
}
|
}
|
||||||
OptionalFromJson.of(resource.getEmailAddress())
|
OptionalFromJson.of(resource.getEmailAddress())
|
||||||
.ifPresent(entity::setEmailAddress);
|
.ifPresent(entity::setEmailAddress);
|
||||||
OptionalFromJson.of(resource.getTwoFactorAuth())
|
OptionalFromJson.of(resource.getTotpSecret())
|
||||||
.ifPresent(entity::setTwoFactorAuth);
|
.ifPresent(entity::setTotpSecret);
|
||||||
OptionalFromJson.of(resource.getSmsNumber())
|
OptionalFromJson.of(resource.getSmsNumber())
|
||||||
.ifPresent(entity::setSmsNumber);
|
.ifPresent(entity::setSmsNumber);
|
||||||
OptionalFromJson.of(resource.getPhonePassword())
|
OptionalFromJson.of(resource.getPhonePassword())
|
||||||
|
@@ -8,7 +8,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> {
|
public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> {
|
||||||
|
|
||||||
@Timed("app.booking.debitor.repo.findByUuid")
|
@Timed("app.booking.debitor.repo.findByUuid")
|
||||||
|
@@ -6,7 +6,7 @@ import org.springframework.data.repository.Repository;
|
|||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> {
|
public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> {
|
||||||
|
|
||||||
@Timed("app.booking.items.repo.save")
|
@Timed("app.booking.items.repo.save")
|
||||||
|
@@ -32,7 +32,7 @@ import static java.util.Optional.ofNullable;
|
|||||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
@SecurityRequirement(name = "casTicket")
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsBookingItemController implements HsBookingItemsApi {
|
public class HsBookingItemController implements HsBookingItemsApi {
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface HsBookingItemRbacRepository extends HsBookingItemRepository<HsBookingItemRbacEntity>,
|
public interface HsBookingItemRbacRepository extends HsBookingItemRepository<HsBookingItemRbacEntity>,
|
||||||
Repository<HsBookingItemRbacEntity, UUID> {
|
Repository<HsBookingItemRbacEntity, UUID> {
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface HsBookingItemRealRepository extends HsBookingItemRepository<HsBookingItemRealEntity>,
|
public interface HsBookingItemRealRepository extends HsBookingItemRepository<HsBookingItemRealEntity>,
|
||||||
Repository<HsBookingItemRealEntity, UUID> {
|
Repository<HsBookingItemRealEntity, UUID> {
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface HsBookingItemRepository<E extends HsBookingItem> {
|
public interface HsBookingItemRepository<E extends HsBookingItem> {
|
||||||
|
|
||||||
Optional<E> findByUuid(final UUID bookingItemUuid);
|
Optional<E> findByUuid(final UUID bookingItemUuid);
|
||||||
|
@@ -22,7 +22,7 @@ import java.util.UUID;
|
|||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
@SecurityRequirement(name = "casTicket")
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsBookingProjectController implements HsBookingProjectsApi {
|
public class HsBookingProjectController implements HsBookingProjectsApi {
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface HsBookingProjectRbacRepository extends HsBookingProjectRepository<HsBookingProjectRbacEntity>,
|
public interface HsBookingProjectRbacRepository extends HsBookingProjectRepository<HsBookingProjectRbacEntity>,
|
||||||
Repository<HsBookingProjectRbacEntity, UUID> {
|
Repository<HsBookingProjectRbacEntity, UUID> {
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface HsBookingProjectRealRepository extends HsBookingProjectRepository<HsBookingProjectRealEntity>,
|
public interface HsBookingProjectRealRepository extends HsBookingProjectRepository<HsBookingProjectRealEntity>,
|
||||||
Repository<HsBookingProjectRealEntity, UUID> {
|
Repository<HsBookingProjectRealEntity, UUID> {
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface HsBookingProjectRepository<E extends HsBookingProject> {
|
public interface HsBookingProjectRepository<E extends HsBookingProject> {
|
||||||
|
|
||||||
@Timed("app.booking.projects.repo.findByUuid")
|
@Timed("app.booking.projects.repo.findByUuid")
|
||||||
|
@@ -29,7 +29,7 @@ import java.util.UUID;
|
|||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
@SecurityRequirement(name = "casTicket")
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsHostingAssetController implements HsHostingAssetsApi {
|
public class HsHostingAssetController implements HsHostingAssetsApi {
|
||||||
|
|
||||||
|
@@ -14,7 +14,7 @@ import java.util.Map;
|
|||||||
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
@NoSecurityRequirement
|
@NoSecurityRequirement
|
||||||
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
|
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<HsHostingAssetRbacEntity>, Repository<HsHostingAssetRbacEntity, UUID> {
|
public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<HsHostingAssetRbacEntity>, Repository<HsHostingAssetRbacEntity, UUID> {
|
||||||
|
|
||||||
@Timed("app.hostingAsset.repo.findByUuid.rbac")
|
@Timed("app.hostingAsset.repo.findByUuid.rbac")
|
||||||
|
@@ -10,7 +10,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<HsHostingAssetRealEntity>, Repository<HsHostingAssetRealEntity, UUID> {
|
public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<HsHostingAssetRealEntity>, Repository<HsHostingAssetRealEntity, UUID> {
|
||||||
|
|
||||||
@Timed("app.hostingAsset.repo.findByUuid.real")
|
@Timed("app.hostingAsset.repo.findByUuid.real")
|
||||||
|
@@ -7,7 +7,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public interface HsHostingAssetRepository<E extends HsHostingAsset> {
|
public interface HsHostingAssetRepository<E extends HsHostingAsset> {
|
||||||
|
|
||||||
@Timed("app.hosting.assets.repo.findByUuid")
|
@Timed("app.hosting.assets.repo.findByUuid")
|
||||||
|
@@ -17,7 +17,7 @@ import org.springframework.context.annotation.Profile;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Profile("!only-office")
|
@Profile("!only-prod-schema")
|
||||||
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
|
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@@ -16,8 +16,12 @@ paths:
|
|||||||
|
|
||||||
# Credentials
|
# Credentials
|
||||||
|
|
||||||
/api/hs/credentials/credentials:
|
/api/hs/credentials/credentials/{credentialsUuid}/used:
|
||||||
$ref: "credentials.yaml"
|
$ref: "credentials-with-uuid-used.yaml"
|
||||||
|
|
||||||
/api/hs/credentials/credentials/{credentialsUuid}:
|
/api/hs/credentials/credentials/{credentialsUuid}:
|
||||||
$ref: "credentials-with-uuid.yaml"
|
$ref: "credentials-with-uuid.yaml"
|
||||||
|
|
||||||
|
/api/hs/credentials/credentials:
|
||||||
|
$ref: "credentials.yaml"
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ components:
|
|||||||
uuid:
|
uuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
twoFactorAuth:
|
totpSecret:
|
||||||
type: string
|
type: string
|
||||||
telephonePassword:
|
telephonePassword:
|
||||||
type: string
|
type: string
|
||||||
@@ -29,6 +29,9 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
||||||
|
lastUsed:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
required:
|
required:
|
||||||
- uuid
|
- uuid
|
||||||
- active
|
- active
|
||||||
@@ -38,7 +41,7 @@ components:
|
|||||||
CredentialsPatch:
|
CredentialsPatch:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
twoFactorAuth:
|
totpSecret:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
phonePassword:
|
phonePassword:
|
||||||
@@ -64,7 +67,7 @@ components:
|
|||||||
uuid:
|
uuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
twoFactorAuth:
|
totpSecret:
|
||||||
type: string
|
type: string
|
||||||
telephonePassword:
|
telephonePassword:
|
||||||
type: string
|
type: string
|
||||||
|
@@ -0,0 +1,24 @@
|
|||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- -credentials
|
||||||
|
description: 'Is called when credentials got used for a login.'
|
||||||
|
operationId: credentialsUsed
|
||||||
|
parameters:
|
||||||
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
|
- name: credentialsUuid
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
$ref: 'credentials-schemas.yaml#/components/schemas/Credentials'
|
||||||
|
"401":
|
||||||
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
|
"403":
|
||||||
|
$ref: 'error-responses.yaml#/components/responses/Forbidden'
|
@@ -13,11 +13,12 @@ create table hs_credentials.credentials
|
|||||||
person_uuid uuid not null references hs_office.person(uuid),
|
person_uuid uuid not null references hs_office.person(uuid),
|
||||||
|
|
||||||
active bool,
|
active bool,
|
||||||
|
last_used timestamp,
|
||||||
global_uid int unique, -- w/o
|
global_uid int unique, -- w/o
|
||||||
global_gid int unique, -- w/o
|
global_gid int unique, -- w/o
|
||||||
onboarding_token text, -- w/o
|
onboarding_token text, -- w/o, but can be set to null to invalidate
|
||||||
|
|
||||||
two_factor_auth text,
|
totp_secret text,
|
||||||
phone_password text,
|
phone_password text,
|
||||||
email_address text,
|
email_address text,
|
||||||
sms_number text
|
sms_number text
|
||||||
|
@@ -51,7 +51,7 @@ begin
|
|||||||
-- call rbac.grantRoleToRole(hs_credentials.context_REFERRER(context_MATRIX_internal), rbac.global_ADMIN());
|
-- call rbac.grantRoleToRole(hs_credentials.context_REFERRER(context_MATRIX_internal), rbac.global_ADMIN());
|
||||||
|
|
||||||
-- Add test credentials (linking to assumed rbac.subject UUIDs)
|
-- Add test credentials (linking to assumed rbac.subject UUIDs)
|
||||||
INSERT INTO hs_credentials.credentials (uuid, version, person_uuid, active, global_uid, global_gid, onboarding_token, two_factor_auth, phone_password, email_address, sms_number) VALUES
|
INSERT INTO hs_credentials.credentials (uuid, version, person_uuid, active, global_uid, global_gid, onboarding_token, totp_secret, phone_password, email_address, sms_number) VALUES
|
||||||
( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, 'token-abc', 'otp-secret-1', 'phone-pw-1', 'alex@example.com', '111-222-3333'),
|
( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, 'token-abc', 'otp-secret-1', 'phone-pw-1', 'alex@example.com', '111-222-3333'),
|
||||||
( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, 'token-def', 'otp-secret-2', 'phone-pw-2', 'fran@example.com', '444-555-6666');
|
( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, 'token-def', 'otp-secret-2', 'phone-pw-2', 'fran@example.com', '444-555-6666');
|
||||||
|
|
||||||
|
@@ -174,59 +174,61 @@ databaseChangeLog:
|
|||||||
|
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/6-hs-booking/600-hs-booking-schema.sql
|
file: db/changelog/6-hs-booking/600-hs-booking-schema.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/6-hs-booking/610-booking-debitor/6100-hs-booking-debitor.sql
|
file: db/changelog/6-hs-booking/610-booking-debitor/6100-hs-booking-debitor.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/6-hs-booking/620-booking-project/6200-hs-booking-project.sql
|
file: db/changelog/6-hs-booking/620-booking-project/6200-hs-booking-project.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql
|
file: db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/6-hs-booking/620-booking-project/6208-hs-booking-project-test-data.sql
|
file: db/changelog/6-hs-booking/620-booking-project/6208-hs-booking-project-test-data.sql
|
||||||
context: "!only-office and !without-test-data"
|
context: "!only-prod-schema and !without-test-data"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/6-hs-booking/630-booking-item/6300-hs-booking-item.sql
|
file: db/changelog/6-hs-booking/630-booking-item/6300-hs-booking-item.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.sql
|
file: db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/6-hs-booking/630-booking-item/6308-hs-booking-item-test-data.sql
|
file: db/changelog/6-hs-booking/630-booking-item/6308-hs-booking-item-test-data.sql
|
||||||
context: "!only-office and !without-test-data"
|
context: "!only-prod-schema and !without-test-data"
|
||||||
|
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/7-hs-hosting/700-hs-hosting-schema.sql
|
file: db/changelog/7-hs-hosting/700-hs-hosting-schema.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql
|
file: db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql
|
file: db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql
|
file: db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql
|
file: db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql
|
||||||
context: "!only-office and !without-test-data"
|
context: "!only-prod-schema and !without-test-data"
|
||||||
|
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/9-hs-global/9000-statistics.sql
|
file: db/changelog/9-hs-global/9000-statistics.sql
|
||||||
context: "!only-office"
|
context: "!only-prod-schema"
|
||||||
|
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/9-hs-global/950-credentials/9500-hs-credentials-schema.sql
|
file: db/changelog/9-hs-global/950-credentials/9500-hs-credentials-schema.sql
|
||||||
|
context: "!only-prod-schema"
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/9-hs-global/950-credentials/9510-hs-credentials.sql
|
file: db/changelog/9-hs-global/950-credentials/9510-hs-credentials.sql
|
||||||
|
context: "!only-prod-schema"
|
||||||
# TODO_impl: RBAC rules for _rv do not yet work properly
|
# TODO_impl: RBAC rules for _rv do not yet work properly
|
||||||
# - include:
|
# - include:
|
||||||
# file: db/changelog/9-hs-global/950-credentials/9513-hs-credentials-rbac.sql
|
# file: db/changelog/9-hs-global/950-credentials/9513-hs-credentials-rbac.sql
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/9-hs-global/950-credentials/9519-hs-credentials-test-data.sql
|
file: db/changelog/9-hs-global/950-credentials/9519-hs-credentials-test-data.sql
|
||||||
context: "!without-test-data"
|
context: "!only-prod-schema and !without-test-data"
|
||||||
|
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/9-hs-global/960-integrations/9600-hs-integration-schema.sql
|
file: db/changelog/9-hs-global/960-integrations/9600-hs-integration-schema.sql
|
||||||
|
@@ -0,0 +1,113 @@
|
|||||||
|
package net.hostsharing.hsadminng.credentials;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
||||||
|
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
||||||
|
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||||
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
||||||
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
|
import org.hamcrest.CustomMatcher;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManagerFactory;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
@WebMvcTest(HsCredentialsController.class)
|
||||||
|
@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class, MessageTranslator.class })
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class HsCredentialsControllerRestTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MockMvc mockMvc;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
Context contextMock;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@SuppressWarnings("unused") // not used in test, but in controller class
|
||||||
|
StrictMapper mapper;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
EntityManagerWrapper em;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
EntityManagerFactory emf;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
HsOfficePersonRbacRepository personRbacRepo;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
HsCredentialsContextRbacRepository loginContextRbacRepo;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
HsCredentialsRepository credentialsRepo;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void patchCredentialsUsed() throws Exception {
|
||||||
|
|
||||||
|
// given
|
||||||
|
final var givenCredentialsUuid = UUID.randomUUID();
|
||||||
|
when(credentialsRepo.findByUuid(givenCredentialsUuid)).thenReturn(Optional.of(
|
||||||
|
HsCredentialsEntity.builder()
|
||||||
|
.uuid(givenCredentialsUuid)
|
||||||
|
.lastUsed(null)
|
||||||
|
.onboardingToken("fake-onboarding-token")
|
||||||
|
.build()
|
||||||
|
));
|
||||||
|
when(credentialsRepo.save(any())).thenAnswer(invocation ->
|
||||||
|
invocation.getArgument(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// when
|
||||||
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
|
.post("/api/hs/credentials/credentials/%{credentialsUuid}/used"
|
||||||
|
.replace("%{credentialsUuid}", givenCredentialsUuid.toString()))
|
||||||
|
.header("Authorization", "Bearer superuser-alex@hostsharing.net")
|
||||||
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(print())
|
||||||
|
|
||||||
|
// then
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath(
|
||||||
|
"$", lenientlyEquals("""
|
||||||
|
{
|
||||||
|
"uuid": "%{credentialsUuid}",
|
||||||
|
"onboardingToken": null
|
||||||
|
}
|
||||||
|
""".replace("%{credentialsUuid}", givenCredentialsUuid.toString())
|
||||||
|
)))
|
||||||
|
.andExpect(jsonPath("$.lastUsed").value(new CustomMatcher<String>("lastUsed should have recent timestamp") {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(final Object o) {
|
||||||
|
if (o == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final var lastUsed = ZonedDateTime.parse(o.toString(), DateTimeFormatter.ISO_DATE_TIME)
|
||||||
|
.toLocalDateTime();
|
||||||
|
return lastUsed.isAfter(LocalDateTime.now().minusMinutes(1)) &&
|
||||||
|
lastUsed.isBefore(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
@@ -33,13 +33,13 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
|||||||
|
|
||||||
private static final Boolean INITIAL_ACTIVE = true;
|
private static final Boolean INITIAL_ACTIVE = true;
|
||||||
private static final String INITIAL_EMAIL_ADDRESS = "initial@example.com";
|
private static final String INITIAL_EMAIL_ADDRESS = "initial@example.com";
|
||||||
private static final String INITIAL_TWO_FACTOR_AUTH = "initial_2fa";
|
private static final String INITIAL_TOTP_SECRET = "initial_2fa";
|
||||||
private static final String INITIAL_SMS_NUMBER = "initial_sms";
|
private static final String INITIAL_SMS_NUMBER = "initial_sms";
|
||||||
private static final String INITIAL_PHONE_PASSWORD = "initial_phone_pw";
|
private static final String INITIAL_PHONE_PASSWORD = "initial_phone_pw";
|
||||||
|
|
||||||
private static final Boolean PATCHED_ACTIVE = false;
|
private static final Boolean PATCHED_ACTIVE = false;
|
||||||
private static final String PATCHED_EMAIL_ADDRESS = "patched@example.com";
|
private static final String PATCHED_EMAIL_ADDRESS = "patched@example.com";
|
||||||
private static final String PATCHED_TWO_FACTOR_AUTH = "patched_2fa";
|
private static final String PATCHED_TOTP_SECRET = "patched_2fa";
|
||||||
private static final String PATCHED_SMS_NUMBER = "patched_sms";
|
private static final String PATCHED_SMS_NUMBER = "patched_sms";
|
||||||
private static final String PATCHED_PHONE_PASSWORD = "patched_phone_pw";
|
private static final String PATCHED_PHONE_PASSWORD = "patched_phone_pw";
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
|||||||
entity.setUuid(INITIAL_CREDENTIALS_UUID);
|
entity.setUuid(INITIAL_CREDENTIALS_UUID);
|
||||||
entity.setActive(INITIAL_ACTIVE);
|
entity.setActive(INITIAL_ACTIVE);
|
||||||
entity.setEmailAddress(INITIAL_EMAIL_ADDRESS);
|
entity.setEmailAddress(INITIAL_EMAIL_ADDRESS);
|
||||||
entity.setTwoFactorAuth(INITIAL_TWO_FACTOR_AUTH);
|
entity.setTotpSecret(INITIAL_TOTP_SECRET);
|
||||||
entity.setSmsNumber(INITIAL_SMS_NUMBER);
|
entity.setSmsNumber(INITIAL_SMS_NUMBER);
|
||||||
entity.setPhonePassword(INITIAL_PHONE_PASSWORD);
|
entity.setPhonePassword(INITIAL_PHONE_PASSWORD);
|
||||||
// Ensure loginContexts is a mutable set for the patcher
|
// Ensure loginContexts is a mutable set for the patcher
|
||||||
@@ -137,11 +137,11 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
|||||||
HsCredentialsEntity::setEmailAddress,
|
HsCredentialsEntity::setEmailAddress,
|
||||||
PATCHED_EMAIL_ADDRESS),
|
PATCHED_EMAIL_ADDRESS),
|
||||||
new JsonNullableProperty<>(
|
new JsonNullableProperty<>(
|
||||||
"twoFactorAuth",
|
"totpSecret",
|
||||||
CredentialsPatchResource::setTwoFactorAuth,
|
CredentialsPatchResource::setTotpSecret,
|
||||||
PATCHED_TWO_FACTOR_AUTH,
|
PATCHED_TOTP_SECRET,
|
||||||
HsCredentialsEntity::setTwoFactorAuth,
|
HsCredentialsEntity::setTotpSecret,
|
||||||
PATCHED_TWO_FACTOR_AUTH),
|
PATCHED_TOTP_SECRET),
|
||||||
new JsonNullableProperty<>(
|
new JsonNullableProperty<>(
|
||||||
"smsNumber",
|
"smsNumber",
|
||||||
CredentialsPatchResource::setSmsNumber,
|
CredentialsPatchResource::setSmsNumber,
|
||||||
|
@@ -138,7 +138,7 @@ public class ImportHostingAssets extends CsvDataImport {
|
|||||||
@Autowired
|
@Autowired
|
||||||
LiquibaseMigration liquibase;
|
LiquibaseMigration liquibase;
|
||||||
|
|
||||||
@Value("${HSADMINNG_OFFICE_DATA_SQL_FILE:/db/released-only-office-schema-with-import-test-data.sql}")
|
@Value("${HSADMINNG_OFFICE_DATA_SQL_FILE:/db/released-only-prod-schema-with-import-test-data.sql}")
|
||||||
String officeSchemaAndDataSqlFile;
|
String officeSchemaAndDataSqlFile;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@@ -24,9 +24,9 @@ import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TE
|
|||||||
* <p>The test works as follows:</p>
|
* <p>The test works as follows:</p>
|
||||||
*
|
*
|
||||||
* <ol>
|
* <ol>
|
||||||
* <li>the database is initialized by `db/released-only-office-schema-with-test-data.sql` from the test-resources</li>
|
* <li>the database is initialized by `db/released-only-prod-schema-with-test-data.sql` from the test-resources</li>
|
||||||
* <li>the current Liquibase-migrations (only-office but with-test-data) are performed</li>
|
* <li>the current Liquibase-migrations (only-prod-schema but with-test-data) are performed</li>
|
||||||
* <li>a new dump is written to `db/released-only-office-schema-with-test-data.sql` in the build-directory</li>
|
* <li>a new dump is written to `db/released-only-prod-schema-with-test-data.sql` in the build-directory</li>
|
||||||
* <li>an extra Liquibase-changeset (liquibase-migration-test) is applied</li>
|
* <li>an extra Liquibase-changeset (liquibase-migration-test) is applied</li>
|
||||||
* <li>it's asserted that the extra changeset got applied</li>
|
* <li>it's asserted that the extra changeset got applied</li>
|
||||||
* </ol>
|
* </ol>
|
||||||
@@ -43,7 +43,7 @@ import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TE
|
|||||||
@DirtiesContext
|
@DirtiesContext
|
||||||
@ActiveProfiles("liquibase-migration-test")
|
@ActiveProfiles("liquibase-migration-test")
|
||||||
@Import(LiquibaseConfig.class)
|
@Import(LiquibaseConfig.class)
|
||||||
@Sql(value = "/db/released-only-office-schema-with-test-data.sql", executionPhase = BEFORE_TEST_CLASS) // release-schema
|
@Sql(value = "/db/released-only-prod-schema-with-test-data.sql", executionPhase = BEFORE_TEST_CLASS) // release-schema
|
||||||
public class LiquibaseCompatibilityIntegrationTest {
|
public class LiquibaseCompatibilityIntegrationTest {
|
||||||
|
|
||||||
private static final String EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION = "hs-global-liquibase-migration-test";
|
private static final String EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION = "hs-global-liquibase-migration-test";
|
||||||
@@ -62,8 +62,8 @@ public class LiquibaseCompatibilityIntegrationTest {
|
|||||||
EXPECTED_LIQUIBASE_CHANGELOGS_IN_PROD_SCHEMA_DUMP, EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION);
|
EXPECTED_LIQUIBASE_CHANGELOGS_IN_PROD_SCHEMA_DUMP, EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION);
|
||||||
|
|
||||||
// run the current migrations and dump the result to the build-directory
|
// run the current migrations and dump the result to the build-directory
|
||||||
liquibase.runWithContexts("only-office", "with-test-data");
|
liquibase.runWithContexts("only-prod-schema", "with-test-data");
|
||||||
PostgresTestcontainer.dump(jdbcUrl, new File("build/db/released-only-office-schema-with-test-data.sql"));
|
PostgresTestcontainer.dump(jdbcUrl, new File("build/db/released-only-prod-schema-with-test-data.sql"));
|
||||||
|
|
||||||
// then add another migration and assert if it was applied
|
// then add another migration and assert if it was applied
|
||||||
liquibase.runWithContexts("liquibase-migration-test");
|
liquibase.runWithContexts("liquibase-migration-test");
|
||||||
|
Reference in New Issue
Block a user