Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit da0c2aa

Browse files
new: plugin endpoint security basics
1 parent 488e234 commit da0c2aa

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

Lines changed: 24 additions & 0 deletions
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();
Lines changed: 16 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /