From 0e5f8d7b1c3adf119f740dcf4efa0582703ffee9 Mon Sep 17 00:00:00 2001 From: Zaine Arch Date: Sat, 9 May 2026 01:19:43 +0100 Subject: [PATCH] Revert "auth" This reverts commit c8937357e50e039c8ec92467efd680bf07f9bf17. --- README.md | 7 -- .../zaine/app/controller/AuthController.java | 4 +- .../app/controller/CalibreController.java | 4 +- .../app/controller/CommentsController.java | 2 - .../controller/CompetenciesController.java | 2 - .../zaine/app/controller/NotesController.java | 3 +- .../zaine/app/controller/WirdController.java | 2 - .../app/controller/zone/BuildController.java | 20 +--- .../zaine/app/security/ApiKeyAuthService.java | 31 ------ .../org/zaine/app/security/JwtAuthFilter.java | 36 ++++--- .../app/security/ApiKeyAuthServiceTest.java | 27 ------ .../zaine/app/security/JwtAuthFilterTest.java | 97 ------------------- 12 files changed, 25 insertions(+), 210 deletions(-) delete mode 100644 src/main/java/org/zaine/app/security/ApiKeyAuthService.java delete mode 100644 src/test/java/org/zaine/app/security/ApiKeyAuthServiceTest.java delete mode 100644 src/test/java/org/zaine/app/security/JwtAuthFilterTest.java diff --git a/README.md b/README.md index 40bafcb..cf998de 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,6 @@ Create that file once on the server with the production values required by the app. The Gitea deploy step verifies that it exists, but it does not create or overwrite it. -Protected API routes require the shared API key from `ORG_BACKEND_API_KEY`. -Clients send it as: - -```text -X-Org-Api-Key: -``` - The live systemd unit at `/etc/systemd/system/org-backend.service` is managed as a symlink to `misc/org-backend.service`, so the project copy is the source of truth. diff --git a/src/main/java/org/zaine/app/controller/AuthController.java b/src/main/java/org/zaine/app/controller/AuthController.java index f110a9c..41144ce 100644 --- a/src/main/java/org/zaine/app/controller/AuthController.java +++ b/src/main/java/org/zaine/app/controller/AuthController.java @@ -4,7 +4,6 @@ package org.zaine.app.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.zaine.app.security.JwtUtil; -import org.zaine.app.security.RequiresAuth; import org.zaine.app.user.User; import org.zaine.app.user.UserRepository; import org.springframework.http.*; @@ -17,7 +16,6 @@ import java.util.Map; @RestController @RequestMapping("/api/auth") -@RequiresAuth @Tag(name = "Authentication") public class AuthController { @@ -76,4 +74,4 @@ public class AuthController { public record LoginRequest(String username, String password) {} public record RegisterRequest(String username, String password) {} -} +} \ No newline at end of file diff --git a/src/main/java/org/zaine/app/controller/CalibreController.java b/src/main/java/org/zaine/app/controller/CalibreController.java index 33c7160..724a894 100644 --- a/src/main/java/org/zaine/app/controller/CalibreController.java +++ b/src/main/java/org/zaine/app/controller/CalibreController.java @@ -7,7 +7,6 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.zaine.app.dto.CalibreBookDTO; -import org.zaine.app.security.RequiresAuth; import org.zaine.app.service.CalibreService; import io.swagger.v3.oas.annotations.Operation; @@ -17,7 +16,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; @RestController @RequestMapping("/api/calibre") -@RequiresAuth @Tag(name = "Calibre", description = "Calibre API") public class CalibreController { @@ -34,4 +32,4 @@ public class CalibreController { public List getBooks() { return calibreService.getAllBooks(); } -} +} \ No newline at end of file diff --git a/src/main/java/org/zaine/app/controller/CommentsController.java b/src/main/java/org/zaine/app/controller/CommentsController.java index 7843f07..22e3cc4 100644 --- a/src/main/java/org/zaine/app/controller/CommentsController.java +++ b/src/main/java/org/zaine/app/controller/CommentsController.java @@ -12,7 +12,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.zaine.app.dto.CreateCommentDTO; import org.zaine.app.model.Comments; -import org.zaine.app.security.RequiresAuth; import org.zaine.app.service.CommentsService; import io.swagger.v3.oas.annotations.Operation; @@ -86,7 +85,6 @@ public class CommentsController { value = "", consumes = "application/json" ) - @RequiresAuth public void addComment(@RequestBody CreateCommentDTO dto) { if (dto.getContent() == null || dto.getContent().trim().isEmpty()) { diff --git a/src/main/java/org/zaine/app/controller/CompetenciesController.java b/src/main/java/org/zaine/app/controller/CompetenciesController.java index 6984ec2..37bbb4b 100644 --- a/src/main/java/org/zaine/app/controller/CompetenciesController.java +++ b/src/main/java/org/zaine/app/controller/CompetenciesController.java @@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.zaine.app.dto.CompetenciesDTO; import org.zaine.app.model.Competencies; -import org.zaine.app.security.RequiresAuth; import org.zaine.app.service.CompetenciesService; import io.swagger.v3.oas.annotations.Operation; @@ -21,7 +20,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; @RestController @RequestMapping("/api/competencies") -@RequiresAuth @Tag(name = "Competencies", description = "Operations about competencies") public class CompetenciesController { diff --git a/src/main/java/org/zaine/app/controller/NotesController.java b/src/main/java/org/zaine/app/controller/NotesController.java index 6155b4d..a5ebf90 100644 --- a/src/main/java/org/zaine/app/controller/NotesController.java +++ b/src/main/java/org/zaine/app/controller/NotesController.java @@ -10,7 +10,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.zaine.app.dto.CreateNoteDTO; import org.zaine.app.model.Notes; -import org.zaine.app.security.RequiresAuth; import org.zaine.app.service.NotesService; import io.swagger.v3.oas.annotations.Operation; @@ -20,7 +19,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; @RestController @RequestMapping("/api") -@RequiresAuth @Tag(name = "Notes", description = "Operations related to notes") public class NotesController { @Autowired @@ -61,3 +59,4 @@ public class NotesController { return notesService.createNote(note); } } + diff --git a/src/main/java/org/zaine/app/controller/WirdController.java b/src/main/java/org/zaine/app/controller/WirdController.java index 68b6a44..9d90e31 100644 --- a/src/main/java/org/zaine/app/controller/WirdController.java +++ b/src/main/java/org/zaine/app/controller/WirdController.java @@ -19,7 +19,6 @@ import org.zaine.app.dto.MotalahSessionDTO; import org.zaine.app.dto.WirdEntryDTO; import org.zaine.app.model.MotalahSession; import org.zaine.app.model.WirdEntry; -import org.zaine.app.security.RequiresAuth; import org.zaine.app.service.MotalahService; import org.zaine.app.service.WirdService; @@ -27,7 +26,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; @RestController @RequestMapping("/api/wird") -@RequiresAuth @Tag(name = "Wird", description = "Wird API") public class WirdController { diff --git a/src/main/java/org/zaine/app/controller/zone/BuildController.java b/src/main/java/org/zaine/app/controller/zone/BuildController.java index de8ef19..20ead76 100644 --- a/src/main/java/org/zaine/app/controller/zone/BuildController.java +++ b/src/main/java/org/zaine/app/controller/zone/BuildController.java @@ -28,7 +28,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import org.zaine.app.security.RequiresAuth; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -143,7 +142,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @PostMapping("/build-web") - @RequiresAuth public ResponseEntity triggerWebBuild() { return startBuild(webBuildDirectory, webBuildLogFile, webBuildRunning, webProcess, true); } @@ -157,7 +155,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @DeleteMapping("/build-web") - @RequiresAuth public ResponseEntity cancelWebBuild() { return killProcess(webProcess, webBuildRunning, "web build"); } @@ -171,7 +168,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @GetMapping("/build-web/status") - @RequiresAuth public ResponseEntity getWebBuildStatus() { return ResponseEntity.ok(new BuildStatus(webBuildRunning.get(), webLastRun, webLastExitCode)); } @@ -185,7 +181,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @GetMapping("/build-web/logs") - @RequiresAuth public SseEmitter streamWebLogs() { return streamLogs(webBuildLogFile); } @@ -203,7 +198,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @PostMapping("/build-roam") - @RequiresAuth public ResponseEntity triggerRoamBuild() { return startBuild(roamBuildDirectory, roamBuildLogFile, roamBuildRunning, roamProcess, false); } @@ -217,7 +211,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @DeleteMapping("/build-roam") - @RequiresAuth public ResponseEntity cancelRoamBuild() { return killProcess(roamProcess, roamBuildRunning, "roam build"); } @@ -231,7 +224,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @GetMapping("/build-roam/status") - @RequiresAuth public ResponseEntity getRoamBuildStatus() { return ResponseEntity.ok(new BuildStatus(roamBuildRunning.get(), roamLastRun, roamLastExitCode)); } @@ -245,7 +237,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @GetMapping("/build-roam/logs") - @RequiresAuth public SseEmitter streamRoamLogs() { return streamLogs(roamBuildLogFile); } @@ -263,7 +254,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @PostMapping("/rerun-emacs") - @RequiresAuth public ResponseEntity triggerEmacs() { log.info("Triggering Emacs command"); return startCommand(emacsRunDirectory, emacsRunLogFile, emacsRunning, emacsProcess); @@ -278,7 +268,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @DeleteMapping("/rerun-emacs") - @RequiresAuth public ResponseEntity cancelEmacs() { return killProcess(emacsProcess, emacsRunning, "emacs"); } @@ -292,7 +281,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @GetMapping("/rerun-emacs/status") - @RequiresAuth public ResponseEntity getEmacsStatus() { return ResponseEntity.ok(new BuildStatus(emacsRunning.get(), emacsLastRun, emacsLastExitCode)); } @@ -306,7 +294,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @GetMapping("/rerun-emacs/logs") - @RequiresAuth public SseEmitter streamEmacsLogs() { return streamLogs(emacsRunLogFile); } @@ -324,7 +311,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @PostMapping("/run-combined") - @RequiresAuth public ResponseEntity triggerCombined() { if (!combinedRunning.compareAndSet(false, true)) { return ResponseEntity.status(HttpStatus.CONFLICT).body("Combined run already in progress"); @@ -403,7 +389,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @DeleteMapping("/run-combined") - @RequiresAuth public ResponseEntity cancelCombined() { return killProcess(combinedProcess, combinedRunning, "combined run"); } @@ -417,7 +402,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @GetMapping("/run-combined/status") - @RequiresAuth public ResponseEntity getCombinedStatus() { return ResponseEntity.ok(new BuildStatus(combinedRunning.get(), combinedLastRun, combinedLastExitCode)); } @@ -431,7 +415,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @GetMapping("/run-combined/logs") - @RequiresAuth public SseEmitter streamCombinedLogs() { return streamLogs(combinedRunLogFile); } @@ -449,7 +432,6 @@ public class BuildController { @ApiResponse(responseCode = "500", description = "Internal server error") }) @GetMapping("/last-runs") - @RequiresAuth public ResponseEntity> getLastRuns() { return ResponseEntity.ok(Map.of( "web", Map.of("lastRun", nullSafe(webLastRun), "exitCode", nullSafe(webLastExitCode)), @@ -794,4 +776,4 @@ public class BuildController { this.lastExitCode = lastExitCode; } } -} +} \ No newline at end of file diff --git a/src/main/java/org/zaine/app/security/ApiKeyAuthService.java b/src/main/java/org/zaine/app/security/ApiKeyAuthService.java deleted file mode 100644 index 3a4eed6..0000000 --- a/src/main/java/org/zaine/app/security/ApiKeyAuthService.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.zaine.app.security; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -public class ApiKeyAuthService { - - private final String apiKey; - - public ApiKeyAuthService(@Value("${org.auth.api-key:}") String apiKey) { - this.apiKey = apiKey == null ? "" : apiKey.trim(); - } - - public boolean isConfigured() { - return !apiKey.isBlank(); - } - - public boolean isValid(String candidate) { - if (!isConfigured() || candidate == null || candidate.isBlank()) { - return false; - } - - byte[] expected = apiKey.getBytes(StandardCharsets.UTF_8); - byte[] provided = candidate.trim().getBytes(StandardCharsets.UTF_8); - return MessageDigest.isEqual(expected, provided); - } -} diff --git a/src/main/java/org/zaine/app/security/JwtAuthFilter.java b/src/main/java/org/zaine/app/security/JwtAuthFilter.java index 2c3f572..9b5d4d1 100644 --- a/src/main/java/org/zaine/app/security/JwtAuthFilter.java +++ b/src/main/java/org/zaine/app/security/JwtAuthFilter.java @@ -19,14 +19,11 @@ import java.util.List; @Component public class JwtAuthFilter extends OncePerRequestFilter { - public static final String API_KEY_HEADER = "X-Org-Api-Key"; - - private final ApiKeyAuthService apiKeyAuthService; + private final JwtUtil jwtUtil; private final RequestMappingHandlerMapping requestMappingHandlerMapping; - public JwtAuthFilter(ApiKeyAuthService apiKeyAuthService, - RequestMappingHandlerMapping requestMappingHandlerMapping) { - this.apiKeyAuthService = apiKeyAuthService; + public JwtAuthFilter(JwtUtil jwtUtil, RequestMappingHandlerMapping requestMappingHandlerMapping) { + this.jwtUtil = jwtUtil; this.requestMappingHandlerMapping = requestMappingHandlerMapping; } @@ -39,17 +36,26 @@ public class JwtAuthFilter extends OncePerRequestFilter { return; } - String apiKey = request.getHeader(API_KEY_HEADER); - if (apiKeyAuthService.isValid(apiKey)) { - var auth = new UsernamePasswordAuthenticationToken("api-key", null, List.of()); - auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(auth); - filterChain.doFilter(request, response); + String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid Authorization header"); return; } - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, - "Missing or invalid " + API_KEY_HEADER + " header"); + String token = authHeader.substring(7); + + if (!jwtUtil.isTokenValid(token)) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired token"); + return; + } + + String username = jwtUtil.extractUsername(token); + var auth = new UsernamePasswordAuthenticationToken(username, null, List.of()); + auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(auth); + + filterChain.doFilter(request, response); } private boolean endpointRequiresAuth(HttpServletRequest request) { @@ -65,4 +71,4 @@ public class JwtAuthFilter extends OncePerRequestFilter { return false; } } -} +} \ No newline at end of file diff --git a/src/test/java/org/zaine/app/security/ApiKeyAuthServiceTest.java b/src/test/java/org/zaine/app/security/ApiKeyAuthServiceTest.java deleted file mode 100644 index 27d1022..0000000 --- a/src/test/java/org/zaine/app/security/ApiKeyAuthServiceTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.zaine.app.security; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class ApiKeyAuthServiceTest { - - @Test - void rejectsAllKeysWhenNoApiKeyIsConfigured() { - ApiKeyAuthService service = new ApiKeyAuthService(""); - - assertFalse(service.isConfigured()); - assertFalse(service.isValid("anything")); - } - - @Test - void acceptsOnlyConfiguredApiKey() { - ApiKeyAuthService service = new ApiKeyAuthService("secret-key"); - - assertTrue(service.isConfigured()); - assertTrue(service.isValid("secret-key")); - assertFalse(service.isValid("wrong-key")); - assertFalse(service.isValid(null)); - } -} diff --git a/src/test/java/org/zaine/app/security/JwtAuthFilterTest.java b/src/test/java/org/zaine/app/security/JwtAuthFilterTest.java deleted file mode 100644 index 1eeccc6..0000000 --- a/src/test/java/org/zaine/app/security/JwtAuthFilterTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.zaine.app.security; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; - -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class JwtAuthFilterTest { - - private TestHandlerMapping handlerMapping; - private JwtAuthFilter filter; - - @BeforeEach - void setUp() { - handlerMapping = new TestHandlerMapping(); - filter = new JwtAuthFilter(new ApiKeyAuthService("secret-key"), handlerMapping); - } - - @Test - void rejectsProtectedEndpointWithoutApiKey() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/secure"); - MockHttpServletResponse response = new MockHttpServletResponse(); - AtomicBoolean chainCalled = new AtomicBoolean(false); - handlerMapping.setHandler(handlerFor("secure")); - - filter.doFilter(request, response, chain(chainCalled)); - - assertEquals(401, response.getStatus()); - assertFalse(chainCalled.get()); - } - - @Test - void allowsProtectedEndpointWithValidApiKey() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/secure"); - request.addHeader(JwtAuthFilter.API_KEY_HEADER, "secret-key"); - MockHttpServletResponse response = new MockHttpServletResponse(); - AtomicBoolean chainCalled = new AtomicBoolean(false); - handlerMapping.setHandler(handlerFor("secure")); - - filter.doFilter(request, response, chain(chainCalled)); - - assertEquals(200, response.getStatus()); - assertTrue(chainCalled.get()); - } - - @Test - void allowsPublicEndpointWithoutApiKey() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/public"); - MockHttpServletResponse response = new MockHttpServletResponse(); - AtomicBoolean chainCalled = new AtomicBoolean(false); - handlerMapping.setHandler(handlerFor("open")); - - filter.doFilter(request, response, chain(chainCalled)); - - assertEquals(200, response.getStatus()); - assertTrue(chainCalled.get()); - } - - private HandlerMethod handlerFor(String methodName) throws NoSuchMethodException { - DemoController controller = new DemoController(); - return new HandlerMethod(controller, DemoController.class.getMethod(methodName)); - } - - private FilterChain chain(AtomicBoolean called) { - return (request, response) -> called.set(true); - } - - static class TestHandlerMapping extends RequestMappingHandlerMapping { - private HandlerMethod handler; - - void setHandler(HandlerMethod handler) { - this.handler = handler; - } - - @Override - protected HandlerMethod getHandlerInternal(HttpServletRequest request) { - return handler; - } - } - - static class DemoController { - @RequiresAuth - public void secure() {} - - public void open() {} - } -}