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;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -32,4 +33,13 @@ public interface UserConnectionService {
|
|||
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.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;
|
||||
|
@ -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) {
|
||||
|
@ -91,6 +98,21 @@ public class UserConnectionServiceImpl implements UserConnectionService {
|
|||
.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);
|
||||
|
|
|
@ -29,6 +29,9 @@ rules:
|
|||
resources: [ "plugins/bundle.js", "plugins/bundle.css" ]
|
||||
resourceNames: [ "-" ]
|
||||
verbs: [ "get" ]
|
||||
- apisGroups: [ "uc.api.auth.halo.run" ]
|
||||
resources: [ "user-connections/disconnect" ]
|
||||
verbs: [ "update" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: "Role"
|
||||
|
|
Loading…
Reference in New Issue