mirror of https://github.com/halo-dev/halo
feat: support aggregate several roles into one combined role (#3568)
#### What type of PR is this? /kind feature /milestone 2.4.x /area core #### What this PR does / why we need it: 支持聚合多个角色到一个角色 see #3560 for more details. how to test it? 创建一个测试角色和和一个 RoleBinding 将此角色的绑定到其他角色,在不修改用户权限的情况下,用户将拥有新创建的测试角色的权限。 #### Which issue(s) this PR fixes: Fixes #3560 #### Does this PR introduce a user-facing change? ```release-note 支持聚合多个角色到一个角色 ```pull/3604/head v2.4.0-rc.1
parent
d194c34848
commit
bd4cc0c72d
|
@ -30,6 +30,8 @@ import run.halo.app.extension.GVK;
|
|||
public class Role extends AbstractExtension {
|
||||
public static final String ROLE_DEPENDENCY_RULES =
|
||||
"rbac.authorization.halo.run/dependency-rules";
|
||||
public static final String ROLE_AGGREGATE_LABEL_PREFIX =
|
||||
"rbac.authorization.halo.run/aggregate-to-";
|
||||
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";
|
||||
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
package run.halo.app.core.extension.service;
|
||||
|
||||
import static run.halo.app.extension.MetadataUtil.nullSafeLabels;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
@ -110,7 +114,38 @@ public class DefaultRoleService implements RoleService {
|
|||
return Flux.fromIterable(dependencies)
|
||||
.filter(dependency -> !visited.contains(dependency))
|
||||
.flatMap(dependencyName -> extensionClient.fetch(Role.class, dependencyName));
|
||||
});
|
||||
})
|
||||
.flatMap(role -> Flux.just(role)
|
||||
.mergeWith(listAggregatedRoles(role.getMetadata().getName()))
|
||||
);
|
||||
}
|
||||
|
||||
Flux<Role> listAggregatedRoles(String roleName) {
|
||||
return extensionClient.list(Role.class,
|
||||
role -> Boolean.parseBoolean(nullSafeLabels(role)
|
||||
.get(Role.ROLE_AGGREGATE_LABEL_PREFIX + roleName)
|
||||
),
|
||||
Comparator.comparing(item -> item.getMetadata().getCreationTimestamp()));
|
||||
}
|
||||
|
||||
Predicate<RoleBinding> getRoleBindingPredicate(Subject targetSubject) {
|
||||
return roleBinding -> {
|
||||
List<Subject> subjects = roleBinding.getSubjects();
|
||||
for (Subject subject : subjects) {
|
||||
return matchSubject(targetSubject, subject);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean matchSubject(Subject targetSubject, Subject subject) {
|
||||
if (targetSubject == null || subject == null) {
|
||||
return false;
|
||||
}
|
||||
return StringUtils.equals(targetSubject.getKind(), subject.getKind())
|
||||
&& StringUtils.equals(targetSubject.getName(), subject.getName())
|
||||
&& StringUtils.defaultString(targetSubject.getApiGroup())
|
||||
.equals(StringUtils.defaultString(subject.getApiGroup()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -109,14 +109,39 @@ public class RbacRequestEvaluation {
|
|||
if (ArrayUtils.isEmpty(rule.getResourceNames())) {
|
||||
return true;
|
||||
}
|
||||
String[] requestedNameParts = ArrayUtils.nullToEmpty(StringUtils.split(requestedName, "/"));
|
||||
for (String ruleName : rule.getResourceNames()) {
|
||||
if (Objects.equals(ruleName, requestedName)) {
|
||||
return true;
|
||||
String[] patternParts = StringUtils.split(ruleName, "/");
|
||||
|
||||
for (int i = 0; i < patternParts.length; i++) {
|
||||
String patternPart = patternParts[i];
|
||||
String textPart = StringUtils.EMPTY;
|
||||
if (requestedNameParts.length > i) {
|
||||
textPart = requestedNameParts[i];
|
||||
}
|
||||
|
||||
if (!matchPart(patternPart, textPart)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean matchPart(String patternPart, String textPart) {
|
||||
if (patternPart.equals("*")) {
|
||||
return true;
|
||||
} else if (patternPart.startsWith("*")) {
|
||||
return textPart.endsWith(patternPart.substring(1));
|
||||
} else if (patternPart.endsWith("*")) {
|
||||
return textPart.startsWith(patternPart.substring(0, patternPart.length() - 1));
|
||||
} else {
|
||||
return patternPart.equals(textPart);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean nonResourceURLMatches(Role.PolicyRule rule, String requestedURL) {
|
||||
for (String ruleURL : rule.getNonResourceURLs()) {
|
||||
if (Objects.equals(ruleURL, WildCard.NonResourceAll)) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.core.extension.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.eq;
|
||||
|
@ -13,6 +14,9 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import org.assertj.core.api.AssertionsForInterfaceTypes;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -23,6 +27,7 @@ import reactor.core.publisher.Flux;
|
|||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.RoleBinding;
|
||||
import run.halo.app.core.extension.TestRole;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
@ -104,6 +109,8 @@ class DefaultRoleServiceTest {
|
|||
when(extensionClient.fetch(Role.class, "role1")).thenReturn(Mono.just(role1));
|
||||
when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2));
|
||||
when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.just(role3));
|
||||
when(extensionClient.list(eq(Role.class), any(), any()))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
// call the method under test
|
||||
Flux<Role> result = roleService.listDependenciesFlux(roleNames);
|
||||
|
@ -132,6 +139,8 @@ class DefaultRoleServiceTest {
|
|||
when(extensionClient.fetch(Role.class, "role1")).thenReturn(Mono.just(role1));
|
||||
when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2));
|
||||
when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.just(role3));
|
||||
when(extensionClient.list(eq(Role.class), any(), any()))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
// call the method under test
|
||||
Flux<Role> result = roleService.listDependenciesFlux(roleNames);
|
||||
|
@ -164,6 +173,8 @@ class DefaultRoleServiceTest {
|
|||
when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2));
|
||||
when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.just(role3));
|
||||
when(extensionClient.fetch(Role.class, "role4")).thenReturn(Mono.just(role4));
|
||||
when(extensionClient.list(eq(Role.class), any(), any()))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
// call the method under test
|
||||
Flux<Role> result = roleService.listDependenciesFlux(roleNames);
|
||||
|
@ -197,6 +208,8 @@ class DefaultRoleServiceTest {
|
|||
when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2));
|
||||
when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.just(role3));
|
||||
when(extensionClient.fetch(Role.class, "role4")).thenReturn(Mono.just(role4));
|
||||
when(extensionClient.list(eq(Role.class), any(), any()))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
// call the method under test
|
||||
Flux<Role> result = roleService.listDependenciesFlux(roleNames);
|
||||
|
@ -230,6 +243,8 @@ class DefaultRoleServiceTest {
|
|||
when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2));
|
||||
when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.just(role3));
|
||||
lenient().when(extensionClient.fetch(Role.class, "role4")).thenReturn(Mono.just(role4));
|
||||
when(extensionClient.list(eq(Role.class), any(), any()))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
// call the method under test
|
||||
Flux<Role> result = roleService.listDependenciesFlux(roleNames);
|
||||
|
@ -272,6 +287,8 @@ class DefaultRoleServiceTest {
|
|||
when(extensionClient.fetch(Role.class, "role2")).thenReturn(Mono.just(role2));
|
||||
when(extensionClient.fetch(Role.class, "role3")).thenReturn(Mono.empty());
|
||||
when(extensionClient.fetch(Role.class, "role4")).thenReturn(Mono.just(role4));
|
||||
when(extensionClient.list(eq(Role.class), any(), any()))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
Flux<Role> result = roleService.listDependenciesFlux(roleNames);
|
||||
// verify the result
|
||||
|
@ -285,6 +302,52 @@ class DefaultRoleServiceTest {
|
|||
verify(extensionClient, times(4)).fetch(eq(Role.class), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubjectMatch() {
|
||||
RoleBinding fakeAuthenticatedBinding =
|
||||
createRoleBinding("authenticated-fake-binding", "fake", "authenticated");
|
||||
RoleBinding fakeEditorBinding =
|
||||
createRoleBinding("editor-fake-binding", "fake", "editor");
|
||||
RoleBinding fakeAnonymousBinding =
|
||||
createRoleBinding("test-anonymous-binding", "test", "anonymous");
|
||||
|
||||
RoleBinding.Subject subject = new RoleBinding.Subject();
|
||||
subject.setName("authenticated");
|
||||
subject.setKind(Role.KIND);
|
||||
subject.setApiGroup(Role.GROUP);
|
||||
|
||||
Predicate<RoleBinding> predicate = roleService.getRoleBindingPredicate(subject);
|
||||
List<RoleBinding> result =
|
||||
Stream.of(fakeAuthenticatedBinding, fakeEditorBinding, fakeAnonymousBinding)
|
||||
.filter(predicate)
|
||||
.toList();
|
||||
AssertionsForInterfaceTypes.assertThat(result)
|
||||
.containsExactly(fakeAuthenticatedBinding);
|
||||
|
||||
subject.setName("editor");
|
||||
predicate = roleService.getRoleBindingPredicate(subject);
|
||||
result =
|
||||
Stream.of(fakeAuthenticatedBinding, fakeEditorBinding, fakeAnonymousBinding)
|
||||
.filter(predicate)
|
||||
.toList();
|
||||
AssertionsForInterfaceTypes.assertThat(result).containsExactly(fakeEditorBinding);
|
||||
}
|
||||
|
||||
RoleBinding createRoleBinding(String name, String refName, String subjectName) {
|
||||
RoleBinding roleBinding = new RoleBinding();
|
||||
roleBinding.setMetadata(new Metadata());
|
||||
roleBinding.getMetadata().setName(name);
|
||||
roleBinding.setRoleRef(new RoleBinding.RoleRef());
|
||||
roleBinding.getRoleRef().setKind(Role.KIND);
|
||||
roleBinding.getRoleRef().setApiGroup(Role.GROUP);
|
||||
roleBinding.getRoleRef().setName(refName);
|
||||
roleBinding.setSubjects(List.of(new RoleBinding.Subject()));
|
||||
roleBinding.getSubjects().get(0).setKind(Role.KIND);
|
||||
roleBinding.getSubjects().get(0).setName(subjectName);
|
||||
roleBinding.getSubjects().get(0).setApiGroup(Role.GROUP);
|
||||
return roleBinding;
|
||||
}
|
||||
|
||||
private Role createRole(String name, String... dependencies) {
|
||||
Role role = new Role();
|
||||
role.setMetadata(new Metadata());
|
||||
|
@ -296,4 +359,4 @@ class DefaultRoleServiceTest {
|
|||
return role;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package run.halo.app.security.authorization;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import run.halo.app.core.extension.Role;
|
||||
|
||||
/**
|
||||
* Tests for {@link RbacRequestEvaluation}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.4.0
|
||||
*/
|
||||
class RbacRequestEvaluationTest {
|
||||
|
||||
@Test
|
||||
void resourceNameMatches() {
|
||||
RbacRequestEvaluation rbacRequestEvaluation = new RbacRequestEvaluation();
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "", "fake/test")).isTrue();
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "", "fake")).isTrue();
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "", "")).isTrue();
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "*", null)).isTrue();
|
||||
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "*/test", "fake/test")).isTrue();
|
||||
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "*/test", "hello/test")).isTrue();
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "*/test", "hello/fake")).isFalse();
|
||||
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "test/*", "hello/fake")).isFalse();
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "test/*", "test/fake")).isTrue();
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "test/*", "test")).isTrue();
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "test/*", "hello")).isFalse();
|
||||
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "*/*", "test/fake")).isTrue();
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "*/*", "test")).isTrue();
|
||||
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "*", "test")).isTrue();
|
||||
assertThat(matchResourceName(rbacRequestEvaluation, "*", "hello")).isTrue();
|
||||
}
|
||||
|
||||
boolean matchResourceName(RbacRequestEvaluation rbacRequestEvaluation, String rule,
|
||||
String requestedName) {
|
||||
return rbacRequestEvaluation.resourceNameMatches(new Role.PolicyRule.Builder()
|
||||
.resourceNames(rule)
|
||||
.build(), requestedName);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue