diff --git a/Dockerfile b/Dockerfile index 51c3379..7121af6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,5 +36,9 @@ EXPOSE 8080 # Copy the compiled jar file from the builder stage COPY --from=builder /app/target/*.jar app.jar +# No root in the container +RUN addgroup -S app && adduser -S app -G app +USER app + # Run the application ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/readme.md b/readme.md index 690c724..8af1635 100644 --- a/readme.md +++ b/readme.md @@ -157,6 +157,12 @@ classes (`AbstractUnitTest`, `AbstractWebMvcTest`, `AbstractPersistenceTest`, `A the base classes carry every annotation, so each kind starts one Spring context. A test with its own annotations starts another one. +**Mirrored in Core.** These files exist in both services with small differences; a change in one needs the same +change in the other: `ClientHeaderFilter`, `RoleAuthorizationManager`, `MethodSecurityConfig`, `Role`, `RequiresRole`, +`Public` (Core also has `Internal`), `UserPrincipal`, the login token check in `JwtService.parseLoginToken`, the +ArchUnit role rules (`RoleAnnotationRules`) and `src/test/resources/contract/auth-core-contract.json` +(`ToBePlayed-Deployment/scripts/check-contract.sh` compares the two). + ### Login flow ```mermaid @@ -280,7 +286,7 @@ Needs Java 25, Docker and your own keys. ``` Spring Boot's Docker Compose support starts PostgreSQL from `compose.yaml`, Flyway applies the migrations, and the -service listens on `http://localhost:8080`. Health: `/actuator/health`, metrics: `/actuator/prometheus`. +service listens on `http://localhost:8080`. Health: `/actuator/health` (the only open actuator endpoint). The cookies are host-only unless `APP_AUTH_COOKIE_DOMAIN` is set; production sets it to the parent domain, so Core gets the access cookie too. Browsers accept `Secure` cookies on `http://localhost`, so local development needs no @@ -317,7 +323,7 @@ Runs unit, WebMvc, persistence and integration tests plus the ArchUnit rules. Ne ## Roadmap -- Account deletion and data export. +See `ROADMAP.md` in the ToBePlayed-Deployment repo. ## License diff --git a/src/main/java/auth_service/configuration/AsyncConfig.java b/src/main/java/auth_service/configuration/AsyncConfig.java deleted file mode 100644 index 1b160d7..0000000 --- a/src/main/java/auth_service/configuration/AsyncConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -package auth_service.configuration; - -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableAsync; - -@Configuration -@EnableAsync -public class AsyncConfig { - // This class enables Spring's @Async annotation functionality globally -} diff --git a/src/main/java/auth_service/configuration/ObservabilityConfig.java b/src/main/java/auth_service/configuration/ObservabilityConfig.java index ac60e1a..af76229 100644 --- a/src/main/java/auth_service/configuration/ObservabilityConfig.java +++ b/src/main/java/auth_service/configuration/ObservabilityConfig.java @@ -13,7 +13,7 @@ public class ObservabilityConfig { /** * Registers the TimedAspect bean to enable @Timed annotations. - * * @param registry The Micrometer MeterRegistry + * @param registry The Micrometer MeterRegistry * * @return A new TimedAspect instance */ diff --git a/src/main/java/auth_service/configuration/security/RoleAuthorizationManager.java b/src/main/java/auth_service/configuration/security/RoleAuthorizationManager.java index c0c9604..8dcd2b7 100644 --- a/src/main/java/auth_service/configuration/security/RoleAuthorizationManager.java +++ b/src/main/java/auth_service/configuration/security/RoleAuthorizationManager.java @@ -10,7 +10,6 @@ import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; -import org.springframework.web.context.request.RequestContextHolder; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; @@ -23,9 +22,6 @@ class RoleAuthorizationManager implements AuthorizationManager @Override public AuthorizationResult authorize(Supplier authentication, MethodInvocation invocation) { - if (RequestContextHolder.getRequestAttributes() == null) { - return GRANTED; - } Class targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : invocation.getMethod().getDeclaringClass(); Method method = AopUtils.getMostSpecificMethod(invocation.getMethod(), targetClass); diff --git a/src/main/java/auth_service/modules/account/api/AccountController.java b/src/main/java/auth_service/modules/account/api/AccountController.java index 7e32334..0da0cad 100644 --- a/src/main/java/auth_service/modules/account/api/AccountController.java +++ b/src/main/java/auth_service/modules/account/api/AccountController.java @@ -20,7 +20,7 @@ public class AccountController { private final AccountService accountService; private final AuthCookies authCookies; - @DeleteMapping(value = "/api/users/me", version = "1.0") + @DeleteMapping(value = "/api/users/me") public ResponseEntity deleteAccount(@AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody DeleteAccountRequest request) { accountService.deleteAccount(principal.getIdAsLong(), request.password()); diff --git a/src/main/java/auth_service/modules/admin/api/AdminUserController.java b/src/main/java/auth_service/modules/admin/api/AdminUserController.java index 2ca87b3..22da7f9 100644 --- a/src/main/java/auth_service/modules/admin/api/AdminUserController.java +++ b/src/main/java/auth_service/modules/admin/api/AdminUserController.java @@ -27,7 +27,7 @@ public class AdminUserController { private final AdminUserService adminUserService; - @GetMapping(version = "1.0") + @GetMapping public ResponseEntity findUsers(@RequestParam(defaultValue = "") String query, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { @@ -36,14 +36,14 @@ public class AdminUserController { found.page(), found.size(), found.totalElements(), found.totalPages())); } - @PutMapping(value = "/{userId}/blocked", version = "1.0") + @PutMapping(value = "/{userId}/blocked") public ResponseEntity setBlocked(@AuthenticationPrincipal UserPrincipal principal, @PathVariable long userId, @Valid @RequestBody BlockRequest request) { adminUserService.setBlocked(principal.getIdAsLong(), userId, request.blocked()); return ResponseEntity.noContent().build(); } - @DeleteMapping(value = "/{userId}", version = "1.0") + @DeleteMapping(value = "/{userId}") public ResponseEntity delete(@AuthenticationPrincipal UserPrincipal principal, @PathVariable long userId) { adminUserService.delete(principal.getIdAsLong(), userId); return ResponseEntity.noContent().build(); diff --git a/src/main/java/auth_service/modules/hint/api/AdminHintController.java b/src/main/java/auth_service/modules/hint/api/AdminHintController.java index dd6b060..1b66b6a 100644 --- a/src/main/java/auth_service/modules/hint/api/AdminHintController.java +++ b/src/main/java/auth_service/modules/hint/api/AdminHintController.java @@ -29,13 +29,13 @@ public class AdminHintController { private final AdminHintService adminHintService; - @GetMapping(version = "1.0") + @GetMapping public ResponseEntity findAll() { return ResponseEntity.ok(new AdminHintListResponse(adminHintService.findAll().stream() .map(AdminHintController::toApi).toList())); } - @PostMapping(version = "1.0") + @PostMapping public ResponseEntity create(@AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody CreateHintRequest request) { long id = adminHintService.create(principal.getIdAsLong(), @@ -43,7 +43,7 @@ public class AdminHintController { return ResponseEntity.status(HttpStatus.CREATED).body(new CreatedHintResponse(id)); } - @PutMapping(value = "/{hintId}/end", version = "1.0") + @PutMapping(value = "/{hintId}/end") public ResponseEntity end(@AuthenticationPrincipal UserPrincipal principal, @PathVariable long hintId) { adminHintService.end(principal.getIdAsLong(), hintId); return ResponseEntity.noContent().build(); diff --git a/src/main/java/auth_service/modules/hint/api/HintController.java b/src/main/java/auth_service/modules/hint/api/HintController.java index 24cc8a6..54ab293 100644 --- a/src/main/java/auth_service/modules/hint/api/HintController.java +++ b/src/main/java/auth_service/modules/hint/api/HintController.java @@ -21,13 +21,13 @@ public class HintController { private final HintService hintService; - @GetMapping(version = "1.0") + @GetMapping public ResponseEntity activeHints(@AuthenticationPrincipal UserPrincipal principal) { return ResponseEntity.ok(new HintListResponse(hintService.activeHints(principal.getIdAsLong()).stream() .map(HintController::toApi).toList())); } - @PutMapping(value = "/{hintId}/done", version = "1.0") + @PutMapping(value = "/{hintId}/done") public ResponseEntity markDone(@AuthenticationPrincipal UserPrincipal principal, @PathVariable long hintId) { hintService.markDone(principal.getIdAsLong(), hintId); return ResponseEntity.noContent().build(); diff --git a/src/main/java/auth_service/modules/login/model/User.java b/src/main/java/auth_service/modules/login/model/User.java index 2545f88..7db7579 100644 --- a/src/main/java/auth_service/modules/login/model/User.java +++ b/src/main/java/auth_service/modules/login/model/User.java @@ -3,12 +3,14 @@ package auth_service.modules.login.model; import auth_service.shared.security.Role; import lombok.Builder; import lombok.Data; +import lombok.ToString; @Builder @Data public class User { private long id; private String email; + @ToString.Exclude private String password; private Long steamId; private boolean enabled; diff --git a/src/main/java/auth_service/modules/login/persistence/MinimalUserRepository.java b/src/main/java/auth_service/modules/login/persistence/MinimalUserRepository.java index df3d74d..dddb6d6 100644 --- a/src/main/java/auth_service/modules/login/persistence/MinimalUserRepository.java +++ b/src/main/java/auth_service/modules/login/persistence/MinimalUserRepository.java @@ -10,7 +10,7 @@ import java.util.Optional; public interface MinimalUserRepository { /** * Finds a user by their email address. - * * @param email The email to search for + * @param email The email to search for * * @return An Optional containing the user if found */ diff --git a/src/main/java/auth_service/modules/registration/api/RegistrationController.java b/src/main/java/auth_service/modules/registration/api/RegistrationController.java index dbacc99..a905bbc 100644 --- a/src/main/java/auth_service/modules/registration/api/RegistrationController.java +++ b/src/main/java/auth_service/modules/registration/api/RegistrationController.java @@ -22,7 +22,7 @@ public class RegistrationController { private final RegistrationService registrationService; - @PostMapping(value = "/new", version = "1.0", produces = "application/json", consumes = "application/json") + @PostMapping(value = "/new", produces = "application/json", consumes = "application/json") @Timed(value = "auth.registration.new.requests.time", description = "Time taken to process registration requests") public ResponseEntity registration(@Valid @RequestBody RegistrationRequest request, HttpServletRequest httpRequest) { var userId = registrationService.register(ApiMapper.mapToModel(request), ClientIp.of(httpRequest)); diff --git a/src/main/java/auth_service/modules/registration/model/Registration.java b/src/main/java/auth_service/modules/registration/model/Registration.java index 4c5d328..4db641d 100644 --- a/src/main/java/auth_service/modules/registration/model/Registration.java +++ b/src/main/java/auth_service/modules/registration/model/Registration.java @@ -2,12 +2,15 @@ package auth_service.modules.registration.model; import lombok.Builder; import lombok.Data; +import lombok.ToString; @Builder @Data public class Registration { private String email; + @ToString.Exclude private String rawPassword; + @ToString.Exclude private String encryptedPassword; private String inviteCode; private String displayName; diff --git a/src/main/java/auth_service/modules/registration/service/RegistrationService.java b/src/main/java/auth_service/modules/registration/service/RegistrationService.java index bffec8d..3da400e 100644 --- a/src/main/java/auth_service/modules/registration/service/RegistrationService.java +++ b/src/main/java/auth_service/modules/registration/service/RegistrationService.java @@ -35,7 +35,7 @@ public class RegistrationService { private void validate(Registration registration) { if (!codeValidationService.isCodeValid(registration.getInviteCode())) { - throw new InvalidInvitationCodeException("The provided invite code is invalid or has already been used."); + throw new InvalidInvitationCodeException("The provided invite code is invalid."); } if (registeredUserRepository.existsByEmail(registration.getEmail())) { diff --git a/src/main/java/auth_service/modules/user/api/SteamLinkController.java b/src/main/java/auth_service/modules/user/api/SteamLinkController.java index b5e46df..667db18 100644 --- a/src/main/java/auth_service/modules/user/api/SteamLinkController.java +++ b/src/main/java/auth_service/modules/user/api/SteamLinkController.java @@ -31,7 +31,7 @@ public class SteamLinkController { private final UserService userService; private final AuthCookies authCookies; - @PostMapping(value = "/api/users/steam-link", version = "1.0", produces = "application/json") + @PostMapping(value = "/api/users/steam-link", produces = "application/json") public ResponseEntity start(@AuthenticationPrincipal UserPrincipal principal) { SteamLinkService.Start start = steamLinkService.start(principal.getIdAsLong()); return ResponseEntity.ok() @@ -42,7 +42,7 @@ public class SteamLinkController { /** * A new access cookie without the steamId claim, so Core stops syncing at once. */ - @DeleteMapping(value = "/api/users/steam-link", version = "1.0") + @DeleteMapping(value = "/api/users/steam-link") public ResponseEntity unlink(@AuthenticationPrincipal UserPrincipal principal) { String token = userService.unlinkSteamId(principal.getIdAsLong(), principal.role()); return ResponseEntity.noContent() @@ -53,7 +53,7 @@ public class SteamLinkController { /** * Steam's redirect back. Always sends the browser on to the settings page with the outcome. */ - @GetMapping(value = SteamLinkService.CALLBACK_PATH, version = "1.0") + @GetMapping(value = SteamLinkService.CALLBACK_PATH) public ResponseEntity callback(@RequestParam(required = false) String state, @RequestParam Map params, HttpServletRequest request) { diff --git a/src/main/java/auth_service/modules/user/api/UserController.java b/src/main/java/auth_service/modules/user/api/UserController.java index 1107c95..f75ef90 100644 --- a/src/main/java/auth_service/modules/user/api/UserController.java +++ b/src/main/java/auth_service/modules/user/api/UserController.java @@ -27,55 +27,51 @@ public class UserController { private final UserService userService; - /** - * Version 1 of the 'me' endpoint, accessed via /api/users/me?v=1 - * * @return The user's dashboard data - */ - @GetMapping(value = "/me", version = "1.0", produces = "application/json") - @Timed(value = "user.me.v1.requests.time", description = "Time taken to process v1 user profile requests") - public ResponseEntity getCurrentUserV1(@AuthenticationPrincipal UserPrincipal principal) { + @GetMapping(value = "/me", produces = "application/json") + @Timed(value = "user.me.requests.time", description = "Time taken to answer the user profile") + public ResponseEntity getCurrentUser(@AuthenticationPrincipal UserPrincipal principal) { var profileResponse = this.userService.getUser(principal.getIdAsLong()); return ResponseEntity.ok(ApiMapper.toApi(profileResponse, principal.role())); } - @PutMapping(value = "/settings", version = "1.0", produces = "application/json") - @Timed(value = "user.me.v1.requests.time", description = "Time taken to process v1 user profile requests") + @PutMapping(value = "/settings", produces = "application/json") + @Timed(value = "user.settings.requests.time", description = "Time taken to save the user settings") public ResponseEntity updateUserSettings(@AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody UserSettingsRequest request) { this.userService.updateUserSettings(principal.getIdAsLong(), request.displayName(), ApiMapper.toDomain(request)); return ResponseEntity.noContent().build(); } - @PutMapping(value = "/settings/dashboard-tiles", version = "1.0") + @PutMapping(value = "/settings/dashboard-tiles") public ResponseEntity updateDashboardTiles(@AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody DashboardTilesRequest request) { this.userService.updateDashboardTiles(principal.getIdAsLong(), ApiMapper.toDomain(request.tiles())); return ResponseEntity.noContent().build(); } - @PutMapping(value = "/settings/profile-tiles", version = "1.0") + @PutMapping(value = "/settings/profile-tiles") public ResponseEntity updateProfileTiles(@AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody ProfileTilesRequest request) { this.userService.updateProfileTiles(principal.getIdAsLong(), ApiMapper.toDomain(request.tiles())); return ResponseEntity.noContent().build(); } - @PutMapping(value = "/settings/favorite-platform", version = "1.0") + @PutMapping(value = "/settings/favorite-platform") public ResponseEntity updateFavoritePlatform(@AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody FavoritePlatformRequest request) { this.userService.updateFavoritePlatform(principal.getIdAsLong(), request.platform()); return ResponseEntity.noContent().build(); } - @PutMapping(value = "/settings/profile-style", version = "1.0") + @PutMapping(value = "/settings/profile-style") public ResponseEntity updateProfileStyle(@AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody ProfileStyleRequest request) { this.userService.updateProfileStyle(principal.getIdAsLong(), ApiMapper.toDomain(request)); return ResponseEntity.noContent().build(); } - @PutMapping(value = "/settings/appearance", version = "1.0") + @PutMapping(value = "/settings/appearance") public ResponseEntity updateAppearance(@AuthenticationPrincipal UserPrincipal principal, @Valid @RequestBody AppearanceRequest request) { this.userService.updateAppearance(principal.getIdAsLong(), ApiMapper.toDomain(request)); diff --git a/src/main/java/auth_service/modules/user/persistence/UserRepositoryImpl.java b/src/main/java/auth_service/modules/user/persistence/UserRepositoryImpl.java index 0586bf5..75259eb 100644 --- a/src/main/java/auth_service/modules/user/persistence/UserRepositoryImpl.java +++ b/src/main/java/auth_service/modules/user/persistence/UserRepositoryImpl.java @@ -32,7 +32,7 @@ public class UserRepositoryImpl implements UserRepository { TypedQuery typedQuery = entityManager.createQuery(userEntityCriteriaQuery); - return typedQuery.getResultStream() + return typedQuery.getResultList().stream() .findFirst() .map(EntityMapper::mapToModel); } @@ -56,7 +56,7 @@ public class UserRepositoryImpl implements UserRepository { "SELECT new auth_service.modules.user.model.SteamIdHolder(u.id, u.steamIdVerified) " + "FROM UserEntity u WHERE u.steamId = :steamId", SteamIdHolder.class) .setParameter("steamId", steamId) - .getResultStream() + .getResultList().stream() .findFirst(); } diff --git a/src/main/java/auth_service/shared/api/ClientIp.java b/src/main/java/auth_service/shared/api/ClientIp.java index 4bfede3..4013c75 100644 --- a/src/main/java/auth_service/shared/api/ClientIp.java +++ b/src/main/java/auth_service/shared/api/ClientIp.java @@ -2,6 +2,9 @@ package auth_service.shared.api; import jakarta.servlet.http.HttpServletRequest; +import java.net.InetAddress; +import java.net.UnknownHostException; + public final class ClientIp { private ClientIp() { @@ -9,15 +12,31 @@ public final class ClientIp { /** * Traefik appends the real client IP as the last X-Forwarded-For entry. Earlier entries come - * from the client and can be faked, so only the last one counts. Without a usable last entry - * (no proxy in front) the connection address counts. + * from the client and can be faked, so only the last one counts. The header counts only from a + * peer in the private network (the proxy); from any other peer, and without a usable last entry, + * the connection address counts. */ public static String of(HttpServletRequest request) { String forwardedFor = request.getHeader("X-Forwarded-For"); - if (forwardedFor == null) { + if (forwardedFor == null || !isPrivate(request.getRemoteAddr())) { return request.getRemoteAddr(); } String lastHop = forwardedFor.substring(forwardedFor.lastIndexOf(',') + 1).trim(); return lastHop.isEmpty() ? request.getRemoteAddr() : lastHop; } + + private static boolean isPrivate(String address) { + if (address == null) { + return false; + } + try { + // The servlet gives an IP literal, so this does no DNS lookup. + InetAddress peer = InetAddress.getByName(address); + // fc00::/7 is IPv6's private range; isSiteLocalAddress knows only the old fec0::/10. + boolean uniqueLocal = peer.getAddress().length == 16 && (peer.getAddress()[0] & 0xfe) == 0xfc; + return peer.isLoopbackAddress() || peer.isSiteLocalAddress() || uniqueLocal; + } catch (UnknownHostException e) { + return false; + } + } } diff --git a/src/main/java/auth_service/shared/security/RequiresRole.java b/src/main/java/auth_service/shared/security/RequiresRole.java index 63d6a16..1536d86 100644 --- a/src/main/java/auth_service/shared/security/RequiresRole.java +++ b/src/main/java/auth_service/shared/security/RequiresRole.java @@ -9,8 +9,8 @@ import java.lang.annotation.Target; /** * The lowest active role that may call this service method, or every method of this class. A method marker * replaces the class marker. Every service method a controller calls carries {@link RequiresRole} or - * {@link Public} (ArchitectureTest). Checked only while an HTTP request is served: scheduled jobs and - * {@code @Async} work run without a user. + * {@link Public} (ArchitectureTest). Also checked outside an HTTP request: a scheduled job or a background + * thread has no user, so it may call only {@link Public} methods. */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 7eb4059..70de0df 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -5,8 +5,6 @@ spring: locations: classpath:db/migration,classpath:db/local app: - cors: - allowed-origins: "http://localhost:5173" auth: # Plain http; Safari drops Secure cookies on localhost. cookie-secure: false diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c0f9a1d..0821ac8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,7 +7,7 @@ spring: profiles: active: prod # Default profile for local startup jpa: - open-in-view: true + open-in-view: false flyway: # Force Flyway to be enabled enabled: true @@ -18,12 +18,6 @@ spring: threads: virtual: enabled: true - mvc: - apiversion: - use: - query-parameter: version - default: "1.0" - supported: 1.0, 2,0 # Actuator & Micrometer Settings @@ -50,8 +44,8 @@ app: commit: ${APP_BUILD_COMMIT:dev} time: ${APP_BUILD_TIME:} cors: - # Production default: strictly allow only the frontend domain - allowed-origins: "https://tbp.andreas-dahm.eu" + # Only the frontend; FRONTEND_HOST in the Deployment compose file. + allowed-origins: ${app.frontend-url} auth: cookie-domain: ${APP_AUTH_COOKIE_DOMAIN:} cookie-secure: true diff --git a/src/test/java/auth_service/configuration/security/RoleAuthorizationManagerTest.java b/src/test/java/auth_service/configuration/security/RoleAuthorizationManagerTest.java index 80192a3..e5512e1 100644 --- a/src/test/java/auth_service/configuration/security/RoleAuthorizationManagerTest.java +++ b/src/test/java/auth_service/configuration/security/RoleAuthorizationManagerTest.java @@ -20,8 +20,8 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class RoleAuthorizationManagerTest extends AbstractUnitTest { @@ -62,9 +62,8 @@ class RoleAuthorizationManagerTest extends AbstractUnitTest { private boolean granted(Authentication authentication, Object target, String method) throws Exception { MethodInvocation invocation = mock(MethodInvocation.class); - // Lenient: without a request the manager does not look at the invocation. - lenient().when(invocation.getThis()).thenReturn(target); - lenient().when(invocation.getMethod()).thenReturn(target.getClass().getMethod(method)); + when(invocation.getThis()).thenReturn(target); + when(invocation.getMethod()).thenReturn(target.getClass().getMethod(method)); return manager.authorize(() -> authentication, invocation).isGranted(); } @@ -94,7 +93,9 @@ class RoleAuthorizationManagerTest extends AbstractUnitTest { } @Test - void withoutARequest_EverythingRuns() throws Exception { - assertTrue(granted(null, new UserMarked(), "audit")); + void withoutARequest_OnlyPublicMethodsRun() throws Exception { + assertFalse(granted(null, new UserMarked(), "audit")); + assertFalse(granted(null, new UserMarked(), "plain")); + assertTrue(granted(null, new UserMarked(), "open")); } } diff --git a/src/test/java/auth_service/configuration/security/RoleCheckFlowTest.java b/src/test/java/auth_service/configuration/security/RoleCheckFlowTest.java index c105247..60b2bd8 100644 --- a/src/test/java/auth_service/configuration/security/RoleCheckFlowTest.java +++ b/src/test/java/auth_service/configuration/security/RoleCheckFlowTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.stereotype.Service; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -19,7 +20,7 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -138,7 +139,7 @@ class RoleCheckFlowTest extends AbstractIntegrationTest { } @Test - void outsideARequest_TheServiceRuns() { - assertEquals("auditor", probeService.auditor()); + void outsideARequest_OnlyPublicMethodsRun() { + assertThrows(AuthenticationCredentialsNotFoundException.class, probeService::auditor); } } diff --git a/src/test/java/auth_service/configuration/security/SessionFlowTest.java b/src/test/java/auth_service/configuration/security/SessionFlowTest.java index 6001601..8b22562 100644 --- a/src/test/java/auth_service/configuration/security/SessionFlowTest.java +++ b/src/test/java/auth_service/configuration/security/SessionFlowTest.java @@ -42,7 +42,8 @@ class SessionFlowTest extends AbstractIntegrationTest { private static final String EMAIL = "session-flow@example.com"; private static final String PASSWORD = "correct horse battery"; private static final String CLIENT = "web"; - private static final String FRONTEND = "https://tbp.andreas-dahm.eu"; + // app.frontend-url in tests; CORS allows only it. + private static final String FRONTEND = "http://localhost:5173"; @Autowired private MockMvc mockMvc; diff --git a/src/test/java/auth_service/modules/login/model/UserTest.java b/src/test/java/auth_service/modules/login/model/UserTest.java new file mode 100644 index 0000000..2f4a872 --- /dev/null +++ b/src/test/java/auth_service/modules/login/model/UserTest.java @@ -0,0 +1,18 @@ +package auth_service.modules.login.model; + +import auth_service.modules.registration.model.Registration; +import auth_service.shared.test.AbstractUnitTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class UserTest extends AbstractUnitTest { + + @Test + void toString_ShowsNoPassword() { + assertThat(User.builder().email("a@example.com").password("hash-secret").build().toString()) + .contains("a@example.com").doesNotContain("hash-secret"); + assertThat(Registration.builder().email("a@example.com").rawPassword("raw-secret").encryptedPassword("hash-secret") + .build().toString()).doesNotContain("raw-secret").doesNotContain("hash-secret"); + } +} diff --git a/src/test/java/auth_service/modules/user/api/UserControllerTest.java b/src/test/java/auth_service/modules/user/api/UserControllerTest.java index 108abe7..1d05200 100644 --- a/src/test/java/auth_service/modules/user/api/UserControllerTest.java +++ b/src/test/java/auth_service/modules/user/api/UserControllerTest.java @@ -80,7 +80,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_Success() throws Exception { + void getCurrentUser_Success() throws Exception { // Arrange var email = "test@example.com"; var settings = UserSettings.builder().language("en").theme("dark").build(); @@ -99,7 +99,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_SteamIdIsSentAsExactString() throws Exception { + void getCurrentUser_SteamIdIsSentAsExactString() throws Exception { // Arrange var email = "test@example.com"; var settings = UserSettings.builder().language("en").theme("dark").build(); @@ -119,7 +119,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_NoSteamId() throws Exception { + void getCurrentUser_NoSteamId() throws Exception { // Arrange var email = "test@example.com"; var settings = UserSettings.builder().language("en").theme("dark").build(); @@ -138,7 +138,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_NotFound() throws Exception { + void getCurrentUser_NotFound() throws Exception { // Arrange var email = "notfound@example.com"; var principal = new UserPrincipal("1", null); @@ -190,7 +190,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_ReturnsDisplayName() throws Exception { + void getCurrentUser_ReturnsDisplayName() throws Exception { // Arrange var email = "test@example.com"; var settings = UserSettings.builder().language("en").theme("dark").build(); @@ -309,7 +309,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_ShowsPublicProfile() throws Exception { + void getCurrentUser_ShowsPublicProfile() throws Exception { var email = "test@example.com"; var settings = UserSettings.builder().language("en").theme("dark").publicProfile(true).build(); SecurityContextHolder.getContext().setAuthentication( @@ -323,7 +323,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_ShowsAppearance() throws Exception { + void getCurrentUser_ShowsAppearance() throws Exception { var email = "test@example.com"; var settings = UserSettings.builder().language("en").theme("dark") .appearance(new Appearance("blue", "round", true, 620L, 1086940L, false)).build(); @@ -343,7 +343,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_WithoutBackground_SendsNull() throws Exception { + void getCurrentUser_WithoutBackground_SendsNull() throws Exception { var email = "test@example.com"; var settings = UserSettings.builder().language("en").theme("dark").build(); SecurityContextHolder.getContext().setAuthentication( @@ -362,7 +362,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_ShowsTheTileLayouts() throws Exception { + void getCurrentUser_ShowsTheTileLayouts() throws Exception { String email = "test@example.com"; UserSettings settings = UserSettings.builder().language("en").theme("dark") .dashboardTiles(List.of(new Tile("STATS_TREND", "L"))) @@ -382,7 +382,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_NeverChangedDashboard_SendsNullTiles() throws Exception { + void getCurrentUser_NeverChangedDashboard_SendsNullTiles() throws Exception { String email = "test@example.com"; SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList()) @@ -518,7 +518,7 @@ class UserControllerTest extends AbstractWebMvcTest { } @Test - void getCurrentUserV1_ShowsTheProfileStyle() throws Exception { + void getCurrentUser_ShowsTheProfileStyle() throws Exception { String email = "test@example.com"; UserSettings settings = UserSettings.builder().language("en").theme("dark") .profileStyle(new ProfileStyle("red", "sharp", 620L, null)).build(); diff --git a/src/test/java/auth_service/shared/api/ClientIpTest.java b/src/test/java/auth_service/shared/api/ClientIpTest.java index cf3985d..39dabc2 100644 --- a/src/test/java/auth_service/shared/api/ClientIpTest.java +++ b/src/test/java/auth_service/shared/api/ClientIpTest.java @@ -25,4 +25,24 @@ class ClientIpTest extends AbstractUnitTest { assertEquals("198.51.100.9", ClientIp.of(request)); } + + private static String clientIp(String peer, String forwardedFor) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr(peer); + request.addHeader("X-Forwarded-For", forwardedFor); + return ClientIp.of(request); + } + + @Test + void publicPeer_CannotChooseItsIpByTheHeader() { + assertEquals("198.51.100.9", clientIp("198.51.100.9", "203.0.113.5")); + } + + @Test + void proxyInThePrivateNetwork_GivesTheLastHop() { + assertEquals("203.0.113.5", clientIp("172.18.0.2", "1.2.3.4, 203.0.113.5")); + assertEquals("203.0.113.5", clientIp("10.0.0.3", "203.0.113.5")); + assertEquals("203.0.113.5", clientIp("127.0.0.1", "203.0.113.5")); + assertEquals("203.0.113.5", clientIp("fd00::2", "203.0.113.5")); + } }