mirror of https://github.com/halo-dev/halo
Support unbinding OAuth2User from Halo side (#6734)
#### What type of PR is this? /kind feature /area core /milestone 2.20.x #### What this PR does / why we need it: This PR provides an endpoint for disconnecting user connection. After the user connection is disconnected, an event `UserConnectionDisconnectedEvent` will be published for plugins. Now, OAuth2 plugin can simplify the authentication, binding and unbinding logic, please see the AuthProvider configuration snippet below: ```diff spec: authenticationUrl: /oauth2/authorization/github - bindingUrl: /apis/api.plugin.halo.run/v1alpha1/plugins/plugin-oauth2/connect/github + bindingUrl: /oauth2/authorization/github - unbindUrl: /apis/api.plugin.halo.run/v1alpha1/plugins/plugin-oauth2/disconnect/github + unbindUrl: /apis/uc.api.auth.halo.run/v1alpha1/user-connections/github/disconnect ``` Please note that, OAuth2 plugin can also define binding and unbinding endpoints by self. #### Special notes for your reviewer: OAuth2 test plugin: [plugin-oauth2-1.0.4-SNAPSHOT.zip](https://github.com/user-attachments/files/17184215/plugin-oauth2-1.0.4-SNAPSHOT.zip) #### Does this PR introduce a user-facing change? ```release-note None ```pull/6740/head
parent
c80ceb460d
commit
8a9b954969
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package run.halo.app.core.user.service;
|
package run.halo.app.core.user.service;
|
||||||
|
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.UserConnection;
|
import run.halo.app.core.extension.UserConnection;
|
||||||
|
|
||||||
|
@ -32,4 +33,13 @@ public interface UserConnectionService {
|
||||||
String registrationId, OAuth2User oauth2User
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,15 @@ import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.UserConnection;
|
import run.halo.app.core.extension.UserConnection;
|
||||||
import run.halo.app.core.extension.UserConnection.UserConnectionSpec;
|
import run.halo.app.core.extension.UserConnection.UserConnectionSpec;
|
||||||
import run.halo.app.core.user.service.UserConnectionService;
|
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.ListOptions;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.MetadataOperator;
|
import run.halo.app.extension.MetadataOperator;
|
||||||
|
@ -25,10 +28,14 @@ public class UserConnectionServiceImpl implements UserConnectionService {
|
||||||
|
|
||||||
private final ReactiveExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
private Clock clock = Clock.systemDefaultZone();
|
private Clock clock = Clock.systemDefaultZone();
|
||||||
|
|
||||||
public UserConnectionServiceImpl(ReactiveExtensionClient client) {
|
public UserConnectionServiceImpl(ReactiveExtensionClient client,
|
||||||
|
ApplicationEventPublisher eventPublisher) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
this.eventPublisher = eventPublisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setClock(Clock clock) {
|
void setClock(Clock clock) {
|
||||||
|
@ -91,6 +98,21 @@ public class UserConnectionServiceImpl implements UserConnectionService {
|
||||||
.flatMap(connection -> updateUserConnection(connection, oauth2User));
|
.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) {
|
private void updateUserInfo(MetadataOperator metadata, OAuth2User oauth2User) {
|
||||||
var annotations = Optional.ofNullable(metadata.getAnnotations())
|
var annotations = Optional.ofNullable(metadata.getAnnotations())
|
||||||
.orElseGet(HashMap::new);
|
.orElseGet(HashMap::new);
|
||||||
|
|
|
@ -29,6 +29,9 @@ rules:
|
||||||
resources: [ "plugins/bundle.js", "plugins/bundle.css" ]
|
resources: [ "plugins/bundle.js", "plugins/bundle.css" ]
|
||||||
resourceNames: [ "-" ]
|
resourceNames: [ "-" ]
|
||||||
verbs: [ "get" ]
|
verbs: [ "get" ]
|
||||||
|
- apisGroups: [ "uc.api.auth.halo.run" ]
|
||||||
|
resources: [ "user-connections/disconnect" ]
|
||||||
|
verbs: [ "update" ]
|
||||||
---
|
---
|
||||||
apiVersion: v1alpha1
|
apiVersion: v1alpha1
|
||||||
kind: "Role"
|
kind: "Role"
|
||||||
|
|
Loading…
Reference in New Issue