Added much more authentication infrastructure for various kinds of authentication.
This commit is contained in:
parent
c7bf9d9058
commit
f4ba6b8bec
|
@ -9,28 +9,68 @@ import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for authentication-related tasks.
|
||||||
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
private final TokenService tokenService;
|
private final TokenService tokenService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The endpoint for users of this Onyx node to login, that is, obtain a
|
||||||
|
* refresh token and access token in exchange for valid credentials.
|
||||||
|
* @param loginRequest The login request.
|
||||||
|
* @return A token pair, if successful.
|
||||||
|
*/
|
||||||
@PostMapping("/auth/login")
|
@PostMapping("/auth/login")
|
||||||
public TokenPair login(@RequestBody LoginRequest loginRequest) {
|
public TokenPair login(@RequestBody LoginRequest loginRequest) {
|
||||||
return tokenService.generateTokenPair(loginRequest);
|
return tokenService.generateTokenPair(loginRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint for obtaining a new access token using a valid refresh token.
|
||||||
|
* @param request The HTTP request.
|
||||||
|
* @return The new access token.
|
||||||
|
*/
|
||||||
@GetMapping("/auth/access")
|
@GetMapping("/auth/access")
|
||||||
public AccessTokenResponse getAccessToken(HttpServletRequest request) {
|
public AccessTokenResponse getAccessToken(HttpServletRequest request) {
|
||||||
return tokenService.generateAccessToken(request);
|
return tokenService.generateAccessToken(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint used to remove all refresh tokens, essentially logging the user
|
||||||
|
* out of all devices that may have stored a refresh token.
|
||||||
|
* @param user The user who is removing their tokens.
|
||||||
|
*/
|
||||||
@DeleteMapping("/auth/refresh-tokens")
|
@DeleteMapping("/auth/refresh-tokens")
|
||||||
public void removeAllRefreshTokens(@AuthenticationPrincipal User user) {
|
public void removeAllRefreshTokens(@AuthenticationPrincipal User user) {
|
||||||
tokenService.removeAllRefreshTokens(user);
|
tokenService.removeAllRefreshTokens(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint for determining the expiration time of an access token.
|
||||||
|
* @param request The HTTP request.
|
||||||
|
* @return An object containing an "expiresAt" field, in milliseconds since
|
||||||
|
* the unix epoch.
|
||||||
|
*/
|
||||||
@GetMapping("/auth/token-expiration")
|
@GetMapping("/auth/token-expiration")
|
||||||
public Object getTokenExpiration(HttpServletRequest request) {
|
public Object getTokenExpiration(HttpServletRequest request) {
|
||||||
return Map.of("expiresAt", tokenService.getTokenExpiration(request));
|
return Map.of("expiresAt", tokenService.getTokenExpiration(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a token belonging to a user of this Onyx node, as requested by
|
||||||
|
* another node. The request itself should have an Authorization header
|
||||||
|
* with a bearer token that proves the identity of the onyx node that's
|
||||||
|
* requesting to verify the user.
|
||||||
|
* @param request The HTTP request.
|
||||||
|
* @param validationData The data needed to validate the user.
|
||||||
|
* @return An object that tells whether the user is verified.
|
||||||
|
*/
|
||||||
|
@PostMapping("/auth/validate-foreign-token")
|
||||||
|
public Object validateToken(HttpServletRequest request, @RequestBody ForeignTokenValidationRequest validationData) {
|
||||||
|
// TODO: Implement this!
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.andrewlalis.onyx.auth.api;
|
||||||
|
|
||||||
|
public record ForeignTokenValidationRequest(
|
||||||
|
String accessToken
|
||||||
|
) {}
|
|
@ -3,6 +3,7 @@ package com.andrewlalis.onyx.auth.components;
|
||||||
import com.andrewlalis.onyx.auth.model.User;
|
import com.andrewlalis.onyx.auth.model.User;
|
||||||
import com.andrewlalis.onyx.content.dao.ContentNodeRepository;
|
import com.andrewlalis.onyx.content.dao.ContentNodeRepository;
|
||||||
import com.andrewlalis.onyx.content.model.ContentNode;
|
import com.andrewlalis.onyx.content.model.ContentNode;
|
||||||
|
import com.andrewlalis.onyx.content.service.ContentAccessService;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
@ -25,6 +26,7 @@ import java.io.IOException;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ContentAccessFilter extends OncePerRequestFilter {
|
public class ContentAccessFilter extends OncePerRequestFilter {
|
||||||
private final ContentNodeRepository contentNodeRepository;
|
private final ContentNodeRepository contentNodeRepository;
|
||||||
|
private final ContentAccessService contentAccessService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(
|
protected void doFilterInternal(
|
||||||
|
@ -48,8 +50,6 @@ public class ContentAccessFilter extends OncePerRequestFilter {
|
||||||
log.warn("Node doesn't exist!");
|
log.warn("Node doesn't exist!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TokenAuthentication auth = (TokenAuthentication) SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
User user = auth.getPrincipal();
|
|
||||||
// TODO: Actually check access rules.
|
// TODO: Actually check access rules.
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
package com.andrewlalis.onyx.auth.components;
|
|
||||||
|
|
||||||
import com.andrewlalis.onyx.auth.service.TokenService;
|
|
||||||
import com.andrewlalis.onyx.auth.service.UserService;
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import io.jsonwebtoken.Jws;
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
public class JwtFilter extends OncePerRequestFilter {
|
|
||||||
private final UserService userService;
|
|
||||||
private final TokenService tokenService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(
|
|
||||||
HttpServletRequest request,
|
|
||||||
HttpServletResponse response,
|
|
||||||
FilterChain filterChain
|
|
||||||
) throws ServletException, IOException {
|
|
||||||
try {
|
|
||||||
Jws<Claims> jws = tokenService.getToken(request);
|
|
||||||
if (jws != null) {
|
|
||||||
long userId = Long.parseLong(jws.getBody().getSubject());
|
|
||||||
userService.findById(userId).ifPresent(user -> SecurityContextHolder.getContext()
|
|
||||||
.setAuthentication(new TokenAuthentication(user, jws))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Exception occurred in JwtFilter.", e);
|
|
||||||
}
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.andrewlalis.onyx.auth.components;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.model.User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type of token authentication that's used for users of this Onyx node,
|
||||||
|
* where we have full access to the user and their info.
|
||||||
|
*/
|
||||||
|
public class LocalUserAuth extends TokenAuth {
|
||||||
|
private final User user;
|
||||||
|
|
||||||
|
public LocalUserAuth(String token, User user) {
|
||||||
|
super(token);
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getPrincipal() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return user.getUsername();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.andrewlalis.onyx.auth.components;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.model.NetworkUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type of token authentication that's used for users of Onyx nodes networked
|
||||||
|
* with this one. We don't have full access to their user data, but we can talk
|
||||||
|
* to the networked node to get some basic information about the user.
|
||||||
|
*/
|
||||||
|
public class NetworkUserAuth extends TokenAuth {
|
||||||
|
private final NetworkUser user;
|
||||||
|
|
||||||
|
public NetworkUserAuth(String token, NetworkUser user) {
|
||||||
|
super(token);
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NetworkUser getPrincipal() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return user.username();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.andrewlalis.onyx.auth.components;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An authentication instance that's used to represent public access to the
|
||||||
|
* API.
|
||||||
|
*/
|
||||||
|
public class PublicUserAuth implements Authentication {
|
||||||
|
// TODO: Add some sort of info here, not just authentication.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCredentials() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getDetails() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getPrincipal() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAuthenticated() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,5 @@
|
||||||
package com.andrewlalis.onyx.auth.components;
|
package com.andrewlalis.onyx.auth.components;
|
||||||
|
|
||||||
import com.andrewlalis.onyx.auth.model.User;
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import io.jsonwebtoken.Jws;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
|
@ -10,12 +7,16 @@ import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The authentication implementation that's used when a user logs in with an
|
* An abstract base class for any authentication instance based on the use of
|
||||||
* access token.
|
* a JWT for authentication.
|
||||||
* @param user The user that the token belongs to.
|
|
||||||
* @param jws The raw token.
|
|
||||||
*/
|
*/
|
||||||
public record TokenAuthentication(User user, Jws<Claims> jws) implements Authentication {
|
public abstract class TokenAuth implements Authentication {
|
||||||
|
public final String token;
|
||||||
|
|
||||||
|
protected TokenAuth(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
@ -23,7 +24,7 @@ public record TokenAuthentication(User user, Jws<Claims> jws) implements Authent
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getCredentials() {
|
public Object getCredentials() {
|
||||||
return this.jws;
|
return this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -31,11 +32,6 @@ public record TokenAuthentication(User user, Jws<Claims> jws) implements Authent
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public User getPrincipal() {
|
|
||||||
return this.user;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isAuthenticated() {
|
public boolean isAuthenticated() {
|
||||||
return true;
|
return true;
|
||||||
|
@ -45,9 +41,4 @@ public record TokenAuthentication(User user, Jws<Claims> jws) implements Authent
|
||||||
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
|
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
|
||||||
throw new RuntimeException("Cannot set the authenticated status of TokenAuthentication.");
|
throw new RuntimeException("Cannot set the authenticated status of TokenAuthentication.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return user.getUsername();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package com.andrewlalis.onyx.auth.components;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.model.User;
|
||||||
|
import com.andrewlalis.onyx.auth.service.TokenService;
|
||||||
|
import com.andrewlalis.onyx.auth.service.UserService;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jws;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that extracts and verifies an HTTP request's JWT, and uses that
|
||||||
|
* to set the current security context's authentication instance accordingly.
|
||||||
|
* <p>
|
||||||
|
* If the JWT originates from this Onyx node, then we simply fetch the User
|
||||||
|
* and set a new {@link LocalUserAuth}.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* If the JWT originates from a networked Onyx node, then we'll try and
|
||||||
|
* confirm with the original node that the JWT is valid, and then set a new
|
||||||
|
* {@link NetworkUserAuth}.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class TokenAuthFilter extends OncePerRequestFilter {
|
||||||
|
private final UserService userService;
|
||||||
|
private final TokenService tokenService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain
|
||||||
|
) throws ServletException, IOException {
|
||||||
|
final var securityContext = SecurityContextHolder.getContext();
|
||||||
|
boolean authContextSet = false;
|
||||||
|
try {
|
||||||
|
String token = tokenService.extractBearerToken(request);
|
||||||
|
Jws<Claims> jws = tokenService.parseToken(token);
|
||||||
|
if (jws != null) {
|
||||||
|
long userId = Long.parseLong(jws.getBody().getSubject());
|
||||||
|
Optional<User> optionalUser = userService.findById(userId);
|
||||||
|
if (optionalUser.isPresent()) {
|
||||||
|
securityContext.setAuthentication(new LocalUserAuth(token, optionalUser.get()));
|
||||||
|
authContextSet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Exception occurred in JwtFilter.", e);
|
||||||
|
}
|
||||||
|
// TODO: Check if the request is coming from a network user, then validate their token.
|
||||||
|
if (!authContextSet) {
|
||||||
|
securityContext.setAuthentication(new PublicUserAuth());
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.andrewlalis.onyx.auth.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to a {@link User}, the NetworkUser contains the information for a
|
||||||
|
* user who's coming to this node from another in the network. We don't have
|
||||||
|
* as much information as with a normal user, but enough to work with.
|
||||||
|
* @param id The user's id, as it is defined by its host Onyx node.
|
||||||
|
* @param username The user's username, as it is defined by its host Onyx node.
|
||||||
|
* @param displayName The user's display name.
|
||||||
|
*/
|
||||||
|
public record NetworkUser(
|
||||||
|
long id,
|
||||||
|
String username,
|
||||||
|
String displayName
|
||||||
|
) {}
|
|
@ -142,7 +142,7 @@ public class TokenService {
|
||||||
|
|
||||||
public long getTokenExpiration(HttpServletRequest request) {
|
public long getTokenExpiration(HttpServletRequest request) {
|
||||||
try {
|
try {
|
||||||
Jws<Claims> jws = getToken(request);
|
Jws<Claims> jws = parseToken(extractBearerToken(request));
|
||||||
return jws.getBody().getExpiration().getTime();
|
return jws.getBody().getExpiration().getTime();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Exception occurred while getting token expiration.", e);
|
log.warn("Exception occurred while getting token expiration.", e);
|
||||||
|
@ -150,16 +150,15 @@ public class TokenService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Jws<Claims> getToken(HttpServletRequest request) throws Exception {
|
public Jws<Claims> parseToken(String token) throws Exception {
|
||||||
String rawToken = extractBearerToken(request);
|
if (token == null) return null;
|
||||||
if (rawToken == null) return null;
|
|
||||||
JwtParserBuilder parserBuilder = Jwts.parserBuilder()
|
JwtParserBuilder parserBuilder = Jwts.parserBuilder()
|
||||||
.setSigningKey(getSigningKey())
|
.setSigningKey(getSigningKey())
|
||||||
.requireIssuer(ISSUER);
|
.requireIssuer(ISSUER);
|
||||||
return parserBuilder.build().parseClaimsJws(rawToken);
|
return parserBuilder.build().parseClaimsJws(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractBearerToken(HttpServletRequest request) {
|
public String extractBearerToken(HttpServletRequest request) {
|
||||||
String authorizationHeader = request.getHeader("Authorization");
|
String authorizationHeader = request.getHeader("Authorization");
|
||||||
if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_PREFIX)) return null;
|
if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_PREFIX)) return null;
|
||||||
String rawToken = authorizationHeader.substring(BEARER_PREFIX.length());
|
String rawToken = authorizationHeader.substring(BEARER_PREFIX.length());
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package com.andrewlalis.onyx.config;
|
package com.andrewlalis.onyx.config;
|
||||||
|
|
||||||
import com.andrewlalis.onyx.auth.components.ContentAccessFilter;
|
import com.andrewlalis.onyx.auth.components.ContentAccessFilter;
|
||||||
import com.andrewlalis.onyx.auth.components.JwtFilter;
|
import com.andrewlalis.onyx.auth.components.TokenAuthFilter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
@ -12,28 +12,30 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
private final JwtFilter jwtFilter;
|
private final TokenAuthFilter tokenAuthFilter;
|
||||||
private final ContentAccessFilter contentAccessFilter;
|
private final ContentAccessFilter contentAccessFilter;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http.authorizeHttpRequests(registry -> {
|
http.authorizeHttpRequests(registry -> {
|
||||||
|
// Public endpoints that require no authentication.
|
||||||
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/auth/login")).permitAll();
|
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/auth/login")).permitAll();
|
||||||
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/auth/access")).permitAll();
|
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/auth/access")).permitAll();
|
||||||
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/auth/token-expiration")).permitAll();
|
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/auth/token-expiration")).permitAll();
|
||||||
|
|
||||||
|
// Any path not explicitly listed here requires authentication to access.
|
||||||
registry.anyRequest().authenticated();
|
registry.anyRequest().authenticated();
|
||||||
});
|
});
|
||||||
http.csrf(AbstractHttpConfigurer::disable);
|
http.csrf(AbstractHttpConfigurer::disable);
|
||||||
http.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.NEVER));
|
http.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.NEVER));
|
||||||
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(tokenAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
http.addFilterAfter(contentAccessFilter, JwtFilter.class);
|
http.addFilterAfter(contentAccessFilter, TokenAuthFilter.class);
|
||||||
http.cors(configurer -> configurer.configure(http));
|
http.cors(configurer -> configurer.configure(http));
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.andrewlalis.onyx.content.dao;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.content.model.access.ContentAccessRules;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ContentAccessRulesRepository extends JpaRepository<ContentAccessRules, Long> {
|
||||||
|
Optional<ContentAccessRules> findByContentNodeId(long contentNodeId);
|
||||||
|
}
|
|
@ -13,4 +13,11 @@ public interface ContentNodeRepository extends JpaRepository<ContentNode, Long>
|
||||||
|
|
||||||
@Query("SELECT cn FROM ContentNode cn WHERE cn.name = '" + ContentNode.ROOT_NODE_NAME + "'")
|
@Query("SELECT cn FROM ContentNode cn WHERE cn.name = '" + ContentNode.ROOT_NODE_NAME + "'")
|
||||||
ContentNode findRoot();
|
ContentNode findRoot();
|
||||||
|
|
||||||
|
interface ParentContainerId {
|
||||||
|
long getParentContainerId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT cn.parentContainer.id FROM ContentNode cn WHERE cn.id = :nodeId")
|
||||||
|
ParentContainerId getParentId(long nodeId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.andrewlalis.onyx.content.dao;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.content.model.access.UserContentAccessRule;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserContentAccessRuleRepository extends JpaRepository<UserContentAccessRule, Long> {
|
||||||
|
@Query("SELECT ucar FROM UserContentAccessRule ucar " +
|
||||||
|
"WHERE ucar.contentAccessRules.contentNode.id = :nodeId AND " +
|
||||||
|
"ucar.user.id = :userId")
|
||||||
|
Optional<UserContentAccessRule> findByContentNodeIdAndUser(long nodeId, long userId);
|
||||||
|
|
||||||
|
@Query("SELECT ucar FROM UserContentAccessRule ucar WHERE ucar.contentAccessRules.contentNode.id IN :nodeIds")
|
||||||
|
List<UserContentAccessRule> findAllByContentNodeIds(List<Long> nodeIds);
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package com.andrewlalis.onyx.content.service;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.model.User;
|
||||||
|
import com.andrewlalis.onyx.content.dao.ContentAccessRulesRepository;
|
||||||
|
import com.andrewlalis.onyx.content.dao.ContentNodeRepository;
|
||||||
|
import com.andrewlalis.onyx.content.dao.UserContentAccessRuleRepository;
|
||||||
|
import com.andrewlalis.onyx.content.model.ContentNode;
|
||||||
|
import com.andrewlalis.onyx.content.model.access.ContentAccessLevel;
|
||||||
|
import com.andrewlalis.onyx.content.model.access.ContentAccessRules;
|
||||||
|
import com.andrewlalis.onyx.content.model.access.UserContentAccessRule;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that's responsible for determining if a user has permission to
|
||||||
|
* interact with certain content nodes.
|
||||||
|
* TODO: Add some sort of caching so recursive traversal of the content graph isn't needed.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ContentAccessService {
|
||||||
|
private final ContentAccessRulesRepository accessRulesRepository;
|
||||||
|
private final ContentNodeRepository contentNodeRepository;
|
||||||
|
private final UserContentAccessRuleRepository userContentAccessRuleRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public boolean currentAuthCanReadContent(long nodeId) {
|
||||||
|
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
var a = getEffectiveAccessLevel(user, nodeId);
|
||||||
|
return a == ContentAccessLevel.VIEW || a == ContentAccessLevel.EDIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public boolean currentAuthCanEditContent(long nodeId) {
|
||||||
|
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
var a = getEffectiveAccessLevel(user, nodeId);
|
||||||
|
return a == ContentAccessLevel.EDIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the effective access level that a user has to a particular content
|
||||||
|
* node. It finds this using the following algorithm:
|
||||||
|
* <ol>
|
||||||
|
* <li>Get a list of ids for this node and all its parents.</li>
|
||||||
|
* <li>
|
||||||
|
* First, recursively check the user-specific access levels for
|
||||||
|
* this and all parent nodes. If there exists a node with a user-
|
||||||
|
* specific access level for the user, then that's returned.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* Otherwise, recursively check the generic access levels for this
|
||||||
|
* and all parent nodes. The first non-INHERIT access level is
|
||||||
|
* returned.
|
||||||
|
* </li>
|
||||||
|
* </ol>
|
||||||
|
* The root node should not logically ever have an INHERIT access level, so
|
||||||
|
* it's the last resort if no others are found.
|
||||||
|
* @param user The user to get the access level for.
|
||||||
|
* @param nodeId The id of the content node to get the access level for.
|
||||||
|
* @return The access level that the given user has to the given node.
|
||||||
|
*/
|
||||||
|
private ContentAccessLevel getEffectiveAccessLevel(User user, long nodeId) {
|
||||||
|
List<Long> nodeIds = getAllNodeIds(nodeId);
|
||||||
|
for (long nId : nodeIds) {
|
||||||
|
Optional<UserContentAccessRule> userAccessRule = userContentAccessRuleRepository.findByContentNodeIdAndUser(nId, user.getId());
|
||||||
|
if (userAccessRule.isPresent() && userAccessRule.get().getAccessLevel() != ContentAccessLevel.INHERIT) {
|
||||||
|
return userAccessRule.get().getAccessLevel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (long nId : nodeIds) {
|
||||||
|
ContentAccessRules accessRules = accessRulesRepository.findByContentNodeId(nId).orElseThrow();
|
||||||
|
// TODO: Check the user's origin: anonymous, network, or node.
|
||||||
|
// For now, we assume node.
|
||||||
|
if (accessRules.getNodeAccessLevel() != ContentAccessLevel.INHERIT) {
|
||||||
|
return accessRules.getNodeAccessLevel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ContentAccessLevel.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> getAllNodeIds(long nodeId) {
|
||||||
|
List<Long> nodeIds = new ArrayList<>();
|
||||||
|
nodeIds.add(nodeId);
|
||||||
|
ContentNode node = contentNodeRepository.findById(nodeId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
ContentNode parent = node.getParentContainer();
|
||||||
|
while (parent != null) {
|
||||||
|
nodeIds.add(parent.getId());
|
||||||
|
parent = parent.getParentContainer();
|
||||||
|
}
|
||||||
|
return nodeIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ContentAccessType {
|
||||||
|
PUBLIC,
|
||||||
|
NETWORK,
|
||||||
|
NODE
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue