Support hooking user creation (#6945)

#### What type of PR is this?

/kind feature
/area core
/milestone 2.20.x

#### What this PR does / why we need it:

This PR adds support for hooking user creating. Plugin developers can define extension points of `UserPreCreatingHandler` and `UserPostCreatingHandler` to do something else.

#### Does this PR introduce a user-facing change?

```release-note
支持在插件中定义用户创建的前置和后置处理器
```
pull/6952/head
John Niang 2024-10-25 15:55:54 +08:00 committed by GitHub
parent 180b6b2b87
commit a0b352ac2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 99 additions and 7 deletions

View File

@ -0,0 +1,23 @@
package run.halo.app.core.user.service;
import org.pf4j.ExtensionPoint;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.User;
/**
* User post-creating handler.
*
* @author johnniang
* @since 2.20.8
*/
public interface UserPostCreatingHandler extends ExtensionPoint {
/**
* Do something after creating user.
*
* @param user create user.
* @return {@code Mono.empty()} if handling successfully.
*/
Mono<Void> postCreating(User user);
}

View File

@ -0,0 +1,23 @@
package run.halo.app.core.user.service;
import org.pf4j.ExtensionPoint;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.User;
/**
* User pre-creating handler.
*
* @author johnniang
* @since 2.20.8
*/
public interface UserPreCreatingHandler extends ExtensionPoint {
/**
* Do something before user creating.
*
* @param user modifiable user detail
* @return {@code Mono.empty()} if handling successfully.
*/
Mono<Void> preCreating(User user);
}

View File

@ -1,4 +1,4 @@
package run.halo.app.core.user.service;
package run.halo.app.core.user.service.impl;
import static run.halo.app.extension.ExtensionUtil.defaultSort;
import static run.halo.app.extension.index.query.QueryFactory.equal;
@ -25,6 +25,12 @@ import reactor.util.retry.Retry;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.RoleBinding;
import run.halo.app.core.extension.User;
import run.halo.app.core.user.service.EmailVerificationService;
import run.halo.app.core.user.service.RoleService;
import run.halo.app.core.user.service.SignUpData;
import run.halo.app.core.user.service.UserPostCreatingHandler;
import run.halo.app.core.user.service.UserPreCreatingHandler;
import run.halo.app.core.user.service.UserService;
import run.halo.app.event.user.PasswordChangedEvent;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.Metadata;
@ -38,6 +44,7 @@ import run.halo.app.infra.exception.DuplicateNameException;
import run.halo.app.infra.exception.EmailVerificationFailed;
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
import run.halo.app.infra.exception.UserNotFoundException;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
@Service
@RequiredArgsConstructor
@ -57,6 +64,8 @@ public class UserServiceImpl implements UserService {
private final EmailVerificationService emailVerificationService;
private final ExtensionGetter extensionGetter;
private Clock clock = Clock.systemUTC();
void setClock(Clock clock) {
@ -222,13 +231,20 @@ public class UserServiceImpl implements UserService {
)
.then();
})
.then(Mono.defer(() -> client.create(user)
.flatMap(newUser -> grantRoles(user.getMetadata().getName(), roleNames)
.retryWhen(
Retry.backoff(5, Duration.ofMillis(100))
.then(extensionGetter.getExtensions(UserPreCreatingHandler.class)
.concatMap(handler -> handler.preCreating(user))
.then(Mono.defer(() -> client.create(user)
.flatMap(newUser -> grantRoles(user.getMetadata().getName(), roleNames)
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
.filter(OptimisticLockingFailureException.class::isInstance)
)
)
))
.flatMap(createdUser -> extensionGetter.getExtensions(UserPostCreatingHandler.class)
.concatMap(handler -> handler.postCreating(createdUser))
.then()
.thenReturn(createdUser)
)
);
}

View File

@ -1,4 +1,4 @@
package run.halo.app.core.user.service;
package run.halo.app.core.user.service.impl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -8,6 +8,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.assertArg;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doReturn;
@ -19,6 +20,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static run.halo.app.extension.GroupVersionKind.fromExtension;
import java.util.HashMap;
import java.util.Set;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@ -37,6 +39,10 @@ import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.RoleBinding;
import run.halo.app.core.extension.RoleBinding.Subject;
import run.halo.app.core.extension.User;
import run.halo.app.core.user.service.RoleService;
import run.halo.app.core.user.service.SignUpData;
import run.halo.app.core.user.service.UserPostCreatingHandler;
import run.halo.app.core.user.service.UserPreCreatingHandler;
import run.halo.app.event.user.PasswordChangedEvent;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient;
@ -46,6 +52,7 @@ import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.exception.DuplicateNameException;
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
import run.halo.app.infra.exception.UserNotFoundException;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@ -65,6 +72,9 @@ class UserServiceImplTest {
@Mock
RoleService roleService;
@Mock
ExtensionGetter extensionGetter;
@InjectMocks
UserServiceImpl userService;
@ -305,6 +315,7 @@ class UserServiceImplTest {
@Nested
class SignUpTest {
@Test
void signUpWhenRegistrationNotAllowed() {
SystemSetting.User userSetting = new SystemSetting.User();
@ -354,6 +365,8 @@ class UserServiceImplTest {
when(passwordEncoder.encode(eq("fake-password"))).thenReturn("fake-password");
when(client.fetch(eq(User.class), eq("fake-user")))
.thenReturn(Mono.just(createFakeUser("test", "test")));
when(extensionGetter.getExtensions(UserPreCreatingHandler.class))
.thenReturn(Flux.empty());
var signUpData = createSignUpData("fake-user", "fake-password");
userService.signUp(signUpData)
@ -382,6 +395,20 @@ class UserServiceImplTest {
UserServiceImpl spyUserService = spy(userService);
doReturn(Mono.just(fakeUser)).when(spyUserService).grantRoles(eq("fake-user"),
anySet());
when(extensionGetter.getExtensions(UserPreCreatingHandler.class))
.thenReturn(Flux.just(user -> {
if (user.getMetadata().getAnnotations() == null) {
user.getMetadata().setAnnotations(new HashMap<>());
}
user.getMetadata().getAnnotations()
.put("pre.creating.handler.handled", "true");
return Mono.empty();
}));
when(extensionGetter.getExtensions(UserPostCreatingHandler.class))
.thenReturn(Flux.just(user -> {
assertEquals(fakeUser, user);
return Mono.empty();
}));
spyUserService.signUp(signUpData)
.as(StepVerifier::create)
@ -391,7 +418,10 @@ class UserServiceImplTest {
})
.verifyComplete();
verify(client).create(any(User.class));
verify(client).create(assertArg(u -> {
var handled = u.getMetadata().getAnnotations().get("pre.creating.handler.handled");
assertEquals("true", handled);
}));
verify(spyUserService).grantRoles(eq("fake-user"), anySet());
}