diff --git a/README.md b/README.md index f304494f..0e553c08 100644 --- a/README.md +++ b/README.md @@ -674,13 +674,13 @@ howto Add `--args='--spring.profiles.active=...` with the wanted profile selector: ```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: - **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 diff --git a/doc/ideas/login-credentials-data-model.mermaid b/doc/ideas/login-credentials-data-model.mermaid index 5d811421..8f69450c 100644 --- a/doc/ideas/login-credentials-data-model.mermaid +++ b/doc/ideas/login-credentials-data-model.mermaid @@ -8,7 +8,7 @@ classDiagram Credentials "1..n" --o "1" CredentialsContextMapping class Credentials{ - +twoFactorAuth: text + +totpSecret: text +telephonePassword: text +emailAdress: text +smsNumber: text diff --git a/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsController.java b/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsController.java index fef10830..6c282d4c 100644 --- a/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsController.java +++ b/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsController.java @@ -1,7 +1,11 @@ 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.UUID; +import java.util.function.BiConsumer; import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -24,6 +28,8 @@ import jakarta.persistence.EntityNotFoundException; @SecurityRequirement(name = "casTicket") public class HsCredentialsController implements CredentialsApi { + private static DateTimeFormatter FULL_TIMESTAMP_FORMAT = DateTimeFormatter.BASIC_ISO_DATE; + @Autowired private Context context; @@ -67,7 +73,7 @@ public class HsCredentialsController implements CredentialsApi { 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); @@ -113,4 +119,25 @@ public class HsCredentialsController implements CredentialsApi { final var mapped = mapper.map(saved, CredentialsResource.class); return ResponseEntity.ok(mapped); } + + @Override + @Timed("app.credentials.credentials.credentialsUsed") + public ResponseEntity 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 ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { + resource.setLastUsed(entity.getLastUsed().atOffset(ZoneOffset.UTC)); + }; } diff --git a/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntity.java b/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntity.java index cac48352..6c6a2ae0 100644 --- a/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntity.java @@ -9,6 +9,7 @@ import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringifyable; // import net.hostsharing.hsadminng.rbac.RbacSubjectEntity; // Assuming RbacSubjectEntity exists for the FK relationship +import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -29,7 +30,7 @@ public class HsCredentialsEntity implements BaseEntity, Str protected static Stringify stringify = stringify(HsCredentialsEntity.class, "credentials") .withProp(HsCredentialsEntity::isActive) .withProp(HsCredentialsEntity::getEmailAddress) - .withProp(HsCredentialsEntity::getTwoFactorAuth) + .withProp(HsCredentialsEntity::getTotpSecret) .withProp(HsCredentialsEntity::getPhonePassword) .withProp(HsCredentialsEntity::getSmsNumber) .quotedValues(false); @@ -49,6 +50,9 @@ public class HsCredentialsEntity implements BaseEntity, Str @Version private int version; + @Column + private LocalDateTime lastUsed; + @Column private boolean active; @@ -62,7 +66,7 @@ public class HsCredentialsEntity implements BaseEntity, Str private String onboardingToken; @Column - private String twoFactorAuth; + private String totpSecret; @Column private String phonePassword; diff --git a/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntityPatcher.java index 44f1ccd2..1e1a7639 100644 --- a/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntityPatcher.java @@ -31,8 +31,8 @@ public class HsCredentialsEntityPatcher implements EntityPatcher { @Timed("app.booking.debitor.repo.findByUuid") diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventRepository.java index 2ae18218..e562dbdf 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/BookingItemCreatedEventRepository.java @@ -6,7 +6,7 @@ import org.springframework.data.repository.Repository; import java.util.UUID; -@Profile("!only-office") +@Profile("!only-prod-schema") public interface BookingItemCreatedEventRepository extends Repository { @Timed("app.booking.items.repo.save") diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index ddd8a281..30733d53 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -32,7 +32,7 @@ import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; @RestController -@Profile("!only-office") +@Profile("!only-prod-schema") @SecurityRequirement(name = "casTicket") public class HsBookingItemController implements HsBookingItemsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRbacRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRbacRepository.java index 6715e610..f58c1106 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRbacRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRbacRepository.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -@Profile("!only-office") +@Profile("!only-prod-schema") public interface HsBookingItemRbacRepository extends HsBookingItemRepository, Repository { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRealRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRealRepository.java index ef4c205b..520664bc 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRealRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRealRepository.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -@Profile("!only-office") +@Profile("!only-prod-schema") public interface HsBookingItemRealRepository extends HsBookingItemRepository, Repository { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java index 310027cb..4cf0e225 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -@Profile("!only-office") +@Profile("!only-prod-schema") public interface HsBookingItemRepository { Optional findByUuid(final UUID bookingItemUuid); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java index 98c2f60e..3340dd1d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java @@ -22,7 +22,7 @@ import java.util.UUID; import java.util.function.BiConsumer; @RestController -@Profile("!only-office") +@Profile("!only-prod-schema") @SecurityRequirement(name = "casTicket") public class HsBookingProjectController implements HsBookingProjectsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRbacRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRbacRepository.java index a9360b34..6a9f82b4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRbacRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRbacRepository.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -@Profile("!only-office") +@Profile("!only-prod-schema") public interface HsBookingProjectRbacRepository extends HsBookingProjectRepository, Repository { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRealRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRealRepository.java index cc2e9a28..7581cf48 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRealRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRealRepository.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -@Profile("!only-office") +@Profile("!only-prod-schema") public interface HsBookingProjectRealRepository extends HsBookingProjectRepository, Repository { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepository.java index 1f652196..c29dbdd0 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepository.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -@Profile("!only-office") +@Profile("!only-prod-schema") public interface HsBookingProjectRepository { @Timed("app.booking.projects.repo.findByUuid") diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index 28b4e020..31dd5610 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -29,7 +29,7 @@ import java.util.UUID; import java.util.function.BiConsumer; @RestController -@Profile("!only-office") +@Profile("!only-prod-schema") @SecurityRequirement(name = "casTicket") public class HsHostingAssetController implements HsHostingAssetsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java index bea792ba..ed842a11 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java @@ -14,7 +14,7 @@ import java.util.Map; @RestController -@Profile("!only-office") +@Profile("!only-prod-schema") @NoSecurityRequirement public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRbacRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRbacRepository.java index bdb6a65e..d60b18fd 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRbacRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRbacRepository.java @@ -9,7 +9,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -@Profile("!only-office") +@Profile("!only-prod-schema") public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository, Repository { @Timed("app.hostingAsset.repo.findByUuid.rbac") diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java index 4853b665..4dfc983a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRealRepository.java @@ -10,7 +10,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -@Profile("!only-office") +@Profile("!only-prod-schema") public interface HsHostingAssetRealRepository extends HsHostingAssetRepository, Repository { @Timed("app.hostingAsset.repo.findByUuid.real") diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java index c87f6a2c..48b13bdf 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -@Profile("!only-office") +@Profile("!only-prod-schema") public interface HsHostingAssetRepository { @Timed("app.hosting.assets.repo.findByUuid") diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java index d26ebe55..a63e406c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/factories/HsBookingItemCreatedListener.java @@ -17,7 +17,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @Component -@Profile("!only-office") +@Profile("!only-prod-schema") public class HsBookingItemCreatedListener implements ApplicationListener { @Autowired diff --git a/src/main/resources/api-definition/credentials/api-paths.yaml b/src/main/resources/api-definition/credentials/api-paths.yaml index db0ca97c..0d42a123 100644 --- a/src/main/resources/api-definition/credentials/api-paths.yaml +++ b/src/main/resources/api-definition/credentials/api-paths.yaml @@ -16,8 +16,12 @@ paths: # Credentials - /api/hs/credentials/credentials: - $ref: "credentials.yaml" + /api/hs/credentials/credentials/{credentialsUuid}/used: + $ref: "credentials-with-uuid-used.yaml" /api/hs/credentials/credentials/{credentialsUuid}: $ref: "credentials-with-uuid.yaml" + + /api/hs/credentials/credentials: + $ref: "credentials.yaml" + diff --git a/src/main/resources/api-definition/credentials/credentials-schemas.yaml b/src/main/resources/api-definition/credentials/credentials-schemas.yaml index 64e9d954..8dbf8b43 100644 --- a/src/main/resources/api-definition/credentials/credentials-schemas.yaml +++ b/src/main/resources/api-definition/credentials/credentials-schemas.yaml @@ -9,7 +9,7 @@ components: uuid: type: string format: uuid - twoFactorAuth: + totpSecret: type: string telephonePassword: type: string @@ -29,6 +29,9 @@ components: type: array items: $ref: 'context-schemas.yaml#/components/schemas/Context' + lastUsed: + type: string + format: date-time required: - uuid - active @@ -38,7 +41,7 @@ components: CredentialsPatch: type: object properties: - twoFactorAuth: + totpSecret: type: string nullable: true phonePassword: @@ -64,7 +67,7 @@ components: uuid: type: string format: uuid - twoFactorAuth: + totpSecret: type: string telephonePassword: type: string diff --git a/src/main/resources/api-definition/credentials/credentials-with-uuid-used.yaml b/src/main/resources/api-definition/credentials/credentials-with-uuid-used.yaml new file mode 100644 index 00000000..8d4e828b --- /dev/null +++ b/src/main/resources/api-definition/credentials/credentials-with-uuid-used.yaml @@ -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' diff --git a/src/main/resources/db/changelog/9-hs-global/950-credentials/9510-hs-credentials.sql b/src/main/resources/db/changelog/9-hs-global/950-credentials/9510-hs-credentials.sql index 5c054ef5..be615053 100644 --- a/src/main/resources/db/changelog/9-hs-global/950-credentials/9510-hs-credentials.sql +++ b/src/main/resources/db/changelog/9-hs-global/950-credentials/9510-hs-credentials.sql @@ -13,11 +13,12 @@ create table hs_credentials.credentials person_uuid uuid not null references hs_office.person(uuid), active bool, + last_used timestamp, global_uid 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, email_address text, sms_number text diff --git a/src/main/resources/db/changelog/9-hs-global/950-credentials/9519-hs-credentials-test-data.sql b/src/main/resources/db/changelog/9-hs-global/950-credentials/9519-hs-credentials-test-data.sql index ad74b8c1..4a1e7342 100644 --- a/src/main/resources/db/changelog/9-hs-global/950-credentials/9519-hs-credentials-test-data.sql +++ b/src/main/resources/db/changelog/9-hs-global/950-credentials/9519-hs-credentials-test-data.sql @@ -51,7 +51,7 @@ begin -- call rbac.grantRoleToRole(hs_credentials.context_REFERRER(context_MATRIX_internal), rbac.global_ADMIN()); -- 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'), ( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, 'token-def', 'otp-secret-2', 'phone-pw-2', 'fran@example.com', '444-555-6666'); diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 4c993af5..eb7f53ca 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -174,59 +174,61 @@ databaseChangeLog: - include: file: db/changelog/6-hs-booking/600-hs-booking-schema.sql - context: "!only-office" + context: "!only-prod-schema" - include: file: db/changelog/6-hs-booking/610-booking-debitor/6100-hs-booking-debitor.sql - context: "!only-office" + context: "!only-prod-schema" - include: file: db/changelog/6-hs-booking/620-booking-project/6200-hs-booking-project.sql - context: "!only-office" + context: "!only-prod-schema" - include: file: db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql - context: "!only-office" + context: "!only-prod-schema" - include: 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: file: db/changelog/6-hs-booking/630-booking-item/6300-hs-booking-item.sql - context: "!only-office" + context: "!only-prod-schema" - include: file: db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.sql - context: "!only-office" + context: "!only-prod-schema" - include: 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: file: db/changelog/7-hs-hosting/700-hs-hosting-schema.sql - context: "!only-office" + context: "!only-prod-schema" - include: file: db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql - context: "!only-office" + context: "!only-prod-schema" - include: file: db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql - context: "!only-office" + context: "!only-prod-schema" - include: file: db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql - context: "!only-office" + context: "!only-prod-schema" - include: 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: file: db/changelog/9-hs-global/9000-statistics.sql - context: "!only-office" + context: "!only-prod-schema" - include: file: db/changelog/9-hs-global/950-credentials/9500-hs-credentials-schema.sql + context: "!only-prod-schema" - include: 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 # - include: # file: db/changelog/9-hs-global/950-credentials/9513-hs-credentials-rbac.sql - include: 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: file: db/changelog/9-hs-global/960-integrations/9600-hs-integration-schema.sql diff --git a/src/test/java/net/hostsharing/hsadminng/credentials/HsCredentialsControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/credentials/HsCredentialsControllerRestTest.java new file mode 100644 index 00000000..d1971749 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/credentials/HsCredentialsControllerRestTest.java @@ -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("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()); + } + })); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntityPatcherUnitTest.java index e86171fc..cf652aa1 100644 --- a/src/test/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/credentials/HsCredentialsEntityPatcherUnitTest.java @@ -33,13 +33,13 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< private static final Boolean INITIAL_ACTIVE = true; 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_PHONE_PASSWORD = "initial_phone_pw"; private static final Boolean PATCHED_ACTIVE = false; 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_PHONE_PASSWORD = "patched_phone_pw"; @@ -102,7 +102,7 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< entity.setUuid(INITIAL_CREDENTIALS_UUID); entity.setActive(INITIAL_ACTIVE); entity.setEmailAddress(INITIAL_EMAIL_ADDRESS); - entity.setTwoFactorAuth(INITIAL_TWO_FACTOR_AUTH); + entity.setTotpSecret(INITIAL_TOTP_SECRET); entity.setSmsNumber(INITIAL_SMS_NUMBER); entity.setPhonePassword(INITIAL_PHONE_PASSWORD); // Ensure loginContexts is a mutable set for the patcher @@ -137,11 +137,11 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< HsCredentialsEntity::setEmailAddress, PATCHED_EMAIL_ADDRESS), new JsonNullableProperty<>( - "twoFactorAuth", - CredentialsPatchResource::setTwoFactorAuth, - PATCHED_TWO_FACTOR_AUTH, - HsCredentialsEntity::setTwoFactorAuth, - PATCHED_TWO_FACTOR_AUTH), + "totpSecret", + CredentialsPatchResource::setTotpSecret, + PATCHED_TOTP_SECRET, + HsCredentialsEntity::setTotpSecret, + PATCHED_TOTP_SECRET), new JsonNullableProperty<>( "smsNumber", CredentialsPatchResource::setSmsNumber, diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java index 23ececa9..6c075570 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java @@ -138,7 +138,7 @@ public class ImportHostingAssets extends CsvDataImport { @Autowired 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; @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseCompatibilityIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseCompatibilityIntegrationTest.java index e674d7ae..28b31de5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseCompatibilityIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseCompatibilityIntegrationTest.java @@ -24,9 +24,9 @@ import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TE *

The test works as follows:

* *
    - *
  1. the database is initialized by `db/released-only-office-schema-with-test-data.sql` from the test-resources
  2. - *
  3. the current Liquibase-migrations (only-office but with-test-data) are performed
  4. - *
  5. a new dump is written to `db/released-only-office-schema-with-test-data.sql` in the build-directory
  6. + *
  7. the database is initialized by `db/released-only-prod-schema-with-test-data.sql` from the test-resources
  8. + *
  9. the current Liquibase-migrations (only-prod-schema but with-test-data) are performed
  10. + *
  11. a new dump is written to `db/released-only-prod-schema-with-test-data.sql` in the build-directory
  12. *
  13. an extra Liquibase-changeset (liquibase-migration-test) is applied
  14. *
  15. it's asserted that the extra changeset got applied
  16. *
@@ -43,7 +43,7 @@ import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TE @DirtiesContext @ActiveProfiles("liquibase-migration-test") @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 { 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); // run the current migrations and dump the result to the build-directory - liquibase.runWithContexts("only-office", "with-test-data"); - PostgresTestcontainer.dump(jdbcUrl, new File("build/db/released-only-office-schema-with-test-data.sql")); + liquibase.runWithContexts("only-prod-schema", "with-test-data"); + 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 liquibase.runWithContexts("liquibase-migration-test"); diff --git a/src/test/resources/db/released-only-office-schema-with-import-test-data.sql b/src/test/resources/db/released-only-prod-schema-with-import-test-data.sql similarity index 100% rename from src/test/resources/db/released-only-office-schema-with-import-test-data.sql rename to src/test/resources/db/released-only-prod-schema-with-import-test-data.sql diff --git a/src/test/resources/db/released-only-office-schema-with-test-data.sql b/src/test/resources/db/released-only-prod-schema-with-test-data.sql similarity index 100% rename from src/test/resources/db/released-only-office-schema-with-test-data.sql rename to src/test/resources/db/released-only-prod-schema-with-test-data.sql