mirror of https://github.com/halo-dev/halo
Add rolebinding reconciler to reset role names of users (#2217)
* Add rolebinding reconciler to reset role names of users Signed-off-by: johnniang <johnniang@fastmail.com> * Add @Bean annotations on roleBindingController * Fix errors complained by unneccesary catch Signed-off-by: johnniang <johnniang@fastmail.com>pull/2222/head
parent
f62d089237
commit
2afe19f1b9
|
@ -6,7 +6,9 @@ import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import run.halo.app.core.extension.Role;
|
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.reconciler.RoleBindingReconciler;
|
||||||
import run.halo.app.core.extension.reconciler.RoleReconciler;
|
import run.halo.app.core.extension.reconciler.RoleReconciler;
|
||||||
import run.halo.app.core.extension.reconciler.UserReconciler;
|
import run.halo.app.core.extension.reconciler.UserReconciler;
|
||||||
import run.halo.app.extension.DefaultExtensionClient;
|
import run.halo.app.extension.DefaultExtensionClient;
|
||||||
|
@ -62,4 +64,12 @@ public class ExtensionConfiguration {
|
||||||
.extension(new Role())
|
.extension(new Role())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Controller roleBindingController(ExtensionClient client) {
|
||||||
|
return new ControllerBuilder("role-binding-controller", client)
|
||||||
|
.reconciler(new RoleBindingReconciler(client))
|
||||||
|
.extension(new RoleBinding())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.util.Lazy;
|
||||||
|
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.extension.ExtensionClient;
|
||||||
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class RoleBindingReconciler implements Reconciler {
|
||||||
|
|
||||||
|
static final String ROLE_NAMES_ANNO = "rbac.authorization.halo.run/role-names";
|
||||||
|
|
||||||
|
private final ExtensionClient client;
|
||||||
|
|
||||||
|
public RoleBindingReconciler(ExtensionClient client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result reconcile(Request request) {
|
||||||
|
client.fetch(RoleBinding.class, request.name()).ifPresent(roleBinding -> {
|
||||||
|
// get all usernames;
|
||||||
|
var usernames = roleBinding.getSubjects().stream()
|
||||||
|
.filter(subject -> "User".equals(subject.getKind()))
|
||||||
|
.map(Subject::getName)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// get all role-bindings lazily
|
||||||
|
var bindings =
|
||||||
|
Lazy.of(() -> client.list(RoleBinding.class, containsUser(usernames), null));
|
||||||
|
|
||||||
|
usernames.forEach(username -> {
|
||||||
|
var roleNames = bindings.get().stream()
|
||||||
|
.filter(containsUser(username))
|
||||||
|
.map(RoleBinding::getRoleRef)
|
||||||
|
.filter(roleRef -> Objects.equals(roleRef.getKind(), "Role"))
|
||||||
|
.map(RoleBinding.RoleRef::getName)
|
||||||
|
.sorted()
|
||||||
|
// we have to use LinkedHashSet below to make sure the sorted above functional
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
// we should update the role names even if the role names are empty
|
||||||
|
client.fetch(User.class, username).ifPresent(user -> {
|
||||||
|
var annotations = user.getMetadata().getAnnotations();
|
||||||
|
if (annotations == null) {
|
||||||
|
annotations = new HashMap<>();
|
||||||
|
}
|
||||||
|
var oldAnnotations = Map.copyOf(annotations);
|
||||||
|
annotations.put(ROLE_NAMES_ANNO, JsonUtils.objectToJson(roleNames));
|
||||||
|
user.getMetadata().setAnnotations(annotations);
|
||||||
|
if (!Objects.deepEquals(oldAnnotations, annotations)) {
|
||||||
|
// update user
|
||||||
|
client.update(user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return new Result(false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Predicate<RoleBinding> containsUser(String username) {
|
||||||
|
return roleBinding -> roleBinding.getMetadata().getDeletionTimestamp() == null
|
||||||
|
&& roleBinding.getSubjects().stream()
|
||||||
|
.anyMatch(subject -> "User".equals(subject.getKind())
|
||||||
|
&& username.equals(subject.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Predicate<RoleBinding> containsUser(Set<String> usernames) {
|
||||||
|
return roleBinding -> roleBinding.getMetadata().getDeletionTimestamp() == null
|
||||||
|
&& roleBinding.getSubjects().stream()
|
||||||
|
.anyMatch(subject -> "User".equals(subject.getKind())
|
||||||
|
&& usernames.contains(subject.getName()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
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.reconciler.RoleBindingReconciler.ROLE_NAMES_ANNO;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import run.halo.app.core.extension.RoleBinding;
|
||||||
|
import run.halo.app.core.extension.RoleBinding.RoleRef;
|
||||||
|
import run.halo.app.core.extension.RoleBinding.Subject;
|
||||||
|
import run.halo.app.core.extension.User;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
|
import run.halo.app.extension.controller.Reconciler.Result;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class RoleBindingReconcilerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionClient client;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
RoleBindingReconciler reconciler;
|
||||||
|
|
||||||
|
final Result doNotReEnQueue = new Result(false, null);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDoNothingIfRequestNotFound() {
|
||||||
|
var bindingName = "fake-binding-name";
|
||||||
|
when(client.fetch(RoleBinding.class, bindingName))
|
||||||
|
.thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
var result = reconciler.reconcile(new Reconciler.Request(bindingName));
|
||||||
|
assertEquals(doNotReEnQueue, result);
|
||||||
|
|
||||||
|
verify(client, times(1)).fetch(same(RoleBinding.class), anyString());
|
||||||
|
verify(client, never()).list(same(RoleBinding.class), any(), any());
|
||||||
|
verify(client, never()).fetch(same(User.class), anyString());
|
||||||
|
verify(client, never()).update(isA(User.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDoNothingIfNotContainAnyUserSubject() {
|
||||||
|
var bindingName = "fake-binding-name";
|
||||||
|
var binding = mock(RoleBinding.class);
|
||||||
|
|
||||||
|
when(client.fetch(RoleBinding.class, bindingName))
|
||||||
|
.thenReturn(Optional.of(binding));
|
||||||
|
when(binding.getSubjects()).thenReturn(List.of());
|
||||||
|
|
||||||
|
var result = reconciler.reconcile(new Reconciler.Request(bindingName));
|
||||||
|
assertEquals(doNotReEnQueue, result);
|
||||||
|
|
||||||
|
verify(client, times(1)).fetch(same(RoleBinding.class), anyString());
|
||||||
|
verify(client, never()).list(same(RoleBinding.class), any(), any());
|
||||||
|
verify(client, never()).fetch(same(User.class), anyString());
|
||||||
|
verify(client, never()).update(isA(User.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDoNothingIfUserNotFound() {
|
||||||
|
var bindingName = "fake-binding-name";
|
||||||
|
var userName = "fake-user-name";
|
||||||
|
var binding = mock(RoleBinding.class);
|
||||||
|
var subject = mock(Subject.class);
|
||||||
|
|
||||||
|
when(client.fetch(RoleBinding.class, bindingName))
|
||||||
|
.thenReturn(Optional.of(binding));
|
||||||
|
when(binding.getSubjects()).thenReturn(List.of(subject));
|
||||||
|
when(subject.getKind()).thenReturn("User");
|
||||||
|
when(subject.getName()).thenReturn(userName);
|
||||||
|
when(client.fetch(User.class, userName)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
var result = reconciler.reconcile(new Reconciler.Request(bindingName));
|
||||||
|
assertEquals(doNotReEnQueue, result);
|
||||||
|
|
||||||
|
verify(client, times(1)).fetch(same(RoleBinding.class), anyString());
|
||||||
|
verify(client, times(1)).list(same(RoleBinding.class), any(), any());
|
||||||
|
verify(client, times(1)).fetch(same(User.class), eq(userName));
|
||||||
|
|
||||||
|
verify(client, never()).update(isA(User.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateRoleNamesIfNoBindingRelatedTheUser() {
|
||||||
|
var bindingName = "fake-binding-name";
|
||||||
|
var userName = "fake-user-name";
|
||||||
|
var subject = mock(Subject.class);
|
||||||
|
var binding = createRoleBinding("Role", "fake-role", false, subject);
|
||||||
|
var user = mock(User.class);
|
||||||
|
var userMetadata = mock(Metadata.class);
|
||||||
|
|
||||||
|
when(client.fetch(RoleBinding.class, bindingName))
|
||||||
|
.thenReturn(Optional.of(binding));
|
||||||
|
when(binding.getSubjects()).thenReturn(List.of(subject));
|
||||||
|
when(subject.getKind()).thenReturn("User");
|
||||||
|
when(subject.getName()).thenReturn(userName);
|
||||||
|
when(client.fetch(User.class, userName)).thenReturn(Optional.of(user));
|
||||||
|
when(user.getMetadata()).thenReturn(userMetadata);
|
||||||
|
when(client.list(same(RoleBinding.class), any(), any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
var result = reconciler.reconcile(new Reconciler.Request(bindingName));
|
||||||
|
assertEquals(doNotReEnQueue, result);
|
||||||
|
|
||||||
|
verify(client, times(1)).fetch(same(RoleBinding.class), anyString());
|
||||||
|
verify(client, times(1)).list(same(RoleBinding.class), any(), any());
|
||||||
|
verify(client, times(1)).fetch(same(User.class), anyString());
|
||||||
|
verify(client, times(1)).update(isA(User.class));
|
||||||
|
|
||||||
|
verify(userMetadata).setAnnotations(argThat(annotation -> {
|
||||||
|
String roleNames = annotation.get(ROLE_NAMES_ANNO);
|
||||||
|
return roleNames != null && roleNames.equals("[]");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateRoleNames() {
|
||||||
|
var bindingName = "fake-binding-name";
|
||||||
|
var userName = "fake-user-name";
|
||||||
|
var subject = mock(Subject.class);
|
||||||
|
var user = mock(User.class);
|
||||||
|
var userMetadata = mock(Metadata.class);
|
||||||
|
var binding = createRoleBinding("Role", "fake-role", false, subject);
|
||||||
|
|
||||||
|
when(client.fetch(RoleBinding.class, bindingName))
|
||||||
|
.thenReturn(Optional.of(binding));
|
||||||
|
when(binding.getSubjects()).thenReturn(List.of(subject));
|
||||||
|
|
||||||
|
when(subject.getKind()).thenReturn("User");
|
||||||
|
when(subject.getName()).thenReturn(userName);
|
||||||
|
when(client.fetch(User.class, userName)).thenReturn(Optional.of(user));
|
||||||
|
when(user.getMetadata()).thenReturn(userMetadata);
|
||||||
|
var bindings = List.of(
|
||||||
|
createRoleBinding("Role", "fake-role-01", false, subject),
|
||||||
|
createRoleBinding("Role", "fake-role-03", false, subject),
|
||||||
|
createRoleBinding("Role", "fake-role-02", false, subject),
|
||||||
|
createRoleBinding("NotRole", "fake-role-04", false, subject),
|
||||||
|
createRoleBinding("Role", "fake-role-05", true, subject)
|
||||||
|
);
|
||||||
|
when(client.list(same(RoleBinding.class), any(), any())).thenReturn(bindings);
|
||||||
|
|
||||||
|
var result = reconciler.reconcile(new Reconciler.Request(bindingName));
|
||||||
|
assertEquals(doNotReEnQueue, result);
|
||||||
|
|
||||||
|
verify(client, times(1)).fetch(same(RoleBinding.class), anyString());
|
||||||
|
verify(client, times(1)).list(same(RoleBinding.class), any(), any());
|
||||||
|
verify(client, times(1)).fetch(same(User.class), anyString());
|
||||||
|
verify(client, times(1)).update(isA(User.class));
|
||||||
|
|
||||||
|
verify(userMetadata).setAnnotations(argThat(annotation -> {
|
||||||
|
var roleNames = annotation.get(ROLE_NAMES_ANNO);
|
||||||
|
return roleNames != null && roleNames.equals("""
|
||||||
|
["fake-role-01","fake-role-02","fake-role-03"]""");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
RoleBinding createRoleBinding(String roleRefKind,
|
||||||
|
String roleRefName,
|
||||||
|
boolean deleting,
|
||||||
|
Subject subject) {
|
||||||
|
var binding = mock(RoleBinding.class);
|
||||||
|
var roleRef = mock(RoleRef.class);
|
||||||
|
var metadata = mock(Metadata.class);
|
||||||
|
lenient().when(roleRef.getKind()).thenReturn(roleRefKind);
|
||||||
|
lenient().when(roleRef.getName()).thenReturn(roleRefName);
|
||||||
|
lenient().when(metadata.getDeletionTimestamp()).thenReturn(deleting ? Instant.now() : null);
|
||||||
|
lenient().when(binding.getRoleRef()).thenReturn(roleRef);
|
||||||
|
lenient().when(binding.getMetadata()).thenReturn(metadata);
|
||||||
|
lenient().when(binding.getSubjects()).thenReturn(List.of(subject));
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue