Add an endpoint to grant permissions to user (#2239)

#### What type of PR is this?

/kind feature
/kind api-change
/area core
/milestone 2.0

#### What this PR does / why we need it:

Add an endpoint to grant permissions to user.

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

Fixes #

#### Special notes for your reviewer:

Test steps:

1. Start Halo
2. Check the initial password in the console log
3. Request <http://localhost:8090/webjars/swagger-ui/index.html> from browser and you will be redirected to login page
4. Input the username(admin) and the password you got just now
5. Grant permission as you wish

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

```release-note
None
```
pull/2241/head
John Niang 2022-07-13 15:17:08 +08:00 committed by GitHub
parent de493ccb2c
commit faae645e88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 355 additions and 29 deletions

View File

@ -1,6 +1,9 @@
package run.halo.app.core.extension; package run.halo.app.core.extension;
import static java.util.Arrays.compare; import static java.util.Arrays.compare;
import static run.halo.app.core.extension.Role.GROUP;
import static run.halo.app.core.extension.Role.KIND;
import static run.halo.app.core.extension.Role.VERSION;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List; import java.util.List;
@ -18,13 +21,17 @@ import run.halo.app.extension.GVK;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true) @ToString(callSuper = true)
@GVK(group = "", @GVK(group = GROUP,
version = "v1alpha1", version = VERSION,
kind = "Role", kind = KIND,
plural = "roles", plural = "roles",
singular = "role") singular = "role")
public class Role extends AbstractExtension { public class Role extends AbstractExtension {
public static final String GROUP = "";
public static final String VERSION = "v1alpha1";
public static final String KIND = "Role";
@Schema(required = true) @Schema(required = true)
List<PolicyRule> rules; List<PolicyRule> rules;

View File

@ -1,13 +1,22 @@
package run.halo.app.core.extension; package run.halo.app.core.extension;
import static run.halo.app.core.extension.RoleBinding.GROUP;
import static run.halo.app.core.extension.RoleBinding.KIND;
import static run.halo.app.core.extension.RoleBinding.VERSION;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString; import lombok.ToString;
import run.halo.app.extension.AbstractExtension; import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.ExtensionOperator;
import run.halo.app.extension.GVK; import run.halo.app.extension.GVK;
import run.halo.app.extension.Metadata;
/** /**
* RoleBinding references a role, but does not contain it. * RoleBinding references a role, but does not contain it.
@ -20,13 +29,19 @@ import run.halo.app.extension.GVK;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true) @ToString(callSuper = true)
@GVK(group = "", @GVK(group = GROUP,
version = "v1alpha1", version = VERSION,
kind = "RoleBinding", kind = KIND,
plural = "rolebindings", plural = "rolebindings",
singular = "rolebinding") singular = "rolebinding")
public class RoleBinding extends AbstractExtension { public class RoleBinding extends AbstractExtension {
public static final String GROUP = "";
public static final String VERSION = "v1alpha1";
public static final String KIND = "RoleBinding";
/** /**
* Subjects holds references to the objects the role applies to. * Subjects holds references to the objects the role applies to.
*/ */
@ -91,5 +106,55 @@ public class RoleBinding extends AbstractExtension {
* Defaults to "rbac.authorization.halo.run" for User and Group subjects. * Defaults to "rbac.authorization.halo.run" for User and Group subjects.
*/ */
String apiGroup; String apiGroup;
public static Predicate<Subject> isUser(String username) {
return subject -> User.KIND.equals(subject.getKind())
&& User.GROUP.equals(subject.getApiGroup())
&& username.equals(subject.getName());
}
public static Predicate<Subject> containsUser(Set<String> usernames) {
return subject -> User.KIND.equals(subject.getKind())
&& User.GROUP.equals(subject.apiGroup)
&& usernames.contains(subject.getName());
}
}
public static RoleBinding create(String username, String roleName) {
var metadata = new Metadata();
metadata.setName(String.join("-", username, roleName, "binding"));
var roleRef = new RoleRef();
roleRef.setKind(Role.KIND);
roleRef.setName(roleName);
roleRef.setApiGroup(Role.GROUP);
var subject = new Subject();
subject.setKind(User.KIND);
subject.setName(username);
subject.setApiGroup(User.GROUP);
var binding = new RoleBinding();
binding.setMetadata(metadata);
binding.setRoleRef(roleRef);
// keep the subjects mutable
var subjects = new LinkedList<Subject>();
subjects.add(subject);
binding.setSubjects(subjects);
return binding;
}
public static Predicate<RoleBinding> containsUser(String username) {
return ExtensionOperator.<RoleBinding>isNotDeleted().and(
binding -> binding.getSubjects().stream()
.anyMatch(Subject.isUser(username)));
}
public static Predicate<RoleBinding> containsUser(Set<String> usernames) {
return ExtensionOperator.<RoleBinding>isNotDeleted()
.and(binding -> binding.getSubjects().stream()
.anyMatch(Subject.containsUser(usernames)));
} }
} }

View File

@ -1,5 +1,9 @@
package run.halo.app.core.extension; package run.halo.app.core.extension;
import static run.halo.app.core.extension.User.GROUP;
import static run.halo.app.core.extension.User.KIND;
import static run.halo.app.core.extension.User.VERSION;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
@ -17,13 +21,17 @@ import run.halo.app.extension.GVK;
@Data @Data
@ToString(callSuper = true) @ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@GVK(group = "", @GVK(group = GROUP,
version = "v1alpha1", version = VERSION,
kind = "User", kind = KIND,
singular = "user", singular = "user",
plural = "users") plural = "users")
public class User extends AbstractExtension { public class User extends AbstractExtension {
public static final String GROUP = "";
public static final String VERSION = "v1alpha1";
public static final String KIND = "User";
@Schema(required = true) @Schema(required = true)
private UserSpec spec; private UserSpec spec;

View File

@ -1,15 +1,26 @@
package run.halo.app.core.extension.endpoint; package run.halo.app.core.extension.endpoint;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import java.util.HashSet;
import java.util.Set;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
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.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.exception.ExtensionNotFoundException; import run.halo.app.extension.exception.ExtensionNotFoundException;
@ -35,13 +46,76 @@ public class UserEndpoint implements CustomEndpoint {
.bodyValue(user)); .bodyValue(user));
} }
Mono<ServerResponse> grantPermission(ServerRequest request) {
var username = request.pathVariable("name");
return request.bodyToMono(GrantRequest.class)
.switchIfEmpty(Mono.error(() -> new ServerWebInputException("Request body is empty")))
.flatMap(grant -> {
// preflight check
client.fetch(User.class, username)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"User " + username + " was not found"));
grant.roles.forEach(roleName -> client.fetch(Role.class, roleName)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Role " + roleName + " was not found")));
var bindings =
client.list(RoleBinding.class, RoleBinding.containsUser(username), null);
var bindingToUpdate = new HashSet<RoleBinding>();
var bindingToDelete = new HashSet<RoleBinding>();
var existingRoles = new HashSet<String>();
bindings.forEach(binding -> {
var roleName = binding.getRoleRef().getName();
if (grant.roles.contains(roleName)) {
existingRoles.add(roleName);
return;
}
binding.getSubjects().removeIf(RoleBinding.Subject.isUser(username));
if (CollectionUtils.isEmpty(binding.getSubjects())) {
// remove it if subjects is empty
bindingToDelete.add(binding);
} else {
bindingToUpdate.add(binding);
}
});
bindingToUpdate.forEach(client::update);
bindingToDelete.forEach(client::delete);
// remove existing roles
var roles = new HashSet<>(grant.roles);
roles.removeAll(existingRoles);
roles.stream()
.map(roleName -> RoleBinding.create(username, roleName))
.forEach(client::create);
return ServerResponse.ok().build();
});
}
record GrantRequest(Set<String> roles) {
}
@Override @Override
public RouterFunction<ServerResponse> endpoint() { public RouterFunction<ServerResponse> endpoint() {
var tag = "api.halo.run/v1alpha1/User";
return SpringdocRouteBuilder.route() return SpringdocRouteBuilder.route()
.GET("/users/-", this::me, builder -> builder.operationId("GetCurrentUserDetail") .GET("/users/-", this::me, builder -> builder.operationId("GetCurrentUserDetail")
.description("Get current user detail") .description("Get current user detail")
.tag("api.halo.run/v1alpha1/User") .tag(tag)
.response(responseBuilder().implementation(User.class))) .response(responseBuilder().implementation(User.class)))
.POST("/users/{name}/permissions", this::grantPermission,
builder -> builder.operationId("GrantPermission")
.description("Grant permissions to user")
.tag(tag)
.parameter(parameterBuilder().in(ParameterIn.PATH).name("name")
.description("User name")
.required(true))
.requestBody(
requestBodyBuilder().required(true).implementation(GrantRequest.class))
.response(responseBuilder().implementation(User.class)))
.build(); .build();
} }
} }

View File

@ -1,14 +1,15 @@
package run.halo.app.core.extension.reconciler; package run.halo.app.core.extension.reconciler;
import static run.halo.app.core.extension.RoleBinding.containsUser;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Lazy; import org.springframework.data.util.Lazy;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.RoleBinding;
import run.halo.app.core.extension.RoleBinding.Subject; import run.halo.app.core.extension.RoleBinding.Subject;
import run.halo.app.core.extension.User; import run.halo.app.core.extension.User;
@ -32,7 +33,7 @@ public class RoleBindingReconciler implements Reconciler {
client.fetch(RoleBinding.class, request.name()).ifPresent(roleBinding -> { client.fetch(RoleBinding.class, request.name()).ifPresent(roleBinding -> {
// get all usernames; // get all usernames;
var usernames = roleBinding.getSubjects().stream() var usernames = roleBinding.getSubjects().stream()
.filter(subject -> "User".equals(subject.getKind())) .filter(subject -> User.KIND.equals(subject.getKind()))
.map(Subject::getName) .map(Subject::getName)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -44,7 +45,7 @@ public class RoleBindingReconciler implements Reconciler {
var roleNames = bindings.get().stream() var roleNames = bindings.get().stream()
.filter(containsUser(username)) .filter(containsUser(username))
.map(RoleBinding::getRoleRef) .map(RoleBinding::getRoleRef)
.filter(roleRef -> Objects.equals(roleRef.getKind(), "Role")) .filter(roleRef -> Objects.equals(roleRef.getKind(), Role.KIND))
.map(RoleBinding.RoleRef::getName) .map(RoleBinding.RoleRef::getName)
.sorted() .sorted()
// we have to use LinkedHashSet below to make sure the sorted above functional // we have to use LinkedHashSet below to make sure the sorted above functional
@ -68,17 +69,4 @@ public class RoleBindingReconciler implements Reconciler {
return new Result(false, null); 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

@ -3,6 +3,7 @@ package run.halo.app.extension;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.util.function.Predicate;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -91,4 +92,7 @@ public interface ExtensionOperator {
return GroupVersionKind.fromAPIVersionAndKind(getApiVersion(), getKind()); return GroupVersionKind.fromAPIVersionAndKind(getApiVersion(), getKind());
} }
static <T extends ExtensionOperator> Predicate<T> isNotDeleted() {
return ext -> ext.getMetadata().getDeletionTimestamp() == null;
}
} }

View File

@ -0,0 +1,43 @@
package run.halo.app.core.extension;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Instant;
import java.util.List;
import org.junit.jupiter.api.Test;
import run.halo.app.extension.Metadata;
class RoleBindingTest {
@Test
void shouldContainUser() {
var subject = new RoleBinding.Subject();
subject.setName("fake-name");
subject.setApiGroup("");
subject.setKind("User");
var binding = new RoleBinding();
binding.setMetadata(new Metadata());
binding.setSubjects(List.of(subject));
assertTrue(RoleBinding.containsUser("fake-name").test(binding));
assertFalse(RoleBinding.containsUser("non-exist-fake-name").test(binding));
}
@Test
void shouldNotContainUserWhenBindingIsDeleted() {
var subject = new RoleBinding.Subject();
subject.setName("fake-name");
subject.setApiGroup("");
subject.setKind("User");
var binding = new RoleBinding();
var metadata = new Metadata();
metadata.setDeletionTimestamp(Instant.now());
binding.setMetadata(metadata);
binding.setSubjects(List.of(subject));
assertFalse(RoleBinding.containsUser("fake-name").test(binding));
assertFalse(RoleBinding.containsUser("non-exist-fake-name").test(binding));
}
}

View File

@ -1,11 +1,22 @@
package run.halo.app.core.extension.endpoint; package run.halo.app.core.extension.endpoint;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; 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.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 org.mockito.Mockito.when;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
@ -15,6 +26,7 @@ import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
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.service.RoleService; import run.halo.app.core.extension.service.RoleService;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
@ -44,6 +56,8 @@ class UserEndpointTest {
var role = new Role(); var role = new Role();
role.setRules(List.of(rule)); role.setRules(List.of(rule));
when(roleService.getRole(anyString())).thenReturn(role); when(roleService.getRole(anyString())).thenReturn(role);
// prevent from initializing the super admin.
when(client.fetch(User.class, "admin")).thenReturn(Optional.of(mock(User.class)));
} }
@Test @Test
@ -70,4 +84,99 @@ class UserEndpointTest {
.expectBody(User.class) .expectBody(User.class)
.isEqualTo(user); .isEqualTo(user);
} }
@Nested
class GrantPermissionEndpointTest {
@Test
@WithMockUser("fake-user")
void shouldGetBadRequestIfRequestBodyIsEmpty() {
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
.contentType(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isBadRequest();
// Why one more time to verify? Because the SuperAdminInitializer will fetch admin user.
verify(client, never()).fetch(same(User.class), eq("fake-user"));
verify(client, never()).fetch(same(Role.class), eq("fake-role"));
}
@Test
@WithMockUser("fake-user")
void shouldGetNotFoundIfUserNotFound() {
when(client.fetch(User.class, "fake-user")).thenReturn(Optional.empty());
when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.of(mock(Role.class)));
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new UserEndpoint.GrantRequest(Set.of("fake-role")))
.exchange()
.expectStatus().isNotFound();
verify(client, times(1)).fetch(same(User.class), eq("fake-user"));
verify(client, never()).fetch(same(Role.class), eq("fake-role"));
}
@Test
@WithMockUser("fake-user")
void shouldGetNotFoundIfRoleNotFound() {
when(client.fetch(User.class, "fake-user")).thenReturn(Optional.of(mock(User.class)));
when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.empty());
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new UserEndpoint.GrantRequest(Set.of("fake-role")))
.exchange()
.expectStatus().isNotFound();
verify(client, times(1)).fetch(same(User.class), eq("fake-user"));
verify(client, times(1)).fetch(same(Role.class), eq("fake-role"));
}
@Test
@WithMockUser("fake-user")
void shouldCreateRoleBindingIfNotExist() {
when(client.fetch(User.class, "fake-user")).thenReturn(Optional.of(mock(User.class)));
var role = mock(Role.class);
when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.of(role));
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new UserEndpoint.GrantRequest(Set.of("fake-role")))
.exchange()
.expectStatus().isOk();
verify(client, times(1)).fetch(same(User.class), eq("fake-user"));
verify(client, times(1)).fetch(same(Role.class), eq("fake-role"));
verify(client, times(1)).create(RoleBinding.create("fake-user", "fake-role"));
verify(client, never()).update(isA(RoleBinding.class));
verify(client, never()).delete(isA(RoleBinding.class));
}
@Test
@WithMockUser("fake-user")
void shouldDeleteRoleBindingIfNotProvided() {
when(client.fetch(User.class, "fake-user")).thenReturn(Optional.of(mock(User.class)));
var role = mock(Role.class);
when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.of(role));
var roleBinding = RoleBinding.create("fake-user", "non-provided-fake-role");
when(client.list(same(RoleBinding.class), any(), any())).thenReturn(
List.of(roleBinding));
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new UserEndpoint.GrantRequest(Set.of("fake-role")))
.exchange()
.expectStatus().isOk();
verify(client, times(1)).fetch(same(User.class), eq("fake-user"));
verify(client, times(1)).fetch(same(Role.class), eq("fake-role"));
verify(client, times(1)).create(RoleBinding.create("fake-user", "fake-role"));
verify(client, times(1)).delete(argThat(binding ->
binding.getMetadata().getName().equals(roleBinding.getMetadata().getName())));
verify(client, never()).update(isA(RoleBinding.class));
}
}
} }

View File

@ -137,6 +137,10 @@ class RoleBindingReconcilerTest {
var bindingName = "fake-binding-name"; var bindingName = "fake-binding-name";
var userName = "fake-user-name"; var userName = "fake-user-name";
var subject = mock(Subject.class); var subject = mock(Subject.class);
when(subject.getKind()).thenReturn("User");
when(subject.getName()).thenReturn(userName);
when(subject.getApiGroup()).thenReturn("");
var user = mock(User.class); var user = mock(User.class);
var userMetadata = mock(Metadata.class); var userMetadata = mock(Metadata.class);
var binding = createRoleBinding("Role", "fake-role", false, subject); var binding = createRoleBinding("Role", "fake-role", false, subject);
@ -145,8 +149,6 @@ class RoleBindingReconcilerTest {
.thenReturn(Optional.of(binding)); .thenReturn(Optional.of(binding));
when(binding.getSubjects()).thenReturn(List.of(subject)); 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(client.fetch(User.class, userName)).thenReturn(Optional.of(user));
when(user.getMetadata()).thenReturn(userMetadata); when(user.getMetadata()).thenReturn(userMetadata);
var bindings = List.of( var bindings = List.of(
@ -182,6 +184,7 @@ class RoleBindingReconcilerTest {
var metadata = mock(Metadata.class); var metadata = mock(Metadata.class);
lenient().when(roleRef.getKind()).thenReturn(roleRefKind); lenient().when(roleRef.getKind()).thenReturn(roleRefKind);
lenient().when(roleRef.getName()).thenReturn(roleRefName); lenient().when(roleRef.getName()).thenReturn(roleRefName);
lenient().when(roleRef.getApiGroup()).thenReturn("");
lenient().when(metadata.getDeletionTimestamp()).thenReturn(deleting ? Instant.now() : null); lenient().when(metadata.getDeletionTimestamp()).thenReturn(deleting ? Instant.now() : null);
lenient().when(binding.getRoleRef()).thenReturn(roleRef); lenient().when(binding.getRoleRef()).thenReturn(roleRef);
lenient().when(binding.getMetadata()).thenReturn(metadata); lenient().when(binding.getMetadata()).thenReturn(metadata);

View File

@ -0,0 +1,25 @@
package run.halo.app.extension;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.time.Instant;
import org.junit.jupiter.api.Test;
class ExtensionOperatorTest {
@Test
void testIsNotDeleted() {
var ext = mock(ExtensionOperator.class);
var metadata = mock(Metadata.class);
when(metadata.getDeletionTimestamp()).thenReturn(null);
when(ext.getMetadata()).thenReturn(metadata);
assertTrue(ExtensionOperator.isNotDeleted().test(ext));
when(metadata.getDeletionTimestamp()).thenReturn(Instant.now());
assertFalse(ExtensionOperator.isNotDeleted().test(ext));
}
}