refactor: allow users to modify their own annotations in metadata (#3739)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.5.x

#### What this PR does / why we need it:
允许用户修改自己的元数据信息

how to test it
使用 API 修改元数据 `PUT localhost:8090/apis/api.console.halo.run/v1alpha1/users/-`
1. 修改 annotations 中的 `"rbac.authorization.halo.run/role-names": "[\"super-role\",\"fake-role\"]"` 会被复原
2. 修改其他的 annotations 能正确修改,也能增加新的 annotation

#### Which issue(s) this PR fixes:

Fixes #3544

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

```release-note
允许用户修改自己的元数据信息
```
pull/3767/head
guqing 2023-04-17 21:46:38 +08:00 committed by GitHub
parent eb141e4966
commit 602b783506
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 97 additions and 12 deletions

View File

@ -148,12 +148,14 @@ public class UserEndpoint implements CustomEndpoint {
.map(Authentication::getName) .map(Authentication::getName)
.flatMap(currentUserName -> client.get(User.class, currentUserName)) .flatMap(currentUserName -> client.get(User.class, currentUserName))
.flatMap(currentUser -> request.bodyToMono(User.class) .flatMap(currentUser -> request.bodyToMono(User.class)
.filter(user -> .filter(user -> user.getMetadata() != null
Objects.equals(user.getMetadata().getName(), && Objects.equals(user.getMetadata().getName(),
currentUser.getMetadata().getName())) currentUser.getMetadata().getName())
)
.switchIfEmpty( .switchIfEmpty(
Mono.error(() -> new ServerWebInputException("Username didn't match."))) Mono.error(() -> new ServerWebInputException("Username didn't match.")))
.map(user -> { .map(user -> {
currentUser.getMetadata().setAnnotations(user.getMetadata().getAnnotations());
var spec = currentUser.getSpec(); var spec = currentUser.getSpec();
var newSpec = user.getSpec(); var newSpec = user.getSpec();
spec.setAvatar(newSpec.getAvatar()); spec.setAvatar(newSpec.getAvatar());

View File

@ -1,31 +1,42 @@
package run.halo.app.core.extension.reconciler; package run.halo.app.core.extension.reconciler;
import static run.halo.app.core.extension.User.GROUP;
import static run.halo.app.core.extension.User.KIND;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.retry.support.RetryTemplate; import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
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.extension.User;
import run.halo.app.core.extension.UserConnection; import run.halo.app.core.extension.UserConnection;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.GroupKind;
import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler; import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.controller.Reconciler.Request; import run.halo.app.extension.controller.Reconciler.Request;
import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.infra.utils.PathUtils; import run.halo.app.infra.utils.PathUtils;
@Slf4j @Slf4j
@Component @Component
@AllArgsConstructor @RequiredArgsConstructor
public class UserReconciler implements Reconciler<Request> { public class UserReconciler implements Reconciler<Request> {
private static final String FINALIZER_NAME = "user-protection"; private static final String FINALIZER_NAME = "user-protection";
private final ExtensionClient client; private final ExtensionClient client;
private final ExternalUrlSupplier externalUrlSupplier; private final ExternalUrlSupplier externalUrlSupplier;
private final RoleService roleService;
private final RetryTemplate retryTemplate = RetryTemplate.builder() private final RetryTemplate retryTemplate = RetryTemplate.builder()
.maxAttempts(20) .maxAttempts(20)
.fixedBackoff(300) .fixedBackoff(300)
@ -41,11 +52,43 @@ public class UserReconciler implements Reconciler<Request> {
} }
addFinalizerIfNecessary(user); addFinalizerIfNecessary(user);
ensureRoleNamesAnno(request.name());
updatePermalink(request.name()); updatePermalink(request.name());
}); });
return new Result(false, null); return new Result(false, null);
} }
private void ensureRoleNamesAnno(String name) {
client.fetch(User.class, name).ifPresent(user -> {
Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(user);
Map<String, String> oldAnnotations = Map.copyOf(annotations);
List<String> roleNames = listRoleNamesRef(name);
annotations.put(User.ROLE_NAMES_ANNO, JsonUtils.objectToJson(roleNames));
if (!oldAnnotations.equals(annotations)) {
client.update(user);
}
});
}
List<String> listRoleNamesRef(String username) {
var subject = new RoleBinding.Subject(KIND, username, GROUP);
return roleService.listRoleRefs(subject)
.filter(this::isRoleRef)
.map(RoleBinding.RoleRef::getName)
.distinct()
.collectList()
.blockOptional()
.orElse(List.of());
}
private boolean isRoleRef(RoleBinding.RoleRef roleRef) {
var roleGvk = new Role().groupVersionKind();
var gk = new GroupKind(roleRef.getApiGroup(), roleRef.getKind());
return gk.equals(roleGvk.groupKind());
}
private void updatePermalink(String name) { private void updatePermalink(String name) {
client.fetch(User.class, name).ifPresent(user -> { client.fetch(User.class, name).ifPresent(user -> {
if (AnonymousUserConst.isAnonymousUser(name)) { if (AnonymousUserConst.isAnonymousUser(name)) {

View File

@ -3,22 +3,29 @@ package run.halo.app.core.extension.reconciler;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static run.halo.app.core.extension.User.GROUP;
import static run.halo.app.core.extension.User.KIND;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher; import reactor.core.publisher.Flux;
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.extension.User;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.controller.Reconciler; import run.halo.app.extension.controller.Reconciler;
@ -33,18 +40,23 @@ import run.halo.app.infra.ExternalUrlSupplier;
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class UserReconcilerTest { class UserReconcilerTest {
@Mock
private ApplicationEventPublisher eventPublisher;
@Mock @Mock
private ExternalUrlSupplier externalUrlSupplier; private ExternalUrlSupplier externalUrlSupplier;
@Mock @Mock
private ExtensionClient client; private ExtensionClient client;
@Mock
private RoleService roleService;
@InjectMocks @InjectMocks
private UserReconciler userReconciler; private UserReconciler userReconciler;
@BeforeEach
void setUp() {
lenient().when(roleService.listRoleRefs(any())).thenReturn(Flux.empty());
}
@Test @Test
void permalinkForFakeUser() throws URISyntaxException { void permalinkForFakeUser() throws URISyntaxException {
when(externalUrlSupplier.get()).thenReturn(new URI("http://localhost:8090")); when(externalUrlSupplier.get()).thenReturn(new URI("http://localhost:8090"));
@ -52,10 +64,10 @@ class UserReconcilerTest {
when(client.fetch(eq(User.class), eq("fake-user"))) when(client.fetch(eq(User.class), eq("fake-user")))
.thenReturn(Optional.of(user("fake-user"))); .thenReturn(Optional.of(user("fake-user")));
userReconciler.reconcile(new Reconciler.Request("fake-user")); userReconciler.reconcile(new Reconciler.Request("fake-user"));
verify(client, times(1)).update(any(User.class)); verify(client, times(2)).update(any(User.class));
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class); ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(client, times(1)).update(captor.capture()); verify(client, times(2)).update(captor.capture());
assertThat(captor.getValue().getStatus().getPermalink()) assertThat(captor.getValue().getStatus().getPermalink())
.isEqualTo("http://localhost:8090/authors/fake-user"); .isEqualTo("http://localhost:8090/authors/fake-user");
} }
@ -65,7 +77,35 @@ class UserReconcilerTest {
when(client.fetch(eq(User.class), eq(AnonymousUserConst.PRINCIPAL))) when(client.fetch(eq(User.class), eq(AnonymousUserConst.PRINCIPAL)))
.thenReturn(Optional.of(user(AnonymousUserConst.PRINCIPAL))); .thenReturn(Optional.of(user(AnonymousUserConst.PRINCIPAL)));
userReconciler.reconcile(new Reconciler.Request(AnonymousUserConst.PRINCIPAL)); userReconciler.reconcile(new Reconciler.Request(AnonymousUserConst.PRINCIPAL));
verify(client, times(0)).update(any(User.class)); verify(client, times(1)).update(any(User.class));
}
@Test
void ensureRoleNamesAnno() {
RoleBinding.RoleRef roleRef = new RoleBinding.RoleRef();
roleRef.setName("fake-role");
roleRef.setKind(Role.KIND);
roleRef.setApiGroup(Role.GROUP);
RoleBinding.RoleRef notworkRef = new RoleBinding.RoleRef();
notworkRef.setName("super-role");
notworkRef.setKind("Fake");
notworkRef.setApiGroup("fake.halo.run");
RoleBinding.Subject subject = new RoleBinding.Subject(KIND, "fake-user", GROUP);
when(roleService.listRoleRefs(eq(subject))).thenReturn(Flux.just(roleRef, notworkRef));
when(client.fetch(eq(User.class), eq("fake-user")))
.thenReturn(Optional.of(user("fake-user")));
when(externalUrlSupplier.get()).thenReturn(URI.create("/"));
userReconciler.reconcile(new Reconciler.Request("fake-user"));
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(client, times(2)).update(captor.capture());
User user = captor.getAllValues().get(1);
assertThat(user.getMetadata().getAnnotations().get(User.ROLE_NAMES_ANNO))
.isEqualTo("[\"fake-role\"]");
} }
User user(String name) { User user(String name) {