Skip to content

Commit b58a742

Browse files
committed
new: plugin endpoint security basics
1 parent 20f5bc8 commit b58a742

File tree

9 files changed

+105
-122
lines changed

9 files changed

+105
-122
lines changed

server/api-service/PLUGIN.md

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Lowcoder plugin system (WIP)
1+
# Lowcoder backend plugin system
22

33
This is an ongoing effort to refactor current plugin system based on pf4j library.
44

@@ -50,73 +50,14 @@ Plugin jar can be structured in any way you like. It can be a plain java project
5050
5151
It is composed from several parts:
5252
- class(es) implementing **LowcoderPlugin** interface
53-
- class(es) implementing **LowcoderEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format:
53+
- class(es) implementing **PluginEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format:
5454
5555
```java
5656
@EndpointExtension(uri = <endpoint uri>, method = <HTTP method>)
57-
public Mono<ServerResponse> <handler name>(ServerRequest request)
57+
public EndpointResponse <handler name>(EndpointRequest request)
5858
{
5959
... your endpoint logic implementation
6060
}
61-
62-
for example:
63-
64-
@EndpointExtension(uri = "/hello-world", method = Method.GET)
65-
public Mono<ServerResponse> helloWorld(ServerRequest request)
66-
{
67-
return ServerResponse.ok().body(Mono.just(Hello.builder().message("Hello world!").build()), Hello.class);
68-
}
6961
```
7062
- TODO: class(es) impelemting **LowcoderDatasource** interface
7163

72-
### LowcoderPlugin implementations
73-
74-
Methods of interest:
75-
- **pluginId()** - unique plugin ID - if a plugin with such ID is already loaded, subsequent plugins whith this ID will be ignored
76-
- **description()** - short plugin description
77-
- **load(ApplicationContext parentContext)** - is called during plugin startup - this is the place where you should completely initialize your plugin. If initialization fails, return false
78-
- **unload()** - is called during lowcoder API server shutdown - this is the place where you should release all resources
79-
- **endpoints()** - needs to contain all initialized **PluginEndpoints** you want to expose, for example:
80-
81-
```java
82-
@Override
83-
public List<PluginEndpoint> endpoints()
84-
{
85-
List<PluginEndpoint> endpoints = new ArrayList<>();
86-
87-
endpoints.add(new HelloWorldEndpoint());
88-
89-
return endpoints;
90-
}
91-
```
92-
- **pluginInfo()** - should return a record object with additional information about your plugin. It is serialized to JSON as part of the **/plugins** listing (see **"info"** object in this example):
93-
94-
```json
95-
[
96-
{
97-
"id": "example-plugin",
98-
"description": "Example plugin for lowcoder platform",
99-
"info": {}
100-
},
101-
{
102-
"id": "enterprise",
103-
"description": "Lowcoder enterprise plugin",
104-
"info": {
105-
"enabledFeatures": [
106-
"endpointApiUsage"
107-
]
108-
}
109-
}
110-
]
111-
```
112-
113-
## TODOs
114-
115-
1. Implement endpoint security - currently all plugin endpoints are public (probably by adding **security** attribute to **@EndpointExtension** and enforcing it)
116-
117-
118-
## QUESTIONS / CONSIDERATIONS
119-
120-
1. currently the plugin endpoints are prefixed with **/plugin/{pluginId}/** - this is hardcoded, do we want to make it configurable?
121-
122-

server/api-service/lowcoder-sdk/pom.xml

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,6 @@
1313

1414
<name>lowcoder-sdk</name>
1515

16-
<properties>
17-
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
18-
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
19-
20-
<java.version>17</java.version>
21-
</properties>
22-
2316
<dependencies>
2417
<dependency>
2518
<groupId>org.springframework.boot</groupId>
@@ -173,7 +166,17 @@
173166
<artifactId>validation-api</artifactId>
174167
</dependency>
175168
</dependencies>
176-
169+
170+
<properties>
171+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
172+
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
173+
174+
<java.version>17</java.version>
175+
176+
<maven.compiler.source>17</maven.compiler.source>
177+
<maven.compiler.target>17</maven.compiler.target>
178+
</properties>
179+
177180
<dependencyManagement>
178181
<dependencies>
179182
<dependency>

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.lowcoder.domain.application.model.ApplicationStatus;
2828
import org.lowcoder.domain.application.model.ApplicationType;
2929
import org.lowcoder.domain.permission.model.ResourceRole;
30-
import org.lowcoder.infra.event.EventType;
3130
import org.springframework.web.bind.annotation.PathVariable;
3231
import org.springframework.web.bind.annotation.RequestBody;
3332
import org.springframework.web.bind.annotation.RequestParam;
@@ -106,15 +105,15 @@ public Mono<ResponseView<ApplicationView>> getPublishedApplication(@PathVariable
106105
public Mono<ResponseView<ApplicationView>> getPublishedMarketPlaceApplication(@PathVariable String applicationId) {
107106
return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_MARKETPLACE)
108107
.delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId))
109-
.delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW))
108+
.delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW))
110109
.map(ResponseView::success);
111110
}
112111

113112
@Override
114113
public Mono<ResponseView<ApplicationView>> getAgencyProfileApplication(@PathVariable String applicationId) {
115114
return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.AGENCY_PROFILE)
116115
.delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId))
117-
.delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW))
116+
.delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW))
118117
.map(ResponseView::success);
119118
}
120119

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.lowcoder.api.framework.plugin;
22

33
import java.io.IOException;
4-
import java.io.InputStream;
54
import java.net.MalformedURLException;
65
import java.net.URL;
76
import java.net.URLClassLoader;
@@ -20,6 +19,11 @@ public class PluginClassLoader extends URLClassLoader
2019
private static final ClassLoader baseClassLoader = ClassLoader.getPlatformClassLoader();
2120
private final ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader();
2221

22+
private static final String[] excludedPaths = new String[] {
23+
"org.lowcoder.plugin.api.",
24+
"org/lowcoder/plugin/api/"
25+
};
26+
2327
public PluginClassLoader(String name, Path pluginPath)
2428
{
2529
super(name, pathToURLs(pluginPath), baseClassLoader);
@@ -34,7 +38,7 @@ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundE
3438
return clazz;
3539
}
3640

37-
if (name.startsWith("org.lowcoder.plugin.api."))
41+
if (StringUtils.startsWithAny(name, excludedPaths))
3842
{
3943
try
4044
{
@@ -67,7 +71,7 @@ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundE
6771
@Override
6872
public URL getResource(String name) {
6973
Objects.requireNonNull(name);
70-
if (StringUtils.startsWithAny(name, "org/lowcoder/plugin/api/", "org.lowcoder.plugin.api."))
74+
if (StringUtils.startsWithAny(name, excludedPaths))
7175
{
7276
return appClassLoader.getResource(name);
7377
}
@@ -79,7 +83,7 @@ public URL getResource(String name) {
7983
public Enumeration<URL> getResources(String name) throws IOException
8084
{
8185
Objects.requireNonNull(name);
82-
if (StringUtils.startsWithAny(name, "org/lowcoder/plugin/api/", "org.lowcoder.plugin.api."))
86+
if (StringUtils.startsWithAny(name, excludedPaths))
8387
{
8488
return appClassLoader.getResources(name);
8589
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,23 @@
1616
import org.apache.commons.collections4.CollectionUtils;
1717
import org.apache.commons.lang3.StringUtils;
1818
import org.lowcoder.api.framework.plugin.data.PluginServerRequest;
19+
import org.lowcoder.api.framework.plugin.security.SecuredEndpoint;
1920
import org.lowcoder.plugin.api.EndpointExtension;
2021
import org.lowcoder.plugin.api.PluginEndpoint;
2122
import org.lowcoder.plugin.api.data.EndpointRequest;
2223
import org.lowcoder.plugin.api.data.EndpointResponse;
2324
import org.lowcoder.sdk.exception.BaseException;
25+
import org.springframework.aop.TargetSource;
26+
import org.springframework.aop.framework.ProxyFactoryBean;
27+
import org.springframework.aop.target.SimpleBeanTargetSource;
2428
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
2529
import org.springframework.context.ApplicationContext;
2630
import org.springframework.context.support.GenericApplicationContext;
2731
import org.springframework.core.ResolvableType;
2832
import org.springframework.http.ResponseCookie;
2933
import org.springframework.security.access.prepost.PreAuthorize;
34+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
3035
import org.springframework.stereotype.Component;
31-
import org.springframework.web.reactive.function.server.HandlerFunction;
3236
import org.springframework.web.reactive.function.server.RequestPredicate;
3337
import org.springframework.web.reactive.function.server.RouterFunction;
3438
import org.springframework.web.reactive.function.server.ServerRequest;
@@ -80,48 +84,47 @@ public List<RouterFunction<ServerResponse>> registeredEndpoints()
8084

8185
private void registerEndpointHandler(String urlPrefix, PluginEndpoint endpoint, Method handler)
8286
{
83-
if (handler.isAnnotationPresent(EndpointExtension.class))
87+
if (!handler.isAnnotationPresent(EndpointExtension.class) || !checkHandlerMethod(handler))
8488
{
85-
if (checkHandlerMethod(handler))
89+
if (handler.isAnnotationPresent(EndpointExtension.class))
8690
{
87-
88-
EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class);
89-
String endpointName = endpoint.getClass().getSimpleName() + "_" + handler.getName();
90-
91-
RouterFunction<ServerResponse> routerFunction = route(createRequestPredicate(urlPrefix, endpointMeta), req ->
92-
{
93-
Mono<ServerResponse> result = null;
94-
try
95-
{
96-
EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(req));
97-
result = createServerResponse(response);
98-
}
99-
catch (IllegalAccessException | InvocationTargetException cause)
100-
{
101-
throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !");
102-
}
103-
return result;
104-
});
105-
routes.add(routerFunction);
106-
registerRouterFunctionMapping(endpointName, routerFunction);
107-
108-
log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri());
109-
}
110-
else
111-
{
112-
log.error("Cannot register plugin endpoint: {} -> {}! Handler method must be defined as: public Mono<ServerResponse> {}(ServerRequest request)", endpoint.getClass().getSimpleName(), handler.getName(), handler.getName());
91+
log.debug("Not registering plugin endpoint method: {} -> {}! Handler method must be defined as: public EndpointResponse methodName(EndpointRequest request)", endpoint.getClass().getSimpleName(), handler.getName(), handler.getName());
11392
}
93+
return;
11494
}
95+
96+
EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class);
97+
String endpointName = endpoint.getClass().getSimpleName() + "_" + handler.getName();
98+
RouterFunction<ServerResponse> routerFunction = route(createRequestPredicate(urlPrefix, endpointMeta), req -> runPluginEndpointMethod(endpoint, endpointMeta, handler, req));
99+
routes.add(routerFunction);
100+
registerRouterFunctionMapping(endpointName, routerFunction);
101+
102+
log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri());
115103
}
116104

105+
@SecuredEndpoint
106+
public Mono<ServerResponse> runPluginEndpointMethod(PluginEndpoint endpoint, EndpointExtension endpointMeta, Method handler, ServerRequest request)
107+
{
108+
Mono<ServerResponse> result = null;
109+
try
110+
{
111+
log.info("Running plugin endpoint method {}\nRequest: {}", handler.getName(), request);
112+
113+
EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(request));
114+
result = createServerResponse(response);
115+
}
116+
catch (IllegalAccessException | InvocationTargetException cause)
117+
{
118+
throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !");
119+
}
120+
return result;
121+
}
122+
123+
117124
private void registerRouterFunctionMapping(String endpointName, RouterFunction<ServerResponse> routerFunction)
118125
{
119126
String beanName = "pluginEndpoint_" + endpointName + "_" + System.currentTimeMillis();
120-
121-
((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> {
122-
return routerFunction;
123-
});
124-
127+
((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> routerFunction );
125128
log.debug("Registering RouterFunction bean definition: {}", beanName);
126129
}
127130

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.lowcoder.api.framework.plugin.security;
2+
3+
import java.util.function.Supplier;
4+
5+
import org.aopalliance.intercept.MethodInvocation;
6+
import org.springframework.security.authorization.AuthorizationDecision;
7+
import org.springframework.security.authorization.AuthorizationManager;
8+
import org.springframework.security.core.Authentication;
9+
10+
import lombok.extern.slf4j.Slf4j;
11+
12+
@Slf4j
13+
public class EndpointAuthorizationManager implements AuthorizationManager<MethodInvocation>
14+
{
15+
16+
@Override
17+
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation)
18+
{
19+
log.info("Checking plugin endpoint invocation security for {}", invocation.getMethod().getName());
20+
21+
return new AuthorizationDecision(true);
22+
}
23+
24+
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import org.aopalliance.intercept.MethodInvocation;
66
import org.apache.commons.lang3.StringUtils;
77
import org.lowcoder.plugin.api.EndpointExtension;
8-
import org.springframework.core.annotation.AnnotationUtils;
98
import org.springframework.expression.EvaluationContext;
109
import org.springframework.expression.EvaluationException;
1110
import org.springframework.expression.Expression;
@@ -21,7 +20,7 @@
2120
import reactor.core.publisher.Mono;
2221

2322
@Slf4j
24-
@Component
23+
//@Component
2524
public class PluginAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation>
2625
{
2726
private final MethodSecurityExpressionHandler expressionHandler;
@@ -34,10 +33,9 @@ public PluginAuthorizationManager()
3433
@Override
3534
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, MethodInvocation invocation)
3635
{
37-
log.info(" invocation :: {}", invocation.getMethod());
36+
log.info("Checking plugin reactive endpoint invocation security for {}", invocation.getMethod().getName());
3837

39-
Method method = invocation.getMethod();
40-
EndpointExtension endpointExtension = AnnotationUtils.findAnnotation(method, EndpointExtension.class);
38+
EndpointExtension endpointExtension = (EndpointExtension)invocation.getArguments()[1];
4139
if (endpointExtension == null || StringUtils.isBlank(endpointExtension.authorize()))
4240
{
4341
return Mono.empty();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.lowcoder.api.framework.plugin.security;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Inherited;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
@Target({ ElementType.METHOD, ElementType.TYPE })
11+
@Retention(RetentionPolicy.RUNTIME)
12+
@Inherited
13+
@Documented
14+
public @interface SecuredEndpoint {
15+
16+
}

server/api-service/pom.xml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,12 @@
1212

1313

1414
<properties>
15-
<revision>2.3.0-SNAPSHOT</revision>
15+
<revision>2.4.0</revision>
1616
<java.version>17</java.version>
1717
<javadoc.disabled>true</javadoc.disabled>
1818
<deploy.disabled>true</deploy.disabled>
1919
<source.disabled>true</source.disabled>
20-
<project.groupId>org.lowcoder</project.groupId>
21-
<project.version>1.0-SNAPSHOT</project.version>
2220
<skipDockerBuild>true</skipDockerBuild>
23-
<log4j2.version>2.17.0</log4j2.version>
24-
<maven.compiler.source>17</maven.compiler.source>
25-
<maven.compiler.target>17</maven.compiler.target>
2621
</properties>
2722

2823
<repositories>

0 commit comments

Comments
 (0)