diff --git a/pom.xml b/pom.xml index 34ea79c..3c83e34 100644 --- a/pom.xml +++ b/pom.xml @@ -169,6 +169,13 @@ 3.20.0 compile + + + commons-codec + commons-codec + 1.20.0 + compile + diff --git a/src/main/java/org/example/cyberwatch/config/DataInitializer.java b/src/main/java/org/example/cyberwatch/config/DataInitializer.java index faef873..5430096 100644 --- a/src/main/java/org/example/cyberwatch/config/DataInitializer.java +++ b/src/main/java/org/example/cyberwatch/config/DataInitializer.java @@ -1,10 +1,13 @@ package org.example.cyberwatch.config; import org.example.cyberwatch.config.security.EncryptionService; +import org.example.cyberwatch.features.form.model.EmploymentForm; +import org.example.cyberwatch.features.form.repository.EmploymentFormRepository; import org.example.cyberwatch.features.staff.model.Staff; import org.example.cyberwatch.features.staff.repository.StaffRepository; -import org.example.cyberwatch.shared.model.enums.Department; -import org.example.cyberwatch.shared.model.enums.Role; +import org.example.cyberwatch.features.ticket.model.Ticket; +import org.example.cyberwatch.features.ticket.repository.TicketRepository; +import org.example.cyberwatch.shared.model.enums.*; import org.springframework.boot.CommandLineRunner; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @@ -15,11 +18,16 @@ public class DataInitializer implements CommandLineRunner { private final StaffRepository staffRepository; + private final EmploymentFormRepository employmentFormRepository; + private final TicketRepository ticketRepository; private final EncryptionService encryptionService; private final PasswordEncoder passwordEncoder; - public DataInitializer(StaffRepository staffRepository, EncryptionService encryptionService, PasswordEncoder passwordEncoder) { + public DataInitializer(StaffRepository staffRepository, EmploymentFormRepository employmentFormRepository, + TicketRepository ticketRepository, EncryptionService encryptionService, PasswordEncoder passwordEncoder) { this.staffRepository = staffRepository; + this.employmentFormRepository = employmentFormRepository; + this.ticketRepository = ticketRepository; this.encryptionService = encryptionService; this.passwordEncoder = passwordEncoder; } @@ -30,26 +38,97 @@ public void run(String... args) { return; } + // Skapa admin-användare + List admins = staffRepository.saveAll(List.of( + createStaff("19900101-0101", "Eric", "Thilen", "ericthilen2003@gmail.com", "0701111111", Role.ADMIN, Department.BACKEND, "testPass123", "ONLINE"), + createStaff("19900505-0505", "Caroline", "Nordbrandt", "nordbrandtcaroline@gmail.com", "0702222222", Role.ADMIN, Department.DEVOPS, "testPass1234", "ONLINE"), + createStaff("19900707-0707", "Alice", "Wersen", "alicewersen@hotmail.com", "0703333333", Role.ADMIN, Department.HR, "testPass12345", "OFFLINE"), + createStaff("19900909-0909", "Younes", "Lamia", "younescool94@gmail.com", "0704444444", Role.ADMIN, Department.FRONTEND, "testPass1234", "OFFLINE") + )); + + // Fler dummies för att testa filter och scroll staffRepository.saveAll(List.of( - createStaff("19900101-0101", "Eric", "Thilen", "ericthilen2003@gmail.com", "0701111111", Role.ADMIN, Department.BACKEND, "testPass123"), - createStaff("19900505-0505", "Caroline", "Nordbrandt", "nordbrandtcaroline@gmail.com", "0702222222", Role.ADMIN, Department.DEVOPS, "testPass1234"), - createStaff("19900707-0707", "Alice", "Wersen", "alicewersen@hotmail.com", "0703333333", Role.ADMIN, Department.HR, "testPass12345"), - createStaff("19900909-0909", "Younes", "Lamia", "younescool94@gmail.com", "0704444444", Role.ADMIN, Department.FRONTEND, "testPass1234") + createStaff("19850212-1212", "Johan", "Andersson", "johan.a@cyberwatch.se", "0705555555", Role.CTO, Department.BACKEND, "pass123", "BUSY"), + createStaff("19920314-3412", "Sofia", "Lindgren", "sofia.l@cyberwatch.se", "0706666666", Role.HR, Department.HR, "pass123", "OFFLINE"), + createStaff("19881120-5678", "Marcus", "Ek", "marcus.ek@cyberwatch.se", "0707777777", Role.CEO, Department.MANAGEMENT, "pass123", "AWAY"), + createStaff("19950606-9999", "Linnea", "Berg", "linnea.b@cyberwatch.se", "0708888888", Role.CONSULTANT, Department.FRONTEND, "pass123", "OFFLINE"), + createStaff("19910825-4433", "Niklas", "Sjöberg", "niklas.s@cyberwatch.se", "0709999999", Role.CONSULTANT, Department.DEVOPS, "pass123", "AWAY"), + createStaff("19931201-1010", "Elena", "Popova", "elena.p@cyberwatch.se", "0701010101", Role.CONSULTANT, Department.BACKEND, "pass123", "OFFLINE"), + createStaff("19870412-2233", "Mikael", "Vesterberg", "mikael.v@cyberwatch.se", "0721112233", Role.PROJECT_MANAGER, Department.DEVOPS, "testPass123", "BUSY"), + createStaff("19941030-4455", "Sara", "Lundin", "sara.l@cyberwatch.se", "0734445566", Role.CONSULTANT, Department.BACKEND, "testPass123", "BUSY"), + createStaff("19910228-6677", "David", "Holm", "david.h@cyberwatch.se", "0767778899", Role.HR, Department.HR, "testPass123", "ONLINE"), + createStaff("19890515-8899", "Emma", "Sjölin", "emma.s@cyberwatch.se", "0708889900", Role.PROJECT_MANAGER, Department.FRONTEND, "testPass123", "ONLINE"), + createStaff("19960820-1122", "Lucas", "Karlsson", "lucas.k@cyberwatch.se", "0791112233", Role.CONSULTANT, Department.BACKEND, "testPass123", "OFFLINE") + )); + + // Skapa 3 pending employment forms + Staff hrStaff = admins.getFirst(); // Eric (Admin) + employmentFormRepository.saveAll(List.of( + createEmploymentForm("19800315-2525", "Peter", "Bergström", "peter.bergstrom@cyberwatch.se", "0715151515", Role.CONSULTANT, Department.BACKEND, hrStaff), + createEmploymentForm("19870723-3636", "Victoria", "Ström", "victoria.strom@cyberwatch.se", "0726262626", Role.PROJECT_MANAGER, Department.FRONTEND, hrStaff), + createEmploymentForm("19920411-4747", "Martin", "Nordin", "martin.nordin@cyberwatch.se", "0737373737", Role.CONSULTANT, Department.DEVOPS, hrStaff) + )); + + // Skapa 10 tickets + List allStaff = staffRepository.findAll(); + ticketRepository.saveAll(List.of( + createTicket("TICKET-1001", "Fix login bug on mobile", "Login button not responding on iOS 17", Status.IN_PROGRESS, Priority.HIGH, IssueType.SOFTWARE, allStaff.get(4), List.of(allStaff.get(7))), + createTicket("TICKET-1002", "Add dark mode feature", "Implement dark theme for better accessibility", Status.SUBMITTED, Priority.MEDIUM, IssueType.SOFTWARE, allStaff.get(5), List.of(allStaff.get(9))), + createTicket("TICKET-1003", "Database optimization", "Query performance issues in staff list endpoint", Status.IN_PROGRESS, Priority.HIGH, IssueType.SOFTWARE, allStaff.get(6), List.of()), + createTicket("TICKET-1004", "Update API documentation", "Document new endpoints for ticket filtering", Status.SUBMITTED, Priority.LOW, IssueType.OTHER, allStaff.get(7), List.of()), + createTicket("TICKET-1005", "Memory leak in S3 upload", "Application consuming too much memory during large file uploads", Status.RESOLVED, Priority.HIGH, IssueType.SOFTWARE, allStaff.get(8), List.of(allStaff.get(5))), + createTicket("TICKET-1006", "Enhance error messages", "Make error messages more user-friendly", Status.SUBMITTED, Priority.MEDIUM, IssueType.SOFTWARE, allStaff.get(9), List.of(allStaff.get(8))), + createTicket("TICKET-1007", "SSL certificate expiring", "Renew SSL certificate before expiration", Status.IN_PROGRESS, Priority.CRITICAL, IssueType.SECURITY, allStaff.get(4), List.of(allStaff.get(6))), + createTicket("TICKET-1008", "Add password recovery", "Implement forgot password functionality", Status.SUBMITTED, Priority.MEDIUM, IssueType.SOFTWARE, allStaff.get(10), List.of(allStaff.get(7))), + createTicket("TICKET-1009", "Network latency issues", "Reported high latency in VPN connection", Status.CLOSED, Priority.LOW, IssueType.NETWORK, allStaff.get(5), List.of()), + createTicket("TICKET-1010", "Update security headers", "Add missing security headers to HTTP responses", Status.IN_PROGRESS, Priority.HIGH, IssueType.SECURITY, allStaff.get(6), List.of(allStaff.get(4))) )); } - private Staff createStaff(String ssn, String firstName, String lastName, String email, String phone, Role role, Department department, String rawPassword) { + private Staff createStaff(String ssn, String firstName, String lastName, String email, String phone, + Role role, Department department, String rawPassword, String status) { Staff staff = new Staff(); staff.setSocialSecurityNumber(encryptionService.encrypt(ssn)); + staff.setSsnHash(encryptionService.hmac(ssn)); staff.setFirstName(firstName); staff.setLastName(lastName); staff.setEmail(email); staff.setPhoneNumber(phone); staff.setRole(role); staff.setDepartment(department); - staff.setStatus("OFFLINE"); + staff.setStatus(status); staff.setPassword(passwordEncoder.encode(rawPassword)); return staff; } + + private EmploymentForm createEmploymentForm(String ssn, String firstName, String lastName, String email, + String phone, Role role, Department department, Staff createdBy) { + EmploymentForm form = new EmploymentForm(); + form.setSocialSecurityNumber(encryptionService.encrypt(ssn)); + form.setSsnHash(encryptionService.hmac(ssn)); + form.setFirstName(firstName); + form.setLastName(lastName); + form.setEmail(email); + form.setPhoneNumber(phone); + form.setRole(role); + form.setDepartment(department); + form.setStatus(ApprovalStatus.PENDING); + form.setCreatedBy(createdBy); + return form; + } + + private Ticket createTicket(String ticketCode, String title, String description, Status status, Priority priority, IssueType issueType, + Staff createdBy, List assignedStaff) { + Ticket ticket = new Ticket(); + ticket.setTicketCode(ticketCode); + ticket.setTitle(title); + ticket.setDescription(description); + ticket.setStatus(status); + ticket.setPriority(priority); + ticket.setIssueType(issueType); + ticket.setCreatedBy(createdBy); + ticket.setAssignedStaff(assignedStaff); + return ticket; + } } diff --git a/src/main/java/org/example/cyberwatch/config/EncryptionConfig.java b/src/main/java/org/example/cyberwatch/config/EncryptionConfig.java index e914265..95bf5a7 100644 --- a/src/main/java/org/example/cyberwatch/config/EncryptionConfig.java +++ b/src/main/java/org/example/cyberwatch/config/EncryptionConfig.java @@ -1,5 +1,6 @@ package org.example.cyberwatch.config; +import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -9,14 +10,61 @@ @Configuration public class EncryptionConfig { - @Value("${app.encryption.key}") - private String encryptionKey; + @Value("${app.encryption.password}") + private String encryptionPass; - @Value("${app.encryption.salt}") - private String salt; + @Value("${app.encryption.pepper}") + private String pepper; + //Vi implementerade fail-fast validation för alla krypteringsnycklar. Applikationen startar INTE om någon nyckel är tom eller för svag. + @PostConstruct + public void validateEncryptionKeys() { + if (encryptionPass == null || encryptionPass.isBlank()) { + throw new IllegalArgumentException( + "CRITICAL: app.encryption.password is missing or blank. " + + "AES-256 encryption disabled. Set a strong password in environment variables." + ); + } + + if (pepper == null || pepper.isBlank()) { + throw new IllegalArgumentException( + "CRITICAL: app.encryption.pepper is missing or blank. " + + "Key derivation weakened. Set a strong pepper value in environment variables." + ); + } + + // Minsta entropy-krav för password: 32 tecken + if (encryptionPass.length() < 32) { + throw new IllegalArgumentException( + "CRITICAL: app.encryption.password is too short (" + encryptionPass.length() + " chars). " + + "Requires minimum 32 characters for AES-256 security. " + + "Use a strong random password like: openssl rand -base64 24" + ); + } + + // Minsta entropy-krav för pepper: 16 tecken + if (pepper.length() < 16) { + throw new IllegalArgumentException( + "CRITICAL: app.encryption.pepper is too short (" + pepper.length() + " chars). " + + "Requires minimum 16 characters. Use: openssl rand -hex 8" + ); + } + } + + /** + * pass + pepper = key, delux kör dem genom en nyckelderivationsfunktion (PBKDF2) + * för att skapa den faktiska AES-nyckeln som vi sedan använder för att kunna låsa upp/låsa personnummer + * delux lägger till ett randomiserat salt-värde(Nonce/IV) vid varje anrop av encrypt + * delux-objektet lever i minnet under hela applikationens körning - Spring hanterar livscykeln som en singleton Bean, + * så det randomiserade saltet är unikt per kryptering och gör att samma input ger olika output varje gång + * Det håller internt koll på: + * encryptionPass + pepper → för att kunna kryptera/dekryptera och sätter in/plockar ut IV(saltet) vid encrypt/decrypt + * Logiken för att generera ett nytt slumpmässigt IV/salt vid varje encrypt()-anrop + * + * Säkerhet: Om password eller pepper är tom/svag kastas IllegalArgumentException vid startup (@PostConstruct) + */ @Bean public TextEncryptor textEncryptor() { - return Encryptors.text(encryptionKey, salt); + return Encryptors.delux(encryptionPass, pepper); } } diff --git a/src/main/java/org/example/cyberwatch/config/security/EncryptionService.java b/src/main/java/org/example/cyberwatch/config/security/EncryptionService.java index f614c89..e2c26c0 100644 --- a/src/main/java/org/example/cyberwatch/config/security/EncryptionService.java +++ b/src/main/java/org/example/cyberwatch/config/security/EncryptionService.java @@ -1,6 +1,10 @@ package org.example.cyberwatch.config.security; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import org.apache.commons.codec.digest.HmacAlgorithms; +import org.apache.commons.codec.digest.HmacUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.stereotype.Service; @@ -10,22 +14,62 @@ public class EncryptionService { private final TextEncryptor textEncryptor; + @Value("${app.encryption.hmac-key}") // En separat fast nyckel för sökning + private String hmacKey; + + //Fail-fas validering. Detta garanterar att blind indexet alltid är starkt och predictable duplicates är omöjliga. + @PostConstruct + public void validateHmacKey() { + if (hmacKey == null || hmacKey.isBlank()) { + throw new IllegalArgumentException( + "CRITICAL: app.encryption.hmac-key is missing or blank. " + + "Blind index security compromised. Set a strong, non-empty key in environment variables." + ); + } + + // Minsta entropy-krav: 16 tecken (128 bits) + if (hmacKey.length() < 16) { + throw new IllegalArgumentException( + "CRITICAL: app.encryption.hmac-key is too short (" + hmacKey.length() + " chars). " + + "Requires minimum 16 characters for adequate entropy. " + + "Use a strong random key like: " + + "openssl rand -hex 16" + ); + } + } + + /** + * Delegerar till delux som genererar ett unikt IV, krypterar med AES-256 nycken + * och returnerar IV + krypterad data som en enda sträng, det är denna som sedan lagras i databasen. + */ public String encrypt(String data) { return textEncryptor.encrypt(data); } + /** + * Delegerar till delux som plockar ut IV från strängen och dekrypterar + * med AES-256 för att återge klartexten. + */ public String decrypt(String data) { return textEncryptor.decrypt(data); } - public String maskLastFour(String encrypted) { - if (encrypted == null || encrypted.isBlank()) { + /** + * Skapar ett "Blind Index" (deterministisk hash, sökbar). + * Används för att jämföra om personnummer redan finns i databasen. + */ + public String hmac(String data) { + if (data == null) return null; + return new HmacUtils(HmacAlgorithms.HMAC_SHA_256, hmacKey).hmacHex(data); + } + + public String maskLastFour(String plainText) { + if (plainText == null || plainText.isBlank()) { return "****"; } - String decrypted = textEncryptor.decrypt(encrypted); - if (decrypted.length() <= 4) { + if (plainText.length() <= 4) { return "****"; } - return decrypted.substring(0, decrypted.length() - 4) + "****"; + return plainText.substring(0, plainText.length() - 4) + "****"; } } diff --git a/src/main/java/org/example/cyberwatch/features/form/dto/CreateEmploymentDTO.java b/src/main/java/org/example/cyberwatch/features/form/dto/CreateEmploymentDTO.java index 92a984b..40b16f2 100644 --- a/src/main/java/org/example/cyberwatch/features/form/dto/CreateEmploymentDTO.java +++ b/src/main/java/org/example/cyberwatch/features/form/dto/CreateEmploymentDTO.java @@ -18,7 +18,8 @@ public class CreateEmploymentDTO { @NotBlank(message = "Social security number cannot be blank") - @Pattern(regexp = "\\d{6}-\\d{4}|\\d{8}-\\d{4}", message = "Social security number must be in format YYMMDD-NNNN or YYYYMMDD-NNNN") + @Pattern(regexp = "^(19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])-\\d{4}$", + message = "Social security number must match format YYYYMMDD-NNNN") private String socialSecurityNumber; @NotBlank(message = "First name cannot be blank") diff --git a/src/main/java/org/example/cyberwatch/features/form/mapper/EmploymentMapper.java b/src/main/java/org/example/cyberwatch/features/form/mapper/EmploymentMapper.java index 164a54e..129735d 100644 --- a/src/main/java/org/example/cyberwatch/features/form/mapper/EmploymentMapper.java +++ b/src/main/java/org/example/cyberwatch/features/form/mapper/EmploymentMapper.java @@ -34,7 +34,7 @@ public EmploymentFormDTO toDTO(EmploymentForm entity) { public EmploymentForm toEntity(CreateEmploymentDTO dto) { if (dto == null) return null; EmploymentForm entity = new EmploymentForm(); - entity.setSocialSecurityNumber(dto.getSocialSecurityNumber()); + // Note: socialSecurityNumber and ssnHash are handled separately in service layer for encryption/hashing entity.setFirstName(dto.getFirstName()); entity.setLastName(dto.getLastName()); entity.setEmail(dto.getEmail()); @@ -49,7 +49,7 @@ public EmploymentForm toEntity(CreateEmploymentDTO dto) { public void updateEntity(UpdateEmploymentDTO dto, EmploymentForm entity) { if (dto == null || entity == null) return; - entity.setSocialSecurityNumber(dto.getSocialSecurityNumber()); + // Note: socialSecurityNumber and ssnHash are handled separately in service layer for encryption/hashing entity.setFirstName(dto.getFirstName()); entity.setLastName(dto.getLastName()); entity.setEmail(dto.getEmail()); @@ -61,7 +61,9 @@ public void updateEntity(UpdateEmploymentDTO dto, EmploymentForm entity) { public Staff formToStaff(EmploymentForm form) { if (form == null) return null; Staff staff = new Staff(); + // Copy already-encrypted SSN and its hash from approved form staff.setSocialSecurityNumber(form.getSocialSecurityNumber()); + staff.setSsnHash(form.getSsnHash()); staff.setFirstName(form.getFirstName()); staff.setLastName(form.getLastName()); staff.setEmail(form.getEmail()); diff --git a/src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java b/src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java index f8def19..9877bdf 100644 --- a/src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java +++ b/src/main/java/org/example/cyberwatch/features/form/model/EmploymentForm.java @@ -14,9 +14,6 @@ import java.time.LocalDateTime; -//Could it be an idea that the EmployeeForm needs to be approved by an Manager, like a signature on a paper form? -// Then we could have a status field in the EmployeeForm with the following states: -//DRAFT -> SUBMITTED -> APPROVED -> COMPLETED -> REJECTED @Getter @Setter @Entity @@ -27,10 +24,14 @@ public class EmploymentForm { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "social_security_number", nullable = false, unique = true) + @Column(name = "social_security_number", nullable = false) @NotBlank(message = "Social security number cannot be blank") private String socialSecurityNumber; + @Column(name = "ssn_hash", length = 64, nullable = false, unique = true) + @NotBlank(message = "SSN hash cannot be blank") + private String ssnHash; + @Column(name = "first_name") @NotBlank(message = "First name cannot be blank") @Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters") diff --git a/src/main/java/org/example/cyberwatch/features/form/repository/EmploymentFormRepository.java b/src/main/java/org/example/cyberwatch/features/form/repository/EmploymentFormRepository.java index dcf002b..d715f23 100644 --- a/src/main/java/org/example/cyberwatch/features/form/repository/EmploymentFormRepository.java +++ b/src/main/java/org/example/cyberwatch/features/form/repository/EmploymentFormRepository.java @@ -11,5 +11,6 @@ public interface EmploymentFormRepository extends JpaRepository { List findByStatus(ApprovalStatus status); + boolean existsBySsnHash(String ssnHash); boolean existsBySocialSecurityNumber(String socialSecurityNumber); } diff --git a/src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java b/src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java index 483be5b..2a01246 100644 --- a/src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java +++ b/src/main/java/org/example/cyberwatch/features/form/service/EmploymentFormService.java @@ -55,42 +55,39 @@ public EmploymentFormDTO createForm(CreateEmploymentDTO form, Staff hrStaff) { validateSsnNotExists(form.getSocialSecurityNumber()); - //NOTE: Set HR based on logged in HR-staff EmploymentForm formEntity = employmentMapper.toEntity(form); - // Set default status to PENDING formEntity.setStatus(ApprovalStatus.PENDING); formEntity.setCreatedBy(hrStaff); - formEntity.setSocialSecurityNumber( - encryptionService.encrypt(form.getSocialSecurityNumber()) - ); + String encryptedSsn = encryptionService.encrypt(form.getSocialSecurityNumber()); + formEntity.setSocialSecurityNumber(encryptedSsn); + formEntity.setSsnHash(encryptionService.hmac(form.getSocialSecurityNumber())); EmploymentForm savedForm = employmentFormRepository.save(formEntity); logger.info("New employment form created with ID: {} by HR staffId={}", savedForm.getId(), hrStaff.getId()); - return toSafeDto(savedForm); + return applySsnPolicy(savedForm); } @PreAuthorize("hasAnyRole('HR', 'CEO', 'CTO', 'ADMIN')") public List getFormsByFilterApproval(ApprovalStatus status) { - // Om status är angiven (t.ex. PENDING eller APPROVED), filtrera på den + List forms; if (status != null) { - return employmentFormRepository.findByStatus(status) - .stream() - .map(employmentMapper::toDTO) - .toList(); + forms = employmentFormRepository.findByStatus(status); + } else { + forms = employmentFormRepository.findAll(); } - return getAllForms(); + // Använd din nya policy-metod på varje element i listan! + return forms.stream() + .map(this::applySsnPolicy) + .toList(); } - private List getAllForms() { - return employmentMapper.toDTOList(employmentFormRepository.findAll()); - } // Get a single form by ID @PreAuthorize("hasAnyRole('HR', 'CEO', 'CTO', 'ADMIN')") public EmploymentFormDTO getFormById(Long formId) { - return toSafeDto(findFormById(formId)); + return applySsnPolicy(findFormById(formId)); } // Update form before approval (only PENDING forms can be updated) @@ -124,12 +121,13 @@ public EmploymentFormDTO updateFormBeforeApproval(Long formId, UpdateEmploymentD if (!existingSsnPlain.equals(newSsnPlain)) { validateSsnNotExists(newSsnPlain); existingForm.setSocialSecurityNumber(encryptionService.encrypt(newSsnPlain)); + existingForm.setSsnHash(encryptionService.hmac(newSsnPlain)); } employmentMapper.updateEntity(updatedForm, existingForm); logger.info("Form {} updated by staffId={}", formId, loggedInHr.getId()); - return toSafeDto(employmentFormRepository.save(existingForm)); + return applySsnPolicy(employmentFormRepository.save(existingForm)); } // Reject a form (only PENDING forms can be rejected, and only by management) @@ -235,31 +233,23 @@ private String generateSecurePassword() { } private void validateSsnNotExists(String ssn) { + String ssnHash = encryptionService.hmac(ssn); - // TODO: Replace with SSN hash lookup when database schema is updated - // Current workaround: encrypt before searching (requires double encryption in createForm which is costly) - // Check employment forms by decrypting and comparing - List existingForms = employmentFormRepository.findAll(); - for (EmploymentForm form : existingForms) { - String decryptedSsn = encryptionService.decrypt(form.getSocialSecurityNumber()); - if (decryptedSsn.equals(ssn)) { - throw new IllegalStateException("An application with this SSN already exists."); - } + // Kontrollera EmploymentForm + if (employmentFormRepository.existsBySsnHash(ssnHash)) { + throw new IllegalStateException("An application with this SSN already exists."); } - // Check staff by decrypting and comparing - List existingStaff = staffRepository.findAll(); - for (Staff staff : existingStaff) { - String decryptedSsn = encryptionService.decrypt(staff.getSocialSecurityNumber()); - if (decryptedSsn.equals(ssn)) { - throw new IllegalStateException("An employee with this SSN already exists."); - } + // Kontrollera Staff + if (staffRepository.existsBySsnHash(ssnHash)) { + throw new IllegalStateException("An employee with this SSN already exists."); } } - private EmploymentFormDTO toSafeDto(EmploymentForm form) { + private EmploymentFormDTO applySsnPolicy(EmploymentForm form) { EmploymentFormDTO dto = employmentMapper.toDTO(form); - dto.setSocialSecurityNumber(encryptionService.maskLastFour(form.getSocialSecurityNumber())); + String decrypted = encryptionService.decrypt(form.getSocialSecurityNumber()); + dto.setSocialSecurityNumber(decrypted); return dto; } diff --git a/src/main/java/org/example/cyberwatch/features/staff/model/Staff.java b/src/main/java/org/example/cyberwatch/features/staff/model/Staff.java index 7a3d4e9..51a0936 100644 --- a/src/main/java/org/example/cyberwatch/features/staff/model/Staff.java +++ b/src/main/java/org/example/cyberwatch/features/staff/model/Staff.java @@ -19,10 +19,14 @@ public class Staff { @Column(name = "employee_id") private Long id; - @Column(name = "social_security_number", nullable = false, unique = true) + @Column(name = "social_security_number", nullable = false) @NotBlank(message = "Social security number cannot be blank") private String socialSecurityNumber; + @Column(name = "ssn_hash", length = 64, nullable = false, unique = true) + @NotBlank(message = "SSN hash cannot be blank") + private String ssnHash; + @Column(name = "first_name") @NotBlank(message = "First name cannot be blank") @Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters") diff --git a/src/main/java/org/example/cyberwatch/features/staff/model/StaffMapper.java b/src/main/java/org/example/cyberwatch/features/staff/model/StaffMapper.java index e53ac62..fe83be0 100644 --- a/src/main/java/org/example/cyberwatch/features/staff/model/StaffMapper.java +++ b/src/main/java/org/example/cyberwatch/features/staff/model/StaffMapper.java @@ -24,6 +24,8 @@ public StaffDTO toDto(Staff entity) { public void updateEntity(UpdateStaffDTO dto, Staff entity) { if (dto == null || entity == null) return; + + // Note: socialSecurityNumber is handled separately in StaffService for encryption/hashing entity.setFirstName(dto.getFirstName()); entity.setLastName(dto.getLastName()); entity.setEmail(dto.getEmail()); diff --git a/src/main/java/org/example/cyberwatch/features/staff/model/UpdateStaffDTO.java b/src/main/java/org/example/cyberwatch/features/staff/model/UpdateStaffDTO.java index d23506f..06ae79f 100644 --- a/src/main/java/org/example/cyberwatch/features/staff/model/UpdateStaffDTO.java +++ b/src/main/java/org/example/cyberwatch/features/staff/model/UpdateStaffDTO.java @@ -22,6 +22,10 @@ public class UpdateStaffDTO { @Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters") private String lastName; + @Pattern(regexp = "^(19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])-\\d{4}$", + message = "Social security number must match format YYYYMMDD-NNNN") + private String socialSecurityNumber; + @Email(message = "Email should be valid") @NotBlank(message = "Email cannot be blank") private String email; diff --git a/src/main/java/org/example/cyberwatch/features/staff/repository/StaffRepository.java b/src/main/java/org/example/cyberwatch/features/staff/repository/StaffRepository.java index 27054d5..9448b21 100644 --- a/src/main/java/org/example/cyberwatch/features/staff/repository/StaffRepository.java +++ b/src/main/java/org/example/cyberwatch/features/staff/repository/StaffRepository.java @@ -13,6 +13,8 @@ public interface StaffRepository extends JpaRepository { boolean existsBySocialSecurityNumber(String socialSecurityNumber); + boolean existsBySsnHash(String ssnHash); + Optional findByEmail(String email); List findByRole(Role role); diff --git a/src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java b/src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java index 5783727..92e7fa1 100644 --- a/src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java +++ b/src/main/java/org/example/cyberwatch/features/staff/service/StaffService.java @@ -50,7 +50,7 @@ public StaffDTO getUserStaff(Staff user) { if (user == null) { throw new IllegalArgumentException("Staff cannot be null"); } - return toMaskedDto(user); + return toDtoWithSsnPolicy(user, user); } @PreAuthorize("hasAnyRole('HR', 'ADMIN')") @@ -71,6 +71,16 @@ public StaffDTO updateStaff(Long staffId, UpdateStaffDTO dto, Staff requester) { }); } + // Hantera SSN separat för kryptering och hashning + String existingSsnPlain = encryptionService.decrypt(existingStaff.getSocialSecurityNumber()); + String newSsnPlain = dto.getSocialSecurityNumber(); + + if (!existingSsnPlain.equals(newSsnPlain)) { + validateSsnNotExists(newSsnPlain); + existingStaff.setSocialSecurityNumber(encryptionService.encrypt(newSsnPlain)); + existingStaff.setSsnHash(encryptionService.hmac(newSsnPlain)); + } + staffMapper.updateEntity(dto, existingStaff); Staff savedStaff = staffRepository.save(existingStaff); @@ -89,6 +99,7 @@ public void deleteStaff(Long staffId) { logger.info("Staff {} deleted", staffId); } + public List getStaffByRoleOrDepartment(Role role, Department department, Staff requester) { List staffList; @@ -124,23 +135,34 @@ public StaffDTO updateStatus(Long staffId, String status, Staff requester) { return toDtoWithSsnPolicy(savedStaff, requester); // Returnerar maskat SSN efter statusuppdatering } + private void validateSsnNotExists(String ssn) { + String ssnHash = encryptionService.hmac(ssn); + + // Kontrollera om SSN redan finns i Staff + if (staffRepository.existsBySsnHash(ssnHash)) { + throw new IllegalStateException("An employee with this SSN already exists."); + } + } // ADMIN/HR får se hela, andra får se maskat. private StaffDTO toDtoWithSsnPolicy(Staff staff, Staff requester) { StaffDTO dto = staffMapper.toDto(staff); String rawEncryptedSsn = staff.getSocialSecurityNumber(); - if (requester != null && (requester.getRole() == Role.ADMIN || requester.getRole() == Role.HR)) { - dto.setSocialSecurityNumber(encryptionService.decrypt(rawEncryptedSsn)); + String plainText = encryptionService.decrypt(rawEncryptedSsn); + + boolean isPrivileged = requester != null && ( + requester.getRole() == Role.ADMIN || + requester.getRole() == Role.HR || + Objects.equals(requester.getId(), staff.getId()) + ); + + if (isPrivileged) { + dto.setSocialSecurityNumber(plainText); } else { - dto.setSocialSecurityNumber(encryptionService.maskLastFour(rawEncryptedSsn)); + dto.setSocialSecurityNumber(encryptionService.maskLastFour(plainText)); } return dto; } - private StaffDTO toMaskedDto(Staff staff) { - StaffDTO dto = staffMapper.toDto(staff); - dto.setSocialSecurityNumber(encryptionService.maskLastFour(staff.getSocialSecurityNumber())); - return dto; - } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 405af20..254d7f4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,8 +17,9 @@ app.s3.allowed-origins=http://localhost:8080 spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} spring.security.oauth2.client.registration.google.scope=email,profile -app.encryption.key=${ENCRYPTION_KEY} -app.encryption.salt=${ENCRYPTION_SALT} +app.encryption.password=${ENCRYPTION_PASSWORD} +app.encryption.pepper=${ENCRYPTION_PEPPER} +app.encryption.hmac-key=${APP_ENCRYPTION_HMAC_KEY} management.endpoints.web.exposure.include=logfile management.endpoint.logfile.enabled=true diff --git a/src/main/resources/db/migration/V12__add_ssn_hash.sql b/src/main/resources/db/migration/V12__add_ssn_hash.sql new file mode 100644 index 0000000..fe4b54f --- /dev/null +++ b/src/main/resources/db/migration/V12__add_ssn_hash.sql @@ -0,0 +1,12 @@ +ALTER TABLE staff + ADD COLUMN ssn_hash VARCHAR(64) NOT NULL; +ALTER TABLE employment_form + ADD COLUMN ssn_hash VARCHAR(64) NOT NULL; + +--För att snabbt och enkelt kunna söka och jämföra i databasen +CREATE UNIQUE INDEX idx_staff_ssn_hash ON staff (ssn_hash); +CREATE UNIQUE INDEX idx_form_ssn_hash ON employment_form (ssn_hash); + +--Uniqueness sköts via ssn_hash istället +ALTER TABLE staff DROP CONSTRAINT IF EXISTS uc_staff_social_security_number; +ALTER TABLE employment_form DROP CONSTRAINT IF EXISTS uc_employmentform_social_security_number; diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index 295cdad..d28d446 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -215,6 +215,63 @@ input, select, textarea { border: 1px solid var(--border); } +/* SSN style */ +.ssn-wrapper { + display: inline-flex; + align-items: center; + gap: 12px; + padding: 4px 10px; + background: #f1f5f9; + border-radius: 8px; +} + +.ssn-display-row { + display: flex; + align-items: center; /* Centrerar "SSN:" vertikalt med boxen */ + gap: 8px; /* Avståndet mellan texten och den grå boxen */ + margin-top: 0.5rem; +} + +.ssn-label { + font-weight: 700; /* Samma fetstil som "Status:" */ + color: #1e293b; + min-width: 45px; /* Valfritt: ger alla etiketter samma startpunkt om du vill */ +} + +.ssn-placeholder { + color: #94a3b8; + letter-spacing: 2px; + font-weight: bold; +} + +.ssn-value { + font-family: 'Courier New', monospace; + color: #1e293b; + font-size: 0.95rem; +} + +.btn-ssn-action { + background: white; + border: 1px solid #cbd5e0; + border-radius: 6px; + padding: 4px 12px; + font-size: 12px; + font-weight: 600; + color: #475569; + cursor: pointer; + text-transform: uppercase; +} + +.btn-ssn-action:hover { + background: #f8fafc; + border-color: #94a3b8; +} + +.btn-ssn-action.active { + background: #f1f5f9; + border-color: #94a3b8; +} + /* Login */ .login-body { background: radial-gradient(circle at center, #065f46, #042f24); diff --git a/src/main/resources/static/js/employment.js b/src/main/resources/static/js/employment.js index d816875..d908640 100644 --- a/src/main/resources/static/js/employment.js +++ b/src/main/resources/static/js/employment.js @@ -20,6 +20,20 @@ async function initEmploymentPage() { } } +function getInitialSsnHtml(id, type) { + return ` +
+ SSN: +
+ ••••••-•••• + +
+
+ `; +} + function setupEmploymentPageButtons() { const openBtn = document.getElementById("openCreateEmploymentBtn"); const cancelBtn = document.getElementById("cancelCreateEmploymentBtn"); @@ -120,17 +134,64 @@ function renderEmploymentStaffList() {
-

ID: ${staff.id}

Email: ${escapeHtml(staff.email || "-")}

-

Telefon: ${escapeHtml(staff.phoneNumber || "-")}

+

Phone: ${escapeHtml(staff.phoneNumber || "-")}

Department: ${escapeHtml(staff.department || "-")}

-

Personnummer: ${escapeHtml(staff.socialSecurityNumber || "-")}

Status: ${escapeHtml(staff.status || "OFFLINE")}

+ +
+ ${getInitialSsnHtml(staff.id, 'staff')} +
`).join(""); } +async function toggleSsnView(id, type) { + // Bestäm prefix och endpoint baserat på typen + const prefix = (type === 'staff') ? 'ssn' : 'form-ssn'; + const endpoint = (type === 'staff') ? 'staff' : 'forms'; + + const container = document.getElementById(`${prefix}-container-${id}`); + if (!container) return; + + container.innerHTML = "Laddar..."; + + try { + const res = await apiFetch(`/${endpoint}/${id}`); + if (!res.ok) throw new Error("Behörighet saknas"); + + const data = await res.json(); + + container.innerHTML = ` +
+ SSN: +
+ ${escapeHtml(data.socialSecurityNumber)} + +
+
+`; + + // Maskera automatiskt efter 10 sekunder + setTimeout(() => resetSsnView(id, type), 10000); + + } catch (e) { + container.innerHTML = "Kunde inte hämta data."; + console.error(e); + } +} + +function resetSsnView(id, type) { + const prefix = (type === 'staff') ? 'ssn' : 'form-ssn'; + const container = document.getElementById(`${prefix}-container-${id}`); + if (container) { + container.innerHTML = getInitialSsnHtml(id, type); + } +} + async function submitEmploymentForm(e) { e.preventDefault(); @@ -200,11 +261,13 @@ async function loadPendingEmploymentForms() {

Email: ${escapeHtml(form.email)}

-

Telefon: ${escapeHtml(form.phoneNumber)}

-

Roll: ${escapeHtml(form.role)}

+

Phone: ${escapeHtml(form.phoneNumber)}

+

Role: ${escapeHtml(form.role)}

Department: ${escapeHtml(form.department)}

-

Personnummer: ${escapeHtml(form.socialSecurityNumber)}

-
+
+ ${getInitialSsnHtml(form.id, 'form')} +
+
${canApprove ? ` diff --git a/src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java b/src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java index b8601b0..60bccce 100644 --- a/src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java +++ b/src/test/java/org/example/cyberwatch/features/form/service/EmploymentFormServiceTest.java @@ -24,7 +24,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import tools.jackson.databind.ObjectMapper; -import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @@ -66,6 +65,7 @@ void createForm_Success() { // Arrange String rawSsn = "19900101-1234"; String encryptedSsn = "krypterat-ssn"; + String ssnHash = "abc-123-hash"; CreateEmploymentDTO dto = new CreateEmploymentDTO( rawSsn, "Alice", "Andersson", @@ -73,9 +73,12 @@ void createForm_Success() { ); Staff hrStaff = createHrStaff(); EmploymentForm entity = new EmploymentForm(); + entity.setId(1L); + + when(encryptionService.hmac(rawSsn)).thenReturn(ssnHash); + when(formRepository.existsBySsnHash(ssnHash)).thenReturn(false); + when(staffRepository.existsBySsnHash(ssnHash)).thenReturn(false); - when(formRepository.findAll()).thenReturn(List.of()); - when(staffRepository.findAll()).thenReturn(List.of()); when(encryptionService.encrypt(rawSsn)).thenReturn(encryptedSsn); when(mapper.toEntity(dto)).thenReturn(entity); when(formRepository.save(any(EmploymentForm.class))).thenReturn(entity); @@ -176,22 +179,22 @@ void approveAndFinalize_Success() throws Exception { @DisplayName("Should throw exception if SSN already exists in form repository") void createForm_DuplicateSsnInForms_ThrowsException() { // Arrange - Staff creator = new Staff(); - creator.setEmail("owner@cyberwatch.local"); + String rawSsn = "19900101-1234"; + String ssnHash = "existing-staff-hash"; CreateEmploymentDTO dto = new CreateEmploymentDTO( - "19900101-1234", "Alice", "Andersson", + rawSsn, "Alice", "Andersson", "alice@test.com", "070", Role.HR, Department.BACKEND, null, null, null ); - EmploymentForm existing = new EmploymentForm(); - existing.setSocialSecurityNumber("krypterat-ssn"); + Staff creator = createHrStaff(); - when(formRepository.findAll()).thenReturn(List.of(existing)); - when(encryptionService.decrypt("krypterat-ssn")).thenReturn("19900101-1234"); + when(encryptionService.hmac(rawSsn)).thenReturn(ssnHash); + when(formRepository.existsBySsnHash(ssnHash)).thenReturn(true); // Finns redan i forms // Act & Assert assertThrows(IllegalStateException.class, () -> service.createForm(dto, creator) ); + verify(formRepository, never()).save(any()); } @@ -199,20 +202,17 @@ void createForm_DuplicateSsnInForms_ThrowsException() { @DisplayName("Should throw exception if SSN already exists in staff repository") void createForm_DuplicateSsnInStaff_ThrowsException() { // Arrange - Staff creator = new Staff(); String rawSsn = "19900101-1234"; - + String ssnHash = "existing-staff-hash"; CreateEmploymentDTO dto = new CreateEmploymentDTO( rawSsn, "Alice", "Andersson", "alice@test.com", "070", Role.HR, Department.BACKEND, null, null, null ); + Staff creator = createHrStaff(); - Staff existingStaff = new Staff(); - existingStaff.setSocialSecurityNumber("krypterat-ssn"); - - when(formRepository.findAll()).thenReturn(List.of());//no match - when(staffRepository.findAll()).thenReturn(List.of(existingStaff)); - when(encryptionService.decrypt("krypterat-ssn")).thenReturn("19900101-1234"); + when(encryptionService.hmac(rawSsn)).thenReturn(ssnHash); + when(formRepository.existsBySsnHash(ssnHash)).thenReturn(false); // Finns inte i forms + when(staffRepository.existsBySsnHash(ssnHash)).thenReturn(true); // Finns i staff // Act & Assert assertThrows(IllegalStateException.class, () -> diff --git a/src/test/java/org/example/cyberwatch/features/staff/service/StaffServiceTest.java b/src/test/java/org/example/cyberwatch/features/staff/service/StaffServiceTest.java index d3a3cc7..d8a339e 100644 --- a/src/test/java/org/example/cyberwatch/features/staff/service/StaffServiceTest.java +++ b/src/test/java/org/example/cyberwatch/features/staff/service/StaffServiceTest.java @@ -86,16 +86,18 @@ void updateStatus_ShouldUpdateAndReturnStaffDTO() { @Test @DisplayName("Should return masked SSN for non-admin") void getStaffById_NonAdminOrHr_ReturnsMaskedSsn() { - newStaff.setRole(Role.CONSULTANT); + Staff requester = new Staff(); + requester.setId(99L); + requester.setRole(Role.CONSULTANT); + when(staffRepository.findById(1L)).thenReturn(Optional.of(newStaff)); when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); - when(encryptionService.maskLastFour("krypterat-ssn")).thenReturn("19900101-****"); + when(encryptionService.decrypt("krypterat-ssn")).thenReturn("19900101-1234"); + when(encryptionService.maskLastFour("19900101-1234")).thenReturn("19900101-****"); - StaffDTO result = staffService.getStaffById(1L, newStaff); + StaffDTO result = staffService.getStaffById(1L, requester); assertThat(result.getSocialSecurityNumber()).isEqualTo("19900101-****"); - verify(encryptionService).maskLastFour("krypterat-ssn"); - verify(encryptionService, never()).decrypt(any()); } @Test @@ -133,10 +135,13 @@ void getStaffById_NullId() { @DisplayName("Should update and return DTO when valid input") void updateStaff_Valid() { newStaff.setRole(Role.ADMIN); - UpdateStaffDTO dto = new UpdateStaffDTO("Anna", "Nilsson", + String rawSsn = "19900101-1234"; + UpdateStaffDTO dto = new UpdateStaffDTO("Anna", "Nilsson", rawSsn, "anna@cyberwatch.se", "0701234567", Role.HR, Department.BACKEND); when(staffRepository.findById(1L)).thenReturn(Optional.of(newStaff)); + when(encryptionService.decrypt("krypterat-ssn")).thenReturn(rawSsn); + when(staffRepository.save(newStaff)).thenReturn(newStaff); when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); @@ -179,26 +184,34 @@ void deleteStaff_NotFound() { @Test @DisplayName("Should filter by role and mask SSN for non-admin or hr") void getStaffByRoleOrDepartment_FilterByRole_MaskedSsn() { - newStaff.setRole(Role.CEO); + Staff requester = new Staff(); + requester.setRole(Role.CONSULTANT); + when(staffRepository.findByRole(Role.HR)).thenReturn(List.of(newStaff)); when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); - when(encryptionService.maskLastFour("krypterat-ssn")).thenReturn("19900101-****"); - List result = staffService.getStaffByRoleOrDepartment(Role.HR, null, newStaff); + // FIX: Nu behövs dessa även i list-vyn + when(encryptionService.decrypt("krypterat-ssn")).thenReturn("19900101-1234"); + when(encryptionService.maskLastFour("19900101-1234")).thenReturn("19900101-****"); + + List result = staffService.getStaffByRoleOrDepartment(Role.HR, null, requester); assertThat(result).hasSize(1); - verify(staffRepository, times(1)).findByRole(Role.HR); - verify(staffRepository, never()).findByDepartment(any()); + assertThat(result.getFirst().getSocialSecurityNumber()).isEqualTo("19900101-****"); } @Test @DisplayName("Should filter by role and decrypt SSN for admin") void getStaffByRoleOrDepartment_FilterByRole_DecryptedSsn() { + Staff requester = new Staff(); + requester.setRole(Role.ADMIN); + when(staffRepository.findByRole(Role.HR)).thenReturn(List.of(newStaff)); when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); + when(encryptionService.decrypt("krypterat-ssn")).thenReturn("19900101-1234"); - List result = staffService.getStaffByRoleOrDepartment(Role.HR, null, newStaff); + List result = staffService.getStaffByRoleOrDepartment(Role.HR, null, requester); assertThat(result).hasSize(1); assertThat(result.getFirst().getSocialSecurityNumber()).isEqualTo("19900101-1234"); @@ -237,13 +250,18 @@ void getStaffByRoleOrDepartment_NoFilter() { @Test @DisplayName("Should return all staff when no filter with masked last four") void getStaffByRoleOrDepartment_NoFilter_MaskedFour() { - newStaff.setRole(Role.PROJECT_MANAGER); + Staff requester = new Staff(); + requester.setRole(Role.PROJECT_MANAGER); when(staffRepository.findAll()).thenReturn(List.of(newStaff)); when(staffMapper.toDto(newStaff)).thenReturn(newStaffDTO); - when(encryptionService.maskLastFour("krypterat-ssn")).thenReturn("19900101-****"); - List result = staffService.getStaffByRoleOrDepartment(null, null, newStaff); + + when(encryptionService.decrypt("krypterat-ssn")).thenReturn("19900101-1234"); + when(encryptionService.maskLastFour("19900101-1234")).thenReturn("19900101-****"); + + List result = staffService.getStaffByRoleOrDepartment(null, null, requester); assertThat(result).hasSize(1); + assertThat(result.getFirst().getSocialSecurityNumber()).isEqualTo("19900101-****"); verify(staffRepository).findAll(); } } diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 47e7380..d078224 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -1,5 +1,6 @@ -ENCRYPTION_KEY=testencryptionkey1234567890123456 -ENCRYPTION_SALT=0123456789abcdef +app.encryption.password=testencryptionkey1234567890123456 +app.encryption.pepper=0123456789abcdef +app.encryption.hmac-key=0123456789abcdef0123456789abcdef S3_ENDPOINT=http://localhost:9002 S3_BUCKET=test-bucket S3_ACCESS_KEY=minioadmin