mirror of https://github.com/halo-dev/halo
feat: add request mapping handler mapping for plugin (#2161)
* feat: add requet mapping handler mapping for plugin * fix: request info resolve * fix: test code style * refactor: plugin api version resolve * fix: merge conflictspull/2175/head
parent
89eeccd99c
commit
7cd1282ad3
|
@ -4,7 +4,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
|
@ -32,29 +34,28 @@ public class Role extends AbstractExtension {
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@Getter
|
||||
public static class 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
|
||||
* resources in any API group will be allowed.
|
||||
*/
|
||||
String[] apiGroups;
|
||||
final String[] apiGroups;
|
||||
|
||||
/**
|
||||
* Resources is a list of resources this rule applies to. '*' represents all resources in
|
||||
* the specified apiGroups.
|
||||
* '*/foo' represents the subresource 'foo' for all resources in
|
||||
* the specified apiGroups.
|
||||
* '*/foo' represents the subresource 'foo' for all resources in the specified
|
||||
* apiGroups.
|
||||
*/
|
||||
String[] resources;
|
||||
final String[] resources;
|
||||
|
||||
/**
|
||||
* ResourceNames is an optional white list of names that the rule applies to. An empty set
|
||||
* means that everything is allowed.
|
||||
*/
|
||||
String[] resourceNames;
|
||||
final String[] resourceNames;
|
||||
|
||||
/**
|
||||
* NonResourceURLs is a set of partial urls that a user should have access to.
|
||||
|
@ -66,19 +67,26 @@ public class Role extends AbstractExtension {
|
|||
* Rules can either apply to API resources (such as "pods" or "secrets") or non-resource
|
||||
* URL paths (such as "/api"), but not both.
|
||||
*/
|
||||
String[] nonResourceURLs;
|
||||
final String[] nonResourceURLs;
|
||||
|
||||
/**
|
||||
* about who the rule applies to or which namespace the rule applies to.
|
||||
*/
|
||||
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() {
|
||||
this(null, null, null, null, null);
|
||||
this(null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public PolicyRule(String[] apiGroups, String[] resources, String[] resourceNames,
|
||||
public PolicyRule(String pluginName, String[] apiGroups, String[] resources,
|
||||
String[] resourceNames,
|
||||
String[] nonResourceURLs, String[] verbs) {
|
||||
this.pluginName = StringUtils.defaultString(pluginName);
|
||||
this.apiGroups = nullElseEmpty(apiGroups);
|
||||
this.resources = nullElseEmpty(resources);
|
||||
this.resourceNames = nullElseEmpty(resourceNames);
|
||||
|
@ -95,11 +103,22 @@ public class Role extends AbstractExtension {
|
|||
|
||||
public static class Builder {
|
||||
String[] apiGroups;
|
||||
|
||||
String[] resources;
|
||||
|
||||
String[] resourceNames;
|
||||
|
||||
String[] nonResourceURLs;
|
||||
|
||||
String[] verbs;
|
||||
|
||||
String pluginName;
|
||||
|
||||
public Builder pluginName(String pluginName) {
|
||||
this.pluginName = pluginName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder apiGroups(String... apiGroups) {
|
||||
this.apiGroups = apiGroups;
|
||||
return this;
|
||||
|
@ -126,7 +145,9 @@ public class Role extends AbstractExtension {
|
|||
}
|
||||
|
||||
public PolicyRule build() {
|
||||
return new PolicyRule(apiGroups, resources, resourceNames, nonResourceURLs, verbs);
|
||||
return new PolicyRule(pluginName, apiGroups, resources, resourceNames,
|
||||
nonResourceURLs,
|
||||
verbs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Api version.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Target({ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ApiVersion {
|
||||
|
||||
/**
|
||||
* Api version value.
|
||||
*
|
||||
* @return api version string
|
||||
*/
|
||||
String value();
|
||||
}
|
|
@ -183,7 +183,7 @@ public class HaloPluginManager extends DefaultPluginManager
|
|||
|
||||
pluginWrapper.getPlugin().start();
|
||||
|
||||
requestMappingManager.registerControllers(pluginWrapper);
|
||||
requestMappingManager.registerHandlerMappings(pluginWrapper);
|
||||
|
||||
pluginWrapper.setPluginState(PluginState.STARTED);
|
||||
pluginWrapper.setFailedException(null);
|
||||
|
@ -259,7 +259,7 @@ public class HaloPluginManager extends DefaultPluginManager
|
|||
// create plugin instance and start it
|
||||
pluginWrapper.getPlugin().start();
|
||||
|
||||
requestMappingManager.registerControllers(pluginWrapper);
|
||||
requestMappingManager.registerHandlerMappings(pluginWrapper);
|
||||
|
||||
pluginWrapper.setPluginState(PluginState.STARTED);
|
||||
startedPlugins.add(pluginWrapper);
|
||||
|
@ -358,7 +358,7 @@ public class HaloPluginManager extends DefaultPluginManager
|
|||
*/
|
||||
public void releaseAdditionalResources(String pluginId) {
|
||||
// release request mapping
|
||||
requestMappingManager.removeControllerMapping(pluginId);
|
||||
requestMappingManager.removeHandlerMappings(pluginId);
|
||||
try {
|
||||
pluginApplicationInitializer.contextDestroyed(pluginId);
|
||||
} catch (Exception e) {
|
||||
|
|
|
@ -14,11 +14,12 @@ import org.pf4j.PluginLoader;
|
|||
import org.pf4j.PluginManager;
|
||||
import org.pf4j.PluginStatusProvider;
|
||||
import org.pf4j.RuntimeMode;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
|
||||
/**
|
||||
* Plugin autoconfiguration for Spring Boot.
|
||||
|
@ -32,18 +33,26 @@ import org.springframework.web.reactive.result.method.annotation.RequestMappingH
|
|||
public class PluginAutoConfiguration {
|
||||
|
||||
private final PluginProperties pluginProperties;
|
||||
|
||||
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||
@Qualifier("webFluxContentTypeResolver")
|
||||
private final RequestedContentTypeResolver requestedContentTypeResolver;
|
||||
|
||||
public PluginAutoConfiguration(PluginProperties pluginProperties,
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping) {
|
||||
RequestedContentTypeResolver requestedContentTypeResolver) {
|
||||
this.pluginProperties = pluginProperties;
|
||||
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
|
||||
this.requestedContentTypeResolver = requestedContentTypeResolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping() {
|
||||
PluginRequestMappingHandlerMapping mapping = new PluginRequestMappingHandlerMapping();
|
||||
mapping.setContentTypeResolver(requestedContentTypeResolver);
|
||||
mapping.setOrder(-1);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PluginRequestMappingManager pluginRequestMappingManager() {
|
||||
return new PluginRequestMappingManager(requestMappingHandlerMapping);
|
||||
return new PluginRequestMappingManager(pluginRequestMappingHandlerMapping());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
@ -98,6 +98,9 @@ public class PluginCompositeRouterFunction implements RouterFunction<ServerRespo
|
|||
@SuppressWarnings("unchecked")
|
||||
private List<RouterFunction<ServerResponse>> routerFunctions(
|
||||
PluginApplicationContext applicationContext) {
|
||||
// TODO: Since the parent of the ApplicationContext of the plugin is RootApplicationContext
|
||||
// obtaining the RouterFunction here will obtain the existing in the parent
|
||||
// resulting in a loop when there is no matching route
|
||||
List<RouterFunction<ServerResponse>> functions =
|
||||
applicationContext.getBeanProvider(RouterFunction.class)
|
||||
.orderedStream()
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.reactive.result.method.RequestMappingInfo;
|
||||
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
|
||||
/**
|
||||
* An extension of {@link RequestMappingInfoHandlerMapping} that creates
|
||||
* {@link RequestMappingInfo} instances from class-level and method-level
|
||||
* {@link RequestMapping} annotations used by plugin.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class PluginRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
|
||||
|
||||
private final MultiValueMap<String, RequestMappingInfo> pluginMappingInfo =
|
||||
new LinkedMultiValueMap<>();
|
||||
|
||||
/**
|
||||
* Register handler methods according to the plugin id and the controller(annotated
|
||||
* {@link Controller}) bean.
|
||||
*
|
||||
* @param pluginId plugin id to be registered
|
||||
* @param handler controller bean
|
||||
*/
|
||||
public void registerHandlerMethods(String pluginId, Object handler) {
|
||||
Class<?> handlerType = (handler instanceof String beanName
|
||||
? obtainApplicationContext().getType(beanName) : handler.getClass());
|
||||
|
||||
if (handlerType != null) {
|
||||
final Class<?> userType = ClassUtils.getUserClass(handlerType);
|
||||
Map<Method, RequestMappingInfo> methods = MethodIntrospector.selectMethods(userType,
|
||||
(MethodIntrospector.MetadataLookup<RequestMappingInfo>)
|
||||
method -> getPluginMappingForMethod(pluginId, method, userType));
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(formatMappings(userType, methods));
|
||||
} else if (mappingsLogger.isDebugEnabled()) {
|
||||
mappingsLogger.debug(formatMappings(userType, methods));
|
||||
}
|
||||
methods.forEach((method, mapping) -> {
|
||||
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
|
||||
registerHandlerMethod(handler, invocableMethod, mapping);
|
||||
pluginMappingInfo.add(pluginId, mapping);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private String formatMappings(Class<?> userType, Map<Method, RequestMappingInfo> methods) {
|
||||
String packageName = ClassUtils.getPackageName(userType);
|
||||
String formattedType = (StringUtils.hasText(packageName)
|
||||
? Arrays.stream(packageName.split("\\."))
|
||||
.map(packageSegment -> packageSegment.substring(0, 1))
|
||||
.collect(Collectors.joining(".", "", "." + userType.getSimpleName())) :
|
||||
userType.getSimpleName());
|
||||
Function<Method, String> methodFormatter =
|
||||
method -> Arrays.stream(method.getParameterTypes())
|
||||
.map(Class::getSimpleName)
|
||||
.collect(Collectors.joining(",", "(", ")"));
|
||||
return methods.entrySet().stream()
|
||||
.map(e -> {
|
||||
Method method = e.getKey();
|
||||
return e.getValue() + ": " + method.getName() + methodFormatter.apply(method);
|
||||
})
|
||||
.collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove handler methods and mapping based on plugin id.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
*/
|
||||
public void unregister(String pluginId) {
|
||||
Assert.notNull(pluginId, "The pluginId must not be null.");
|
||||
if (!pluginMappingInfo.containsKey(pluginId)) {
|
||||
return;
|
||||
}
|
||||
pluginMappingInfo.remove(pluginId).forEach(this::unregisterMapping);
|
||||
}
|
||||
|
||||
protected List<RequestMappingInfo> getMappings(String pluginId) {
|
||||
List<RequestMappingInfo> requestMappingInfos = pluginMappingInfo.get(pluginId);
|
||||
if (requestMappingInfos == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return List.copyOf(requestMappingInfos);
|
||||
}
|
||||
|
||||
protected RequestMappingInfo getPluginMappingForMethod(String pluginId,
|
||||
Method method, Class<?> handlerType) {
|
||||
RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
|
||||
if (info != null) {
|
||||
ApiVersion apiVersion = handlerType.getAnnotation(ApiVersion.class);
|
||||
if (apiVersion == null) {
|
||||
throw new PluginRuntimeException(
|
||||
"The handler [" + handlerType + "] is missing @ApiVersion annotation.");
|
||||
}
|
||||
info = RequestMappingInfo.paths(buildPrefix(pluginId, apiVersion.value())).build()
|
||||
.combine(info);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
protected String buildPrefix(String pluginId, String version) {
|
||||
GroupVersion groupVersion = GroupVersion.parseAPIVersion(version);
|
||||
return String.format("/api/%s/plugins/%s", groupVersion.version(), pluginId);
|
||||
}
|
||||
}
|
|
@ -1,13 +1,10 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
/**
|
||||
|
@ -19,56 +16,25 @@ import org.springframework.web.reactive.result.method.annotation.RequestMappingH
|
|||
@Slf4j
|
||||
public class PluginRequestMappingManager {
|
||||
|
||||
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||
private final PluginRequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||
|
||||
public PluginRequestMappingManager(
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping) {
|
||||
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
|
||||
PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping) {
|
||||
this.requestMappingHandlerMapping = pluginRequestMappingHandlerMapping;
|
||||
}
|
||||
|
||||
public void registerControllers(PluginWrapper pluginWrapper) {
|
||||
public void registerHandlerMappings(PluginWrapper pluginWrapper) {
|
||||
String pluginId = pluginWrapper.getPluginId();
|
||||
getControllerBeans(pluginId)
|
||||
.forEach(this::registerController);
|
||||
.forEach(handler ->
|
||||
requestMappingHandlerMapping.registerHandlerMethods(pluginId, handler));
|
||||
}
|
||||
|
||||
private void registerController(Object controller) {
|
||||
log.debug("Registering plugin request mapping for bean: [{}]", controller);
|
||||
Method detectHandlerMethods = ReflectionUtils.findMethod(RequestMappingHandlerMapping.class,
|
||||
"detectHandlerMethods", Object.class);
|
||||
if (detectHandlerMethods == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
detectHandlerMethods.setAccessible(true);
|
||||
detectHandlerMethods.invoke(requestMappingHandlerMapping, controller);
|
||||
} catch (IllegalStateException ise) {
|
||||
// ignore this
|
||||
log.warn(ise.getMessage());
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvocationTargetException e) {
|
||||
log.warn("invocation target exception: [{}]", e.getMessage(), e);
|
||||
}
|
||||
public void removeHandlerMappings(String pluginId) {
|
||||
requestMappingHandlerMapping.unregister(pluginId);
|
||||
}
|
||||
|
||||
private void unregisterControllerMappingInternal(Object controller) {
|
||||
requestMappingHandlerMapping.getHandlerMethods()
|
||||
.forEach((mapping, handlerMethod) -> {
|
||||
if (controller == handlerMethod.getBean()) {
|
||||
log.debug("Removed plugin request mapping [{}] from bean [{}]", mapping,
|
||||
controller);
|
||||
requestMappingHandlerMapping.unregisterMapping(mapping);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void removeControllerMapping(String pluginId) {
|
||||
getControllerBeans(pluginId)
|
||||
.forEach(this::unregisterControllerMappingInternal);
|
||||
}
|
||||
|
||||
public Collection<Object> getControllerBeans(String pluginId) {
|
||||
private Collection<Object> getControllerBeans(String pluginId) {
|
||||
GenericApplicationContext pluginContext =
|
||||
ExtensionContextRegistry.getInstance().getByPluginId(pluginId);
|
||||
return pluginContext.getBeansWithAnnotation(Controller.class).values();
|
||||
|
|
|
@ -65,4 +65,6 @@ public interface Attributes {
|
|||
* @return returns the path of the request
|
||||
*/
|
||||
String getPath();
|
||||
|
||||
String pluginName();
|
||||
}
|
||||
|
|
|
@ -67,4 +67,9 @@ public class AttributesRecord implements Attributes {
|
|||
public String getPath() {
|
||||
return requestInfo.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pluginName() {
|
||||
return requestInfo.getPluginName();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import java.util.Objects;
|
|||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.Role.PolicyRule;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -38,7 +39,8 @@ public class RbacRequestEvaluation {
|
|||
return verbMatches(rule, requestAttributes.getVerb())
|
||||
&& apiGroupMatches(rule, requestAttributes.getApiGroup())
|
||||
&& resourceMatches(rule, combinedResource, requestAttributes.getSubresource())
|
||||
&& resourceNameMatches(rule, requestAttributes.getName());
|
||||
&& resourceNameMatches(rule, requestAttributes.getName())
|
||||
&& pluginNameMatches(rule, requestAttributes.pluginName());
|
||||
}
|
||||
return verbMatches(rule, requestAttributes.getVerb())
|
||||
&& nonResourceURLMatches(rule, requestAttributes.getPath());
|
||||
|
@ -123,4 +125,8 @@ public class RbacRequestEvaluation {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean pluginNameMatches(PolicyRule rule, String pluginName) {
|
||||
return StringUtils.equals(rule.getPluginName(), pluginName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,11 @@ import java.util.Objects;
|
|||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
|
||||
/**
|
||||
* RequestInfo holds information parsed from the {@link ServerHttpRequest}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
@ -14,7 +17,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||
public class RequestInfo {
|
||||
boolean isResourceRequest;
|
||||
final String path;
|
||||
String namespace;
|
||||
String pluginName;
|
||||
String verb;
|
||||
String apiPrefix;
|
||||
String apiGroup;
|
||||
|
@ -28,14 +31,14 @@ public class RequestInfo {
|
|||
this(isResourceRequest, path, null, verb, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public RequestInfo(boolean isResourceRequest, String path, String namespace, String verb,
|
||||
public RequestInfo(boolean isResourceRequest, String path, String pluginName, String verb,
|
||||
String apiPrefix,
|
||||
String apiGroup,
|
||||
String apiVersion, String resource, String subresource, String name,
|
||||
String[] parts) {
|
||||
this.isResourceRequest = isResourceRequest;
|
||||
this.path = StringUtils.defaultString(path, "");
|
||||
this.namespace = StringUtils.defaultString(namespace, "");
|
||||
this.pluginName = StringUtils.defaultString(pluginName, "");
|
||||
this.verb = StringUtils.defaultString(verb, "");
|
||||
this.apiPrefix = StringUtils.defaultString(apiPrefix, "");
|
||||
this.apiGroup = StringUtils.defaultString(apiGroup, "");
|
||||
|
|
|
@ -8,6 +8,8 @@ import org.springframework.http.server.PathContainer;
|
|||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
|
||||
/**
|
||||
* Creates {@link RequestInfo} from {@link ServerHttpRequest}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
@ -25,6 +27,9 @@ public class RequestInfoFactory {
|
|||
*/
|
||||
final Set<String> grouplessApiPrefixes;
|
||||
|
||||
/**
|
||||
* special verbs no subresources.
|
||||
*/
|
||||
final Set<String> specialVerbs;
|
||||
|
||||
public RequestInfoFactory(Set<String> apiPrefixes, Set<String> grouplessApiPrefixes) {
|
||||
|
@ -39,39 +44,35 @@ public class RequestInfoFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* newRequestInfo returns the information from the http request. If error is not occurred,
|
||||
* <p>newRequestInfo returns the information from the http request. If error is not occurred,
|
||||
* RequestInfo holds the information as best it is known before the failure
|
||||
* It handles both resource and non-resource requests and fills in all the pertinent
|
||||
* information
|
||||
* for each.
|
||||
* <p>
|
||||
* information.</p>
|
||||
* <p>for each.</p>
|
||||
* Valid Inputs:
|
||||
* <p>
|
||||
* Resource paths
|
||||
* <p>Resource paths</p>
|
||||
* <pre>
|
||||
* /apis/{api-group}/{version}/namespaces
|
||||
* /api/{version}/namespaces
|
||||
* /api/{version}/namespaces/{namespace}
|
||||
* /api/{version}/namespaces/{namespace}/{resource}
|
||||
* /api/{version}/namespaces/{namespace}/{resource}/{resourceName}
|
||||
* /api/{version}/plugins
|
||||
* /api/{version}/plugins/{pluginName}
|
||||
* /api/{version}/plugins/{pluginName}/{resource}
|
||||
* /api/{version}/plugins/{pluginName}/{resource}/{resourceName}
|
||||
* /api/{version}/{resource}
|
||||
* /api/{version}/{resource}/{resourceName}
|
||||
* </pre>
|
||||
*
|
||||
* <p>Special verbs without subresources:</p>
|
||||
* <pre>
|
||||
* Special verbs without subresources:
|
||||
* /api/{version}/proxy/{resource}/{resourceName}
|
||||
* /api/{version}/proxy/namespaces/{namespace}/{resource}/{resourceName}
|
||||
* </pre>
|
||||
*
|
||||
* <p>Special verbs with subresources:</p>
|
||||
* <pre>
|
||||
* Special verbs with subresources:
|
||||
* /api/{version}/watch/{resource}
|
||||
* /api/{version}/watch/namespaces/{namespace}/{resource}
|
||||
* </pre>
|
||||
*
|
||||
* <p>NonResource paths:</p>
|
||||
* <pre>
|
||||
* NonResource paths
|
||||
* /apis/{api-group}/{version}
|
||||
* /apis/{api-group}
|
||||
* /apis
|
||||
|
@ -137,29 +138,29 @@ public class RequestInfoFactory {
|
|||
default -> "";
|
||||
};
|
||||
}
|
||||
Set<String> namespaceSubresources = Set.of("status", "finalize");
|
||||
// URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative
|
||||
// URL forms: /plugins/{plugin-name}/{kind}/*, where parts are adjusted to be relative
|
||||
// to kind
|
||||
if (Objects.equals(currentParts[0], "namespaces")) {
|
||||
if (Objects.equals(currentParts[0], "plugins")
|
||||
&& StringUtils.isEmpty(requestInfo.getApiGroup())) {
|
||||
if (currentParts.length > 1) {
|
||||
requestInfo.namespace = currentParts[1];
|
||||
requestInfo.pluginName = currentParts[1];
|
||||
|
||||
// if there is another step after the namespace name and it is not a known
|
||||
// namespace subresource
|
||||
// if there is another step after the plugin name and it is not a known
|
||||
// plugins subresource
|
||||
// move currentParts to include it as a resource in its own right
|
||||
if (currentParts.length > 2 && !namespaceSubresources.contains(currentParts[2])) {
|
||||
if (currentParts.length > 2) {
|
||||
currentParts = Arrays.copyOfRange(currentParts, 2, currentParts.length);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
requestInfo.namespace = "";
|
||||
requestInfo.pluginName = "";
|
||||
}
|
||||
|
||||
// parsing successful, so we now know the proper value for .Parts
|
||||
requestInfo.parts = currentParts;
|
||||
Set<String> specialVerbsNoSubresources = Set.of("proxy");
|
||||
// special verbs no subresources
|
||||
// parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret
|
||||
if (requestInfo.parts.length >= 3 && !specialVerbsNoSubresources.contains(
|
||||
if (requestInfo.parts.length >= 3 && !specialVerbs.contains(
|
||||
requestInfo.verb)) {
|
||||
requestInfo.subresource = requestInfo.parts[2];
|
||||
}
|
||||
|
@ -195,7 +196,7 @@ public class RequestInfoFactory {
|
|||
return "1".equals(requestParam) || "true".equals(requestParam);
|
||||
}
|
||||
|
||||
public String[] splitPath(String path) {
|
||||
private String[] splitPath(String path) {
|
||||
path = StringUtils.strip(path, "/");
|
||||
if (StringUtils.isEmpty(path)) {
|
||||
return new String[] {};
|
||||
|
|
|
@ -42,10 +42,11 @@ class ExtensionConfigurationTest {
|
|||
@BeforeEach
|
||||
void setUp() {
|
||||
// disable authorization
|
||||
var rule = new Role.PolicyRule();
|
||||
rule.setApiGroups(new String[] {"*"});
|
||||
rule.setResources(new String[] {"*"});
|
||||
rule.setVerbs(new String[] {"*"});
|
||||
var rule = new Role.PolicyRule.Builder()
|
||||
.apiGroups("*")
|
||||
.resources("*")
|
||||
.verbs("*")
|
||||
.build();
|
||||
var role = new Role();
|
||||
role.setRules(List.of(rule));
|
||||
when(roleService.getRole(anyString())).thenReturn(role);
|
||||
|
|
|
@ -0,0 +1,301 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.get;
|
||||
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.post;
|
||||
import static org.springframework.web.bind.annotation.RequestMethod.GET;
|
||||
import static org.springframework.web.bind.annotation.RequestMethod.HEAD;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.security.Principal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.result.method.RequestMappingInfo;
|
||||
import org.springframework.web.server.MethodNotAllowedException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
/**
|
||||
* Tests for {@link PluginRequestMappingHandlerMapping}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class PluginRequestMappingHandlerMappingTest {
|
||||
|
||||
private final StaticWebApplicationContext wac = new StaticWebApplicationContext();
|
||||
|
||||
private PluginRequestMappingHandlerMapping handlerMapping;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
handlerMapping = new PluginRequestMappingHandlerMapping();
|
||||
this.handlerMapping.setApplicationContext(wac);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAddPathPrefixWhenExistingApiVersion() throws Exception {
|
||||
Method method = UserController.class.getMethod("getUser");
|
||||
RequestMappingInfo info =
|
||||
this.handlerMapping.getPluginMappingForMethod("fakePlugin", method,
|
||||
UserController.class);
|
||||
|
||||
assertThat(info).isNotNull();
|
||||
assertThat(info.getPatternsCondition().getPatterns()).isEqualTo(
|
||||
Collections.singleton(
|
||||
new PathPatternParser().parse("/api/v1alpha1/plugins/fakePlugin/user/{id}")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailWhenMissingApiVersion() throws Exception {
|
||||
Method method = AppleMissingApiVersionController.class.getMethod("getName");
|
||||
assertThatThrownBy(() ->
|
||||
this.handlerMapping.getPluginMappingForMethod("fakePlugin", method,
|
||||
AppleMissingApiVersionController.class)).isInstanceOf(PluginRuntimeException.class)
|
||||
.hasMessage(
|
||||
"The handler [class run.halo.app.plugin"
|
||||
+ ".PluginRequestMappingHandlerMappingTest$AppleMissingApiVersionController] "
|
||||
+ "is missing @ApiVersion annotation.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerHandlerMethods() {
|
||||
assertThat(handlerMapping.getMappings("fakePlugin")).isEmpty();
|
||||
|
||||
UserController userController = mock(UserController.class);
|
||||
handlerMapping.registerHandlerMethods("fakePlugin", userController);
|
||||
|
||||
List<RequestMappingInfo> mappings = handlerMapping.getMappings("fakePlugin");
|
||||
assertThat(mappings).hasSize(1);
|
||||
assertThat(mappings.get(0).toString()).isEqualTo(
|
||||
"{GET /api/v1alpha1/plugins/fakePlugin/user/{id}}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unregister() {
|
||||
UserController userController = mock(UserController.class);
|
||||
// register handler methods first
|
||||
handlerMapping.registerHandlerMethods("fakePlugin", userController);
|
||||
assertThat(handlerMapping.getMappings("fakePlugin")).hasSize(1);
|
||||
|
||||
// unregister
|
||||
handlerMapping.unregister("fakePlugin");
|
||||
assertThat(handlerMapping.getMappings("fakePlugin")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHandlerDirectMatch() {
|
||||
// register handler methods first
|
||||
handlerMapping.registerHandlerMethods("fakePlugin", new TestController());
|
||||
|
||||
// resolve an expected method from TestController
|
||||
Method expected =
|
||||
ResolvableMethod.on(TestController.class).annot(getMapping("/foo")).build();
|
||||
|
||||
// get handler by mock exchange
|
||||
ServerWebExchange exchange =
|
||||
MockServerWebExchange.from(get("/api/v1alpha1/plugins/fakePlugin/foo"));
|
||||
HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
|
||||
|
||||
assertThat(hm).isNotNull();
|
||||
assertThat(hm.getMethod()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHandlerBestMatch() {
|
||||
// register handler methods first
|
||||
handlerMapping.registerHandlerMethods("fakePlugin", new TestController());
|
||||
|
||||
Method expected =
|
||||
ResolvableMethod.on(TestController.class).annot(getMapping("/foo").params("p")).build();
|
||||
|
||||
String requestPath = "/api/v1alpha1/plugins/fakePlugin/foo?p=anything";
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(get(requestPath));
|
||||
HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
|
||||
|
||||
assertThat(hm).isNotNull();
|
||||
assertThat(hm.getMethod()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHandlerRootPathMatch() {
|
||||
// register handler methods first
|
||||
handlerMapping.registerHandlerMethods("fakePlugin", new TestController());
|
||||
Method expected =
|
||||
ResolvableMethod.on(TestController.class).annot(getMapping("")).build();
|
||||
|
||||
String requestPath = "/api/v1alpha1/plugins/fakePlugin";
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(get(requestPath));
|
||||
HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
|
||||
|
||||
assertThat(hm).isNotNull();
|
||||
assertThat(hm.getMethod()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHandlerRequestMethodNotAllowed() {
|
||||
// register handler methods first
|
||||
handlerMapping.registerHandlerMethods("fakePlugin", new TestController());
|
||||
|
||||
String requestPath = "/api/v1alpha1/plugins/fakePlugin/bar";
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(post(requestPath));
|
||||
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
|
||||
|
||||
assertError(mono, MethodNotAllowedException.class,
|
||||
ex -> assertThat(ex.getSupportedMethods()).isEqualTo(
|
||||
Set.of(HttpMethod.GET, HttpMethod.HEAD)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> void assertError(Mono<Object> mono, final Class<T> exceptionClass,
|
||||
final Consumer<T> consumer) {
|
||||
StepVerifier.create(mono)
|
||||
.consumeErrorWith(error -> {
|
||||
assertThat(error.getClass()).isEqualTo(exceptionClass);
|
||||
consumer.accept((T) error);
|
||||
})
|
||||
.verify();
|
||||
}
|
||||
|
||||
private RequestMappingPredicate getMapping(String... path) {
|
||||
return new RequestMappingPredicate(path).method(GET).params();
|
||||
}
|
||||
|
||||
public static class ResolvableMethod {
|
||||
private final Class<?> objectClass;
|
||||
private final List<Predicate<Method>> filters = new ArrayList<>(4);
|
||||
|
||||
public ResolvableMethod(Class<?> objectClass) {
|
||||
this.objectClass = objectClass;
|
||||
}
|
||||
|
||||
public static ResolvableMethod on(Class<?> objectClass) {
|
||||
return new ResolvableMethod(objectClass);
|
||||
}
|
||||
|
||||
public ResolvableMethod annot(Predicate<Method> predicate) {
|
||||
filters.add(predicate);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Method build() {
|
||||
Set<Method> methods = MethodIntrospector.selectMethods(this.objectClass, this::isMatch);
|
||||
Assert.state(!methods.isEmpty(), () -> "No matching method: " + this);
|
||||
Assert.state(methods.size() == 1,
|
||||
() -> "Multiple matching methods: " + this + formatMethods(methods));
|
||||
return methods.iterator().next();
|
||||
}
|
||||
|
||||
private String formatMethods(Set<Method> methods) {
|
||||
return "\nMatched:\n" + methods.stream()
|
||||
.map(Method::toGenericString).collect(Collectors.joining(",\n\t", "[\n\t", "\n]"));
|
||||
}
|
||||
|
||||
private boolean isMatch(Method method) {
|
||||
return this.filters.stream().allMatch(p -> p.test(method));
|
||||
}
|
||||
}
|
||||
|
||||
public static class RequestMappingPredicate implements Predicate<Method> {
|
||||
|
||||
private final String[] path;
|
||||
|
||||
private RequestMethod[] method = {};
|
||||
|
||||
private String[] params;
|
||||
|
||||
|
||||
private RequestMappingPredicate(String... path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
|
||||
public RequestMappingPredicate method(RequestMethod... methods) {
|
||||
this.method = methods;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestMappingPredicate params(String... params) {
|
||||
this.params = params;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(Method method) {
|
||||
RequestMapping annot =
|
||||
AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
|
||||
return annot != null
|
||||
&& Arrays.equals(this.path, annot.path())
|
||||
&& Arrays.equals(this.method, annot.method())
|
||||
&& (this.params == null || Arrays.equals(this.params, annot.params()));
|
||||
}
|
||||
}
|
||||
|
||||
@ApiVersion("v1alpha1")
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
static class UserController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Principal getUser() {
|
||||
return mock(Principal.class);
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/apples")
|
||||
static class AppleMissingApiVersionController {
|
||||
|
||||
@GetMapping
|
||||
public String getName() {
|
||||
return mock(String.class);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiVersion("v1alpha1")
|
||||
@Controller
|
||||
@RequestMapping
|
||||
static class TestController {
|
||||
@GetMapping("/foo")
|
||||
public void foo() {
|
||||
}
|
||||
|
||||
@GetMapping(path = "/foo", params = "p")
|
||||
public void fooParam() {
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/ba*", method = {GET, HEAD})
|
||||
public void bar() {
|
||||
}
|
||||
|
||||
@GetMapping("")
|
||||
public void empty() {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,6 @@ import run.halo.app.core.extension.service.DefaultRoleBindingService;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
// @ExtendWith(SpringExtension.class)
|
||||
public class DefaultRoleBindingServiceTest {
|
||||
|
||||
private DefaultRoleBindingService roleBindingLister;
|
||||
|
|
|
@ -5,8 +5,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
|
@ -25,21 +27,38 @@ class PolicyRuleTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void constructPolicyRule() throws JsonProcessingException {
|
||||
Role.PolicyRule policyRule = new Role.PolicyRule(null, null, null, null, null);
|
||||
public void constructPolicyRule() throws JsonProcessingException, JSONException {
|
||||
Role.PolicyRule policyRule = new Role.PolicyRule(null, null, null, null, null, null);
|
||||
assertThat(policyRule).isNotNull();
|
||||
JsonNode policyRuleJson = objectMapper.valueToTree(policyRule);
|
||||
assertThat(policyRuleJson).isEqualTo(objectMapper.readTree("""
|
||||
{"apiGroups":[],"resources":[],"resourceNames":[],"nonResourceURLs":[],"verbs":[]}
|
||||
"""));
|
||||
JSONAssert.assertEquals("""
|
||||
{
|
||||
"pluginName": "",
|
||||
"apiGroups": [],
|
||||
"resources": [],
|
||||
"resourceNames": [],
|
||||
"nonResourceURLs": [],
|
||||
"verbs": []
|
||||
}
|
||||
""",
|
||||
JsonUtils.objectToJson(policyRule),
|
||||
true);
|
||||
|
||||
Role.PolicyRule policyByBuilder = new Role.PolicyRule.Builder().build();
|
||||
JsonNode policyByBuilderJson = objectMapper.valueToTree(policyByBuilder);
|
||||
assertThat(policyByBuilderJson).isEqualTo(objectMapper.readTree("""
|
||||
{"apiGroups":[],"resources":[],"resourceNames":[],"nonResourceURLs":[],"verbs":[]}
|
||||
"""));
|
||||
JSONAssert.assertEquals("""
|
||||
{
|
||||
"pluginName": "",
|
||||
"apiGroups": [],
|
||||
"resources": [],
|
||||
"resourceNames": [],
|
||||
"nonResourceURLs": [],
|
||||
"verbs": []
|
||||
}
|
||||
""",
|
||||
JsonUtils.objectToJson(policyByBuilder),
|
||||
true);
|
||||
|
||||
Role.PolicyRule policyNonNull = new Role.PolicyRule.Builder()
|
||||
.pluginName("fakePluginName")
|
||||
.apiGroups("group")
|
||||
.resources("resource-1", "resource-2")
|
||||
.resourceNames("resourceName")
|
||||
|
@ -49,6 +68,7 @@ class PolicyRuleTest {
|
|||
|
||||
JsonNode expected = objectMapper.readTree("""
|
||||
{
|
||||
"pluginName": "fakePluginName",
|
||||
"apiGroups": [
|
||||
"group"
|
||||
],
|
||||
|
|
|
@ -85,6 +85,67 @@ public class RequestInfoResolverTest {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void pluginsScopedAndPluginManage() {
|
||||
List<SuccessCase> testCases =
|
||||
List.of(new SuccessCase("DELETE", "/apis/extensions/v1/plugins/other/posts",
|
||||
"delete", "apis", "extensions", "v1", "", "plugins", "posts", "other",
|
||||
new String[] {"plugins", "other", "posts"}),
|
||||
|
||||
// api group identification
|
||||
new SuccessCase("POST", "/apis/extensions/v1/plugins/other/posts", "create", "apis",
|
||||
"extensions", "v1", "", "plugins", "posts", "other",
|
||||
new String[] {"plugins", "other", "posts"}),
|
||||
|
||||
// api version identification
|
||||
new SuccessCase("POST", "/apis/extensions/v1beta3/plugins/other/posts", "create",
|
||||
"apis", "extensions", "v1beta3", "", "plugins", "posts", "other",
|
||||
new String[] {"plugins", "other", "posts"}));
|
||||
|
||||
// 以 /apis 开头的 plugins 资源为 core 中管理插件使用的资源
|
||||
for (SuccessCase successCase : testCases) {
|
||||
var request =
|
||||
method(HttpMethod.valueOf(successCase.method),
|
||||
successCase.url).build();
|
||||
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
|
||||
assertThat(requestInfo).isNotNull();
|
||||
assertRequestInfoCase(successCase, requestInfo);
|
||||
}
|
||||
|
||||
List<SuccessCase> pluginScopedCases =
|
||||
List.of(new SuccessCase("DELETE", "/api/v1/plugins/other/posts",
|
||||
"deletecollection", "api", "", "v1", "other", "posts", "", "",
|
||||
new String[] {"posts"}),
|
||||
|
||||
// api group identification
|
||||
new SuccessCase("POST", "/api/v1/plugins/other/posts", "create", "api",
|
||||
"", "v1", "other", "posts", "", "", new String[] {"posts"}),
|
||||
|
||||
// api version identification
|
||||
new SuccessCase("POST", "/api/v1beta3/plugins/other/posts", "create",
|
||||
"api", "", "v1beta3", "other", "posts", "", "",
|
||||
new String[] {"posts"}));
|
||||
|
||||
for (SuccessCase pluginScopedCase : pluginScopedCases) {
|
||||
var request =
|
||||
method(HttpMethod.valueOf(pluginScopedCase.method),
|
||||
pluginScopedCase.url).build();
|
||||
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
|
||||
assertThat(requestInfo).isNotNull();
|
||||
assertRequestInfoCase(pluginScopedCase, requestInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertRequestInfoCase(SuccessCase pluginScopedCase, RequestInfo requestInfo) {
|
||||
assertThat(requestInfo.getPluginName()).isEqualTo(pluginScopedCase.expectedPluginName);
|
||||
assertThat(requestInfo.getVerb()).isEqualTo(pluginScopedCase.expectedVerb);
|
||||
assertThat(requestInfo.getParts()).isEqualTo(pluginScopedCase.expectedParts);
|
||||
assertThat(requestInfo.getApiGroup()).isEqualTo(pluginScopedCase.expectedAPIGroup);
|
||||
assertThat(requestInfo.getResource()).isEqualTo(pluginScopedCase.expectedResource);
|
||||
assertThat(requestInfo.getSubresource())
|
||||
.isEqualTo(pluginScopedCase.expectedSubresource());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void errorCaseTest() {
|
||||
List<ErrorCases> errorCases = List.of(new ErrorCases("no resource path", "/"),
|
||||
|
@ -113,6 +174,10 @@ public class RequestInfoResolverTest {
|
|||
List<PolicyRule> rules = List.of(
|
||||
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get")
|
||||
.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().nonResourceURLs("/healthy").verbs("get", "post", "head")
|
||||
.build());
|
||||
|
@ -165,6 +230,11 @@ public class RequestInfoResolverTest {
|
|||
new RequestResolveCase("/api/v1/posts", "DELETE", 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
|
||||
new RequestResolveCase("/apis/group/v1/posts", "GET", false),
|
||||
|
||||
|
@ -185,7 +255,7 @@ public class RequestInfoResolverTest {
|
|||
|
||||
public record SuccessCase(String method, String url, String expectedVerb,
|
||||
String expectedAPIPrefix, String expectedAPIGroup,
|
||||
String expectedAPIVersion, String expectedNamespace,
|
||||
String expectedAPIVersion, String expectedPluginName,
|
||||
String expectedResource, String expectedSubresource,
|
||||
String expectedName, String[] expectedParts) {
|
||||
}
|
||||
|
@ -194,31 +264,31 @@ public class RequestInfoResolverTest {
|
|||
List<SuccessCase> getTestRequestInfos() {
|
||||
String namespaceAll = "*";
|
||||
return List.of(
|
||||
new SuccessCase("GET", "/api/v1/namespaces", "list", "api", "", "v1", "", "namespaces",
|
||||
"", "", new String[] {"namespaces"}),
|
||||
new SuccessCase("GET", "/api/v1/namespaces/other", "get", "api", "", "v1", "other",
|
||||
"namespaces", "", "other", new String[] {"namespaces", "other"}),
|
||||
new SuccessCase("GET", "/api/v1/plugins", "list", "api", "", "v1", "", "plugins",
|
||||
"", "", new String[] {"plugins"}),
|
||||
new SuccessCase("GET", "/api/v1/plugins/other", "get", "api", "", "v1", "other",
|
||||
"plugins", "", "other", new String[] {"plugins", "other"}),
|
||||
|
||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts", "list", "api", "", "v1",
|
||||
new SuccessCase("GET", "/api/v1/plugins/other/posts", "list", "api", "", "v1",
|
||||
"other", "posts", "", "", new String[] {"posts"}),
|
||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo", "get", "api", "", "v1",
|
||||
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo", "get", "api", "", "v1",
|
||||
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||
new SuccessCase("HEAD", "/api/v1/namespaces/other/posts/foo", "get", "api", "", "v1",
|
||||
new SuccessCase("HEAD", "/api/v1/plugins/other/posts/foo", "get", "api", "", "v1",
|
||||
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||
new SuccessCase("GET", "/api/v1/posts", "list", "api", "", "v1", namespaceAll, "posts",
|
||||
"", "", new String[] {"posts"}),
|
||||
new SuccessCase("HEAD", "/api/v1/posts", "list", "api", "", "v1", namespaceAll, "posts",
|
||||
"", "", new String[] {"posts"}),
|
||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo", "get", "api", "", "v1",
|
||||
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo", "get", "api", "", "v1",
|
||||
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts", "list", "api", "", "v1",
|
||||
new SuccessCase("GET", "/api/v1/plugins/other/posts", "list", "api", "", "v1",
|
||||
"other", "posts", "", "", new String[] {"posts"}),
|
||||
|
||||
// special verbs
|
||||
new SuccessCase("GET", "/api/v1/proxy/namespaces/other/posts/foo", "proxy", "api", "",
|
||||
new SuccessCase("GET", "/api/v1/proxy/plugins/other/posts/foo", "proxy", "api", "",
|
||||
"v1", "other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||
new SuccessCase("GET",
|
||||
"/api/v1/proxy/namespaces/other/posts/foo/subpath/not/a/subresource", "proxy",
|
||||
"/api/v1/proxy/plugins/other/posts/foo/subpath/not/a/subresource", "proxy",
|
||||
"api", "", "v1", "other", "posts", "", "foo",
|
||||
new String[] {"posts", "foo", "subpath", "not", "a", "subresource"}),
|
||||
new SuccessCase("GET", "/api/v1/watch/posts", "watch", "api", "", "v1", namespaceAll,
|
||||
|
@ -227,52 +297,35 @@ public class RequestInfoResolverTest {
|
|||
namespaceAll, "posts", "", "", new String[] {"posts"}),
|
||||
new SuccessCase("GET", "/api/v1/posts?watch=false", "list", "api", "", "v1",
|
||||
namespaceAll, "posts", "", "", new String[] {"posts"}),
|
||||
new SuccessCase("GET", "/api/v1/watch/namespaces/other/posts", "watch", "api", "", "v1",
|
||||
new SuccessCase("GET", "/api/v1/watch/plugins/other/posts", "watch", "api", "", "v1",
|
||||
"other", "posts", "", "", new String[] {"posts"}),
|
||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts?watch=1", "watch", "api", "",
|
||||
new SuccessCase("GET", "/api/v1/plugins/other/posts?watch=1", "watch", "api", "",
|
||||
"v1", "other", "posts", "", "", new String[] {"posts"}),
|
||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts?watch=0", "list", "api", "",
|
||||
new SuccessCase("GET", "/api/v1/plugins/other/posts?watch=0", "list", "api", "",
|
||||
"v1", "other", "posts", "", "", new String[] {"posts"}),
|
||||
|
||||
// subresource identification
|
||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo/status", "get", "api", "",
|
||||
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo/status", "get", "api", "",
|
||||
"v1", "other", "posts", "status", "foo", new String[] {"posts", "foo", "status"}),
|
||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo/proxy/subpath", "get", "api",
|
||||
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo/proxy/subpath", "get", "api",
|
||||
"", "v1", "other", "posts", "proxy", "foo",
|
||||
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
|
||||
new SuccessCase("PATCH", "/api/v1/namespaces/other/posts/foo", "patch", "api", "", "v1",
|
||||
new SuccessCase("PATCH", "/api/v1/plugins/other/posts/foo", "patch", "api", "", "v1",
|
||||
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||
new SuccessCase("DELETE", "/api/v1/namespaces/other/posts/foo", "delete", "api", "",
|
||||
new SuccessCase("DELETE", "/api/v1/plugins/other/posts/foo", "delete", "api", "",
|
||||
"v1", "other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||
new SuccessCase("POST", "/api/v1/namespaces/other/posts", "create", "api", "", "v1",
|
||||
new SuccessCase("POST", "/api/v1/plugins/other/posts", "create", "api", "", "v1",
|
||||
"other", "posts", "", "", new String[] {"posts"}),
|
||||
|
||||
// deletecollection verb identification
|
||||
new SuccessCase("DELETE", "/api/v1/nodes", "deletecollection", "api", "", "v1", "",
|
||||
"nodes", "", "", new String[] {"nodes"}),
|
||||
new SuccessCase("DELETE", "/api/v1/namespaces", "deletecollection", "api", "", "v1", "",
|
||||
"namespaces", "", "", new String[] {"namespaces"}),
|
||||
new SuccessCase("DELETE", "/api/v1/namespaces/other/posts", "deletecollection", "api",
|
||||
"", "v1", "other", "posts", "", "", new String[] {"posts"}),
|
||||
new SuccessCase("DELETE", "/apis/extensions/v1/namespaces/other/posts",
|
||||
"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"}));
|
||||
new SuccessCase("DELETE", "/api/v1/plugins", "deletecollection", "api", "", "v1", "",
|
||||
"plugins", "", "", new String[] {"plugins"}),
|
||||
new SuccessCase("DELETE", "/api/v1/plugins/other/posts", "deletecollection", "api",
|
||||
"", "v1", "other", "posts", "", "", new String[] {"posts"}));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue