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
John Niang 2022-07-07 11:36:10 +08:00 committed by GitHub
parent f62d089237
commit 2afe19f1b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 286 additions and 0 deletions

View File

@ -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();
}
}

View File

@ -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()));
}
}

View File

@ -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;
}
}