Skip to content

Commit

Permalink
Merge branch 'main' into feature/uc-attachments
Browse files Browse the repository at this point in the history
  • Loading branch information
ruibaby authored Sep 30, 2024
2 parents ddda643 + e11a494 commit d7e2364
Show file tree
Hide file tree
Showing 25 changed files with 455 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package run.halo.app.event.user;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;
import run.halo.app.core.extension.UserConnection;
import run.halo.app.plugin.SharedEvent;

/**
* An event that will be triggered after a user connection is disconnected.
*
* @author johnniang
* @since 2.20.0
*/
@SharedEvent
public class UserConnectionDisconnectedEvent extends ApplicationEvent {

@Getter
private final UserConnection userConnection;

public UserConnectionDisconnectedEvent(Object source, UserConnection userConnection) {
super(source);
this.userConnection = userConnection;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package run.halo.app.core.endpoint.uc;

import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;

import io.swagger.v3.oas.annotations.enums.ParameterIn;
import org.springdoc.core.fn.builders.parameter.Builder;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.core.extension.UserConnection;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.core.user.service.UserConnectionService;
import run.halo.app.extension.GroupVersion;

/**
* User connection endpoint.
*
* @author johnniang
* @since 2.20.0
*/
@Component
public class UserConnectionEndpoint implements CustomEndpoint {

private final UserConnectionService connectionService;

private final AuthenticationTrustResolver authenticationTrustResolver =
new AuthenticationTrustResolverImpl();

public UserConnectionEndpoint(UserConnectionService connectionService) {
this.connectionService = connectionService;
}

@Override
public RouterFunction<ServerResponse> endpoint() {
var tag = "UserConnectionV1alpha1Uc";
return SpringdocRouteBuilder.route()
.PUT(
"/user-connections/{registerId}/disconnect",
request -> {
var removedUserConnections = ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(authenticationTrustResolver::isAuthenticated)
.map(Authentication::getName)
.flatMapMany(username -> connectionService.removeUserConnection(
request.pathVariable("registerId"), username)
);
return ServerResponse.ok().body(removedUserConnections, UserConnection.class);
},
builder -> builder.operationId("DisconnectMyConnection")
.description("Disconnect my connection from a third-party platform.")
.tag(tag)
.parameter(Builder.parameterBuilder()
.in(ParameterIn.PATH)
.name("registerId")
.description("The registration ID of the third-party platform.")
.required(true)
.implementation(String.class)
)
.response(responseBuilder().implementationArray(UserConnection.class))
)
.build();
}

@Override
public GroupVersion groupVersion() {
return GroupVersion.parseAPIVersion("uc.api.auth.halo.run/v1alpha1");
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package run.halo.app.core.user.service;

import org.springframework.security.oauth2.core.user.OAuth2User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.UserConnection;

Expand Down Expand Up @@ -32,4 +33,13 @@ Mono<UserConnection> updateUserConnectionIfPresent(
String registrationId, OAuth2User oauth2User
);

/**
* Remove user connection.
*
* @param registrationId Registration ID
* @param username Username
* @return A list of user connections
*/
Flux<UserConnection> removeUserConnection(String registrationId, String username);

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
import java.time.Clock;
import java.util.HashMap;
import java.util.Optional;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.UserConnection;
import run.halo.app.core.extension.UserConnection.UserConnectionSpec;
import run.halo.app.core.user.service.UserConnectionService;
import run.halo.app.event.user.UserConnectionDisconnectedEvent;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.MetadataOperator;
Expand All @@ -25,10 +28,14 @@ public class UserConnectionServiceImpl implements UserConnectionService {

private final ReactiveExtensionClient client;

private final ApplicationEventPublisher eventPublisher;

private Clock clock = Clock.systemDefaultZone();

public UserConnectionServiceImpl(ReactiveExtensionClient client) {
public UserConnectionServiceImpl(ReactiveExtensionClient client,
ApplicationEventPublisher eventPublisher) {
this.client = client;
this.eventPublisher = eventPublisher;
}

void setClock(Clock clock) {
Expand Down Expand Up @@ -91,6 +98,21 @@ public Mono<UserConnection> updateUserConnectionIfPresent(String registrationId,
.flatMap(connection -> updateUserConnection(connection, oauth2User));
}

@Override
public Flux<UserConnection> removeUserConnection(String registrationId, String username) {
var listOptions = ListOptions.builder()
.fieldQuery(and(
equal("spec.registrationId", registrationId),
equal("spec.username", username)
))
.build();
return client.listAll(UserConnection.class, listOptions, defaultSort())
.flatMap(client::delete)
.doOnNext(deleted ->
eventPublisher.publishEvent(new UserConnectionDisconnectedEvent(this, deleted))
);
}

private void updateUserInfo(MetadataOperator metadata, OAuth2User oauth2User) {
var annotations = Optional.ofNullable(metadata.getAnnotations())
.orElseGet(HashMap::new);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
Expand All @@ -36,6 +37,7 @@
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.security.DefaultUserDetailService;
import run.halo.app.security.HaloServerRequestCache;
import run.halo.app.security.authentication.CryptoService;
import run.halo.app.security.authentication.SecurityConfigurer;
import run.halo.app.security.authentication.impl.RsaKeyService;
Expand Down Expand Up @@ -64,7 +66,8 @@ SecurityWebFilterChain filterChain(ServerHttpSecurity http,
ServerSecurityContextRepository securityContextRepository,
ReactiveExtensionClient client,
CryptoService cryptoService,
HaloProperties haloProperties) {
HaloProperties haloProperties,
ServerRequestCache serverRequestCache) {

var pathMatcher = pathMatchers("/**");
var staticResourcesMatcher = pathMatchers(HttpMethod.GET,
Expand Down Expand Up @@ -134,14 +137,20 @@ SecurityWebFilterChain filterChain(ServerHttpSecurity http,
haloProperties.getSecurity().getReferrerOptions().getPolicy())
)
.hsts(hstsSpec -> hstsSpec.includeSubdomains(false))
);
)
.requestCache(spec -> spec.requestCache(serverRequestCache));

// Integrate with other configurers separately
securityConfigurers.orderedStream()
.forEach(securityConfigurer -> securityConfigurer.configure(http));
return http.build();
}

@Bean
ServerRequestCache serverRequestCache() {
return new HaloServerRequestCache();
}

@Bean
ServerSecurityContextRepository securityContextRepository() {
return new WebSessionServerSecurityContextRepository();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import run.halo.app.content.PostContentService;
import run.halo.app.core.extension.service.AttachmentService;
import run.halo.app.extension.DefaultSchemeManager;
Expand Down Expand Up @@ -79,6 +80,12 @@ public static ApplicationContext create(ApplicationContext rootContext) {
.ifUnique(rateLimiterRegistry ->
beanFactory.registerSingleton("rateLimiterRegistry", rateLimiterRegistry)
);

// Authentication plugins may need this RequestCache to handle successful login redirect
rootContext.getBeanProvider(ServerRequestCache.class)
.ifUnique(serverRequestCache ->
beanFactory.registerSingleton("serverRequestCache", serverRequestCache)
);
// TODO add more shared instance here

sharedContext.refresh();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
import org.springframework.web.server.ServerWebExchange;
Expand All @@ -30,17 +31,19 @@ public class DefaultServerAuthenticationEntryPoint implements ServerAuthenticati

private final RedirectServerAuthenticationEntryPoint redirectEntryPoint;

public DefaultServerAuthenticationEntryPoint() {
this.redirectEntryPoint =
public DefaultServerAuthenticationEntryPoint(ServerRequestCache serverRequestCache) {
var entryPoint =
new RedirectServerAuthenticationEntryPoint("/login?authentication_required");
entryPoint.setRequestCache(serverRequestCache);
this.redirectEntryPoint = entryPoint;
}

@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
return xhrMatcher.matches(exchange)
.filter(MatchResult::isMatch)
.switchIfEmpty(
Mono.defer(() -> this.redirectEntryPoint.commence(exchange, ex)).then(Mono.empty())
Mono.defer(() -> this.redirectEntryPoint.commence(exchange, ex).then(Mono.empty()))
)
.flatMap(match -> Mono.defer(
() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher;
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler;
import org.springframework.security.web.server.authorization.ServerWebExchangeDelegatingServerAccessDeniedHandler;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.stereotype.Component;
Expand All @@ -24,10 +25,14 @@ public class ExceptionSecurityConfigurer implements SecurityConfigurer {

private final ServerResponse.Context context;

private final ServerRequestCache serverRequestCache;

public ExceptionSecurityConfigurer(MessageSource messageSource,
ServerResponse.Context context) {
ServerResponse.Context context,
ServerRequestCache serverRequestCache) {
this.messageSource = messageSource;
this.context = context;
this.serverRequestCache = serverRequestCache;
}

@Override
Expand Down Expand Up @@ -59,7 +64,7 @@ public void configure(ServerHttpSecurity http) {
));
entryPoints.add(new DelegatingServerAuthenticationEntryPoint.DelegateEntry(
exchange -> ServerWebExchangeMatcher.MatchResult.match(),
new DefaultServerAuthenticationEntryPoint()
new DefaultServerAuthenticationEntryPoint(serverRequestCache)
));

exception.authenticationEntryPoint(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package run.halo.app.security;

import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;

import java.net.URI;
import java.util.Collections;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.RequestPath;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import reactor.core.publisher.Mono;

/**
* Halo server request cache implementation for saving redirect URI from query.
*
* @author johnniang
*/
public class HaloServerRequestCache extends WebSessionServerRequestCache {

/**
* Currently, we have no idea to customize the sessionAttributeName in
* WebSessionServerRequestCache, so we have to copy the attr into here.
*/
private static final String DEFAULT_SAVED_REQUEST_ATTR = "SPRING_SECURITY_SAVED_REQUEST";

private static final String REDIRECT_URI_QUERY = "redirect_uri";

private final String sessionAttrName = DEFAULT_SAVED_REQUEST_ATTR;

public HaloServerRequestCache() {
super();
setSaveRequestMatcher(createDefaultRequestMatcher());
}

@Override
public Mono<Void> saveRequest(ServerWebExchange exchange) {
var redirectUriQuery = exchange.getRequest().getQueryParams().getFirst(REDIRECT_URI_QUERY);
if (StringUtils.isNotBlank(redirectUriQuery)) {
var redirectUri = URI.create(redirectUriQuery);
return saveRedirectUri(exchange, redirectUri);
}
return super.saveRequest(exchange);
}

@Override
public Mono<URI> getRedirectUri(ServerWebExchange exchange) {
return super.getRedirectUri(exchange);
}

@Override
public Mono<ServerHttpRequest> removeMatchingRequest(ServerWebExchange exchange) {
return super.removeMatchingRequest(exchange);
}

private Mono<Void> saveRedirectUri(ServerWebExchange exchange, URI redirectUri) {
var requestPath = exchange.getRequest().getPath();
var redirectPath = RequestPath.parse(redirectUri, requestPath.contextPath().value());
var query = redirectUri.getRawQuery();
var finalRedirect =
redirectPath.pathWithinApplication() + (query == null ? "" : "?" + query);
return exchange.getSession()
.map(WebSession::getAttributes)
.doOnNext(attributes -> attributes.put(this.sessionAttrName, finalRedirect))
.then();
}

private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
var get = pathMatchers(HttpMethod.GET, "/**");
var notFavicon = new NegatedServerWebExchangeMatcher(
pathMatchers(
"/favicon.*", "/login/**", "/signup/**", "/password-reset/**", "/challenges/**"
));
var html = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
return new AndServerWebExchangeMatcher(get, notFavicon, html);
}

}
Loading

0 comments on commit d7e2364

Please sign in to comment.