Skip to content

Feature/s3 integration - #15

Merged
MartinStenhagen merged 12 commits into
mainfrom
feature/s3-integration
Apr 15, 2026
Merged

Feature/s3 integration#15
MartinStenhagen merged 12 commits into
mainfrom
feature/s3-integration

Conversation

@Rickank

@Rickank Rickank commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Added S3 file handling pipeline using AWS SDK and LocalStack 3.8.1 for local development.

Includes upload, download and delete functionality exposed via REST API:

  • POST /api/files/upload
  • GET /api/files/download/{key}
  • DELETE /api/files/delete/{key}

Unit tested with Mockito. No real AWS credentials required anymore.

Summary by CodeRabbit

  • New Features

    • Public file upload, download and delete HTTP endpoints under /api/files with unauthenticated access.
    • S3-backed storage support with configurable endpoint for local S3-compatible services.
  • Tests

    • Unit tests for upload/download/delete, test profile config, and example HTTP requests for manual testing.
  • Chores

    • Added local S3-compatible service for development and init script; updated build configuration to include AWS SDK and bumped framework version.

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@Rickank has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 25 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 13 minutes and 25 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa32ce50-3235-41ab-9ce8-735d14a4847d

📥 Commits

Reviewing files that changed from the base of the PR and between dc738ed and 0b181cc.

📒 Files selected for processing (1)
  • init-localstack.sh
📝 Walkthrough

Walkthrough

Adds AWS S3 integration with LocalStack: Docker Compose adds LocalStack; Maven adds AWS SDK BOM and S3 dependency; Spring config supplies an S3Client; new S3Service and S3Controller provide upload/download/delete endpoints; security permits /api/files/**; tests and HTTP examples added. (47 words)

Changes

Cohort / File(s) Summary
Infrastructure
docker-compose.yaml, init-localstack.sh
Added localstack service (localstack/localstack:3.8.1, container team4you-localstack, 4566:4566, SERVICES=s3, DEFAULT_REGION=eu-north-1), healthcheck, and init script to create S3 bucket.
Build & Dependencies
pom.xml
Bumped Spring Boot parent 4.0.3→4.0.4; added AWS SDK BOM software.amazon.awssdk:bom:2.42.25 and software.amazon.awssdk:s3; adjusted security/validation/test dependencies and cleaned whitespace.
Configuration
src/main/resources/application.properties, src/test/resources/application-test.properties
Added AWS properties (aws.access-key, aws.secret-key, aws.region, aws.bucket-name, aws.endpoint-url) and test profile properties for LocalStack.
S3 Client Config
src/main/java/backendlab/team4you/config/S3Config.java
New conditional @Configuration creating AWS v2 S3Client using static creds, region, and optional endpoint-url override (for LocalStack) with forcePathStyle enabled.
Service Layer
src/main/java/backendlab/team4you/s3/S3Service.java
New Spring service wrapping S3Client with uploadFile, downloadFile, and deleteFile using configured aws.bucket-name.
API Layer
src/main/java/backendlab/team4you/s3/S3Controller.java, src/main/java/backendlab/team4you/s3/s3-test.http
New @RestController at /api/files exposing POST /upload, GET /download/{key}, DELETE /delete/{key}; added HTTP example requests.
Security
src/main/java/backendlab/team4you/config/SecurityConfig.java
Disabled CSRF and added requestMatchers("/api/files/**").permitAll() to allow unauthenticated access to file endpoints.
Tests
src/test/java/backendlab/team4you/s3/S3ServiceTest.java, src/test/java/backendlab/team4you/Team4youApplicationTests.java
Added Mockito unit tests for S3Service and set @ActiveProfiles("test") for application tests.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,230,255,0.5)
    participant Client
    end
    rect rgba(220,255,200,0.5)
    participant S3Controller
    participant S3Service
    end
    rect rgba(255,230,200,0.5)
    participant S3Client as AWS S3Client
    participant LocalStack
    end

    Client->>S3Controller: POST /api/files/upload (MultipartFile)
    S3Controller->>S3Service: uploadFile(key, bytes, contentType)
    S3Service->>S3Client: putObject(PutObjectRequest, RequestBody)
    S3Client->>LocalStack: HTTP PUT to S3 endpoint
    LocalStack-->>S3Client: PutObjectResponse
    S3Client-->>S3Service: return
    S3Service-->>S3Controller: return
    S3Controller-->>Client: 200 OK (success message)
Loading
sequenceDiagram
    rect rgba(200,230,255,0.5)
    participant Client
    end
    rect rgba(220,255,200,0.5)
    participant S3Controller
    participant S3Service
    end
    rect rgba(255,230,200,0.5)
    participant S3Client as AWS S3Client
    participant LocalStack
    end

    Client->>S3Controller: GET /api/files/download/{key}
    S3Controller->>S3Service: downloadFile(key)
    S3Service->>S3Client: getObject(GetObjectRequest)
    S3Client->>LocalStack: HTTP GET to S3 endpoint
    LocalStack-->>S3Client: ResponseInputStream (object data)
    S3Client-->>S3Service: return InputStream
    S3Service-->>S3Controller: return InputStream
    S3Controller->>S3Controller: readAllBytes() -> byte[]
    S3Controller-->>Client: 200 OK (file bytes + Content-Disposition)
Loading
sequenceDiagram
    rect rgba(200,230,255,0.5)
    participant Client
    end
    rect rgba(220,255,200,0.5)
    participant S3Controller
    participant S3Service
    end
    rect rgba(255,230,200,0.5)
    participant S3Client as AWS S3Client
    participant LocalStack
    end

    Client->>S3Controller: DELETE /api/files/delete/{key}
    S3Controller->>S3Service: deleteFile(key)
    S3Service->>S3Client: deleteObject(DeleteObjectRequest)
    S3Client->>LocalStack: HTTP DELETE to S3 endpoint
    LocalStack-->>S3Client: DeleteObjectResponse
    S3Client-->>S3Service: return
    S3Service-->>S3Controller: return
    S3Controller-->>Client: 200 OK (deletion success)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • gvaguirres
  • JohanHiths

Poem

🐰 I hopped to LocalStack's door today,
I tucked a file in clouds of gray,
I fetched it back with joyful cheer,
Then cleaned the bucket—nothing to fear.
Bytes that bounce make my whiskers sway.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/s3 integration' directly summarizes the main change: adding S3 file handling functionality with upload, download, and delete REST API endpoints. It is clear, specific, and matches the PR's primary purpose.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/s3-integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (4)
src/main/java/backendlab/team4you/config/S3Config.java (1)

28-37: Consider closing S3Client on application shutdown.

S3Client implements SdkAutoCloseable and holds HTTP connections. While the connection pool may eventually be garbage collected, explicitly closing the client on shutdown is best practice to release resources promptly.

♻️ Proposed fix to ensure S3Client cleanup
     `@Bean`
+    `@PreDestroy`
     public S3Client s3Client() {
         AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
         return S3Client.builder()
                 .region(Region.of(region))
                 .credentialsProvider(StaticCredentialsProvider.create(credentials))
                 .endpointOverride(URI.create(endpointUrl)) // Points to LocalStack instead of real AWS
                 .forcePathStyle(true) // Required for LocalStack
                 .build();
     }
+
+    `@Bean`(destroyMethod = "close")
+    public S3Client s3Client() { ... }

Alternatively, annotate the bean with destroyMethod:

`@Bean`(destroyMethod = "close")
public S3Client s3Client() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/config/S3Config.java` around lines 28 - 37,
The S3Client bean (s3Client method) isn't being closed on JVM shutdown; update
the bean definition to ensure the S3Client is closed by the Spring container
(e.g., declare the `@Bean` with destroyMethod="close" or implement a lifecycle
hook that calls close on the S3Client) so the SdkAutoCloseable connections are
released when the application stops. Ensure the change targets the s3Client()
bean and uses the S3Client.close() method as the destroy action.
src/main/java/backendlab/team4you/s3/S3Service.java (2)

27-36: Consider validating the key parameter.

If key is null or empty, the S3 SDK will throw an exception. Adding validation provides clearer error messages.

♻️ Proposed validation
     public void uploadFile(String key, byte[] data, String contentType) {
+        if (key == null || key.isBlank()) {
+            throw new IllegalArgumentException("File key cannot be null or empty");
+        }
         s3Client.putObject(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/s3/S3Service.java` around lines 27 - 36,
The uploadFile method in S3Service should validate the key parameter before
calling s3Client.putObject: check that key is not null and not blank (e.g.,
after trimming) and if invalid throw an IllegalArgumentException with a clear
message like "S3 key must not be null or empty"; update the uploadFile(String
key, byte[] data, String contentType) method to perform this validation and only
call s3Client.putObject (and use bucketName) when the key passes validation.

38-46: Document or address InputStream lifecycle.

The returned InputStream is a ResponseInputStream from AWS SDK that holds an open HTTP connection. Callers must close it to avoid resource leaks. Consider either:

  1. Documenting this contract in Javadoc
  2. Returning byte[] directly to simplify resource management for callers
♻️ Option: Return byte[] to simplify resource management
-    // Download a file from S3
-    public InputStream downloadFile(String key) {
-        return s3Client.getObject(
+    // Download a file from S3
+    public byte[] downloadFile(String key) throws IOException {
+        try (InputStream stream = s3Client.getObject(
                 GetObjectRequest.builder()
                         .bucket(bucketName)
                         .key(key)
                         .build()
-        );
+        )) {
+            return stream.readAllBytes();
+        }
     }

This moves the responsibility of closing the stream to the service layer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/s3/S3Service.java` around lines 38 - 46,
The downloadFile method currently returns the raw ResponseInputStream (open HTTP
connection) which callers must close; change downloadFile(String key) to return
a byte[] instead and inside the method call
s3Client.getObject(GetObjectRequest.builder().bucket(bucketName).key(key).build())
within a try-with-resources block (capturing the
ResponseInputStream<GetObjectResponse>), read all bytes (e.g.,
response.readAllBytes()) and return the byte[] so the service closes the stream
itself; alternatively if you keep the InputStream signature, add Javadoc to
downloadFile(String key) noting it returns a ResponseInputStream that the caller
MUST close and update callers accordingly.
src/test/java/backendlab/team4you/s3/S3ServiceTest.java (1)

50-62: Unchecked cast warning from mocking generic type.

The pipeline warning about "unchecked or unsafe operations" comes from mock(ResponseInputStream.class) without type parameters. This is a known Mockito limitation with generics.

♻️ Suppress the warning if needed
     `@Test`
+    `@SuppressWarnings`("unchecked")
     void downloadFile_shouldCallGetObject() {
         // Arrange
         ResponseInputStream<GetObjectResponse> mockStream = mock(ResponseInputStream.class);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/backendlab/team4you/s3/S3ServiceTest.java` around lines 50 -
62, The unchecked generic-mock warning in
S3ServiceTest.downloadFile_shouldCallGetObject comes from mocking
ResponseInputStream without type parameters; fix it by performing an explicit
cast to ResponseInputStream<GetObjectResponse> and suppressing the unchecked
warning for that statement or the test method (e.g., add
`@SuppressWarnings`("unchecked") and write ResponseInputStream<GetObjectResponse>
mockStream = (ResponseInputStream<GetObjectResponse>)
mock(ResponseInputStream.class)); keep the verify(s3Client,
times(1)).getObject(...) assertion unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docker-compose.yaml`:
- Around line 14-26: LocalStack currently starts without creating the S3 bucket
so S3 operations fail; add an init hook to create the bucket on startup by
adding an init script (e.g., init-localstack.sh) that runs awslocal s3 mb
s3://${AWS_BUCKET_NAME:-my-bucket} and wire it into the localstack service
configuration (or enable EAGER_SERVICE_LOADING and an init hook) so the bucket
named by AWS_BUCKET_NAME is created before the app runs; update the
docker-compose localstack service (service name "localstack") to execute this
initialization.

In `@src/main/java/backendlab/team4you/config/SecurityConfig.java`:
- Line 28: Current code in SecurityConfig uses .csrf(csrf -> csrf.disable()),
which disables CSRF globally; change this to disable CSRF only for API endpoints
by replacing the global disable with a scoped configuration that ignores or
disables CSRF for requests matching your REST API paths (e.g., "/api/**")
instead; locate the .csrf(csrf -> csrf.disable()) call in SecurityConfig and
update it to use csrf(...).ignoringRequestMatchers or an equivalent
request-matcher-based API so non-API (form-based) endpoints keep CSRF
protection.
- Around line 35-36: SecurityConfig currently permits all access to
"/api/files/**", which is unsafe; update the HTTP security rules to require
authentication for file-mutating endpoints (e.g., change the matcher for
POST/PUT/DELETE to .authenticated() and optionally allow GET for public
downloads) by using method-specific requestMatchers (HttpMethod.POST/PUT/DELETE
with "/api/files/**") instead of a blanket .permitAll(), and/or add method-level
security annotations in S3Controller on the mutating handlers (annotate
uploadFile, deleteFile, updateFile methods with
`@PreAuthorize`("isAuthenticated()" or a role check) and enable
`@EnableMethodSecurity` if not already configured. Ensure the SecurityConfig
ordering places these matchers before any broader permits so the authenticated
rules are enforced.

In `@src/main/java/backendlab/team4you/s3/S3Controller.java`:
- Around line 34-41: The InputStream from s3Service.downloadFile(key) in the
controller method downloadFile(`@PathVariable` String key) is not closed; wrap the
call to s3Service.downloadFile(key) in a try-with-resources so the
ResponseInputStream is closed after calling readAllBytes(), then build and
return the ResponseEntity<byte[]> from the bytes read; keep the method signature
(throws IOException) and ensure no open stream is left holding the S3 HTTP
connection.
- Line 38: The Content-Disposition header currently inserts the raw key into
HttpHeaders.CONTENT_DISPOSITION which can allow header injection; update the
S3Controller (where the response header is built using
HttpHeaders.CONTENT_DISPOSITION and the variable key) to build a safe header
value via org.springframework.http.ContentDisposition (e.g.,
ContentDisposition.builder("attachment").filename(...).build().toString()) and
ensure the filename is encoded/normalized using
java.nio.charset.StandardCharsets (or set both filename and filename* with
UTF-8) so quotes/newlines are handled and the header value is sanitized.
- Around line 26-30: uploadFile currently uses
MultipartFile.getOriginalFilename() which may return null; validate the filename
in the uploadFile method and reject or generate a safe key: if
getOriginalFilename() is null or blank, return
ResponseEntity.badRequest().body("Missing filename") (or alternatively generate
a safe key such as UUID.randomUUID().toString()); then call
s3Service.uploadFile(...) with the validated/sanitized key and the file
bytes/content type. Ensure you reference uploadFile and s3Service.uploadFile
when making the change and avoid passing a null key to the S3 SDK.

In `@src/test/java/backendlab/team4you/s3/S3ServiceTest.java`:
- Around line 19-23: The tests don't inject S3Service.bucketName (annotated with
`@Value`) so it's null; in S3ServiceTest set the private field on the injected
S3Service before running tests (e.g., in a `@BeforeEach` setup) using
ReflectionTestUtils.setField(s3Service, "bucketName", "expected-bucket") so
PutObjectRequest/GetObjectRequest/DeleteObjectRequest use a real bucket; after
setting the field, replace broad any() bucket matchers in verifications with
explicit checks (ArgumentCaptor or eq("expected-bucket")) to assert the correct
bucket is used.

---

Nitpick comments:
In `@src/main/java/backendlab/team4you/config/S3Config.java`:
- Around line 28-37: The S3Client bean (s3Client method) isn't being closed on
JVM shutdown; update the bean definition to ensure the S3Client is closed by the
Spring container (e.g., declare the `@Bean` with destroyMethod="close" or
implement a lifecycle hook that calls close on the S3Client) so the
SdkAutoCloseable connections are released when the application stops. Ensure the
change targets the s3Client() bean and uses the S3Client.close() method as the
destroy action.

In `@src/main/java/backendlab/team4you/s3/S3Service.java`:
- Around line 27-36: The uploadFile method in S3Service should validate the key
parameter before calling s3Client.putObject: check that key is not null and not
blank (e.g., after trimming) and if invalid throw an IllegalArgumentException
with a clear message like "S3 key must not be null or empty"; update the
uploadFile(String key, byte[] data, String contentType) method to perform this
validation and only call s3Client.putObject (and use bucketName) when the key
passes validation.
- Around line 38-46: The downloadFile method currently returns the raw
ResponseInputStream (open HTTP connection) which callers must close; change
downloadFile(String key) to return a byte[] instead and inside the method call
s3Client.getObject(GetObjectRequest.builder().bucket(bucketName).key(key).build())
within a try-with-resources block (capturing the
ResponseInputStream<GetObjectResponse>), read all bytes (e.g.,
response.readAllBytes()) and return the byte[] so the service closes the stream
itself; alternatively if you keep the InputStream signature, add Javadoc to
downloadFile(String key) noting it returns a ResponseInputStream that the caller
MUST close and update callers accordingly.

In `@src/test/java/backendlab/team4you/s3/S3ServiceTest.java`:
- Around line 50-62: The unchecked generic-mock warning in
S3ServiceTest.downloadFile_shouldCallGetObject comes from mocking
ResponseInputStream without type parameters; fix it by performing an explicit
cast to ResponseInputStream<GetObjectResponse> and suppressing the unchecked
warning for that statement or the test method (e.g., add
`@SuppressWarnings`("unchecked") and write ResponseInputStream<GetObjectResponse>
mockStream = (ResponseInputStream<GetObjectResponse>)
mock(ResponseInputStream.class)); keep the verify(s3Client,
times(1)).getObject(...) assertion unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a6adc669-f748-4b4c-85cc-ce8515561134

📥 Commits

Reviewing files that changed from the base of the PR and between 527d158 and 0fed61a.

📒 Files selected for processing (9)
  • docker-compose.yaml
  • pom.xml
  • src/main/java/backendlab/team4you/config/S3Config.java
  • src/main/java/backendlab/team4you/config/SecurityConfig.java
  • src/main/java/backendlab/team4you/s3/S3Controller.java
  • src/main/java/backendlab/team4you/s3/S3Service.java
  • src/main/java/backendlab/team4you/s3/s3-test.http
  • src/main/resources/application.properties
  • src/test/java/backendlab/team4you/s3/S3ServiceTest.java

Comment thread docker-compose.yaml
CustomAuthenticationSuccessHandler successHandler) throws Exception {

return http
.csrf(csrf -> csrf.disable())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Disabling CSRF globally is overly broad.

CSRF is disabled for the entire application, which exposes all form-based endpoints (login, profile, etc.) to CSRF attacks. If CSRF needs to be disabled for the REST API endpoints, consider a more targeted approach using request matchers.

🔒 Proposed fix to scope CSRF disable to API endpoints only
 return http
-                .csrf(csrf -> csrf.disable())
+                .csrf(csrf -> csrf
+                        .ignoringRequestMatchers("/api/**"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.csrf(csrf -> csrf.disable())
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/config/SecurityConfig.java` at line 28,
Current code in SecurityConfig uses .csrf(csrf -> csrf.disable()), which
disables CSRF globally; change this to disable CSRF only for API endpoints by
replacing the global disable with a scoped configuration that ignores or
disables CSRF for requests matching your REST API paths (e.g., "/api/**")
instead; locate the .csrf(csrf -> csrf.disable()) call in SecurityConfig and
update it to use csrf(...).ignoringRequestMatchers or an equivalent
request-matcher-based API so non-API (form-based) endpoints keep CSRF
protection.

Comment on lines +35 to +36
.requestMatchers("/api/files/**").permitAll()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unrestricted access to file operations is a security risk.

Permitting unauthenticated access to /api/files/** allows anyone to upload, download, and delete files without authentication. This could lead to:

  • Unauthorized data exfiltration via download
  • Storage abuse via unlimited uploads
  • Data loss via anonymous deletions

Consider restricting at least the upload and delete operations to authenticated users, or add method-level security annotations (@PreAuthorize) in S3Controller.java.

🔒 Proposed fix to require authentication for file operations
-                                .requestMatchers("/api/files/**").permitAll()
+                                .requestMatchers("/api/files/download/**").permitAll()
+                                .requestMatchers("/api/files/upload", "/api/files/delete/**").hasAnyRole("USER", "ADMIN")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.requestMatchers("/api/files/**").permitAll()
.requestMatchers("/api/files/download/**").permitAll()
.requestMatchers("/api/files/upload", "/api/files/delete/**").hasAnyRole("USER", "ADMIN")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/config/SecurityConfig.java` around lines 35
- 36, SecurityConfig currently permits all access to "/api/files/**", which is
unsafe; update the HTTP security rules to require authentication for
file-mutating endpoints (e.g., change the matcher for POST/PUT/DELETE to
.authenticated() and optionally allow GET for public downloads) by using
method-specific requestMatchers (HttpMethod.POST/PUT/DELETE with
"/api/files/**") instead of a blanket .permitAll(), and/or add method-level
security annotations in S3Controller on the mutating handlers (annotate
uploadFile, deleteFile, updateFile methods with
`@PreAuthorize`("isAuthenticated()" or a role check) and enable
`@EnableMethodSecurity` if not already configured. Ensure the SecurityConfig
ordering places these matchers before any broader permits so the authenticated
rules are enforced.

Comment thread src/main/java/backendlab/team4you/s3/S3Controller.java Outdated
Comment thread src/main/java/backendlab/team4you/s3/S3Controller.java Outdated
InputStream stream = s3Service.downloadFile(key);
byte[] bytes = stream.readAllBytes();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + key + "\"")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Sanitize filename in Content-Disposition header.

The key is used directly in the Content-Disposition header. If the key contains characters like " or newlines, it could cause header injection or malformed responses.

🛡️ Proposed fix to sanitize the filename
-                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + key + "\"")
+                .header(HttpHeaders.CONTENT_DISPOSITION, 
+                        ContentDisposition.attachment()
+                                .filename(key, StandardCharsets.UTF_8)
+                                .build()
+                                .toString())

Add import:

import org.springframework.http.ContentDisposition;
import java.nio.charset.StandardCharsets;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + key + "\"")
.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.attachment()
.filename(key, StandardCharsets.UTF_8)
.build()
.toString())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/s3/S3Controller.java` at line 38, The
Content-Disposition header currently inserts the raw key into
HttpHeaders.CONTENT_DISPOSITION which can allow header injection; update the
S3Controller (where the response header is built using
HttpHeaders.CONTENT_DISPOSITION and the variable key) to build a safe header
value via org.springframework.http.ContentDisposition (e.g.,
ContentDisposition.builder("attachment").filename(...).build().toString()) and
ensure the filename is encoded/normalized using
java.nio.charset.StandardCharsets (or set both filename and filename* with
UTF-8) so quotes/newlines are handled and the header value is sanitized.

Comment thread src/test/java/backendlab/team4you/s3/S3ServiceTest.java Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/main/java/backendlab/team4you/s3/S3Controller.java (1)

41-41: ⚠️ Potential issue | 🟠 Major

Build Content-Disposition safely from untrusted key.

Line [41] inserts raw key into a header value. Use ContentDisposition builder to avoid malformed/header-injection values.

💡 Proposed fix
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
+import org.springframework.http.ContentDisposition;
 import org.springframework.http.ResponseEntity;
+import java.nio.charset.StandardCharsets;
@@
-                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + key + "\"")
+                    .header(HttpHeaders.CONTENT_DISPOSITION,
+                            ContentDisposition.attachment()
+                                    .filename(key, StandardCharsets.UTF_8)
+                                    .build()
+                                    .toString())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/s3/S3Controller.java` at line 41, The
Content-Disposition header in S3Controller is built by concatenating the
untrusted key into the header value (HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + key + "\""), which risks header injection; fix it by
using Spring's ContentDisposition.builder() to construct a safe disposition from
the key (e.g., setType("attachment") and filename(key) or filenameEncoded if
needed) and then pass contentDisposition.toString() (or ContentDisposition
header string) to the header() call so the framework safely escapes/encodes the
filename.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@init-localstack.sh`:
- Line 2: Make the bucket creation idempotent and safely quoted: instead of
unconditionally running awslocal s3 mb s3://${AWS_BUCKET_NAME:-team4you-files},
check for the bucket first (e.g., using awslocal s3api head-bucket or
list-buckets) and only call awslocal s3 mb if the check indicates the bucket
does not exist, and always quote the bucket name variable (use
"${AWS_BUCKET_NAME:-team4you-files}") in the check and create commands; update
the invocation in init-localstack.sh where awslocal s3 mb is used.

In `@src/main/java/backendlab/team4you/s3/S3Controller.java`:
- Around line 13-16: The S3 endpoints are publicly accessible; remove the
permissive rule and require authenticated access: update SecurityConfig to
remove permitAll() for the "/api/files/**" matcher (so it requires
authentication) and/or replace it with an .authenticated() or role-based rule
(e.g., hasRole("ADMIN")), then add method-level protection in S3Controller by
annotating the upload and delete handlers (the POST "/api/files/upload" method
and DELETE "/api/files/delete/{key}" method) with `@PreAuthorize` or `@Secured` as
appropriate (e.g., `@PreAuthorize`("isAuthenticated()" or
`@PreAuthorize`("hasRole('ADMIN')")) to ensure only authorized users can perform
upload/delete. Ensure imports for `@PreAuthorize/`@Secured are added and Spring
Security global method security is enabled if not already.
- Around line 37-44: The downloadFile method in S3Controller currently reads the
whole InputStream into memory via stream.readAllBytes(); change it to return a
ResponseEntity<StreamingResponseBody> that obtains the InputStream from
s3Service.downloadFile(key) and writes it to the response using
StreamingResponseBody with InputStream.transferTo(outputStream), preserving the
Content-Disposition and contentType headers; ensure the InputStream is closed
after transfer (use try-with-resources inside the StreamingResponseBody lambda)
and propagate IO errors as appropriate.

---

Duplicate comments:
In `@src/main/java/backendlab/team4you/s3/S3Controller.java`:
- Line 41: The Content-Disposition header in S3Controller is built by
concatenating the untrusted key into the header value
(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + key + "\""), which
risks header injection; fix it by using Spring's ContentDisposition.builder() to
construct a safe disposition from the key (e.g., setType("attachment") and
filename(key) or filenameEncoded if needed) and then pass
contentDisposition.toString() (or ContentDisposition header string) to the
header() call so the framework safely escapes/encodes the filename.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f2d1b71d-dd7f-433e-9ac0-03b6c32dd266

📥 Commits

Reviewing files that changed from the base of the PR and between 912b164 and dc738ed.

📒 Files selected for processing (4)
  • docker-compose.yaml
  • init-localstack.sh
  • src/main/java/backendlab/team4you/s3/S3Controller.java
  • src/test/java/backendlab/team4you/s3/S3ServiceTest.java
✅ Files skipped from review due to trivial changes (1)
  • src/test/java/backendlab/team4you/s3/S3ServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • docker-compose.yaml

Comment thread init-localstack.sh Outdated
Comment on lines +13 to +16
@RestController
// All endpoints start with /api/files
@RequestMapping("/api/files")
public class S3Controller {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 --type=java 'requestMatchers\("/api/files/\*\*"\)\.permitAll\(\)|@RequestMapping\("/api/files"\)|@PostMapping\("/upload"\)|@DeleteMapping\("/delete/\{key\}"\)'

Repository: ithsjava25/project-backend-team4you

Length of output: 2123


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the full S3Controller to check for method-level security annotations
echo "=== S3Controller.java (full) ==="
cat -n src/main/java/backendlab/team4you/s3/S3Controller.java

echo -e "\n=== SecurityConfig.java (full) ==="
cat -n src/main/java/backendlab/team4you/config/SecurityConfig.java

Repository: ithsjava25/project-backend-team4you

Length of output: 7312


Restrict /api/files/** endpoints to authenticated users; currently allows anonymous upload/delete.

The endpoints at /api/files/upload (POST) and /api/files/delete/{key} (DELETE) are destructive operations but are publicly accessible. SecurityConfig.java line 35 permits /api/files/** to all users without authentication. The S3Controller methods have no @PreAuthorize, @Secured, or internal authorization checks. Remove permitAll() from line 35 and require authentication; optionally restrict to specific roles if only certain users should manage files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/s3/S3Controller.java` around lines 13 - 16,
The S3 endpoints are publicly accessible; remove the permissive rule and require
authenticated access: update SecurityConfig to remove permitAll() for the
"/api/files/**" matcher (so it requires authentication) and/or replace it with
an .authenticated() or role-based rule (e.g., hasRole("ADMIN")), then add
method-level protection in S3Controller by annotating the upload and delete
handlers (the POST "/api/files/upload" method and DELETE
"/api/files/delete/{key}" method) with `@PreAuthorize` or `@Secured` as appropriate
(e.g., `@PreAuthorize`("isAuthenticated()" or `@PreAuthorize`("hasRole('ADMIN')"))
to ensure only authorized users can perform upload/delete. Ensure imports for
`@PreAuthorize/`@Secured are added and Spring Security global method security is
enabled if not already.

Comment on lines +37 to +44
public ResponseEntity<byte[]> downloadFile(@PathVariable String key) throws IOException {
try (InputStream stream = s3Service.downloadFile(key)) {
byte[] bytes = stream.readAllBytes();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + key + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "S3Controller.java" -o -name "*.java" | head -20

Repository: ithsjava25/project-backend-team4you

Length of output: 61


🏁 Script executed:

find . -path "*/s3/*" -name "*.java" | head -20

Repository: ithsjava25/project-backend-team4you

Length of output: 247


🏁 Script executed:

find . -name "S3Controller.java"

Repository: ithsjava25/project-backend-team4you

Length of output: 135


🏁 Script executed:

cat -n ./src/main/java/backendlab/team4you/s3/S3Controller.java

Repository: ithsjava25/project-backend-team4you

Length of output: 2442


🏁 Script executed:

cat -n ./src/main/java/backendlab/team4you/s3/S3Service.java

Repository: ithsjava25/project-backend-team4you

Length of output: 2330


🏁 Script executed:

find . -name "pom.xml" -o -name "build.gradle"

Repository: ithsjava25/project-backend-team4you

Length of output: 88


🏁 Script executed:

cat -n ./pom.xml

Repository: ithsjava25/project-backend-team4you

Length of output: 5111


Stream S3 downloads to prevent memory spikes with large files.

The current implementation at line 39 uses readAllBytes() which loads the entire S3 object into heap memory. For large files, this causes memory spikes and risks OOM. Use StreamingResponseBody with transferTo() to stream the response instead.

💡 Proposed fix
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@@
-    public ResponseEntity<byte[]> downloadFile(`@PathVariable` String key) throws IOException {
-        try (InputStream stream = s3Service.downloadFile(key)) {
-            byte[] bytes = stream.readAllBytes();
-            return ResponseEntity.ok()
-                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + key + "\"")
-                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
-                    .body(bytes);
-        }
+    public ResponseEntity<StreamingResponseBody> downloadFile(`@PathVariable` String key) {
+        StreamingResponseBody body = outputStream -> {
+            try (InputStream stream = s3Service.downloadFile(key)) {
+                stream.transferTo(outputStream);
+            }
+        };
+        return ResponseEntity.ok()
+                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + key + "\"")
+                .contentType(MediaType.APPLICATION_OCTET_STREAM)
+                .body(body);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/s3/S3Controller.java` around lines 37 - 44,
The downloadFile method in S3Controller currently reads the whole InputStream
into memory via stream.readAllBytes(); change it to return a
ResponseEntity<StreamingResponseBody> that obtains the InputStream from
s3Service.downloadFile(key) and writes it to the response using
StreamingResponseBody with InputStream.transferTo(outputStream), preserving the
Content-Disposition and contentType headers; ensure the InputStream is closed
after transfer (use try-with-resources inside the StreamingResponseBody lambda)
and propagate IO errors as appropriate.

@MartinStenhagen
MartinStenhagen merged commit dda5e31 into main Apr 15, 2026
2 checks passed
@MartinStenhagen
MartinStenhagen deleted the feature/s3-integration branch April 15, 2026 14:54
@coderabbitai coderabbitai Bot mentioned this pull request Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants