Skip to content

Lowcoder Plugin System #767

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 24, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -9,3 +9,4 @@ client/node_modules/
client/packages/lowcoder-plugin-demo/.yarn/install-state.gz
client/packages/lowcoder-plugin-demo/yarn.lock
client/packages/lowcoder-plugin-demo/.yarn/cache/@types-node-npm-16.18.68-56f72825c0-094ae9ed80.zip
application-dev.yml
2 changes: 1 addition & 1 deletion client/packages/lowcoder-comps/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lowcoder-comps",
"version": "0.0.26",
"version": "0.0.27",
"type": "module",
"license": "MIT",
"dependencies": {
Original file line number Diff line number Diff line change
@@ -120,7 +120,9 @@ ChartTmpComp = withViewFn(ChartTmpComp, (comp) => {

const handleOnMapScriptLoad = () => {
setMapScriptLoaded(true);
loadGoogleMapData();
setTimeout(() => {
loadGoogleMapData();
})
}

useEffect(() => {
Original file line number Diff line number Diff line change
@@ -52,6 +52,8 @@ import {

import { useContext } from "react";
import { EditorContext } from "comps/editorState";
import { migrateOldData } from "comps/generators/simpleGenerators";
import { fixOldInputCompData } from "../textInputComp/textInputConstants";

const getStyle = (style: InputLikeStyleType) => {
return css`
@@ -372,7 +374,7 @@ const CustomInputNumber = (props: RecordConstructorToView<typeof childrenMap>) =
);
};

const NumberInputTmpComp = (function () {
let NumberInputTmpComp = (function () {
return new UICompBuilder(childrenMap, (props) => {
return props.label({
required: props.required,
@@ -434,6 +436,8 @@ const NumberInputTmpComp = (function () {
.build();
})();

NumberInputTmpComp = migrateOldData(NumberInputTmpComp, fixOldInputCompData);

const NumberInputTmp2Comp = withMethodExposing(
NumberInputTmpComp,
refMethods([
Original file line number Diff line number Diff line change
@@ -22,6 +22,8 @@ import { ValueFromOption } from "lowcoder-design";
import { EllipsisTextCss } from "lowcoder-design";
import { trans } from "i18n";
import { RefControl } from "comps/controls/refControl";
import { migrateOldData } from "comps/generators/simpleGenerators";
import { fixOldInputCompData } from "../textInputComp/textInputConstants";

export const getStyle = (style: CheckboxStyleType) => {
return css`
@@ -126,7 +128,7 @@ const CheckboxGroup = styled(AntdCheckboxGroup) <{
}}
`;

const CheckboxBasicComp = (function () {
let CheckboxBasicComp = (function () {
const childrenMap = {
defaultValue: arrayStringExposingStateControl("defaultValue"),
value: arrayStringExposingStateControl("value"),
@@ -176,6 +178,8 @@ const CheckboxBasicComp = (function () {
.build();
})();

CheckboxBasicComp = migrateOldData(CheckboxBasicComp, fixOldInputCompData);

export const CheckboxComp = withExposingConfigs(CheckboxBasicComp, [
new NameConfig("value", trans("selectInput.valueDesc")),
SelectInputInvalidConfig,
Original file line number Diff line number Diff line change
@@ -14,9 +14,10 @@ import { SelectInputInvalidConfig, useSelectInputValidate } from "./selectInputC

import { PaddingControl } from "../../controls/paddingControl";
import { MarginControl } from "../../controls/marginControl";
import { useEffect, useRef } from "react";
import { migrateOldData } from "comps/generators/simpleGenerators";
import { fixOldInputCompData } from "../textInputComp/textInputConstants";

const MultiSelectBasicComp = (function () {
let MultiSelectBasicComp = (function () {
const childrenMap = {
...SelectChildrenMap,
defaultValue: arrayStringExposingStateControl("defaultValue", ["1", "2"]),
@@ -52,6 +53,8 @@ const MultiSelectBasicComp = (function () {
.build();
})();

MultiSelectBasicComp = migrateOldData(MultiSelectBasicComp, fixOldInputCompData);

export const MultiSelectComp = withExposingConfigs(MultiSelectBasicComp, [
new NameConfig("value", trans("selectInput.valueDesc")),
new NameConfig("inputValue", trans("select.inputValueDesc")),
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ import {
} from "./selectInputConstants";
import { EllipsisTextCss, ValueFromOption } from "lowcoder-design";
import { trans } from "i18n";
import { fixOldInputCompData } from "../textInputComp/textInputConstants";
import { migrateOldData } from "comps/generators/simpleGenerators";

const getStyle = (style: RadioStyleType) => {
return css`
@@ -93,7 +95,7 @@ const Radio = styled(AntdRadioGroup)<{
}}
`;

const RadioBasicComp = (function () {
let RadioBasicComp = (function () {
return new UICompBuilder(RadioChildrenMap, (props) => {
const [
validateState,
@@ -129,6 +131,8 @@ const RadioBasicComp = (function () {
.build();
})();

RadioBasicComp = migrateOldData(RadioBasicComp, fixOldInputCompData);

export const RadioComp = withExposingConfigs(RadioBasicComp, [
new NameConfig("value", trans("selectInput.valueDesc")),
SelectInputInvalidConfig,
Original file line number Diff line number Diff line change
@@ -25,6 +25,9 @@ import { RefControl } from "comps/controls/refControl";

import { useContext } from "react";
import { EditorContext } from "comps/editorState";
import { migrateOldData } from "comps/generators/simpleGenerators";
import { fixOldInputCompData } from "../textInputComp/textInputConstants";


const getStyle = (style: SegmentStyleType) => {
return css`
@@ -83,7 +86,7 @@ const SegmentChildrenMap = {
...formDataChildren,
};

const SegmentedControlBasicComp = (function () {
let SegmentedControlBasicComp = (function () {
return new UICompBuilder(SegmentChildrenMap, (props) => {
const [
validateState,
@@ -147,6 +150,8 @@ const SegmentedControlBasicComp = (function () {
.build();
})();

SegmentedControlBasicComp = migrateOldData(SegmentedControlBasicComp, fixOldInputCompData);

export const SegmentedControlComp = withExposingConfigs(SegmentedControlBasicComp, [
new NameConfig("value", trans("selectInput.valueDesc")),
SelectInputInvalidConfig,
Original file line number Diff line number Diff line change
@@ -17,8 +17,10 @@ import {
} from "./selectInputConstants";
import { useRef } from "react";
import { RecordConstructorToView } from "lowcoder-core";
import { fixOldInputCompData } from "../textInputComp/textInputConstants";
import { migrateOldData } from "comps/generators/simpleGenerators";

const SelectBasicComp = (function () {
let SelectBasicComp = (function () {
const childrenMap = {
...SelectChildrenMap,
defaultValue: stringExposingStateControl("defaultValue"),
@@ -55,6 +57,8 @@ const SelectBasicComp = (function () {
.build();
})();

SelectBasicComp = migrateOldData(SelectBasicComp, fixOldInputCompData);

export const SelectComp = withExposingConfigs(SelectBasicComp, [
new NameConfig("value", trans("selectInput.valueDesc")),
new NameConfig("inputValue", trans("select.inputValueDesc")),
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import styled from "styled-components";
import { UICompBuilder } from "../../generators";
import { FormDataPropertyView } from "../formComp/formDataConstants";
import {
fixOldInputCompData,
getStyle,
inputRefMethods,
TextInputBasicSection,
@@ -30,6 +31,7 @@ import { IconControl } from "comps/controls/iconControl";
import { hasIcon } from "comps/utils";
import { InputRef } from "antd/es/input";
import { RefControl } from "comps/controls/refControl";
import { migrateOldData } from "comps/generators/simpleGenerators";

import React, { useContext } from "react";
import { EditorContext } from "comps/editorState";
@@ -52,7 +54,7 @@ const childrenMap = {
suffixIcon: IconControl,
};

export const InputComp = new UICompBuilder(childrenMap, (props) => {
let InputBasicComp = new UICompBuilder(childrenMap, (props) => {
const [inputProps, validateState] = useTextInputProps(props);
return props.label({
required: props.required,
@@ -108,3 +110,8 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => {
...TextInputConfigs,
])
.build();


const InputComp = migrateOldData(InputBasicComp, fixOldInputCompData);

export { InputComp };
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import { UICompBuilder } from "../../generators";
import { FormDataPropertyView } from "../formComp/formDataConstants";
import {
checkMentionListData,
fixOldInputCompData,
textInputChildren,
} from "./textInputConstants";
import {
@@ -42,7 +43,7 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils";
import {
textInputValidate,
} from "../textInputComp/textInputConstants";
import { jsonControl } from "@lowcoder-ee/comps/controls/codeControl";
import { jsonControl } from "comps/controls/codeControl";
import {
submitEvent,
eventHandlerControl,
@@ -54,6 +55,7 @@ import {

import React, { useContext } from "react";
import { EditorContext } from "comps/editorState";
import { migrateOldData } from "comps/generators/simpleGenerators";

const Wrapper = styled.div<{
$style: InputLikeStyleType;
@@ -267,12 +269,15 @@ let MentionTmpComp = (function () {
.build();
})();


MentionTmpComp = class extends MentionTmpComp {
override autoHeight(): boolean {
return this.children.autoHeight.getView();
}
};

MentionTmpComp = migrateOldData(MentionTmpComp, fixOldInputCompData);

const TextareaTmp2Comp = withMethodExposing(
MentionTmpComp,
refMethods([focusWithOptions, blurMethod])
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import { LabelControl } from "../../controls/labelControl";
import { UICompBuilder, withDefault } from "../../generators";
import { FormDataPropertyView } from "../formComp/formDataConstants";
import {
fixOldInputCompData,
getStyle,
inputRefMethods,
TextInputBasicSection,
@@ -40,14 +41,15 @@ import { hasIcon } from "comps/utils";
import { RefControl } from "comps/controls/refControl";
import React, { useContext } from "react";
import { EditorContext } from "comps/editorState";
import { migrateOldData } from "comps/generators/simpleGenerators";

const PasswordStyle = styled(InputPassword)<{
$style: InputLikeStyleType;
}>`
${(props) => props.$style && getStyle(props.$style)}
`;

const PasswordTmpComp = (function () {
let PasswordTmpComp = (function () {
const childrenMap = {
...textInputChildren,
viewRef: RefControl<InputRef>,
@@ -111,6 +113,8 @@ const PasswordTmpComp = (function () {
.build();
})();

PasswordTmpComp = migrateOldData(PasswordTmpComp, fixOldInputCompData);

const PasswordTmp2Comp = withMethodExposing(PasswordTmpComp, inputRefMethods);

export const PasswordComp = withExposingConfigs(PasswordTmp2Comp, [
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import { AutoHeightControl } from "../../controls/autoHeightControl";
import { UICompBuilder, withDefault } from "../../generators";
import { FormDataPropertyView } from "../formComp/formDataConstants";
import {
fixOldInputCompData,
getStyle,
TextInputBasicSection,
textInputChildren,
@@ -35,6 +36,7 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils";

import React, { useContext } from "react";
import { EditorContext } from "comps/editorState";
import { migrateOldData } from "comps/generators/simpleGenerators";

const TextAreaStyled = styled(TextArea)<{
$style: InputLikeStyleType;
@@ -126,6 +128,8 @@ TextAreaTmpComp = class extends TextAreaTmpComp {
}
};

TextAreaTmpComp = migrateOldData(TextAreaTmpComp, fixOldInputCompData);

const TextareaTmp2Comp = withMethodExposing(
TextAreaTmpComp,
refMethods([focusWithOptions, blurMethod])
Original file line number Diff line number Diff line change
@@ -305,3 +305,17 @@ export function checkMentionListData(data: any) {
}
return data
}

// separate defaultValue and value for old components
export function fixOldInputCompData(oldData: any) {
if (!oldData) return oldData;
if (Boolean(oldData.value) && !Boolean(oldData.defaultValue)) {
const value = oldData.value;
return {
...oldData,
defaultValue: value,
value: '',
};
}
return oldData;
}
20 changes: 11 additions & 9 deletions deploy/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -2,20 +2,14 @@
## Build Lowcoder api-service application
##
FROM maven:3.9-eclipse-temurin-17 AS build-api-service

# Build lowcoder-api
COPY ./server/api-service /lowcoder-server
WORKDIR /lowcoder-server
RUN --mount=type=cache,target=/root/.m2 mvn -f pom.xml clean package -DskipTests

# Create required folder structure
RUN mkdir -p /lowcoder/api-service/plugins /lowcoder/api-service/config /lowcoder/api-service/logs

# Define lowcoder main jar and plugin jars
ARG JAR_FILE=/lowcoder-server/lowcoder-server/target/lowcoder-server-*.jar
ARG PLUGIN_JARS=/lowcoder-server/lowcoder-plugins/*/target/*.jar

# Copy lowcoder server application and plugins
RUN cp ${JAR_FILE} /lowcoder/api-service/server.jar \
&& cp ${PLUGIN_JARS} /lowcoder/api-service/plugins/
RUN mkdir -p /lowcoder/api-service/config /lowcoder/api-service/logs /lowcoder/plugins

# Copy lowcoder server configuration
COPY server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml /lowcoder/api-service/config/
@@ -43,6 +37,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends gosu \
# Copy lowcoder server configuration
COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder/api-service /lowcoder/api-service

# Copy lowcoder api service app, dependencies and libs
COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/app /lowcoder/api-service/app
COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/dependencies /lowcoder/api-service/dependencies
COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/libs /lowcoder/api-service/libs
COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/plugins /lowcoder/api-service/plugins
COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/set-classpath.sh /lowcoder/api-service/set-classpath.sh

EXPOSE 8080
CMD [ "sh" , "/lowcoder/api-service/entrypoint.sh" ]

@@ -202,6 +203,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-instal

# Add lowcoder api-service
COPY --chown=lowcoder:lowcoder --from=lowcoder-ce-api-service /lowcoder/api-service /lowcoder/api-service
RUN mkdir -p /lowcoder/plugins/ && chown lowcoder:lowcoder /lowcoder/plugins/

# Add lowcoder node-service
COPY --chown=lowcoder:lowcoder --from=lowcoder-ce-node-service /lowcoder/node-service /lowcoder/node-service
6 changes: 5 additions & 1 deletion deploy/docker/api-service/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -27,12 +27,16 @@ ${JAVA_HOME}/bin/java -version
echo

cd /lowcoder/api-service
source set-classpath.sh

exec gosu ${USER_ID}:${GROUP_ID} ${JAVA_HOME}/bin/java \
-Djava.util.prefs.userRoot=/tmp \
-Djava.security.egd=file:/dev/./urandom \
-Dhttps.protocols=TLSv1.1,TLSv1.2 \
-Dlog4j2.formatMsgNoLookups=true \
-Dspring.config.location="file:///lowcoder/api-service/config/application.yml,file:///lowcoder/api-service/config/application-selfhost.yml" \
--add-opens java.base/java.nio=ALL-UNNAMED \
-cp "${LOWCODER_CLASSPATH:=.}" \
${JAVA_OPTS} \
-jar "${APP_JAR}" --spring.webflux.base-path=${CONTEXT_PATH} ${CUSTOM_APP_PROPERTIES}
org.lowcoder.api.ServerApplication --spring.webflux.base-path=${CONTEXT_PATH} ${CUSTOM_APP_PROPERTIES}

6 changes: 3 additions & 3 deletions server/api-service/.gitignore
Original file line number Diff line number Diff line change
@@ -23,8 +23,9 @@ dependency-reduced-pom.xml
.run/**
logs/**
tmp/**
/openblocks-server/logs/

# Ignore plugin.properties which are generated dynamically
**/plugin.properties

# to ignore the node_modeules folder
node_modules
@@ -34,5 +35,4 @@ package-lock.json
# test coverage
coverage-summary.json
app/client/cypress/locators/Widgets.json
/openblocks-domain/logs/
application-lowcoder.yml
application-lowcoder.yml
63 changes: 63 additions & 0 deletions server/api-service/PLUGIN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Lowcoder backend plugin system

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

## Reasoning

1. create a cleaner and simpler plugin system with clearly defined purpose(s) (new endpoints, new datasource types, etc..)
2. lowcoder does not need live plugin loading/reloading/unloading/updates, therefore the main feature of pf4j is rendered useless, in fact it adds a lot of complexity due to classloaders used for managing plugins (especially in spring/boot applications)
3. simpler and easier plugin detection - just a jar with a class implementing a common interface (be it a simple pojo project or a complex spring/boot implementation)

## How it works

The main entrypoint for plugin system is in **lowcoder-server** module with class **org.lowcoder.api.framework.configuration.PluginConfiguration**
It creates:
- LowcoderPluginManager bean which is responsible for plugin lifecycle management
- Adds plugin defined endpoints to lowcoder by creating **pluginEndpoints** bean
- TODO: Adds plugin defined datasources to lowcoder by creating **pluginDatasources** bean

### lowcoder-plugin-api library

This library contains APIs for plugin implementations.
It is used by both, lowcoder API server as well as all plugins.

### PluginLoader

The sole purpose of a PluginLoader is to find plugin candidates and load them into VM.
There is currently one implementation that based on paths - **PathBasedPluginLoader**, it:
- looks in folders and subfolders defined in **application.yaml** - entries can point to a folder or specific jar file. If a relative path is supplied, the location of lowcoder API server application jar is used as parent folder (when run in non-packaged state, eg. in IDE, it uses the folder where ServerApplication.class is generated)

```yaml
common:
plugin-dirs:
- plugins
- /some/custom/path/myGreatPlugin.jar
```
- finds all **jar**(s) and inspects them for classes implementing **LowcoderPlugin** interface
- instantiates all LowcoderPlugin implementations
### LowcoderPluginManager
The main job of plugin manager is to:
- register plugins found and instantiated by **PluginLoader**
- start registered plugins by calling **LowcoderPlugin.load()** method
- create and register **RouterFunction**(s) for all loaded plugin endpoints
- TODO: create and register datasources for all loaded plugin datasources
## Plugin project structure
Plugin jar can be structured in any way you like. It can be a plain java project, but also a spring/boot based project or based on any other framework.
It is composed from several parts:
- class(es) implementing **LowcoderPlugin** interface
- class(es) implementing **PluginEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format:
```java
@EndpointExtension(uri = <endpoint uri>, method = <HTTP method>)
public EndpointResponse <handler name>(EndpointRequest request)
{
... your endpoint logic implementation
}
```
- TODO: class(es) impelemting **LowcoderDatasource** interface

84 changes: 84 additions & 0 deletions server/api-service/distribution/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.lowcoder</groupId>
<artifactId>lowcoder-root</artifactId>
<version>${revision}</version>
</parent>

<artifactId>distribution</artifactId>
<packaging>pom</packaging>

<properties>
<assembly.lib.directory>${project.build.directory}/dependencies</assembly.lib.directory>
</properties>


<!-- Dependency added here only to make sure this module is built after
everything alse was built -->
<dependencies>
<dependency>
<groupId>org.lowcoder</groupId>
<artifactId>lowcoder-sdk</artifactId>
</dependency>
<dependency>
<groupId>org.lowcoder</groupId>
<artifactId>lowcoder-infra</artifactId>
</dependency>
<dependency>
<groupId>org.lowcoder</groupId>
<artifactId>lowcoder-domain</artifactId>
</dependency>
<dependency>
<groupId>org.lowcoder</groupId>
<artifactId>lowcoder-server</artifactId>
</dependency>
</dependencies>

<build>
<finalName>lowcoder-api-service</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${assembly.lib.directory}</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
<prependGroupId>true</prependGroupId>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>distro-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<attach>false</attach>
<descriptors>
<descriptor>src/assembly/bin.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
72 changes: 72 additions & 0 deletions server/api-service/distribution/src/assembly/bin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 http://maven.apache.org/xsd/assembly-2.2.0.xsd">
<id>bin</id>
<formats>
<format>dir</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>

<files>
<file>
<source>src/assembly/set-classpath.sh</source>
<outputDirectory></outputDirectory>
</file>
</files>
<fileSets>
<fileSet>
<directory>${assembly.lib.directory}</directory>
<outputDirectory>dependencies</outputDirectory>
<excludes>
<exclude>${project.groupId}:*</exclude>
</excludes>
</fileSet>
</fileSets>

<moduleSets>
<!-- Main lowcoder API server application -->
<moduleSet>
<useAllReactorProjects>true</useAllReactorProjects>
<includes>
<include>org.lowcoder:lowcoder-server</include>
</includes>
<binaries>
<outputDirectory>app</outputDirectory>
<includeDependencies>false</includeDependencies>
<unpack>false</unpack>
</binaries>
</moduleSet>

<!-- Lowcoder API server dependencies -->
<moduleSet>
<useAllReactorProjects>true</useAllReactorProjects>
<includes>
<include>org.lowcoder:lowcoder-domain</include>
<include>org.lowcoder:lowcoder-infra</include>
<include>org.lowcoder:lowcoder-sdk</include>
</includes>
<binaries>
<outputDirectory>libs</outputDirectory>
<includeDependencies>false</includeDependencies>
<unpack>false</unpack>
</binaries>
</moduleSet>

<!-- Lowcoder plugins -->
<moduleSet>
<useAllReactorProjects>true</useAllReactorProjects>
<includeSubModules>true</includeSubModules>
<includes>
<include>org.lowcoder:*Plugin</include>
</includes>
<excludes>
<exclude>org.lowcoder:sqlBasedPlugin</exclude>
</excludes>
<binaries>
<outputDirectory>plugins</outputDirectory>
<includeDependencies>false</includeDependencies>
<unpack>false</unpack>
</binaries>
</moduleSet>
</moduleSets>
</assembly>
11 changes: 11 additions & 0 deletions server/api-service/distribution/src/assembly/set-classpath.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash

#
# Set lowcoder api service classpath for use in startup script
#
export LOWCODER_CLASSPATH="`find libs/ dependencies/ app/ -type f -name "*.jar" | tr '\n' ':' | sed -e 's/:$//'`"

#
# Example usage:
#
# java -cp "${LOWCODER_CLASSPATH}" org.lowcoder.api.ServerApplication
224 changes: 224 additions & 0 deletions server/api-service/lowcoder-dependencies/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<parent>
<artifactId>lowcoder-root</artifactId>
<groupId>org.lowcoder</groupId>
<version>${revision}</version>
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>lowcoder-dependencies</artifactId>
<packaging>pom</packaging>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.1.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.lowcoder.plugin</groupId>
<artifactId>lowcoder-plugin-api</artifactId>
<version>2.3.0</version>
</dependency>

<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j</artifactId>
<version>3.5.0</version>
</dependency>

<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20230227</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>

<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.5.0.202303070854-r</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>

<dependency>
<groupId>tv.twelvetone.rjson</groupId>
<artifactId>rjson</artifactId>
<version>1.3.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk7</artifactId>
<version>1.6.21</version>
</dependency>

<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>es.moki.ratelimitj</groupId>
<artifactId>ratelimitj-core</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>com.github.spullara.mustache.java</groupId>
<artifactId>compiler</artifactId>
<version>0.9.6</version>
</dependency>

<dependency>
<groupId>es.moki.ratelimitj</groupId>
<artifactId>ratelimitj-redis</artifactId>
<version>0.7.0</version>
</dependency>

<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.29</version>
</dependency>

<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<version>0.8.0</version>
</dependency>

<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
</dependency>

<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-spring-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>

<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-core</artifactId>
<version>1.5.0</version>
</dependency>

<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>

<dependency>
<groupId>com.github.cloudyrock.mongock</groupId>
<artifactId>mongock-bom</artifactId>
<version>4.3.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>io.projectreactor.tools</groupId>
<artifactId>blockhound</artifactId>
<version>1.0.6.RELEASE</version>
</dependency>

<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
</dependency>

<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<version>3.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo.spring30x</artifactId>
<version>4.7.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
</dependencies>
</dependencyManagement>


</project>

36 changes: 33 additions & 3 deletions server/api-service/lowcoder-domain/pom.xml
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
<parent>
<artifactId>lowcoder-root</artifactId>
<groupId>org.lowcoder</groupId>
<version>${revision}</version>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>

@@ -186,6 +186,12 @@
<dependency>
<groupId>es.moki.ratelimitj</groupId>
<artifactId>ratelimitj-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
@@ -242,6 +248,18 @@

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>

<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
@@ -268,9 +286,21 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.lowcoder</groupId>
<artifactId>lowcoder-dependencies</artifactId>
<version>${revision}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

</project>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -17,4 +17,9 @@ public class Folder extends HasIdAndAuditing {
@Nullable
private String parentFolderId; // null represents folder in the root folder
private String name;
private String title;
private String description;
private String category;
private String type;
private String image;
}
Original file line number Diff line number Diff line change
@@ -36,6 +36,11 @@ public boolean isAdmin() {
return role == MemberRole.ADMIN;
}

public boolean isSuperAdmin() {
return role == MemberRole.SUPER_ADMIN;
}


@JsonIgnore
public boolean isInvalid() {
return this == NOT_EXIST || StringUtils.isBlank(groupId);
Original file line number Diff line number Diff line change
@@ -7,7 +7,8 @@
public enum MemberRole {

MEMBER("member"),
ADMIN("admin");
ADMIN("admin"),
SUPER_ADMIN("super_admin");

private static final Map<String, MemberRole> VALUE_MAP;

Original file line number Diff line number Diff line change
@@ -52,6 +52,10 @@ public MemberRole getRole() {
return role;
}

public boolean isSuperAdmin() {
return role == MemberRole.SUPER_ADMIN;
}

public boolean isAdmin() {
return role == MemberRole.ADMIN;
}
Original file line number Diff line number Diff line change
@@ -17,9 +17,9 @@ public interface OrganizationService {
@PossibleEmptyMono
Mono<Organization> getOrganizationInEnterpriseMode();

Mono<Organization> create(Organization organization, String creatorUserId);
Mono<Organization> create(Organization organization, String creatorUserId, boolean isSuperAdmin);

Mono<Organization> createDefault(User user);
Mono<Organization> createDefault(User user, boolean isSuperAdmin);

Mono<Organization> getById(String id);

Original file line number Diff line number Diff line change
@@ -86,7 +86,7 @@ public OrganizationServiceImpl(ConfigCenter configCenter) {
}

@Override
public Mono<Organization> createDefault(User user) {
public Mono<Organization> createDefault(User user, boolean isSuperAdmin) {
return Mono.deferContextual(contextView -> {
Locale locale = getLocale(contextView);
String userOrgSuffix = getMessage(locale, "USER_ORG_SUFFIX");
@@ -96,7 +96,7 @@ public Mono<Organization> createDefault(User user) {
organization.setIsAutoGeneratedOrganization(true);
// saas mode
if (commonConfig.getWorkspace().getMode() == WorkspaceMode.SAAS) {
return create(organization, user.getId());
return create(organization, user.getId(), isSuperAdmin);
}
// enterprise mode
return joinOrganizationInEnterpriseMode(user.getId())
@@ -107,7 +107,7 @@ public Mono<Organization> createDefault(User user) {
OrganizationDomain organizationDomain = new OrganizationDomain();
organizationDomain.setConfigs(List.of(DEFAULT_AUTH_CONFIG));
organization.setOrganizationDomain(organizationDomain);
return create(organization, user.getId());
return create(organization, user.getId(), isSuperAdmin);
});
});
}
@@ -145,7 +145,7 @@ private Mono<Organization> getByEnterpriseOrgId() {
}

@Override
public Mono<Organization> create(Organization organization, String creatorId) {
public Mono<Organization> create(Organization organization, String creatorId, boolean isSuperAdmin) {

return Mono.defer(() -> {
if (organization == null || StringUtils.isNotBlank(organization.getId())) {
@@ -155,19 +155,19 @@ public Mono<Organization> create(Organization organization, String creatorId) {
return Mono.just(organization);
})
.flatMap(repository::save)
.flatMap(newOrg -> onOrgCreated(creatorId, newOrg))
.flatMap(newOrg -> onOrgCreated(creatorId, newOrg, isSuperAdmin))
.log();
}

private Mono<Organization> onOrgCreated(String userId, Organization newOrg) {
private Mono<Organization> onOrgCreated(String userId, Organization newOrg, boolean isSuperAdmin) {
return groupService.createAllUserGroup(newOrg.getId())
.then(groupService.createDevGroup(newOrg.getId()))
.then(setOrgAdmin(userId, newOrg))
.then(setOrgAdmin(userId, newOrg, isSuperAdmin))
.thenReturn(newOrg);
}

private Mono<Boolean> setOrgAdmin(String userId, Organization newOrg) {
return orgMemberService.addMember(newOrg.getId(), userId, MemberRole.ADMIN);
private Mono<Boolean> setOrgAdmin(String userId, Organization newOrg, boolean isSuperAdmin) {
return orgMemberService.addMember(newOrg.getId(), userId, isSuperAdmin ? MemberRole.SUPER_ADMIN : MemberRole.ADMIN);
}

@Override
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ public Mono<Map<String, List<ResourcePermission>>> getAllMatchingPermissions(Str
return getOrgId(resourceIds.iterator().next())
.flatMap(orgId -> orgMemberService.getOrgMember(orgId, userId))
.flatMap(orgMember -> {
if (orgMember.isAdmin()) {
if (orgMember.isAdmin() || orgMember.isSuperAdmin()) {
return Mono.just(buildAdminPermissions(resourceType, resourceIds, userId));
}
return getAllMatchingPermissions0(userId, orgMember.getOrgId(), resourceType, resourceIds, resourceAction);
@@ -112,7 +112,7 @@ public Mono<UserPermissionOnResourceStatus> checkUserPermissionStatusOnResource(
Mono<UserPermissionOnResourceStatus> orgUserPermissionMono = getOrgId(resourceId)
.flatMap(orgId -> orgMemberService.getOrgMember(orgId, userId))
.flatMap(orgMember -> {
if (orgMember.isAdmin()) {
if (orgMember.isAdmin() || orgMember.isSuperAdmin()) {
return Mono.just(UserPermissionOnResourceStatus.success(buildAdminPermission(resourceType, resourceId, userId)));
}
return getAllMatchingPermissions0(userId, orgMember.getOrgId(), resourceType, Collections.singleton(resourceId), resourceAction)
Original file line number Diff line number Diff line change
@@ -335,7 +335,7 @@ protected Mono<List<Map<String, String>>> buildUserDetailGroups(String userId, O
Locale locale) {
String orgId = orgMember.getOrgId();
Flux<Group> groups;
if (orgMember.isAdmin()) {
if (orgMember.isAdmin() || orgMember.isSuperAdmin()) {
groups = groupService.getByOrgId(orgId).sort();
} else {
if (withoutDynamicGroups) {
19 changes: 19 additions & 0 deletions server/api-service/lowcoder-infra/pom.xml
Original file line number Diff line number Diff line change
@@ -127,14 +127,33 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.lowcoder.plugin</groupId>
<artifactId>lowcoder-plugin-api</artifactId>
</dependency>

</dependencies>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>17</java.version>

<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.lowcoder</groupId>
<artifactId>lowcoder-dependencies</artifactId>
<version>${revision}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.lowcoder.infra.event;

import lombok.Getter;
import lombok.experimental.SuperBuilder;
import org.springframework.util.MultiValueMap;

@Getter
@SuperBuilder
public class APICallEvent extends AbstractEvent {

private final EventType type;
private final String httpMethod;
private final String requestUri;
private final MultiValueMap<String, String> headers;
private final MultiValueMap<String, String> queryParams;

@Override
public EventType getEventType() {
return EventType.API_CALL_EVENT;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,56 @@
package org.lowcoder.infra.event;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import org.lowcoder.plugin.api.event.LowcoderEvent;

import lombok.Getter;
import lombok.experimental.SuperBuilder;

@Getter
@SuperBuilder
public abstract class AbstractEvent implements Event {

public abstract class AbstractEvent implements LowcoderEvent
{
protected final String orgId;
protected final String userId;
protected final String sessionHash;
protected final Boolean isAnonymous;
private final String ipAddress;
protected Map<String, Object> details;

public Map<String, Object> details()
{
return this.details;
}

public static abstract class AbstractEventBuilder<C extends AbstractEvent, B extends AbstractEvent.AbstractEventBuilder<C, B>>
{
public B detail(String name, String value)
{
if (details == null)
{
details = new HashMap<>();
}
this.details.put(name, value);
return self();
}
}

public void populateDetails() {
if (details == null) {
details = new HashMap<>();
}
for(Field f : getClass().getDeclaredFields()){
Object value = null;
try {
f.setAccessible(Boolean.TRUE);
value = f.get(this);
details.put(f.getName(), value);
} catch (Exception e) {
}

}
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.lowcoder.infra.event;

import org.checkerframework.checker.units.qual.C;

import lombok.Getter;
import lombok.experimental.SuperBuilder;

@Getter
@SuperBuilder
public class SystemCommonEvent extends AbstractEvent
{
private final long apiCalls;

@Override
public EventType getEventType() {
return EventType.SERVER_INFO;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.lowcoder.infra.event.datasource;

import org.lowcoder.infra.event.AbstractEvent;
import org.lowcoder.infra.event.EventType;

import lombok.Getter;
import lombok.experimental.SuperBuilder;
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@
import java.util.Collection;

import org.lowcoder.infra.event.AbstractEvent;
import org.lowcoder.infra.event.EventType;

import lombok.Getter;
import lombok.experimental.SuperBuilder;
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.lowcoder.infra.event.group;

import org.lowcoder.infra.event.EventType;

import lombok.experimental.SuperBuilder;

@SuperBuilder
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.lowcoder.infra.event.group;

import org.lowcoder.infra.event.EventType;

import lombok.experimental.SuperBuilder;

@SuperBuilder
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.lowcoder.infra.event.group;

import org.lowcoder.infra.event.EventType;

import lombok.experimental.SuperBuilder;

@SuperBuilder
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.lowcoder.infra.event.groupmember;

import org.lowcoder.infra.event.EventType;

import lombok.experimental.SuperBuilder;

@SuperBuilder
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.lowcoder.infra.event.groupmember;

import org.lowcoder.infra.event.EventType;

import lombok.experimental.SuperBuilder;

@SuperBuilder
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.lowcoder.infra.event.groupmember;

import org.lowcoder.infra.event.EventType;

import lombok.experimental.SuperBuilder;

@SuperBuilder
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.lowcoder.infra.event.groupmember;

import org.lowcoder.infra.event.EventType;

import lombok.experimental.SuperBuilder;

@SuperBuilder
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.lowcoder.infra.event.user;

import org.lowcoder.infra.event.AbstractEvent;
import org.lowcoder.infra.event.EventType;

import lombok.Getter;
import lombok.experimental.SuperBuilder;
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.lowcoder.infra.event.user;

import org.lowcoder.infra.event.AbstractEvent;
import org.lowcoder.infra.event.EventType;

import lombok.experimental.SuperBuilder;

Original file line number Diff line number Diff line change
@@ -83,7 +83,7 @@ public ReloadableCache<T> build() {
private void startScheduledReloadTask(ReloadableCache<T> cache) {
ScheduledExecutorService scheduledExecutor = newSingleThreadScheduledExecutor();
scheduledExecutor.scheduleAtFixedRate(() -> {
log.debug("{} scheduled reload...", cacheName);
log.trace("{} scheduled reload...", cacheName);
try {
cache.cachedValue = factory.getValue().block();
} catch (Exception e) {
Original file line number Diff line number Diff line change
@@ -10,8 +10,10 @@
import java.util.concurrent.TimeUnit;

import org.apache.commons.collections4.CollectionUtils;
import org.lowcoder.infra.event.SystemCommonEvent;
import org.lowcoder.infra.perf.PerfHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@@ -27,6 +29,9 @@ public class ServerLogService {
@Autowired
private PerfHelper perfHelper;

@Autowired
private ApplicationEventPublisher applicationEventPublisher;

private volatile Queue<ServerLog> serverLogs = new ConcurrentLinkedQueue<>();

public void record(ServerLog serverLog) {
@@ -43,7 +48,13 @@ private void scheduledInsert() {
serverLogRepository.saveAll(tmp)
.collectList()
.subscribe(result -> {
int count = result.size();
perfHelper.count(SERVER_LOG_BATCH_INSERT, Tags.of("size", String.valueOf(result.size())));
applicationEventPublisher.publishEvent(SystemCommonEvent.builder()
.apiCalls(count)
.detail("apiCalls", Integer.toString(count))
.build()
);
});
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

3 changes: 3 additions & 0 deletions server/api-service/lowcoder-plugins/oraclePlugin/pom.xml
Original file line number Diff line number Diff line change
@@ -13,6 +13,9 @@


<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>

8 changes: 8 additions & 0 deletions server/api-service/lowcoder-plugins/pom.xml
Original file line number Diff line number Diff line change
@@ -79,6 +79,14 @@

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.lowcoder</groupId>
<artifactId>lowcoder-dependencies</artifactId>
<version>${revision}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.lowcoder</groupId>
<artifactId>sqlBasedPlugin</artifactId>

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

30 changes: 24 additions & 6 deletions server/api-service/lowcoder-sdk/pom.xml
Original file line number Diff line number Diff line change
@@ -5,19 +5,14 @@
<parent>
<artifactId>lowcoder-root</artifactId>
<groupId>org.lowcoder</groupId>
<version>${revision}</version>
<version>${revision}</version>
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>lowcoder-sdk</artifactId>

<name>lowcoder-sdk</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -171,4 +166,27 @@
<artifactId>validation-api</artifactId>
</dependency>
</dependencies>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>17</java.version>

<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.lowcoder</groupId>
<artifactId>lowcoder-dependencies</artifactId>
<version>${revision}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

</project>
Original file line number Diff line number Diff line change
@@ -44,6 +44,8 @@ public class CommonConfig {
private Cookie cookie = new Cookie();
private JsExecutor jsExecutor = new JsExecutor();
private Set<String> disallowedHosts = new HashSet<>();
private List<String> pluginDirs = new ArrayList<>();
private SuperAdmin superAdmin = new SuperAdmin();
private Marketplace marketplace = new Marketplace();

public boolean isSelfHost() {
@@ -158,4 +160,10 @@ public static class Marketplace {
public static class Query {
private long readStructureTimeout = 15000;
}

@Data
public static class SuperAdmin {
private String userName;
private String password;
}
}
33 changes: 33 additions & 0 deletions server/api-service/lowcoder-server/cert/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
To generate the signing keys in PKCS#12 format:

$ keytool -genkey -alias dev -keyalg RSA -keysize 4096 -validity 36500 -keystore signing.p12 -storetype pkcs12

Enter keystore password:
Re-enter new password:
What is your first and last name?
[Unknown]: dev.lowcoder.org
What is the name of your organizational unit?
[Unknown]: dev
What is the name of your organization?
[Unknown]: Lowcoder Software LTD
What is the name of your City or Locality?
[Unknown]: London
What is the name of your State or Province?
[Unknown]: United Kingdom
What is the two-letter country code for this unit?
[Unknown]: UK
Is CN=dev.lowcoder.org, OU=dev, O=Lowcoder Software LTD, L=London, ST=United Kingdom, C=UK correct?
[no]: yes

Generating 4,096 bit RSA key pair and self-signed certificate (SHA384withRSA) with a validity of 36,500 days
for: CN=dev.lowcoder.org, OU=dev, O=Lowcoder Software LTD, L=London, ST=United Kingdom, C=UK



To export the public key from generated key pair:

$ openssl rsa -in signing.p12 -pubout -out lowcoder.pub

Enter pass phrase for PKCS12 import pass phrase:
writing RSA key

Binary file not shown.
623 changes: 361 additions & 262 deletions server/api-service/lowcoder-server/pom.xml

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions server/api-service/lowcoder-server/src/main/assembly/assembly.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 http://maven.apache.org/xsd/assembly-2.2.0.xsd">

<id>lowcoder-dist</id>
<formats>
<format>dir</format>
</formats>

<includeBaseDirectory>true</includeBaseDirectory>
<baseDirectory>lowcoder</baseDirectory>

<files>
<file>
<source>target/${project.artifactId}-${project.version}.jar</source>
<outputDirectory></outputDirectory>
<destName>application.jar</destName>
</file>
</files>

<!--
<fileSets>
<fileSet>
<directory>applicationbuild/${buildname}</directory>
<outputDirectory>${buildname}/config</outputDirectory>
<includes>
<include>*.xml</include>
<include>*.properties</include>
</includes>
</fileSet>
<fileSet>
<directory>applicationbuild/${buildname}</directory>
<outputDirectory>bin</outputDirectory>
<includes>
<include>*.sh</include>
</includes>
<lineEnding>unix</lineEnding>
<fileMode>0755</fileMode>
</fileSet>
<fileSet>
<directory>./</directory>
<outputDirectory>logs</outputDirectory>
<excludes>
<exclude>*/**</exclude>
</excludes>
</fileSet>
<fileSet>
<directory>./</directory>
<outputDirectory>${buildname}/sourcedata</outputDirectory>
<excludes>
<exclude>*/**</exclude>
</excludes>
</fileSet>
</fileSets>
-->


</assembly>
Original file line number Diff line number Diff line change
@@ -45,6 +45,9 @@ public void init() {

public static void main(String[] args) {

/** Disable Java Flight Recorder for Redis Lettuce driver **/
System.setProperty("io.lettuce.core.jfr", "false");

Schedulers.enableMetrics();

new SpringApplicationBuilder(ServerApplication.class)
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package org.lowcoder.api.application;

import static org.apache.commons.collections4.SetUtils.emptyIfNull;
import static org.lowcoder.infra.event.EventType.APPLICATION_CREATE;
import static org.lowcoder.infra.event.EventType.APPLICATION_DELETE;
import static org.lowcoder.infra.event.EventType.APPLICATION_RECYCLED;
import static org.lowcoder.infra.event.EventType.APPLICATION_RESTORE;
import static org.lowcoder.infra.event.EventType.APPLICATION_UPDATE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_CREATE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_DELETE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_RECYCLED;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_RESTORE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_UPDATE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_VIEW;
import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER;
import static org.lowcoder.sdk.util.ExceptionUtils.ofError;

@@ -26,7 +27,6 @@
import org.lowcoder.domain.application.model.ApplicationStatus;
import org.lowcoder.domain.application.model.ApplicationType;
import org.lowcoder.domain.permission.model.ResourceRole;
import org.lowcoder.infra.event.EventType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
@@ -93,28 +93,27 @@ public Mono<ResponseView<ApplicationView>> getEditingApplication(@PathVariable S
.map(ResponseView::success);
}

// will call the check in ApplicationApiService and ApplicationService
@Override
public Mono<ResponseView<ApplicationView>> getPublishedApplication(@PathVariable String applicationId) {
return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_ALL)
.delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId))
.delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW))
.delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW))
.map(ResponseView::success);
}

@Override
public Mono<ResponseView<ApplicationView>> getPublishedMarketPlaceApplication(@PathVariable String applicationId) {
return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_MARKETPLACE)
.delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId))
.delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW))
.delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW))
.map(ResponseView::success);
}

@Override
public Mono<ResponseView<ApplicationView>> getAgencyProfileApplication(@PathVariable String applicationId) {
return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.AGENCY_PROFILE)
.delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId))
.delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW))
.delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW))
.map(ResponseView::success);
}

Original file line number Diff line number Diff line change
@@ -149,7 +149,7 @@ public Mono<Void> loginOrRegister(AuthUser authUser, ServerWebExchange exchange,
boolean createWorkspace =
authUser.getOrgId() == null && StringUtils.isBlank(invitationId) && authProperties.getWorkspaceCreation();
if (user.getIsNewUser() && createWorkspace) {
return onUserRegister(user);
return onUserRegister(user, false);
}
return Mono.empty();
})
@@ -166,7 +166,7 @@ public Mono<Void> loginOrRegister(AuthUser authUser, ServerWebExchange exchange,
.then(businessEventPublisher.publishUserLoginEvent(authUser.getSource()));
}

private Mono<User> updateOrCreateUser(AuthUser authUser, boolean linkExistingUser) {
public Mono<User> updateOrCreateUser(AuthUser authUser, boolean linkExistingUser) {

if(linkExistingUser) {
return sessionUserService.getVisitor()
@@ -256,8 +256,8 @@ protected Connection getAuthConnection(AuthUser authUser, User user) {
.get();
}

protected Mono<Void> onUserRegister(User user) {
return organizationService.createDefault(user).then();
public Mono<Void> onUserRegister(User user, boolean isSuperAdmin) {
return organizationService.createDefault(user, isSuperAdmin).then();
}

protected Mono<Void> onUserLogin(String orgId, User user, String source) {
@@ -362,7 +362,7 @@ private Mono<Void> removeTokensByAuthId(String authId) {
private Mono<Void> checkIfAdmin() {
return sessionUserService.getVisitorOrgMemberCache()
.flatMap(orgMember -> {
if (orgMember.isAdmin()) {
if (orgMember.isAdmin() || orgMember.isSuperAdmin()) {
return Mono.empty();
}
return deferredError(BizError.NOT_AUTHORIZED, "NOT_AUTHORIZED");
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package org.lowcoder.api.datasource;

import static org.lowcoder.infra.event.EventType.DATA_SOURCE_CREATE;
import static org.lowcoder.infra.event.EventType.DATA_SOURCE_DELETE;
import static org.lowcoder.infra.event.EventType.DATA_SOURCE_PERMISSION_DELETE;
import static org.lowcoder.infra.event.EventType.DATA_SOURCE_PERMISSION_GRANT;
import static org.lowcoder.infra.event.EventType.DATA_SOURCE_PERMISSION_UPDATE;
import static org.lowcoder.infra.event.EventType.DATA_SOURCE_UPDATE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_CREATE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_DELETE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_PERMISSION_DELETE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_PERMISSION_GRANT;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_PERMISSION_UPDATE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_UPDATE;
import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER;
import static org.lowcoder.sdk.util.ExceptionUtils.ofError;
import static org.lowcoder.sdk.util.LocaleUtils.getLocale;
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.lowcoder.api.framework.configuration;

import org.lowcoder.api.ServerApplication;
import org.lowcoder.sdk.config.CommonConfig;
import org.pf4j.spring.SpringPluginManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.system.ApplicationHome;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -15,6 +18,18 @@ public class ApplicationConfiguration
@Autowired
private CommonConfig common;

@Bean("applicationHome")
public ApplicationHome applicatioHome()
{
return new ApplicationHome(ServerApplication.class);
}

@Bean
public SpringPluginManager pluginManager()
{
return new SpringPluginManager();
}

@Bean
public MultipartConfigElement multipartConfigElement()
{
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.lowcoder.api.framework.configuration;

import org.lowcoder.api.framework.plugin.endpoint.ReloadableRouterFunctionMapping;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.WebFluxConfigurationSupport;
import org.springframework.web.reactive.function.server.support.RouterFunctionMapping;

@Configuration
public class CustomWebFluxConfigurationSupport extends WebFluxConfigurationSupport
{
@Override
protected RouterFunctionMapping createRouterFunctionMapping()
{
return new ReloadableRouterFunctionMapping();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.lowcoder.api.framework.configuration;

import java.util.ArrayList;

import org.lowcoder.api.framework.plugin.LowcoderPluginManager;
import org.lowcoder.api.framework.plugin.endpoint.PluginEndpointHandler;
import org.lowcoder.api.framework.plugin.security.PluginAuthorizationManager;
import org.lowcoder.plugin.api.EndpointExtension;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Role;
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Mono;


@Configuration
public class PluginConfiguration
{

@SuppressWarnings("unchecked")
@Bean
@DependsOn("lowcoderPluginManager")
RouterFunction<?> pluginEndpoints(LowcoderPluginManager pluginManager, PluginEndpointHandler pluginEndpointHandler)
{
RouterFunction<?> pluginsList = RouterFunctions.route()
.GET(RequestPredicates.path(PluginEndpointHandler.PLUGINS_BASE_URL), req -> ServerResponse.ok().body(Mono.just(pluginManager.getLoadedPluginsInfo()), ArrayList.class))
.build();

RouterFunction<?> endpoints = pluginEndpointHandler.registeredEndpoints().stream()
.map(r-> (RouterFunction<ServerResponse>)r)
.reduce((o, r )-> (RouterFunction<ServerResponse>) o.andOther(r))
.orElse(null);

return (endpoints == null) ? pluginsList : pluginsList.andOther(endpoints);
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor protectPluginEndpoints(PluginAuthorizationManager pluginAauthManager)
{
AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(EndpointExtension.class, true);
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pointcut, pluginAauthManager);
interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() -1);
return interceptor;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.lowcoder.api.framework.filter;

import org.lowcoder.infra.config.repository.ServerConfigRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.time.Duration;

import static org.lowcoder.api.framework.filter.FilterOrder.API_DELAY_FILTER;

@Component
public class APIDelayFilter implements WebFilter, Ordered {

@Autowired
private ServerConfigRepository serverConfigRepository;

@Override
public int getOrder() {
return API_DELAY_FILTER.getOrder();
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return serverConfigRepository.findByKey("isRateLimited")
.map(serverConfig -> {
if(serverConfig.getValue() != null && Boolean.parseBoolean(serverConfig.getValue().toString())) {
return Mono.delay(Duration.ofSeconds(5)).block();
} else {
return Mono.empty();
}
}).then(chain.filter(exchange));
}
}
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ public enum FilterOrder {
REQUEST_COST(BEFORE_PROXY_CHAIN),
THROTTLING(BEFORE_PROXY_CHAIN),

API_DELAY_FILTER(BEFORE_PROXY_CHAIN),

// WEB_FILTER_CHAIN_PROXY here

USER_BAN(AFTER_PROXY_CHAIN),
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.lowcoder.api.framework.filter;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

@Configuration
public class ReactiveRequestContextFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
return chain.filter(exchange)
.contextWrite(ctx -> ctx.put(ReactiveRequestContextHolder.SERVER_HTTP_REQUEST, request));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.lowcoder.api.framework.filter;

import org.springframework.http.server.reactive.ServerHttpRequest;
import reactor.core.publisher.Mono;

public class ReactiveRequestContextHolder {
public static final Class<ServerHttpRequest> SERVER_HTTP_REQUEST = ServerHttpRequest.class;

public static Mono<ServerHttpRequest> getRequest() {
return Mono.subscriberContext()
.map(ctx -> ctx.get(SERVER_HTTP_REQUEST));
}
}
Original file line number Diff line number Diff line change
@@ -48,7 +48,7 @@ public class ThrottlingFilter implements WebFilter, Ordered {
@PostConstruct
private void init() {
urlRateLimiter = configCenter.threshold().ofMap("urlRateLimiter", String.class, Integer.class, emptyMap());
log.info("API rate limit filter enabled with default rate limit set to: {} requests per second");
log.info("API rate limit filter enabled with default rate limit set to: {} requests per second", defaultApiRateLimit);
}

@Nonnull
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.lowcoder.api.framework.plugin;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.lowcoder.plugin.api.LowcoderPlugin;
import org.lowcoder.plugin.api.LowcoderServices;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.stereotype.Component;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RequiredArgsConstructor
@Component
@Slf4j
public class LowcoderPluginManager
{
private final LowcoderServices lowcoderServices;
private final PluginLoader pluginLoader;
private final Environment environment;

private Map<String, LowcoderPlugin> plugins = new LinkedHashMap<>();

@PostConstruct
private void loadPlugins()
{
registerPlugins();
List<LowcoderPlugin> sorted = new ArrayList<>(plugins.values());
sorted.sort(Comparator.comparing(LowcoderPlugin::loadOrder));

for (LowcoderPlugin plugin : sorted)
{
PluginExecutor executor = new PluginExecutor(plugin, getPluginEnvironmentVariables(plugin), lowcoderServices);
executor.start();
}
}

@PreDestroy
public void unloadPlugins()
{
for (LowcoderPlugin plugin : plugins.values())
{
try
{
plugin.unload();
}
catch(Throwable cause)
{
log.warn("Error unloading plugin: {}!", plugin.pluginId(), cause);
}
}
}

public List<PluginInfo> getLoadedPluginsInfo()
{
List<PluginInfo> infos = new ArrayList<>();
for (LowcoderPlugin plugin : plugins.values())
{
infos.add(new PluginInfo(plugin.pluginId(), plugin.description(), plugin.pluginInfo()));
}
return infos;
}

private Map<String, Object> getPluginEnvironmentVariables(LowcoderPlugin plugin)
{
Map<String, Object> env = new HashMap<>();

String varPrefix = "PLUGIN_" + plugin.pluginId().toUpperCase().replaceAll("-", "_") + "_";
MutablePropertySources propertySources = ((AbstractEnvironment) environment).getPropertySources();
List<String> properties = StreamSupport.stream(propertySources.spliterator(), false)
.filter(propertySource -> propertySource instanceof EnumerablePropertySource)
.map(propertySource -> ((EnumerablePropertySource<?>) propertySource).getPropertyNames())
.flatMap(Arrays::<String> stream)
.distinct()
.sorted()
.filter(prop -> prop.startsWith(varPrefix))
.collect(Collectors.toList());

for (String prop : properties)
{
env.put(StringUtils.removeStart(prop, varPrefix), environment.getProperty(prop));
}

return env;
}

private void registerPlugins()
{
List<LowcoderPlugin> loaded = pluginLoader.loadPlugins();
if (CollectionUtils.isNotEmpty(loaded))
{
for (LowcoderPlugin plugin : loaded)
{
if (!plugins.containsKey(plugin.pluginId()))
{
log.info("Registered plugin: {} ({})", plugin.pluginId(), plugin.getClass().getName());
plugins.put(plugin.pluginId(), plugin);
}
else
{
log.warn("Plugin {} already registered (from: {}), skipping {}.", plugin.pluginId(),
plugins.get(plugin.pluginId()).getClass().getName(),
plugin.getClass().getName());
}
}
}
}

private record PluginInfo(
String id,
String description,
Object info
) {}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package org.lowcoder.api.framework.plugin;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.lowcoder.plugin.api.LowcoderPlugin;
import org.lowcoder.sdk.config.CommonConfig;
import org.springframework.boot.system.ApplicationHome;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
@Component
public class PathBasedPluginLoader implements PluginLoader
{
private final CommonConfig common;
private final ApplicationHome applicationHome;

@Override
public List<LowcoderPlugin> loadPlugins()
{
List<LowcoderPlugin> plugins = new ArrayList<>();

List<String> pluginJars = findPluginsJars();
if (pluginJars.isEmpty())
{
return plugins;
}

for (String pluginJar : pluginJars)
{
log.debug("Inspecting plugin jar candidate: {}", pluginJar);
List<LowcoderPlugin> loadedPlugins = loadPluginCandidates(pluginJar);
if (loadedPlugins.isEmpty())
{
log.debug(" - no plugins found in the jar file");
}
else
{
for (LowcoderPlugin plugin : loadedPlugins)
{
plugins.add(plugin);
}
}
}

return plugins;
}

protected List<String> findPluginsJars()
{
List<String> candidates = new ArrayList<>();
if (CollectionUtils.isNotEmpty(common.getPluginDirs()))
{
for (String pluginDir : common.getPluginDirs())
{
final Path pluginPath = getAbsoluteNormalizedPath(pluginDir);
if (pluginPath != null)
{
candidates.addAll(findPluginCandidates(pluginPath));
}
}
}

return candidates;
}


protected List<String> findPluginCandidates(Path pluginsDir)
{
List<String> pluginCandidates = new ArrayList<>();
try
{
Files.walk(pluginsDir)
.filter(Files::isRegularFile)
.filter(path -> StringUtils.endsWithIgnoreCase(path.toAbsolutePath().toString(), ".jar"))
.forEach(path -> pluginCandidates.add(path.toString()));
}
catch(IOException cause)
{
log.error("Error walking plugin folder! - {}", cause.getMessage());
}

return pluginCandidates;
}

protected List<LowcoderPlugin> loadPluginCandidates(String pluginJar)
{
List<LowcoderPlugin> pluginCandidates = new ArrayList<>();

try
{
Path pluginPath = Path.of(pluginJar);
PluginClassLoader pluginClassLoader = new PluginClassLoader(pluginPath.getFileName().toString(), pluginPath);

ServiceLoader<LowcoderPlugin> pluginServices = ServiceLoader.load(LowcoderPlugin.class, pluginClassLoader);
if (pluginServices != null )
{
Iterator<LowcoderPlugin> pluginIterator = pluginServices.iterator();
while(pluginIterator.hasNext())
{
LowcoderPlugin plugin = pluginIterator.next();
log.debug(" - loaded plugin: {} - {}", plugin.pluginId(), plugin.description());
pluginCandidates.add(plugin);
}
}
}
catch(Throwable cause)
{
log.warn("Error loading plugin!", cause);
}

return pluginCandidates;
}

private Path getAbsoluteNormalizedPath(String path)
{
if (StringUtils.isNotBlank(path))
{
Path absPath = Path.of(path);
if (!absPath.isAbsolute())
{
absPath = Path.of(applicationHome.getDir().getAbsolutePath(), absPath.toString());
}
return absPath.normalize().toAbsolutePath();
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.lowcoder.api.framework.plugin;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.Objects;

import org.apache.commons.lang3.StringUtils;

import lombok.extern.slf4j.Slf4j;


@Slf4j
public class PluginClassLoader extends URLClassLoader
{
private static final ClassLoader baseClassLoader = ClassLoader.getPlatformClassLoader();
private final ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader();

private static final String[] excludedPaths = new String[] {
"org.lowcoder.plugin.api.",
"org/lowcoder/plugin/api/"
};

public PluginClassLoader(String name, Path pluginPath)
{
super(name, pathToURLs(pluginPath), baseClassLoader);
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
Class<?> clazz = findLoadedClass(name);
if (clazz != null)
{
return clazz;
}

if (StringUtils.startsWithAny(name, excludedPaths))
{
try
{
clazz = appClassLoader.loadClass(name);
return clazz;
}
catch(Throwable cause)
{
log.error("[{}] :: Error loading class with appClassLoader - {}", name, cause.getMessage(), cause );
}
}


try
{
clazz = super.loadClass(name, resolve);
if (clazz != null)
{
return clazz;
}
}
catch(NoClassDefFoundError cause)
{
log.error("[{}] :: Error loading class - {}", name, cause.getMessage(), cause );
}

return null;
}

@Override
public URL getResource(String name) {
Objects.requireNonNull(name);
if (StringUtils.startsWithAny(name, excludedPaths))
{
return appClassLoader.getResource(name);
}
return super.getResource(name);
}


@Override
public Enumeration<URL> getResources(String name) throws IOException
{
Objects.requireNonNull(name);
if (StringUtils.startsWithAny(name, excludedPaths))
{
return appClassLoader.getResources(name);
}
return super.getResources(name);
}

private static URL[] pathToURLs(Path path)
{
URL[] urls = null;
try
{
urls = new URL[] { path.toUri().toURL() };
}
catch(MalformedURLException cause)
{
/** should not happen **/
}

return urls;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.lowcoder.api.framework.plugin;

import java.util.Map;

import org.lowcoder.plugin.api.LowcoderPlugin;
import org.lowcoder.plugin.api.LowcoderServices;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class PluginExecutor extends Thread
{
private Map<String, Object> env;
private LowcoderPlugin plugin;
private LowcoderServices services;

public PluginExecutor(LowcoderPlugin plugin, Map<String, Object> env, LowcoderServices services)
{
this.env = env;
this.plugin = plugin;
this.services = services;
this.setContextClassLoader(plugin.getClass().getClassLoader());
this.setName(plugin.pluginId());
}

@Override
public void run()
{
if (plugin.load(env, services))
{
log.info("Plugin [{}] loaded and running.", plugin.pluginId());
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.lowcoder.api.framework.plugin;

import java.util.List;

import org.lowcoder.plugin.api.LowcoderPlugin;

public interface PluginLoader
{
List<LowcoderPlugin> loadPlugins();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.lowcoder.api.framework.plugin;

import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;

import org.lowcoder.api.framework.plugin.endpoint.PluginEndpointHandler;
import org.lowcoder.infra.config.repository.ServerConfigRepository;
import org.lowcoder.plugin.api.LowcoderServices;
import org.lowcoder.plugin.api.PluginEndpoint;
import org.lowcoder.plugin.api.event.LowcoderEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Component
public class SharedPluginServices implements LowcoderServices
{
private final PluginEndpointHandler pluginEndpointHandler;

@Autowired
private ServerConfigRepository serverConfigRepository;

private List<Consumer<LowcoderEvent>> eventListeners = new LinkedList<>();

@Override
public void registerEventListener(Consumer<LowcoderEvent> listener)
{
this.eventListeners.add(listener);
}

@EventListener(classes = LowcoderEvent.class)
private void publishEvents(LowcoderEvent event)
{
for (Consumer<LowcoderEvent> listener : eventListeners)
{
listener.accept(event);
}
}

@Override
public void registerEndpoints(String urlPrefix, List<PluginEndpoint> endpoints)
{
pluginEndpointHandler.registerEndpoints(urlPrefix, endpoints);
}

@Override
public void setConfig(String key, Object value) {
serverConfigRepository.upsert(key, value).block();
}

@Override
public Object getConfig(String key) {
return serverConfigRepository.findByKey(key).block();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package org.lowcoder.api.framework.plugin.data;

import org.lowcoder.plugin.api.PluginEndpoint;
import org.lowcoder.plugin.api.PluginEndpoint.Method;
import org.lowcoder.plugin.api.data.EndpointRequest;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.reactive.function.server.ServerRequest;

import java.net.URI;
import java.security.Principal;
import java.util.AbstractMap.SimpleEntry;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;

public class PluginServerRequest implements EndpointRequest
{
private URI uri;
private PluginEndpoint.Method method;
private CompletableFuture<byte[]> body;
private Map<String, List<String>> headers;
private Map<String, List<Map.Entry<String, String>>> cookies;
private Map<String, Object> attributes;
private Map<String, String> pathVariables;

private Map<String, List<String>> queryParams;
private CompletableFuture<? extends Principal> principal;


public PluginServerRequest()
{
headers = new HashMap<>();
cookies = new HashMap<>();
attributes = new HashMap<>();
pathVariables = new HashMap<>();
queryParams = new HashMap<>();
}

public static PluginServerRequest fromServerRequest(ServerRequest request)
{
PluginServerRequest psr = new PluginServerRequest();

psr.uri = request.uri();
psr.method = fromHttpMetod(request.method());
psr.body = request.bodyToMono(byte[].class).toFuture();

if (request.headers() != null)
{
HttpHeaders httpHeaders = request.headers().asHttpHeaders();
psr.headers = httpHeaders;
}

if (request.cookies() != null)
{
request.cookies().entrySet().stream()
.forEach(entry -> {
psr.cookies.put(entry.getKey(), fromHttpCookieList(entry.getValue()));
});
}

if (request.attributes() != null)
{
request.attributes().forEach((name, value) -> {
psr.attributes.put(name, value);
});
}

if (request.pathVariables() != null)
{
request.pathVariables().entrySet()
.forEach(entry -> {
psr.pathVariables.put(entry.getKey(), entry.getValue());
});
}

if (request.queryParams() != null)
{
request.queryParams().entrySet()
.forEach(entry -> {
psr.queryParams.put(entry.getKey(), entry.getValue());
});
}

psr.principal = request.principal().toFuture();

return psr;
}

private static List<Map.Entry<String, String>> fromHttpCookieList(List<HttpCookie> cookies)
{
List<Map.Entry<String, String>> list = new LinkedList<>();

if (cookies != null)
{
cookies.stream()
.forEach(cookie -> {
list.add(new SimpleEntry<String, String>(cookie.getName(), cookie.getValue()));
});
}

return list;
}



@Override
public URI uri() {
return uri;
}
@Override
public Method method() {
return method;
}
@Override
public CompletableFuture<byte[]> body() {
return body;
}
@Override
public Map<String, List<String>> headers() {
return headers;
}
@Override
public Map<String, List<Entry<String, String>>> cookies() {
return cookies;
}
@Override
public Map<String, Object> attributes() {
return attributes;
}
@Override
public Map<String, String> pathVariables() {
return pathVariables;
}

@Override
public Map<String, List<String>> queryParams() {
return queryParams;
}
@Override
public CompletableFuture<? extends Principal> principal() {
return principal;
}


public static HttpMethod fromPluginEndpointMethod(PluginEndpoint.Method method)
{
switch(method)
{
case GET:
return HttpMethod.GET;
case POST:
return HttpMethod.POST;
case PUT:
return HttpMethod.PUT;
case PATCH:
return HttpMethod.PATCH;
case DELETE:
return HttpMethod.DELETE;
case OPTIONS:
return HttpMethod.OPTIONS;
}
return null;
}

public static PluginEndpoint.Method fromHttpMetod(HttpMethod method)
{
if (method == HttpMethod.GET)
{
return PluginEndpoint.Method.GET;
}
else if (method == HttpMethod.POST)
{
return PluginEndpoint.Method.POST;
}
else if (method == HttpMethod.PUT)
{
return PluginEndpoint.Method.PUT;
}
else if (method == HttpMethod.PATCH)
{
return PluginEndpoint.Method.PATCH;
}
else if (method == HttpMethod.DELETE)
{
return PluginEndpoint.Method.DELETE;
}
else if (method == HttpMethod.OPTIONS)
{
return PluginEndpoint.Method.OPTIONS;
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.lowcoder.api.framework.plugin.endpoint;

import java.util.List;

import org.lowcoder.plugin.api.PluginEndpoint;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

public interface PluginEndpointHandler
{
public static final String PLUGINS_BASE_URL = "/api/plugins/";

void registerEndpoints(String urlPrefix, List<PluginEndpoint> endpoints);
List<RouterFunction<ServerResponse>> registeredEndpoints();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package org.lowcoder.api.framework.plugin.endpoint;

import static org.springframework.web.reactive.function.server.RequestPredicates.DELETE;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.OPTIONS;
import static org.springframework.web.reactive.function.server.RequestPredicates.PATCH;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.PUT;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.lowcoder.api.framework.plugin.data.PluginServerRequest;
import org.lowcoder.api.framework.plugin.security.SecuredEndpoint;
import org.lowcoder.plugin.api.EndpointExtension;
import org.lowcoder.plugin.api.PluginEndpoint;
import org.lowcoder.plugin.api.data.EndpointRequest;
import org.lowcoder.plugin.api.data.EndpointResponse;
import org.lowcoder.sdk.exception.BaseException;
import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.aop.target.SimpleBeanTargetSource;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.http.ResponseCookie;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

@Slf4j
@RequiredArgsConstructor
@Component
public class PluginEndpointHandlerImpl implements PluginEndpointHandler
{
private List<RouterFunction<ServerResponse>> routes = new ArrayList<>();

private final ApplicationContext applicationContext;
private final DefaultListableBeanFactory beanFactory;

@Override
public void registerEndpoints(String pluginUrlPrefix, List<PluginEndpoint> endpoints)
{
String urlPrefix = PLUGINS_BASE_URL + pluginUrlPrefix;

if (CollectionUtils.isNotEmpty(endpoints))
{
for (PluginEndpoint endpoint : endpoints)
{
Method[] handlers = endpoint.getClass().getDeclaredMethods();
if (handlers != null && handlers.length > 0)
{
for (Method handler : handlers)
{
registerEndpointHandler(urlPrefix, endpoint, handler);
}
}
}

((ReloadableRouterFunctionMapping)beanFactory.getBean("routerFunctionMapping")).reloadFunctionMappings();
}
}

@Override
public List<RouterFunction<ServerResponse>> registeredEndpoints()
{
return routes;
}

private void registerEndpointHandler(String urlPrefix, PluginEndpoint endpoint, Method handler)
{
if (!handler.isAnnotationPresent(EndpointExtension.class) || !checkHandlerMethod(handler))
{
if (handler.isAnnotationPresent(EndpointExtension.class))
{
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());
}
return;
}

EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class);
String endpointName = endpoint.getClass().getSimpleName() + "_" + handler.getName();
RouterFunction<ServerResponse> routerFunction = route(createRequestPredicate(urlPrefix, endpointMeta), req -> runPluginEndpointMethod(endpoint, endpointMeta, handler, req));
routes.add(routerFunction);
registerRouterFunctionMapping(endpointName, routerFunction);

log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri());
}

@SecuredEndpoint
public Mono<ServerResponse> runPluginEndpointMethod(PluginEndpoint endpoint, EndpointExtension endpointMeta, Method handler, ServerRequest request)
{
Mono<ServerResponse> result = null;
try
{
log.info("Running plugin endpoint method {}\nRequest: {}", handler.getName(), request);

EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(request));
result = createServerResponse(response);
}
catch (IllegalAccessException | InvocationTargetException cause)
{
throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !");
}
return result;
}


private void registerRouterFunctionMapping(String endpointName, RouterFunction<ServerResponse> routerFunction)
{
String beanName = "pluginEndpoint_" + endpointName + "_" + System.currentTimeMillis();
((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> routerFunction );
log.debug("Registering RouterFunction bean definition: {}", beanName);
}


private Mono<ServerResponse> createServerResponse(EndpointResponse pluginResponse)
{
/** Create response with given status **/
BodyBuilder builder = ServerResponse.status(pluginResponse.statusCode());

/** Set response headers **/
if (pluginResponse.headers() != null && !pluginResponse.headers().isEmpty())
{
pluginResponse.headers().entrySet()
.forEach(entry -> builder.header(entry.getKey(), entry.getValue().toArray(new String[] {})));
}

/** Set cookies if available **/
if (pluginResponse.cookies() != null && !pluginResponse.cookies().isEmpty())
{
pluginResponse.cookies().values()
.forEach(cookies -> cookies
.forEach(cookie -> builder
.cookie(ResponseCookie.from(cookie.getKey(), cookie.getValue()).build())));
}

/** Set response body if available **/
if (pluginResponse.body() != null)
{
return builder.bodyValue(pluginResponse.body());
}

return builder.build();
}

private boolean checkHandlerMethod(Method method)
{
ResolvableType returnType = ResolvableType.forMethodReturnType(method);

return (returnType.getRawClass().isAssignableFrom(EndpointResponse.class)
&& method.getParameterCount() == 1
&& method.getParameterTypes()[0].isAssignableFrom(EndpointRequest.class)
);
}

private RequestPredicate createRequestPredicate(String basePath, EndpointExtension endpoint)
{
switch(endpoint.method())
{
case GET:
return GET(pluginEndpointUri(basePath, endpoint.uri()));
case POST:
return POST(pluginEndpointUri(basePath, endpoint.uri()));
case PUT:
return PUT(pluginEndpointUri(basePath, endpoint.uri()));
case PATCH:
return PATCH(pluginEndpointUri(basePath, endpoint.uri()));
case DELETE:
return DELETE(pluginEndpointUri(basePath, endpoint.uri()));
case OPTIONS:
return OPTIONS(pluginEndpointUri(basePath, endpoint.uri()));
}
return null;
}

private String pluginEndpointUri(String basePath, String uri)
{
return StringUtils.join(basePath, StringUtils.prependIfMissing(uri, "/"));
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.lowcoder.api.framework.plugin.endpoint;

import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.support.RouterFunctionMapping;


public class ReloadableRouterFunctionMapping extends RouterFunctionMapping
{
/**
* Rescan application context for RouterFunction beans
*/
public void reloadFunctionMappings()
{
initRouterFunctions();
if (getRouterFunction() != null)
{
RouterFunctions.changeParser(getRouterFunction(), getPathPatternParser());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.lowcoder.api.framework.plugin.security;

import java.util.function.Supplier;

import org.aopalliance.intercept.MethodInvocation;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class EndpointAuthorizationManager implements AuthorizationManager<MethodInvocation>
{

@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation)
{
log.info("Checking plugin endpoint invocation security for {}", invocation.getMethod().getName());

return new AuthorizationDecision(true);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.lowcoder.api.framework.plugin.security;

import java.lang.reflect.Method;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.StringUtils;
import org.lowcoder.plugin.api.EndpointExtension;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

@Slf4j
//@Component
public class PluginAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation>
{
private final MethodSecurityExpressionHandler expressionHandler;

public PluginAuthorizationManager()
{
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
}

@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, MethodInvocation invocation)
{
log.info("Checking plugin reactive endpoint invocation security for {}", invocation.getMethod().getName());

EndpointExtension endpointExtension = (EndpointExtension)invocation.getArguments()[1];
if (endpointExtension == null || StringUtils.isBlank(endpointExtension.authorize()))
{
return Mono.empty();
}

Expression authorizeExpression = this.expressionHandler.getExpressionParser()
.parseExpression(endpointExtension.authorize());

return authentication
.map(auth -> expressionHandler.createEvaluationContext(auth, invocation))
.flatMap(ctx -> evaluateAsBoolean(authorizeExpression, ctx))
.map(granted -> new ExpressionAuthorizationDecision(granted, authorizeExpression));
}


private Mono<Boolean> evaluateAsBoolean(Expression expr, EvaluationContext ctx)
{
return Mono.defer(() ->
{
Object value;
try
{
value = expr.getValue(ctx);
}
catch (EvaluationException ex)
{
return Mono.error(() -> new IllegalArgumentException(
"Failed to evaluate expression '" + expr.getExpressionString() + "'", ex));
}

if (value instanceof Boolean bool)
{
return Mono.just(bool);
}

if (value instanceof Mono<?> monoBool)
{
Mono<?> monoValue = monoBool;
return monoValue
.filter(Boolean.class::isInstance)
.map(Boolean.class::cast)
.switchIfEmpty(createInvalidReturnTypeMono(expr));
}
return createInvalidReturnTypeMono(expr);
});
}

private static Mono<Boolean> createInvalidReturnTypeMono(Expression expr)
{
return Mono.error(() -> new IllegalStateException(
"Expression: '" + expr.getExpressionString() + "' must return boolean or Mono<Boolean>"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.lowcoder.api.framework.plugin.security;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SecuredEndpoint {

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
package org.lowcoder.api.framework.security;


import static org.lowcoder.infra.constant.NewUrl.GITHUB_STAR;
import static org.lowcoder.infra.constant.Url.APPLICATION_URL;
import static org.lowcoder.infra.constant.Url.CONFIG_URL;
import static org.lowcoder.infra.constant.Url.CUSTOM_AUTH;
import static org.lowcoder.infra.constant.Url.DATASOURCE_URL;
import static org.lowcoder.infra.constant.Url.GROUP_URL;
import static org.lowcoder.infra.constant.Url.INVITATION_URL;
import static org.lowcoder.infra.constant.Url.ORGANIZATION_URL;
import static org.lowcoder.infra.constant.Url.QUERY_URL;
import static org.lowcoder.infra.constant.Url.STATE_URL;
import static org.lowcoder.infra.constant.Url.USER_URL;
import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER;
import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER_ID;

import java.util.List;

import javax.annotation.Nonnull;

import org.lowcoder.api.authentication.request.AuthRequestFactory;
import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl;
import org.lowcoder.api.authentication.util.JWTUtils;
@@ -14,7 +32,6 @@
import org.lowcoder.infra.constant.NewUrl;
import org.lowcoder.sdk.config.CommonConfig;
import org.lowcoder.sdk.util.CookieHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
@@ -23,6 +40,7 @@
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity.CsrfSpec;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
@@ -32,48 +50,24 @@
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;

import javax.annotation.Nonnull;
import java.util.List;

import static org.lowcoder.infra.constant.NewUrl.GITHUB_STAR;
import static org.lowcoder.infra.constant.Url.*;
import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER;
import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER_ID;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@EnableReactiveMethodSecurity(useAuthorizationManager = true)
public class SecurityConfig {

@Autowired
private CommonConfig commonConfig;

@Autowired
private SessionUserService sessionUserService;

@Autowired
private UserService userService;

@Autowired
private AccessDeniedHandler accessDeniedHandler;

@Autowired
private ServerAuthenticationEntryPoint serverAuthenticationEntryPoint;

@Autowired
private CookieHelper cookieHelper;

@Autowired
AuthenticationService authenticationService;

@Autowired
AuthenticationApiServiceImpl authenticationApiService;

@Autowired
AuthRequestFactory<AuthRequestContext> authRequestFactory;

@Autowired
JWTUtils jwtUtils;
private final CommonConfig commonConfig;
private final SessionUserService sessionUserService;
private final UserService userService;
private final AccessDeniedHandler accessDeniedHandler;
private final ServerAuthenticationEntryPoint serverAuthenticationEntryPoint;
private final CookieHelper cookieHelper;
private final AuthenticationService authenticationService;
private final AuthenticationApiServiceImpl authenticationApiService;
private final AuthRequestFactory<AuthRequestContext> authRequestFactory;
private final JWTUtils jwtUtils;

@Bean
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
@@ -90,7 +84,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {

http
.cors(cors -> cors.configurationSource(buildCorsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.csrf(CsrfSpec::disable)
.anonymous(anonymous -> anonymous.principal(createAnonymousUser()))
.httpBasic(Customizer.withDefaults())
.authorizeExchange(customizer -> customizer
@@ -146,7 +140,9 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.DATASOURCE_URL + "/jsDatasourcePlugins"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/api/docs/**")
)
.permitAll()
.permitAll()
.pathMatchers("/api/plugins/**")
.permitAll()
.pathMatchers("/api/**")
.authenticated()
.pathMatchers("/test/**")
@@ -223,7 +219,7 @@ private CorsConfiguration skipCheckCorsForAllowListDomains() {
}

@Bean
public ForwardedHeaderTransformer forwardedHeaderTransformer() {
ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}

Original file line number Diff line number Diff line change
@@ -182,6 +182,11 @@ private Mono<Void> removePermissions(String folderId) {
public Mono<FolderInfoView> update(Folder folder) {
Folder newFolder = new Folder();
newFolder.setName(folder.getName());
newFolder.setTitle(folder.getTitle());
newFolder.setType(folder.getType());
newFolder.setCategory(folder.getCategory());
newFolder.setDescription(folder.getDescription());
newFolder.setImage(folder.getImage());
return checkManagePermission(folder.getId())
.then(folderService.updateById(folder.getId(), newFolder))
.then(folderService.findById(folder.getId()))
@@ -241,7 +246,7 @@ public Flux<?> getElements(@Nullable String folderId, @Nullable ApplicationType
if (folderInfoView == null) {
return;
}
folderInfoView.setManageable(orgMember.isAdmin() || orgMember.getUserId().equals(folderInfoView.getCreateBy()));
folderInfoView.setManageable(orgMember.isAdmin() || orgMember.isSuperAdmin() || orgMember.getUserId().equals(folderInfoView.getCreateBy()));

List<FolderInfoView> folderInfoViews = folderNode.getFolderChildren().stream().filter(FolderInfoView::isVisible).toList();
folderInfoView.setSubFolders(folderInfoViews);
@@ -335,7 +340,7 @@ private Mono<Tree<ApplicationInfoView, FolderInfoView>> buildApplicationInfoView
private Mono<OrgMember> checkManagePermission(String folderId) {
return sessionUserService.getVisitorOrgMemberCache()
.flatMap(orgMember -> {
if (orgMember.isAdmin()) {
if (orgMember.isAdmin() || orgMember.isSuperAdmin()) {
return Mono.just(orgMember);
}
return isCreator(folderId)
@@ -421,6 +426,10 @@ public Mono<FolderInfoView> buildFolderInfoView(Folder folder, boolean visible,
.folderId(folder.getId())
.parentFolderId(folder.getParentFolderId())
.name(folder.getName())
.description(folder.getDescription())
.category(folder.getCategory())
.type(folder.getType())
.image(folder.getImage())
.createAt(folder.getCreatedAt() == null ? 0 : folder.getCreatedAt().toEpochMilli())
.createBy(user.getName())
.createTime(folder.getCreatedAt())
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.lowcoder.api.home;

import static org.lowcoder.infra.event.EventType.APPLICATION_MOVE;
import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_MOVE;
import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER;
import static org.lowcoder.sdk.util.ExceptionUtils.ofError;

@@ -13,7 +13,11 @@
import org.lowcoder.domain.folder.model.Folder;
import org.lowcoder.domain.folder.service.FolderService;
import org.lowcoder.domain.permission.model.ResourceRole;
import org.lowcoder.infra.event.EventType;
import org.lowcoder.infra.constant.NewUrl;
import org.lowcoder.plugin.api.event.LowcoderEvent.EventType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
Original file line number Diff line number Diff line change
@@ -20,6 +20,11 @@ public class FolderInfoView {
private final String folderId;
private final String parentFolderId;
private final String name;
private final String title;
private final String description;
private final String category;
private final String type;
private final String image;
private final Long createAt;
private final String createBy;
private boolean isVisible;
Original file line number Diff line number Diff line change
@@ -18,6 +18,8 @@ public interface SessionUserService {
@NonEmptyMono
Mono<OrgMember> getVisitorOrgMemberCache();

Mono<OrgMember> getVisitorOrgMemberCacheSilent();

Mono<OrgMember> getVisitorOrgMember();

Mono<Boolean> isAnonymousUser();
@@ -33,4 +35,6 @@ public interface SessionUserService {
Mono<User> resolveSessionUserForJWT(Claims claims, String token);

Mono<Boolean> tokenExist(String token);

Mono<String> getVisitorToken();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.lowcoder.api.home;

import static org.lowcoder.sdk.constants.GlobalContext.CURRENT_ORG_MEMBER;
import static org.lowcoder.sdk.constants.GlobalContext.VISITOR_TOKEN;
import static org.lowcoder.sdk.exception.BizError.UNABLE_TO_FIND_VALID_ORG;
import static org.lowcoder.sdk.util.ExceptionUtils.deferredError;
import static org.lowcoder.sdk.util.JsonUtils.fromJsonQuietly;
@@ -74,6 +75,17 @@ public Mono<OrgMember> getVisitorOrgMemberCache() {
.switchIfEmpty(deferredError(UNABLE_TO_FIND_VALID_ORG, "UNABLE_TO_FIND_VALID_ORG"));
}

@Override
public Mono<OrgMember> getVisitorOrgMemberCacheSilent() {
return Mono.deferContextual(contextView -> (Mono<OrgMember>) contextView.get(CURRENT_ORG_MEMBER))
.delayUntil(Mono::just);
}

@Override
public Mono<String> getVisitorToken() {
return Mono.deferContextual(contextView -> Mono.just(contextView.get(VISITOR_TOKEN)));
}

@Override
public Mono<OrgMember> getVisitorOrgMember() {
return getVisitorId()
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@
import org.lowcoder.api.util.BusinessEventPublisher;
import org.lowcoder.domain.query.model.LibraryQuery;
import org.lowcoder.domain.query.service.LibraryQueryService;
import org.lowcoder.infra.event.EventType;
import org.lowcoder.plugin.api.event.LowcoderEvent.EventType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
Loading