refactor: plugin custom api generate rules (#2211)

* refactor: plugin custom api generate rules

* refactor: boolean convert

* fix: unit test
pull/2212/head
guqing 2022-07-05 15:00:12 +08:00 committed by GitHub
parent 27435a1aeb
commit f7945081a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 158 additions and 123 deletions

View File

@ -6,7 +6,6 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import run.halo.app.extension.AbstractExtension; import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK; import run.halo.app.extension.GVK;
@ -74,19 +73,13 @@ public class Role extends AbstractExtension {
*/ */
final String[] verbs; final String[] verbs;
/**
* If the plugin name exists, it means that the API is provided by the plugin.
*/
final String pluginName;
public PolicyRule() { public PolicyRule() {
this(null, null, null, null, null, null); this(null, null, null, null, null);
} }
public PolicyRule(String pluginName, String[] apiGroups, String[] resources, public PolicyRule(String[] apiGroups, String[] resources,
String[] resourceNames, String[] resourceNames,
String[] nonResourceURLs, String[] verbs) { String[] nonResourceURLs, String[] verbs) {
this.pluginName = StringUtils.defaultString(pluginName);
this.apiGroups = nullElseEmpty(apiGroups); this.apiGroups = nullElseEmpty(apiGroups);
this.resources = nullElseEmpty(resources); this.resources = nullElseEmpty(resources);
this.resourceNames = nullElseEmpty(resourceNames); this.resourceNames = nullElseEmpty(resourceNames);
@ -114,11 +107,6 @@ public class Role extends AbstractExtension {
String pluginName; String pluginName;
public Builder pluginName(String pluginName) {
this.pluginName = pluginName;
return this;
}
public Builder apiGroups(String... apiGroups) { public Builder apiGroups(String... apiGroups) {
this.apiGroups = apiGroups; this.apiGroups = apiGroups;
return this; return this;
@ -145,7 +133,7 @@ public class Role extends AbstractExtension {
} }
public PolicyRule build() { public PolicyRule build() {
return new PolicyRule(pluginName, apiGroups, resources, resourceNames, return new PolicyRule(apiGroups, resources, resourceNames,
nonResourceURLs, nonResourceURLs,
verbs); verbs);
} }

View File

@ -121,6 +121,7 @@ public class PluginRequestMappingHandlerMapping extends RequestMappingHandlerMap
protected String buildPrefix(String pluginId, String version) { protected String buildPrefix(String pluginId, String version) {
GroupVersion groupVersion = GroupVersion.parseAPIVersion(version); GroupVersion groupVersion = GroupVersion.parseAPIVersion(version);
return String.format("/api/%s/plugins/%s", groupVersion.version(), pluginId); return String.format("/apis/plugin.api.halo.run/%s/plugins/%s", groupVersion.version(),
pluginId);
} }
} }

View File

@ -66,5 +66,5 @@ public interface Attributes {
*/ */
String getPath(); String getPath();
String pluginName(); String getSubName();
} }

View File

@ -69,7 +69,7 @@ public class AttributesRecord implements Attributes {
} }
@Override @Override
public String pluginName() { public String getSubName() {
return requestInfo.getPluginName(); return requestInfo.getSubName();
} }
} }

View File

@ -5,7 +5,6 @@ import java.util.Objects;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import run.halo.app.core.extension.Role; import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.Role.PolicyRule;
/** /**
* @author guqing * @author guqing
@ -35,17 +34,26 @@ public class RbacRequestEvaluation {
combinedResource = combinedResource =
requestAttributes.getResource() + "/" + requestAttributes.getSubresource(); requestAttributes.getResource() + "/" + requestAttributes.getSubresource();
} }
return verbMatches(rule, requestAttributes.getVerb()) return verbMatches(rule, requestAttributes.getVerb())
&& apiGroupMatches(rule, requestAttributes.getApiGroup()) && apiGroupMatches(rule, requestAttributes.getApiGroup())
&& resourceMatches(rule, combinedResource, requestAttributes.getSubresource()) && resourceMatches(rule, combinedResource, requestAttributes.getSubresource())
&& resourceNameMatches(rule, requestAttributes.getName()) && resourceNameMatches(rule,
&& pluginNameMatches(rule, requestAttributes.pluginName()); combineResourceName(requestAttributes.getName(), requestAttributes.getSubName()));
} }
return verbMatches(rule, requestAttributes.getVerb()) return verbMatches(rule, requestAttributes.getVerb())
&& nonResourceURLMatches(rule, requestAttributes.getPath()); && nonResourceURLMatches(rule, requestAttributes.getPath());
} }
private String combineResourceName(String name, String subName) {
if (StringUtils.isBlank(name)) {
return subName;
}
if (StringUtils.isBlank(subName)) {
return name;
}
return name + "/" + subName;
}
protected boolean verbMatches(Role.PolicyRule rule, String requestedVerb) { protected boolean verbMatches(Role.PolicyRule rule, String requestedVerb) {
for (String ruleVerb : rule.getVerbs()) { for (String ruleVerb : rule.getVerbs()) {
if (Objects.equals(ruleVerb, WildCard.VerbAll)) { if (Objects.equals(ruleVerb, WildCard.VerbAll)) {
@ -71,7 +79,7 @@ public class RbacRequestEvaluation {
} }
protected boolean resourceMatches(Role.PolicyRule rule, String combinedRequestedResource, protected boolean resourceMatches(Role.PolicyRule rule, String combinedRequestedResource,
String requestedSubresource) { String requestedSubresource) {
for (String ruleResource : rule.getResources()) { for (String ruleResource : rule.getResources()) {
// if everything is allowed, we match // if everything is allowed, we match
if (Objects.equals(ruleResource, WildCard.ResourceAll)) { if (Objects.equals(ruleResource, WildCard.ResourceAll)) {
@ -125,8 +133,4 @@ public class RbacRequestEvaluation {
} }
return false; return false;
} }
protected boolean pluginNameMatches(PolicyRule rule, String pluginName) {
return StringUtils.equals(rule.getPluginName(), pluginName);
}
} }

View File

@ -17,34 +17,40 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
public class RequestInfo { public class RequestInfo {
boolean isResourceRequest; boolean isResourceRequest;
final String path; final String path;
String pluginName; String namespace;
String verb; String verb;
String apiPrefix; String apiPrefix;
String apiGroup; String apiGroup;
String apiVersion; String apiVersion;
String resource; String resource;
String subresource;
String name; String name;
String subresource;
String subName;
String[] parts; String[] parts;
public RequestInfo(boolean isResourceRequest, String path, String verb) { public RequestInfo(boolean isResourceRequest, String path, String verb) {
this(isResourceRequest, path, null, verb, null, null, null, null, null, null, null); this(isResourceRequest, path, null, verb, null, null, null, null, null, null, null, null);
} }
public RequestInfo(boolean isResourceRequest, String path, String pluginName, String verb, public RequestInfo(boolean isResourceRequest, String path, String namespace, String verb,
String apiPrefix, String apiPrefix,
String apiGroup, String apiGroup,
String apiVersion, String resource, String subresource, String name, String apiVersion, String resource, String name, String subresource, String subName,
String[] parts) { String[] parts) {
this.isResourceRequest = isResourceRequest; this.isResourceRequest = isResourceRequest;
this.path = StringUtils.defaultString(path, ""); this.path = StringUtils.defaultString(path, "");
this.pluginName = StringUtils.defaultString(pluginName, ""); this.namespace = StringUtils.defaultString(namespace, "");
this.verb = StringUtils.defaultString(verb, ""); this.verb = StringUtils.defaultString(verb, "");
this.apiPrefix = StringUtils.defaultString(apiPrefix, ""); this.apiPrefix = StringUtils.defaultString(apiPrefix, "");
this.apiGroup = StringUtils.defaultString(apiGroup, ""); this.apiGroup = StringUtils.defaultString(apiGroup, "");
this.apiVersion = StringUtils.defaultString(apiVersion, ""); this.apiVersion = StringUtils.defaultString(apiVersion, "");
this.resource = StringUtils.defaultString(resource, ""); this.resource = StringUtils.defaultString(resource, "");
this.subresource = StringUtils.defaultString(subresource, ""); this.subresource = StringUtils.defaultString(subresource, "");
this.subName = StringUtils.defaultString(subName, "");
this.name = StringUtils.defaultString(name, ""); this.name = StringUtils.defaultString(name, "");
this.parts = Objects.requireNonNullElseGet(parts, () -> new String[] {}); this.parts = Objects.requireNonNullElseGet(parts, () -> new String[] {});
} }

View File

@ -52,10 +52,11 @@ public class RequestInfoFactory {
* Valid Inputs: * Valid Inputs:
* <p>Resource paths</p> * <p>Resource paths</p>
* <pre> * <pre>
* /api/{version}/plugins * /apis/{api-group}/{version}/namespaces
* /api/{version}/plugins/{pluginName} * /api/{version}/namespaces
* /api/{version}/plugins/{pluginName}/{resource} * /api/{version}/namespaces/{namespace}
* /api/{version}/plugins/{pluginName}/{resource}/{resourceName} * /api/{version}/namespaces/{namespace}/{resource}
* /api/{version}/namespaces/{namespace}/{resource}/{resourceName}
* /api/{version}/{resource} * /api/{version}/{resource}
* /api/{version}/{resource}/{resourceName} * /api/{version}/{resource}/{resourceName}
* </pre> * </pre>
@ -138,22 +139,22 @@ public class RequestInfoFactory {
default -> ""; default -> "";
}; };
} }
// URL forms: /plugins/{plugin-name}/{kind}/*, where parts are adjusted to be relative // URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative
// to kind // to kind
if (Objects.equals(currentParts[0], "plugins") Set<String> namespaceSubresources = Set.of("status", "finalize");
&& StringUtils.isEmpty(requestInfo.getApiGroup())) { if (Objects.equals(currentParts[0], "namespaces")) {
if (currentParts.length > 1) { if (currentParts.length > 1) {
requestInfo.pluginName = currentParts[1]; requestInfo.namespace = currentParts[1];
// if there is another step after the plugin name and it is not a known // if there is another step after the namespace name and it is not a known
// plugins subresource // namespace subresource
// move currentParts to include it as a resource in its own right // move currentParts to include it as a resource in its own right
if (currentParts.length > 2) { if (currentParts.length > 2 && !namespaceSubresources.contains(currentParts[2])) {
currentParts = Arrays.copyOfRange(currentParts, 2, currentParts.length); currentParts = Arrays.copyOfRange(currentParts, 2, currentParts.length);
} }
} }
} else { } else {
requestInfo.pluginName = ""; requestInfo.namespace = "";
} }
// parsing successful, so we now know the proper value for .Parts // parsing successful, so we now know the proper value for .Parts
@ -163,6 +164,10 @@ public class RequestInfoFactory {
if (requestInfo.parts.length >= 3 && !specialVerbs.contains( if (requestInfo.parts.length >= 3 && !specialVerbs.contains(
requestInfo.verb)) { requestInfo.verb)) {
requestInfo.subresource = requestInfo.parts[2]; requestInfo.subresource = requestInfo.parts[2];
// if there is another step after the subresource name and it is not a known
if (requestInfo.parts.length >= 4) {
requestInfo.subName = requestInfo.parts[3];
}
} }
if (requestInfo.parts.length >= 2) { if (requestInfo.parts.length >= 2) {
@ -177,7 +182,7 @@ public class RequestInfoFactory {
// verb is a list or a watch // verb is a list or a watch
if (requestInfo.name.length() == 0 && "get".equals(requestInfo.verb)) { if (requestInfo.name.length() == 0 && "get".equals(requestInfo.verb)) {
var watch = request.getQueryParams().getFirst("watch"); var watch = request.getQueryParams().getFirst("watch");
if (isWatch(watch)) { if (Boolean.parseBoolean(watch)) {
requestInfo.verb = "watch"; requestInfo.verb = "watch";
} else { } else {
requestInfo.verb = "list"; requestInfo.verb = "list";
@ -185,17 +190,15 @@ public class RequestInfoFactory {
} }
// if there's no name on the request and we thought it was a deleted before, then the // if there's no name on the request and we thought it was a deleted before, then the
// actual verb is deletecollection // actual verb is deletecollection
if (requestInfo.name.length() == 0 if (Objects.equals(requestInfo.verb, "delete")) {
&& Objects.equals(requestInfo.verb, "delete")) { var deleteAll = request.getQueryParams().getFirst("all");
requestInfo.verb = "deletecollection"; if (Boolean.parseBoolean(deleteAll)) {
requestInfo.verb = "deletecollection";
}
} }
return requestInfo; return requestInfo;
} }
boolean isWatch(String requestParam) {
return "1".equals(requestParam) || "true".equals(requestParam);
}
private String[] splitPath(String path) { private String[] splitPath(String path) {
path = StringUtils.strip(path, "/"); path = StringUtils.strip(path, "/");
if (StringUtils.isEmpty(path)) { if (StringUtils.isEmpty(path)) {

View File

@ -69,7 +69,8 @@ class PluginRequestMappingHandlerMappingTest {
assertThat(info).isNotNull(); assertThat(info).isNotNull();
assertThat(info.getPatternsCondition().getPatterns()).isEqualTo( assertThat(info.getPatternsCondition().getPatterns()).isEqualTo(
Collections.singleton( Collections.singleton(
new PathPatternParser().parse("/api/v1alpha1/plugins/fakePlugin/user/{id}"))); new PathPatternParser().parse(
"/apis/plugin.api.halo.run/v1alpha1/plugins/fakePlugin/user/{id}")));
} }
@Test @Test
@ -94,7 +95,7 @@ class PluginRequestMappingHandlerMappingTest {
List<RequestMappingInfo> mappings = handlerMapping.getMappings("fakePlugin"); List<RequestMappingInfo> mappings = handlerMapping.getMappings("fakePlugin");
assertThat(mappings).hasSize(1); assertThat(mappings).hasSize(1);
assertThat(mappings.get(0).toString()).isEqualTo( assertThat(mappings.get(0).toString()).isEqualTo(
"{GET /api/v1alpha1/plugins/fakePlugin/user/{id}}"); "{GET /apis/plugin.api.halo.run/v1alpha1/plugins/fakePlugin/user/{id}}");
} }
@Test @Test
@ -120,7 +121,8 @@ class PluginRequestMappingHandlerMappingTest {
// get handler by mock exchange // get handler by mock exchange
ServerWebExchange exchange = ServerWebExchange exchange =
MockServerWebExchange.from(get("/api/v1alpha1/plugins/fakePlugin/foo")); MockServerWebExchange.from(
get("/apis/plugin.api.halo.run/v1alpha1/plugins/fakePlugin/foo"));
HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block(); HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
assertThat(hm).isNotNull(); assertThat(hm).isNotNull();
@ -135,7 +137,7 @@ class PluginRequestMappingHandlerMappingTest {
Method expected = Method expected =
ResolvableMethod.on(TestController.class).annot(getMapping("/foo").params("p")).build(); ResolvableMethod.on(TestController.class).annot(getMapping("/foo").params("p")).build();
String requestPath = "/api/v1alpha1/plugins/fakePlugin/foo?p=anything"; String requestPath = "/apis/plugin.api.halo.run/v1alpha1/plugins/fakePlugin/foo?p=anything";
ServerWebExchange exchange = MockServerWebExchange.from(get(requestPath)); ServerWebExchange exchange = MockServerWebExchange.from(get(requestPath));
HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block(); HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
@ -150,7 +152,7 @@ class PluginRequestMappingHandlerMappingTest {
Method expected = Method expected =
ResolvableMethod.on(TestController.class).annot(getMapping("")).build(); ResolvableMethod.on(TestController.class).annot(getMapping("")).build();
String requestPath = "/api/v1alpha1/plugins/fakePlugin"; String requestPath = "/apis/plugin.api.halo.run/v1alpha1/plugins/fakePlugin";
ServerWebExchange exchange = MockServerWebExchange.from(get(requestPath)); ServerWebExchange exchange = MockServerWebExchange.from(get(requestPath));
HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block(); HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
@ -163,7 +165,7 @@ class PluginRequestMappingHandlerMappingTest {
// register handler methods first // register handler methods first
handlerMapping.registerHandlerMethods("fakePlugin", new TestController()); handlerMapping.registerHandlerMethods("fakePlugin", new TestController());
String requestPath = "/api/v1alpha1/plugins/fakePlugin/bar"; String requestPath = "/apis/plugin.api.halo.run/v1alpha1/plugins/fakePlugin/bar";
ServerWebExchange exchange = MockServerWebExchange.from(post(requestPath)); ServerWebExchange exchange = MockServerWebExchange.from(post(requestPath));
Mono<Object> mono = this.handlerMapping.getHandler(exchange); Mono<Object> mono = this.handlerMapping.getHandler(exchange);

View File

@ -28,11 +28,10 @@ class PolicyRuleTest {
@Test @Test
public void constructPolicyRule() throws JsonProcessingException, JSONException { public void constructPolicyRule() throws JsonProcessingException, JSONException {
Role.PolicyRule policyRule = new Role.PolicyRule(null, null, null, null, null, null); Role.PolicyRule policyRule = new Role.PolicyRule(null, null, null, null, null);
assertThat(policyRule).isNotNull(); assertThat(policyRule).isNotNull();
JSONAssert.assertEquals(""" JSONAssert.assertEquals("""
{ {
"pluginName": "",
"apiGroups": [], "apiGroups": [],
"resources": [], "resources": [],
"resourceNames": [], "resourceNames": [],
@ -46,7 +45,6 @@ class PolicyRuleTest {
Role.PolicyRule policyByBuilder = new Role.PolicyRule.Builder().build(); Role.PolicyRule policyByBuilder = new Role.PolicyRule.Builder().build();
JSONAssert.assertEquals(""" JSONAssert.assertEquals("""
{ {
"pluginName": "",
"apiGroups": [], "apiGroups": [],
"resources": [], "resources": [],
"resourceNames": [], "resourceNames": [],
@ -58,7 +56,6 @@ class PolicyRuleTest {
true); true);
Role.PolicyRule policyNonNull = new Role.PolicyRule.Builder() Role.PolicyRule policyNonNull = new Role.PolicyRule.Builder()
.pluginName("fakePluginName")
.apiGroups("group") .apiGroups("group")
.resources("resource-1", "resource-2") .resources("resource-1", "resource-2")
.resourceNames("resourceName") .resourceNames("resourceName")
@ -68,7 +65,6 @@ class PolicyRuleTest {
JsonNode expected = objectMapper.readTree(""" JsonNode expected = objectMapper.readTree("""
{ {
"pluginName": "fakePluginName",
"apiGroups": [ "apiGroups": [
"group" "group"
], ],

View File

@ -87,23 +87,28 @@ public class RequestInfoResolverTest {
@Test @Test
void pluginsScopedAndPluginManage() { void pluginsScopedAndPluginManage() {
List<SuccessCase> testCases = List<CustomSuccessCase> testCases =
List.of(new SuccessCase("DELETE", "/apis/extensions/v1/plugins/other/posts", List.of(
"delete", "apis", "extensions", "v1", "", "plugins", "posts", "other", new CustomSuccessCase("DELETE", "/apis/plugin.api.halo.run/v1/plugins/other/posts",
"delete", "apis", "plugin.api.halo.run", "v1", "", "plugins", "posts", "", "",
new String[] {"plugins", "other", "posts"}), new String[] {"plugins", "other", "posts"}),
// api group identification // api group identification
new SuccessCase("POST", "/apis/extensions/v1/plugins/other/posts", "create", "apis", new CustomSuccessCase("POST",
"extensions", "v1", "", "plugins", "posts", "other", "/apis/plugin.api.halo.run/v1/plugins/other/posts/foo",
new String[] {"plugins", "other", "posts"}), "create", "apis",
"plugin.api.halo.run", "v1", "", "plugins", "posts", "other", "foo",
new String[] {"plugins", "other", "posts", "foo"}),
// api version identification // api version identification
new SuccessCase("POST", "/apis/extensions/v1beta3/plugins/other/posts", "create", new CustomSuccessCase("POST",
"apis", "extensions", "v1beta3", "", "plugins", "posts", "other", "/apis/plugin.api.halo.run/v1beta3/plugins/other/posts/bar", "create",
new String[] {"plugins", "other", "posts"})); "apis", "plugin.api.halo.run", "v1beta3", "", "plugins", "posts", "other",
"bar",
new String[] {"plugins", "other", "posts", "bar"}));
// 以 /apis 开头的 plugins 资源为 core 中管理插件使用的资源 // 以 /apis 开头的 plugins 资源为 core 中管理插件使用的资源
for (SuccessCase successCase : testCases) { for (CustomSuccessCase successCase : testCases) {
var request = var request =
method(HttpMethod.valueOf(successCase.method), method(HttpMethod.valueOf(successCase.method),
successCase.url).build(); successCase.url).build();
@ -112,21 +117,19 @@ public class RequestInfoResolverTest {
assertRequestInfoCase(successCase, requestInfo); assertRequestInfoCase(successCase, requestInfo);
} }
List<SuccessCase> pluginScopedCases = List<CustomSuccessCase> pluginScopedCases =
List.of(new SuccessCase("DELETE", "/api/v1/plugins/other/posts", List.of(
"deletecollection", "api", "", "v1", "other", "posts", "", "", new CustomSuccessCase("DELETE", "/apis/plugin.api.halo.run/v1/plugins/other/posts",
new String[] {"posts"}), "delete", "apis", "plugin.api.halo.run", "v1", "", "plugins", "posts",
"other", "", new String[] {"plugins", "other", "posts"}),
// api group identification // api group identification
new SuccessCase("POST", "/api/v1/plugins/other/posts", "create", "api", new CustomSuccessCase("POST",
"", "v1", "other", "posts", "", "", new String[] {"posts"}), "/apis/plugin.api.halo.run/v1/plugins/other/posts/some-name", "create", "apis",
"plugin.api.halo.run", "v1", "other", "plugins", "posts", "other", "some-name",
new String[] {"plugins", "other", "posts", "some-name"}));
// api version identification for (CustomSuccessCase pluginScopedCase : pluginScopedCases) {
new SuccessCase("POST", "/api/v1beta3/plugins/other/posts", "create",
"api", "", "v1beta3", "other", "posts", "", "",
new String[] {"posts"}));
for (SuccessCase pluginScopedCase : pluginScopedCases) {
var request = var request =
method(HttpMethod.valueOf(pluginScopedCase.method), method(HttpMethod.valueOf(pluginScopedCase.method),
pluginScopedCase.url).build(); pluginScopedCase.url).build();
@ -136,14 +139,16 @@ public class RequestInfoResolverTest {
} }
} }
private void assertRequestInfoCase(SuccessCase pluginScopedCase, RequestInfo requestInfo) { private void assertRequestInfoCase(CustomSuccessCase pluginScopedCase,
assertThat(requestInfo.getPluginName()).isEqualTo(pluginScopedCase.expectedPluginName); RequestInfo requestInfo) {
assertThat(requestInfo.getVerb()).isEqualTo(pluginScopedCase.expectedVerb); assertThat(requestInfo.getVerb()).isEqualTo(pluginScopedCase.expectedVerb);
assertThat(requestInfo.getParts()).isEqualTo(pluginScopedCase.expectedParts); assertThat(requestInfo.getParts()).isEqualTo(pluginScopedCase.expectedParts);
assertThat(requestInfo.getApiGroup()).isEqualTo(pluginScopedCase.expectedAPIGroup); assertThat(requestInfo.getApiGroup()).isEqualTo(pluginScopedCase.expectedAPIGroup);
assertThat(requestInfo.getResource()).isEqualTo(pluginScopedCase.expectedResource); assertThat(requestInfo.getResource()).isEqualTo(pluginScopedCase.expectedResource);
assertThat(requestInfo.getSubresource()) assertThat(requestInfo.getSubresource())
.isEqualTo(pluginScopedCase.expectedSubresource()); .isEqualTo(pluginScopedCase.expectedSubresource());
assertThat(requestInfo.getSubName())
.isEqualTo(pluginScopedCase.expectedSubName());
} }
@Test @Test
@ -174,11 +179,11 @@ public class RequestInfoResolverTest {
List<PolicyRule> rules = List.of( List<PolicyRule> rules = List.of(
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get") new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get")
.build(), .build(),
new PolicyRule.Builder().pluginName("fakePlugin").apiGroups("").resources("posts")
.verbs("list", "get").build(),
new PolicyRule.Builder().pluginName("fakePlugin").apiGroups("")
.resources("posts/tags").verbs("list", "get").build(),
new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*").build(), new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*").build(),
new PolicyRule.Builder().apiGroups("plugin.api.halo.run").resources("plugins/users")
.resourceNames("foo/bar").verbs("*").build(),
new PolicyRule.Builder().apiGroups("plugin.api.halo.run").resources("plugins/users")
.resourceNames("foo").verbs("*").build(),
new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head") new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head")
.build()); .build());
role.setRules(rules); role.setRules(rules);
@ -230,14 +235,17 @@ public class RequestInfoResolverTest {
new RequestResolveCase("/api/v1/posts", "DELETE", false), new RequestResolveCase("/api/v1/posts", "DELETE", false),
new RequestResolveCase("/api/v1/posts/aName", "UPDATE", false), new RequestResolveCase("/api/v1/posts/aName", "UPDATE", false),
// plugin resource
new RequestResolveCase("/api/v1/plugins/fakePlugin/posts", "GET", true),
new RequestResolveCase("/api/v1/plugins/fakePlugin/posts/some-name", "GET", true),
new RequestResolveCase("/api/v1/plugins/fakePlugin/posts/some-name/tags", "GET", true),
// group resource url // group resource url
new RequestResolveCase("/apis/group/v1/posts", "GET", false), new RequestResolveCase("/apis/group/v1/posts", "GET", false),
// plugin custom resource url
new RequestResolveCase("/apis/plugin.api.halo.run/v1alpha1/plugins/foo/users", "GET",
true),
new RequestResolveCase("/apis/plugin.api.halo.run/v1alpha1/plugins/foo/users/bar",
"GET", true),
new RequestResolveCase("/apis/plugin.api.halo.run/v1alpha1/plugins/foo/posts/bar",
"GET", false),
// non resource url // non resource url
new RequestResolveCase("/healthy", "GET", true), new RequestResolveCase("/healthy", "GET", true),
new RequestResolveCase("/healthy", "POST", true), new RequestResolveCase("/healthy", "POST", true),
@ -255,40 +263,47 @@ public class RequestInfoResolverTest {
public record SuccessCase(String method, String url, String expectedVerb, public record SuccessCase(String method, String url, String expectedVerb,
String expectedAPIPrefix, String expectedAPIGroup, String expectedAPIPrefix, String expectedAPIGroup,
String expectedAPIVersion, String expectedPluginName, String expectedAPIVersion, String expectedNamespace,
String expectedResource, String expectedSubresource, String expectedResource, String expectedSubresource,
String expectedName, String[] expectedParts) { String expectedName, String[] expectedParts) {
} }
public record CustomSuccessCase(String method, String url, String expectedVerb,
String expectedAPIPrefix, String expectedAPIGroup,
String expectedAPIVersion, String expectedNamespace,
String expectedResource, String expectedSubresource,
String expectedName, String expectedSubName,
String[] expectedParts) {
}
List<SuccessCase> getTestRequestInfos() { List<SuccessCase> getTestRequestInfos() {
String namespaceAll = "*"; String namespaceAll = "*";
return List.of( return List.of(
new SuccessCase("GET", "/api/v1/plugins", "list", "api", "", "v1", "", "plugins", new SuccessCase("GET", "/api/v1/namespaces", "list", "api", "", "v1", "", "namespaces",
"", "", new String[] {"plugins"}), "", "", new String[] {"namespaces"}),
new SuccessCase("GET", "/api/v1/plugins/other", "get", "api", "", "v1", "other", new SuccessCase("GET", "/api/v1/namespaces/other", "get", "api", "", "v1", "other",
"plugins", "", "other", new String[] {"plugins", "other"}), "namespaces", "", "other", new String[] {"namespaces", "other"}),
new SuccessCase("GET", "/api/v1/plugins/other/posts", "list", "api", "", "v1", new SuccessCase("GET", "/api/v1/namespaces/other/posts", "list", "api", "", "v1",
"other", "posts", "", "", new String[] {"posts"}), "other", "posts", "", "", new String[] {"posts"}),
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo", "get", "api", "", "v1", new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo", "get", "api", "", "v1",
"other", "posts", "", "foo", new String[] {"posts", "foo"}), "other", "posts", "", "foo", new String[] {"posts", "foo"}),
new SuccessCase("HEAD", "/api/v1/plugins/other/posts/foo", "get", "api", "", "v1", new SuccessCase("HEAD", "/api/v1/namespaces/other/posts/foo", "get", "api", "", "v1",
"other", "posts", "", "foo", new String[] {"posts", "foo"}), "other", "posts", "", "foo", new String[] {"posts", "foo"}),
new SuccessCase("GET", "/api/v1/posts", "list", "api", "", "v1", namespaceAll, "posts", new SuccessCase("GET", "/api/v1/posts", "list", "api", "", "v1", namespaceAll, "posts",
"", "", new String[] {"posts"}), "", "", new String[] {"posts"}),
new SuccessCase("HEAD", "/api/v1/posts", "list", "api", "", "v1", namespaceAll, "posts", new SuccessCase("HEAD", "/api/v1/posts", "list", "api", "", "v1", namespaceAll, "posts",
"", "", new String[] {"posts"}), "", "", new String[] {"posts"}),
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo", "get", "api", "", "v1", new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo", "get", "api", "", "v1",
"other", "posts", "", "foo", new String[] {"posts", "foo"}), "other", "posts", "", "foo", new String[] {"posts", "foo"}),
new SuccessCase("GET", "/api/v1/plugins/other/posts", "list", "api", "", "v1", new SuccessCase("GET", "/api/v1/namespaces/other/posts", "list", "api", "", "v1",
"other", "posts", "", "", new String[] {"posts"}), "other", "posts", "", "", new String[] {"posts"}),
// special verbs // special verbs
new SuccessCase("GET", "/api/v1/proxy/plugins/other/posts/foo", "proxy", "api", "", new SuccessCase("GET", "/api/v1/proxy/namespaces/other/posts/foo", "proxy", "api", "",
"v1", "other", "posts", "", "foo", new String[] {"posts", "foo"}), "v1", "other", "posts", "", "foo", new String[] {"posts", "foo"}),
new SuccessCase("GET", new SuccessCase("GET",
"/api/v1/proxy/plugins/other/posts/foo/subpath/not/a/subresource", "proxy", "/api/v1/proxy/namespaces/other/posts/foo/subpath/not/a/subresource", "proxy",
"api", "", "v1", "other", "posts", "", "foo", "api", "", "v1", "other", "posts", "", "foo",
new String[] {"posts", "foo", "subpath", "not", "a", "subresource"}), new String[] {"posts", "foo", "subpath", "not", "a", "subresource"}),
new SuccessCase("GET", "/api/v1/watch/posts", "watch", "api", "", "v1", namespaceAll, new SuccessCase("GET", "/api/v1/watch/posts", "watch", "api", "", "v1", namespaceAll,
@ -297,35 +312,55 @@ public class RequestInfoResolverTest {
namespaceAll, "posts", "", "", new String[] {"posts"}), namespaceAll, "posts", "", "", new String[] {"posts"}),
new SuccessCase("GET", "/api/v1/posts?watch=false", "list", "api", "", "v1", new SuccessCase("GET", "/api/v1/posts?watch=false", "list", "api", "", "v1",
namespaceAll, "posts", "", "", new String[] {"posts"}), namespaceAll, "posts", "", "", new String[] {"posts"}),
new SuccessCase("GET", "/api/v1/watch/plugins/other/posts", "watch", "api", "", "v1", new SuccessCase("GET", "/api/v1/watch/namespaces/other/posts", "watch", "api", "", "v1",
"other", "posts", "", "", new String[] {"posts"}), "other", "posts", "", "", new String[] {"posts"}),
new SuccessCase("GET", "/api/v1/plugins/other/posts?watch=1", "watch", "api", "", new SuccessCase("GET", "/api/v1/namespaces/other/posts?watch=true", "watch", "api", "",
"v1", "other", "posts", "", "", new String[] {"posts"}), "v1", "other", "posts", "", "", new String[] {"posts"}),
new SuccessCase("GET", "/api/v1/plugins/other/posts?watch=0", "list", "api", "", new SuccessCase("GET", "/api/v1/namespaces/other/posts?watch=false", "list", "api", "",
"v1", "other", "posts", "", "", new String[] {"posts"}), "v1", "other", "posts", "", "", new String[] {"posts"}),
// subresource identification // subresource identification
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo/status", "get", "api", "", new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo/status", "get", "api", "",
"v1", "other", "posts", "status", "foo", new String[] {"posts", "foo", "status"}), "v1", "other", "posts", "status", "foo", new String[] {"posts", "foo", "status"}),
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo/proxy/subpath", "get", "api", new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo/proxy/subpath", "get", "api",
"", "v1", "other", "posts", "proxy", "foo", "", "v1", "other", "posts", "proxy", "foo",
new String[] {"posts", "foo", "proxy", "subpath"}), new String[] {"posts", "foo", "proxy", "subpath"}),
new SuccessCase("PUT", "/api/v1/namespaces/other/finalize", "update", "api", "", "v1",
"other", "namespaces", "finalize", "other",
new String[] {"namespaces", "other", "finalize"}),
new SuccessCase("PUT", "/api/v1/namespaces/other/status", "update", "api", "", "v1",
"other", "namespaces", "status", "other",
new String[] {"namespaces", "other", "status"}),
// verb identification // verb identification
new SuccessCase("PATCH", "/api/v1/plugins/other/posts/foo", "patch", "api", "", "v1", new SuccessCase("PATCH", "/api/v1/namespaces/other/posts/foo", "patch", "api", "", "v1",
"other", "posts", "", "foo", new String[] {"posts", "foo"}), "other", "posts", "", "foo", new String[] {"posts", "foo"}),
new SuccessCase("DELETE", "/api/v1/plugins/other/posts/foo", "delete", "api", "", new SuccessCase("DELETE", "/api/v1/namespaces/other/posts/foo", "delete", "api", "",
"v1", "other", "posts", "", "foo", new String[] {"posts", "foo"}), "v1", "other", "posts", "", "foo", new String[] {"posts", "foo"}),
new SuccessCase("POST", "/api/v1/plugins/other/posts", "create", "api", "", "v1", new SuccessCase("POST", "/api/v1/namespaces/other/posts", "create", "api", "", "v1",
"other", "posts", "", "", new String[] {"posts"}), "other", "posts", "", "", new String[] {"posts"}),
// deletecollection verb identification // deletecollection verb identification
new SuccessCase("DELETE", "/api/v1/nodes", "deletecollection", "api", "", "v1", "", new SuccessCase("DELETE", "/api/v1/nodes?all=true", "deletecollection", "api", "", "v1",
"",
"nodes", "", "", new String[] {"nodes"}), "nodes", "", "", new String[] {"nodes"}),
new SuccessCase("DELETE", "/api/v1/plugins", "deletecollection", "api", "", "v1", "", new SuccessCase("DELETE", "/api/v1/namespaces?all=false", "delete", "api", "",
"plugins", "", "", new String[] {"plugins"}), "v1", "",
new SuccessCase("DELETE", "/api/v1/plugins/other/posts", "deletecollection", "api", "namespaces", "", "", new String[] {"namespaces"}),
"", "v1", "other", "posts", "", "", new String[] {"posts"})); new SuccessCase("DELETE", "/api/v1/namespaces/other/posts?all=true", "deletecollection",
"api",
"", "v1", "other", "posts", "", "", new String[] {"posts"}),
new SuccessCase("DELETE", "/apis/extensions/v1/namespaces/other/posts?all=true",
"deletecollection", "apis", "extensions", "v1", "other", "posts", "", "",
new String[] {"posts"}),
// api group identification
new SuccessCase("POST", "/apis/extensions/v1/namespaces/other/posts", "create", "apis",
"extensions", "v1", "other", "posts", "", "", new String[] {"posts"}),
// api version identification
new SuccessCase("POST", "/apis/extensions/v1beta3/namespaces/other/posts", "create",
"apis", "extensions", "v1beta3", "other", "posts", "", "", new String[] {"posts"}));
} }
} }