Skip to content

Commit 50c1a09

Browse files
Release/13.3 (#335)
* Release/13.2 (#332) * Update GrantMandatoryQuestionFundingLocation enum to replace 'OUTSIDE_UK' with 'INTERNATIONAL' * Update GrantMandatoryQuestionFundingLocation enum to replace 'INTERNATIONAL' with 'OUTSIDE_UK' * Add submission anonymisation feature - Introduced SubmissionAnonymisationConfigProperties for configuration settings. - Added EXPIRED status to SubmissionStatus enum. - Enhanced GrantAttachmentRepository and GrantMandatoryQuestionRepository with delete methods for submissions. - Updated SubmissionRepository with methods for anonymising submissions and deleting related data. - Implemented SubmissionAnonymisationScheduler to handle scheduled anonymisation of submissions. - Created SubmissionAnonymisationService to manage the anonymisation process, including S3 object deletion and database cleanup. - Added application properties for submission anonymisation configuration. - Created database migration to document the new EXPIRED status in the submission table. * Refactor SubmissionAnonymisationConfigProperties - Removed Lombok annotations: @builder, @AllArgsConstructor, and @NoArgsConstructor. - Simplified the class by retaining only @Getter and @Setter annotations. - Adjusted the default value for daysBeforeExpiry to 90, ensuring clarity in configuration settings. * Enhance S3Service to support deletion of attachments using S3 URI - Updated deleteAttachment method to accept an S3 URI instead of just the object key. - Extracted bucket name and key from the S3 URI for improved flexibility. - Added logging to indicate which bucket and object are being deleted. - Ensured deletion from both the specified bucket and the attachments bucket. * Refactor SubmissionAnonymisationService to improve S3 deletion handling - Updated the S3 deletion logic to abort anonymisation if any deletion fails, ensuring the submission remains in IN_PROGRESS for retry. - Enhanced logging to provide clearer context on failures during S3 object deletion, improving traceability and error handling.
1 parent d73c331 commit 50c1a09

10 files changed

Lines changed: 203 additions & 3 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package gov.cabinetoffice.gap.adminbackend.config;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
import org.springframework.context.annotation.Configuration;
7+
8+
@Getter
9+
@Setter
10+
@Configuration
11+
@ConfigurationProperties(prefix = "submission-anonymisation-scheduler")
12+
public class SubmissionAnonymisationConfigProperties {
13+
14+
private int daysBeforeExpiry = 90;
15+
16+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package gov.cabinetoffice.gap.adminbackend.enums;
22

33
public enum SubmissionStatus {
4-
IN_PROGRESS, SUBMITTED, GRANT_CLOSED
4+
IN_PROGRESS, SUBMITTED, GRANT_CLOSED, EXPIRED
55
}

src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/GrantAttachmentRepository.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@
33
import gov.cabinetoffice.gap.adminbackend.entities.GrantAttachment;
44
import org.springframework.data.jpa.repository.JpaRepository;
55
import org.springframework.stereotype.Repository;
6+
import org.springframework.transaction.annotation.Transactional;
67

8+
import java.util.List;
79
import java.util.UUID;
810

911
@Repository
1012
public interface GrantAttachmentRepository extends JpaRepository<GrantAttachment, UUID> {
13+
1114
boolean existsBySubmissionId(UUID id);
15+
16+
List<GrantAttachment> findBySubmission_Id(UUID submissionId);
17+
18+
@Transactional
19+
void deleteBySubmission_Id(UUID submissionId);
20+
1221
}

src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/GrantMandatoryQuestionRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import gov.cabinetoffice.gap.adminbackend.entities.GrantMandatoryQuestions;
44
import org.springframework.data.jpa.repository.JpaRepository;
55
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.transaction.annotation.Transactional;
67

78
import java.util.List;
89
import java.util.UUID;
@@ -35,4 +36,7 @@ public interface GrantMandatoryQuestionRepository extends JpaRepository<GrantMan
3536
+ "and g.submission.status = 'SUBMITTED'")
3637
boolean existBySchemeIdAndCompletedStatusAndSubmittedSubmission(Integer id);
3738

39+
@Transactional
40+
void deleteBySubmission_Id(UUID submissionId);
41+
3842
}

src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/SubmissionRepository.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.springframework.stereotype.Repository;
1212

1313
import java.time.Instant;
14+
import java.time.LocalDateTime;
1415
import java.util.List;
1516
import java.util.Optional;
1617
import java.util.UUID;
@@ -36,4 +37,32 @@ void updateLastRequiredChecksExportByGrantApplicationIdAndStatus(Instant lastReq
3637
void updateLastRequiredChecksExportBySchemeIdAndStatus(Instant lastRequiredChecksExport, Integer id,
3738
SubmissionStatus status);
3839

40+
List<Submission> findByStatusAndLastUpdatedBefore(SubmissionStatus status, LocalDateTime cutoff);
41+
42+
@Transactional
43+
@Modifying
44+
@Query(value = """
45+
UPDATE grant_submission
46+
SET status = 'EXPIRED',
47+
definition = NULL,
48+
submission_name = NULL,
49+
gap_id = NULL,
50+
applicant_id = NULL,
51+
created_by = NULL,
52+
last_updated_by = NULL,
53+
last_updated = :now
54+
WHERE id IN :ids
55+
""", nativeQuery = true)
56+
void anonymiseSubmissions(@Param("ids") List<UUID> ids, @Param("now") LocalDateTime now);
57+
58+
@Transactional
59+
@Modifying
60+
@Query(value = "DELETE FROM grant_beneficiary WHERE submission_id IN :ids", nativeQuery = true)
61+
void deleteBeneficiaryRowsBySubmissionIds(@Param("ids") List<UUID> ids);
62+
63+
@Transactional
64+
@Modifying
65+
@Query(value = "DELETE FROM diligence_check WHERE submission_id IN :ids", nativeQuery = true)
66+
void deleteDiligenceCheckRowsBySubmissionIds(@Param("ids") List<UUID> ids);
67+
3968
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package gov.cabinetoffice.gap.adminbackend.schedulers;
2+
3+
import gov.cabinetoffice.gap.adminbackend.config.SubmissionAnonymisationConfigProperties;
4+
import gov.cabinetoffice.gap.adminbackend.entities.Submission;
5+
import gov.cabinetoffice.gap.adminbackend.enums.SubmissionStatus;
6+
import gov.cabinetoffice.gap.adminbackend.repositories.SubmissionRepository;
7+
import gov.cabinetoffice.gap.adminbackend.services.SubmissionAnonymisationService;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.log4j.Log4j2;
10+
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
12+
import org.springframework.scheduling.annotation.Scheduled;
13+
import org.springframework.stereotype.Service;
14+
15+
import java.time.LocalDateTime;
16+
import java.util.List;
17+
18+
@Log4j2
19+
@Service
20+
@RequiredArgsConstructor
21+
@ConditionalOnProperty(name = "submission-anonymisation-scheduler.enabled", havingValue = "true")
22+
public class SubmissionAnonymisationScheduler {
23+
24+
private final SubmissionRepository submissionRepository;
25+
26+
private final SubmissionAnonymisationService submissionAnonymisationService;
27+
28+
private final SubmissionAnonymisationConfigProperties config;
29+
30+
@Scheduled(cron = "${submission-anonymisation-scheduler.cronExpression:0 0 3 * * ?}", zone = "UTC")
31+
@SchedulerLock(name = "submissionAnonymisation_anonymiseInactiveSubmissions",
32+
lockAtMostFor = "${submission-anonymisation-scheduler.lock.atMostFor:30m}",
33+
lockAtLeastFor = "${submission-anonymisation-scheduler.lock.atLeastFor:5m}")
34+
public void anonymiseInactiveSubmissions() {
35+
final LocalDateTime cutoff = LocalDateTime.now().minusDays(config.getDaysBeforeExpiry());
36+
37+
log.info("Submission anonymisation scheduler started. Anonymising IN_PROGRESS submissions last updated before {}",
38+
cutoff);
39+
40+
final List<Submission> dueForAnonymisation = submissionRepository
41+
.findByStatusAndLastUpdatedBefore(SubmissionStatus.IN_PROGRESS, cutoff);
42+
43+
log.info("Found {} submission(s) to anonymise", dueForAnonymisation.size());
44+
45+
dueForAnonymisation
46+
.forEach(submission -> submissionAnonymisationService.anonymiseSubmission(submission.getId()));
47+
48+
log.info("Submission anonymisation scheduler completed successfully.");
49+
}
50+
51+
}

src/main/java/gov/cabinetoffice/gap/adminbackend/services/S3Service.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.amazonaws.HttpMethod;
44
import com.amazonaws.services.s3.AmazonS3;
55
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
6+
import com.amazonaws.services.s3.AmazonS3URI;
7+
import com.amazonaws.services.s3.model.DeleteObjectRequest;
68
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
79
import lombok.RequiredArgsConstructor;
810
import org.springframework.beans.factory.annotation.Value;
@@ -18,13 +20,24 @@
1820
public class S3Service {
1921

2022
@Value("${cloud.aws.s3.submissions-export-bucket-name}")
21-
private String attachmentsBucket;
23+
private String exportBucketName;
24+
25+
@Value("${aws.attachmentsBucket}")
26+
private String attachmentsBucketName;
2227

2328
private final AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();
2429

30+
public void deleteAttachment(String location) {
31+
final AmazonS3URI s3Uri = new AmazonS3URI(location);
32+
final String key = s3Uri.getKey();
33+
log.info("Deleting S3 object {} from bucket {}", key, s3Uri.getBucket());
34+
s3Client.deleteObject(new DeleteObjectRequest(s3Uri.getBucket(), key));
35+
s3Client.deleteObject(new DeleteObjectRequest(attachmentsBucketName, key));
36+
}
37+
2538
public String generateExportDocSignedUrl(String objectKey) {
2639
int linkTimeoutDuration = 604800;
27-
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(attachmentsBucket,
40+
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(exportBucketName,
2841
objectKey).withMethod(HttpMethod.GET)
2942
.withExpiration(Date.from(Instant.now().plusSeconds(linkTimeoutDuration)));
3043

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package gov.cabinetoffice.gap.adminbackend.services;
2+
3+
import gov.cabinetoffice.gap.adminbackend.entities.GrantAttachment;
4+
import gov.cabinetoffice.gap.adminbackend.repositories.GrantAttachmentRepository;
5+
import gov.cabinetoffice.gap.adminbackend.repositories.GrantMandatoryQuestionRepository;
6+
import gov.cabinetoffice.gap.adminbackend.repositories.SubmissionRepository;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.log4j.Log4j2;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
11+
12+
import java.time.LocalDateTime;
13+
import java.util.List;
14+
import java.util.UUID;
15+
16+
@Log4j2
17+
@Service
18+
@RequiredArgsConstructor
19+
public class SubmissionAnonymisationService {
20+
21+
private final SubmissionRepository submissionRepository;
22+
23+
private final GrantAttachmentRepository grantAttachmentRepository;
24+
25+
private final GrantMandatoryQuestionRepository grantMandatoryQuestionRepository;
26+
27+
private final S3Service s3Service;
28+
29+
@Transactional
30+
public void anonymiseSubmission(UUID submissionId) {
31+
log.info("Anonymising submission {}", submissionId);
32+
33+
// 1. Delete S3 objects — if any fail, abort and leave the submission in
34+
// IN_PROGRESS so the scheduler retries it on the next run
35+
final List<GrantAttachment> attachments = grantAttachmentRepository.findBySubmission_Id(submissionId);
36+
for (GrantAttachment attachment : attachments) {
37+
try {
38+
s3Service.deleteAttachment(attachment.getLocation());
39+
log.debug("Deleted S3 object {} for submission {}", attachment.getLocation(), submissionId);
40+
}
41+
catch (Exception e) {
42+
log.warn(
43+
"Aborting anonymisation of submission {} — failed to delete S3 object {}: {}. "
44+
+ "Submission will be retried on the next scheduler run.",
45+
submissionId, attachment.getLocation(), e.getMessage());
46+
return;
47+
}
48+
}
49+
50+
// 2. Delete diligence_check rows (no cascade on this FK)
51+
submissionRepository.deleteDiligenceCheckRowsBySubmissionIds(List.of(submissionId));
52+
53+
// 3. Delete grant_beneficiary rows
54+
submissionRepository.deleteBeneficiaryRowsBySubmissionIds(List.of(submissionId));
55+
56+
// 4. Delete mandatory question rows
57+
grantMandatoryQuestionRepository.deleteBySubmission_Id(submissionId);
58+
59+
// 5. Delete attachment DB rows
60+
grantAttachmentRepository.deleteBySubmission_Id(submissionId);
61+
62+
// 6. Null out personal data on the submission row and mark EXPIRED
63+
submissionRepository.anonymiseSubmissions(List.of(submissionId), LocalDateTime.now());
64+
65+
log.info("Anonymisation complete for submission {}", submissionId);
66+
}
67+
68+
}

src/main/resources/application.properties

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ cloud.aws.sqs.submissions-export-queue=cloud-platform-gap-apply-submissions-expo
3030
cloud.aws.sqs.event-service-queue=gap-apply-events-service-queue
3131
cloud.aws.sqs.event-service-queue-enabled=true
3232
cloud.aws.s3.submissions-export-bucket-name=cloud-platform-gap-apply-submissions-export-bucket
33+
aws.attachmentsBucket=attachments
3334
gov-notify-api-key=secretGovNotifyApiKey
3435
gov-notify-lambda-export-template-id=exportEmailTemplateId
3536

@@ -45,6 +46,13 @@ completion-statistics-scheduler.cronExpression=0 6 * * * ?
4546
completion-statistics-scheduler.lock.atMostFor=30m
4647
completion-statistics-scheduler.lock.atLeastFor=5m
4748

49+
#submissionAnonymisationScheduler configurable properties
50+
submission-anonymisation-scheduler.enabled=false
51+
submission-anonymisation-scheduler.cronExpression=0 0 3 * * ?
52+
submission-anonymisation-scheduler.daysBeforeExpiry=90
53+
submission-anonymisation-scheduler.lock.atMostFor=30m
54+
submission-anonymisation-scheduler.lock.atLeastFor=5m
55+
4856
#grantAdvertsScheduler configurable properties
4957
grant-adverts-scheduler.cronExpression=0 01 0 * * ?
5058
grant-adverts-scheduler.lock.atMostFor=30m
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
COMMENT ON COLUMN grant_submission.status IS
2+
'Valid values: IN_PROGRESS, SUBMITTED, GRANT_CLOSED, EXPIRED';

0 commit comments

Comments
 (0)