mirror of https://github.com/halo-dev/halo
feat: add an API to fetch user permissions (#2240)
<!-- Thanks for sending a pull request! Here are some tips for you: 1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。 1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>. 2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。 2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. 3. 请确保你已经添加并运行了适当的测试。 3. Ensure you have added or ran the appropriate tests for your PR. --> #### What type of PR is this? /kind feature /area core /milestone 2.0 <!-- 添加其中一个类别: Add one of the following kinds: /kind bug /kind cleanup /kind documentation /kind feature /kind improvement 适当添加其中一个或多个类别(可选): Optionally add one or more of the following kinds if applicable: /kind api-change /kind deprecation /kind failing-test /kind flake /kind regression --> #### What this PR does / why we need it: 新增 `/apis/api.halo.run/v1alpha1/users/{name}/permissions` endpoint 用于根据用户名查询所具有的角色和 ui 权限 1. 当 Role 变更时在 Role reconciler 中查询该 Role 的依赖 Role 聚合其 metadata.annotations 中的 `rbac.authorization.halo.run/ui-permissions` 到当前 Role 的 metadata.annotations 中 key 为`rbac.authorization.halo.run/ui-permissions-aggregated`避免覆盖修改当前 Role 的 `rbac.authorization.halo.run/ui-permissions` 2. 根据用户名查询 ui 权限时,先根据用户名获取 RoleBinding 再获取 Role 然后合并 metadata.annotation 中的两个 key:`rbac.authorization.halo.run/ui-permissions` 和 `rbac.authorization.halo.run/ui-permissions-aggregated` 得到权限作为 API 返回值 #### Which issue(s) this PR fixes: <!-- PR 合并时自动关闭 issue。 Automatically closes linked issue when PR is merged. 用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)` Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`. --> Fixes # #### Special notes for your reviewer: /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? <!-- 如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。 否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change), Release Note 需要以 `action required` 开头。 If no, just write "NONE" in the release-note block below. If yes, a release note is required: Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required". --> ```release-note None ```pull/2243/head
parent
fe816e6843
commit
55040d6918
|
@ -13,6 +13,7 @@ import run.halo.app.core.extension.reconciler.PluginReconciler;
|
|||
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.core.extension.service.RoleService;
|
||||
import run.halo.app.extension.DefaultExtensionClient;
|
||||
import run.halo.app.extension.DefaultSchemeManager;
|
||||
import run.halo.app.extension.DefaultSchemeWatcherManager;
|
||||
|
@ -62,9 +63,9 @@ public class ExtensionConfiguration {
|
|||
}
|
||||
|
||||
@Bean
|
||||
Controller roleController(ExtensionClient client) {
|
||||
Controller roleController(ExtensionClient client, RoleService roleService) {
|
||||
return new ControllerBuilder("role-controller", client)
|
||||
.reconciler(new RoleReconciler(client))
|
||||
.reconciler(new RoleReconciler(client, roleService))
|
||||
.extension(new Role())
|
||||
.build();
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import lombok.Data;
|
|||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.lang.NonNull;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
|
@ -27,6 +28,12 @@ import run.halo.app.extension.GVK;
|
|||
plural = "roles",
|
||||
singular = "role")
|
||||
public class Role extends AbstractExtension {
|
||||
public static final String ROLE_DEPENDENCY_RULES =
|
||||
"rbac.authorization.halo.run/dependency-rules";
|
||||
public static final String ROLE_DEPENDENCIES_ANNO = "rbac.authorization.halo.run/dependencies";
|
||||
public static final String UI_PERMISSIONS_ANNO = "rbac.authorization.halo.run/ui-permissions";
|
||||
public static final String UI_PERMISSIONS_AGGREGATED_ANNO =
|
||||
"rbac.authorization.halo.run/ui-permissions-aggregated";
|
||||
|
||||
public static final String GROUP = "";
|
||||
public static final String VERSION = "v1alpha1";
|
||||
|
@ -43,7 +50,7 @@ public class Role extends AbstractExtension {
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@Getter
|
||||
public static class PolicyRule implements Comparable {
|
||||
public static class PolicyRule implements Comparable<PolicyRule> {
|
||||
/**
|
||||
* APIGroups is the name of the APIGroup that contains the resources.
|
||||
* If multiple API groups are specified, any action requested against one of the enumerated
|
||||
|
@ -104,28 +111,25 @@ public class Role extends AbstractExtension {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Object o) {
|
||||
if (o instanceof PolicyRule other) {
|
||||
int result = compare(apiGroups, other.apiGroups);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = compare(resources, other.resources);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = compare(resourceNames, other.resourceNames);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = compare(nonResourceURLs, other.nonResourceURLs);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = compare(verbs, other.verbs);
|
||||
public int compareTo(@NonNull PolicyRule other) {
|
||||
int result = compare(apiGroups, other.apiGroups);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return 1;
|
||||
result = compare(resources, other.resources);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = compare(resourceNames, other.resourceNames);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = compare(nonResourceURLs, other.nonResourceURLs);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = compare(verbs, other.verbs);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
|
|
@ -32,6 +32,8 @@ public class User extends AbstractExtension {
|
|||
public static final String VERSION = "v1alpha1";
|
||||
public static final String KIND = "User";
|
||||
|
||||
public static final String ROLE_NAMES_ANNO = "rbac.authorization.halo.run/role-names";
|
||||
|
||||
@Schema(required = true)
|
||||
private UserSpec spec;
|
||||
|
||||
|
|
|
@ -4,12 +4,19 @@ 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 com.fasterxml.jackson.core.type.TypeReference;
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
@ -22,18 +29,23 @@ 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.service.UserService;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
@Component
|
||||
public class UserEndpoint implements CustomEndpoint {
|
||||
|
||||
private final ExtensionClient client;
|
||||
private final UserService userService;
|
||||
|
||||
public UserEndpoint(ExtensionClient client) {
|
||||
public UserEndpoint(ExtensionClient client, UserService userService) {
|
||||
this.client = client;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
Mono<ServerResponse> me(ServerRequest request) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(ctx -> {
|
||||
|
@ -46,6 +58,7 @@ public class UserEndpoint implements CustomEndpoint {
|
|||
.bodyValue(user));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
Mono<ServerResponse> grantPermission(ServerRequest request) {
|
||||
var username = request.pathVariable("name");
|
||||
return request.bodyToMono(GrantRequest.class)
|
||||
|
@ -115,7 +128,58 @@ public class UserEndpoint implements CustomEndpoint {
|
|||
.requestBody(
|
||||
requestBodyBuilder().required(true).implementation(GrantRequest.class))
|
||||
.response(responseBuilder().implementation(User.class)))
|
||||
.GET("/users/{name}/permissions", this::getUserPermission,
|
||||
builder -> builder.operationId("GetPermissions")
|
||||
.description("Get permissions of user")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().in(ParameterIn.PATH).name("name")
|
||||
.description("User name")
|
||||
.required(true))
|
||||
.response(responseBuilder().implementation(Set.class)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Mono<ServerResponse> getUserPermission(ServerRequest request) {
|
||||
String name = request.pathVariable("name");
|
||||
return userService.listRoles(name)
|
||||
.reduce(new LinkedHashSet<Role>(), (list, role) -> {
|
||||
list.add(role);
|
||||
return list;
|
||||
})
|
||||
.map(roles -> {
|
||||
Set<String> uiPermissions =
|
||||
roles.stream()
|
||||
.map(role -> role.getMetadata().getAnnotations())
|
||||
.filter(Objects::nonNull)
|
||||
.map(this::mergeUiPermissions)
|
||||
.flatMap(Set::stream)
|
||||
.collect(Collectors.toSet());
|
||||
return new UserPermission(roles, uiPermissions);
|
||||
})
|
||||
.flatMap(result -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(result)
|
||||
);
|
||||
}
|
||||
|
||||
private Set<String> mergeUiPermissions(Map<String, String> annotations) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
String permissionsStr = annotations.get(Role.UI_PERMISSIONS_AGGREGATED_ANNO);
|
||||
if (StringUtils.isNotBlank(permissionsStr)) {
|
||||
result.addAll(JsonUtils.jsonToObject(permissionsStr,
|
||||
new TypeReference<LinkedHashSet<String>>() {
|
||||
}));
|
||||
}
|
||||
String uiPermissionStr = annotations.get(Role.UI_PERMISSIONS_ANNO);
|
||||
if (StringUtils.isNotBlank(uiPermissionStr)) {
|
||||
result.addAll(JsonUtils.jsonToObject(uiPermissionStr,
|
||||
new TypeReference<LinkedHashSet<String>>() {
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
record UserPermission(Set<Role> roles, Set<String> uiPermissions) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,6 @@ 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) {
|
||||
|
@ -57,7 +55,7 @@ public class RoleBindingReconciler implements Reconciler {
|
|||
annotations = new HashMap<>();
|
||||
}
|
||||
var oldAnnotations = Map.copyOf(annotations);
|
||||
annotations.put(ROLE_NAMES_ANNO, JsonUtils.objectToJson(roleNames));
|
||||
annotations.put(User.ROLE_NAMES_ANNO, JsonUtils.objectToJson(roleNames));
|
||||
user.getMetadata().setAnnotations(annotations);
|
||||
if (!Objects.deepEquals(oldAnnotations, annotations)) {
|
||||
// update user
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
@ -14,6 +10,7 @@ import java.util.Set;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.service.RoleService;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
@ -26,81 +23,66 @@ import run.halo.app.infra.utils.JsonUtils;
|
|||
*/
|
||||
@Slf4j
|
||||
public class RoleReconciler implements Reconciler {
|
||||
public static final String ROLE_DEPENDENCIES = "halo.run/dependencies";
|
||||
public static final String ROLE_DEPENDENCY_RULES = "halo.run/dependency-rules";
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
public RoleReconciler(ExtensionClient client) {
|
||||
private final RoleService roleService;
|
||||
|
||||
public RoleReconciler(ExtensionClient client, RoleService roleService) {
|
||||
this.client = client;
|
||||
this.roleService = roleService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(Role.class, request.name()).ifPresent(role -> {
|
||||
final Role oldRole = JsonUtils.deepCopy(role);
|
||||
Map<String, String> annotations = role.getMetadata().getAnnotations();
|
||||
if (annotations == null) {
|
||||
annotations = new HashMap<>();
|
||||
role.getMetadata().setAnnotations(annotations);
|
||||
}
|
||||
Map<String, String> oldAnnotations = Map.copyOf(annotations);
|
||||
|
||||
String s = annotations.get(ROLE_DEPENDENCIES);
|
||||
List<String> roleDependencies = readValue(s);
|
||||
List<Role.PolicyRule> dependencyRules = listDependencyRoles(roleDependencies)
|
||||
.stream()
|
||||
Set<String> roleDependencies = readValue(annotations.get(Role.ROLE_DEPENDENCIES_ANNO));
|
||||
|
||||
List<Role> dependenciesRole = roleService.listDependencies(roleDependencies);
|
||||
|
||||
List<Role.PolicyRule> dependencyRules = dependenciesRole.stream()
|
||||
.map(Role::getRules)
|
||||
.flatMap(List::stream)
|
||||
.sorted()
|
||||
.toList();
|
||||
List<String> uiPermissions = aggregateUiPermissions(dependenciesRole);
|
||||
// override dependency rules to annotations
|
||||
annotations.put(ROLE_DEPENDENCY_RULES, JsonUtils.objectToJson(dependencyRules));
|
||||
if (!Objects.deepEquals(oldAnnotations, annotations)) {
|
||||
annotations.put(Role.ROLE_DEPENDENCY_RULES, JsonUtils.objectToJson(dependencyRules));
|
||||
annotations.put(Role.UI_PERMISSIONS_AGGREGATED_ANNO,
|
||||
JsonUtils.objectToJson(uiPermissions));
|
||||
if (!Objects.equals(oldRole, role)) {
|
||||
client.update(role);
|
||||
}
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
|
||||
private List<String> readValue(String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return JsonUtils.DEFAULT_JSON_MAPPER.readValue(json, new TypeReference<>() {
|
||||
});
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
private List<String> aggregateUiPermissions(List<Role> dependencyRoles) {
|
||||
return dependencyRoles.stream()
|
||||
.filter(role -> role.getMetadata().getAnnotations() != null)
|
||||
.map(role -> {
|
||||
Map<String, String> roleAnnotations = role.getMetadata().getAnnotations();
|
||||
return roleAnnotations.get(Role.UI_PERMISSIONS_ANNO);
|
||||
})
|
||||
.map(this::readValue)
|
||||
.flatMap(Set::stream)
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<Role> listDependencyRoles(List<String> dependencies) {
|
||||
List<Role> result = new ArrayList<>();
|
||||
if (dependencies == null) {
|
||||
return result;
|
||||
private Set<String> readValue(String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
return new LinkedHashSet<>();
|
||||
}
|
||||
Set<String> visited = new HashSet<>();
|
||||
Deque<String> queue = new ArrayDeque<>(dependencies);
|
||||
while (!queue.isEmpty()) {
|
||||
String roleName = queue.poll();
|
||||
// detecting cycle in role dependencies
|
||||
if (visited.contains(roleName)) {
|
||||
log.warn("Detected a cycle in role dependencies: {},and skipped automatically",
|
||||
roleName);
|
||||
continue;
|
||||
}
|
||||
visited.add(roleName);
|
||||
client.fetch(Role.class, roleName).ifPresent(role -> {
|
||||
result.add(role);
|
||||
// add role dependencies to queue
|
||||
Map<String, String> annotations = role.getMetadata().getAnnotations();
|
||||
if (annotations != null) {
|
||||
String roleNameDependencies = annotations.get(ROLE_DEPENDENCIES);
|
||||
List<String> roleDependencies = readValue(roleNameDependencies);
|
||||
queue.addAll(roleDependencies);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
return JsonUtils.jsonToObject(json, new TypeReference<>() {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
package run.halo.app.core.extension.service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
@ -8,11 +19,13 @@ 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.extension.ExtensionClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DefaultRoleService implements RoleService {
|
||||
|
||||
|
@ -35,4 +48,46 @@ public class DefaultRoleService implements RoleService {
|
|||
null))
|
||||
.map(RoleBinding::getRoleRef);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<Role> listDependencies(Set<String> names) {
|
||||
List<Role> result = new ArrayList<>();
|
||||
if (names == null) {
|
||||
return result;
|
||||
}
|
||||
Set<String> visited = new HashSet<>();
|
||||
Deque<String> queue = new ArrayDeque<>(names);
|
||||
while (!queue.isEmpty()) {
|
||||
String roleName = queue.poll();
|
||||
// detecting cycle in role dependencies
|
||||
if (visited.contains(roleName)) {
|
||||
log.warn("Detected a cycle in role dependencies: {},and skipped automatically",
|
||||
roleName);
|
||||
continue;
|
||||
}
|
||||
visited.add(roleName);
|
||||
extensionClient.fetch(Role.class, roleName).ifPresent(role -> {
|
||||
result.add(role);
|
||||
// add role dependencies to queue
|
||||
Map<String, String> annotations = role.getMetadata().getAnnotations();
|
||||
if (annotations != null) {
|
||||
String roleNameDependencies = annotations.get(Role.ROLE_DEPENDENCIES_ANNO);
|
||||
List<String> roleDependencies = stringToList(roleNameDependencies);
|
||||
queue.addAll(roleDependencies);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<String> stringToList(String str) {
|
||||
if (StringUtils.isBlank(str)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return JsonUtils.jsonToObject(str,
|
||||
new TypeReference<>() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package run.halo.app.core.extension.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Flux;
|
||||
import run.halo.app.core.extension.Role;
|
||||
|
@ -16,4 +18,6 @@ public interface RoleService {
|
|||
Role getRole(String name);
|
||||
|
||||
Flux<RoleRef> listRoleRefs(Subject subject);
|
||||
|
||||
List<Role> listDependencies(Set<String> names);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package run.halo.app.core.extension.service;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.User;
|
||||
|
||||
public interface UserService {
|
||||
|
@ -9,4 +11,5 @@ public interface UserService {
|
|||
|
||||
Mono<Void> updatePassword(String username, String newPassword);
|
||||
|
||||
Flux<Role> listRoles(String username);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
package run.halo.app.core.extension.service;
|
||||
|
||||
import static run.halo.app.core.extension.RoleBinding.containsUser;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
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.extension.ExtensionClient;
|
||||
|
||||
|
@ -28,4 +34,14 @@ public class UserServiceImpl implements UserService {
|
|||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Role> listRoles(String name) {
|
||||
return Flux.fromStream(client.list(RoleBinding.class, containsUser(name), null)
|
||||
.stream()
|
||||
.filter(roleBinding -> Role.KIND.equals(roleBinding.getRoleRef().getKind()))
|
||||
.map(roleBinding -> roleBinding.getRoleRef().getName())
|
||||
.map(roleName -> client.fetch(Role.class, roleName).orElse(null))
|
||||
.filter(Objects::nonNull));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
|
@ -96,4 +97,36 @@ public class JsonUtils {
|
|||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to deserialize JSON content from given JSON content String.
|
||||
*
|
||||
* @param json json content
|
||||
* @param typeReference type reference to convert
|
||||
* @param <T> real type to convert
|
||||
* @return converted object
|
||||
*/
|
||||
public static <T> T jsonToObject(String json, TypeReference<T> typeReference) {
|
||||
try {
|
||||
return DEFAULT_JSON_MAPPER.readValue(json, typeReference);
|
||||
} catch (Exception e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to deserialize JSON content and serialize back from given Object.
|
||||
*
|
||||
* @param source source object to copy
|
||||
* @param <T> real type to deep copy
|
||||
* @return deep copy of the source object
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T deepCopy(T source) {
|
||||
try {
|
||||
return (T) DEFAULT_JSON_MAPPER.readValue(objectToJson(source), source.getClass());
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import org.apache.commons.lang3.StringUtils;
|
|||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.reconciler.RoleReconciler;
|
||||
import run.halo.app.core.extension.service.DefaultRoleBindingService;
|
||||
import run.halo.app.core.extension.service.RoleBindingService;
|
||||
import run.halo.app.core.extension.service.RoleService;
|
||||
|
@ -81,7 +80,7 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
|||
}
|
||||
// merge policy rules
|
||||
String roleDependencyRules = metadata.getAnnotations()
|
||||
.get(RoleReconciler.ROLE_DEPENDENCY_RULES);
|
||||
.get(Role.ROLE_DEPENDENCY_RULES);
|
||||
List<Role.PolicyRule> rules = convertFrom(roleDependencyRules);
|
||||
rules.addAll(role.getRules());
|
||||
return rules;
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package run.halo.app.core.extension;
|
||||
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Roles to test.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class TestRole {
|
||||
|
||||
public static Role getRoleManage() {
|
||||
return JsonUtils.jsonToObject("""
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Role",
|
||||
"metadata": {
|
||||
"name": "role-template-apple-manage"
|
||||
},
|
||||
"rules": [{
|
||||
"resources": ["apples"],
|
||||
"verbs": ["create"]
|
||||
}]
|
||||
}
|
||||
""", Role.class);
|
||||
}
|
||||
|
||||
public static Role getRoleView() {
|
||||
return JsonUtils.jsonToObject("""
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Role",
|
||||
"metadata": {
|
||||
"name": "role-template-apple-view"
|
||||
},
|
||||
"rules": [{
|
||||
"resources": ["apples"],
|
||||
"verbs": ["list"]
|
||||
}]
|
||||
}
|
||||
""", Role.class);
|
||||
}
|
||||
|
||||
public static Role getRoleOther() {
|
||||
return JsonUtils.jsonToObject("""
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Role",
|
||||
"metadata": {
|
||||
"name": "role-template-apple-other"
|
||||
},
|
||||
"rules": [{
|
||||
"resources": ["apples"],
|
||||
"verbs": ["update"]
|
||||
}]
|
||||
}
|
||||
""", Role.class);
|
||||
}
|
||||
}
|
|
@ -25,12 +25,15 @@ import org.springframework.boot.test.mock.mockito.MockBean;
|
|||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
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.core.extension.service.UserService;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureWebTestClient
|
||||
|
@ -45,6 +48,9 @@ class UserEndpointTest {
|
|||
@MockBean
|
||||
ExtensionClient client;
|
||||
|
||||
@MockBean
|
||||
UserService userService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// disable authorization
|
||||
|
@ -165,18 +171,68 @@ class UserEndpointTest {
|
|||
|
||||
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()
|
||||
.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, times(1))
|
||||
.delete(argThat(binding -> binding.getMetadata().getName()
|
||||
.equals(roleBinding.getMetadata().getName())));
|
||||
verify(client, never()).update(isA(RoleBinding.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser("fake-user")
|
||||
void shouldGetPermission() {
|
||||
Role roleA = JsonUtils.jsonToObject("""
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Role",
|
||||
"metadata": {
|
||||
"name": "test-A",
|
||||
"annotations": {
|
||||
"rbac.authorization.halo.run/ui-permissions": "[\\"permission-A\\"]",
|
||||
"rbac.authorization.halo.run/ui-permissions-aggregated":
|
||||
"[\\"permission-B\\"]"
|
||||
}
|
||||
},
|
||||
"rules": []
|
||||
}
|
||||
""", Role.class);
|
||||
when(userService.listRoles(eq("fake-user"))).thenReturn(
|
||||
Flux.fromIterable(List.of(roleA)));
|
||||
|
||||
webClient.get().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody()
|
||||
.json("""
|
||||
{ "roles": [{
|
||||
"rules": [],
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Role",
|
||||
"metadata": {
|
||||
"name": "test-A",
|
||||
"annotations": {
|
||||
"rbac.authorization.halo.run/ui-permissions":
|
||||
"[\\"permission-A\\"]",
|
||||
"rbac.authorization.halo.run/ui-permissions-aggregated":
|
||||
"[\\"permission-B\\"]"
|
||||
}
|
||||
}
|
||||
}],
|
||||
"uiPermissions": [
|
||||
"permission-A",
|
||||
"permission-B"
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
verify(userService, times(1)).listRoles(eq("fake-user"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -13,7 +13,7 @@ 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 static run.halo.app.core.extension.User.ROLE_NAMES_ANNO;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
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.doNothing;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -16,9 +23,10 @@ import org.mockito.Mock;
|
|||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.TestRole;
|
||||
import run.halo.app.core.extension.service.RoleService;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link RoleReconciler}.
|
||||
|
@ -32,89 +40,32 @@ class RoleReconcilerTest {
|
|||
@Mock
|
||||
private ExtensionClient extensionClient;
|
||||
|
||||
private RoleReconciler roleReconciler;
|
||||
@Mock
|
||||
private RoleService roleService;
|
||||
|
||||
private String roleOther;
|
||||
private RoleReconciler roleReconciler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
roleReconciler = new RoleReconciler(extensionClient);
|
||||
Role roleManage = JsonUtils.jsonToObject("""
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Role",
|
||||
"metadata": {
|
||||
"name": "role-template-apple-manage",
|
||||
"annotations": {
|
||||
"halo.run/dependencies": "[\\"role-template-apple-view\\"]",
|
||||
"halo.run/module": "Apple Management",
|
||||
"halo.run/display-name": "苹果管理"
|
||||
}
|
||||
},
|
||||
"rules": [{
|
||||
"resources": ["apples"],
|
||||
"verbs": ["create"]
|
||||
}]
|
||||
}
|
||||
""", Role.class);
|
||||
|
||||
Role roleView = JsonUtils.jsonToObject("""
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Role",
|
||||
"metadata": {
|
||||
"name": "role-template-apple-view",
|
||||
"annotations": {
|
||||
"halo.run/dependencies": "[\\"role-template-apple-other\\"]"
|
||||
}
|
||||
},
|
||||
"rules": [{
|
||||
"resources": ["apples"],
|
||||
"verbs": ["list"]
|
||||
}]
|
||||
}
|
||||
""", Role.class);
|
||||
|
||||
roleOther = """
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "Role",
|
||||
"metadata": {
|
||||
"name": "role-template-apple-other"
|
||||
},
|
||||
"rules": [{
|
||||
"resources": ["apples"],
|
||||
"verbs": ["update"]
|
||||
}]
|
||||
}
|
||||
""";
|
||||
when(extensionClient.fetch(eq(Role.class), eq("role-template-apple-manage"))).thenReturn(
|
||||
Optional.of(roleManage));
|
||||
when(extensionClient.fetch(eq(Role.class), eq("role-template-apple-view"))).thenReturn(
|
||||
Optional.of(roleView));
|
||||
roleReconciler = new RoleReconciler(extensionClient, roleService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcile() throws JSONException {
|
||||
when(extensionClient.fetch(eq(Role.class), eq("role-template-apple-other"))).thenReturn(
|
||||
Optional.of(JsonUtils.jsonToObject(roleOther, Role.class)));
|
||||
assertReconcile();
|
||||
}
|
||||
Role roleManage = TestRole.getRoleManage();
|
||||
Map<String, String> manageAnnotations = new HashMap<>();
|
||||
manageAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-view\"]");
|
||||
roleManage.getMetadata().setAnnotations(manageAnnotations);
|
||||
|
||||
@Test
|
||||
void detectingCycle() throws JSONException {
|
||||
Role roleToUse = JsonUtils.jsonToObject(roleOther, Role.class);
|
||||
// build a cycle in the dependency
|
||||
Map<String, String> annotations =
|
||||
Map.of("halo.run/dependencies", "[\"role-template-apple-view\"]");
|
||||
roleToUse.getMetadata().setAnnotations(annotations);
|
||||
when(extensionClient.fetch(eq(Role.class), eq("role-template-apple-other"))).thenReturn(
|
||||
Optional.of(roleToUse));
|
||||
Role roleView = TestRole.getRoleView();
|
||||
|
||||
assertReconcile();
|
||||
}
|
||||
Role roleOther = TestRole.getRoleOther();
|
||||
when(roleService.listDependencies(eq(Set.of("role-template-apple-view"))))
|
||||
.thenReturn(List.of(roleView, roleOther));
|
||||
|
||||
when(extensionClient.fetch(eq(Role.class), eq("role-template-apple-manage"))).thenReturn(
|
||||
Optional.of(roleManage));
|
||||
|
||||
private void assertReconcile() throws JSONException {
|
||||
ArgumentCaptor<Role> roleCaptor = ArgumentCaptor.forClass(Role.class);
|
||||
doNothing().when(extensionClient).update(roleCaptor.capture());
|
||||
|
||||
|
@ -135,6 +86,47 @@ class RoleReconcilerTest {
|
|||
Role updateArgs = roleCaptor.getValue();
|
||||
assertThat(updateArgs).isNotNull();
|
||||
JSONAssert.assertEquals(expected, updateArgs.getMetadata().getAnnotations()
|
||||
.get(RoleReconciler.ROLE_DEPENDENCY_RULES), false);
|
||||
.get(Role.ROLE_DEPENDENCY_RULES), false);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void reconcileUiPermission() {
|
||||
Role roleManage = TestRole.getRoleManage();
|
||||
Map<String, String> annotations = new LinkedHashMap<>();
|
||||
annotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-view\"]");
|
||||
annotations.put(Role.UI_PERMISSIONS_ANNO, "[\"apples:manage\"]");
|
||||
roleManage.getMetadata().setAnnotations(annotations);
|
||||
|
||||
Role roleView = TestRole.getRoleView();
|
||||
Map<String, String> roleViewAnnotations = new LinkedHashMap<>();
|
||||
roleViewAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-other\"]");
|
||||
roleViewAnnotations.put(Role.UI_PERMISSIONS_ANNO, "[\"apples:view\"]");
|
||||
roleView.getMetadata().setAnnotations(roleViewAnnotations);
|
||||
|
||||
Role roleOther = TestRole.getRoleOther();
|
||||
Map<String, String> roleOtherAnnotations = new LinkedHashMap<>();
|
||||
roleOtherAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-other\"]");
|
||||
roleOtherAnnotations.put(Role.UI_PERMISSIONS_ANNO, "[\"apples:foo\", \"apples:bar\"]");
|
||||
roleOther.getMetadata().setAnnotations(roleOtherAnnotations);
|
||||
|
||||
when(extensionClient.fetch(eq(Role.class), eq("role-template-apple-manage"))).thenReturn(
|
||||
Optional.of(roleManage));
|
||||
|
||||
when(roleService.listDependencies(any()))
|
||||
.thenReturn(List.of(roleView, roleOther));
|
||||
|
||||
ArgumentCaptor<Role> roleCaptor = ArgumentCaptor.forClass(Role.class);
|
||||
|
||||
roleReconciler.reconcile(new Reconciler.Request("role-template-apple-manage"));
|
||||
verify(extensionClient, times(1)).update(roleCaptor.capture());
|
||||
|
||||
// assert that the user has the correct roles
|
||||
Role value = roleCaptor.getValue();
|
||||
Map<String, String> resultAnnotations = value.getMetadata().getAnnotations();
|
||||
assertThat(resultAnnotations).isNotNull();
|
||||
assertThat(resultAnnotations.containsKey(Role.UI_PERMISSIONS_AGGREGATED_ANNO)).isTrue();
|
||||
assertThat(resultAnnotations.get(Role.UI_PERMISSIONS_AGGREGATED_ANNO)).isEqualTo(
|
||||
"[\"apples:bar\",\"apples:foo\",\"apples:view\"]");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package run.halo.app.core.extension.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.TestRole;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultRoleService}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DefaultRoleServiceTest {
|
||||
@Mock
|
||||
private ExtensionClient extensionClient;
|
||||
|
||||
private DefaultRoleService roleService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
roleService = new DefaultRoleService(extensionClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listDependencie() {
|
||||
Role roleManage = TestRole.getRoleManage();
|
||||
Map<String, String> manageAnnotations = new HashMap<>();
|
||||
manageAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-view\"]");
|
||||
roleManage.getMetadata().setAnnotations(manageAnnotations);
|
||||
|
||||
Role roleView = TestRole.getRoleView();
|
||||
Map<String, String> viewAnnotations = new HashMap<>();
|
||||
viewAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-other\"]");
|
||||
roleView.getMetadata().setAnnotations(viewAnnotations);
|
||||
|
||||
Role roleOther = TestRole.getRoleOther();
|
||||
|
||||
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-manage")))
|
||||
.thenReturn(Optional.of(roleManage));
|
||||
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-view")))
|
||||
.thenReturn(Optional.of(roleView));
|
||||
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-other")))
|
||||
.thenReturn(Optional.of(roleOther));
|
||||
|
||||
// list without cycle
|
||||
List<Role> roles = roleService.listDependencies(Set.of("role-template-apple-manage"));
|
||||
|
||||
verify(extensionClient, times(1)).fetch(same(Role.class), eq("role-template-apple-manage"));
|
||||
verify(extensionClient, times(1)).fetch(same(Role.class), eq("role-template-apple-view"));
|
||||
verify(extensionClient, times(1)).fetch(same(Role.class), eq("role-template-apple-other"));
|
||||
|
||||
assertThat(roles).hasSize(3);
|
||||
assertThat(roles).containsExactly(roleManage, roleView, roleOther);
|
||||
|
||||
// list dependencies with a cycle relation
|
||||
Map<String, String> anotherAnnotations = new HashMap<>();
|
||||
anotherAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-view\"]");
|
||||
roleOther.getMetadata().setAnnotations(anotherAnnotations);
|
||||
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-other")))
|
||||
.thenReturn(Optional.of(roleOther));
|
||||
// correct behavior is to ignore the cycle relation
|
||||
List<Role> rolesFromCycle =
|
||||
roleService.listDependencies(Set.of("role-template-apple-manage"));
|
||||
assertThat(rolesFromCycle).hasSize(3);
|
||||
}
|
||||
}
|
|
@ -4,10 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
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 java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
@ -15,8 +17,12 @@ import org.mockito.InjectMocks;
|
|||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.test.StepVerifier;
|
||||
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.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceImplTest {
|
||||
|
@ -76,4 +82,116 @@ class UserServiceImplTest {
|
|||
verify(client, times(1)).fetch(eq(User.class), eq("faker"));
|
||||
verify(client, times(0)).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldListRolesIfUserFoundInExtension() {
|
||||
User fakeUser = new User();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("faker");
|
||||
fakeUser.setMetadata(metadata);
|
||||
fakeUser.setSpec(new User.UserSpec());
|
||||
|
||||
when(client.list(eq(RoleBinding.class), any(), any())).thenReturn(getRoleBindings());
|
||||
Role roleA = new Role();
|
||||
Metadata metadataA = new Metadata();
|
||||
metadataA.setName("test-A");
|
||||
roleA.setMetadata(metadataA);
|
||||
|
||||
Role roleB = new Role();
|
||||
Metadata metadataB = new Metadata();
|
||||
metadataB.setName("test-B");
|
||||
roleB.setMetadata(metadataB);
|
||||
|
||||
Role roleC = new Role();
|
||||
Metadata metadataC = new Metadata();
|
||||
metadataC.setName("ddd");
|
||||
roleC.setMetadata(metadataC);
|
||||
|
||||
when(client.fetch(eq(Role.class), eq("test-A"))).thenReturn(Optional.of(roleA));
|
||||
when(client.fetch(eq(Role.class), eq("test-B"))).thenReturn(Optional.of(roleB));
|
||||
lenient().when(client.fetch(eq(Role.class), eq("ddd"))).thenReturn(Optional.of(roleC));
|
||||
|
||||
StepVerifier.create(userService.listRoles("faker"))
|
||||
.expectNext(roleA)
|
||||
.expectNext(roleB)
|
||||
.verifyComplete();
|
||||
|
||||
verify(client, times(1)).list(eq(RoleBinding.class), any(), any());
|
||||
|
||||
verify(client, times(1)).fetch(eq(Role.class), eq("test-A"));
|
||||
verify(client, times(1)).fetch(eq(Role.class), eq("test-B"));
|
||||
verify(client, times(0)).fetch(eq(Role.class), eq("ddd"));
|
||||
}
|
||||
|
||||
List<RoleBinding> getRoleBindings() {
|
||||
String bindA = """
|
||||
{
|
||||
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "RoleBinding",
|
||||
"metadata": {
|
||||
"name": "bind-A"
|
||||
},
|
||||
"subjects": [{
|
||||
"kind": "User",
|
||||
"name": "faker",
|
||||
"apiGroup": ""
|
||||
}],
|
||||
"roleRef": {
|
||||
"kind": "Role",
|
||||
"name": "test-A",
|
||||
"apiGroup": ""
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String bindB = """
|
||||
{
|
||||
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "RoleBinding",
|
||||
"metadata": {
|
||||
"name": "bind-B"
|
||||
},
|
||||
"subjects": [{
|
||||
"kind": "User",
|
||||
"name": "faker",
|
||||
"apiGroup": ""
|
||||
},
|
||||
{
|
||||
"kind": "User",
|
||||
"name": "zhangsan",
|
||||
"apiGroup": ""
|
||||
}],
|
||||
"roleRef": {
|
||||
"kind": "Role",
|
||||
"name": "test-B",
|
||||
"apiGroup": ""
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String bindC = """
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "RoleBinding",
|
||||
"metadata": {
|
||||
"name": "bind-C"
|
||||
},
|
||||
"subjects": [{
|
||||
"kind": "User",
|
||||
"name": "faker",
|
||||
"apiGroup": ""
|
||||
}],
|
||||
"roleRef": {
|
||||
"kind": "Fake",
|
||||
"name": "ddd",
|
||||
"apiGroup": ""
|
||||
}
|
||||
}
|
||||
""";
|
||||
return List.of(JsonUtils.jsonToObject(bindA, RoleBinding.class),
|
||||
JsonUtils.jsonToObject(bindB, RoleBinding.class),
|
||||
JsonUtils.jsonToObject(bindC, RoleBinding.class));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue