diff --git a/.gitignore b/.gitignore index 2e1b56cd59..81c0e6c82a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index 01733833eb..029be11e2c 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-comps", - "version": "0.0.26", + "version": "0.0.27", "type": "module", "license": "MIT", "dependencies": { diff --git a/client/packages/lowcoder-comps/src/comps/chartComp/chartComp.tsx b/client/packages/lowcoder-comps/src/comps/chartComp/chartComp.tsx index b6668f1c93..092a8d382f 100644 --- a/client/packages/lowcoder-comps/src/comps/chartComp/chartComp.tsx +++ b/client/packages/lowcoder-comps/src/comps/chartComp/chartComp.tsx @@ -120,7 +120,9 @@ ChartTmpComp = withViewFn(ChartTmpComp, (comp) => { const handleOnMapScriptLoad = () => { setMapScriptLoaded(true); - loadGoogleMapData(); + setTimeout(() => { + loadGoogleMapData(); + }) } useEffect(() => { diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx index cb5a7836ef..58dcfe8c47 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx @@ -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) = ); }; -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([ diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx index f8b154e42e..0cb4587a62 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx @@ -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, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx index a8c2c0dc11..c45c0cdc61 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx @@ -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")), diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx index 11bfceed09..4ab4add860 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx @@ -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, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx index 73a7d46750..a73827c2a7 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx @@ -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, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx index 1a30f25221..50455b3354 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx @@ -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")), diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx index fc34bc7238..5eacf07cfa 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx @@ -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 }; diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx index 51815260fc..9bad13d1ee 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx @@ -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]) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx index b5c3d701dc..7659cdf72e 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx @@ -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,6 +41,7 @@ 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; @@ -47,7 +49,7 @@ const PasswordStyle = styled(InputPassword)<{ ${(props) => props.$style && getStyle(props.$style)} `; -const PasswordTmpComp = (function () { +let PasswordTmpComp = (function () { const childrenMap = { ...textInputChildren, viewRef: RefControl, @@ -111,6 +113,8 @@ const PasswordTmpComp = (function () { .build(); })(); +PasswordTmpComp = migrateOldData(PasswordTmpComp, fixOldInputCompData); + const PasswordTmp2Comp = withMethodExposing(PasswordTmpComp, inputRefMethods); export const PasswordComp = withExposingConfigs(PasswordTmp2Comp, [ diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx index e11027a698..fe6a4ad24e 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx @@ -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]) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 1d01266afa..9c9d17cbbd 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -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; +} diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 6f55ed0fc4..c536b8a242 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -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 diff --git a/deploy/docker/api-service/entrypoint.sh b/deploy/docker/api-service/entrypoint.sh index 5f2e3ad2ee..0f43580fea 100644 --- a/deploy/docker/api-service/entrypoint.sh +++ b/deploy/docker/api-service/entrypoint.sh @@ -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} diff --git a/server/api-service/.gitignore b/server/api-service/.gitignore index 044c6298ea..a9fc541a99 100644 --- a/server/api-service/.gitignore +++ b/server/api-service/.gitignore @@ -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 \ No newline at end of file +application-lowcoder.yml diff --git a/server/api-service/PLUGIN.md b/server/api-service/PLUGIN.md new file mode 100644 index 0000000000..65a99adef5 --- /dev/null +++ b/server/api-service/PLUGIN.md @@ -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 = , method = ) + public EndpointResponse (EndpointRequest request) + { + ... your endpoint logic implementation + } +``` +- TODO: class(es) impelemting **LowcoderDatasource** interface + diff --git a/server/api-service/distribution/pom.xml b/server/api-service/distribution/pom.xml new file mode 100644 index 0000000000..d68b3fab4a --- /dev/null +++ b/server/api-service/distribution/pom.xml @@ -0,0 +1,84 @@ + + 4.0.0 + + org.lowcoder + lowcoder-root + ${revision} + + + distribution + pom + + + ${project.build.directory}/dependencies + + + + + + + org.lowcoder + lowcoder-sdk + + + org.lowcoder + lowcoder-infra + + + org.lowcoder + lowcoder-domain + + + org.lowcoder + lowcoder-server + + + + + lowcoder-api-service + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${assembly.lib.directory} + false + false + true + true + + + + + + maven-assembly-plugin + + + distro-assembly + package + + single + + + false + + src/assembly/bin.xml + + + + + + + + + \ No newline at end of file diff --git a/server/api-service/distribution/src/assembly/bin.xml b/server/api-service/distribution/src/assembly/bin.xml new file mode 100644 index 0000000000..b6422619ec --- /dev/null +++ b/server/api-service/distribution/src/assembly/bin.xml @@ -0,0 +1,72 @@ + + bin + + dir + + false + + + + src/assembly/set-classpath.sh + + + + + + ${assembly.lib.directory} + dependencies + + ${project.groupId}:* + + + + + + + + true + + org.lowcoder:lowcoder-server + + + app + false + false + + + + + + true + + org.lowcoder:lowcoder-domain + org.lowcoder:lowcoder-infra + org.lowcoder:lowcoder-sdk + + + libs + false + false + + + + + + true + true + + org.lowcoder:*Plugin + + + org.lowcoder:sqlBasedPlugin + + + plugins + false + false + + + + \ No newline at end of file diff --git a/server/api-service/distribution/src/assembly/set-classpath.sh b/server/api-service/distribution/src/assembly/set-classpath.sh new file mode 100755 index 0000000000..de82ddf7fc --- /dev/null +++ b/server/api-service/distribution/src/assembly/set-classpath.sh @@ -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 diff --git a/server/api-service/lowcoder-dependencies/pom.xml b/server/api-service/lowcoder-dependencies/pom.xml new file mode 100644 index 0000000000..53ffadf95f --- /dev/null +++ b/server/api-service/lowcoder-dependencies/pom.xml @@ -0,0 +1,224 @@ + + + + + lowcoder-root + org.lowcoder + ${revision} + + + 4.0.0 + lowcoder-dependencies + pom + + + + + org.springframework.boot + spring-boot-dependencies + 3.1.2 + pom + import + + + + org.lowcoder.plugin + lowcoder-plugin-api + 2.3.0 + + + + org.pf4j + pf4j + 3.5.0 + + + + org.json + json + 20230227 + + + + org.projectlombok + lombok + 1.18.26 + + + + org.apache.commons + commons-text + 1.10.0 + + + commons-io + commons-io + 2.13.0 + + + org.glassfish + javax.el + 3.0.0 + + + javax.el + javax.el-api + 3.0.0 + + + + org.eclipse.jgit + org.eclipse.jgit + 6.5.0.202303070854-r + + + + org.apache.commons + commons-collections4 + 4.4 + + + com.google.guava + guava + 30.0-jre + + + + tv.twelvetone.rjson + rjson + 1.3.1-SNAPSHOT + + + org.jetbrains.kotlin + kotlin-stdlib-jdk7 + 1.6.21 + + + + com.jayway.jsonpath + json-path + 2.7.0 + + + com.github.ben-manes.caffeine + caffeine + 3.0.5 + + + es.moki.ratelimitj + ratelimitj-core + 0.7.0 + + + com.github.spullara.mustache.java + compiler + 0.9.6 + + + + es.moki.ratelimitj + ratelimitj-redis + 0.7.0 + + + + io.projectreactor + reactor-core + 3.4.29 + + + + org.pf4j + pf4j-spring + 0.8.0 + + + + com.querydsl + querydsl-apt + 5.0.0 + + + + io.sentry + sentry-spring-boot-starter + 3.1.2 + + + + org.jgrapht + jgrapht-core + 1.5.0 + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + javax.activation + activation + 1.1.1 + + + + org.glassfish.jaxb + jaxb-runtime + 2.3.3 + + + + com.github.cloudyrock.mongock + mongock-bom + 4.3.8 + pom + import + + + + io.projectreactor.tools + blockhound + 1.0.6.RELEASE + + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + + + + io.projectreactor + reactor-test + 3.3.5.RELEASE + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring30x + 4.7.0 + + + org.mockito + mockito-inline + 5.2.0 + test + + + javax.validation + validation-api + 2.0.1.Final + + + + + + + diff --git a/server/api-service/lowcoder-domain/pom.xml b/server/api-service/lowcoder-domain/pom.xml index 2150c484a7..d7b96e0272 100644 --- a/server/api-service/lowcoder-domain/pom.xml +++ b/server/api-service/lowcoder-domain/pom.xml @@ -5,7 +5,7 @@ lowcoder-root org.lowcoder - ${revision} + ${revision} 4.0.0 @@ -186,6 +186,12 @@ es.moki.ratelimitj ratelimitj-redis + + + io.lettuce + lettuce-core + + @@ -242,6 +248,18 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + -parameters + + + + com.mysema.maven apt-maven-plugin @@ -268,9 +286,21 @@ UTF-8 + UTF-8 + 17 - 17 - 17 + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + + diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java deleted file mode 100644 index 18d73fdf5e..0000000000 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.lowcoder.domain.configurations; - -import org.pf4j.spring.SpringPluginManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class Pf4jConfiguration { - - @Bean - public SpringPluginManager pluginManager() { - return new SpringPluginManager(); - } - -} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java index a254da0f12..88bc8b7da2 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java @@ -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; } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java index 2a754bb6d7..634a8cdb13 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java @@ -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); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java index 5aefdbae69..7e7a9daf06 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java @@ -7,7 +7,8 @@ public enum MemberRole { MEMBER("member"), - ADMIN("admin"); + ADMIN("admin"), + SUPER_ADMIN("super_admin"); private static final Map VALUE_MAP; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java index 66e83f49e6..5e990485a7 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java @@ -52,6 +52,10 @@ public MemberRole getRole() { return role; } + public boolean isSuperAdmin() { + return role == MemberRole.SUPER_ADMIN; + } + public boolean isAdmin() { return role == MemberRole.ADMIN; } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java index 4dc918374e..5a4d82ec63 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java @@ -17,9 +17,9 @@ public interface OrganizationService { @PossibleEmptyMono Mono getOrganizationInEnterpriseMode(); - Mono create(Organization organization, String creatorUserId); + Mono create(Organization organization, String creatorUserId, boolean isSuperAdmin); - Mono createDefault(User user); + Mono createDefault(User user, boolean isSuperAdmin); Mono getById(String id); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java index 48e4bc6de9..9b9da95494 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java @@ -86,7 +86,7 @@ public OrganizationServiceImpl(ConfigCenter configCenter) { } @Override - public Mono createDefault(User user) { + public Mono 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 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 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 getByEnterpriseOrgId() { } @Override - public Mono create(Organization organization, String creatorId) { + public Mono create(Organization organization, String creatorId, boolean isSuperAdmin) { return Mono.defer(() -> { if (organization == null || StringUtils.isNotBlank(organization.getId())) { @@ -155,19 +155,19 @@ public Mono 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 onOrgCreated(String userId, Organization newOrg) { + private Mono 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 setOrgAdmin(String userId, Organization newOrg) { - return orgMemberService.addMember(newOrg.getId(), userId, MemberRole.ADMIN); + private Mono setOrgAdmin(String userId, Organization newOrg, boolean isSuperAdmin) { + return orgMemberService.addMember(newOrg.getId(), userId, isSuperAdmin ? MemberRole.SUPER_ADMIN : MemberRole.ADMIN); } @Override diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java index 8b05874805..3841a42b9a 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java @@ -66,7 +66,7 @@ public Mono>> 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 checkUserPermissionStatusOnResource( Mono 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) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index e7526be8de..16ac4f8e29 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -335,7 +335,7 @@ protected Mono>> buildUserDetailGroups(String userId, O Locale locale) { String orgId = orgMember.getOrgId(); Flux groups; - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { groups = groupService.getByOrgId(orgId).sort(); } else { if (withoutDynamicGroups) { diff --git a/server/api-service/lowcoder-infra/pom.xml b/server/api-service/lowcoder-infra/pom.xml index 5c34fde9c0..39a8a8640c 100644 --- a/server/api-service/lowcoder-infra/pom.xml +++ b/server/api-service/lowcoder-infra/pom.xml @@ -127,14 +127,33 @@ org.springframework.boot spring-boot-starter-webflux + + org.lowcoder.plugin + lowcoder-plugin-api + UTF-8 + UTF-8 + 17 + 17 17 + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + + diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java new file mode 100644 index 0000000000..f000e640f2 --- /dev/null +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java @@ -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 headers; + private final MultiValueMap queryParams; + + @Override + public EventType getEventType() { + return EventType.API_CALL_EVENT; + } +} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java index 018ec98942..c11381cd2d 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java @@ -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 details; + + public Map details() + { + return this.details; + } + + public static abstract class AbstractEventBuilder> + { + 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) { + } + + } + } } diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java deleted file mode 100644 index 29dd3a36c2..0000000000 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.lowcoder.infra.event; - -public interface Event { - - EventType getEventType(); -} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java deleted file mode 100644 index 52260736fd..0000000000 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.lowcoder.infra.event; - -import java.util.Locale; - -import org.lowcoder.sdk.util.LocaleUtils; - -public enum EventType { - - USER_LOGIN("EVENT_TYPE_USER_LOGIN"), - USER_LOGOUT("EVENT_TYPE_USER_LOGOUT"), - - // application - VIEW("EVENT_TYPE_VIEW"), - APPLICATION_CREATE("EVENT_TYPE_APPLICATION_CREATE"), - APPLICATION_DELETE("EVENT_TYPE_APPLICATION_DELETE"), - APPLICATION_UPDATE("EVENT_TYPE_APPLICATION_UPDATE"), - APPLICATION_MOVE("EVENT_TYPE_APPLICATION_MOVE"), - APPLICATION_RECYCLED("EVENT_TYPE_APPLICATION_RECYCLED"), - APPLICATION_RESTORE("EVENT_TYPE_APPLICATION_RESTORE"), - - // folder - FOLDER_CREATE("EVENT_TYPE_FOLDER_CREATE"), - FOLDER_DELETE("EVENT_TYPE_FOLDER_DELETE"), - FOLDER_UPDATE("EVENT_TYPE_FOLDER_UPDATE"), - - // query - QUERY_EXECUTION("EVENT_TYPE_QUERY_EXECUTION"), - // group - GROUP_CREATE("EVENT_TYPE_GROUP_CREATE"), - GROUP_UPDATE("EVENT_TYPE_GROUP_UPDATE"), - GROUP_DELETE("EVENT_TYPE_GROUP_DELETE"), - GROUP_MEMBER_ADD("EVENT_TYPE_GROUP_MEMBER_ADD"), - GROUP_MEMBER_ROLE_UPDATE("EVENT_TYPE_GROUP_MEMBER_ROLE_UPDATE"), - GROUP_MEMBER_LEAVE("EVENT_TYPE_GROUP_MEMBER_LEAVE"), - GROUP_MEMBER_REMOVE("EVENT_TYPE_GROUP_MEMBER_REMOVE"), - //system - SERVER_START_UP("EVENT_TYPE_SERVER_START_UP"), - - // data source - DATA_SOURCE_CREATE("DATA_SOURCE_CREATE"), - DATA_SOURCE_UPDATE("DATA_SOURCE_UPDATE"), - DATA_SOURCE_DELETE("DATA_SOURCE_DELETE"), - DATA_SOURCE_PERMISSION_GRANT("DATA_SOURCE_PERMISSION_GRANT"), - DATA_SOURCE_PERMISSION_UPDATE("DATA_SOURCE_PERMISSION_UPDATE"), - DATA_SOURCE_PERMISSION_DELETE("DATA_SOURCE_PERMISSION_DELETE"), - - // library query - LIBRARY_QUERY_CREATE("LIBRARY_QUERY_CREATE"), - LIBRARY_QUERY_UPDATE("LIBRARY_QUERY_UPDATE"), - LIBRARY_QUERY_DELETE("LIBRARY_QUERY_DELETE"), - LIBRARY_QUERY_PUBLISH("LIBRARY_QUERY_PUBLISH"), - ; - - private final String desc; - - EventType(String desc) { - this.desc = desc; - } - - public String getDesc(Locale locale) { - return LocaleUtils.getMessage(locale, this.desc); - } -} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java new file mode 100644 index 0000000000..5ddacf5c1a --- /dev/null +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java @@ -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; + } +} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java index 7c724b68d0..4c5471d68a 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java @@ -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; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java index 9e967e2482..99d2703cb8 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java @@ -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; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java index d2983a29c6..ab80e0cc09 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.group; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java index 4da2b51e3d..2d7caa4956 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.group; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java index ac6ef697d7..9d06c459ac 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.group; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java index bf5bcd89fc..52c17df485 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java index bd43fa4823..d35db51988 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java index 888da0aff0..6b4fef1d2e 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java index 62ea394785..785a28fc59 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java index c0e7fafd28..aa840de74a 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java @@ -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; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java index 8e0a8b073f..cf2fdd7147 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java @@ -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; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java index 0eb36e5859..f50939f949 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java @@ -83,7 +83,7 @@ public ReloadableCache build() { private void startScheduledReloadTask(ReloadableCache 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) { diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java index b45708a203..6faf54dc78 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java @@ -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 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() + ); }); } diff --git a/server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties b/server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties deleted file mode 100644 index 822e4fa853..0000000000 --- a/server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=clickHouse-plugin -plugin.class=org.lowcoder.plugin.clickhouse.ClickHousePlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties b/server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties deleted file mode 100644 index 87717ad573..0000000000 --- a/server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=es-plugin -plugin.class=org.lowcoder.plugin.es.EsPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties b/server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties deleted file mode 100644 index 7c9cd8c663..0000000000 --- a/server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=googleSheets-plugin -plugin.class=org.lowcoder.plugin.googlesheets.GoogleSheetsPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties b/server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties deleted file mode 100644 index 5d4dd5bbae..0000000000 --- a/server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=graphql-plugin -plugin.class=org.lowcoder.plugin.graphql.GraphQLPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties b/server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties deleted file mode 100644 index 545de1ba2a..0000000000 --- a/server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=lowcoder-api-plugin -plugin.class=org.lowcoder.plugin.LowcoderApiPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties b/server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties deleted file mode 100644 index a18bf7f80a..0000000000 --- a/server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=mongo-plugin -plugin.class=org.lowcoder.plugin.mongo.MongoPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties b/server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties deleted file mode 100644 index 002e438518..0000000000 --- a/server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=mssql-plugin -plugin.class=org.lowcoder.plugin.mssql.MssqlPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties b/server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties deleted file mode 100644 index 2e2c880085..0000000000 --- a/server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=mysql-plugin -plugin.class=org.lowcoder.plugin.mysql.MysqlPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties b/server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties deleted file mode 100644 index 516f2de006..0000000000 --- a/server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=oracle-plugin -plugin.class=org.lowcoder.plugin.oracle.OraclePlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml b/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml index fcd91b289b..67eb51702d 100644 --- a/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml +++ b/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml @@ -13,6 +13,9 @@ + UTF-8 + UTF-8 + 17 17 diff --git a/server/api-service/lowcoder-plugins/pom.xml b/server/api-service/lowcoder-plugins/pom.xml index 11807e458c..90512a3f50 100644 --- a/server/api-service/lowcoder-plugins/pom.xml +++ b/server/api-service/lowcoder-plugins/pom.xml @@ -79,6 +79,14 @@ + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + org.lowcoder sqlBasedPlugin diff --git a/server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties b/server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties deleted file mode 100644 index bbd887fb01..0000000000 --- a/server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=postgres-plugin -plugin.class=org.lowcoder.plugin.postgres.PostgresPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/redisPlugin/plugin.properties b/server/api-service/lowcoder-plugins/redisPlugin/plugin.properties deleted file mode 100644 index ded41c272b..0000000000 --- a/server/api-service/lowcoder-plugins/redisPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=redis-plugin -plugin.class=org.lowcoder.plugin.redis.RedisPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties b/server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties deleted file mode 100644 index 0ed0b7d875..0000000000 --- a/server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=restapi-plugin -plugin.class=org.lowcoder.plugin.restapi.RestApiPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties b/server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties deleted file mode 100644 index 70d475de9f..0000000000 --- a/server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=smtp-plugin -plugin.class=org.lowcoder.plugins.SmtpPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties b/server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties deleted file mode 100644 index 5f7dbca583..0000000000 --- a/server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=snowflake-plugin -plugin.class=org.lowcoder.plugin.snowflake.SnowflakePlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-sdk/pom.xml b/server/api-service/lowcoder-sdk/pom.xml index cbd69d47c0..22e6cb8151 100644 --- a/server/api-service/lowcoder-sdk/pom.xml +++ b/server/api-service/lowcoder-sdk/pom.xml @@ -5,7 +5,7 @@ lowcoder-root org.lowcoder - ${revision} + ${revision} 4.0.0 @@ -13,11 +13,6 @@ lowcoder-sdk - - UTF-8 - 17 - - org.springframework.boot @@ -171,4 +166,27 @@ validation-api + + + UTF-8 + UTF-8 + + 17 + + 17 + 17 + + + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + + diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java index d1fcf3ea81..8334e55621 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java @@ -44,6 +44,8 @@ public class CommonConfig { private Cookie cookie = new Cookie(); private JsExecutor jsExecutor = new JsExecutor(); private Set disallowedHosts = new HashSet<>(); + private List 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; + } } diff --git a/server/api-service/lowcoder-server/cert/README b/server/api-service/lowcoder-server/cert/README new file mode 100644 index 0000000000..0589816e81 --- /dev/null +++ b/server/api-service/lowcoder-server/cert/README @@ -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 + diff --git a/server/api-service/lowcoder-server/cert/signing.p12 b/server/api-service/lowcoder-server/cert/signing.p12 new file mode 100644 index 0000000000..2f336a1f6d Binary files /dev/null and b/server/api-service/lowcoder-server/cert/signing.p12 differ diff --git a/server/api-service/lowcoder-server/pom.xml b/server/api-service/lowcoder-server/pom.xml index cd2d2ed86f..5021d2b610 100644 --- a/server/api-service/lowcoder-server/pom.xml +++ b/server/api-service/lowcoder-server/pom.xml @@ -1,281 +1,380 @@ - - 4.0.0 - - lowcoder-root - org.lowcoder - ${revision} - + + 4.0.0 + + lowcoder-root + org.lowcoder + ${revision} + - lowcoder-server - jar + lowcoder-server + jar - lowcoder-server + lowcoder-server - - 17 - false - ${skipTests} - ${skipTests} - + + UTF-8 + UTF-8 - + 17 - - org.lowcoder - lowcoder-sdk - - - org.lowcoder - lowcoder-infra - - - org.lowcoder - lowcoder-domain - + false + ${skipTests} + ${skipTests} - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security - spring-security-config - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springdoc - springdoc-openapi-starter-webflux-ui - 2.2.0 - - - io.projectreactor.tools - blockhound - - - org.springframework.boot - spring-boot-starter-data-mongodb-reactive - + cert/signing.p12 + pkcs12 + dev + lowcoder + ${keystore.password} + ${keystore.password} + - - org.springframework.boot - spring-boot-starter-data-redis-reactive - + - - org.projectlombok - lombok - + + org.lowcoder + lowcoder-sdk + + + org.lowcoder + lowcoder-infra + + + org.lowcoder + lowcoder-domain + + + org.lowcoder.plugin + lowcoder-plugin-api + - com.google.guava - guava - - - commons-io - commons-io - - - org.springframework.boot - spring-boot-starter-actuator - - - io.micrometer - micrometer-registry-prometheus - - - io.sentry - sentry-spring-boot-starter - - - org.apache.httpcomponents - httpclient - - - org.apache.commons - commons-text - - - - org.apache.commons - commons-collections4 - - + + org.apache.commons + commons-collections4 + + + - - io.netty - netty-all - runtime - - - io.projectreactor - reactor-tools - - - org.mockito - mockito-inline - test - - - org.mockito - mockito-core - test - - - junit - junit - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - io.projectreactor - reactor-test - test - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo.spring30x - test - - - com.jayway.jsonpath - json-path - - - jakarta.servlet - jakarta.servlet-api - + + io.netty + netty-all + runtime + + + io.projectreactor + reactor-tools + + + org.mockito + mockito-inline + test + + + org.mockito + mockito-core + test + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + io.projectreactor + reactor-test + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring30x + test + + + com.jayway.jsonpath + json-path + + + jakarta.servlet + jakarta.servlet-api + - - com.auth0 - java-jwt - 4.4.0 - + + com.auth0 + java-jwt + 4.4.0 + - - it.ozimov - embedded-redis - 0.7.3 - test - - - org.apache.directory.server - apacheds-test-framework - 2.0.0.AM26 - test - - - org.junit.vintage - junit-vintage-engine - 5.9.3 - test - - - io.jsonwebtoken - jjwt-api - 0.11.5 - compile - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - compile - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - + + org.passay + passay + 1.6.3 + + + + it.ozimov + embedded-redis + 0.7.3 + test + + + org.apache.directory.server + apacheds-test-framework + 2.0.0.AM26 + test + + + org.junit.vintage + junit-vintage-engine + 5.9.3 + test + + + io.jsonwebtoken + jjwt-api + 0.11.5 + compile + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + compile + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + org.springframework + spring-aspects + + + org.springframework + spring-aspects + + + + + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + - + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + -parameters + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.lowcoder.api.ServerApplication + true + true + true + + + + + + org.apache.maven.plugins + maven-jarsigner-plugin + 3.0.0 + + + sign + + sign + + + + verify + + verify + + + + + ${keystore.type} + ${keystore.path} + ${keystore.alias} + ${keystore.store.password} + ${keystore.key.password} + + - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - 3.1.2 - - ${skipUnitTests} - - **/*IntegrationTest.java - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - ${skipIntegrationTests} - - **/*IntegrationTest.java - - - -Dpf4j.pluginsDir=../lowcoder-plugins/plugins - - - - - - integration-test - verify - - - - - - maven-antrun-plugin - - - copy-plugins-jar-for-integration-tests - pre-integration-test - - - - - - - - - - run - - - - delete-plugins-after-integration-tests-phase - post-integration-test - - - - - - - run - - - - - - + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + ${skipUnitTests} + + **/*IntegrationTest.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${skipIntegrationTests} + + **/*IntegrationTest.java + + + -Dpf4j.pluginsDir=../lowcoder-plugins/plugins + + + + + + integration-test + verify + + + + + + maven-antrun-plugin + + + copy-plugins-jar-for-integration-tests + pre-integration-test + + + + + + + + + + run + + + + delete-plugins-after-integration-tests-phase + post-integration-test + + + + + + + run + + + + + + diff --git a/server/api-service/lowcoder-server/src/main/assembly/assembly.xml b/server/api-service/lowcoder-server/src/main/assembly/assembly.xml new file mode 100644 index 0000000000..b2f6bb4204 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/assembly/assembly.xml @@ -0,0 +1,58 @@ + + + lowcoder-dist + + dir + + + true + lowcoder + + + + target/${project.artifactId}-${project.version}.jar + + application.jar + + + + + + + \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java index 3a442255b4..09c94ee062 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java @@ -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) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java index d12297b332..de398e01f3 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java @@ -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,12 +93,11 @@ public Mono> getEditingApplication(@PathVariable S .map(ResponseView::success); } - // will call the check in ApplicationApiService and ApplicationService @Override public Mono> 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); } @@ -106,7 +105,7 @@ public Mono> getPublishedApplication(@PathVariable public Mono> 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); } @@ -114,7 +113,7 @@ public Mono> getPublishedMarketPlaceApplication(@P public Mono> 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); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java index 166801e7dd..c28740cdcc 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java @@ -149,7 +149,7 @@ public Mono 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 loginOrRegister(AuthUser authUser, ServerWebExchange exchange, .then(businessEventPublisher.publishUserLoginEvent(authUser.getSource())); } - private Mono updateOrCreateUser(AuthUser authUser, boolean linkExistingUser) { + public Mono updateOrCreateUser(AuthUser authUser, boolean linkExistingUser) { if(linkExistingUser) { return sessionUserService.getVisitor() @@ -256,8 +256,8 @@ protected Connection getAuthConnection(AuthUser authUser, User user) { .get(); } - protected Mono onUserRegister(User user) { - return organizationService.createDefault(user).then(); + public Mono onUserRegister(User user, boolean isSuperAdmin) { + return organizationService.createDefault(user, isSuperAdmin).then(); } protected Mono onUserLogin(String orgId, User user, String source) { @@ -362,7 +362,7 @@ private Mono removeTokensByAuthId(String authId) { private Mono checkIfAdmin() { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.empty(); } return deferredError(BizError.NOT_AUTHORIZED, "NOT_AUTHORIZED"); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java index 1494f77862..1cbfeef9ae 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java @@ -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; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java index 1170b97612..763dccd7ce 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java @@ -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() { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java new file mode 100644 index 0000000000..d57b0ab1d7 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java @@ -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(); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java new file mode 100644 index 0000000000..a5d9df9553 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java @@ -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)r) + .reduce((o, r )-> (RouterFunction) 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; + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java new file mode 100644 index 0000000000..6f45c7e7cf --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java @@ -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 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)); + } +} \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java index 8e8c0d9be3..9bf6b41004 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java @@ -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), diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java new file mode 100644 index 0000000000..e8c2fb7653 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java @@ -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 filter(ServerWebExchange exchange, WebFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + return chain.filter(exchange) + .contextWrite(ctx -> ctx.put(ReactiveRequestContextHolder.SERVER_HTTP_REQUEST, request)); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java new file mode 100644 index 0000000000..98477a0125 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java @@ -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 SERVER_HTTP_REQUEST = ServerHttpRequest.class; + + public static Mono getRequest() { + return Mono.subscriberContext() + .map(ctx -> ctx.get(SERVER_HTTP_REQUEST)); + } +} \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java index e3e8ba1383..edbf45c9f7 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java @@ -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 diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java new file mode 100644 index 0000000000..e4107919f4 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java @@ -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 plugins = new LinkedHashMap<>(); + + @PostConstruct + private void loadPlugins() + { + registerPlugins(); + List 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 getLoadedPluginsInfo() + { + List infos = new ArrayList<>(); + for (LowcoderPlugin plugin : plugins.values()) + { + infos.add(new PluginInfo(plugin.pluginId(), plugin.description(), plugin.pluginInfo())); + } + return infos; + } + + private Map getPluginEnvironmentVariables(LowcoderPlugin plugin) + { + Map env = new HashMap<>(); + + String varPrefix = "PLUGIN_" + plugin.pluginId().toUpperCase().replaceAll("-", "_") + "_"; + MutablePropertySources propertySources = ((AbstractEnvironment) environment).getPropertySources(); + List properties = StreamSupport.stream(propertySources.spliterator(), false) + .filter(propertySource -> propertySource instanceof EnumerablePropertySource) + .map(propertySource -> ((EnumerablePropertySource) propertySource).getPropertyNames()) + .flatMap(Arrays:: 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 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 + ) {} + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java new file mode 100644 index 0000000000..ddd66ba3f5 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java @@ -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 loadPlugins() + { + List plugins = new ArrayList<>(); + + List pluginJars = findPluginsJars(); + if (pluginJars.isEmpty()) + { + return plugins; + } + + for (String pluginJar : pluginJars) + { + log.debug("Inspecting plugin jar candidate: {}", pluginJar); + List 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 findPluginsJars() + { + List 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 findPluginCandidates(Path pluginsDir) + { + List 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 loadPluginCandidates(String pluginJar) + { + List pluginCandidates = new ArrayList<>(); + + try + { + Path pluginPath = Path.of(pluginJar); + PluginClassLoader pluginClassLoader = new PluginClassLoader(pluginPath.getFileName().toString(), pluginPath); + + ServiceLoader pluginServices = ServiceLoader.load(LowcoderPlugin.class, pluginClassLoader); + if (pluginServices != null ) + { + Iterator 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; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java new file mode 100644 index 0000000000..34945cdafc --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java @@ -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 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; + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java new file mode 100644 index 0000000000..bbce19994b --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java @@ -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 env; + private LowcoderPlugin plugin; + private LowcoderServices services; + + public PluginExecutor(LowcoderPlugin plugin, Map 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()); + } + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java new file mode 100644 index 0000000000..25ed33eb44 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java @@ -0,0 +1,11 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.List; + +import org.lowcoder.plugin.api.LowcoderPlugin; + +public interface PluginLoader +{ + List loadPlugins(); + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java new file mode 100644 index 0000000000..1cd455e20b --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java @@ -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> eventListeners = new LinkedList<>(); + + @Override + public void registerEventListener(Consumer listener) + { + this.eventListeners.add(listener); + } + + @EventListener(classes = LowcoderEvent.class) + private void publishEvents(LowcoderEvent event) + { + for (Consumer listener : eventListeners) + { + listener.accept(event); + } + } + + @Override + public void registerEndpoints(String urlPrefix, List 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(); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java new file mode 100644 index 0000000000..aa75bdc179 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java @@ -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 body; + private Map> headers; + private Map>> cookies; + private Map attributes; + private Map pathVariables; + + private Map> queryParams; + private CompletableFuture 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> fromHttpCookieList(List cookies) + { + List> list = new LinkedList<>(); + + if (cookies != null) + { + cookies.stream() + .forEach(cookie -> { + list.add(new SimpleEntry(cookie.getName(), cookie.getValue())); + }); + } + + return list; + } + + + + @Override + public URI uri() { + return uri; + } + @Override + public Method method() { + return method; + } + @Override + public CompletableFuture body() { + return body; + } + @Override + public Map> headers() { + return headers; + } + @Override + public Map>> cookies() { + return cookies; + } + @Override + public Map attributes() { + return attributes; + } + @Override + public Map pathVariables() { + return pathVariables; + } + + @Override + public Map> queryParams() { + return queryParams; + } + @Override + public CompletableFuture 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; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java new file mode 100644 index 0000000000..11922c3ddc --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java @@ -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 endpoints); + List> registeredEndpoints(); +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java new file mode 100644 index 0000000000..2142528276 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java @@ -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> routes = new ArrayList<>(); + + private final ApplicationContext applicationContext; + private final DefaultListableBeanFactory beanFactory; + + @Override + public void registerEndpoints(String pluginUrlPrefix, List 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> 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 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 runPluginEndpointMethod(PluginEndpoint endpoint, EndpointExtension endpointMeta, Method handler, ServerRequest request) + { + Mono 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 routerFunction) + { + String beanName = "pluginEndpoint_" + endpointName + "_" + System.currentTimeMillis(); + ((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> routerFunction ); + log.debug("Registering RouterFunction bean definition: {}", beanName); + } + + + private Mono 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, "/")); + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java new file mode 100644 index 0000000000..42e8e56904 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java @@ -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()); + } + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java new file mode 100644 index 0000000000..6ad5090447 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java @@ -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 +{ + + @Override + public AuthorizationDecision check(Supplier authentication, MethodInvocation invocation) + { + log.info("Checking plugin endpoint invocation security for {}", invocation.getMethod().getName()); + + return new AuthorizationDecision(true); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java new file mode 100644 index 0000000000..2375676437 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java @@ -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 +{ + private final MethodSecurityExpressionHandler expressionHandler; + + public PluginAuthorizationManager() + { + this.expressionHandler = new DefaultMethodSecurityExpressionHandler(); + } + + @Override + public Mono check(Mono 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 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 createInvalidReturnTypeMono(Expression expr) + { + return Mono.error(() -> new IllegalStateException( + "Expression: '" + expr.getExpressionString() + "' must return boolean or Mono")); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java new file mode 100644 index 0000000000..aadc0c7fde --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java @@ -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 { + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java index b933a63e1f..555c0a64ba 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java @@ -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 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 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(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java index 69e4517d5b..fcb066195a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java @@ -182,6 +182,11 @@ private Mono removePermissions(String folderId) { public Mono 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 folderInfoViews = folderNode.getFolderChildren().stream().filter(FolderInfoView::isVisible).toList(); folderInfoView.setSubFolders(folderInfoViews); @@ -335,7 +340,7 @@ private Mono> buildApplicationInfoView private Mono 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 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()) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java index ae7e2f2c0c..4f07b03426 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java @@ -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; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java index b1abb505fa..17776f298a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java @@ -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; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java index 9104839d96..a96485eaef 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java @@ -18,6 +18,8 @@ public interface SessionUserService { @NonEmptyMono Mono getVisitorOrgMemberCache(); + Mono getVisitorOrgMemberCacheSilent(); + Mono getVisitorOrgMember(); Mono isAnonymousUser(); @@ -33,4 +35,6 @@ public interface SessionUserService { Mono resolveSessionUserForJWT(Claims claims, String token); Mono tokenExist(String token); + + Mono getVisitorToken(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java index 5c0b5e1fe4..75b5bec8d9 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java @@ -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 getVisitorOrgMemberCache() { .switchIfEmpty(deferredError(UNABLE_TO_FIND_VALID_ORG, "UNABLE_TO_FIND_VALID_ORG")); } + @Override + public Mono getVisitorOrgMemberCacheSilent() { + return Mono.deferContextual(contextView -> (Mono) contextView.get(CURRENT_ORG_MEMBER)) + .delayUntil(Mono::just); + } + + @Override + public Mono getVisitorToken() { + return Mono.deferContextual(contextView -> Mono.just(contextView.get(VISITOR_TOKEN))); + } + @Override public Mono getVisitorOrgMember() { return getVisitorId() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java index 968fabc2c2..99702c6f26 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java @@ -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; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java index c25c78cd45..0bd0300da1 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java @@ -58,6 +58,9 @@ public Mono getGroupMembers(String groupId, int page, Mono visitorRoleMono = groupAndOrgMemberInfo.flatMap(tuple -> { GroupMember groupMember = tuple.getT1(); OrgMember orgMember = tuple.getT2(); + if (groupMember.isSuperAdmin() || orgMember.isSuperAdmin()) { + return Mono.just(MemberRole.SUPER_ADMIN); + } if (groupMember.isAdmin() || orgMember.isAdmin()) { return Mono.just(MemberRole.ADMIN); } @@ -109,7 +112,7 @@ private boolean hasReadPermission(Tuple2 tuple) { private boolean hasManagePermission(Tuple2 tuple) { GroupMember groupMember = tuple.getT1(); OrgMember orgMember = tuple.getT2(); - return groupMember.isAdmin() || orgMember.isAdmin(); + return groupMember.isAdmin() || orgMember.isAdmin() || groupMember.isSuperAdmin() || orgMember.isSuperAdmin(); } private Mono> getGroupAndOrgMemberInfo(String groupId) { @@ -175,10 +178,16 @@ public Mono> getGroups() { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { String orgId = orgMember.getOrgId(); - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { + MemberRole memberRole; + if(orgMember.isAdmin()) { + memberRole = MemberRole.ADMIN; + } else { + memberRole = MemberRole.SUPER_ADMIN; + } return groupService.getByOrgId(orgId) .sort() - .flatMapSequential(group -> GroupView.from(group, MemberRole.ADMIN.getValue())) + .flatMapSequential(group -> GroupView.from(group, memberRole.getValue())) .collectList(); } return groupMemberService.getUserGroupMembersInOrg(orgId, orgMember.getUserId()) @@ -211,7 +220,7 @@ public Mono deleteGroup(String groupId) { public Mono create(CreateGroupRequest createGroupRequest) { return sessionUserService.getVisitorOrgMemberCache() - .filter(OrgMember::isAdmin) + .filter(orgMember -> orgMember.isAdmin() || orgMember.isSuperAdmin()) .switchIfEmpty(deferredError(BizError.NOT_AUTHORIZED, NOT_AUTHORIZED)) .delayUntil(orgMember -> bizThresholdChecker.checkMaxGroupCount(orgMember)) .flatMap(orgMember -> { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java index 6663e09cb9..ac3023f745 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java @@ -270,7 +270,7 @@ public Mono create(Organization organization) { return sessionUserService.getVisitorId() .delayUntil(userId -> bizThresholdChecker.checkMaxOrgCount(userId)) .delayUntil(__ -> checkIfSaasMode()) - .flatMap(userId -> organizationService.create(organization, userId)) + .flatMap(userId -> organizationService.create(organization, userId, false)) .map(OrgView::new); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java index fc247766a5..315c5f6f21 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java @@ -44,7 +44,7 @@ public Mono checkCurrentOrgDev() { public Mono isCurrentOrgDev() { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.just(true); } return inDevGroup(orgMember.getOrgId(), orgMember.getUserId()); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java index 42161bd5a1..252a4f8377 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java @@ -46,7 +46,7 @@ public Mono getUserDetailById(String userId) { private Mono checkAdminPermissionAndUserBelongsToCurrentOrg(String userId) { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (!orgMember.isAdmin()) { + if (!orgMember.isAdmin() && !orgMember.isSuperAdmin()) { return ofError(UNSUPPORTED_OPERATION, "BAD_REQUEST"); } return orgMemberService.getOrgMember(orgMember.getOrgId(), userId) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java new file mode 100644 index 0000000000..109d5abd5c --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java @@ -0,0 +1,90 @@ +package org.lowcoder.api.util; + +import com.google.common.hash.Hashing; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.lowcoder.api.framework.filter.ReactiveRequestContextHolder; +import org.lowcoder.api.home.SessionUserService; +import org.lowcoder.domain.organization.model.OrgMember; +import org.lowcoder.infra.event.APICallEvent; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; +import org.lowcoder.sdk.constants.Authentication; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +import static org.springframework.http.HttpHeaders.writableHttpHeaders; + +@Slf4j +@Aspect +@Component +public class ApiCallEventPublisher { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + @Autowired + private SessionUserService sessionUserService; + + @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)") + public void getMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") + public void postMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)") + public void putMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.DeleteMapping)") + public void deleteMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PatchMapping)") + public void patchMapping(){} + + @Around("(getMapping() || postMapping() || putMapping() || deleteMapping() || patchMapping())") + public Object handleAPICallEvent(ProceedingJoinPoint joinPoint) throws Throwable { + + return sessionUserService.getVisitorToken() + .zipWith(sessionUserService.getVisitorOrgMemberCacheSilent().defaultIfEmpty(OrgMember.NOT_EXIST)) + .zipWith(ReactiveRequestContextHolder.getRequest()) + .doOnNext( + tuple -> { + String token = tuple.getT1().getT1(); + OrgMember orgMember = tuple.getT1().getT2(); + ServerHttpRequest request = tuple.getT2(); + if (orgMember == OrgMember.NOT_EXIST) { + return; + } + MultiValueMap headers = writableHttpHeaders(request.getHeaders()); + headers.remove("Cookie"); + String ipAddress = headers.remove("X-Real-IP").stream().findFirst().get(); + APICallEvent event = APICallEvent.builder() + .userId(orgMember.getUserId()) + .orgId(orgMember.getOrgId()) + .type(EventType.API_CALL_EVENT) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) + .httpMethod(request.getMethod().name()) + .requestUri(request.getURI().getPath()) + .headers(headers) + .queryParams(request.getQueryParams()) + .ipAddress(ipAddress) + .build(); + event.populateDetails(); + applicationEventPublisher.publishEvent(event); + }) + .onErrorResume(throwable -> { + log.error("handleAPICallEvent error {} for: {} ", joinPoint.getSignature().getName(), EventType.API_CALL_EVENT, throwable); + return Mono.empty(); + }) + .then((Mono) joinPoint.proceed()); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java index e81f5136c3..850c33d788 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java @@ -1,15 +1,7 @@ package org.lowcoder.api.util; -import static org.lowcoder.domain.permission.model.ResourceHolder.USER; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Optional; - -import javax.annotation.Nullable; - +import com.google.common.hash.Hashing; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.lowcoder.api.application.view.ApplicationInfoView; import org.lowcoder.api.application.view.ApplicationView; @@ -32,7 +24,6 @@ import org.lowcoder.domain.user.model.User; import org.lowcoder.domain.user.service.UserService; import org.lowcoder.infra.event.ApplicationCommonEvent; -import org.lowcoder.infra.event.EventType; import org.lowcoder.infra.event.FolderCommonEvent; import org.lowcoder.infra.event.LibraryQueryEvent; import org.lowcoder.infra.event.QueryExecutionEvent; @@ -47,14 +38,20 @@ import org.lowcoder.infra.event.groupmember.GroupMemberRoleUpdateEvent; import org.lowcoder.infra.event.user.UserLoginEvent; import org.lowcoder.infra.event.user.UserLogoutEvent; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; +import org.lowcoder.sdk.constants.Authentication; import org.lowcoder.sdk.util.LocaleUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; - -import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; +import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static org.lowcoder.domain.permission.model.ResourceHolder.USER; + @Slf4j @Component public class BusinessEventPublisher { @@ -77,16 +74,24 @@ public class BusinessEventPublisher { private ResourcePermissionService resourcePermissionService; public Mono publishFolderCommonEvent(String folderId, String folderName, EventType eventType) { - return sessionUserService.getVisitorOrgMemberCache() - .doOnNext(orgMember -> { - FolderCommonEvent event = FolderCommonEvent.builder() - .id(folderId) - .name(folderName) - .userId(orgMember.getUserId()) - .orgId(orgMember.getOrgId()) - .type(eventType) - .build(); - applicationEventPublisher.publishEvent(event); + + return sessionUserService.getVisitorToken() + .zipWith(sessionUserService.getVisitorOrgMemberCache()) + .doOnNext( + tuple -> { + String token = tuple.getT1(); + OrgMember orgMember = tuple.getT2(); + FolderCommonEvent event = FolderCommonEvent.builder() + .id(folderId) + .name(folderName) + .userId(orgMember.getUserId()) + .orgId(orgMember.getOrgId()) + .type(eventType) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) + .build(); + event.populateDetails(); + applicationEventPublisher.publishEvent(event); }) .then() .onErrorResume(throwable -> { @@ -106,6 +111,7 @@ public Mono publishApplicationCommonEvent(String applicationId, @Nullable return ApplicationView.builder() .applicationInfoView(applicationInfoView) .build(); + }) .flatMap(applicationView -> publishApplicationCommonEvent(applicationView, eventType)); } @@ -126,9 +132,11 @@ public Mono publishApplicationCommonEvent(ApplicationView applicationView, .map(Optional::of) .onErrorReturn(Optional.empty()); })) + .zipWith(sessionUserService.getVisitorToken()) .doOnNext(tuple -> { - OrgMember orgMember = tuple.getT1(); - Optional optional = tuple.getT2(); + OrgMember orgMember = tuple.getT1().getT1(); + Optional optional = tuple.getT1().getT2(); + String token = tuple.getT2(); ApplicationInfoView applicationInfoView = applicationView.getApplicationInfoView(); ApplicationCommonEvent event = ApplicationCommonEvent.builder() .orgId(orgMember.getOrgId()) @@ -138,7 +146,10 @@ public Mono publishApplicationCommonEvent(ApplicationView applicationView, .type(eventType) .folderId(optional.map(Folder::getId).orElse(null)) .folderName(optional.map(Folder::getName).orElse(null)) + .isAnonymous(anonymous) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); }) .then() @@ -150,13 +161,18 @@ public Mono publishApplicationCommonEvent(ApplicationView applicationView, } public Mono publishUserLoginEvent(String source) { - return sessionUserService.getVisitorOrgMember() - .doOnNext(orgMember -> { + return sessionUserService.getVisitorOrgMember().zipWith(sessionUserService.getVisitorToken()) + .doOnNext(tuple -> { + OrgMember orgMember = tuple.getT1(); + String token = tuple.getT2(); UserLoginEvent event = UserLoginEvent.builder() .orgId(orgMember.getOrgId()) .userId(orgMember.getUserId()) .source(source) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); }) .then() @@ -168,11 +184,17 @@ public Mono publishUserLoginEvent(String source) { public Mono publishUserLogoutEvent() { return sessionUserService.getVisitorOrgMemberCache() - .doOnNext(orgMember -> { + .zipWith(sessionUserService.getVisitorToken()) + .doOnNext(tuple -> { + OrgMember orgMember = tuple.getT1(); + String token = tuple.getT2(); UserLogoutEvent event = UserLogoutEvent.builder() .orgId(orgMember.getOrgId()) .userId(orgMember.getUserId()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); }) .then() @@ -184,15 +206,19 @@ public Mono publishUserLogoutEvent() { public Mono publishGroupCreateEvent(Group group) { return sessionUserService.getVisitorOrgMemberCache() - .delayUntil(orgMember -> + .zipWith(sessionUserService.getVisitorToken()) + .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); GroupCreateEvent event = GroupCreateEvent.builder() - .orgId(orgMember.getOrgId()) - .userId(orgMember.getUserId()) + .orgId(tuple.getT1().getOrgId()) + .userId(tuple.getT1().getUserId()) .groupId(group.getId()) .groupName(group.getName(locale)) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -208,15 +234,19 @@ public Mono publishGroupUpdateEvent(boolean publish, Group previousGroup, return Mono.empty(); } return sessionUserService.getVisitorOrgMemberCache() - .delayUntil(orgMember -> + .zipWith(sessionUserService.getVisitorToken()) + .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); GroupUpdateEvent event = GroupUpdateEvent.builder() - .orgId(orgMember.getOrgId()) - .userId(orgMember.getUserId()) + .orgId(tuple.getT1().getOrgId()) + .userId(tuple.getT1().getUserId()) .groupId(previousGroup.getId()) .groupName(previousGroup.getName(locale) + " => " + newGroupName) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -232,15 +262,19 @@ public Mono publishGroupDeleteEvent(boolean publish, Group previousGroup) return Mono.empty(); } return sessionUserService.getVisitorOrgMemberCache() - .delayUntil(orgMember -> + .zipWith(sessionUserService.getVisitorToken()) + .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); GroupDeleteEvent event = GroupDeleteEvent.builder() - .orgId(orgMember.getOrgId()) - .userId(orgMember.getUserId()) + .orgId(tuple.getT1().getOrgId()) + .userId(tuple.getT1().getUserId()) .groupId(previousGroup.getId()) .groupName(previousGroup.getName(locale)) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -257,13 +291,15 @@ public Mono publishGroupMemberAddEvent(boolean publish, String groupId, Ad } return Mono.zip(groupService.getById(groupId), sessionUserService.getVisitorOrgMemberCache(), - userService.findById(addMemberRequest.getUserId())) + userService.findById(addMemberRequest.getUserId()), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); Group group = tuple.getT1(); OrgMember orgMember = tuple.getT2(); User member = tuple.getT3(); + String token = tuple.getT4(); GroupMemberAddEvent event = GroupMemberAddEvent.builder() .orgId(orgMember.getOrgId()) .userId(orgMember.getUserId()) @@ -272,7 +308,10 @@ public Mono publishGroupMemberAddEvent(boolean publish, String groupId, Ad .memberId(member.getId()) .memberName(member.getName()) .memberRole(addMemberRequest.getRole()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -290,7 +329,8 @@ public Mono publishGroupMemberRoleUpdateEvent(boolean publish, String grou } return Mono.zip(groupService.getById(groupId), sessionUserService.getVisitorOrgMemberCache(), - userService.findById(previousGroupMember.getUserId())) + userService.findById(previousGroupMember.getUserId()), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); @@ -305,7 +345,10 @@ public Mono publishGroupMemberRoleUpdateEvent(boolean publish, String grou .memberId(member.getId()) .memberName(member.getName()) .memberRole(previousGroupMember.getRole().getValue() + " => " + updateRoleRequest.getRole()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT4(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -322,7 +365,8 @@ public Mono publishGroupMemberLeaveEvent(boolean publish, GroupMember grou } return Mono.zip(groupService.getById(groupMember.getGroupId()), userService.findById(groupMember.getUserId()), - sessionUserService.getVisitorOrgMemberCache()) + sessionUserService.getVisitorOrgMemberCache(), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); @@ -337,7 +381,10 @@ public Mono publishGroupMemberLeaveEvent(boolean publish, GroupMember grou .memberId(user.getId()) .memberName(user.getName()) .memberRole(groupMember.getRole().getValue()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT4(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -354,7 +401,8 @@ public Mono publishGroupMemberRemoveEvent(boolean publish, GroupMember pre } return Mono.zip(sessionUserService.getVisitorOrgMemberCache(), groupService.getById(previousGroupMember.getGroupId()), - userService.findById(previousGroupMember.getUserId())) + userService.findById(previousGroupMember.getUserId()), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); @@ -369,7 +417,10 @@ public Mono publishGroupMemberRemoveEvent(boolean publish, GroupMember pre .memberId(member.getId()) .memberName(member.getName()) .memberRole(previousGroupMember.getRole().getValue()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT4(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -395,15 +446,19 @@ public Mono publishDatasourceEvent(String id, EventType eventType) { public Mono publishDatasourceEvent(Datasource datasource, EventType eventType) { return sessionUserService.getVisitorOrgMemberCache() - .flatMap(orgMember -> { + .zipWith(sessionUserService.getVisitorToken()) + .flatMap(tuple -> { DatasourceEvent event = DatasourceEvent.builder() .datasourceId(datasource.getId()) .name(datasource.getName()) .type(datasource.getType()) .eventType(eventType) - .userId(orgMember.getUserId()) - .orgId(orgMember.getOrgId()) + .userId(tuple.getT1().getUserId()) + .orgId(tuple.getT1().getOrgId()) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono. empty(); }) @@ -435,7 +490,9 @@ public Mono publishDatasourcePermissionEvent(String permissionId, EventTyp public Mono publishDatasourcePermissionEvent(String datasourceId, Collection userIds, Collection groupIds, String role, EventType eventType) { - return Mono.zip(sessionUserService.getVisitorOrgMemberCache(), datasourceService.getById(datasourceId)) + return Mono.zip(sessionUserService.getVisitorOrgMemberCache(), + datasourceService.getById(datasourceId), + sessionUserService.getVisitorToken()) .flatMap(tuple -> { OrgMember orgMember = tuple.getT1(); Datasource datasource = tuple.getT2(); @@ -449,7 +506,10 @@ public Mono publishDatasourcePermissionEvent(String datasourceId, .groupIds(groupIds) .role(role) .eventType(eventType) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT3(), StandardCharsets.UTF_8).toString()) .build(); + datasourcePermissionEvent.populateDetails(); applicationEventPublisher.publishEvent(datasourcePermissionEvent); return Mono. empty(); }) @@ -465,13 +525,20 @@ public Mono publishLibraryQuery(LibraryQuery libraryQuery, EventType event public Mono publishLibraryQueryEvent(String id, String name, EventType eventType) { return sessionUserService.getVisitorOrgMemberCache() - .map(orgMember -> LibraryQueryEvent.builder() - .userId(orgMember.getUserId()) - .orgId(orgMember.getOrgId()) - .id(id) - .name(name) - .eventType(eventType) - .build()) + .zipWith(sessionUserService.getVisitorToken()) + .map(tuple -> { + LibraryQueryEvent event = LibraryQueryEvent.builder() + .userId(tuple.getT1().getUserId()) + .orgId(tuple.getT1().getOrgId()) + .id(id) + .name(name) + .eventType(eventType) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) + .build(); + event.populateDetails(); + return event; + }) .doOnNext(applicationEventPublisher::publishEvent) .then() .onErrorResume(throwable -> { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java new file mode 100644 index 0000000000..57701daa82 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java @@ -0,0 +1,28 @@ +package org.lowcoder.api.util; + +import org.passay.CharacterData; +import org.passay.CharacterRule; +import org.passay.EnglishCharacterData; +import org.passay.PasswordGenerator; + +public class RandomPasswordGeneratorConfig { + + public String generatePassayPassword() { + PasswordGenerator gen = new PasswordGenerator(); + CharacterData lowerCaseChars = EnglishCharacterData.LowerCase; + CharacterRule lowerCaseRule = new CharacterRule(lowerCaseChars); + lowerCaseRule.setNumberOfCharacters(3); + + CharacterData upperCaseChars = EnglishCharacterData.UpperCase; + CharacterRule upperCaseRule = new CharacterRule(upperCaseChars); + upperCaseRule.setNumberOfCharacters(3); + + CharacterData digitChars = EnglishCharacterData.Digit; + CharacterRule digitRule = new CharacterRule(digitChars); + digitRule.setNumberOfCharacters(3); + + + String password = gen.generatePassword(10, lowerCaseRule, upperCaseRule, digitRule); + return password; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java index 6e33d075bd..5364a59319 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java @@ -18,6 +18,7 @@ import org.lowcoder.infra.config.model.ServerConfig; import org.lowcoder.infra.eventlog.EventLog; import org.lowcoder.infra.serverlog.ServerLog; +import org.lowcoder.runner.migrations.job.AddSuperAdminUser; import org.lowcoder.runner.migrations.job.AddPtmFieldsJob; import org.lowcoder.runner.migrations.job.CompleteAuthType; import org.lowcoder.runner.migrations.job.MigrateAuthConfigJob; @@ -183,7 +184,12 @@ public void addOrgIdIndexOnServerLog(MongockTemplate mongoTemplate) { ); } - @ChangeSet(order = "020", id = "add-ptm-fields-to-applications", author = "") + @ChangeSet(order = "020", id = "add-super-admin-user", author = "") + public void addSuperAdminUser(AddSuperAdminUser addSuperAdminUser) { + addSuperAdminUser.addSuperAdmin(); + } + + @ChangeSet(order = "021", id = "add-ptm-fields-to-applications", author = "") public void addPtmFieldsToApplicatgions(AddPtmFieldsJob addPtmFieldsJob) { addPtmFieldsJob.migrateApplicationsToInitPtmFields(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java new file mode 100644 index 0000000000..2aea53af3e --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java @@ -0,0 +1,6 @@ +package org.lowcoder.runner.migrations.job; + +public interface AddSuperAdminUser { + + void addSuperAdmin(); +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java new file mode 100644 index 0000000000..72e7391d77 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java @@ -0,0 +1,67 @@ +package org.lowcoder.runner.migrations.job; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl; +import org.lowcoder.api.util.RandomPasswordGeneratorConfig; +import org.lowcoder.domain.authentication.context.AuthRequestContext; +import org.lowcoder.domain.authentication.context.FormAuthRequestContext; +import org.lowcoder.domain.user.model.AuthUser; +import org.lowcoder.sdk.config.CommonConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import static org.lowcoder.domain.authentication.AuthenticationService.DEFAULT_AUTH_CONFIG; + +@RequiredArgsConstructor +@Component +@Slf4j(topic = "AddSuperAdminUserImpl") +public class AddSuperAdminUserImpl implements AddSuperAdminUser { + + private final AuthenticationApiServiceImpl authenticationApiService; + private final CommonConfig commonConfig; + + @Override + public void addSuperAdmin() { + + AuthUser authUser = formulateAuthUser(); + + authenticationApiService.updateOrCreateUser(authUser, false) + .delayUntil(user -> { + if (user.getIsNewUser()) { + return authenticationApiService.onUserRegister(user, true); + } + return Mono.empty(); + }) + .block(); + } + + private AuthUser formulateAuthUser() { + String username = formulateUserName(); + String password = formulatePassword(); + AuthRequestContext authRequestContext = new FormAuthRequestContext(username, password, true, null); + authRequestContext.setAuthConfig(DEFAULT_AUTH_CONFIG); + return AuthUser.builder() + .uid(username) + .username(username) + .authContext(authRequestContext) + .build(); + } + private String formulateUserName() { + if(commonConfig.getSuperAdmin().getUserName() != null) { + return commonConfig.getSuperAdmin().getUserName(); + } + return "admin@lowcoder.pro"; + } + + private String formulatePassword() { + if(commonConfig.getSuperAdmin().getPassword() != null) { + return commonConfig.getSuperAdmin().getPassword(); + } + RandomPasswordGeneratorConfig passGen = new RandomPasswordGeneratorConfig(); + String password = passGen.generatePassayPassword(); + log.info("PASSWORD FOR SUPER-ADMIN is: {}", password); + return password; + } +} diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index 66d022e68a..d7ad21a53f 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -10,7 +10,14 @@ spring: allow-bean-definition-overriding: true allow-circular-references: true +logging: + level: + root: info + web: debug + server: + error: + includeStacktrace: ALWAYS compression: enabled: true forward-headers-strategy: NATIVE @@ -44,6 +51,11 @@ common: block-hound-enable: false js-executor: host: http://127.0.0.1:6060 + plugin-dirs: + - /tmp/plugins + super-admin: + username: test@lowcoder.pro + password: Password@123 marketplace: private-mode: false diff --git a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml index 258833aea8..30cd78b3be 100644 --- a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml +++ b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml @@ -17,7 +17,7 @@ spring: codec: max-in-memory-size: 20MB webflux: - context-path: / + base-path: / server: compression: @@ -53,6 +53,8 @@ common: max-query-timeout: ${LOWCODER_MAX_QUERY_TIMEOUT:120} workspace: mode: ${LOWCODER_WORKSPACE_MODE:SAAS} + plugin-dirs: + - ${LOWCODER_PLUGINS_DIR:plugins} marketplace: private-mode: ${LOWCODER_MARKETPLACE_PRIVATE_MODE:true} diff --git a/server/api-service/pom.xml b/server/api-service/pom.xml index 23ffce7ad3..8ec6f774d6 100644 --- a/server/api-service/pom.xml +++ b/server/api-service/pom.xml @@ -1,335 +1,151 @@ - - - - org.springframework.boot - spring-boot-starter-parent - 3.1.1 - - - - 4.0.0 - org.lowcoder - lowcoder-root - ${revision} - pom - lowcoder-root - - - 2.3.0-SNAPSHOT - 17 - true - true - true - org.lowcoder - 1.0-SNAPSHOT - true - 2.17.0 - 17 - 17 - - - - - sonatype - https://oss.sonatype.org/content/repositories/snapshots - - - - - - - cloud - - cloud - - - true - - - - - false - src/main/java - - **/*.java - - - - src/main/resources - - **/selfhost/application*.yml - - - - - - - selfhost - - selfhost - - - - - false - src/main/java - - **/*.java - - - - src/main/resources - - **/application*.yml - - - - - - - - - - - org.codehaus.mojo - license-maven-plugin - 2.0.0 - - - maven-dependency-plugin - 3.1.2 - - - - - - - + + + 4.0.0 + org.lowcoder + lowcoder-root + pom + lowcoder-root + ${revision} + + + + 2.4.0 + 17 + true + true + true + true + + + + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + + + + + + cloud + + cloud + + + true + + + + + false + src/main/java + + **/*.java + + + + src/main/resources + + **/selfhost/application*.yml + + + + + + + selfhost + + selfhost + + + + + false + src/main/java + + **/*.java + + + + src/main/resources + + **/application*.yml + + + + + + + + + + + org.codehaus.mojo + license-maven-plugin + 2.0.0 + + + maven-dependency-plugin + + + + + + maven-assembly-plugin + 3.6.0 + + + src/assembly/bin.xml + + + + + + + + + + org.lowcoder lowcoder-sdk ${revision} - + org.lowcoder lowcoder-infra ${revision} - + org.lowcoder lowcoder-domain ${revision} - + org.lowcoder lowcoder-plugins ${revision} - + org.lowcoder lowcoder-server ${revision} - - - - org.pf4j - pf4j - 3.5.0 - - - - org.json - json - 20230227 - - - - org.projectlombok - lombok - 1.18.26 - - - - org.apache.commons - commons-text - 1.10.0 - - - commons-io - commons-io - 2.13.0 - - - org.glassfish - javax.el - 3.0.0 - - - javax.el - javax.el-api - 3.0.0 - - - - org.eclipse.jgit - org.eclipse.jgit - 6.7.0.202309050840-r - - - - org.apache.commons - commons-collections4 - 4.4 - - - com.google.guava - guava - 30.0-jre - - - - tv.twelvetone.rjson - rjson - 1.3.1-SNAPSHOT - - - org.jetbrains.kotlin - kotlin-stdlib-jdk7 - 1.6.21 - - - - com.jayway.jsonpath - json-path - 2.7.0 - - - com.github.ben-manes.caffeine - caffeine - 3.0.5 - - - es.moki.ratelimitj - ratelimitj-core - 0.7.0 - - - com.github.spullara.mustache.java - compiler - 0.9.6 - - - - es.moki.ratelimitj - ratelimitj-redis - 0.7.0 - - - - io.projectreactor - reactor-core - 3.4.29 - - - - org.pf4j - pf4j-spring - 0.8.0 - - - - com.querydsl - querydsl-apt - 5.0.0 - - - - io.sentry - sentry-spring-boot-starter - 3.1.2 - - - - org.jgrapht - jgrapht-core - 1.5.0 - - - - javax.xml.bind - jaxb-api - 2.3.1 - - - javax.activation - activation - 1.1.1 - - - - org.glassfish.jaxb - jaxb-runtime - 2.3.3 - - - - com.github.cloudyrock.mongock - mongock-bom - 4.3.8 - pom - import - - - - io.projectreactor.tools - blockhound - 1.0.6.RELEASE - - - - jakarta.servlet - jakarta.servlet-api - 6.0.0 - - - io.projectreactor - reactor-test - 3.3.5.RELEASE - - - org.apache.httpcomponents - httpclient - 4.5.14 - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo.spring30x - 4.7.0 - - - org.mockito - mockito-inline - 5.2.0 - test - - - javax.validation - validation-api - 2.0.1.Final - - - - - - lowcoder-sdk - lowcoder-infra - lowcoder-domain - lowcoder-plugins - lowcoder-server - + + + + + lowcoder-dependencies + lowcoder-sdk + lowcoder-infra + lowcoder-domain + lowcoder-plugins + lowcoder-server + distribution + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy