Public Access
refactor: item 29 cleanup
A10 A11 A12 A13 A14 A15 X10 X11 X12: role check denies outside a request, no API versioning, open-in-view off, no passwords in toString, X-Forwarded-For only from private peers, CORS from APP_FRONTEND_URL, non-root image.
This commit is contained in:
1 parent
89e2fa3df2
commit
9f56742ff6
27 files changed
+134
-85
No files matched your search
@@ -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"]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<MethodInvocation>
|
||||
|
||||
@Override
|
||||
public AuthorizationResult authorize(Supplier<? extends Authentication> 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);
|
||||
|
||||
@@ -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<Void> deleteAccount(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@Valid @RequestBody DeleteAccountRequest request) {
|
||||
accountService.deleteAccount(principal.getIdAsLong(), request.password());
|
||||
|
||||
@@ -27,7 +27,7 @@ public class AdminUserController {
|
||||
|
||||
private final AdminUserService adminUserService;
|
||||
|
||||
@GetMapping(version = "1.0")
|
||||
@GetMapping
|
||||
public ResponseEntity<AdminUserPageResponse> 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<Void> 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<Void> delete(@AuthenticationPrincipal UserPrincipal principal, @PathVariable long userId) {
|
||||
adminUserService.delete(principal.getIdAsLong(), userId);
|
||||
return ResponseEntity.noContent().build();
|
||||
|
||||
@@ -29,13 +29,13 @@ public class AdminHintController {
|
||||
|
||||
private final AdminHintService adminHintService;
|
||||
|
||||
@GetMapping(version = "1.0")
|
||||
@GetMapping
|
||||
public ResponseEntity<AdminHintListResponse> findAll() {
|
||||
return ResponseEntity.ok(new AdminHintListResponse(adminHintService.findAll().stream()
|
||||
.map(AdminHintController::toApi).toList()));
|
||||
}
|
||||
|
||||
@PostMapping(version = "1.0")
|
||||
@PostMapping
|
||||
public ResponseEntity<CreatedHintResponse> 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<Void> end(@AuthenticationPrincipal UserPrincipal principal, @PathVariable long hintId) {
|
||||
adminHintService.end(principal.getIdAsLong(), hintId);
|
||||
return ResponseEntity.noContent().build();
|
||||
|
||||
@@ -21,13 +21,13 @@ public class HintController {
|
||||
|
||||
private final HintService hintService;
|
||||
|
||||
@GetMapping(version = "1.0")
|
||||
@GetMapping
|
||||
public ResponseEntity<HintListResponse> 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<Void> markDone(@AuthenticationPrincipal UserPrincipal principal, @PathVariable long hintId) {
|
||||
hintService.markDone(principal.getIdAsLong(), hintId);
|
||||
return ResponseEntity.noContent().build();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<RegistrationResponse> registration(@Valid @RequestBody RegistrationRequest request, HttpServletRequest httpRequest) {
|
||||
var userId = registrationService.register(ApiMapper.mapToModel(request), ClientIp.of(httpRequest));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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<SteamLinkResponse> 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<Void> 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<Void> callback(@RequestParam(required = false) String state,
|
||||
@RequestParam Map<String, String> params,
|
||||
HttpServletRequest request) {
|
||||
|
||||
@@ -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<UserProfileResponse> 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<UserProfileResponse> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> updateAppearance(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@Valid @RequestBody AppearanceRequest request) {
|
||||
this.userService.updateAppearance(principal.getIdAsLong(), ApiMapper.toDomain(request));
|
||||
|
||||
@@ -32,7 +32,7 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
|
||||
TypedQuery<UserEntity> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user