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.ServerResponse;
|
||||
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.reconciler.RoleBindingReconciler;
|
||||
import run.halo.app.core.extension.reconciler.RoleReconciler;
|
||||
import run.halo.app.core.extension.reconciler.UserReconciler;
|
||||
import run.halo.app.extension.DefaultExtensionClient;
|
||||
|
@ -62,4 +64,12 @@ public class ExtensionConfiguration {
|
|||
.extension(new Role())
|
||||
.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