Repository navigation
π‘οΈ Sentinel: [CRITICAL] Add authentication to Admin endpoints #706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d5c4e9b
6570729
a178d55
960f0c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| 1. **Explore `TenantPermissions.java` and `TenantAccessService.java`** | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π― Functional Correctness | π‘ Minor | β‘ Quick win λ¬Έμ 맨 μμ μ΅μμ μ λͺ©μ μΆκ°νμΈμ.
π§° Toolsπͺ markdownlint-cli2 (0.23.3)[warning] 1-1: First line in a file should be a top-level heading (MD041, first-line-heading, first-line-h1) π€ Prompt for AI AgentsSources: Coding guidelines, Linters/SAST tools |
||
| - The memory states: "Administrative endpoints must be secured using `TenantAccessService` and an explicit cross-tenant admin permission (e.g., `admin:operate`) passed via request headers to prevent unauthorized access." | ||
| - Currently, `AdminController.java` lacks any authentication or authorization checks. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π Plan describes unfinished authorization work The plan says Was this helpful? React with π or π to provide feedback. |
||
| 2. **Update `TenantPermissions.java`** | ||
| - Add the `ADMIN_OPERATE` permission as `"admin:operate"`. | ||
| 3. **Update `AdminController.java`** | ||
| - Inject `TenantAccessService` via constructor. | ||
| - For every endpoint, read the `@RequestHeader HttpHeaders headers` and check `tenantAccessService.require(headers, TenantPermissions.ADMIN_OPERATE)`. | ||
| 4. **Update `AdminControllerTest.java`** | ||
| - Add the new headers to requests or mock `TenantAccessService`. | ||
| 5. **Pre-commit checks** | ||
| - Ensure the required testing, format, lint, and verification processes are complete. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,9 +11,13 @@ | |
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.web.bind.annotation.RequestHeader; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import org.springframework.web.server.ResponseStatusException; | ||
|
|
||
| import com.clearfolio.viewer.auth.TenantAccessService; | ||
| import com.clearfolio.viewer.auth.TenantPermissions; | ||
| import com.clearfolio.viewer.api.AdminJobListResponse; | ||
| import com.clearfolio.viewer.model.ConversionJob; | ||
| import com.clearfolio.viewer.service.DocumentConversionService; | ||
|
|
@@ -26,24 +30,31 @@ | |
| public class AdminController { | ||
|
|
||
| private final DocumentConversionService conversionService; | ||
| private final TenantAccessService tenantAccessService; | ||
|
|
||
| /** | ||
| * Creates a controller for admin operations. | ||
| * | ||
| * @param conversionService conversion service | ||
| * @param tenantAccessService tenant access service | ||
| */ | ||
| public AdminController(DocumentConversionService conversionService) { | ||
| public AdminController( | ||
| final DocumentConversionService conversionService, | ||
| final TenantAccessService tenantAccessService) { | ||
| this.conversionService = conversionService; | ||
| this.tenantAccessService = tenantAccessService; | ||
| } | ||
|
|
||
| /** | ||
| * Retrieves all conversion jobs, optionally filtered by dead-letter status. | ||
| * | ||
| * @param headers request headers | ||
| * @param deadLettered optional filter for dead-lettered jobs | ||
| * @return list of conversion jobs | ||
| */ | ||
| @GetMapping("/api/v1/admin/convert/jobs") | ||
| public AdminJobListResponse getAllJobs(@RequestParam(required = false) Boolean deadLettered) { | ||
| public AdminJobListResponse getAllJobs(@RequestHeader final HttpHeaders headers, @RequestParam(required = false) final Boolean deadLettered) { | ||
| tenantAccessService.require(headers, TenantPermissions.ADMIN_OPERATE); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π₯ Unsigned headers grant cross-tenant admin access When Was this helpful? React with π or π to provide feedback. |
||
| Iterable<ConversionJob> allJobs = conversionService.getAllJobs(); | ||
|
|
||
| if (deadLettered == null) { | ||
|
|
@@ -62,23 +73,27 @@ public AdminJobListResponse getAllJobs(@RequestParam(required = false) Boolean d | |
| /** | ||
| * Deletes a conversion job. | ||
| * | ||
| * @param headers request headers | ||
| * @param jobId conversion job identifier | ||
| * @return no content on success | ||
| */ | ||
| @DeleteMapping("/api/v1/admin/convert/jobs/{jobId}") | ||
| public ResponseEntity<Void> deleteJob(@PathVariable UUID jobId) { | ||
| public ResponseEntity<Void> deleteJob(@RequestHeader final HttpHeaders headers, @PathVariable final UUID jobId) { | ||
| tenantAccessService.require(headers, TenantPermissions.ADMIN_OPERATE); | ||
| conversionService.deleteJob(jobId); | ||
| return ResponseEntity.noContent().build(); | ||
| } | ||
|
|
||
| /** | ||
| * Retries a dead-lettered conversion job. | ||
| * | ||
| * @param headers request headers | ||
| * @param jobId conversion job identifier | ||
| * @return accepted response on success | ||
| */ | ||
| @PostMapping("/api/v1/admin/convert/jobs/{jobId}/retry") | ||
| public ResponseEntity<Void> retryDeadLettered(@PathVariable UUID jobId) { | ||
| public ResponseEntity<Void> retryDeadLettered(@RequestHeader final HttpHeaders headers, @PathVariable final UUID jobId) { | ||
| tenantAccessService.require(headers, TenantPermissions.ADMIN_OPERATE); | ||
| RetryDeadLetterResult result = conversionService.retryDeadLettered(jobId, "admin"); | ||
| if (result == RetryDeadLetterResult.NOT_FOUND) { | ||
| throw new ResponseStatusException(HttpStatus.NOT_FOUND, "job not found"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,9 @@ | |
| import java.util.UUID; | ||
|
|
||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.springframework.http.HttpHeaders; | ||
| import com.clearfolio.viewer.auth.TenantAccessService; | ||
| import com.clearfolio.viewer.auth.TenantPermissions; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.test.web.reactive.server.WebTestClient; | ||
|
|
||
|
|
@@ -17,13 +20,15 @@ | |
| class AdminControllerTest { | ||
|
|
||
| private DocumentConversionService conversionService; | ||
| private TenantAccessService tenantAccessService; | ||
| private WebTestClient webTestClient; | ||
| private AdminController controller; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| conversionService = mock(DocumentConversionService.class); | ||
| controller = new AdminController(conversionService); | ||
| tenantAccessService = mock(TenantAccessService.class); | ||
| controller = new AdminController(conversionService, tenantAccessService); | ||
|
Comment on lines
+30
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| webTestClient = WebTestClient.bindToController(controller) | ||
| .controllerAdvice(new ApiExceptionHandler()) | ||
| .build(); | ||
|
|
@@ -37,6 +42,7 @@ void getAllJobsReturnsAllJobsWhenNoFilterProvided() { | |
|
|
||
| webTestClient.get() | ||
| .uri("/api/v1/admin/convert/jobs") | ||
| .header("Authorization", "Bearer token") | ||
| .exchange() | ||
| .expectStatus().isOk() | ||
| .expectBody() | ||
|
|
@@ -55,6 +61,7 @@ void getAllJobsFiltersByDeadLetteredTrue() { | |
|
|
||
| webTestClient.get() | ||
| .uri("/api/v1/admin/convert/jobs?deadLettered=true") | ||
| .header("Authorization", "Bearer token") | ||
| .exchange() | ||
| .expectStatus().isOk() | ||
| .expectBody() | ||
|
|
@@ -72,6 +79,7 @@ void getAllJobsFiltersByDeadLetteredFalse() { | |
|
|
||
| webTestClient.get() | ||
| .uri("/api/v1/admin/convert/jobs?deadLettered=false") | ||
| .header("Authorization", "Bearer token") | ||
| .exchange() | ||
| .expectStatus().isOk() | ||
| .expectBody() | ||
|
|
@@ -85,6 +93,7 @@ void deleteJobReturnsNoContent() { | |
|
|
||
| webTestClient.delete() | ||
| .uri("/api/v1/admin/convert/jobs/" + jobId) | ||
| .header("Authorization", "Bearer token") | ||
| .exchange() | ||
| .expectStatus().isNoContent(); | ||
| } | ||
|
|
@@ -96,6 +105,7 @@ void retryDeadLetteredReturnsAcceptedWhenAccepted() { | |
|
|
||
| webTestClient.post() | ||
| .uri("/api/v1/admin/convert/jobs/" + jobId + "/retry") | ||
| .header("Authorization", "Bearer token") | ||
| .exchange() | ||
| .expectStatus().isAccepted(); | ||
| } | ||
|
|
@@ -107,6 +117,7 @@ void retryDeadLetteredReturnsNotFoundWhenNotFound() { | |
|
|
||
| webTestClient.post() | ||
| .uri("/api/v1/admin/convert/jobs/" + jobId + "/retry") | ||
| .header("Authorization", "Bearer token") | ||
| .exchange() | ||
| .expectStatus().isNotFound(); | ||
| } | ||
|
|
@@ -118,6 +129,7 @@ void retryDeadLetteredReturnsConflictWhenNotEligible() { | |
|
|
||
| webTestClient.post() | ||
| .uri("/api/v1/admin/convert/jobs/" + jobId + "/retry") | ||
| .header("Authorization", "Bearer token") | ||
| .exchange() | ||
| .expectStatus().isEqualTo(409); // isConflict() isn't always available depending on spring-test version, so using isEqualTo(409) is safer | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Plan starts without a Markdown heading
The changed-docs lint gate retains MD041, which requires a top-level heading first. plan.md begins with a list and needs a heading.
Was this helpful? React with π or π to provide feedback.