diff --git a/.gitignore b/.gitignore index 2e1b56cd5..81c0e6c82 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 01733833e..029be11e2 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/src/components/CompName.tsx b/client/packages/lowcoder/src/components/CompName.tsx index d6a92dc46..47d9edea2 100644 --- a/client/packages/lowcoder/src/components/CompName.tsx +++ b/client/packages/lowcoder/src/components/CompName.tsx @@ -112,6 +112,13 @@ export const CompName = (props: Iprops) => { if (compInfo.isRemote) { + items.push({ + text: trans("history.currentVersion") + ": " + compInfo.packageVersion, + onClick: () => { + + }, + }); + items.push({ text: trans("comp.menuUpgradeToLatest"), onClick: () => { diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx index cb5a7836e..58dcfe8c4 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 f8b154e42..0cb4587a6 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 a8c2c0dc1..c45c0cdc6 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 11bfceed0..4ab4add86 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 73a7d4675..a73827c2a 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 1a30f2522..50455b335 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 fc34bc723..5eacf07cf 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 51815260f..9bad13d1e 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 b5c3d701d..7659cdf72 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 e11027a69..fe6a4ad24 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 1d01266af..9c9d17cbb 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/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginCompItem.tsx b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginCompItem.tsx index 43f211c7f..e6895fd2c 100644 --- a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginCompItem.tsx +++ b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginCompItem.tsx @@ -96,7 +96,7 @@ export function PluginCompItem(props: PluginCompItemProps) {
{compMeta.name}
-
{compMeta.description || "No description."}
+
{compMeta.description || "No description."} v{packageVersion || ""}
); diff --git a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx index 427b586a8..61c0d9210 100644 --- a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx +++ b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx @@ -7,7 +7,7 @@ import { getUser } from "redux/selectors/usersSelectors"; import { BluePlusIcon, CustomModal, DocLink, TacoButton, TacoInput } from "lowcoder-design"; import { getCommonSettings } from "redux/selectors/commonSettingSelectors"; import styled from "styled-components"; -import { normalizeNpmPackage, validateNpmPackage } from "comps/utils/remote"; +import { getNpmPackageMeta, normalizeNpmPackage, validateNpmPackage } from "comps/utils/remote"; import { ComListTitle, ExtensionContentWrapper } from "../styledComponent"; import { EmptyContent } from "components/EmptyContent"; import { messageInstance } from "lowcoder-design"; @@ -37,6 +37,8 @@ export default function PluginPanel() { [commonSettings?.npmPlugins] ); + console.log("plugins: ", plugins); + const handleSetNpmPlugins = (nextNpmPlugins: string[]) => { dispatch( setCommonSettings({ diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 6f55ed0fc..c536b8a24 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 5f2e3ad2e..0f43580fe 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 044c6298e..a9fc541a9 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 000000000..65a99adef --- /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 000000000..d68b3fab4 --- /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 000000000..b6422619e --- /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 000000000..de82ddf7f --- /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 000000000..771a160c2 --- /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 + 20231013 + + + + 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 2150c484a..d7b96e027 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/asset/service/AssetServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/asset/service/AssetServiceImpl.java index 405734f47..ff802c5d7 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/asset/service/AssetServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/asset/service/AssetServiceImpl.java @@ -61,7 +61,7 @@ public Mono upload(Part filePart, int maxFileSizeKB, boolean isThumbnail) // The reason we restrict file types here is to avoid having to deal with dangerous image types such as SVG, // which can have arbitrary HTML/JS inside of them. - + final MediaType contentType = filePart.headers().getContentType(); if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) { return Mono.error(new BizException(BizError.INVALID_PARAMETER, "INCORRECT_IMAGE_TYPE")); } 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 18d73fdf5..000000000 --- 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 a254da0f1..88bc8b7da 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 2a754bb6d..634a8cdb1 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 5aefdbae6..7e7a9daf0 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 66e83f49e..5e990485a 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 4dc918374..5a4d82ec6 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 48e4bc6de..9b9da9549 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 8b0587480..3841a42b9 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 e7526be8d..16ac4f8e2 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 5c34fde9c..39a8a8640 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 000000000..f000e640f --- /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 018ec9894..c11381cd2 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 29dd3a36c..000000000 --- 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 52260736f..000000000 --- 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 000000000..5ddacf5c1 --- /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 7c724b68d..4c5471d68 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 9e967e248..99d2703cb 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 d2983a29c..ab80e0cc0 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 4da2b51e3..2d7caa495 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 ac6ef697d..9d06c459a 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 bf5bcd89f..52c17df48 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 bd43fa482..d35db5198 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 888da0aff..6b4fef1d2 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 62ea39478..785a28fc5 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 c0e7fafd2..aa840de74 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 8e0a8b073..cf2fdd714 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 0eb36e585..f50939f94 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 b45708a20..6faf54dc7 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 822e4fa85..000000000 --- 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 87717ad57..000000000 --- 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 7c9cd8c66..000000000 --- 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 5d4dd5bba..000000000 --- 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 545de1ba2..000000000 --- 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 a18bf7f80..000000000 --- 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 002e43851..000000000 --- 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 2e2c88008..000000000 --- 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 516f2de00..000000000 --- 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 fcd91b289..67eb51702 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 11807e458..90512a3f5 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 bbd887fb0..000000000 --- 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 ded41c272..000000000 --- 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 0ed0b7d87..000000000 --- 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 70d475de9..000000000 --- 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 5f7dbca58..000000000 --- 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 cbd69d47c..22e6cb815 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 d1fcf3ea8..8334e5562 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 000000000..0589816e8 --- /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 000000000..2f336a1f6 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 cd2d2ed86..5021d2b61 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 000000000..b2f6bb420 --- /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 3a442255b..09c94ee06 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 d12297b33..de398e01f 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 166801e7d..c28740cdc 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 1494f7786..1cbfeef9a 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 1170b9761..763dccd7c 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 000000000..d57b0ab1d --- /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 000000000..a5d9df955 --- /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 000000000..6f45c7e7c --- /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 8e8c0d9be..9bf6b4100 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 000000000..e8c2fb765 --- /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 000000000..98477a012 --- /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 e3e8ba138..edbf45c9f 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 000000000..e4107919f --- /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 000000000..ddd66ba3f --- /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 000000000..34945cdaf --- /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 000000000..bbce19994 --- /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 000000000..25ed33eb4 --- /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 000000000..1cd455e20 --- /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 000000000..aa75bdc17 --- /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 000000000..11922c3dd --- /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 000000000..214252827 --- /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 000000000..42e8e5690 --- /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 000000000..6ad509044 --- /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 000000000..237567643 --- /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 000000000..aadc0c7fd --- /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 b933a63e1..555c0a64b 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 69e4517d5..fcb066195 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 ae7e2f2c0..4f07b0342 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 b1abb505f..17776f298 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 9104839d9..a96485eae 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 5c0b5e1fe..75b5bec8d 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 968fabc2c..99702c6f2 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 c25c78cd4..0bd0300da 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 6663e09cb..ac3023f74 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 fc247766a..315c5f6f2 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 42161bd5a..252a4f837 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 000000000..109d5abd5 --- /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 e81f5136c..850c33d78 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 000000000..57701daa8 --- /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 6e33d075b..5364a5931 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 000000000..2aea53af3 --- /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 000000000..72e7391d7 --- /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 66d022e68..d7ad21a53 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 258833aea..30cd78b3b 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 23ffce7ad..8ec6f774d 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 +