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)
.flatMap(currentUserName -> client.get(User.class, currentUserName))
.flatMap(currentUser -> request.bodyToMono(User.class)
.filter(user ->
Objects.equals(user.getMetadata().getName(),
currentUser.getMetadata().getName()))
.filter(user -> user.getMetadata() != null
&& Objects.equals(user.getMetadata().getName(),
currentUser.getMetadata().getName())
)
.switchIfEmpty(
Mono.error(() -> new ServerWebInputException("Username didn't match.")))
.map(user -> {
currentUser.getMetadata().setAnnotations(user.getMetadata().getAnnotations());
var spec = currentUser.getSpec();
var newSpec = user.getSpec();
spec.setAvatar(newSpec.getAvatar());

View File

@ -1,31 +1,42 @@
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.List;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.retry.support.RetryTemplate;
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.UserConnection;
import run.halo.app.core.extension.service.RoleService;
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.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.controller.Reconciler.Request;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.infra.utils.PathUtils;
@Slf4j
@Component
@AllArgsConstructor
@RequiredArgsConstructor
public class UserReconciler implements Reconciler<Request> {
private static final String FINALIZER_NAME = "user-protection";
private final ExtensionClient client;
private final ExternalUrlSupplier externalUrlSupplier;
private final RoleService roleService;
private final RetryTemplate retryTemplate = RetryTemplate.builder()
.maxAttempts(20)
.fixedBackoff(300)
@ -41,11 +52,43 @@ public class UserReconciler implements Reconciler<Request> {
}
addFinalizerIfNecessary(user);
ensureRoleNamesAnno(request.name());
updatePermalink(request.name());
});
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) {
client.fetch(User.class, name).ifPresent(user -> {
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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
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.URISyntaxException;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
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.service.RoleService;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.controller.Reconciler;
@ -33,18 +40,23 @@ import run.halo.app.infra.ExternalUrlSupplier;
*/
@ExtendWith(MockitoExtension.class)
class UserReconcilerTest {
@Mock
private ApplicationEventPublisher eventPublisher;
@Mock
private ExternalUrlSupplier externalUrlSupplier;
@Mock
private ExtensionClient client;
@Mock
private RoleService roleService;
@InjectMocks
private UserReconciler userReconciler;
@BeforeEach
void setUp() {
lenient().when(roleService.listRoleRefs(any())).thenReturn(Flux.empty());
}
@Test
void permalinkForFakeUser() throws URISyntaxException {
when(externalUrlSupplier.get()).thenReturn(new URI("http://localhost:8090"));
@ -52,10 +64,10 @@ class UserReconcilerTest {
when(client.fetch(eq(User.class), eq("fake-user")))
.thenReturn(Optional.of(user("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);
verify(client, times(1)).update(captor.capture());
verify(client, times(2)).update(captor.capture());
assertThat(captor.getValue().getStatus().getPermalink())
.isEqualTo("http://localhost:8090/authors/fake-user");
}
@ -65,7 +77,35 @@ class UserReconcilerTest {
when(client.fetch(eq(User.class), eq(AnonymousUserConst.PRINCIPAL)))
.thenReturn(Optional.of(user(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) {