This reverts commit c8937357e5.
This commit is contained in:
@@ -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
|
app. The Gitea deploy step verifies that it exists, but it does not create or
|
||||||
overwrite it.
|
overwrite it.
|
||||||
|
|
||||||
Protected API routes require the shared API key from `ORG_BACKEND_API_KEY`.
|
|
||||||
Clients send it as:
|
|
||||||
|
|
||||||
```text
|
|
||||||
X-Org-Api-Key: <key>
|
|
||||||
```
|
|
||||||
|
|
||||||
The live systemd unit at `/etc/systemd/system/org-backend.service` is managed
|
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
|
as a symlink to `misc/org-backend.service`, so the project copy is the source of
|
||||||
truth.
|
truth.
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package org.zaine.app.controller;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.zaine.app.security.JwtUtil;
|
import org.zaine.app.security.JwtUtil;
|
||||||
import org.zaine.app.security.RequiresAuth;
|
|
||||||
import org.zaine.app.user.User;
|
import org.zaine.app.user.User;
|
||||||
import org.zaine.app.user.UserRepository;
|
import org.zaine.app.user.UserRepository;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
@@ -17,7 +16,6 @@ import java.util.Map;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/auth")
|
@RequestMapping("/api/auth")
|
||||||
@RequiresAuth
|
|
||||||
@Tag(name = "Authentication")
|
@Tag(name = "Authentication")
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.zaine.app.dto.CalibreBookDTO;
|
import org.zaine.app.dto.CalibreBookDTO;
|
||||||
import org.zaine.app.security.RequiresAuth;
|
|
||||||
import org.zaine.app.service.CalibreService;
|
import org.zaine.app.service.CalibreService;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -17,7 +16,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/calibre")
|
@RequestMapping("/api/calibre")
|
||||||
@RequiresAuth
|
|
||||||
@Tag(name = "Calibre", description = "Calibre API")
|
@Tag(name = "Calibre", description = "Calibre API")
|
||||||
public class CalibreController {
|
public class CalibreController {
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.zaine.app.dto.CreateCommentDTO;
|
import org.zaine.app.dto.CreateCommentDTO;
|
||||||
import org.zaine.app.model.Comments;
|
import org.zaine.app.model.Comments;
|
||||||
import org.zaine.app.security.RequiresAuth;
|
|
||||||
import org.zaine.app.service.CommentsService;
|
import org.zaine.app.service.CommentsService;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -86,7 +85,6 @@ public class CommentsController {
|
|||||||
value = "",
|
value = "",
|
||||||
consumes = "application/json"
|
consumes = "application/json"
|
||||||
)
|
)
|
||||||
@RequiresAuth
|
|
||||||
public void addComment(@RequestBody CreateCommentDTO dto) {
|
public void addComment(@RequestBody CreateCommentDTO dto) {
|
||||||
|
|
||||||
if (dto.getContent() == null || dto.getContent().trim().isEmpty()) {
|
if (dto.getContent() == null || dto.getContent().trim().isEmpty()) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.zaine.app.dto.CompetenciesDTO;
|
import org.zaine.app.dto.CompetenciesDTO;
|
||||||
import org.zaine.app.model.Competencies;
|
import org.zaine.app.model.Competencies;
|
||||||
import org.zaine.app.security.RequiresAuth;
|
|
||||||
import org.zaine.app.service.CompetenciesService;
|
import org.zaine.app.service.CompetenciesService;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -21,7 +20,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/competencies")
|
@RequestMapping("/api/competencies")
|
||||||
@RequiresAuth
|
|
||||||
@Tag(name = "Competencies", description = "Operations about competencies")
|
@Tag(name = "Competencies", description = "Operations about competencies")
|
||||||
public class CompetenciesController {
|
public class CompetenciesController {
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.zaine.app.dto.CreateNoteDTO;
|
import org.zaine.app.dto.CreateNoteDTO;
|
||||||
import org.zaine.app.model.Notes;
|
import org.zaine.app.model.Notes;
|
||||||
import org.zaine.app.security.RequiresAuth;
|
|
||||||
import org.zaine.app.service.NotesService;
|
import org.zaine.app.service.NotesService;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -20,7 +19,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api")
|
||||||
@RequiresAuth
|
|
||||||
@Tag(name = "Notes", description = "Operations related to notes")
|
@Tag(name = "Notes", description = "Operations related to notes")
|
||||||
public class NotesController {
|
public class NotesController {
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -61,3 +59,4 @@ public class NotesController {
|
|||||||
return notesService.createNote(note);
|
return notesService.createNote(note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import org.zaine.app.dto.MotalahSessionDTO;
|
|||||||
import org.zaine.app.dto.WirdEntryDTO;
|
import org.zaine.app.dto.WirdEntryDTO;
|
||||||
import org.zaine.app.model.MotalahSession;
|
import org.zaine.app.model.MotalahSession;
|
||||||
import org.zaine.app.model.WirdEntry;
|
import org.zaine.app.model.WirdEntry;
|
||||||
import org.zaine.app.security.RequiresAuth;
|
|
||||||
import org.zaine.app.service.MotalahService;
|
import org.zaine.app.service.MotalahService;
|
||||||
import org.zaine.app.service.WirdService;
|
import org.zaine.app.service.WirdService;
|
||||||
|
|
||||||
@@ -27,7 +26,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/wird")
|
@RequestMapping("/api/wird")
|
||||||
@RequiresAuth
|
|
||||||
@Tag(name = "Wird", description = "Wird API")
|
@Tag(name = "Wird", description = "Wird API")
|
||||||
public class WirdController {
|
public class WirdController {
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
@@ -143,7 +142,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@PostMapping("/build-web")
|
@PostMapping("/build-web")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<String> triggerWebBuild() {
|
public ResponseEntity<String> triggerWebBuild() {
|
||||||
return startBuild(webBuildDirectory, webBuildLogFile, webBuildRunning, webProcess, true);
|
return startBuild(webBuildDirectory, webBuildLogFile, webBuildRunning, webProcess, true);
|
||||||
}
|
}
|
||||||
@@ -157,7 +155,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@DeleteMapping("/build-web")
|
@DeleteMapping("/build-web")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<String> cancelWebBuild() {
|
public ResponseEntity<String> cancelWebBuild() {
|
||||||
return killProcess(webProcess, webBuildRunning, "web build");
|
return killProcess(webProcess, webBuildRunning, "web build");
|
||||||
}
|
}
|
||||||
@@ -171,7 +168,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/build-web/status")
|
@GetMapping("/build-web/status")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<BuildStatus> getWebBuildStatus() {
|
public ResponseEntity<BuildStatus> getWebBuildStatus() {
|
||||||
return ResponseEntity.ok(new BuildStatus(webBuildRunning.get(), webLastRun, webLastExitCode));
|
return ResponseEntity.ok(new BuildStatus(webBuildRunning.get(), webLastRun, webLastExitCode));
|
||||||
}
|
}
|
||||||
@@ -185,7 +181,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/build-web/logs")
|
@GetMapping("/build-web/logs")
|
||||||
@RequiresAuth
|
|
||||||
public SseEmitter streamWebLogs() {
|
public SseEmitter streamWebLogs() {
|
||||||
return streamLogs(webBuildLogFile);
|
return streamLogs(webBuildLogFile);
|
||||||
}
|
}
|
||||||
@@ -203,7 +198,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@PostMapping("/build-roam")
|
@PostMapping("/build-roam")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<String> triggerRoamBuild() {
|
public ResponseEntity<String> triggerRoamBuild() {
|
||||||
return startBuild(roamBuildDirectory, roamBuildLogFile, roamBuildRunning, roamProcess, false);
|
return startBuild(roamBuildDirectory, roamBuildLogFile, roamBuildRunning, roamProcess, false);
|
||||||
}
|
}
|
||||||
@@ -217,7 +211,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@DeleteMapping("/build-roam")
|
@DeleteMapping("/build-roam")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<String> cancelRoamBuild() {
|
public ResponseEntity<String> cancelRoamBuild() {
|
||||||
return killProcess(roamProcess, roamBuildRunning, "roam build");
|
return killProcess(roamProcess, roamBuildRunning, "roam build");
|
||||||
}
|
}
|
||||||
@@ -231,7 +224,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/build-roam/status")
|
@GetMapping("/build-roam/status")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<BuildStatus> getRoamBuildStatus() {
|
public ResponseEntity<BuildStatus> getRoamBuildStatus() {
|
||||||
return ResponseEntity.ok(new BuildStatus(roamBuildRunning.get(), roamLastRun, roamLastExitCode));
|
return ResponseEntity.ok(new BuildStatus(roamBuildRunning.get(), roamLastRun, roamLastExitCode));
|
||||||
}
|
}
|
||||||
@@ -245,7 +237,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/build-roam/logs")
|
@GetMapping("/build-roam/logs")
|
||||||
@RequiresAuth
|
|
||||||
public SseEmitter streamRoamLogs() {
|
public SseEmitter streamRoamLogs() {
|
||||||
return streamLogs(roamBuildLogFile);
|
return streamLogs(roamBuildLogFile);
|
||||||
}
|
}
|
||||||
@@ -263,7 +254,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@PostMapping("/rerun-emacs")
|
@PostMapping("/rerun-emacs")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<String> triggerEmacs() {
|
public ResponseEntity<String> triggerEmacs() {
|
||||||
log.info("Triggering Emacs command");
|
log.info("Triggering Emacs command");
|
||||||
return startCommand(emacsRunDirectory, emacsRunLogFile, emacsRunning, emacsProcess);
|
return startCommand(emacsRunDirectory, emacsRunLogFile, emacsRunning, emacsProcess);
|
||||||
@@ -278,7 +268,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@DeleteMapping("/rerun-emacs")
|
@DeleteMapping("/rerun-emacs")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<String> cancelEmacs() {
|
public ResponseEntity<String> cancelEmacs() {
|
||||||
return killProcess(emacsProcess, emacsRunning, "emacs");
|
return killProcess(emacsProcess, emacsRunning, "emacs");
|
||||||
}
|
}
|
||||||
@@ -292,7 +281,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/rerun-emacs/status")
|
@GetMapping("/rerun-emacs/status")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<BuildStatus> getEmacsStatus() {
|
public ResponseEntity<BuildStatus> getEmacsStatus() {
|
||||||
return ResponseEntity.ok(new BuildStatus(emacsRunning.get(), emacsLastRun, emacsLastExitCode));
|
return ResponseEntity.ok(new BuildStatus(emacsRunning.get(), emacsLastRun, emacsLastExitCode));
|
||||||
}
|
}
|
||||||
@@ -306,7 +294,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/rerun-emacs/logs")
|
@GetMapping("/rerun-emacs/logs")
|
||||||
@RequiresAuth
|
|
||||||
public SseEmitter streamEmacsLogs() {
|
public SseEmitter streamEmacsLogs() {
|
||||||
return streamLogs(emacsRunLogFile);
|
return streamLogs(emacsRunLogFile);
|
||||||
}
|
}
|
||||||
@@ -324,7 +311,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@PostMapping("/run-combined")
|
@PostMapping("/run-combined")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<String> triggerCombined() {
|
public ResponseEntity<String> triggerCombined() {
|
||||||
if (!combinedRunning.compareAndSet(false, true)) {
|
if (!combinedRunning.compareAndSet(false, true)) {
|
||||||
return ResponseEntity.status(HttpStatus.CONFLICT).body("Combined run already in progress");
|
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")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@DeleteMapping("/run-combined")
|
@DeleteMapping("/run-combined")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<String> cancelCombined() {
|
public ResponseEntity<String> cancelCombined() {
|
||||||
return killProcess(combinedProcess, combinedRunning, "combined run");
|
return killProcess(combinedProcess, combinedRunning, "combined run");
|
||||||
}
|
}
|
||||||
@@ -417,7 +402,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/run-combined/status")
|
@GetMapping("/run-combined/status")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<BuildStatus> getCombinedStatus() {
|
public ResponseEntity<BuildStatus> getCombinedStatus() {
|
||||||
return ResponseEntity.ok(new BuildStatus(combinedRunning.get(), combinedLastRun, combinedLastExitCode));
|
return ResponseEntity.ok(new BuildStatus(combinedRunning.get(), combinedLastRun, combinedLastExitCode));
|
||||||
}
|
}
|
||||||
@@ -431,7 +415,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/run-combined/logs")
|
@GetMapping("/run-combined/logs")
|
||||||
@RequiresAuth
|
|
||||||
public SseEmitter streamCombinedLogs() {
|
public SseEmitter streamCombinedLogs() {
|
||||||
return streamLogs(combinedRunLogFile);
|
return streamLogs(combinedRunLogFile);
|
||||||
}
|
}
|
||||||
@@ -449,7 +432,6 @@ public class BuildController {
|
|||||||
@ApiResponse(responseCode = "500", description = "Internal server error")
|
@ApiResponse(responseCode = "500", description = "Internal server error")
|
||||||
})
|
})
|
||||||
@GetMapping("/last-runs")
|
@GetMapping("/last-runs")
|
||||||
@RequiresAuth
|
|
||||||
public ResponseEntity<Map<String, Object>> getLastRuns() {
|
public ResponseEntity<Map<String, Object>> getLastRuns() {
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of(
|
||||||
"web", Map.of("lastRun", nullSafe(webLastRun), "exitCode", nullSafe(webLastExitCode)),
|
"web", Map.of("lastRun", nullSafe(webLastRun), "exitCode", nullSafe(webLastExitCode)),
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,14 +19,11 @@ import java.util.List;
|
|||||||
@Component
|
@Component
|
||||||
public class JwtAuthFilter extends OncePerRequestFilter {
|
public class JwtAuthFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
public static final String API_KEY_HEADER = "X-Org-Api-Key";
|
private final JwtUtil jwtUtil;
|
||||||
|
|
||||||
private final ApiKeyAuthService apiKeyAuthService;
|
|
||||||
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||||
|
|
||||||
public JwtAuthFilter(ApiKeyAuthService apiKeyAuthService,
|
public JwtAuthFilter(JwtUtil jwtUtil, RequestMappingHandlerMapping requestMappingHandlerMapping) {
|
||||||
RequestMappingHandlerMapping requestMappingHandlerMapping) {
|
this.jwtUtil = jwtUtil;
|
||||||
this.apiKeyAuthService = apiKeyAuthService;
|
|
||||||
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
|
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,17 +36,26 @@ public class JwtAuthFilter extends OncePerRequestFilter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String apiKey = request.getHeader(API_KEY_HEADER);
|
String authHeader = request.getHeader("Authorization");
|
||||||
if (apiKeyAuthService.isValid(apiKey)) {
|
|
||||||
var auth = new UsernamePasswordAuthenticationToken("api-key", null, List.of());
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid Authorization header");
|
||||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
|
String token = authHeader.substring(7);
|
||||||
"Missing or invalid " + API_KEY_HEADER + " header");
|
|
||||||
|
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) {
|
private boolean endpointRequiresAuth(HttpServletRequest request) {
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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() {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user