diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 987763d5a6..de825cddf9 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -1,20 +1,24 @@ name: Backend Build on: - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - # Allows you to reuse workflows by referencing their YAML files workflow_call: jobs: build-backend: - name: Backend + name: Build + timeout-minutes: 10 runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + path: cloudbeaver + + - name: Clone Deps Repositories + uses: dbeaver/github-actions/clone-repositories@devel + with: + project_deps_path: './cloudbeaver/project.deps' - name: Set up JDK 17 uses: actions/setup-java@v4 @@ -23,34 +27,16 @@ jobs: java-version: "17" cache: maven - - uses: stCarolas/setup-maven@v5 - with: - maven-version: 3.9.0 - - - name: Give permissions - run: | - sudo chmod 777 ../ - shell: bash - - - name: Determine branches - id: determine-branch - run: | - echo "pr_branch=${{ github.head_ref }}" >> $GITHUB_ENV - echo "base_branch=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV - - - name: Clone dbeaver/dbeaver - id: clone-repo - run: | - git clone -b ${{ env.pr_branch }} https://github.com/dbeaver/dbeaver.git ../dbeaver || git clone -b ${{ env.base_branch }} https://github.com/dbeaver/dbeaver.git ../dbeaver + - uses: dbeaver/github-actions/install-maven@devel - name: Run build script run: ./build-backend.sh shell: bash - working-directory: ./deploy + working-directory: ./cloudbeaver/deploy - name: Archive build artifacts uses: actions/upload-artifact@v4 with: name: backend-build-artifacts - path: deploy/cloudbeaver + path: cloudbeaver/deploy/cloudbeaver if-no-files-found: error diff --git a/.github/workflows/backend-lint.yml b/.github/workflows/backend-lint.yml deleted file mode 100644 index 8bda3def26..0000000000 --- a/.github/workflows/backend-lint.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Backend Lint - -on: - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - - # Allows you to reuse workflows by referencing their YAML files - workflow_call: - inputs: - skip_cache: - required: false - type: string - -jobs: - lint: - name: Backend - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Checkout checkstyle config repository - uses: actions/checkout@v4 - with: - repository: dbeaver/dbeaver - path: dbeaver-config - - - name: Copy checkstyle config - run: cp dbeaver-config/dbeaver-checkstyle-config.xml ./dbeaver-checkstyle-config.xml - - - name: Remove checkstyle config directory - run: rm -rf dbeaver-config - - - uses: dbelyaev/action-checkstyle@master - with: - github_token: ${{ secrets.github_token }} - reporter: github-pr-review - filter_mode: diff_context - checkstyle_config: ./dbeaver-checkstyle-config.xml - fail_on_error: true diff --git a/.github/workflows/common-cleanup.yml b/.github/workflows/common-cleanup.yml index 8914495aee..9acd34dd17 100644 --- a/.github/workflows/common-cleanup.yml +++ b/.github/workflows/common-cleanup.yml @@ -1,57 +1,14 @@ -name: Cleanup checks +name: Cleanup on: pull_request: types: [closed] + push: + branches: + - devel jobs: delete-caches: - runs-on: ubuntu-latest - steps: - - name: Cleanup - run: | - gh extension install actions/gh-actions-cache - - echo "Fetching list of cache key" - cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) - - ## Setting this to not fail the workflow while deleting cache keys. - set +e - echo "Deleting caches..." - for cacheKey in $cacheKeysForPR - do - gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm - done - echo "Done" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge - - # delete-docker-image: - # name: Delete Docker Image - # if: github.event.pull_request.merged == true - # runs-on: ubuntu-latest - - # steps: - # - name: Check out the repository - # uses: actions/checkout@v4 - - # - name: Set up Docker Buildx - # uses: docker/setup-buildx-action@v3 - - # - name: Determine Docker Image Tag - # run: | - # REPO_NAME=$(basename ${{ github.repository }}) - # IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/$REPO_NAME - # BRANCH_NAME=${{ github.event.pull_request.head.ref }} - # TAG_NAME=$(echo $BRANCH_NAME | sed 's/[^a-zA-Z0-9._-]/-/g') - # echo "image=$IMAGE_NAME:$TAG_NAME" >> $GITHUB_ENV - - # - name: Log in to GitHub Container Registry - # run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin - - # - name: Delete Docker Image - # run: | - # docker rmi ${{ env.image }} - # echo "Deleted image: ${{ env.image }}" + name: Cleanup + uses: dbeaver/dbeaver-common/.github/workflows/cleanup-caches.yml@devel + secrets: inherit diff --git a/.github/workflows/frontend-build.yml b/.github/workflows/frontend-build.yml index 11822894bb..b5495fe474 100644 --- a/.github/workflows/frontend-build.yml +++ b/.github/workflows/frontend-build.yml @@ -1,9 +1,6 @@ name: Frontend Build on: - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - # Allows you to reuse workflows by referencing their YAML files workflow_call: outputs: @@ -20,8 +17,11 @@ on: jobs: frontend-build: - name: Frontend + name: Build runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read outputs: build-status: ${{ steps.build.outcome }} @@ -34,6 +34,7 @@ jobs: steps: - uses: actions/checkout@v4 + - run: corepack enable - uses: actions/setup-node@v4 with: node-version: "20" @@ -58,30 +59,14 @@ jobs: - name: yarn clean if: env.skip_cache == 'true' - uses: borales/actions-yarn@v5 - with: - dir: webapp - cmd: clean - - - name: yarn install --frozen-lockfile - uses: borales/actions-yarn@v5 - with: - dir: webapp - cmd: install + run: yarn clear - - name: build - id: build - uses: borales/actions-yarn@v5 - with: - dir: webapp/packages/product-default - cmd: bundle + - run: yarn install --immutable + + - run: yarn bundle + working-directory: ./webapp/packages/product-default - - name: test - id: test - uses: borales/actions-yarn@v5 - with: - dir: webapp - cmd: test + - run: yarn test - name: Archive build artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/frontend-lint.yml b/.github/workflows/frontend-lint.yml index 3f22b3df53..0d969da4a3 100644 --- a/.github/workflows/frontend-lint.yml +++ b/.github/workflows/frontend-lint.yml @@ -1,20 +1,25 @@ name: Frontend Lint on: - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - # Allows you to reuse workflows by referencing their YAML files workflow_call: jobs: lint: - name: Frontend + name: Lint runs-on: ubuntu-latest - + timeout-minutes: 5 + permissions: + contents: read + + defaults: + run: + working-directory: ./webapp + steps: - uses: actions/checkout@v4 + - run: corepack enable - uses: actions/setup-node@v4 with: node-version: "20" @@ -29,17 +34,12 @@ jobs: restore-keys: | ${{ runner.os }}-node_modules- - - name: yarn install --frozen-lockfile - uses: borales/actions-yarn@v5 - with: - dir: webapp - cmd: install - - - name: Lint - uses: reviewdog/action-eslint@v1 - with: - reporter: github-pr-review - filter_mode: file - workdir: webapp - fail_on_error: true - eslint_flags: "--ext .ts,.tsx" + - run: | + yarn install --immutable + git fetch origin "${{ github.base_ref }}" --depth=1 + FILES=$(git diff --name-only 'origin/${{ github.base_ref }}' ${{ github.sha }} -- . | sed 's|^webapp/||') + if [ -n "$FILES" ]; then + yarn lint --pass-on-no-patterns --no-error-on-unmatched-pattern $FILES + else + echo "No files to lint" + fi diff --git a/.github/workflows/common.yml b/.github/workflows/push-pr-devel.yml similarity index 58% rename from .github/workflows/common.yml rename to .github/workflows/push-pr-devel.yml index 2d1ba04acb..ddcc1e14f8 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/push-pr-devel.yml @@ -1,40 +1,38 @@ -name: Check +name: CI on: - push: - branches: - - devel pull_request: - branches: - - devel + types: + - opened + - synchronize + - reopened + push: + branches: [devel] - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - inputs: - skip_cache: - description: "Skip cache restoration" - required: false - default: "false" +concurrency: + group: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'push-pr-devel' }} + cancel-in-progress: true jobs: - call-backend-build: - name: Build + build-server: + name: Server uses: ./.github/workflows/backend-build.yml + secrets: inherit - call-frontend-build: - name: Build + build-frontend: + name: Frontend uses: ./.github/workflows/frontend-build.yml - with: - skip_cache: ${{ github.event.inputs.skip_cache }} + secrets: inherit - call-frontend-lint: - name: Lint - needs: call-frontend-build - uses: ./.github/workflows/frontend-lint.yml + lint-server: + name: Server + uses: dbeaver/dbeaver-common/.github/workflows/java-checkstyle.yml@devel + secrets: inherit - call-backend-lint: - name: Lint - uses: ./.github/workflows/backend-lint.yml + lint-frontend: + name: Frontend + uses: ./.github/workflows/frontend-lint.yml + secrets: inherit # call-frontend-tests: # name: Frontend Unit Tests diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml deleted file mode 100644 index 9ccd4113b7..0000000000 --- a/.github/workflows/validation.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: validation - -on: - pull_request: - branches: - - devel - types: - - opened - - synchronize - - reopened - - edited - - ready_for_review - - labeled - -jobs: - commit-message: - uses: dbeaver/dbeaver/.github/workflows/reused-commit-msgs-validator.yml@devel diff --git a/.gitignore b/.gitignore index 8906c71da1..93b1d9e73b 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ server/test/io.cloudbeaver.test.platform/workspace/.data/ .classpath .settings/ +## Eclipse PDE +*.product.launch + workspace-dev-ce/ deploy/cloudbeaver server/**/target diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1db4798379..b0cfd8d15d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -15,7 +15,6 @@ "streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker-russian", "syler.sass-indented", - "VisualStudioExptTeam.intellicode-api-usage-examples", "VisualStudioExptTeam.vscodeintellicode", "yzhang.markdown-all-in-one", "GraphQL.vscode-graphql-syntax", diff --git a/.vscode/launch.json b/.vscode/launch.json index 69ae494895..70779c5a17 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,7 @@ { "type": "chrome", "request": "launch", - "name": "Chrome", + "name": "CloudBeaver CE", "url": "http://localhost:8080", "webRoot": "${workspaceFolder}/..", "outFiles": [ @@ -17,75 +17,60 @@ }, { "type": "java", - "name": "CloudBeaver CE", - "cwd": "${workspaceFolder}/workspace-dev-ce", + "name": "CloudBeaver CE Server", + "cwd": "${workspaceFolder}/../opt/cbce", "request": "launch", "mainClass": "org.jkiss.dbeaver.launcher.DBeaverLauncher", - "windows": { - "type": "java", - "name": "CloudBeaver CE", - "request": "launch", - "mainClass": "org.jkiss.dbeaver.launcher.DBeaverLauncher", - "args": [ - "-product", - "io.cloudbeaver.product.ce.product", - "-configuration", - "file:${workspaceFolder}/../dbeaver-workspace/products/CloudbeaverServer.product/", - "-dev", - "file:${workspaceFolder}/../dbeaver-workspace/products/CloudbeaverServer.product/dev.properties", - "-os", - "win32", - "-ws", - "win32", - "-arch", - "x86_64", - "-nl", - "en", - "-showsplash", - "-web-config", - "conf/cloudbeaver.conf" - ], - "vmArgs": [ - "-XX:+IgnoreUnrecognizedVMOptions", - "--add-modules=ALL-SYSTEM", - "-Xms64m", - "-Xmx1024m", - "-Declipse.pde.launch=true" - ] - }, - "osx": { - "type": "java", - "name": "CloudBeaver CE", - "request": "launch", - "mainClass": "org.jkiss.dbeaver.launcher.DBeaverLauncher", - "args": [ - "-product", - "io.cloudbeaver.product.ce.product", - "-configuration", - "file:${workspaceFolder}/../dbeaver-workspace/products/CloudbeaverServer.product/", - "-dev", - "file:${workspaceFolder}/../dbeaver-workspace/products/CloudbeaverServer.product/dev.properties", - "-os", - "macosx", - "-ws", - "cocoa", - "-arch", - "aarch64", - "-nl", - "en", - "-showsplash", - "-web-config", - "conf/cloudbeaver.conf" - ], - "vmArgs": [ - "-XX:+IgnoreUnrecognizedVMOptions", - "--add-modules=ALL-SYSTEM", - "-Xms64m", - "-Xmx1024m", - "-Declipse.pde.launch=true", - "-XstartOnFirstThread" - ] - } + "args": [ + "-product", + "io.cloudbeaver.product.ce.product", + "-configuration", + "file:${workspaceFolder}/../dbeaver-workspace/products/CloudbeaverServer.product/", + "-dev", + "file:${workspaceFolder}/../dbeaver-workspace/products/CloudbeaverServer.product/dev.properties", + "-nl", + "en", + "-web-config", + "conf/cloudbeaver.conf", + "-registryMultiLanguage" + ], + // "windows": { + // "args": ["-os", "win32", "-ws", "win32", "-arch", "x86_64"] + // }, + // "osx": { + // "args": ["-os", "macosx", "-ws", "cocoa", "-arch", "aarch64"] + // }, + + "vmArgs": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "-Xms64m", + "-Xmx1024m", + "-Declipse.pde.launch=true", + "-XstartOnFirstThread", + "-Dfile.encoding=UTF-8", + "--add-modules=ALL-SYSTEM", + "--add-opens=java.base/java.io=ALL-UNNAMED", + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens=java.base/java.net=ALL-UNNAMED", + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/java.nio.charset=ALL-UNNAMED", + "--add-opens=java.base/java.text=ALL-UNNAMED", + "--add-opens=java.base/java.time=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED", + "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED", + "--add-opens=java.base/jdk.internal.vm=ALL-UNNAMED", + "--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", + "--add-opens=java.base/sun.security.ssl=ALL-UNNAMED", + "--add-opens=java.base/sun.security.action=ALL-UNNAMED", + "--add-opens=java.base/sun.security.util=ALL-UNNAMED", + "--add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED", + "--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED", + "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED", + "--add-opens=java.sql/java.sql=ALL-UNNAMED" + ] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 21924f722a..a039fe497d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,6 +46,6 @@ "*.css": "postcss" }, - "java.checkstyle.configuration": "${workspaceFolder}/../dbeaver/dbeaver-checkstyle-config.xml", + "java.checkstyle.configuration": "${workspaceFolder}/../dbeaver-common/.github/dbeaver-checkstyle-config.xml", "java.checkstyle.version": "10.12.0" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 96fcdd0661..3fef97a54a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -55,10 +55,10 @@ "label": "Build CE", "type": "shell", "windows": { - "command": "./build-sqlite.bat" + "command": "./build.bat" }, "osx": { - "command": "./build-sqlite.sh" + "command": "./build.sh" }, "options": { "cwd": "${workspaceFolder}/deploy" diff --git a/README.md b/README.md index de208fa04e..cce2fd8ea8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ -# CloudBeaver Community + - +# CloudBeaver Community Cloud Database Manager - Community Edition. -CloudBeaver is a web server which provides rich web interface. Server itself is a Java application, web part is written on TypeScript and React. +CloudBeaver is a web server that provides a rich web interface. The server itself is a Java application, and the web part is written in TypeScript and React. It is free to use and open-source (licensed under [Apache 2](https://github.com/dbeaver/cloudbeaver/blob/devel/LICENSE) license). -See out [WIKI](https://github.com/dbeaver/cloudbeaver/wiki) for more details. +See our [WIKI](https://github.com/dbeaver/cloudbeaver/wiki) for more details. -![](https://github.com/dbeaver/cloudbeaver/wiki/images/demo_screenshot_1.png) + + + + ## Run in Docker @@ -16,71 +19,56 @@ See out [WIKI](https://github.com/dbeaver/cloudbeaver/wiki) for more details. ## Demo server -You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io +You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io [Database access instructions](https://github.com/dbeaver/cloudbeaver/wiki/Demo-Server) ## Changelog -### 24.1.4. 2024-08-05 -- Redesigned administration navigation panel - now it is more compact and clear; -- Added the ability to close editor tabs with the middle mouse button; -- Improved display of the long error messages in the SQL Editor and Log viewer; -- SQL Editor auto-completion was enhanced to get column, table, and function names faster. - -### 24.1.3. 2024-07-22 -- Data Editor: - - Enhanced IPv6 and DateTime32 data representation for Clickhouse; - - Data editing was fixed for DuckDB; -- SQL Editor: - - We changed the save script icon to a floppy disk for better recognition; - - The 'Use long objects names' preference behavior was enhanced for auto-completion actions; -- DDL generation for Oracle Tablespaces was added (thanks to @pandya09); -- Many minor bug fixes, enhancements, and improvements have been made. +### 24.2.2. 2024-10-07 +- Schemas were added to the SQL autocompletion for PostgreSQL, H2, and SQL Server; +- CloudBeaver can now correctly display negative dates for MySQL database; +- A search option was added for preferences in the Administration part; +- Keyboard navigation has been enhanced. You can now use the arrow keys to move through navigator tree elements and the tab key to switch between editors tabs; +- Sample SQLite database was removed. -### 24.1.2. 2024-07-08 -- Added the ability to change the default commit mode for each connection separately; -- Added additional notifications about restricted operations; -- Improved application behavior when closing a connection - open editors won't be closed on disconnect; -- Added the "Keep alive" setting for Db2 LUW and IMB i, Apache Kyuubi, Clickhouse, Firebird and Trino; -- Fixed the dollar-quoted string parsing in the SQL editor for PostgreSQL; -- Many minor bug fixes, enhancements, and improvements have been made. - -### 24.1.1. 2024-06-24 -- Unauthorized access vulnerability was fixed; -- French language support was added (thanks to @matthieukhl); -- Updated Firebird driver to version 5.0.4; -- Many minor bug fixes, enhancements, and improvements have been made. +### 24.2.1. 2024-09-23 +- Chinese localization has been improved (thanks to [cashlifei](https://github.com/cashlifei)); +- Environment variables configuration has been improved - now you can configure more variables on the initial stage of the Docker setup; +- SQL Server driver has been updated to version 12.8.0. -### 24.1.0. 2024-06-03 -### Changes since 24.0.0: +### 24.2.0. 2024-09-02 +### Changes since 24.1.0: - General: - - Added the ability to back up the internal database before schema migration (for H2 and PostgreSQL) - - The process of application update has improved - you can track the application update process now; - - Added the ability for users to configure personal settings for the interface, SQL editor, and data viewer through the settings panel - - All popup dialogs became available for screen readers, including JAWS, to improve the experience for users with disabilities; -- User authorization: - - Security for unauthorized access enhanced; - - Added LDAP authentication; -- Data viewer and SQL editor: - - Added support for manual and automatic modes for committing changes to the database - - Large text values (more than 100 Kb) are now automatically opened in the Value panel; - - Row count calculation in the grid can be canceled for Data Editor and SQL Editor; - - Added the ability to set null values for BLOB and GIS data via the cell's context menu in the table; - - Added spatial data visualization for DuckDB; - - Aliases autocompletion fixed for DuckDB; - - Procedure creation query recognition fixed for DB2i. -- Connection settings: - - Implemented support for utilizing environment variables within connection configurations; -- Data transfer: - - Added the ability to import data to the database from CSV file; - - Added the ability to select a case for column names for export to CSV; + - French language support was added (thanks to [matthieukhl](https://github.com/matthieukhl)) + - Added the ability to close editor tabs with the middle mouse button + - Added right-click support to open the context menu in the Metadata Editor + - The list of forbidden characters for naming and renaming resource manager files has been updated, and now it includes the following characters: / : " \ ' <> | ? * + - Application cookies security was improved +- Authentication: + - Improved LDAP authentication: added the ability to filter users via service account parameters and ability to specify custom unique user identifiers +- Data Editor: + - Added additional notifications about the restricted operations + - Enhanced IPv6 and DateTime32 data representation for Clickhouse + - Data editing was fixed for DuckDB +- SQL Editor: + - SQL Editor auto-completion was enhanced to get column, table, and function names faster + - Fixed the dollar-quoted string parsing in the SQL Editor for PostgreSQL + - Improved display of the long error messages in the SQL Editor and Log viewer + - Changed the save script icon to a floppy disk for better recognition + - Improved application behavior when closing a connection - open editors won't be closed on disconnect +- Administration: + - Redesigned administration navigation panel - now it is more compact and clear + - Added the ability to change the default commit mode for each connection separately + - Added the ability to configure the server property rootURI parameter (thanks to [arioko](https://github.com/arioko)) - Databases: - - Added a new Apache Kyuubi driver (thanks to @pan3793); - - Enhanced security for connection through H2 driver; - - DuckDB driver updated to version 0.10.2; - - Oracle driver updated to version 23.2.0.0; - - SQLite driver updated to version 3.44.1.0; - - Clickhouse driver updated to version 0.6.0-patch2; - - Trino driver updated to version 438 (thanks to @alaturqua). + - Added the "Keep alive" setting for Db2 LUW and IMB i, Apache Kyuubi, Clickhouse, Firebird, and Trino + - Updated Firebird driver to version 5.0.4 + - DDL generation for Oracle Tablespaces was added (thanks to [pandya09](https://github.com/pandya09)) + +## Contribution +As a community-driven open-source project, we warmly welcome contributions through GitHub pull requests. +[We are happy to reward](https://dbeaver.com/help-dbeaver/) our most active contributors every major sprint. +The most significant contribution to our code for the major release 24.2.0 was made by: +1. [matthieukhl](https://github.com/matthieukhl) diff --git a/SECURITY.md b/SECURITY.md index 9d72644c1e..094ed1ba00 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,9 +9,10 @@ currently being supported with security updates. | ------- | --------- | | 22.x | yes | | 23.x | yes | +| 24.x | yes | ## Reporting a Vulnerability -Please report (suspected) security vulnerabilities to devops@dbeaver.com. -You will receive a response from us within 48 hours. -If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days. +Please report (suspected) security vulnerabilities to devops@dbeaver.com. +You will receive a response from us within 48 hours. +If the issue is confirmed, we will release a patch as soon as possible, depending on complexity, but historically, within a few days. diff --git a/config/GlobalConfiguration/.dbeaver/data-sources.json b/config/GlobalConfiguration/.dbeaver/data-sources.json new file mode 100644 index 0000000000..a5f18e204f --- /dev/null +++ b/config/GlobalConfiguration/.dbeaver/data-sources.json @@ -0,0 +1,4 @@ +{ + "folders": {}, + "connections": {} +} diff --git a/config/sample-databases/DefaultConfiguration/GlobalConfiguration/.dbeaver/provided-connections.json b/config/GlobalConfiguration/.dbeaver/provided-connections.json similarity index 100% rename from config/sample-databases/DefaultConfiguration/GlobalConfiguration/.dbeaver/provided-connections.json rename to config/GlobalConfiguration/.dbeaver/provided-connections.json diff --git a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf b/config/core/cloudbeaver.conf similarity index 61% rename from config/sample-databases/DefaultConfiguration/cloudbeaver.conf rename to config/core/cloudbeaver.conf index a703497fcb..8f6ec86e77 100644 --- a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf +++ b/config/core/cloudbeaver.conf @@ -1,14 +1,14 @@ { server: { - serverPort: 8978, + serverPort: "${CLOUDBEAVER_SERVICE_PORT:8978}", - workspaceLocation: "workspace", + workspaceLocation: "${CLOUDBEAVER_WORKSPACE_LOCATION:workspace}", contentRoot: "web", driversLocation: "drivers", sslConfigurationPath:"${CLOUDBEAVER_SSL_CONF_PATH:workspace/.data/ssl-config.xml}", - rootURI: "/", + rootURI: "${CLOUDBEAVER_ROOT_URI:/}", serviceURI: "/api/", productSettings: { @@ -21,15 +21,13 @@ plugin.sql-editor.maxFileSize: 10240, plugin.log-viewer.disabled: false, plugin.log-viewer.logBatchSize: 1000, - plugin.log-viewer.maxFailedRequests: 3, plugin.log-viewer.maxLogRecords: 2000, - plugin.log-viewer.refreshTimeout: 3000, sql.proposals.insert.table.alias: PLAIN }, - expireSessionAfterPeriod: 1800000, + expireSessionAfterPeriod: "${CLOUDBEAVER_EXPIRE_SESSION_AFTER_PERIOD:1800000}", - develMode: false, + develMode: "${CLOUDBEAVER_DEVEL_MODE:false}", enableSecurityManager: false, @@ -64,29 +62,28 @@ }, app: { - anonymousAccessEnabled: true, - anonymousUserRole: "user", - defaultUserTeam: "user", - grantConnectionsAccessToAnonymousTeam: false, - supportsCustomConnections: false, - showReadOnlyConnectionInfo: false, + anonymousAccessEnabled: "${CLOUDBEAVER_APP_ANONYMOUS_ACCESS_ENABLED:true}", + anonymousUserRole: user, + defaultUserTeam: "${CLOUDBEAVER_APP_DEFAULT_USER_TEAM:user}", + grantConnectionsAccessToAnonymousTeam: "${CLOUDBEAVER_APP_GRANT_CONNECTIONS_ACCESS_TO_ANONYMOUS_TEAM:false}", + supportsCustomConnections: "${CLOUDBEAVER_APP_SUPPORTS_CUSTOM_CONNECTIONS:false}", + showReadOnlyConnectionInfo: "${CLOUDBEAVER_APP_READ_ONLY_CONNECTION_INFO:false}", systemVariablesResolvingEnabled: "${CLOUDBEAVER_SYSTEM_VARIABLES_RESOLVING_ENABLED:false}", - forwardProxy: false, + forwardProxy: "${CLOUDBEAVER_APP_FORWARD_PROXY:false}", - publicCredentialsSaveEnabled: true, - adminCredentialsSaveEnabled: true, + publicCredentialsSaveEnabled: "${CLOUDBEAVER_APP_PUBLIC_CREDENTIALS_SAVE_ENABLED:true}", + adminCredentialsSaveEnabled: "${CLOUDBEAVER_APP_ADMIN_CREDENTIALS_SAVE_ENABLED:true}", - resourceManagerEnabled: true, + resourceManagerEnabled: "${CLOUDBEAVER_APP_RESOURCE_MANAGER_ENABLED:true}", resourceQuotas: { - dataExportFileSizeLimit: 10000000, - resourceManagerFileSizeLimit: 500000, - sqlMaxRunningQueries: 100, - sqlResultSetRowsLimit: 100000, - sqlResultSetMemoryLimit: 2000000, - sqlTextPreviewMaxLength: 4096, - sqlBinaryPreviewMaxLength: 261120 + dataExportFileSizeLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_DATA_EXPORT_FILE_SIZE_LIMIT:10000000}", + resourceManagerFileSizeLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_RESOURCE_MANAGER_FILE_SIZE_LIMIT:500000}", + sqlMaxRunningQueries: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_MAX_RUNNING_QUERIES:100}", + sqlResultSetRowsLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_RESULT_SET_ROWS_LIMIT:100000}", + sqlTextPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_TEXT_PREVIEW_MAX_LENGTH:4096}", + sqlBinaryPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_BINARY_PREVIEW_MAX_LENGTH:261120}" }, enabledAuthProviders: [ "local" diff --git a/config/sample-databases/DefaultConfiguration/GlobalConfiguration/.dbeaver/data-sources.json b/config/sample-databases/DefaultConfiguration/GlobalConfiguration/.dbeaver/data-sources.json deleted file mode 100644 index c954ec82d9..0000000000 --- a/config/sample-databases/DefaultConfiguration/GlobalConfiguration/.dbeaver/data-sources.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "folders": {}, - "connections": { - "postgresql-template-1": { - "provider": "postgresql", - "driver": "postgres-jdbc", - "name": "PostgreSQL (Template)", - "save-password": false, - "show-system-objects": false, - "read-only": true, - "template": true, - "configuration": { - "host": "localhost", - "port": "5432", - "database": "postgres", - "url": "jdbc:postgresql://localhost:5432/postgres", - "type": "dev", - "provider-properties": { - "@dbeaver-show-non-default-db@": "false" - } - } - } - } -} diff --git a/config/sample-databases/README.md b/config/sample-databases/README.md deleted file mode 100644 index a6fe87c936..0000000000 --- a/config/sample-databases/README.md +++ /dev/null @@ -1,3 +0,0 @@ -### Sample databases - -Provides access to locally deployed SQLite sample database diff --git a/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/data-sources.json b/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/data-sources.json deleted file mode 100644 index 27dbbe907a..0000000000 --- a/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/data-sources.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "folders": {}, - "connections": { - "sqlite_xerial-sample-database": { - "provider": "generic", - "driver": "sqlite_jdbc", - "name": "SQLite - Chinook (Sample)", - "save-password": true, - "navigator-show-only-entities": false, - "navigator-hide-folders": false, - "read-only": false, - "template": false, - "configuration": { - "database": "${application.path}/../samples/db/Chinook.sqlitedb", - "type": "dev", - "auth-model": "native" - } - }, - "postgresql-template-1": { - "provider": "postgresql", - "driver": "postgres-jdbc", - "name": "PostgreSQL (Template)", - "save-password": false, - "show-system-objects": false, - "read-only": true, - "template": true, - "configuration": { - "host": "localhost", - "port": "5432", - "database": "postgres", - "url": "jdbc:postgresql://localhost:5432/postgres", - "type": "dev", - "provider-properties": { - "@dbeaver-show-non-default-db@": "false" - } - } - } - } -} diff --git a/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/provided-connections.json b/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/provided-connections.json deleted file mode 100644 index edc5802a0a..0000000000 --- a/config/sample-databases/SQLiteConfiguration/GlobalConfiguration/.dbeaver/provided-connections.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "folders": {}, - "connections": {} -} diff --git a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf b/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf deleted file mode 100644 index 7c9bec251d..0000000000 --- a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf +++ /dev/null @@ -1,102 +0,0 @@ -{ - server: { - serverPort: 8978, - - workspaceLocation: "workspace", - contentRoot: "web", - driversLocation: "drivers", - - rootURI: "/", - serviceURI: "/api/", - - productSettings: { - # Global properties - core.theming.theme: 'light', - core.localization.localization: 'en', - plugin.sql-editor.autoSave: true, - plugin.sql-editor.disabled: false, - # max size of the file that can be uploaded to the editor (in kilobytes) - plugin.sql-editor.maxFileSize: 10240, - plugin.log-viewer.disabled: false, - plugin.log-viewer.logBatchSize: 1000, - plugin.log-viewer.maxFailedRequests: 3, - plugin.log-viewer.maxLogRecords: 2000, - plugin.log-viewer.refreshTimeout: 3000, - sql.proposals.insert.table.alias: PLAIN - }, - - expireSessionAfterPeriod: 1800000, - - develMode: false, - - enableSecurityManager: false, - - database: { - driver: "${CLOUDBEAVER_DB_DRIVER:h2_embedded_v2}", - url: "${CLOUDBEAVER_DB_URL:jdbc:h2:${workspace}/.data/cb.h2v2.dat}", - schema: "${CLOUDBEAVER_DB_SCHEMA:''}", - user: "${CLOUDBEAVER_DB_USER:''}", - password: "${CLOUDBEAVER_DB_PASSWORD:''}", - initialDataConfiguration: "${CLOUDBEAVER_DB_INITIAL_DATA:conf/initial-data.conf}", - pool: { - minIdleConnections: "${CLOUDBEAVER_DB_MIN_IDLE_CONNECTIONS:4}", - maxIdleConnections: "${CLOUDBEAVER_DB_MAX_IDLE_CONNECTIONS:10}", - maxConnections: "${CLOUDBEAVER_DB_MAX_CONNECTIONS:100}", - validationQuery: "${CLOUDBEAVER_DB_VALIDATION_QUERY:SELECT 1}" - }, - backupEnabled: "${CLOUDBEAVER_DB_BACKUP_ENABLED:true}" - }, - sm: { - enableBruteForceProtection: "${CLOUDBEAVER_BRUTE_FORCE_PROTECTION_ENABLED:true}", - maxFailedLogin: "${CLOUDBEAVER_MAX_FAILED_LOGINS:10}", - minimumLoginTimeout: "${CLOUDBEAVER_MINIMUM_LOGIN_TIMEOUT:1}", - blockLoginPeriod: "${CLOUDBEAVER_BLOCK_PERIOD:300}", - passwordPolicy: { - minLength: "${CLOUDBEAVER_POLICY_MIN_LENGTH:8}", - requireMixedCase: "${CLOUDBEAVER_POLICY_REQUIRE_MIXED_CASE:true}", - minNumberCount: "${CLOUDBEAVER_POLICY_MIN_NUMBER_COUNT:1}", - minSymbolCount: "${CLOUDBEAVER_POLICY_MIN_SYMBOL_COUNT:0}" - } - } - - }, - app: { - anonymousAccessEnabled: true, - anonymousUserRole: "user", - grantConnectionsAccessToAnonymousTeam: false, - supportsCustomConnections: false, - showReadOnlyConnectionInfo: false, - systemVariablesResolvingEnabled: "${CLOUDBEAVER_SYSTEM_VARIABLES_RESOLVING_ENABLED:false}", - - forwardProxy: false, - - publicCredentialsSaveEnabled: true, - adminCredentialsSaveEnabled: true, - - resourceManagerEnabled: true, - - resourceQuotas: { - dataExportFileSizeLimit: 10000000, - resourceManagerFileSizeLimit: 500000, - sqlMaxRunningQueries: 100, - sqlResultSetRowsLimit: 100000, - sqlResultSetMemoryLimit: 2000000, - sqlTextPreviewMaxLength: 4096, - sqlBinaryPreviewMaxLength: 261120 - }, - enabledAuthProviders: [ - "local" - ], - - disabledDrivers: [ - "h2:h2_embedded", - "h2:h2_embedded_v2", - "clickhouse:yandex_clickhouse" - ], - disabledBetaFeatures: [ - - ] - - } - -} diff --git a/config/sample-databases/db/Chinook.sqlitedb b/config/sample-databases/db/Chinook.sqlitedb deleted file mode 100644 index 7eb421570e..0000000000 Binary files a/config/sample-databases/db/Chinook.sqlitedb and /dev/null differ diff --git a/config/sample-databases/db/README b/config/sample-databases/db/README deleted file mode 100644 index 0882204624..0000000000 --- a/config/sample-databases/db/README +++ /dev/null @@ -1,6 +0,0 @@ - Chinook Database - Version 1.3 - Script: Chinook_Sqlite.sql - Description: Creates and populates the Chinook database. - DB Server: Sqlite - Author: Luis Rocha - License: http://www.codeplex.com/ChinookDatabase/license diff --git a/deploy/build-backend.sh b/deploy/build-backend.sh index 051ed0a798..5b1b55d45f 100755 --- a/deploy/build-backend.sh +++ b/deploy/build-backend.sh @@ -2,12 +2,6 @@ set -Eeo pipefail set +u -# #command line arguments -# CONFIGURATION_PATH=${1-"../config/sample-databases/DefaultConfiguration"} -# SAMPLE_DATABASE_PATH=${2-""} - -# echo $CONFIGURATION_PATH -# echo $SAMPLE_DATABASE_PATH echo "Clone and build Cloudbeaver" rm -rf ./drivers @@ -43,20 +37,8 @@ cp -rp ../server/product/web-server/target/products/io.cloudbeaver.product/all/a cp -p ./scripts/* ./cloudbeaver mkdir cloudbeaver/samples -if [[ -z $SAMPLE_DATABASE_PATH ]]; then - SAMPLE_DATABASE_PATH="" -else - mkdir cloudbeaver/samples/db - cp -rp "${SAMPLE_DATABASE_PATH}" cloudbeaver/samples/ -fi - -if [[ -z "$CONFIGURATION_PATH" ]]; then - CONFIGURATION_PATH="../config/sample-databases/DefaultConfiguration" -fi - cp -rp ../config/core/* cloudbeaver/conf -cp -rp "${CONFIGURATION_PATH}"/GlobalConfiguration/.dbeaver/data-sources.json cloudbeaver/conf/initial-data-sources.conf -cp -p "${CONFIGURATION_PATH}"/*.conf cloudbeaver/conf/ +cp -rp ../config/GlobalConfiguration/.dbeaver/data-sources.json cloudbeaver/conf/initial-data-sources.conf mv drivers cloudbeaver echo "End of backend build" \ No newline at end of file diff --git a/deploy/build-sqlite.bat b/deploy/build-sqlite.bat deleted file mode 100644 index 16e1c95963..0000000000 --- a/deploy/build-sqlite.bat +++ /dev/null @@ -1 +0,0 @@ -@call build.bat ..\config\sample-databases\SQLiteConfiguration ..\config\sample-databases\db diff --git a/deploy/build-sqlite.sh b/deploy/build-sqlite.sh deleted file mode 100755 index 1a8212fef0..0000000000 --- a/deploy/build-sqlite.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -Eeuo pipefail - -#command line arguments -CONFIGURATION_PATH='../config/sample-databases/SQLiteConfiguration' -SAMPLE_DATABASE_PATH='../config/sample-databases/db' - -source build-backend.sh -source build-frontend.sh \ No newline at end of file diff --git a/deploy/build.bat b/deploy/build.bat index e5a90eb1df..27bc96826e 100644 --- a/deploy/build.bat +++ b/deploy/build.bat @@ -1,11 +1,6 @@ @echo off rem command line arguments -SET CONFIGURATION_PATH=%1 -SET SAMPLE_DATABASE_PATH=%2 - -IF "%CONFIGURATION_PATH%"=="" SET CONFIGURATION_PATH="..\config\sample-databases\DefaultConfiguration" -echo "Configuration path=%CONFIGURATION_PATH%" echo Clone and build Cloudbeaver @@ -40,13 +35,9 @@ xcopy /E /Q ..\server\product\web-server\target\products\io.cloudbeaver.product\ copy scripts\* cloudbeaver >NUL mkdir cloudbeaver\samples -IF NOT "%SAMPLE_DATABASE_PATH%"=="" ( - mkdir cloudbeaver\samples\db - xcopy /E /Q %SAMPLE_DATABASE_PATH% cloudbeaver\samples\db >NUL -) + copy ..\config\core\* cloudbeaver\conf >NUL -copy %CONFIGURATION_PATH%\GlobalConfiguration\.dbeaver\data-sources.json cloudbeaver\conf\initial-data-sources.conf >NUL -copy %CONFIGURATION_PATH%\*.conf cloudbeaver\conf >NUL +copy ..\config\DefaultConfiguration\GlobalConfiguration\.dbeaver\data-sources.json cloudbeaver\conf\initial-data-sources.conf >NUL move drivers cloudbeaver >NUL diff --git a/deploy/build.sh b/deploy/build.sh index 04efba9759..c3e481a4eb 100755 --- a/deploy/build.sh +++ b/deploy/build.sh @@ -1,9 +1,5 @@ #!/bin/bash set -Eeuo pipefail -#command line arguments -CONFIGURATION_PATH="../config/sample-databases/DefaultConfiguration" -SAMPLE_DATABASE_PATH="" - source build-backend.sh source build-frontend.sh \ No newline at end of file diff --git a/deploy/docker/cloudbeaver-ce/Dockerfile b/deploy/docker/cloudbeaver-ce/Dockerfile index 361df92c0a..95bdcc6812 100644 --- a/deploy/docker/cloudbeaver-ce/Dockerfile +++ b/deploy/docker/cloudbeaver-ce/Dockerfile @@ -2,6 +2,9 @@ FROM dbeaver/base-java MAINTAINER DBeaver Corp, devops@dbeaver.com +RUN apt-get update; \ + apt-get upgrade -y; + COPY cloudbeaver /opt/cloudbeaver EXPOSE 8978 diff --git a/javaConfig.json b/javaConfig.json index 50a5fcc07c..31698aea82 100644 --- a/javaConfig.json +++ b/javaConfig.json @@ -1,4 +1,3 @@ { - "projects": ["../dbeaver-common", "../dbeaver", "../cloudbeaver/server"], "targetPlatform": "./org.jkiss.cloudbeaver.tp.target" } diff --git a/org.jkiss.cloudbeaver.tp.target b/org.jkiss.cloudbeaver.tp.target index e933d075c1..d67fb5bf55 100644 --- a/org.jkiss.cloudbeaver.tp.target +++ b/org.jkiss.cloudbeaver.tp.target @@ -1,6 +1,6 @@ - + diff --git a/osgi-app.properties b/osgi-app.properties index 527e3a5737..423b98ad5f 100644 --- a/osgi-app.properties +++ b/osgi-app.properties @@ -6,6 +6,11 @@ bundlesPaths=\ dbeaver-common/modules;\ dbeaver/plugins;\ cloudbeaver/server/bundles; +testLibraries=\ + org.junit;\ + org.mockito.mockito-core;\ + junit-jupiter-api;\ + org.opentest4j repositories=\ https://p2.dev.dbeaver.com/eclipse-repo/;\ https://download.eclipse.org/releases/${eclipse-version}/; diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index 52d1c5fa84..4f666e5b47 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Model Bundle-SymbolicName: io.cloudbeaver.model;singleton:=true -Bundle-Version: 1.0.59.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.64.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . @@ -18,7 +18,7 @@ Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport, org.jkiss.bundle.graphql.java;visibility:=reexport, org.jkiss.bundle.apache.dbcp, com.google.gson;visibility:=reexport, - jakarta.servlet;visibility:=reexport + jakarta.servlet-api;bundle-version:="6.0.0";visibility:=reexport Export-Package: io.cloudbeaver, io.cloudbeaver.auth, io.cloudbeaver.auth.provider, @@ -28,7 +28,9 @@ Export-Package: io.cloudbeaver, io.cloudbeaver.websocket, io.cloudbeaver.model, io.cloudbeaver.model.app, + io.cloudbeaver.model.config, io.cloudbeaver.model.fs, + io.cloudbeaver.model.log, io.cloudbeaver.model.rm, io.cloudbeaver.model.rm.local, io.cloudbeaver.model.rm.lock, diff --git a/server/bundles/io.cloudbeaver.model/pom.xml b/server/bundles/io.cloudbeaver.model/pom.xml index 0d684c8458..b3a7477c27 100644 --- a/server/bundles/io.cloudbeaver.model/pom.xml +++ b/server/bundles/io.cloudbeaver.model/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.model - 1.0.59-SNAPSHOT + 1.0.64-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java index 76f38d2102..ddbd377c4e 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java @@ -16,23 +16,20 @@ */ package io.cloudbeaver; -import org.eclipse.core.resources.IProject; import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMSessionContext; +import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMControllerProvider; import org.jkiss.dbeaver.model.rm.RMProject; -import org.jkiss.dbeaver.model.rm.RMUtils; -import org.jkiss.dbeaver.registry.BaseProjectImpl; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.Pair; import java.nio.file.Path; import java.util.Collection; -public class BaseWebProjectImpl extends BaseProjectImpl implements RMControllerProvider { +public abstract class BaseWebProjectImpl extends BaseProjectImpl implements RMControllerProvider { @NotNull private final RMProject project; @@ -40,7 +37,6 @@ public class BaseWebProjectImpl extends BaseProjectImpl implements RMControllerP @NotNull private final Path path; @NotNull - protected final DataSourceFilter dataSourceFilter; private final RMController resourceController; public BaseWebProjectImpl( @@ -48,13 +44,12 @@ public BaseWebProjectImpl( @NotNull RMController resourceController, @NotNull SMSessionContext sessionContext, @NotNull RMProject project, - @NotNull DataSourceFilter dataSourceFilter + @NotNull Path path ) { super(workspace, sessionContext); this.resourceController = resourceController; - this.path = RMUtils.getProjectPath(project); + this.path = path; this.project = project; - this.dataSourceFilter = dataSourceFilter; } @NotNull @@ -91,12 +86,6 @@ public Path getAbsolutePath() { return path; } - @Nullable - @Override - public IProject getEclipseProject() { - return null; - } - @Override public boolean isOpen() { return true; @@ -112,11 +101,6 @@ public boolean isUseSecretStorage() { return false; } - @NotNull - public RMProject getRmProject() { - return this.project; - } - /** * Method for Bulk Update of resources properties paths * diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWConstants.java index cce8a6d406..e2fcae3536 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWConstants.java @@ -35,6 +35,7 @@ public interface DBWConstants { String PERMISSION_EDIT_DATA = "edit-data"; String STATE_ATTR_SIGN_IN_STATE = "state.signin"; + String WORK_DATA_FOLDER_NAME = ".work-data"; enum SignInState { GLOBAL, diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebHeadlessSessionProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebHeadlessSessionProjectImpl.java new file mode 100644 index 0000000000..852520ef2f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebHeadlessSessionProjectImpl.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver; + +import io.cloudbeaver.model.session.WebHeadlessSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.rm.RMUtils; + +public class WebHeadlessSessionProjectImpl extends WebProjectImpl { + public WebHeadlessSessionProjectImpl( + @NotNull WebHeadlessSession session, + @NotNull RMProject project + ) { + super( + session.getWorkspace(), + session.getUserContext().getRmController(), + session.getSessionContext(), + project, + session.getUserContext().getPreferenceStore(), + RMUtils.getProjectPath(project) + ); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java index 5582faa051..c6c7144198 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java @@ -26,22 +26,26 @@ import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.task.DBTTaskManager; import org.jkiss.dbeaver.registry.rm.DataSourceRegistryRM; import org.jkiss.dbeaver.runtime.DBWorkbench; +import java.nio.file.Path; + public abstract class WebProjectImpl extends BaseWebProjectImpl { private static final Log log = Log.getLog(WebProjectImpl.class); @NotNull - private final DBPPreferenceStore preferenceStore; + protected final DBPPreferenceStore preferenceStore; + public WebProjectImpl( @NotNull DBPWorkspace workspace, @NotNull RMController resourceController, @NotNull SMSessionContext sessionContext, @NotNull RMProject project, - @NotNull DataSourceFilter dataSourceFilter, - @NotNull DBPPreferenceStore preferenceStore + @NotNull DBPPreferenceStore preferenceStore, + @NotNull Path path ) { - super(workspace, resourceController, sessionContext, project, dataSourceFilter); + super(workspace, resourceController, sessionContext, project, path); this.preferenceStore = preferenceStore; } @@ -70,13 +74,24 @@ public boolean isUseSecretStorage() { return DBWorkbench.isDistributed(); } + @NotNull + @Override + public DBTTaskManager getTaskManager() { + throw new IllegalStateException("Task manager not supported"); + } + @NotNull @Override protected DBPDataSourceRegistry createDataSourceRegistry() { return new WebDataSourceRegistryProxy( new DataSourceRegistryRM(this, getResourceController(), preferenceStore), - dataSourceFilter + getDataSourceFilter() ); } + @NotNull + public DataSourceFilter getDataSourceFilter() { + return (ds) -> true; + } + } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionGlobalProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionGlobalProjectImpl.java new file mode 100644 index 0000000000..66f48d8d03 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionGlobalProjectImpl.java @@ -0,0 +1,118 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver; + +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.DBPEvent; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.security.SMObjectType; +import org.jkiss.dbeaver.model.security.user.SMObjectPermissions; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Global project. + * Connections there can be not accessible. + */ +public class WebSessionGlobalProjectImpl extends WebSessionProjectImpl { + private static final Log log = Log.getLog(WebSessionGlobalProjectImpl.class); + private Set accessibleConnectionIds = Collections.emptySet(); + + public WebSessionGlobalProjectImpl(@NotNull WebSession webSession, @NotNull RMProject project) { + super(webSession, project); + } + + /** + * Update info about accessible connections from a database. + */ + public synchronized void refreshAccessibleConnectionIds() { + this.accessibleConnectionIds = readAccessibleConnectionIds(); + } + + @NotNull + private Set readAccessibleConnectionIds() { + try { + return webSession.getSecurityController() + .getAllAvailableObjectsPermissions(SMObjectType.datasource) + .stream() + .map(SMObjectPermissions::getObjectId) + .collect(Collectors.toSet()); + } catch (DBException e) { + webSession.addSessionError(e); + log.error("Error reading connection grants", e); + return Collections.emptySet(); + } + } + + /** + * Checks if connection is accessible for current user. + */ + public boolean isDataSourceAccessible(@NotNull DBPDataSourceContainer dataSource) { + return dataSource.isExternallyProvided() || + dataSource.isTemporary() || + webSession.hasPermission(DBWConstants.PERMISSION_ADMIN) || + accessibleConnectionIds.contains(dataSource.getId()); + } + + /** + * Adds a connection if it became accessible. + * The method is processed when connection permissions were updated. + */ + public synchronized void addAccessibleConnectionToCache(@NotNull String dsId) { + if (!getRMProject().isGlobal()) { + return; + } + this.accessibleConnectionIds.add(dsId); + var registry = getDataSourceRegistry(); + var dataSource = registry.getDataSource(dsId); + if (dataSource != null) { + addConnection(dataSource); + // reflect changes is navigator model + registry.notifyDataSourceListeners(new DBPEvent(DBPEvent.Action.OBJECT_ADD, dataSource, true)); + } + } + + /** + * Removes a connection if it became not accessible. + * The method is processed when connection permissions were updated. + */ + public synchronized void removeAccessibleConnectionFromCache(@NotNull String dsId) { + if (!getRMProject().isGlobal()) { + return; + } + var registry = getDataSourceRegistry(); + var dataSource = registry.getDataSource(dsId); + if (dataSource != null) { + this.accessibleConnectionIds.remove(dsId); + removeConnection(dataSource); + // reflect changes is navigator model + registry.notifyDataSourceListeners(new DBPEvent(DBPEvent.Action.OBJECT_REMOVE, dataSource)); + dataSource.dispose(); + } + } + + @NotNull + public DataSourceFilter getDataSourceFilter() { + return this::isDataSourceAccessible; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java index 980f0a93a7..b55860c05b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionProjectImpl.java @@ -16,28 +16,59 @@ */ package io.cloudbeaver; +import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.utils.WebDataSourceUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistryCache; import org.jkiss.dbeaver.model.navigator.DBNModel; import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.rm.RMUtils; +import org.jkiss.dbeaver.model.websocket.event.WSEventType; +import org.jkiss.dbeaver.registry.DataSourceDescriptor; +import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; + +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; public class WebSessionProjectImpl extends WebProjectImpl { + private static final Log log = Log.getLog(WebSessionProjectImpl.class); + protected final WebSession webSession; + private final Map connections = new HashMap<>(); + private boolean registryIsLoaded = false; - private final WebSession webSession; + public WebSessionProjectImpl( + @NotNull WebSession webSession, + @NotNull RMProject project + ) { + super( + webSession.getWorkspace(), + webSession.getRmController(), + webSession.getSessionContext(), + project, + webSession.getUserPreferenceStore(), + RMUtils.getProjectPath(project) + ); + this.webSession = webSession; + } public WebSessionProjectImpl( @NotNull WebSession webSession, @NotNull RMProject project, - @NotNull DataSourceFilter dataSourceFilter + @NotNull Path path ) { super( webSession.getWorkspace(), webSession.getRmController(), webSession.getSessionContext(), project, - dataSourceFilter, - webSession.getUserPreferenceStore() + webSession.getUserPreferenceStore(), + path ); this.webSession = webSession; } @@ -47,4 +78,156 @@ public WebSessionProjectImpl( public DBNModel getNavigatorModel() { return webSession.getNavigatorModel(); } + + @NotNull + @Override + protected DBPDataSourceRegistry createDataSourceRegistry() { + DBPDataSourceRegistry dataSourceRegistry = super.createDataSourceRegistry(); + dataSourceRegistry.setAuthCredentialsProvider(webSession); + return dataSourceRegistry; + } + + private synchronized void addDataSourcesToCache() { + if (registryIsLoaded) { + return; + } + getDataSourceRegistry().getDataSources().forEach(this::addConnection); + Throwable lastError = getDataSourceRegistry().getLastError(); + if (lastError != null) { + webSession.addSessionError(lastError); + log.error("Error refreshing connections from project '" + getId() + "'", lastError); + } + registryIsLoaded = true; + } + + @Override + public void dispose() { + super.dispose(); + Map conCopy; + synchronized (this.connections) { + conCopy = new HashMap<>(this.connections); + this.connections.clear(); + } + + for (WebConnectionInfo connectionInfo : conCopy.values()) { + if (connectionInfo.isConnected()) { + new DisconnectJob(connectionInfo.getDataSourceContainer()).schedule(); + } + } + } + + + /** + * Returns web connection info from cache (if exists). + */ + @Nullable + public WebConnectionInfo findWebConnectionInfo(@NotNull String connectionId) { + synchronized (connections) { + return connections.get(connectionId); + } + } + + /** + * Returns web connection info from cache, adds it to cache if not present. + * Throws exception if connection is not found. + */ + @NotNull + public WebConnectionInfo getWebConnectionInfo(@NotNull String connectionId) throws DBWebException { + WebConnectionInfo connectionInfo = findWebConnectionInfo(connectionId); + if (connectionInfo != null) { + return connectionInfo; + } + DBPDataSourceContainer dataSource = getDataSourceRegistry().getDataSource(connectionId); + if (dataSource != null) { + return addConnection(dataSource); + } + throw new DBWebException("Connection '%s' not found".formatted(connectionId)); + } + + /** + * Adds connection to project cache. + */ + @NotNull + public synchronized WebConnectionInfo addConnection(@NotNull DBPDataSourceContainer dataSourceContainer) { + WebConnectionInfo connection = new WebConnectionInfo(webSession, dataSourceContainer); + synchronized (connections) { + connections.put(dataSourceContainer.getId(), connection); + } + return connection; + } + + /** + * Removes connection from project cache. + */ + public void removeConnection(@NotNull DBPDataSourceContainer dataSourceContainer) { + WebConnectionInfo webConnectionInfo = connections.get(dataSourceContainer.getId()); + if (webConnectionInfo != null) { + webConnectionInfo.clearCache(); + synchronized (connections) { + connections.remove(dataSourceContainer.getId()); + } + } + } + + /** + * Loads connection from registry if they are not loaded. + * + * @return connections from cache. + */ + public List getConnections() { + if (!registryIsLoaded) { + addDataSourcesToCache(); + registryIsLoaded = true; + } + synchronized (connections) { + return new ArrayList<>(connections.values()); + } + } + + /** + * updates data sources based on event in web session + * + * @param dataSourceIds list of updated connections + * @param type type of event + */ + public synchronized boolean updateProjectDataSources(@NotNull List dataSourceIds, @NotNull WSEventType type) { + var sendDataSourceUpdatedEvent = false; + DBPDataSourceRegistry registry = getDataSourceRegistry(); + // save old connections + var oldDataSources = dataSourceIds.stream() + .map(registry::getDataSource) + .filter(Objects::nonNull) + .collect(Collectors.toMap( + DBPDataSourceContainer::getId, + ds -> new DataSourceDescriptor((DataSourceDescriptor) ds, ds.getRegistry()) + )); + if (type == WSEventType.DATASOURCE_CREATED || type == WSEventType.DATASOURCE_UPDATED) { + registry.refreshConfig(dataSourceIds); + } + for (String dsId : dataSourceIds) { + DataSourceDescriptor ds = (DataSourceDescriptor) registry.getDataSource(dsId); + if (ds == null) { + continue; + } + switch (type) { + case DATASOURCE_CREATED -> { + addConnection(ds); + sendDataSourceUpdatedEvent = true; + } + case DATASOURCE_UPDATED -> // if settings were changed we need to send event + sendDataSourceUpdatedEvent |= !ds.equalSettings(oldDataSources.get(dsId)); + case DATASOURCE_DELETED -> { + WebDataSourceUtils.disconnectDataSource(webSession, ds); + if (registry instanceof DBPDataSourceRegistryCache dsrc) { + dsrc.removeDataSourceFromList(ds); + } + removeConnection(ds); + sendDataSourceUpdatedEvent = true; + } + default -> { + } + } + } + return sendDataSourceUpdatedEvent; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderFederated.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderFederated.java index bd2bab25e2..6fd0ead93f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderFederated.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/SMAuthProviderFederated.java @@ -46,6 +46,9 @@ default String getUserSignOutLink( @Nullable String getAcsLink(String id, @NotNull Map providerConfig) throws DBException; + @Nullable + String getEntityIdLink(String id, @NotNull Map providerConfig) throws DBException; + @Nullable default String getRedirectLink(String id, @NotNull Map providerConfig) throws DBException { return null; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java index 9c5e42c945..85f9d249d3 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java @@ -445,10 +445,10 @@ public String getRequiredAuth() { private boolean hasProjectPermission(RMProjectPermission projectPermission) { DBPProject project = dataSourceContainer.getProject(); - if (!(project instanceof WebProjectImpl)) { + if (!(project instanceof WebProjectImpl webProject)) { return false; } - return SMUtils.hasProjectPermission(session, ((WebProjectImpl) project).getRmProject(), projectPermission); + return SMUtils.hasProjectPermission(session, webProject.getRMProject(), projectPermission); } private boolean canViewReadOnlyConnections() { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java index 2a3ceed1a9..292ef8601d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebProjectInfo.java @@ -53,11 +53,13 @@ public String getId() { } @Property - public boolean isGlobal() { return project.getRmProject().isGlobal(); } + public boolean isGlobal() { + return project.getRMProject().isGlobal(); + } @Property public boolean isShared() { - return project.getRmProject().isShared(); + return project.getRMProject().isShared(); } @Property @@ -72,7 +74,7 @@ public String getDescription() { @Property public boolean isCanEditDataSources() { - if (project.getRmProject().getType() == RMProjectType.USER && !customPrivateConnectionsEnabled) { + if (project.getRMProject().getType() == RMProjectType.USER && !customPrivateConnectionsEnabled) { return false; } return hasDataSourcePermission(RMProjectPermission.DATA_SOURCES_EDIT); @@ -94,12 +96,12 @@ public boolean isCanViewResources() { } private boolean hasDataSourcePermission(RMProjectPermission permission) { - return SMUtils.hasProjectPermission(session, project.getRmProject(), permission); + return SMUtils.hasProjectPermission(session, project.getRMProject(), permission); } @Property public RMResourceType[] getResourceTypes() { - RMResourceType[] resourceTypes = project.getRmProject().getResourceTypes(); + RMResourceType[] resourceTypes = project.getRMProject().getResourceTypes(); if(resourceTypes == null) { return ArrayUtils.toArray(RMResourceType.class, new ArrayList<>()); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseAuthWebAppConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseAuthWebAppConfiguration.java deleted file mode 100644 index 3067b311df..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseAuthWebAppConfiguration.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudbeaver.model.app; - -import com.google.gson.annotations.Expose; -import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; -import io.cloudbeaver.registry.WebAuthProviderDescriptor; -import io.cloudbeaver.registry.WebAuthProviderRegistry; -import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; -import org.jkiss.utils.ArrayUtils; - -import java.util.*; - -public abstract class BaseAuthWebAppConfiguration extends BaseWebAppConfiguration implements WebAuthConfiguration { - private String defaultAuthProvider; - private String[] enabledAuthProviders; - private final Set authConfigurations; - // Legacy auth configs, left for backward compatibility - @Expose(serialize = false) - private final Map authConfiguration; - - public BaseAuthWebAppConfiguration() { - super(); - this.defaultAuthProvider = LocalAuthProviderConstants.PROVIDER_ID; - this.enabledAuthProviders = null; - this.authConfigurations = new LinkedHashSet<>(); - this.authConfiguration = new LinkedHashMap<>(); - } - - public BaseAuthWebAppConfiguration(BaseAuthWebAppConfiguration src) { - super(src); - this.defaultAuthProvider = src.defaultAuthProvider; - this.enabledAuthProviders = src.enabledAuthProviders; - this.authConfigurations = new LinkedHashSet<>(src.authConfigurations); - this.authConfiguration = new LinkedHashMap<>(src.authConfiguration); - } - - @Override - public String getDefaultAuthProvider() { - return defaultAuthProvider; - } - - public void setDefaultAuthProvider(String defaultAuthProvider) { - this.defaultAuthProvider = defaultAuthProvider; - } - - @Override - public String[] getEnabledAuthProviders() { - if (enabledAuthProviders == null) { - // No config - enable all providers (+backward compatibility) - return WebAuthProviderRegistry.getInstance().getAuthProviders() - .stream().map(WebAuthProviderDescriptor::getId).toArray(String[]::new); - } - return enabledAuthProviders; - } - - public void setEnabledAuthProviders(String[] enabledAuthProviders) { - this.enabledAuthProviders = enabledAuthProviders; - } - - - @Override - public boolean isAuthProviderEnabled(String id) { - var authProviderDescriptor = WebAuthProviderRegistry.getInstance().getAuthProvider(id); - if (authProviderDescriptor == null) { - return false; - } - - if (!ArrayUtils.contains(getEnabledAuthProviders(), id)) { - return false; - } - if (!ArrayUtils.isEmpty(authProviderDescriptor.getRequiredFeatures())) { - for (String rf : authProviderDescriptor.getRequiredFeatures()) { - if (!isFeatureEnabled(rf)) { - return false; - } - } - } - return true; - } - - //////////////////////////////////////////// - // Auth provider configs - @Override - public Set getAuthCustomConfigurations() { - return authConfigurations; - } - - @Override - @Nullable - public SMAuthProviderCustomConfiguration getAuthProviderConfiguration(@NotNull String id) { - synchronized (authConfigurations) { - return authConfigurations.stream().filter(c -> c.getId().equals(id)).findAny().orElse(null); - } - } - - public void addAuthProviderConfiguration(@NotNull SMAuthProviderCustomConfiguration config) { - synchronized (authConfigurations) { - authConfigurations.removeIf(c -> c.getId().equals(config.getId())); - authConfigurations.add(config); - } - } - - public void setAuthProvidersConfigurations(Collection authProviders) { - synchronized (authConfigurations) { - authConfigurations.clear(); - authConfigurations.addAll(authProviders); - } - } - - public boolean deleteAuthProviderConfiguration(@NotNull String id) { - synchronized (authConfigurations) { - return authConfigurations.removeIf(c -> c.getId().equals(id)); - } - } - - public void loadLegacyCustomConfigs() { - // Convert legacy map of configs into list - if (!authConfiguration.isEmpty()) { - for (Map.Entry entry : authConfiguration.entrySet()) { - entry.getValue().setId(entry.getKey()); - authConfigurations.add(entry.getValue()); - } - authConfiguration.clear(); - } - } -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java index 8204ffda61..781f904e81 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java @@ -18,17 +18,87 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.registry.fs.FileSystemProviderRegistry; +import org.jkiss.utils.IOUtils; + +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Path; /** * Abstract class that contains methods for loading configuration with gson. */ -public abstract class BaseServerConfigurationController implements WebServerConfigurationController { +public abstract class BaseServerConfigurationController + implements WebServerConfigurationController { + private static final Log log = Log.getLog(BaseServerConfigurationController.class); + @NotNull + private final Path homeDirectory; + + protected Path workspacePath; + + protected BaseServerConfigurationController(@NotNull Path homeDirectory) { + this.homeDirectory = homeDirectory; + //default workspaceLocation + this.workspacePath = homeDirectory.resolve("workspace"); + } - protected Gson getGson() { + @NotNull + public Gson getGson() { return getGsonBuilder().create(); } + @NotNull protected abstract GsonBuilder getGsonBuilder(); public abstract T getServerConfiguration(); + + + @NotNull + protected synchronized void initWorkspacePath() throws DBException { + if (workspacePath != null && !IOUtils.isFileFromDefaultFS(workspacePath)) { + log.warn("Workspace directory already initialized: " + workspacePath); + } + String workspaceLocation = getWorkspaceLocation(); + URI workspaceUri = URI.create(workspaceLocation); + if (workspaceUri.getScheme() == null) { + // default filesystem + this.workspacePath = getHomeDirectory().resolve(workspaceLocation); + } else { + var externalFsProvider = + FileSystemProviderRegistry.getInstance().getFileSystemProviderBySchema(workspaceUri.getScheme()); + if (externalFsProvider == null) { + throw new DBException("File system not found for scheme: " + workspaceUri.getScheme()); + } + ClassLoader fsClassloader = externalFsProvider.getInstance().getClass().getClassLoader(); + try (FileSystem externalFileSystem = FileSystems.newFileSystem(workspaceUri, + System.getenv(), + fsClassloader);) { + this.workspacePath = externalFileSystem.provider().getPath(workspaceUri); + } catch (Exception e) { + throw new DBException("Failed to initialize workspace path: " + workspaceUri, e); + } + } + log.info("Workspace path initialized: " + workspacePath); + } + + @NotNull + protected abstract String getWorkspaceLocation(); + + @NotNull + protected Path getHomeDirectory() { + return homeDirectory; + } + + @NotNull + @Override + public Path getWorkspacePath() { + if (workspacePath == null) { + throw new RuntimeException("Workspace path not initialized"); + } + return workspacePath; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java index ab30ce377d..4c3a1714e1 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java @@ -84,14 +84,18 @@ public boolean isResourceManagerEnabled() { return resourceManagerEnabled; } + @Override public boolean isFeatureEnabled(String id) { return ArrayUtils.contains(getEnabledFeatures(), id); } + @Override public boolean isFeaturesEnabled(String[] features) { return ArrayUtils.containsAll(getEnabledFeatures(), features); } + @NotNull + @Override public String[] getEnabledFeatures() { if (enabledFeatures == null) { // No config - enable all features (+backward compatibility) diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java index 87119703e6..87f9b85409 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java @@ -16,13 +16,7 @@ */ package io.cloudbeaver.model.app; -import io.cloudbeaver.DataSourceFilter; -import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.WebSessionProjectImpl; import io.cloudbeaver.model.log.SLF4JLogHandler; -import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.WebGlobalWorkspace; -import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.runtime.Platform; import org.eclipse.equinox.app.IApplicationContext; import org.jkiss.code.NotNull; @@ -30,18 +24,16 @@ import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBFileController; -import org.jkiss.dbeaver.model.app.DBPPlatform; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; import org.jkiss.dbeaver.model.auth.SMSessionContext; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.impl.app.ApplicationRegistry; +import org.jkiss.dbeaver.model.impl.app.BaseApplicationImpl; +import org.jkiss.dbeaver.model.impl.app.BaseWorkspaceImpl; import org.jkiss.dbeaver.model.rm.RMController; -import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.secret.DBSSecretController; import org.jkiss.dbeaver.model.websocket.event.WSEventController; -import org.jkiss.dbeaver.registry.BaseApplicationImpl; -import org.jkiss.dbeaver.registry.BaseWorkspaceImpl; import org.jkiss.dbeaver.runtime.IVariableResolver; import org.jkiss.dbeaver.utils.GeneralUtils; import org.jkiss.dbeaver.utils.RuntimeUtils; @@ -62,17 +54,10 @@ public abstract class BaseWebApplication extends BaseApplicationImpl implements public static final String CLI_PARAM_WEB_CONFIG = "-web-config"; public static final String LOGBACK_FILE_NAME = "logback.xml"; - private static final Log log = Log.getLog(BaseWebApplication.class); private String instanceId; - @NotNull - @Override - public DBPWorkspace createWorkspace(@NotNull DBPPlatform platform, @NotNull IWorkspace eclipseWorkspace) { - return new WebGlobalWorkspace(platform, eclipseWorkspace); - } - @Override public RMController createResourceController( @NotNull SMCredentialsProvider credentialsProvider, @@ -170,19 +155,6 @@ private Path getCustomConfigPath(Path configPath, String fileName) { return Files.exists(customConfigPath) ? customConfigPath : configPath.resolve(fileName); } - @Override - public WebProjectImpl createProjectImpl( - @NotNull WebSession webSession, - @NotNull RMProject project, - @NotNull DataSourceFilter dataSourceFilter - ) { - return new WebSessionProjectImpl( - webSession, - project, - dataSourceFilter - ); - } - /** * There is no secret controller in base web app. * Method returns VoidSecretController instance. @@ -261,6 +233,12 @@ public String getWorkspaceIdProperty() throws DBException { return BaseWorkspaceImpl.readWorkspaceIdProperty(); } + @Override + public Path getWorkspaceDirectory() { + return getServerConfigurationController().getWorkspacePath(); + } + + public String getApplicationId() { try { return getApplicationInstanceId(); @@ -280,4 +258,12 @@ public WSEventController getEventController() { public boolean isEnvironmentVariablesAccessible() { return false; } + + protected void closeResource(String name, Runnable closeFunction) { + try { + closeFunction.run(); + } catch (Exception e) { + log.error("Failed close " + name, e); + } + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java index 92c2388921..c377b924b9 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebAppConfiguration.java @@ -17,6 +17,7 @@ package io.cloudbeaver.model.app; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import java.util.Map; @@ -28,6 +29,7 @@ public interface WebAppConfiguration { boolean isAnonymousAccessEnabled(); + @Nullable T getResourceQuota(String quotaId); String getDefaultUserTeam(); @@ -42,6 +44,11 @@ public interface WebAppConfiguration { boolean isFeatureEnabled(String id); + @NotNull + default String[] getEnabledFeatures() { + return new String[0]; + } + default boolean isSupportsCustomConnections() { return true; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java index 6e5a2ccf49..ae1ec9333a 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java @@ -16,9 +16,6 @@ */ package io.cloudbeaver.model.app; -import io.cloudbeaver.DataSourceFilter; -import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.DBFileController; @@ -27,7 +24,6 @@ import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; import org.jkiss.dbeaver.model.auth.SMSessionContext; import org.jkiss.dbeaver.model.rm.RMController; -import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.secret.DBSSecretController; import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.SMController; @@ -58,12 +54,6 @@ default boolean isInitializationMode() { boolean isMultiNode(); - WebProjectImpl createProjectImpl( - @NotNull WebSession webSession, - @NotNull RMProject project, - @NotNull DataSourceFilter dataSourceFilter - ); - SMController createSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException; SMAdminController getAdminSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java index 1c8fa96c2c..82cbfeb05a 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java @@ -16,6 +16,11 @@ */ package io.cloudbeaver.model.app; +import io.cloudbeaver.server.WebServerPreferenceStore; +import org.jkiss.code.NotNull; + +import java.util.Map; + /** * Web server configuration. * Contains only server configuration properties. @@ -27,4 +32,12 @@ default String getRootURI() { return ""; } + /** + * @return the setting values that will be used in {@link WebServerPreferenceStore} + */ + @NotNull + default Map getProductSettings() { + return Map.of(); + } + } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java index febbce7a4d..f6823f6dfb 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java @@ -16,9 +16,12 @@ */ package io.cloudbeaver.model.app; +import com.google.gson.Gson; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import java.nio.file.Path; +import java.util.Map; /** * Server configuration controller. @@ -31,4 +34,16 @@ public interface WebServerConfigurationController getOriginalConfigurationProperties() { + return Map.of(); + } + + @NotNull + Path getWorkspacePath(); + + @NotNull + Gson getGson(); + + void validateFinalServerConfiguration() throws DBException; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/CBAppConfig.java similarity index 66% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/CBAppConfig.java index 798de0732c..a2fc07f6be 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/CBAppConfig.java @@ -14,21 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cloudbeaver.server; +package io.cloudbeaver.model.config; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import io.cloudbeaver.DBWebException; -import io.cloudbeaver.WebServiceUtils; -import io.cloudbeaver.model.app.BaseAuthWebAppConfiguration; +import com.google.gson.annotations.Expose; +import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; +import io.cloudbeaver.model.app.BaseWebAppConfiguration; import io.cloudbeaver.model.app.WebAuthConfiguration; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.DBException; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; @@ -38,11 +36,15 @@ /** * Application configuration */ -public class CBAppConfig extends BaseAuthWebAppConfiguration implements WebAuthConfiguration { +public class CBAppConfig extends BaseWebAppConfiguration implements WebAuthConfiguration { private static final Log log = Log.getLog(CBAppConfig.class); public static final DataSourceNavigatorSettings.Preset PRESET_WEB = new DataSourceNavigatorSettings.Preset("web", "Web", "Default view"); public static final DataSourceNavigatorSettings DEFAULT_VIEW_SETTINGS = PRESET_WEB.getSettings(); + private final Set authConfigurations; + // Legacy auth configs, left for backward compatibility + @Expose(serialize = false) + private final Map authConfiguration; private boolean supportsCustomConnections; private boolean supportsConnectionBrowser; @@ -66,9 +68,14 @@ public class CBAppConfig extends BaseAuthWebAppConfiguration implements WebAuthC private DataSourceNavigatorSettings defaultNavigatorSettings; private final Map resourceQuotas; + private String defaultAuthProvider; + private String[] enabledAuthProviders; public CBAppConfig() { - super(); + this.defaultAuthProvider = LocalAuthProviderConstants.PROVIDER_ID; + this.enabledAuthProviders = null; + this.authConfigurations = new LinkedHashSet<>(); + this.authConfiguration = new LinkedHashMap<>(); this.anonymousAccessEnabled = false; this.anonymousUserRole = DEFAULT_APP_ANONYMOUS_TEAM_NAME; this.anonymousUserTeam = DEFAULT_APP_ANONYMOUS_TEAM_NAME; @@ -91,6 +98,10 @@ public CBAppConfig() { public CBAppConfig(CBAppConfig src) { super(src); + this.defaultAuthProvider = src.defaultAuthProvider; + this.enabledAuthProviders = src.enabledAuthProviders; + this.authConfigurations = new LinkedHashSet<>(src.authConfigurations); + this.authConfiguration = new LinkedHashMap<>(src.authConfiguration); this.anonymousAccessEnabled = src.anonymousAccessEnabled; this.anonymousUserRole = src.anonymousUserRole; this.anonymousUserTeam = src.anonymousUserTeam; @@ -169,6 +180,10 @@ public String[] getEnabledDrivers() { return enabledDrivers; } + public void setEnabledDrivers(String[] enabledDrivers) { + this.enabledDrivers = enabledDrivers; + } + public String[] getDisabledDrivers() { return disabledDrivers; } @@ -204,19 +219,6 @@ public Map getPluginConfig(@NotNull String pluginId) { return getPluginConfig(pluginId, false); } - public T getPluginOptions(@NotNull String pluginId, @NotNull String option, Class theClass) throws DBException { - Map iamSettingsMap = CBPlatform.getInstance().getApplication().getAppConfiguration().getPluginOption( - pluginId, option); - if (CommonUtils.isEmpty(iamSettingsMap)) { - throw new DBException("Settings '" + option + "' not specified in plugin '" + pluginId + "' configuration"); - } - - Gson gson = new GsonBuilder().create(); - return gson.fromJson( - gson.toJsonTree(iamSettingsMap), - theClass); - } - //////////////////////////////////////////// // Quotas @@ -264,43 +266,100 @@ public boolean isGrantConnectionsAccessToAnonymousTeam() { return grantConnectionsAccessToAnonymousTeam; } + public boolean isDriverForceEnabled(@NotNull String driverId) { + return ArrayUtils.containsIgnoreCase(getEnabledDrivers(), driverId); + } + + public boolean isSystemVariablesResolvingEnabled() { + return systemVariablesResolvingEnabled; + } + + @Override + public String getDefaultAuthProvider() { + return defaultAuthProvider; + } - // we disable embedded drivers by default and enable it in enabled drivers list - // that's why we need so complicated logic for disabling drivers + public void setDefaultAuthProvider(String defaultAuthProvider) { + this.defaultAuthProvider = defaultAuthProvider; + } - public void updateDisabledDriversConfig(String[] disabledDriversConfig) { - Set disabledIds = new LinkedHashSet<>(Arrays.asList(disabledDriversConfig)); - Set enabledIds = new LinkedHashSet<>(Arrays.asList(this.enabledDrivers)); + @Override + public String[] getEnabledAuthProviders() { + if (enabledAuthProviders == null) { + // No config - enable all providers (+backward compatibility) + return WebAuthProviderRegistry.getInstance().getAuthProviders() + .stream().map(WebAuthProviderDescriptor::getId).toArray(String[]::new); + } + return enabledAuthProviders; + } - // remove all disabled embedded drivers from enabled drivers list - enabledIds.removeAll(disabledIds); + public void setEnabledAuthProviders(String[] enabledAuthProviders) { + this.enabledAuthProviders = enabledAuthProviders; + } - // enable embedded driver if it is not in disabled drivers list - for (String driverId : this.disabledDrivers) { - if (disabledIds.contains(driverId)) { - // driver is also disabled - continue; - } - // driver is removed from disabled list - // we need to enable if it is embedded - try { - DBPDriver driver = WebServiceUtils.getDriverById(driverId); - if (driver.isEmbedded()) { - enabledIds.add(driverId); + @Override + public boolean isAuthProviderEnabled(String id) { + var authProviderDescriptor = WebAuthProviderRegistry.getInstance().getAuthProvider(id); + if (authProviderDescriptor == null) { + return false; + } + + if (!ArrayUtils.contains(getEnabledAuthProviders(), id)) { + return false; + } + if (!ArrayUtils.isEmpty(authProviderDescriptor.getRequiredFeatures())) { + for (String rf : authProviderDescriptor.getRequiredFeatures()) { + if (!isFeatureEnabled(rf)) { + return false; } - } catch (DBWebException e) { - log.error("Failed to find driver by id", e); } } - this.disabledDrivers = disabledDriversConfig; - this.enabledDrivers = enabledIds.toArray(String[]::new); + return true; } - public boolean isDriverForceEnabled(@NotNull String driverId) { - return ArrayUtils.containsIgnoreCase(getEnabledDrivers(), driverId); + //////////////////////////////////////////// + // Auth provider configs + @Override + public Set getAuthCustomConfigurations() { + return authConfigurations; } - public boolean isSystemVariablesResolvingEnabled() { - return systemVariablesResolvingEnabled; + @Override + @Nullable + public SMAuthProviderCustomConfiguration getAuthProviderConfiguration(@NotNull String id) { + synchronized (authConfigurations) { + return authConfigurations.stream().filter(c -> c.getId().equals(id)).findAny().orElse(null); + } + } + + public void addAuthProviderConfiguration(@NotNull SMAuthProviderCustomConfiguration config) { + synchronized (authConfigurations) { + authConfigurations.removeIf(c -> c.getId().equals(config.getId())); + authConfigurations.add(config); + } + } + + public void setAuthProvidersConfigurations(Collection authProviders) { + synchronized (authConfigurations) { + authConfigurations.clear(); + authConfigurations.addAll(authProviders); + } + } + + public boolean deleteAuthProviderConfiguration(@NotNull String id) { + synchronized (authConfigurations) { + return authConfigurations.removeIf(c -> c.getId().equals(id)); + } + } + + public void loadLegacyCustomConfigs() { + // Convert legacy map of configs into list + if (!authConfiguration.isEmpty()) { + for (Map.Entry entry : authConfiguration.entrySet()) { + entry.getValue().setId(entry.getKey()); + authConfigurations.add(entry.getValue()); + } + authConfiguration.clear(); + } } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfig.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/CBServerConfig.java similarity index 97% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfig.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/CBServerConfig.java index fc02e4baa5..2ab813e572 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfig.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/CBServerConfig.java @@ -14,13 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cloudbeaver.server; +package io.cloudbeaver.model.config; import com.google.gson.annotations.SerializedName; import io.cloudbeaver.auth.CBAuthConstants; import io.cloudbeaver.model.app.WebServerConfiguration; -import io.cloudbeaver.service.security.SMControllerConfiguration; -import io.cloudbeaver.service.security.db.WebDatabaseConfig; +import io.cloudbeaver.server.CBConstants; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/PasswordPolicyConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/PasswordPolicyConfiguration.java similarity index 97% rename from server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/PasswordPolicyConfiguration.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/PasswordPolicyConfiguration.java index 28076d7174..fb5e674afa 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/PasswordPolicyConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/PasswordPolicyConfiguration.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cloudbeaver.service.security; +package io.cloudbeaver.model.config; import org.jkiss.dbeaver.model.meta.Property; diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMControllerConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/SMControllerConfiguration.java similarity index 98% rename from server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMControllerConfiguration.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/SMControllerConfiguration.java index 70a704faf1..ad22bd5736 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMControllerConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/SMControllerConfiguration.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package io.cloudbeaver.service.security; +package io.cloudbeaver.model.config; public class SMControllerConfiguration { //in minutes diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/WebDatabaseConfig.java similarity index 98% rename from server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/WebDatabaseConfig.java index 27bf86f04f..8a5ef39010 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/WebDatabaseConfig.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/config/WebDatabaseConfig.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cloudbeaver.service.security.db; +package io.cloudbeaver.model.config; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.connection.InternalDatabaseConfig; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java index cf601dae8c..d934abf113 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java @@ -20,7 +20,6 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBIcon; import org.jkiss.dbeaver.model.DBPImage; import org.jkiss.dbeaver.model.DBPObject; @@ -37,7 +36,6 @@ import java.util.List; public class DBNResourceManagerProject extends DBNAbstractResourceManagerNode { - private static final Log log = Log.getLog(DBNResourceManagerProject.class); private final RMProject project; @@ -123,21 +121,15 @@ public DBNNode refreshNode(DBRProgressMonitor monitor, Object source) throws DBE return this; } + @Nullable @Override - public String toString() { - return getNodeDisplayName(); - } - - - @Override - public DBPProject getOwnerProject() { + public DBPProject getOwnerProjectOrNull() { List globalProjects = getModel().getModelProjects(); - if (globalProjects == null) { - return null; - } - for (DBPProject modelProject : globalProjects) { - if (CommonUtils.equalObjects(modelProject.getId(), project.getId())) { - return modelProject; + if (globalProjects != null) { + for (DBPProject modelProject : globalProjects) { + if (CommonUtils.equalObjects(modelProject.getId(), project.getId())) { + return modelProject; + } } } return null; @@ -148,4 +140,10 @@ public DBPProject getOwnerProject() { public DBPObject getObjectDetails(@NotNull DBRProgressMonitor monitor, @NotNull SMSessionContext sessionContext, @NotNull Object dataSource) throws DBException { return project; } + + @Override + public String toString() { + return getNodeDisplayName(); + } + } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java index 30919d34ca..4e2cfbc76f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java @@ -203,9 +203,10 @@ public DBPObject getObjectDetails(@NotNull DBRProgressMonitor monitor, @NotNull return resource; } + @Nullable @Override - public DBPProject getOwnerProject() { - return getParentNode().getOwnerProject(); + public DBPProject getOwnerProjectOrNull() { + return getParentNode().getOwnerProjectOrNull(); } public RMResource getResource() { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/BaseLocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/BaseLocalResourceController.java new file mode 100644 index 0000000000..fbea3749e0 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/BaseLocalResourceController.java @@ -0,0 +1,352 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model.rm.local; + +import io.cloudbeaver.BaseWebProjectImpl; +import io.cloudbeaver.model.rm.lock.RMFileLockController; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceConfigurationStorage; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.DBPDataSourceFolder; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.rm.RMEvent; +import org.jkiss.dbeaver.model.rm.RMEventManager; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; +import org.jkiss.dbeaver.registry.*; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.ArrayUtils; +import org.jkiss.utils.IOUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.function.Predicate; + +public abstract class BaseLocalResourceController implements RMController { + private static final Log log = Log.getLog(BaseLocalResourceController.class); + + public static final String DEFAULT_CHANGE_ID = "0"; + private static final String FILE_REGEX = "(?U)[\\w.$()@/\\\\ -]+"; + private static final String PROJECT_REGEX = "(?U)[\\w.$()@ -]+"; // slash not allowed in project name + + @NotNull + protected final DBPWorkspace workspace; + @NotNull + protected final RMFileLockController lockController; + + protected BaseLocalResourceController( + @NotNull DBPWorkspace workspace, + @NotNull RMFileLockController lockController + ) { + this.workspace = workspace; + this.lockController = lockController; + } + + @Override + public RMProject getProject(@NotNull String projectId, boolean readResources, boolean readProperties) + throws DBException { + RMProject project = makeProjectFromId(projectId, true); + if (project == null) { + return null; + } + if (readResources) { + doProjectOperation(projectId, () -> { + project.setChildren( + listResources(projectId, null, null, readProperties, false, true) + ); + return null; + }); + } + return project; + } + + @Override + public Object getProjectProperty(@NotNull String projectId, @NotNull String propName) throws DBException { + var project = getWebProject(projectId, false); + return doFileReadOperation(projectId, + project.getMetadataFilePath(), + () -> project.getProjectProperty(propName)); + } + + @Override + public void setProjectProperty( + @NotNull String projectId, + @NotNull String propName, + @NotNull Object propValue + ) throws DBException { + BaseWebProjectImpl webProject = getWebProject(projectId, false); + doFileWriteOperation(projectId, webProject.getMetadataFilePath(), + () -> { + log.debug("Updating value for property '" + propName + "' in project '" + projectId + "'"); + webProject.setProjectProperty(propName, propValue); + return null; + } + ); + } + + @Override + public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds) + throws DBException { + DBPProject projectMetadata = getWebProject(projectId, false); + return doFileReadOperation( + projectId, + projectMetadata.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = projectMetadata.getDataSourceRegistry(); + registry.refreshConfig(); + registry.checkForErrors(); + DataSourceConfigurationManagerBuffer buffer = new DataSourceConfigurationManagerBuffer(); + Predicate filter = null; + if (!ArrayUtils.isEmpty(dataSourceIds)) { + filter = ds -> ArrayUtils.contains(dataSourceIds, ds.getId()); + } + ((DataSourcePersistentRegistry) registry).saveConfigurationToManager(new VoidProgressMonitor(), + buffer, + filter); + registry.checkForErrors(); + + return new String(buffer.getData(), StandardCharsets.UTF_8); + } + ); + } + + @Override + public void createProjectDataSources( + @NotNull String projectId, + @NotNull String configuration, + @Nullable List dataSourceIds + ) throws DBException { + updateProjectDataSources(projectId, configuration, dataSourceIds); + } + + @Override + public boolean updateProjectDataSources( + @NotNull String projectId, + @NotNull String configuration, + @Nullable List dataSourceIds + ) throws DBException { + try (var lock = lockController.lockProject(projectId, "updateProjectDataSources")) { + DBPProject project = getWebProject(projectId, false); + return doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + DBPDataSourceConfigurationStorage storage = new DataSourceMemoryStorage(configuration.getBytes( + StandardCharsets.UTF_8)); + DataSourceConfigurationManager manager = new DataSourceConfigurationManagerBuffer(); + var configChanged = ((DataSourcePersistentRegistry) registry).loadDataSources( + List.of(storage), + manager, + dataSourceIds, + true, + false + ); + registry.checkForErrors(); + log.debug("Save data sources configuration in project '" + projectId + "'"); + ((DataSourcePersistentRegistry) registry).saveDataSources(); + registry.checkForErrors(); + return configChanged; + } + ); + } + } + + @Override + public void deleteProjectDataSources( + @NotNull String projectId, + @NotNull String[] dataSourceIds + ) throws DBException { + try (var projectLock = lockController.lockProject(projectId, "deleteDatasources")) { + DBPProject project = getWebProject(projectId, false); + doFileWriteOperation(projectId, project.getMetadataFolder(false), () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + for (String dataSourceId : dataSourceIds) { + DBPDataSourceContainer dataSource = registry.getDataSource(dataSourceId); + + if (dataSource != null) { + log.debug("Deleting data source '" + dataSourceId + "' in project '" + projectId + "'"); + registry.removeDataSource(dataSource); + } else { + log.warn("Could not find datasource " + dataSourceId + " for deletion"); + } + } + registry.checkForErrors(); + return null; + }); + } + } + + @Override + public void createProjectDataSourceFolder( + @NotNull String projectId, + @NotNull String folderPath + ) throws DBException { + try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { + DBPProject project = getWebProject(projectId, false); + log.debug("Creating data source folder '" + folderPath + "' in project '" + projectId + "'"); + doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + var result = Path.of(folderPath); + var newName = result.getFileName().toString(); + GeneralUtils.validateResourceName(newName); + var parent = result.getParent(); + var parentFolder = parent == null ? null : registry.getFolder(parent.toString().replace("\\", "/")); + DBPDataSourceFolder newFolder = registry.addFolder(parentFolder, newName); + registry.checkForErrors(); + return null; + } + ); + } + } + + @Override + public void deleteProjectDataSourceFolders( + @NotNull String projectId, + @NotNull String[] folderPaths, + boolean dropContents + ) throws DBException { + try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { + DBPProject project = getWebProject(projectId, false); + doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + for (String folderPath : folderPaths) { + DBPDataSourceFolder folder = registry.getFolder(folderPath); + if (folder != null) { + log.debug("Deleting data source folder '" + folderPath + "' in project '" + projectId + "'"); + registry.removeFolder(folder, dropContents); + } else { + log.warn("Can not find folder by path [" + folderPath + "] for deletion"); + } + } + registry.checkForErrors(); + return null; + } + ); + } + } + + @Override + public void moveProjectDataSourceFolder( + @NotNull String projectId, + @NotNull String oldPath, + @NotNull String newPath + ) throws DBException { + try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { + DBPProject project = getWebProject(projectId, false); + log.debug("Moving data source folder from '" + oldPath + "' to '" + newPath + "' in project '" + projectId + "'"); + doFileWriteOperation(projectId, project.getMetadataFolder(false), + () -> { + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); + registry.moveFolder(oldPath, newPath); + registry.checkForErrors(); + return null; + } + ); + } + } + + protected abstract BaseWebProjectImpl getWebProject(String projectId, boolean refresh) throws DBException; + + protected abstract T doFileWriteOperation(String projectId, Path file, RMFileOperation operation) + throws DBException; + + protected abstract T doFileReadOperation(String projectId, Path file, RMFileOperation operation) + throws DBException; + + protected abstract T doProjectOperation(String projectId, RMFileOperation operation) throws DBException; + + protected abstract RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException; + + protected void validateResourcePath(String resourcePath) throws DBException { + var fullPath = Paths.get(resourcePath); + for (Path path : fullPath) { + String fileName = IOUtils.getFileNameWithoutExtension(path); + GeneralUtils.validateResourceName(fileName); + } + } + + protected void createFolder(Path targetPath) throws DBException { + if (!Files.exists(targetPath)) { + try { + Files.createDirectories(targetPath); + } catch (IOException e) { + throw new DBException("Error creating folder '" + targetPath + "'"); + } + } + } + + protected class InternalWebProjectImpl extends BaseWebProjectImpl { + public InternalWebProjectImpl( + @NotNull SessionContextImpl sessionContext, + @NotNull RMProject rmProject, + @NotNull Path projectPath + ) { + super( + BaseLocalResourceController.this.workspace, + BaseLocalResourceController.this, + sessionContext, + rmProject, + projectPath + ); + } + + @NotNull + @Override + protected DBPDataSourceRegistry createDataSourceRegistry() { + return new DataSourceRegistry(this); + } + } + + protected void fireRmResourceAddEvent(@NotNull String projectId, @NotNull String resourcePath) throws DBException { + RMEventManager.fireEvent( + new RMEvent(RMEvent.Action.RESOURCE_ADD, + getProject(projectId, false, false), + resourcePath) + ); + } + + protected void fireRmResourceDeleteEvent(@NotNull String projectId, @NotNull String resourcePath) + throws DBException { + RMEventManager.fireEvent( + new RMEvent(RMEvent.Action.RESOURCE_DELETE, + makeProjectFromId(projectId, false), + resourcePath + ) + ); + } + + protected void fireRmProjectAddEvent(@NotNull RMProject project) { + RMEventManager.fireEvent( + new RMEvent( + RMEvent.Action.RESOURCE_ADD, + project + ) + ); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index 5daf0a8d8e..b2b33f11a9 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -29,39 +29,31 @@ import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.DBPDataSourceConfigurationStorage; -import org.jkiss.dbeaver.model.DBPDataSourceContainer; -import org.jkiss.dbeaver.model.DBPDataSourceFolder; -import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; -import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMCredentials; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl; import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl; import org.jkiss.dbeaver.model.rm.*; -import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; import org.jkiss.dbeaver.model.security.SMController; import org.jkiss.dbeaver.model.security.SMObjectType; import org.jkiss.dbeaver.model.sql.DBQuotaException; import org.jkiss.dbeaver.model.websocket.event.MessageType; import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.WSSessionLogUpdatedEvent; -import org.jkiss.dbeaver.registry.*; +import org.jkiss.dbeaver.registry.ResourceTypeDescriptor; +import org.jkiss.dbeaver.registry.ResourceTypeRegistry; import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.dbeaver.utils.GeneralUtils; -import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; import org.jkiss.utils.Pair; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.text.MessageFormat; import java.time.OffsetDateTime; import java.time.ZoneId; import java.util.*; -import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -69,15 +61,10 @@ /** * Resource manager API */ -public class LocalResourceController implements RMController { +public class LocalResourceController extends BaseLocalResourceController { private static final Log log = Log.getLog(LocalResourceController.class); - private static final String FILE_REGEX = "(?U)[\\w.$()@/\\\\ -]+"; - private static final String PROJECT_REGEX = "(?U)[\\w.$()@ -]+"; // slash not allowed in project name - public static final String DEFAULT_CHANGE_ID = "0"; - - private final DBPWorkspace workspace; protected final SMCredentialsProvider credentialsProvider; private final Path rootPath; @@ -85,7 +72,6 @@ public class LocalResourceController implements RMController { private final Path sharedProjectsPath; private final String globalProjectName; private Supplier smControllerSupplier; - protected final RMFileLockController lockController; protected final List fileHandlers; private final Map projectRegistries = new LinkedHashMap<>(); @@ -98,13 +84,12 @@ public LocalResourceController( Path sharedProjectsPath, Supplier smControllerSupplier ) throws DBException { - this.workspace = workspace; + super(workspace, new RMFileLockController(WebAppUtils.getWebApplication())); this.credentialsProvider = credentialsProvider; this.rootPath = rootPath; this.userProjectsPath = userProjectsPath; this.sharedProjectsPath = sharedProjectsPath; this.smControllerSupplier = smControllerSupplier; - this.lockController = new RMFileLockController(WebAppUtils.getWebApplication()); this.globalProjectName = DBWorkbench.getPlatform().getApplication().getDefaultProjectName(); this.fileHandlers = RMFileOperationHandlersRegistry.getInstance().getFileHandlers(); @@ -130,12 +115,7 @@ protected BaseWebProjectImpl getWebProject(String projectId, boolean refresh) th if (project == null || refresh) { SessionContextImpl sessionContext = new SessionContextImpl(null); RMProject rmProject = makeProjectFromId(projectId, false); - project = new BaseWebProjectImpl( - workspace, - this, - sessionContext, - rmProject, - (container) -> true); + project = new InternalWebProjectImpl(sessionContext, rmProject, getProjectPath(projectId)); projectRegistries.put(projectId, project); } return project; @@ -346,182 +326,6 @@ public RMProject getProject(@NotNull String projectId, boolean readResources, bo return project; } - @Override - public Object getProjectProperty(@NotNull String projectId, @NotNull String propName) throws DBException { - var project = getWebProject(projectId, false); - return doFileReadOperation(projectId, project.getMetadataFilePath(), () -> project.getProjectProperty(propName)); - } - - @Override - public void setProjectProperty( - @NotNull String projectId, - @NotNull String propName, - @NotNull Object propValue - ) throws DBException { - BaseWebProjectImpl webProject = getWebProject(projectId, false); - doFileWriteOperation(projectId, webProject.getMetadataFilePath(), - () -> { - log.debug("Updating value for property '" + propName + "' in project '" + projectId + "'"); - webProject.setProjectProperty(propName, propValue); - return null; - } - ); - } - - @Override - public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds) throws DBException { - DBPProject projectMetadata = getWebProject(projectId, false); - return doFileReadOperation( - projectId, - projectMetadata.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = projectMetadata.getDataSourceRegistry(); - registry.refreshConfig(); - registry.checkForErrors(); - DataSourceConfigurationManagerBuffer buffer = new DataSourceConfigurationManagerBuffer(); - Predicate filter = null; - if (!ArrayUtils.isEmpty(dataSourceIds)) { - filter = ds -> ArrayUtils.contains(dataSourceIds, ds.getId()); - } - ((DataSourcePersistentRegistry) registry).saveConfigurationToManager(new VoidProgressMonitor(), buffer, filter); - registry.checkForErrors(); - - return new String(buffer.getData(), StandardCharsets.UTF_8); - } - ); - } - - @Override - public void createProjectDataSources( - @NotNull String projectId, - @NotNull String configuration, - @Nullable List dataSourceIds - ) throws DBException { - updateProjectDataSources(projectId, configuration, dataSourceIds); - } - - @Override - public boolean updateProjectDataSources( - @NotNull String projectId, - @NotNull String configuration, - @Nullable List dataSourceIds - ) throws DBException { - try (var lock = lockController.lockProject(projectId, "updateProjectDataSources")) { - DBPProject project = getWebProject(projectId, false); - return doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - DBPDataSourceConfigurationStorage storage = new DataSourceMemoryStorage(configuration.getBytes(StandardCharsets.UTF_8)); - DataSourceConfigurationManager manager = new DataSourceConfigurationManagerBuffer(); - var configChanged = ((DataSourcePersistentRegistry) registry).loadDataSources( - List.of(storage), - manager, - dataSourceIds, - true, - false - ); - registry.checkForErrors(); - log.debug("Save data sources configuration in project '" + projectId + "'"); - ((DataSourcePersistentRegistry) registry).saveDataSources(); - registry.checkForErrors(); - return configChanged; - } - ); - } - } - - @Override - public void deleteProjectDataSources(@NotNull String projectId, - @NotNull String[] dataSourceIds) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "deleteDatasources")) { - DBPProject project = getWebProject(projectId, false); - doFileWriteOperation(projectId, project.getMetadataFolder(false), () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - for (String dataSourceId : dataSourceIds) { - DBPDataSourceContainer dataSource = registry.getDataSource(dataSourceId); - - if (dataSource != null) { - log.debug("Deleting data source '" + dataSourceId + "' in project '" + projectId + "'"); - registry.removeDataSource(dataSource); - } else { - log.warn("Could not find datasource " + dataSourceId + " for deletion"); - } - } - registry.checkForErrors(); - return null; - }); - } - } - - @Override - public void createProjectDataSourceFolder(@NotNull String projectId, - @NotNull String folderPath) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { - DBPProject project = getWebProject(projectId, false); - log.debug("Creating data source folder '" + folderPath + "' in project '" + projectId + "'"); - doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - var result = Path.of(folderPath); - var newName = result.getFileName().toString(); - GeneralUtils.validateResourceName(newName); - var parent = result.getParent(); - var parentFolder = parent == null ? null : registry.getFolder(parent.toString().replace("\\", "/")); - DBPDataSourceFolder newFolder = registry.addFolder(parentFolder, newName); - registry.checkForErrors(); - return null; - } - ); - } - } - - @Override - public void deleteProjectDataSourceFolders( - @NotNull String projectId, - @NotNull String[] folderPaths, - boolean dropContents - ) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { - DBPProject project = getWebProject(projectId, false); - doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - for (String folderPath : folderPaths) { - DBPDataSourceFolder folder = registry.getFolder(folderPath); - if (folder != null) { - log.debug("Deleting data source folder '" + folderPath + "' in project '" + projectId + "'"); - registry.removeFolder(folder, dropContents); - } else { - log.warn("Can not find folder by path [" + folderPath + "] for deletion"); - } - } - registry.checkForErrors(); - return null; - } - ); - } - } - - @Override - public void moveProjectDataSourceFolder( - @NotNull String projectId, - @NotNull String oldPath, - @NotNull String newPath - ) throws DBException { - try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) { - DBPProject project = getWebProject(projectId, false); - log.debug("Moving data source folder from '" + oldPath + "' to '" + newPath + "' in project '" + projectId + "'"); - doFileWriteOperation(projectId, project.getMetadataFolder(false), - () -> { - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - registry.moveFolder(oldPath, newPath); - registry.checkForErrors(); - return null; - } - ); - } - } - @NotNull @Override public RMResource[] listResources( @@ -629,7 +433,10 @@ public String moveResource( throw new DBException("Resource '" + oldTargetPath + "' doesn't exists"); } Path newTargetPath = getTargetPath(projectId, normalizedNewResourcePath); - validateResourcePath(newTargetPath.toString()); + validateResourcePath(rootPath.relativize(newTargetPath).toString()); + if (Files.exists(newTargetPath)) { + throw new DBException("Resource with name %s already exists".formatted(newTargetPath.getFileName())); + } try { Files.move(oldTargetPath, newTargetPath); } catch (IOException e) { @@ -685,7 +492,6 @@ public void deleteResource(@NotNull String projectId, @NotNull String resourcePa if (log.isDebugEnabled()) { log.debug("Removing resource from '" + resourcePath + "' in project '" + projectId + "'" + (recursive ? " recursive" : "")); } - validateResourcePath(resourcePath); Path targetPath = getTargetPath(projectId, resourcePath); doFileWriteOperation(projectId, targetPath, () -> { if (!Files.exists(targetPath)) { @@ -806,15 +612,6 @@ public String setResourceContents( return DEFAULT_CHANGE_ID; } - protected void createFolder(Path targetPath) throws DBException { - if (!Files.exists(targetPath)) { - try { - Files.createDirectories(targetPath); - } catch (IOException e) { - throw new DBException("Error creating folder '" + targetPath + "'"); - } - } - } @NotNull @Override @@ -859,14 +656,6 @@ public String setResourceProperties( } } - private void validateResourcePath(String resourcePath) throws DBException { - var fullPath = Paths.get(resourcePath); - for (Path path : fullPath) { - String fileName = IOUtils.getFileNameWithoutExtension(path); - GeneralUtils.validateResourceName(fileName); - } - } - @NotNull private Path getTargetPath(@NotNull String projectId, @NotNull String resourcePath) throws DBException { Path projectPath = getProjectPath(projectId); @@ -883,7 +672,7 @@ private Path getTargetPath(@NotNull String projectId, @NotNull String resourcePa if (!targetPath.startsWith(projectPath)) { throw new DBException("Invalid resource path"); } - return WebAppUtils.getWebApplication().getHomeDirectory().relativize(targetPath); + return targetPath; } catch (InvalidPathException e) { throw new DBException("Resource path contains invalid characters"); } @@ -896,7 +685,7 @@ private String makeProjectIdFromPath(Path path, RMProjectType type) { } @Nullable - private RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException { + protected RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException { var projectName = parseProjectName(projectId); var projectPath = getProjectPath(projectId); if (!Files.exists(projectPath)) { @@ -1140,32 +929,6 @@ private String getProjectRelativePath(@NotNull String projectId, @NotNull Path p return getProjectPath(projectId).toAbsolutePath().relativize(path).toString().replace('\\', IPath.SEPARATOR); } - private void fireRmResourceAddEvent(@NotNull String projectId, @NotNull String resourcePath) throws DBException { - RMEventManager.fireEvent( - new RMEvent(RMEvent.Action.RESOURCE_ADD, - getProject(projectId, false, false), - resourcePath) - ); - } - - private void fireRmResourceDeleteEvent(@NotNull String projectId, @NotNull String resourcePath) throws DBException { - RMEventManager.fireEvent( - new RMEvent(RMEvent.Action.RESOURCE_DELETE, - makeProjectFromId(projectId, false), - resourcePath - ) - ); - } - - private void fireRmProjectAddEvent(@NotNull RMProject project) { - RMEventManager.fireEvent( - new RMEvent( - RMEvent.Action.RESOURCE_ADD, - project - ) - ); - } - protected void handleProjectOpened(String projectId) throws DBException { createResourceTypeFolders(getProjectPath(projectId)); } @@ -1286,5 +1049,4 @@ public static boolean isProjectOwner(String projectId, String userId) { rmProjectName.name.equals(userId); } - } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java index bf966ea748..b87526cd92 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java @@ -23,6 +23,7 @@ import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.utils.IOUtils; import java.io.IOException; import java.io.Reader; @@ -73,18 +74,23 @@ public RMFileLockController(WebApplication application, int maxLockTime) throws * @return - lock */ @NotNull - public RMLock lockProject(@NotNull String projectId,@NotNull String operationName) throws DBException { + public RMLock lockProject(@NotNull String projectId, @NotNull String operationName) throws DBException { synchronized (RMFileLockController.class) { try { - createLockFolderIfNeeded(); - createProjectFolder(projectId); - Path projectLockFile = getProjectLockFilePath(projectId); - RMLockInfo lockInfo = new RMLockInfo.Builder(projectId, UUID.randomUUID().toString()) .setApplicationId(applicationId) .setOperationName(operationName) .setOperationStartTime(System.currentTimeMillis()) .build(); + Path projectLockFile = getProjectLockFilePath(projectId); + + if (!IOUtils.isFileFromDefaultFS(lockFolderPath)) { + // fake lock for external file system? + return new RMLock(projectLockFile); + } + createLockFolderIfNeeded(); + createProjectFolder(projectId); + createLockFile(projectLockFile, lockInfo); return new RMLock(projectLockFile); } catch (Exception e) { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java index 9f51137716..43b69e137b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java @@ -51,27 +51,33 @@ public abstract class BaseWebSession extends AbstractSessionPersistent { @NotNull protected final WebUserContext userContext; @NotNull - protected final WebAuthApplication application; + protected final WebApplication application; protected volatile long lastAccessTime; private final List sessionEventHandlers = new CopyOnWriteArrayList<>(); private WebSessionEventsFilter eventsFilter = new WebSessionEventsFilter(); private final WebSessionWorkspace workspace; - public BaseWebSession(@NotNull String id, @NotNull WebAuthApplication application) throws DBException { + public BaseWebSession(@NotNull String id, @NotNull WebApplication application) throws DBException { this.id = id; this.application = application; this.createTime = System.currentTimeMillis(); this.lastAccessTime = this.createTime; - this.workspace = new WebSessionWorkspace(this); + this.workspace = createWebWorkspace(); this.workspace.getAuthContext().addSession(this); this.userContext = createUserContext(); } + @NotNull + protected WebSessionWorkspace createWebWorkspace() { + return new WebSessionWorkspace(this); + } + protected WebUserContext createUserContext() throws DBException { return new WebUserContext(this.application, this.workspace); } + @NotNull public WebSessionWorkspace getWorkspace() { return workspace; } @@ -232,6 +238,9 @@ public boolean isValid() { @Property public long getRemainingTime() { - return application.getMaxSessionIdleTime() + lastAccessTime - System.currentTimeMillis(); + if (application instanceof WebAuthApplication authApplication) { + return authApplication.getMaxSessionIdleTime() + lastAccessTime - System.currentTimeMillis(); + } + return Integer.MAX_VALUE; } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java new file mode 100644 index 0000000000..a6dde6469d --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHttpRequestInfo.java @@ -0,0 +1,57 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model.session; + +import jakarta.servlet.http.HttpServletRequest; + +public class WebHttpRequestInfo { + + private final String id; + private final Object locale; + private final String lastRemoteAddress; + private final String lastRemoteUserAgent; + + public WebHttpRequestInfo(HttpServletRequest request) { + this.id = request.getSession().getId(); + this.locale = request.getAttribute("locale"); + this.lastRemoteAddress = request.getRemoteAddr(); + this.lastRemoteUserAgent = request.getHeader("User-Agent"); + } + + public WebHttpRequestInfo(String id, Object locale, String lastRemoteAddress, String lastRemoteUserAgent) { + this.id = id; + this.locale = locale; + this.lastRemoteAddress = lastRemoteAddress; + this.lastRemoteUserAgent = lastRemoteUserAgent; + } + + public String getId() { + return id; + } + + public Object getLocale() { + return locale; + } + + public String getLastRemoteAddress() { + return lastRemoteAddress; + } + + public String getLastRemoteUserAgent() { + return lastRemoteUserAgent; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index 5b1d052433..ce10a80553 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -19,21 +19,18 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; -import io.cloudbeaver.DBWConstants; -import io.cloudbeaver.DBWebException; -import io.cloudbeaver.DataSourceFilter; -import io.cloudbeaver.WebProjectImpl; +import com.google.gson.Strictness; +import io.cloudbeaver.*; import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.WebServerMessage; +import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.model.user.WebUser; import io.cloudbeaver.service.DBWSessionHandler; import io.cloudbeaver.service.sql.WebSQLConstants; import io.cloudbeaver.utils.CBModelConstants; -import io.cloudbeaver.utils.WebAppUtils; import io.cloudbeaver.utils.WebDataSourceUtils; -import jakarta.servlet.http.HttpServletRequest; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; @@ -43,12 +40,8 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBFileController; import org.jkiss.dbeaver.model.DBPDataSourceContainer; -import org.jkiss.dbeaver.model.DBPEvent; import org.jkiss.dbeaver.model.access.DBAAuthCredentials; import org.jkiss.dbeaver.model.access.DBACredentialsProvider; -import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; -import org.jkiss.dbeaver.model.app.DBPDataSourceRegistryCache; -import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.auth.*; import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; import org.jkiss.dbeaver.model.exec.DBCException; @@ -68,18 +61,15 @@ import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.SMConstants; import org.jkiss.dbeaver.model.security.SMController; -import org.jkiss.dbeaver.model.security.SMObjectType; -import org.jkiss.dbeaver.model.security.user.SMObjectPermissions; import org.jkiss.dbeaver.model.sql.DBQuotaException; import org.jkiss.dbeaver.model.websocket.event.MessageType; import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.WSSessionLogUpdatedEvent; -import org.jkiss.dbeaver.registry.DataSourceDescriptor; import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; import org.jkiss.utils.CommonUtils; import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; import java.time.Instant; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @@ -89,6 +79,7 @@ * Web session. * Is the main source of data in web application */ +//TODO: split to authenticated and non authenticated context public class WebSession extends BaseWebSession implements SMSessionWithAuth, SMCredentialsProvider, DBACredentialsProvider, IAdaptable { @@ -105,12 +96,10 @@ public class WebSession extends BaseWebSession private String lastRemoteAddr; private String lastRemoteUserAgent; - private Set accessibleConnectionIds = Collections.emptySet(); - private String locale; private boolean cacheExpired; - private final Map connections = new HashMap<>(); + protected WebSessionGlobalProjectImpl globalProject; private final List sessionMessages = new ArrayList<>(); private final Map asyncTasks = new HashMap<>(); @@ -124,21 +113,34 @@ public class WebSession extends BaseWebSession private final Map sessionHandlers; public WebSession( - @NotNull HttpServletRequest request, + @NotNull WebHttpRequestInfo requestInfo, @NotNull WebAuthApplication application, @NotNull Map sessionHandlers ) throws DBException { - super(request.getSession().getId(), application); + this(requestInfo.getId(), + CommonUtils.toString(requestInfo.getLocale()), + application, + sessionHandlers + ); + updateSessionParameters(requestInfo); + } + + protected WebSession( + @NotNull String id, + @Nullable String locale, + @NotNull WebApplication application, + @NotNull Map sessionHandlers + ) throws DBException { + super(id, application); this.lastAccessTime = this.createTime; - setLocale(CommonUtils.toString(request.getSession().getAttribute(ATTR_LOCALE), this.locale)); this.sessionHandlers = sessionHandlers; + setLocale(CommonUtils.toString(locale, this.locale)); //force authorization of anonymous session to avoid access error, //because before authorization could be called by any request, //but now 'updateInfo' is called only in special requests, //and the order of requests is not guaranteed. //look at CB-4747 refreshSessionAuth(); - updateSessionParameters(request); } @Nullable @@ -152,8 +154,8 @@ public SMSessionPrincipal getSessionPrincipal() { } } - @NotNull - public DBPProject getSingletonProject() { + @Nullable + public WebSessionProjectImpl getSingletonProject() { return getWorkspace().getActiveProject(); } @@ -250,69 +252,6 @@ public synchronized void refreshUserData() { initNavigatorModel(); } - /** - * updates data sources based on event in web session - * - * @param project project of connection - * @param dataSourceIds list of updated connections - * @param type type of event - */ - public synchronized boolean updateProjectDataSources( - DBPProject project, - List dataSourceIds, - WSEventType type - ) { - var sendDataSourceUpdatedEvent = false; - DBPDataSourceRegistry registry = project.getDataSourceRegistry(); - // save old connections - var oldDataSources = dataSourceIds.stream() - .map(registry::getDataSource) - .filter(Objects::nonNull) - .collect(Collectors.toMap( - DBPDataSourceContainer::getId, - ds -> new DataSourceDescriptor((DataSourceDescriptor) ds, ds.getRegistry()) - )); - if (type == WSEventType.DATASOURCE_CREATED || type == WSEventType.DATASOURCE_UPDATED) { - registry.refreshConfig(dataSourceIds); - } - for (String dsId : dataSourceIds) { - DataSourceDescriptor ds = (DataSourceDescriptor) registry.getDataSource(dsId); - if (ds == null) { - continue; - } - switch (type) { - case DATASOURCE_CREATED -> { - WebConnectionInfo connectionInfo = new WebConnectionInfo(this, ds); - this.connections.put(getConnectionId(ds), connectionInfo); - sendDataSourceUpdatedEvent = true; - } - case DATASOURCE_UPDATED -> // if settings were changed we need to send event - sendDataSourceUpdatedEvent |= !ds.equalSettings(oldDataSources.get(dsId)); - case DATASOURCE_DELETED -> { - WebDataSourceUtils.disconnectDataSource(this, ds); - if (registry instanceof DBPDataSourceRegistryCache dsrc) { - dsrc.removeDataSourceFromList(ds); - } - this.connections.remove(getConnectionId(ds)); - sendDataSourceUpdatedEvent = true; - } - default -> { - } - } - } - return sendDataSourceUpdatedEvent; - } - - @NotNull - private String getConnectionId(@NotNull DBPDataSourceContainer container) { - return getConnectionId(container.getProject().getId(), container.getId()); - } - - @NotNull - private String getConnectionId(@NotNull String projectId, @NotNull String dsId) { - return projectId + ":" + dsId; - } - // Note: for admin use only public synchronized void resetUserState() throws DBException { clearAuthTokens(); @@ -333,7 +272,7 @@ private void initNavigatorModel() { this.navigatorModel.dispose(); this.navigatorModel = null; } - this.connections.clear(); + this.globalProject = null; loadProjects(); @@ -353,14 +292,13 @@ private void loadProjects() { // No anonymous mode in distributed apps return; } - refreshAccessibleConnectionIds(); try { RMController controller = getRmController(); RMProject[] rmProjects = controller.listAccessibleProjects(); for (RMProject project : rmProjects) { createWebProject(project); } - if (user == null) { + if (user == null && application.getAppConfiguration().isAnonymousAccessEnabled()) { WebProjectImpl anonymousProject = createWebProject(RMUtils.createAnonymousProject()); anonymousProject.setInMemory(true); } @@ -373,57 +311,33 @@ private void loadProjects() { } } - public WebProjectImpl createWebProject(RMProject project) { - // Do not filter data sources from user project - DataSourceFilter filter = project.getType() == RMProjectType.GLOBAL - ? this::isDataSourceAccessible - : x -> true; - WebProjectImpl sessionProject = application.createProjectImpl(this, project, filter); + private WebSessionProjectImpl createWebProject(RMProject project) throws DBException { + WebSessionProjectImpl sessionProject; + if (project.isGlobal()) { + sessionProject = createGlobalProject(project); + } else { + sessionProject = new WebSessionProjectImpl(this, project, getProjectPath(project)); + } // do not load data sources for anonymous project if (project.getType() == RMProjectType.USER && userContext.getUser() == null) { sessionProject.setInMemory(true); } - DBPDataSourceRegistry dataSourceRegistry = sessionProject.getDataSourceRegistry(); - dataSourceRegistry.setAuthCredentialsProvider(this); addSessionProject(sessionProject); if (!project.isShared() || application.isConfigurationMode()) { getWorkspace().setActiveProject(sessionProject); } - for (DBPDataSourceContainer ds : dataSourceRegistry.getDataSources()) { - addConnection(new WebConnectionInfo(this, ds)); - } - Throwable lastError = dataSourceRegistry.getLastError(); - if (lastError != null) { - addSessionError(lastError); - log.error("Error refreshing connections from project '" + project.getId() + "'", lastError); - } return sessionProject; } - public void filterAccessibleConnections(List connections) { - connections.removeIf(c -> !isDataSourceAccessible(c.getDataSourceContainer())); + @NotNull + protected Path getProjectPath(@NotNull RMProject project) throws DBException { + return RMUtils.getProjectPath(project); } - private boolean isDataSourceAccessible(DBPDataSourceContainer dataSource) { - return dataSource.isExternallyProvided() || - dataSource.isTemporary() || - this.hasPermission(DBWConstants.PERMISSION_ADMIN) || - accessibleConnectionIds.contains(dataSource.getId()); - } - - @NotNull - private Set readAccessibleConnectionIds() { - try { - return getSecurityController() - .getAllAvailableObjectsPermissions(SMObjectType.datasource) - .stream() - .map(SMObjectPermissions::getObjectId) - .collect(Collectors.toSet()); - } catch (DBException e) { - addSessionError(e); - log.error("Error reading connection grants", e); - return Collections.emptySet(); - } + protected WebSessionProjectImpl createGlobalProject(RMProject project) { + globalProject = new WebSessionGlobalProjectImpl(this, project); + globalProject.refreshAccessibleConnectionIds(); + return globalProject; } private void resetSessionCache() throws DBCException { @@ -441,17 +355,7 @@ private void resetSessionCache() throws DBCException { } private void resetNavigationModel() { - Map conCopy; - synchronized (this.connections) { - conCopy = new HashMap<>(this.connections); - this.connections.clear(); - } - - for (WebConnectionInfo connectionInfo : conCopy.values()) { - if (connectionInfo.isConnected()) { - new DisconnectJob(connectionInfo.getDataSourceContainer()).schedule(); - } - } + getWorkspace().getProjects().forEach(WebSessionProjectImpl::dispose); if (this.navigatorModel != null) { this.navigatorModel.dispose(); @@ -465,7 +369,9 @@ private synchronized void refreshSessionAuth() { authAsAnonymousUser(); } else if (getUserId() != null) { userContext.refreshPermissions(); - refreshAccessibleConnectionIds(); + if (globalProject != null) { + globalProject.refreshAccessibleConnectionIds(); + } } } catch (Exception e) { @@ -474,32 +380,6 @@ private synchronized void refreshSessionAuth() { } } - private synchronized void refreshAccessibleConnectionIds() { - this.accessibleConnectionIds = readAccessibleConnectionIds(); - } - - public synchronized void addAccessibleConnectionToCache(@NotNull String dsId) { - this.accessibleConnectionIds.add(dsId); - var registry = getProjectById(WebAppUtils.getGlobalProjectId()).getDataSourceRegistry(); - var dataSource = registry.getDataSource(dsId); - if (dataSource != null) { - connections.put(getConnectionId(dataSource), new WebConnectionInfo(this, dataSource)); - // reflect changes is navigator model - registry.notifyDataSourceListeners(new DBPEvent(DBPEvent.Action.OBJECT_ADD, dataSource, true)); - } - } - - public synchronized void removeAccessibleConnectionFromCache(@NotNull String dsId) { - var registry = getProjectById(WebAppUtils.getGlobalProjectId()).getDataSourceRegistry(); - var dataSource = registry.getDataSource(dsId); - if (dataSource != null) { - this.accessibleConnectionIds.remove(dsId); - connections.remove(getConnectionId(dataSource)); - // reflect changes is navigator model - registry.notifyDataSourceListeners(new DBPEvent(DBPEvent.Action.OBJECT_REMOVE, dataSource)); - dataSource.dispose(); - } - } private synchronized void authAsAnonymousUser() throws DBException { if (!application.getAppConfiguration().isAnonymousAccessEnabled()) { @@ -558,75 +438,12 @@ public synchronized void updateInfo(boolean isOldHttpSessionUsed) { } } - public synchronized void updateSessionParameters(HttpServletRequest request) { - this.lastRemoteAddr = request.getRemoteAddr(); - this.lastRemoteUserAgent = request.getHeader("User-Agent"); + public synchronized void updateSessionParameters(WebHttpRequestInfo requestInfo) { + this.lastRemoteAddr = requestInfo.getLastRemoteAddress(); + this.lastRemoteUserAgent = requestInfo.getLastRemoteUserAgent(); this.cacheExpired = false; } - @Association - public List getConnections() { - synchronized (connections) { - return new ArrayList<>(connections.values()); - } - } - - @NotNull - public WebConnectionInfo getWebConnectionInfo(@Nullable String projectId, String connectionID) throws DBWebException { - WebConnectionInfo connectionInfo = null; - synchronized (connections) { - if (projectId != null) { - connectionInfo = connections.get(getConnectionId(projectId, connectionID)); - } else { - addWarningMessage("Project id is not defined in request. Try to find it from connection cache"); - for (Map.Entry entry : connections.entrySet()) { - String k = entry.getKey(); - WebConnectionInfo v = entry.getValue(); - if (k.contains(connectionID)) { - connectionInfo = v; - break; - } - } - } - } - if (connectionInfo == null) { - WebProjectImpl project = getProjectById(projectId); - if (project == null) { - throw new DBWebException("Project '" + projectId + "' not found in web workspace"); - } - DBPDataSourceContainer dataSource = project.getDataSourceRegistry().getDataSource(connectionID); - if (dataSource != null) { - connectionInfo = new WebConnectionInfo(this, dataSource); - synchronized (connections) { - connections.put(getConnectionId(dataSource), connectionInfo); - } - } else { - throw new DBWebException("Connection '" + connectionID + "' not found"); - } - } - return connectionInfo; - } - - @Nullable - public WebConnectionInfo findWebConnectionInfo(String projectId, String connectionId) { - synchronized (connections) { - return connections.get(getConnectionId(projectId, connectionId)); - } - } - - public void addConnection(WebConnectionInfo connectionInfo) { - synchronized (connections) { - connections.put(getConnectionId(connectionInfo.getDataSourceContainer()), connectionInfo); - } - } - - public void removeConnection(WebConnectionInfo connectionInfo) { - connectionInfo.clearCache(); - synchronized (connections) { - connections.remove(getConnectionId(connectionInfo.getDataSourceContainer())); - } - } - @Override public void close() { try { @@ -810,7 +627,7 @@ public T getAttribute(String name) { synchronized (attributes) { Object value = attributes.get(name); if (value instanceof PersistentAttribute persistentAttribute) { - value = persistentAttribute.getValue(); + value = persistentAttribute.value(); } return (T) value; } @@ -826,7 +643,7 @@ public T getAttribute(String name, Function creator, Function di synchronized (attributes) { Object value = attributes.get(name); if (value instanceof PersistentAttribute persistentAttribute) { - value = persistentAttribute.getValue(); + value = persistentAttribute.value(); } if (value == null) { value = creator.apply(null); @@ -969,9 +786,12 @@ public boolean provideAuthParameters( } configuration.setRuntimeAttribute(RUNTIME_PARAM_AUTH_INFOS, getAllAuthInfo()); - WebConnectionInfo webConnectionInfo = findWebConnectionInfo(dataSourceContainer.getProject().getId(), dataSourceContainer.getId()); - if (webConnectionInfo != null) { - WebDataSourceUtils.saveCredentialsInDataSource(webConnectionInfo, dataSourceContainer, configuration); + WebSessionProjectImpl project = getProjectById(dataSourceContainer.getProject().getId()); + if (project != null) { + WebConnectionInfo webConnectionInfo = project.findWebConnectionInfo(dataSourceContainer.getId()); + if (webConnectionInfo != null) { + WebDataSourceUtils.saveCredentialsInDataSource(webConnectionInfo, dataSourceContainer, configuration); + } } // uncommented because we had the problem with non-native auth models @@ -980,7 +800,7 @@ public boolean provideAuthParameters( InstanceCreator credTypeAdapter = type -> credentials; Gson credGson = new GsonBuilder() - .setLenient() + .setStrictness(Strictness.LENIENT) .registerTypeAdapter(credentials.getClass(), credTypeAdapter) .create(); @@ -1062,12 +882,17 @@ public void refreshSMSession() throws DBException { } @Nullable - public WebProjectImpl getProjectById(@Nullable String projectId) { + public WebSessionProjectImpl getProjectById(@Nullable String projectId) { return getWorkspace().getProjectById(projectId); } - public WebProjectImpl getAccessibleProjectById(@Nullable String projectId) throws DBWebException { - WebProjectImpl project = null; + /** + * Returns project info from session cache. + * + * @throws DBWebException if project with provided id is not found. + */ + public WebSessionProjectImpl getAccessibleProjectById(@Nullable String projectId) throws DBWebException { + WebSessionProjectImpl project = null; if (projectId != null) { project = getWorkspace().getProjectById(projectId); } @@ -1077,18 +902,24 @@ public WebProjectImpl getAccessibleProjectById(@Nullable String projectId) throw return project; } - public List getAccessibleProjects() { + public List getAccessibleProjects() { return getWorkspace().getProjects(); } - public void addSessionProject(@NotNull WebProjectImpl project) { + /** + * Adds project to session cache and navigator tree. + */ + public void addSessionProject(@NotNull WebSessionProjectImpl project) { getWorkspace().addProject(project); if (navigatorModel != null) { navigatorModel.getRoot().addProject(project, false); } } - public void deleteSessionProject(@Nullable WebProjectImpl project) { + /** + * Removes project from session cache and navigator tree. + */ + public void deleteSessionProject(@Nullable WebSessionProjectImpl project) { if (project != null) { project.dispose(); } @@ -1113,10 +944,6 @@ public void removeSessionProject(@Nullable String projectId) throws DBException return; } deleteSessionProject(project); - var projectConnections = project.getDataSourceRegistry().getDataSources(); - for (DBPDataSourceContainer c : projectConnections) { - removeConnection(new WebConnectionInfo(this, c)); - } } @NotNull @@ -1133,6 +960,11 @@ public DBPPreferenceStore getUserPreferenceStore() { return getUserContext().getPreferenceStore(); } + @Nullable + public WebSessionGlobalProjectImpl getGlobalProject() { + return globalProject; + } + private class SessionProgressMonitor extends BaseProgressMonitor { @Override public void beginTask(String name, int totalWork) { @@ -1167,15 +999,6 @@ public void subTask(String name) { } } - private static class PersistentAttribute { - private final Object value; - - public PersistentAttribute(Object value) { - this.value = value; - } - - public Object getValue() { - return value; - } + private record PersistentAttribute(Object value) { } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java index c6713098a2..dab7d1ccb3 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionEventsFilter.java @@ -50,6 +50,9 @@ public void setSubscribedProjects(@NotNull Set subscribedProjectIds) { } public boolean isEventAllowed(WSEvent event) { + if (event.isForceProcessed()) { + return true; + } if (!subscribedEventTopics.isEmpty() && !subscribedEventTopics.contains(event.getTopicId()) ) { return false; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionWorkspace.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionWorkspace.java index e93ce5a938..87581c2634 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionWorkspace.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionWorkspace.java @@ -16,11 +16,12 @@ */ package io.cloudbeaver.model.session; -import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.WebSessionProjectImpl; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.DBPAdaptable; +import org.jkiss.dbeaver.model.DBPImage; import org.jkiss.dbeaver.model.app.DBPPlatform; -import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl; import org.jkiss.dbeaver.model.rm.RMUtils; @@ -38,8 +39,8 @@ public class WebSessionWorkspace implements DBPWorkspace { private final BaseWebSession session; private final SessionContextImpl workspaceAuthContext; - private final List accessibleProjects = new ArrayList<>(); - private WebProjectImpl activeProject; + private final List accessibleProjects = new ArrayList<>(); + private WebSessionProjectImpl activeProject; public WebSessionWorkspace(BaseWebSession session) { this.session = session; @@ -81,20 +82,20 @@ public Path getMetadataFolder() { @NotNull @Override - public List getProjects() { + public List getProjects() { return accessibleProjects; } @Nullable @Override - public DBPProject getActiveProject() { + public WebSessionProjectImpl getActiveProject() { return activeProject; } @Nullable @Override - public WebProjectImpl getProject(@NotNull String projectName) { - for (WebProjectImpl project : accessibleProjects) { + public WebSessionProjectImpl getProject(@NotNull String projectName) { + for (WebSessionProjectImpl project : accessibleProjects) { if (project.getName().equals(projectName)) { return project; } @@ -104,11 +105,11 @@ public WebProjectImpl getProject(@NotNull String projectName) { @Nullable @Override - public WebProjectImpl getProjectById(@NotNull String projectId) { + public WebSessionProjectImpl getProjectById(@NotNull String projectId) { if (projectId == null) { return activeProject; } - for (WebProjectImpl project : accessibleProjects) { + for (WebSessionProjectImpl project : accessibleProjects) { if (project.getId().equals(projectId)) { return project; } @@ -122,26 +123,36 @@ public SessionContextImpl getAuthContext() { return workspaceAuthContext; } + @Override + public void initializeProjects() { + // noop + } + @Override public void dispose() { clearProjects(); } - public void setActiveProject(DBPProject activeProject) { - this.activeProject = (WebProjectImpl) activeProject; + @Override + public DBPImage getResourceIcon(DBPAdaptable resourceAdapter) { + return null; + } + + public void setActiveProject(WebSessionProjectImpl activeProject) { + this.activeProject = activeProject; } - void addProject(WebProjectImpl project) { + void addProject(WebSessionProjectImpl project) { accessibleProjects.add(project); } - void removeProject(WebProjectImpl project) { + void removeProject(WebSessionProjectImpl project) { accessibleProjects.remove(project); } void clearProjects() { if (!this.accessibleProjects.isEmpty()) { - for (WebProjectImpl project : accessibleProjects) { + for (WebSessionProjectImpl project : accessibleProjects) { project.dispose(); } this.activeProject = null; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java index 08d2b06eaf..f94462297b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java @@ -46,6 +46,7 @@ * Web user context. * Contains user state and services based on available permissions */ +//TODO: split to authenticated and non authenticated context public class WebUserContext implements SMCredentialsProvider { private static final Log log = Log.getLog(WebUserContext.class); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderConfiguration.java index 864d765b41..322c1be6bd 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderConfiguration.java @@ -80,7 +80,7 @@ public String getSignInLink() throws DBException { } private String buildRedirectUrl(String baseUrl) { - return baseUrl + "?" + CBAuthConstants.CB_REDIRECT_URL_REQUEST_PARAM + "=" + WebAppUtils.getWebApplication().getServerURL(); + return baseUrl + "?" + CBAuthConstants.CB_REDIRECT_URL_REQUEST_PARAM + "=" + WebAppUtils.getFullServerUrl(); } @Property @@ -109,6 +109,14 @@ public String getAcsLink() throws DBException { return instance instanceof SMAuthProviderFederated ? ((SMAuthProviderFederated) instance).getAcsLink(getId(), config.getParameters()) : null; } + @Property + public String getEntityIdLink() throws DBException { + SMAuthProvider instance = providerDescriptor.getInstance(); + return instance instanceof SMAuthProviderFederated + ? ((SMAuthProviderFederated) instance).getEntityIdLink(getId(), config.getParameters()) + : null; + } + @Override public String toString() { return getDisplayName(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebDriverRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebDriverRegistry.java similarity index 100% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/registry/WebDriverRegistry.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebDriverRegistry.java diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java new file mode 100644 index 0000000000..a2ae8040a2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java @@ -0,0 +1,161 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import io.cloudbeaver.DBWConstants; +import io.cloudbeaver.model.app.WebApplication; +import org.eclipse.core.runtime.Plugin; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBConstants; +import org.jkiss.dbeaver.model.app.DBACertificateStorage; +import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.dbeaver.model.impl.app.DefaultCertificateStorage; +import org.jkiss.dbeaver.model.qm.QMRegistry; +import org.jkiss.dbeaver.model.qm.QMUtils; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.registry.BasePlatformImpl; +import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; +import org.jkiss.dbeaver.runtime.SecurityProviderUtils; +import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter; +import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl; +import org.jkiss.dbeaver.utils.ContentUtils; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.StandardConstants; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public abstract class BaseGQLPlatform extends BasePlatformImpl { + private static final Log log = Log.getLog(BaseGQLPlatform.class); + public static final String BASE_TEMP_DIR = "dbeaver"; + + private Path tempFolder; + + private QMRegistryImpl queryManager; + private QMLogFileWriter qmLogWriter; + private DBACertificateStorage certificateStorage; + private WebGlobalWorkspace workspace; + + @Override + protected synchronized void initialize() { + // Register BC security provider + SecurityProviderUtils.registerSecurityProvider(); + + // Register properties adapter + this.workspace = new WebGlobalWorkspace(this, (WebApplication) getApplication()); + this.workspace.initializeProjects(); + QMUtils.initApplication(this); + + this.queryManager = new QMRegistryImpl(); + + this.qmLogWriter = new QMLogFileWriter(); + this.queryManager.registerMetaListener(qmLogWriter); + + this.certificateStorage = new DefaultCertificateStorage( + WebPlatformActivator.getInstance() + .getStateLocation() + .toFile() + .toPath() + .resolve(DBConstants.CERTIFICATE_STORAGE_FOLDER)); + super.initialize(); + } + + @Override + protected Plugin getProductPlugin() { + return WebPlatformActivator.getInstance(); + } + + @NotNull + @Override + public DBACertificateStorage getCertificateStorage() { + return certificateStorage; + } + + @NotNull + @Override + public DBPWorkspace getWorkspace() { + return workspace; + } + + @NotNull + public Path getTempFolder(@NotNull DBRProgressMonitor monitor, @NotNull String name) { + + if (tempFolder == null) { + synchronized (this) { + if (tempFolder == null) { + initTempFolder(monitor); + } + } + } + Path folder = tempFolder.resolve(name); + if (!Files.exists(folder)) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + log.error("Error creating temp folder '" + folder.toAbsolutePath() + "'", e); + } + } + return folder; + } + + private void initTempFolder(@NotNull DBRProgressMonitor monitor) { + // Make temp folder + monitor.subTask("Create temp folder"); + String sysTempFolder = System.getProperty(StandardConstants.ENV_TMP_DIR); + if (CommonUtils.isNotEmpty(sysTempFolder)) { + tempFolder = Path.of(sysTempFolder).resolve(BASE_TEMP_DIR).resolve(DBWConstants.WORK_DATA_FOLDER_NAME); + } else { + //we do not use workspace because it can be in external file system + tempFolder = getApplication().getHomeDirectory().resolve(DBWConstants.WORK_DATA_FOLDER_NAME); + } + } + + @NotNull + public abstract WebApplication getApplication(); + + @Override + public synchronized void dispose() { + super.dispose(); + if (this.qmLogWriter != null) { + this.queryManager.unregisterMetaListener(qmLogWriter); + this.qmLogWriter.dispose(); + this.qmLogWriter = null; + } + if (this.queryManager != null) { + this.queryManager.dispose(); + //queryManager = null; + } + DataSourceProviderRegistry.dispose(); + + // Remove temp folder + if (tempFolder != null) { + + if (!ContentUtils.deleteFileRecursive(tempFolder.toFile())) { + log.warn("Can't delete temp folder '" + tempFolder.toAbsolutePath() + "'"); + } + tempFolder = null; + } + } + + @NotNull + public QMRegistry getQueryManager() { + return queryManager; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/CBConstants.java similarity index 97% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/CBConstants.java index f87ae5e1cd..0103994c19 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/CBConstants.java @@ -87,4 +87,6 @@ public class CBConstants { public static final String QUOTA_PROP_FILE_LIMIT = "dataExportFileSizeLimit"; public static final String ADMIN_AUTO_GRANT = "auto-grant"; + public static final String HOST_LOCALHOST = "localhost"; + public static final String HOST_127_0_0_1 = "127.0.0.1"; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalProject.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalProject.java new file mode 100644 index 0000000000..6cf1faec56 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalProject.java @@ -0,0 +1,85 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.dbeaver.model.auth.SMSessionContext; +import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl; +import org.jkiss.dbeaver.registry.DataSourceRegistry; + +import java.nio.file.Path; + +/** + * Web global project. + */ +public class WebGlobalProject extends BaseProjectImpl { + + private static final Log log = Log.getLog(WebGlobalProject.class); + + private final String projectName; + + public WebGlobalProject( + @NotNull DBPWorkspace workspace, + @Nullable SMSessionContext sessionContext, + @NotNull String projectName + ) { + super(workspace, sessionContext); + this.projectName = projectName; + } + + @Override + public boolean isVirtual() { + return false; + } + + @NotNull + @Override + public String getName() { + return projectName; + } + + @NotNull + @Override + public Path getAbsolutePath() { + return getWorkspace().getAbsolutePath().resolve(projectName); + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public void ensureOpen() { + + } + + @Override + public boolean isUseSecretStorage() { + return false; + } + + @NotNull + @Override + protected DBPDataSourceRegistry createDataSourceRegistry() { + return new DataSourceRegistry(this); + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java index 62e5996c4b..d44262923c 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java @@ -16,23 +16,92 @@ */ package io.cloudbeaver.server; -import org.eclipse.core.resources.IWorkspace; +import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.model.app.WebApplication; +import io.cloudbeaver.utils.WebAppUtils; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPPlatform; -import org.jkiss.dbeaver.registry.EclipseWorkspaceImpl; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl; +import org.jkiss.dbeaver.model.impl.app.BaseWorkspaceImpl; +import org.jkiss.utils.CommonUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; /** * Web global workspace. - *

- * Basically just a wrapper around Eclipse workspace. */ -public class WebGlobalWorkspace extends EclipseWorkspaceImpl { +public class WebGlobalWorkspace extends BaseWorkspaceImpl { + + private static final Log log = Log.getLog(WebGlobalWorkspace.class); + + protected final Map projects = new LinkedHashMap<>(); + private WebGlobalProject globalProject; + + private final WebApplication application; + + public WebGlobalWorkspace( + @NotNull DBPPlatform platform, + @NotNull WebApplication application + ) { + super(platform, application.getWorkspaceDirectory()); + this.application = application; + } + + @Override + public void initializeProjects() { + initializeWorkspaceSession(); + + // Load global project + Path globalProjectPath = getAbsolutePath().resolve(WebAppUtils.getGlobalProjectId()); + if (!Files.exists(globalProjectPath)) { + try { + Files.createDirectories(globalProjectPath); + } catch (IOException e) { + log.error("Error creating global project path: " + globalProject, e); + } + } - public WebGlobalWorkspace(DBPPlatform platform, IWorkspace eclipseWorkspace) { - super(platform, eclipseWorkspace); + globalProject = new WebGlobalProject( + this, + getAuthContext(), + CommonUtils.notEmpty(WebAppUtils.getWebApplication().getDefaultProjectName()) + ); + activeProject = globalProject; } + @NotNull @Override - protected String initWorkspaceId() { + public String getWorkspaceId() { return readWorkspaceIdProperty(); } + + @Nullable + @Override + public DBPProject getActiveProject() { + return super.getActiveProject(); + } + + @NotNull + @Override + public List getProjects() { + return Collections.singletonList(globalProject); + } + + @Nullable + @Override + public BaseProjectImpl getProject(@NotNull String projectName) { + if (globalProject != null && globalProject.getId().equals(projectName)) { + return globalProject; + } + return null; + } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java index 8234bf2122..d13eb03bcf 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java @@ -16,8 +16,6 @@ */ package io.cloudbeaver.server; -import org.eclipse.core.resources.IWorkspace; -import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Plugin; import org.jkiss.dbeaver.ModelPreferences; import org.jkiss.dbeaver.model.impl.preferences.BundlePreferenceStore; @@ -25,7 +23,6 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; -import java.io.File; import java.io.PrintStream; /** @@ -35,7 +32,6 @@ public class WebPlatformActivator extends Plugin { // The shared instance private static WebPlatformActivator instance; - private static File configDir; private PrintStream debugWriter; private DBPPreferenceStore preferences; @@ -76,13 +72,6 @@ public DBPPreferenceStore getPreferences() { return preferences; } - /** - * Returns the workspace instance. - */ - public static IWorkspace getWorkspace() { - return ResourcesPlugin.getWorkspace(); - } - protected void shutdownPlatform() { } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPreferenceStore.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java similarity index 91% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPreferenceStore.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java index 09c98a86cb..681262bf36 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPreferenceStore.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java @@ -16,6 +16,7 @@ */ package io.cloudbeaver.server; +import io.cloudbeaver.utils.WebAppUtils; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.impl.preferences.AbstractPreferenceStore; import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; @@ -23,16 +24,12 @@ import java.io.IOException; import java.util.Map; -public class CBPreferenceStore extends AbstractPreferenceStore { - @NotNull - private final CBPlatform cbPlatform; +public class WebServerPreferenceStore extends AbstractPreferenceStore { private final DBPPreferenceStore parentStore; - public CBPreferenceStore( - @NotNull CBPlatform cbPlatform, + public WebServerPreferenceStore( @NotNull DBPPreferenceStore parentStore ) { - this.cbPlatform = cbPlatform; this.parentStore = parentStore; } @@ -187,12 +184,8 @@ public void save() throws IOException { throw new RuntimeException("Not Implemented"); } - public CBApplication getApp() { - return cbPlatform.getApplication(); - } - private Map productConf() { - var app = cbPlatform.getApplication(); - return app.getProductConfiguration(); + var app = WebAppUtils.getWebApplication(); + return app.getServerConfiguration().getProductSettings(); } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBindingServlet.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBindingServlet.java index 1018ee3037..5b7e52cc95 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBindingServlet.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceBindingServlet.java @@ -23,6 +23,9 @@ * Servlet service */ public interface DBWServiceBindingServlet extends DBWServiceBinding { + default boolean isApplicable(WebApplication application) { + return true; + } void addServlets(APPLICATION application, DBWServletContext servletContext) throws DBException; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java index a8ff1bd845..561162394c 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLConstants.java @@ -22,7 +22,6 @@ public class WebSQLConstants { public static final String QUOTA_PROP_ROW_LIMIT = "sqlResultSetRowsLimit"; - public static final String QUOTA_PROP_MEMORY_LIMIT = "sqlResultSetMemoryLimit"; public static final String QUOTA_PROP_QUERY_LIMIT = "sqlMaxRunningQueries"; public static final String QUOTA_PROP_SQL_QUERY_TIMEOUT = "sqlQueryTimeout"; public static final String QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH = "sqlTextPreviewMaxLength"; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java index cd7e6571b3..fdc44975e3 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java @@ -38,6 +38,8 @@ import java.nio.file.Path; import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class WebAppUtils { private static final Log log = Log.getLog(WebAppUtils.class); @@ -199,7 +201,7 @@ public static void addResponseCookie(HttpServletRequest request, HttpServletResp path = path.concat("; SameSite=" + sameSite); } } - + sessionCookie.setHttpOnly(true); sessionCookie.setPath(path); response.addCookie(sessionCookie); } @@ -269,4 +271,13 @@ private static void flattenArray(Object[] array, Map result, Str } } + @NotNull + public static String getFullServerUrl() { + WebApplication application = WebAppUtils.getWebApplication(); + return Stream.of(application.getServerURL(), application.getRootURI()) + .map(WebAppUtils::removeSideSlashes) + .filter(CommonUtils::isNotEmpty) + .collect(Collectors.joining("/")); + } + } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebDataSourceUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebDataSourceUtils.java index aba7767b06..72c8775cd1 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebDataSourceUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebDataSourceUtils.java @@ -18,9 +18,9 @@ import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWebException; +import io.cloudbeaver.WebSessionProjectImpl; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.WebNetworkHandlerConfigInput; -import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -37,6 +37,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; public class WebDataSourceUtils { @@ -113,14 +114,22 @@ private static void setSecureProperties(DBWHandlerConfiguration handlerConfig, W @Nullable public static DBPDataSourceContainer getLocalOrGlobalDataSource( - WebApplication application, WebSession webSession, @Nullable String projectId, String connectionId + WebSession webSession, @Nullable String projectId, String connectionId ) throws DBWebException { DBPDataSourceContainer dataSource = null; if (!CommonUtils.isEmpty(connectionId)) { - dataSource = webSession.getProjectById(projectId).getDataSourceRegistry().getDataSource(connectionId); - if (dataSource == null && (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN) || application.isConfigurationMode())) { + WebSessionProjectImpl project = webSession.getProjectById(projectId); + if (project == null) { + throw new DBWebException("Project '" + projectId + "' not found"); + } + dataSource = project.getDataSourceRegistry().getDataSource(connectionId); + if (dataSource == null && + (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN) || webSession.getApplication().isConfigurationMode())) { // If called for new connection in admin mode then this connection may absent in session registry yet - dataSource = getGlobalDataSourceRegistry().getDataSource(connectionId); + project = webSession.getGlobalProject(); + if (project != null) { + dataSource = project.getDataSourceRegistry().getDataSource(connectionId); + } } } return dataSource; @@ -157,4 +166,28 @@ public static boolean disconnectDataSource(@NotNull WebSession webSession, @NotN } return false; } + + /** + * The method that seeks for web connection in session cache by connection id. + * Mostly used when project id is not defined. + */ + @NotNull + public static WebConnectionInfo getWebConnectionInfo( + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull String connectionId + ) throws DBWebException { + if (projectId == null) { + webSession.addWarningMessage("Project id is not defined in request. Try to find it from connection cache"); + // try to find connection in all accessible projects + Optional optional = webSession.getAccessibleProjects().stream() + .flatMap(p -> p.getConnections().stream()) // get connection cache from web projects + .filter(e -> e.getId().contains(connectionId)) + .findFirst(); + if (optional.isPresent()) { + return optional.get(); + } + } + return webSession.getAccessibleProjectById(projectId).getWebConnectionInfo(connectionId); + } } diff --git a/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF index fc9c336190..c3e4f392e8 100644 --- a/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Community Product Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true -Bundle-Version: 24.1.5.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 24.2.4.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.product.ce/pom.xml b/server/bundles/io.cloudbeaver.product.ce/pom.xml index 4dad51a10d..7ce195b084 100644 --- a/server/bundles/io.cloudbeaver.product.ce/pom.xml +++ b/server/bundles/io.cloudbeaver.product.ce/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.product.ce - 24.1.5-SNAPSHOT + 24.2.4-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF index 30b10121d3..8b033f910a 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF @@ -2,8 +2,8 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Base JDBC drivers Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true -Bundle-Version: 1.0.104.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.109.qualifier +Bundle-Release-Date: 20241104 Bundle-Vendor: DBeaver Corp Bundle-ActivationPolicy: lazy Automatic-Module-Name: io.cloudbeaver.resources.drivers.base diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml index 1ebd7b15e4..672c61a2d2 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.resources.drivers.base - 1.0.104-SNAPSHOT + 1.0.109-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF index fe96f41d69..5f2158d77a 100644 --- a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF @@ -3,14 +3,13 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Server Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true -Bundle-Version: 24.1.5.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 24.2.4.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator Bundle-ClassPath: . Require-Bundle: org.eclipse.core.runtime;visibility:=reexport, - org.eclipse.core.resources;visibility:=reexport, org.apache.commons.jexl, org.jkiss.utils;visibility:=reexport, org.jkiss.dbeaver.model.sql;visibility:=reexport, @@ -29,8 +28,11 @@ Export-Package: io.cloudbeaver, io.cloudbeaver.server.events, io.cloudbeaver.server, io.cloudbeaver.server.actions, + io.cloudbeaver.server.jetty, + io.cloudbeaver.server.graphql, io.cloudbeaver.server.jobs, io.cloudbeaver.server.servlets, + io.cloudbeaver.server.websockets, io.cloudbeaver.service, io.cloudbeaver.service.navigator, io.cloudbeaver.service.session, diff --git a/server/bundles/io.cloudbeaver.server/pom.xml b/server/bundles/io.cloudbeaver.server/pom.xml index 214f872d23..80affe3f5e 100644 --- a/server/bundles/io.cloudbeaver.server/pom.xml +++ b/server/bundles/io.cloudbeaver.server/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.server - 24.1.5-SNAPSHOT + 24.2.4-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java index 3383195c22..46ff6fa6bb 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java @@ -19,18 +19,18 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; +import com.google.gson.Strictness; import io.cloudbeaver.model.WebConnectionConfig; import io.cloudbeaver.model.WebNetworkHandlerConfigInput; import io.cloudbeaver.model.WebPropertyInfo; +import io.cloudbeaver.model.config.CBAppConfig; import io.cloudbeaver.model.session.WebActionParameters; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; -import io.cloudbeaver.server.CBAppConfig; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.navigator.WebPropertyFilter; -import io.cloudbeaver.utils.WebAppUtils; import io.cloudbeaver.utils.WebCommonUtils; import io.cloudbeaver.utils.WebDataSourceUtils; import org.jkiss.code.NotNull; @@ -98,10 +98,6 @@ public static DBPDataSourceRegistry getGlobalDataSourceRegistry() throws DBWebEx return WebDataSourceUtils.getGlobalDataSourceRegistry(); } - public static DBPDataSourceRegistry getGlobalRegistry(WebSession session) { - return session.getProjectById(WebAppUtils.getGlobalProjectId()).getDataSourceRegistry(); - } - public static InputStream openStaticResource(String path) { return WebServiceUtils.class.getClassLoader().getResourceAsStream(path); } @@ -299,7 +295,7 @@ public static void saveAuthProperties( // Make new Gson parser with type adapters to deserialize into existing credentials InstanceCreator credTypeAdapter = type -> credentials; Gson credGson = new GsonBuilder() - .setLenient() + .setStrictness(Strictness.LENIENT) .registerTypeAdapter(credentials.getClass(), credTypeAdapter) .create(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java index f9e207bbcb..c1bf020243 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java @@ -18,9 +18,9 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.model.config.CBAppConfig; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.utils.ConfigurationUtils; -import io.cloudbeaver.server.CBAppConfig; import io.cloudbeaver.server.CBApplication; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatasourceAccessCheckHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatasourceAccessCheckHandler.java index 2e0fc051d5..ddd680fb03 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatasourceAccessCheckHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatasourceAccessCheckHandler.java @@ -17,14 +17,19 @@ package io.cloudbeaver.model; +import io.cloudbeaver.model.config.CBAppConfig; import io.cloudbeaver.model.utils.ConfigurationUtils; -import io.cloudbeaver.server.CBAppConfig; import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.utils.WebAppUtils; import org.jkiss.dbeaver.model.connection.DBPDriver; +//TODO move to a separate CBApplication plugin public class WebDatasourceAccessCheckHandler extends BaseDatasourceAccessCheckHandler { @Override protected boolean isDriverDisabled(DBPDriver driver) { + if (!WebAppUtils.getWebApplication().isMultiuser()) { + return false; + } CBAppConfig config = CBApplication.getInstance().getAppConfiguration(); return !ConfigurationUtils.isDriverEnabled( driver, diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java index 826331eea4..02b99023ec 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java @@ -16,11 +16,11 @@ */ package io.cloudbeaver.model; +import io.cloudbeaver.model.config.PasswordPolicyConfiguration; import io.cloudbeaver.registry.WebServiceDescriptor; import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; -import io.cloudbeaver.service.security.PasswordPolicyConfiguration; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java index c3c211c970..e9caa0ea1d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/user/WebAuthProviderInfo.java @@ -19,6 +19,7 @@ import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.auth.SMAuthProviderFederated; import io.cloudbeaver.auth.provisioning.SMProvisioner; +import io.cloudbeaver.model.config.CBAppConfig; import io.cloudbeaver.registry.WebAuthProviderConfiguration; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.server.CBApplication; @@ -64,7 +65,10 @@ public String getDescription() { } public boolean isDefaultProvider() { - return descriptor.getId().equals(CBPlatform.getInstance().getApplication().getAppConfiguration().getDefaultAuthProvider()); + if (CBPlatform.getInstance().getApplication().getAppConfiguration() instanceof CBAppConfig cbAppConfig) { + return descriptor.getId().equals(cbAppConfig.getDefaultAuthProvider()); + } + return false; } public boolean isConfigurable() { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/AppWebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/AppWebSessionManager.java new file mode 100644 index 0000000000..13cfc7796a --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/AppWebSessionManager.java @@ -0,0 +1,64 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebHeadlessSession; +import io.cloudbeaver.model.session.WebSession; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Session; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; + +import java.util.Collection; + +public interface AppWebSessionManager { + BaseWebSession closeSession(@NotNull HttpServletRequest request); + + @NotNull + WebSession getWebSession( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response + ) throws DBWebException; + + @NotNull + WebSession getWebSession( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + boolean errorOnNoFound + ) throws DBWebException; + + @Nullable + BaseWebSession getSession(@NotNull String sessionId); + + @Nullable + WebSession findWebSession(HttpServletRequest request); + + WebSession findWebSession(HttpServletRequest request, boolean errorOnNoFound) throws DBWebException; + + Collection getAllActiveSessions(); + + WebSession getOrRestoreSession(Request httpRequest); + + WebHeadlessSession getHeadlessSession(Request request, Session session, boolean create) throws DBException; + + boolean touchSession(HttpServletRequest request, HttpServletResponse response) throws DBWebException; +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index 32cc1ac346..18fde20361 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -21,12 +21,14 @@ import io.cloudbeaver.model.app.BaseWebApplication; import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.model.app.WebAuthConfiguration; +import io.cloudbeaver.model.config.CBAppConfig; +import io.cloudbeaver.model.config.CBServerConfig; +import io.cloudbeaver.model.config.SMControllerConfiguration; import io.cloudbeaver.registry.WebDriverRegistry; import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.jetty.CBJettyServer; import io.cloudbeaver.service.DBWServiceInitializer; import io.cloudbeaver.service.DBWServiceServerConfigurator; -import io.cloudbeaver.service.security.SMControllerConfiguration; import io.cloudbeaver.service.session.WebSessionManager; import io.cloudbeaver.utils.WebDataSourceUtils; import org.eclipse.core.runtime.Platform; @@ -41,13 +43,13 @@ import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.impl.app.BaseApplicationImpl; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.SMConstants; import org.jkiss.dbeaver.model.security.SMObjectType; import org.jkiss.dbeaver.model.websocket.event.WSEventController; import org.jkiss.dbeaver.model.websocket.event.WSServerConfigurationChangedEvent; -import org.jkiss.dbeaver.registry.BaseApplicationImpl; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.runtime.ui.DBPPlatformUI; import org.jkiss.dbeaver.utils.GeneralUtils; @@ -57,24 +59,22 @@ import org.jkiss.utils.StandardConstants; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.URL; import java.net.UnknownHostException; +import java.nio.file.Files; import java.nio.file.Path; -import java.security.Permission; -import java.security.Policy; -import java.security.ProtectionDomain; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * This class controls all aspects of the application's execution */ -public abstract class CBApplication extends BaseWebApplication implements WebAuthApplication { +public abstract class CBApplication extends + BaseWebApplication implements WebAuthApplication, GQLApplicationAdapter { private static final Log log = Log.getLog(CBApplication.class); @@ -83,8 +83,6 @@ public abstract class CBApplication extends BaseWebApp * In configuration mode sessions expire after a week */ private static final long CONFIGURATION_MODE_SESSION_IDLE_TIME = 60 * 60 * 1000 * 24 * 7; - public static final String HOST_LOCALHOST = "localhost"; - public static final String HOST_127_0_0_1 = "127.0.0.1"; static { @@ -111,6 +109,8 @@ public static CBApplication getInstance() { private final Map initActions = new ConcurrentHashMap<>(); + private CBJettyServer jettyServer; + public CBApplication() { this.homeDirectory = new File(initHomeFolder()); } @@ -203,6 +203,7 @@ protected void startServer() { if (!loadServerConfiguration()) { return; } + if (CommonUtils.isEmpty(this.getAppConfiguration().getDefaultUserTeam())) { throw new DBException("Default user team must be specified"); } @@ -210,6 +211,7 @@ protected void startServer() { log.error(e); return; } + refreshDisabledDriversConfig(); configurationMode = CommonUtils.isEmpty(getServerConfiguration().getServerName()); @@ -221,9 +223,9 @@ protected void startServer() { if (CommonUtils.isEmpty(localHostAddress)) { localHostAddress = System.getProperty(CBConstants.VAR_CB_LOCAL_HOST_ADDR); } - if (CommonUtils.isEmpty(localHostAddress) || HOST_127_0_0_1.equals(localHostAddress) || "::0".equals( + if (CommonUtils.isEmpty(localHostAddress) || CBConstants.HOST_127_0_0_1.equals(localHostAddress) || "::0".equals( localHostAddress)) { - localHostAddress = HOST_LOCALHOST; + localHostAddress = CBConstants.HOST_LOCALHOST; } final Runtime runtime = Runtime.getRuntime(); @@ -231,7 +233,7 @@ protected void startServer() { Location instanceLoc = Platform.getInstanceLocation(); try { - if (!instanceLoc.isSet()) { + if (!instanceLoc.isSet()) { // always false? URL wsLocationURL = new URL( "file", //$NON-NLS-1$ null, @@ -305,7 +307,7 @@ protected void startServer() { if (configurationMode) { // Try to configure automatically - performAutoConfiguration(getMainConfigurationFilePath().toFile().getParentFile()); + performAutoConfiguration(getMainConfigurationFilePath().getParent()); } else if (!isMultiNode()) { var appConfiguration = getServerConfigurationController().getAppConfiguration(); if (appConfiguration.isGrantConnectionsAccessToAnonymousTeam()) { @@ -314,16 +316,6 @@ protected void startServer() { grantPermissionsToConnections(); } - if (getServerConfiguration().isEnableSecurityManager()) { - Policy.setPolicy(new Policy() { - @Override - public boolean implies(ProtectionDomain domain, Permission permission) { - return true; - } - }); - System.setSecurityManager(new SecurityManager()); - } - eventController.scheduleCheckJob(); runWebServer(); @@ -343,7 +335,7 @@ protected void initializeAdditionalConfiguration() { * * @param configPath */ - protected void performAutoConfiguration(File configPath) { + protected void performAutoConfiguration(Path configPath) { String autoServerName = System.getenv(CBConstants.VAR_AUTO_CB_SERVER_NAME); String autoServerURL = System.getenv(CBConstants.VAR_AUTO_CB_SERVER_URL); String autoAdminName = System.getenv(CBConstants.VAR_AUTO_CB_ADMIN_NAME); @@ -352,11 +344,11 @@ protected void performAutoConfiguration(File configPath) { if (CommonUtils.isEmpty(autoServerName) || CommonUtils.isEmpty(autoAdminName) || CommonUtils.isEmpty( autoAdminPassword)) { // Try to load from auto config file - if (configPath.exists()) { - File autoConfigFile = new File(configPath, CBConstants.AUTO_CONFIG_FILE_NAME); - if (autoConfigFile.exists()) { + if (Files.exists(configPath)) { + Path autoConfigFile = configPath.resolve(CBConstants.AUTO_CONFIG_FILE_NAME); + if (Files.exists(autoConfigFile)) { Properties autoProps = new Properties(); - try (InputStream is = new FileInputStream(autoConfigFile)) { + try (InputStream is = Files.newInputStream(autoConfigFile)) { autoProps.load(is); autoServerName = autoProps.getProperty(CBConstants.VAR_AUTO_CB_SERVER_NAME); @@ -364,7 +356,7 @@ protected void performAutoConfiguration(File configPath) { autoAdminName = autoProps.getProperty(CBConstants.VAR_AUTO_CB_ADMIN_NAME); autoAdminPassword = autoProps.getProperty(CBConstants.VAR_AUTO_CB_ADMIN_PASSWORD); } catch (IOException e) { - log.error("Error loading auto configuration file '" + autoConfigFile.getAbsolutePath() + "'", + log.error("Error loading auto configuration file '" + autoConfigFile + "'", e); } } @@ -451,11 +443,6 @@ public Path getDataDirectory(boolean create) { return dataDir.toPath(); } - @Override - public Path getWorkspaceDirectory() { - return Path.of(getServerConfiguration().getWorkspaceLocation()); - } - private void initializeSecurityController() throws DBException { securityController = createGlobalSecurityController(); } @@ -480,7 +467,8 @@ private void runWebServer() { getServerPort(), CommonUtils.isEmpty(getServerHost()) ? "all interfaces" : getServerHost()) ); - new CBJettyServer(this).runServer(); + this.jettyServer = new CBJettyServer(this); + this.jettyServer.runServer(); } @@ -580,6 +568,9 @@ public synchronized void reloadConfiguration(@Nullable SMCredentialsProvider cre sendConfigChangedEvent(credentialsProvider); eventController.setForceSkipEvents(isConfigurationMode()); + if (this.jettyServer != null) { + this.jettyServer.refreshJettyConfig(); + } } protected abstract void finishSecurityServiceConfiguration( diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java index 7a00e3474c..1ee1b5c0f8 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplicationCE.java @@ -17,6 +17,7 @@ package io.cloudbeaver.server; import io.cloudbeaver.auth.NoAuthCredentialsProvider; +import io.cloudbeaver.model.config.CBServerConfig; import io.cloudbeaver.model.rm.local.LocalResourceController; import io.cloudbeaver.service.security.CBEmbeddedSecurityController; import io.cloudbeaver.service.security.EmbeddedSecurityControllerFactory; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java index 4238fb2e70..0cc1833109 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java @@ -22,40 +22,24 @@ import io.cloudbeaver.server.jobs.WebDataSourceMonitorJob; import io.cloudbeaver.server.jobs.WebSessionMonitorJob; import io.cloudbeaver.service.session.WebSessionManager; -import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Plugin; import org.eclipse.core.runtime.Status; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; -import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.DBConstants; import org.jkiss.dbeaver.model.DBFileController; -import org.jkiss.dbeaver.model.app.DBACertificateStorage; -import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor; import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; -import org.jkiss.dbeaver.model.impl.app.DefaultCertificateStorage; import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; -import org.jkiss.dbeaver.model.qm.QMRegistry; -import org.jkiss.dbeaver.model.qm.QMUtils; import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; -import org.jkiss.dbeaver.registry.BasePlatformImpl; import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; import org.jkiss.dbeaver.runtime.DBWorkbench; -import org.jkiss.dbeaver.runtime.SecurityProviderUtils; -import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter; -import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl; -import org.jkiss.dbeaver.utils.ContentUtils; import org.jkiss.utils.IOUtils; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -63,7 +47,7 @@ /** * CBPlatform */ -public class CBPlatform extends BasePlatformImpl { +public class CBPlatform extends BaseGQLPlatform { // The plug-in ID public static final String PLUGIN_ID = "io.cloudbeaver.server"; //$NON-NLS-1$ @@ -72,28 +56,20 @@ public class CBPlatform extends BasePlatformImpl { public static final String TEMP_FILE_FOLDER = "temp-sql-upload-files"; public static final String TEMP_FILE_IMPORT_FOLDER = "temp-import-files"; - public static final String WORK_DATA_FOLDER_NAME = ".work-data"; - @Nullable - private static CBApplication application = null; - - private Path tempFolder; + private static GQLApplicationAdapter application = null; - private QMRegistryImpl queryManager; - private QMLogFileWriter qmLogWriter; - private DBACertificateStorage certificateStorage; - private WebGlobalWorkspace workspace; - private CBPreferenceStore preferenceStore; - private final List applicableDrivers = new ArrayList<>(); + private WebServerPreferenceStore preferenceStore; + protected final List applicableDrivers = new ArrayList<>(); public static CBPlatform getInstance() { return (CBPlatform) DBWorkbench.getPlatform(); } - CBPlatform() { + protected CBPlatform() { } - public static void setApplication(CBApplication application) { + public static void setApplication(@NotNull GQLApplicationAdapter application) { CBPlatform.application = application; } @@ -101,33 +77,24 @@ public static void setApplication(CBApplication application) { protected synchronized void initialize() { long startTime = System.currentTimeMillis(); log.info("Initialize web platform...: "); - this.preferenceStore = new CBPreferenceStore(this, WebPlatformActivator.getInstance().getPreferences()); - // Register BC security provider - SecurityProviderUtils.registerSecurityProvider(); - - // Register properties adapter - this.workspace = new WebGlobalWorkspace(this, ResourcesPlugin.getWorkspace()); - this.workspace.initializeProjects(); - - QMUtils.initApplication(this); - this.queryManager = new QMRegistryImpl(); - - this.qmLogWriter = new QMLogFileWriter(); - this.queryManager.registerMetaListener(qmLogWriter); - - this.certificateStorage = new DefaultCertificateStorage( - WebPlatformActivator.getInstance().getStateLocation().toFile().toPath().resolve(DBConstants.CERTIFICATE_STORAGE_FOLDER)); + this.preferenceStore = new WebServerPreferenceStore(WebPlatformActivator.getInstance().getPreferences()); super.initialize(); - refreshApplicableDrivers(); - new WebSessionMonitorJob(this) - .scheduleMonitor(); + scheduleServerJobs(); + log.info("Web platform initialized (" + (System.currentTimeMillis() - startTime) + "ms)"); + } - new SessionStateJob(this) - .scheduleMonitor(); + protected void scheduleServerJobs() { + if (getSessionManager() instanceof WebSessionManager webSessionManager) { + new WebSessionMonitorJob(this, webSessionManager) + .scheduleMonitor(); - new WebDataSourceMonitorJob(this) + new SessionStateJob(this, webSessionManager) + .scheduleMonitor(); + } + + new WebDataSourceMonitorJob(this, getSessionManager()) .scheduleMonitor(); new AbstractJob("Delete temp folder") { @@ -142,7 +109,6 @@ protected IStatus run(DBRProgressMonitor monitor) { return Status.OK_STATUS; } }.schedule(); - log.info("Web platform initialized (" + (System.currentTimeMillis() - startTime) + "ms)"); } public synchronized void dispose() { @@ -151,34 +117,6 @@ public synchronized void dispose() { super.dispose(); - if (this.qmLogWriter != null) { - this.queryManager.unregisterMetaListener(qmLogWriter); - this.qmLogWriter.dispose(); - this.qmLogWriter = null; - } - if (this.queryManager != null) { - this.queryManager.dispose(); - //queryManager = null; - } - DataSourceProviderRegistry.dispose(); - - if (workspace != null) { - try { - workspace.save(new VoidProgressMonitor()); - } catch (DBException ex) { - log.error("Can't save workspace", ex); //$NON-NLS-1$ - } - } - - // Remove temp folder - if (tempFolder != null) { - - if (!ContentUtils.deleteFileRecursive(tempFolder.toFile())) { - log.warn("Can't delete temp folder '" + tempFolder.toAbsolutePath() + "'"); - } - tempFolder = null; - } - CBPlatform.application = null; System.gc(); log.debug("Shutdown completed in " + (System.currentTimeMillis() - startTime) + "ms"); @@ -186,13 +124,7 @@ public synchronized void dispose() { @NotNull @Override - public DBPWorkspace getWorkspace() { - return workspace; - } - - @NotNull - @Override - public CBApplication getApplication() { + public GQLApplicationAdapter getApplication() { return application; } @@ -200,10 +132,6 @@ public List getApplicableDrivers() { return applicableDrivers; } - @NotNull - public QMRegistry getQueryManager() { - return queryManager; - } @NotNull @Override @@ -211,48 +139,12 @@ public DBPPreferenceStore getPreferenceStore() { return preferenceStore; } - @NotNull - @Override - public DBACertificateStorage getCertificateStorage() { - return certificateStorage; - } - - @NotNull - public Path getTempFolder(@NotNull DBRProgressMonitor monitor, @NotNull String name) { - if (tempFolder == null) { - // Make temp folder - monitor.subTask("Create temp folder"); - tempFolder = workspace.getAbsolutePath().resolve(WORK_DATA_FOLDER_NAME); - } - if (!Files.exists(tempFolder)) { - try { - Files.createDirectories(tempFolder); - } catch (IOException e) { - log.error("Can't create temp directory " + tempFolder, e); - } - } - Path folder = tempFolder.resolve(name); - if (!Files.exists(folder)) { - try { - Files.createDirectories(folder); - } catch (IOException e) { - log.error("Error creating temp folder '" + folder.toAbsolutePath() + "'", e); - } - } - return folder; - } - - @Override - protected Plugin getProductPlugin() { - return WebPlatformActivator.getInstance(); - } - @Override public boolean isShuttingDown() { return false; } - public WebSessionManager getSessionManager() { + public AppWebSessionManager getSessionManager() { return application.getSessionManager(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatformActivator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatformActivator.java index 8bfd80106d..cee656629f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatformActivator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatformActivator.java @@ -28,7 +28,7 @@ public class CBPlatformActivator extends WebPlatformActivator { protected void shutdownPlatform() { try { // Dispose core - if (DBWorkbench.getPlatform() instanceof CBPlatform cbPlatform) { + if (DBWorkbench.isPlatformStarted() && DBWorkbench.getPlatform() instanceof CBPlatform cbPlatform) { cbPlatform.dispose(); } } catch (Throwable e) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java index b4233baf3f..637ea382d3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java @@ -16,13 +16,13 @@ */ package io.cloudbeaver.server; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.InstanceCreator; +import com.google.gson.*; import io.cloudbeaver.model.app.BaseServerConfigurationController; import io.cloudbeaver.model.app.BaseWebApplication; -import io.cloudbeaver.service.security.PasswordPolicyConfiguration; -import io.cloudbeaver.service.security.SMControllerConfiguration; +import io.cloudbeaver.model.config.CBAppConfig; +import io.cloudbeaver.model.config.CBServerConfig; +import io.cloudbeaver.model.config.PasswordPolicyConfiguration; +import io.cloudbeaver.model.config.SMControllerConfiguration; import io.cloudbeaver.utils.WebAppUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -40,6 +40,7 @@ import org.jkiss.dbeaver.utils.PrefUtils; import org.jkiss.dbeaver.utils.SystemVariablesResolver; import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; import java.io.*; import java.net.InetAddress; @@ -66,6 +67,7 @@ public abstract class CBServerConfigurationController private final Map originalConfigurationProperties = new LinkedHashMap<>(); protected CBServerConfigurationController(@NotNull T serverConfiguration, @NotNull Path homeDirectory) { + super(homeDirectory); this.serverConfiguration = serverConfiguration; this.homeDirectory = homeDirectory; } @@ -89,17 +91,25 @@ public void loadServerConfiguration(Path configPath) throws DBException { loadConfiguration(configPath); } + initWorkspacePath(); + // Try to load configuration from runtime app config file Path runtimeConfigPath = getRuntimeAppConfigPath(); if (Files.exists(runtimeConfigPath)) { log.debug("Runtime configuration [" + runtimeConfigPath.toAbsolutePath() + "]"); loadConfiguration(runtimeConfigPath); } - // Set default preferences PrefUtils.setDefaultPreferenceValue(DBWorkbench.getPlatform().getPreferenceStore(), ModelPreferences.UI_DRIVERS_HOME, getServerConfiguration().getDriversLocation()); + validateFinalServerConfiguration(); + } + + @NotNull + @Override + protected String getWorkspaceLocation() { + return getServerConfiguration().getWorkspaceLocation(); } public void loadConfiguration(Path configPath) throws DBException { @@ -144,7 +154,7 @@ protected void parseConfiguration(Map configProps) throws DBExce ); // App config Map appConfig = JSONUtils.getObject(configProps, "app"); - validateConfiguration(appConfig); + preValidateAppConfiguration(appConfig); gson.fromJson(gson.toJson(appConfig), CBAppConfig.class); readProductConfiguration(serverConfig, gson); } @@ -158,7 +168,7 @@ public T parseServerConfiguration() { hostName = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { log.debug("Error resolving localhost address: " + e.getMessage()); - hostName = CBApplication.HOST_LOCALHOST; + hostName = CBConstants.HOST_LOCALHOST; } } config.setServerURL("http://" + hostName + ":" + config.getServerPort()); @@ -167,7 +177,6 @@ public T parseServerConfiguration() { config.setContentRoot(WebAppUtils.getRelativePath(config.getContentRoot(), homeDirectory)); config.setRootURI(readRootUri(config.getRootURI())); config.setDriversLocation(WebAppUtils.getRelativePath(config.getDriversLocation(), homeDirectory)); - config.setWorkspaceLocation(WebAppUtils.getRelativePath(config.getWorkspaceLocation(), homeDirectory)); String staticContentsFile = config.getStaticContent(); if (!CommonUtils.isEmpty(staticContentsFile)) { @@ -180,10 +189,11 @@ public T parseServerConfiguration() { return config; } - protected void validateConfiguration(Map appConfig) throws DBException { + protected void preValidateAppConfiguration(Map appConfig) throws DBException { } + private void readExternalProperties(Map serverConfig) { String externalPropertiesFile = JSONUtils.getString(serverConfig, CBConstants.PARAM_EXTERNAL_PROPERTIES); if (!CommonUtils.isEmpty(externalPropertiesFile)) { @@ -246,19 +256,21 @@ protected void readProductConfiguration(Map serverConfig, Gson g } } - // Add product config from runtime - File rtConfig = getRuntimeProductConfigFilePath().toFile(); - if (rtConfig.exists()) { - log.debug("Load product runtime configuration from '" + rtConfig.getAbsolutePath() + "'"); - try (Reader reader = new InputStreamReader(new FileInputStream(rtConfig), StandardCharsets.UTF_8)) { - var runtimeProductSettings = JSONUtils.parseMap(gson, reader); - var productSettings = serverConfiguration.getProductSettings(); - runtimeProductSettings.putAll(productSettings); - Map flattenConfig = WebAppUtils.flattenMap(runtimeProductSettings); - productSettings.clear(); - productSettings.putAll(flattenConfig); - } catch (Exception e) { - throw new DBException("Error reading product runtime configuration", e); + if (workspacePath != null && IOUtils.isFileFromDefaultFS(getWorkspacePath())) { + // Add product config from runtime + Path rtConfig = getRuntimeProductConfigFilePath(); + if (Files.exists(rtConfig)) { + log.debug("Load product runtime configuration from '" + rtConfig + "'"); + try (Reader reader = new InputStreamReader(Files.newInputStream(rtConfig), StandardCharsets.UTF_8)) { + var runtimeProductSettings = JSONUtils.parseMap(gson, reader); + var productSettings = serverConfiguration.getProductSettings(); + runtimeProductSettings.putAll(productSettings); + Map flattenConfig = WebAppUtils.flattenMap(runtimeProductSettings); + productSettings.clear(); + productSettings.putAll(flattenConfig); + } catch (Exception e) { + throw new DBException("Error reading product runtime configuration", e); + } } } } @@ -305,13 +317,14 @@ protected Map readConfiguration(Path configPath) throws DBExcept } public Map readConfigurationFile(Path path) throws DBException { - try (Reader reader = new InputStreamReader(new FileInputStream(path.toFile()), StandardCharsets.UTF_8)) { + try (Reader reader = new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8)) { return JSONUtils.parseMap(getGson(), reader); } catch (Exception e) { throw new DBException("Error parsing server configuration", e); } } + @NotNull protected GsonBuilder getGsonBuilder() { // Stupid way to populate existing objects but ok google (https://github.com/google/gson/issues/431) InstanceCreator appConfigCreator = type -> appConfiguration; @@ -322,7 +335,8 @@ protected GsonBuilder getGsonBuilder() { InstanceCreator smPasswordPoliceConfigCreator = type -> securityManagerConfiguration.getPasswordPolicyConfiguration(); return new GsonBuilder() - .setLenient() + .setStrictness(Strictness.LENIENT) + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) .registerTypeAdapter(getServerConfiguration().getClass(), serverConfigCreator) .registerTypeAdapter(CBAppConfig.class, appConfigCreator) .registerTypeAdapter(DataSourceNavigatorSettings.class, navSettingsCreator) @@ -356,10 +370,9 @@ private synchronized void writeRuntimeConfig(Path runtimeConfigPath, Map productConfiguration) throws DBException { @@ -625,4 +640,15 @@ private String readRootUri(String uri) { } return uri; } + + @NotNull + @Override + public Map getOriginalConfigurationProperties() { + return originalConfigurationProperties; + } + + @Override + public void validateFinalServerConfiguration() throws DBException { + + } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java index 600b7aeed4..87d28cd7d1 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java @@ -19,7 +19,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; -import io.cloudbeaver.service.security.db.WebDatabaseConfig; +import io.cloudbeaver.model.config.CBServerConfig; +import io.cloudbeaver.model.config.WebDatabaseConfig; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.json.JSONUtils; @@ -91,6 +92,7 @@ private void savePasswordPolicyConfig(Map originServerConfig, Ma } } + @NotNull @Override protected GsonBuilder getGsonBuilder() { GsonBuilder gsonBuilder = super.getGsonBuilder(); @@ -99,6 +101,4 @@ protected GsonBuilder getGsonBuilder() { return gsonBuilder .registerTypeAdapter(WebDatabaseConfig.class, dbConfigCreator); } - - } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/GQLApplicationAdapter.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/GQLApplicationAdapter.java new file mode 100644 index 0000000000..f747216547 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/GQLApplicationAdapter.java @@ -0,0 +1,44 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import io.cloudbeaver.model.app.WebApplication; +import io.cloudbeaver.registry.WebDriverRegistry; +import org.jkiss.code.NotNull; + +import java.net.InetAddress; +import java.util.List; +import java.util.Map; + +//FIXME: this interface should not exist, +// the logic of platforms and applications should be separated from each other +public interface GQLApplicationAdapter extends WebApplication { + AppWebSessionManager getSessionManager(); + + WebDriverRegistry getDriverRegistry(); + + @NotNull + Map getProductConfiguration(); + + List getLocalInetAddresses(); + + Map getInitActions(); + + boolean isLicenseValid(); + + String getLicenseStatus(); +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java index f6c68805f8..bcf97f7aaf 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java @@ -18,6 +18,7 @@ import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWServletHandler; +import io.cloudbeaver.utils.WebAppUtils; import jakarta.servlet.Servlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -43,7 +44,7 @@ protected void createActionFromParams(WebSession session, HttpServletRequest req action.saveInSession(session); // Redirect to home - response.sendRedirect("/"); + response.sendRedirect(WebAppUtils.getWebApplication().getServerConfiguration().getRootURI()); } protected abstract String getActionConsole(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java index 81d54ab417..21928f0203 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java @@ -16,7 +16,7 @@ */ package io.cloudbeaver.server.events; -import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.WebSessionProjectImpl; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; @@ -34,15 +34,13 @@ public class WSDataSourceUpdatedEventHandlerImpl extends WSAbstractProjectEventH @Override protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSDataSourceEvent event) { var sendEvent = true; - if (activeUserSession instanceof WebSession) { - var webSession = (WebSession) activeUserSession; - WebProjectImpl project = webSession.getProjectById(event.getProjectId()); + if (activeUserSession instanceof WebSession webSession) { + WebSessionProjectImpl project = webSession.getProjectById(event.getProjectId()); if (project == null) { log.debug("Project " + event.getProjectId() + " is not found in session " + webSession.getSessionId()); return; } - sendEvent = webSession.updateProjectDataSources( - project, + sendEvent = project.updateProjectDataSources( event.getDataSourceIds(), WSEventType.valueById(event.getId()) ); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java index 0968876b3e..7f4ebeb740 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java @@ -16,102 +16,165 @@ */ package io.cloudbeaver.server.events; +import io.cloudbeaver.WebSessionGlobalProjectImpl; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.service.security.SMUtils; import io.cloudbeaver.utils.WebAppUtils; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.security.SMObjectType; +import org.jkiss.dbeaver.model.security.SMAdminController; +import org.jkiss.dbeaver.model.security.SMObjectPermissionsGrant; import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; import org.jkiss.dbeaver.model.websocket.event.permissions.WSObjectPermissionEvent; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; public class WSObjectPermissionUpdatedEventHandler extends WSDefaultEventHandler { private static final Log log = Log.getLog(WSObjectPermissionUpdatedEventHandler.class); @Override - protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSObjectPermissionEvent event) { - try { + public void handleEvent(@NotNull WSObjectPermissionEvent event) { + String objectId = event.getObjectId(); + Consumer runnable = switch (event.getSmObjectType()) { + case project: + yield getUpdateUserProjectsInfoConsumer(event, objectId); + case datasource: + try { + SMAdminController smController = CBApplication.getInstance().getSecurityController(); + Set dataSourcePermissions = smController.getObjectPermissionGrants(event.getObjectId(), event.getSmObjectType()) + .stream() + .map(SMObjectPermissionsGrant::getSubjectId).collect(Collectors.toSet()); + yield getUpdateUserDataSourcesInfoConsumer(event, objectId, dataSourcePermissions); + } catch (DBException e) { + log.error("Error getting permissions for data source " + objectId, e); + yield null; + } + }; + if (runnable == null) { + return; + } + log.debug(event.getTopicId() + " event handled"); + Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); + for (var activeUserSession : allSessions) { + if (!isAcceptableInSession(activeUserSession, event)) { + log.debug("Cannot handle %s event '%s' in session %s".formatted( + event.getTopicId(), + event.getId(), + activeUserSession.getSessionId() + )); + continue; + } + log.debug("%s event '%s' handled".formatted(event.getTopicId(), event.getId())); + runnable.accept(activeUserSession); + } + } + + @NotNull + private Consumer getUpdateUserDataSourcesInfoConsumer( + @NotNull WSObjectPermissionEvent event, + @NotNull String dataSourceId, + @NotNull Set dataSourcePermissions + ) { + return (activeUserSession) -> { // we have accessible data sources only in web session - if (event.getSmObjectType() == SMObjectType.datasource && !(activeUserSession instanceof WebSession)) { + // admins already have access for all shared connections + if (!(activeUserSession instanceof WebSession webSession) || SMUtils.isAdmin(webSession)) { return; } - var objectId = event.getObjectId(); - - boolean isAccessibleNow; - switch (event.getSmObjectType()) { - case project: - if (WSEventType.OBJECT_PERMISSIONS_UPDATED.getEventId().equals(event.getId())) { - var accessibleProjectIds = activeUserSession.getUserContext().getAccessibleProjectIds(); - if (accessibleProjectIds.contains(event.getObjectId())) { - return; - } - activeUserSession.addSessionProject(objectId); - activeUserSession.addSessionEvent( - WSProjectUpdateEvent.create( - event.getSessionId(), - event.getUserId(), - objectId - ) - ); - } else if (WSEventType.OBJECT_PERMISSIONS_DELETED.getEventId().equals(event.getId())) { - activeUserSession.removeSessionProject(objectId); - activeUserSession.addSessionEvent( - WSProjectUpdateEvent.delete( - event.getSessionId(), - event.getUserId(), - objectId - ) - ); - } - break; - case datasource: - var webSession = (WebSession) activeUserSession; - var dataSources = List.of(objectId); + if (!isAcceptableInSession(webSession, event)) { + return; + } + var user = activeUserSession.getUserContext().getUser(); + var userSubjects = new HashSet<>(Set.of(user.getTeams())); + userSubjects.add(user.getUserId()); + boolean shouldBeAccessible = dataSourcePermissions.stream().anyMatch(userSubjects::contains); + List dataSources = List.of(dataSourceId); + WebSessionGlobalProjectImpl project = webSession.getGlobalProject(); + if (project == null) { + log.error("Project " + WebAppUtils.getGlobalProjectId() + + " is not found in session " + activeUserSession.getSessionId()); + return; + } + boolean isAccessibleNow = project.findWebConnectionInfo(dataSourceId) != null; + if (WSEventType.OBJECT_PERMISSIONS_UPDATED.getEventId().equals(event.getId())) { + if (isAccessibleNow || !shouldBeAccessible) { + return; + } + project.addAccessibleConnectionToCache(dataSourceId); + webSession.addSessionEvent( + WSDataSourceEvent.create( + event.getSessionId(), + event.getUserId(), + project.getId(), + dataSources, + WSDataSourceProperty.CONFIGURATION + ) + ); + } else if (WSEventType.OBJECT_PERMISSIONS_DELETED.getEventId().equals(event.getId())) { + if (!isAccessibleNow || shouldBeAccessible) { + return; + } + project.removeAccessibleConnectionFromCache(dataSourceId); + webSession.addSessionEvent( + WSDataSourceEvent.delete( + event.getSessionId(), + event.getUserId(), + project.getId(), + dataSources, + WSDataSourceProperty.CONFIGURATION + ) + ); + } + }; + } - var project = webSession.getProjectById(WebAppUtils.getGlobalProjectId()); - if (project == null) { - log.error("Project " + WebAppUtils.getGlobalProjectId() + - " is not found in session " + activeUserSession.getSessionId()); + @NotNull + private Consumer getUpdateUserProjectsInfoConsumer( + @NotNull WSObjectPermissionEvent event, + @NotNull String projectId + ) { + return (activeUserSession) -> { + try { + if (WSEventType.OBJECT_PERMISSIONS_UPDATED.getEventId().equals(event.getId())) { + var accessibleProjectIds = activeUserSession.getUserContext().getAccessibleProjectIds(); + if (accessibleProjectIds.contains(event.getObjectId())) { return; } - if (WSEventType.OBJECT_PERMISSIONS_UPDATED.getEventId().equals(event.getId())) { - isAccessibleNow = webSession.findWebConnectionInfo(project.getId(), objectId) != null; - if (isAccessibleNow) { - return; - } - webSession.addAccessibleConnectionToCache(objectId); - webSession.addSessionEvent( - WSDataSourceEvent.create( - event.getSessionId(), - event.getUserId(), - WebAppUtils.getGlobalProjectId(), - dataSources, - WSDataSourceProperty.CONFIGURATION - ) - ); - } else if (WSEventType.OBJECT_PERMISSIONS_DELETED.getEventId().equals(event.getId())) { - webSession.removeAccessibleConnectionFromCache(objectId); - webSession.addSessionEvent( - WSDataSourceEvent.delete( - event.getSessionId(), - event.getUserId(), - WebAppUtils.getGlobalProjectId(), - dataSources, - WSDataSourceProperty.CONFIGURATION - ) - ); - } + activeUserSession.addSessionProject(projectId); + activeUserSession.addSessionEvent( + WSProjectUpdateEvent.create( + event.getSessionId(), + event.getUserId(), + projectId + ) + ); + } else if (WSEventType.OBJECT_PERMISSIONS_DELETED.getEventId().equals(event.getId())) { + activeUserSession.removeSessionProject(projectId); + activeUserSession.addSessionEvent( + WSProjectUpdateEvent.delete( + event.getSessionId(), + event.getUserId(), + projectId + ) + ); + } + } catch (DBException e) { + log.error("Error on changing permissions for project " + + event.getObjectId() + " in session " + activeUserSession.getSessionId(), e); } - } catch (DBException e) { - log.error("Error on changing permissions for project " + - event.getObjectId() + " in session " + activeUserSession.getSessionId(), e); - } + }; } @Override diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java index 48dccc7e55..3466b93b0d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java @@ -53,13 +53,13 @@ private void acceptChangesInNavigatorTree(WSEventType eventType, String resource if (eventType == WSEventType.RM_RESOURCE_CREATED) { RMEventManager.fireEvent( new RMEvent(RMEvent.Action.RESOURCE_ADD, - project.getRmProject(), + project.getRMProject(), resourcePath) ); } else if (eventType == WSEventType.RM_RESOURCE_DELETED) { RMEventManager.fireEvent( new RMEvent(RMEvent.Action.RESOURCE_DELETE, - project.getRmProject(), + project.getRMProject(), resourcePath) ); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java index 4188468da5..14dc4cdb76 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java @@ -32,7 +32,11 @@ public void handleEvent(@NotNull EVENT event) { if (eventType == null) { return; } - WebSessionManager sessionManager = CBPlatform.getInstance().getSessionManager(); + var appSessionManager = CBPlatform.getInstance().getSessionManager(); + if (!(appSessionManager instanceof WebSessionManager)) { + return; + } + var sessionManager = (WebSessionManager) appSessionManager; switch (eventType) { case CLOSE_USER_SESSIONS: if (event instanceof WSUserCloseSessionsEvent closeSessionsEvent) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java index ade211b040..438a1ef858 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserSecretEventHandlerImpl.java @@ -16,6 +16,7 @@ */ package io.cloudbeaver.server.events; +import io.cloudbeaver.WebSessionProjectImpl; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; @@ -36,11 +37,16 @@ public class WSUserSecretEventHandlerImpl extends WSDefaultEventHandler " + apiCall); + } else if (DEBUG) { + log.debug("API > " + query); } } ExecutionInput executionInput = contextBuilder.build(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java index 2eb86a3757..e3c94cce88 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java @@ -16,26 +16,27 @@ */ package io.cloudbeaver.server.jetty; +import io.cloudbeaver.model.config.CBServerConfig; import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.server.CBServerConfig; +import io.cloudbeaver.server.GQLApplicationAdapter; import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.server.servlets.CBImageServlet; import io.cloudbeaver.server.servlets.CBStaticServlet; import io.cloudbeaver.server.servlets.CBStatusServlet; import io.cloudbeaver.server.websockets.CBJettyWebSocketManager; import io.cloudbeaver.service.DBWServiceBindingServlet; +import io.cloudbeaver.service.DBWServiceBindingWebSocket; +import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlet.ServletMapping; import org.eclipse.jetty.server.*; -import org.eclipse.jetty.server.session.DefaultSessionCache; -import org.eclipse.jetty.server.session.DefaultSessionIdManager; -import org.eclipse.jetty.server.session.NullSessionDataStore; -import org.eclipse.jetty.server.session.SessionHandler; -import org.eclipse.jetty.servlet.ErrorPageErrorHandler; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlet.ServletMapping; -import org.eclipse.jetty.util.resource.PathResource; -import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.session.DefaultSessionCache; +import org.eclipse.jetty.session.DefaultSessionIdManager; +import org.eclipse.jetty.session.NullSessionDataStore; +import org.eclipse.jetty.util.resource.ResourceFactory; +import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; import org.eclipse.jetty.xml.XmlConfiguration; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -59,6 +60,7 @@ public class CBJettyServer { } private final CBApplication application; + private Server server; public CBJettyServer(@NotNull CBApplication application) { this.application = application; @@ -67,15 +69,14 @@ public CBJettyServer(@NotNull CBApplication application) { public void runServer() { try { CBServerConfig serverConfiguration = application.getServerConfiguration(); - JettyServer server; int serverPort = serverConfiguration.getServerPort(); String serverHost = serverConfiguration.getServerHost(); Path sslPath = getSslConfigurationPath(); boolean sslConfigurationExists = sslPath != null && Files.exists(sslPath); if (sslConfigurationExists) { - server = new JettyServer(); - XmlConfiguration sslConfiguration = new XmlConfiguration(new PathResource(sslPath)); + server = new Server(); + XmlConfiguration sslConfiguration = new XmlConfiguration(ResourceFactory.of(server).newResource(sslPath)); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // method sslConfiguration.configure() does not see the context class of the Loader, // so we have to configure it manually, then return the old classLoader. @@ -84,23 +85,31 @@ public void runServer() { Thread.currentThread().setContextClassLoader(classLoader); } else { if (CommonUtils.isEmpty(serverHost)) { - server = new JettyServer(serverPort); + server = new Server(serverPort); } else { - server = new JettyServer( + server = new Server( InetSocketAddress.createUnresolved(serverHost, serverPort)); } } { // Handler configuration + Path contentRootPath = Path.of(serverConfiguration.getContentRoot()); ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); - servletContextHandler.setResourceBase(serverConfiguration.getContentRoot()); + servletContextHandler.setBaseResourceAsPath(contentRootPath); String rootURI = serverConfiguration.getRootURI(); servletContextHandler.setContextPath(rootURI); - ServletHolder staticServletHolder = new ServletHolder("static", new CBStaticServlet()); + ServletHolder staticServletHolder = new ServletHolder( + "static", new CBStaticServlet(Path.of(serverConfiguration.getContentRoot())) + ); staticServletHolder.setInitParameter("dirAllowed", "false"); - servletContextHandler.addServlet(staticServletHolder, "/*"); + staticServletHolder.setInitParameter("cacheControl", "public, max-age=" + CBStaticServlet.STATIC_CACHE_SECONDS); + servletContextHandler.addServlet(staticServletHolder, "/"); + + if (Files.isSymbolicLink(contentRootPath)) { + servletContextHandler.addAliasCheck(new CBSymLinkContentAllowedAliasChecker(contentRootPath)); + } ServletHolder imagesServletHolder = new ServletHolder("images", new CBImageServlet()); servletContextHandler.addServlet(imagesServletHolder, serverConfiguration.getServicesURI() + "images/*"); @@ -108,33 +117,56 @@ public void runServer() { servletContextHandler.addServlet(new ServletHolder("status", new CBStatusServlet()), "/status"); servletContextHandler.addServlet(new ServletHolder("graphql", new GraphQLEndpoint()), serverConfiguration.getServicesURI() + "gql/*"); - servletContextHandler.addEventListener(new CBServerContextListener()); + servletContextHandler.addEventListener(new CBServerContextListener(application)); // Add extensions from services CBJettyServletContext servletContext = new CBJettyServletContext(servletContextHandler); - for (DBWServiceBindingServlet wsd : WebServiceRegistry.getInstance().getWebServices(DBWServiceBindingServlet.class)) { - try { - wsd.addServlets(this.application, servletContext); - } catch (DBException e) { - log.error(e.getMessage(), e); + for (DBWServiceBindingServlet wsd : WebServiceRegistry.getInstance() + .getWebServices(DBWServiceBindingServlet.class) + ) { + if (wsd.isApplicable(this.application)) { + try { + wsd.addServlets(this.application, servletContext); + } catch (DBException e) { + log.error(e.getMessage(), e); + } } } - initSessionManager(this.application, servletContextHandler); - - server.setHandler(servletContextHandler); + CBJettyWebSocketContext webSocketContext = new CBJettyWebSocketContext(server, servletContextHandler); + for (DBWServiceBindingWebSocket wsb : WebServiceRegistry.getInstance() + .getWebServices(DBWServiceBindingWebSocket.class) + ) { + if (wsb.isApplicable(this.application)) { + try { + wsb.addWebSockets(this.application, webSocketContext); + } catch (DBException e) { + log.error(e.getMessage(), e); + } + } + } - JettyWebSocketServletContainerInitializer.configure(servletContextHandler, - (context, wsContainer) -> { + WebSocketUpgradeHandler webSocketHandler = WebSocketUpgradeHandler.from(server, servletContextHandler, (wsContainer) -> { wsContainer.setIdleTimeout(Duration.ofMinutes(5)); // Add websockets wsContainer.addMapping( - serverConfiguration.getServicesURI() + "ws/*", + serverConfiguration.getServicesURI() + "ws", new CBJettyWebSocketManager(this.application.getSessionManager()) ); } ); + servletContextHandler.insertHandler(webSocketHandler); + + initSessionManager( + this.application.getMaxSessionIdleTime(), + this.application, + server, + servletContextHandler + ); + + server.setHandler(servletContextHandler); + ErrorPageErrorHandler errorHandler = new ErrorPageErrorHandler(); //errorHandler.addErrorPage(404, "/missing.html"); servletContextHandler.setErrorHandler(errorHandler); @@ -144,6 +176,11 @@ public void runServer() { log.debug("\t" + sm.getServletName() + ": " + Arrays.toString(sm.getPathSpecs())); //$NON-NLS-1$ } + log.debug("Active websocket mappings:"); + for (String mapping : webSocketContext.getMappings()) { + log.debug("\t" + mapping); + } + } boolean forwardProxy = application.getAppConfiguration().isEnabledForwardProxy(); @@ -161,7 +198,7 @@ public void runServer() { } } } - + refreshJettyConfig(); server.start(); server.join(); } catch (Exception e) { @@ -179,13 +216,15 @@ private Path getSslConfigurationPath() { return sslConfiguration.isAbsolute() ? sslConfiguration : application.getHomeDirectory().resolve(sslConfiguration); } - private void initSessionManager( - @NotNull CBApplication application, + public static void initSessionManager( + long maxIdleTime, + @NotNull GQLApplicationAdapter application, + @NotNull Server server, @NotNull ServletContextHandler servletContextHandler ) { // Init sessions persistence - SessionHandler sessionHandler = new SessionHandler(); - var maxIdleTime = application.getMaxSessionIdleTime(); + CBSessionHandler sessionHandler = new CBSessionHandler(application); + sessionHandler.setRefreshCookieAge(CBSessionHandler.ONE_MINUTE); int intMaxIdleSeconds; if (maxIdleTime > Integer.MAX_VALUE) { log.warn("Max session idle time value is greater than Integer.MAX_VALUE. Integer.MAX_VALUE will be used instead"); @@ -194,33 +233,29 @@ private void initSessionManager( intMaxIdleSeconds = (int) (maxIdleTime / 1000); log.debug("Max http session idle time: " + intMaxIdleSeconds + "s"); sessionHandler.setMaxInactiveInterval(intMaxIdleSeconds); + sessionHandler.setMaxCookieAge(intMaxIdleSeconds); DefaultSessionCache sessionCache = new DefaultSessionCache(sessionHandler); sessionCache.setSessionDataStore(new NullSessionDataStore()); sessionHandler.setSessionCache(sessionCache); - servletContextHandler.setSessionHandler(sessionHandler); - } - public static class JettyServer extends Server { - public JettyServer(int serverPort) { - super(serverPort); - } + DefaultSessionIdManager idMgr = new DefaultSessionIdManager(server); + idMgr.setWorkerName(null); + server.addBean(idMgr, true); + } - public JettyServer() { - super(); + public synchronized void refreshJettyConfig() { + if (server == null) { + return; } - public JettyServer(InetSocketAddress addr) { - super(addr); - } - - @Override - public void setSessionIdManager(SessionIdManager sessionIdManager) { - if (sessionIdManager instanceof DefaultSessionIdManager) { - // Nullify worker name to avoid dummy prefixes in session ID cookie - ((DefaultSessionIdManager) sessionIdManager).setWorkerName(null); - } - super.setSessionIdManager(sessionIdManager); + log.info("Refreshing Jetty configuration"); + if (server.getHandler() instanceof ServletContextHandler servletContextHandler + && servletContextHandler.getSessionHandler() instanceof CBSessionHandler cbSessionHandler + ) { + cbSessionHandler.setMaxCookieAge((int) (application.getMaxSessionIdleTime() / 1000)); + var serverUrl = this.application.getServerURL(); + cbSessionHandler.setSecureCookies(serverUrl != null && serverUrl.startsWith("https://")); } } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java index 0579ff22b0..b04374442c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServletContext.java @@ -19,8 +19,8 @@ import io.cloudbeaver.service.DBWServletContext; import jakarta.servlet.http.HttpServlet; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; public class CBJettyServletContext implements DBWServletContext { private final ServletContextHandler contextHandler; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyWebSocketContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyWebSocketContext.java new file mode 100644 index 0000000000..8a19513a84 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyWebSocketContext.java @@ -0,0 +1,56 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.jetty; + +import io.cloudbeaver.service.DBWWebSocketContext; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.websocket.api.Configurable; +import org.eclipse.jetty.websocket.server.WebSocketCreator; +import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; +import org.jkiss.code.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public class CBJettyWebSocketContext implements DBWWebSocketContext { + private final List mappings = new ArrayList<>(); + + private final Server server; + private final ContextHandler handler; + + public CBJettyWebSocketContext(@NotNull Server server, @NotNull ContextHandler handler) { + this.server = server; + this.handler = handler; + } + + @Override + public void addWebSocket(@NotNull String mapping, @NotNull Function configurator) { + handler.insertHandler(WebSocketUpgradeHandler.from( + server, + handler, + container -> container.addMapping(mapping, configurator.apply(container)) + )); + mappings.add(mapping); + } + + @NotNull + public List getMappings() { + return mappings; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBServerContextListener.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBServerContextListener.java index 9bab018ef3..af6215b0ff 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBServerContextListener.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBServerContextListener.java @@ -26,17 +26,22 @@ public class CBServerContextListener implements ServletContextListener { // One week //private static final int CB_SESSION_LIFE_TIME = 60 * 60 * 24 * 7; + private final CBApplication application; + + public CBServerContextListener(CBApplication application) { + this.application = application; + } public void contextInitialized(ServletContextEvent sce) { - SessionCookieConfig scf = sce.getServletContext().getSessionCookieConfig(); + SessionCookieConfig cookieConfig = sce.getServletContext().getSessionCookieConfig(); - scf.setComment("Cloudbeaver Session ID"); + cookieConfig.setComment("Cloudbeaver Session ID"); //scf.setDomain(domain); - //scf.setHttpOnly(httpOnly); //scf.setMaxAge(CB_SESSION_LIFE_TIME); - scf.setPath(CBApplication.getInstance().getRootURI()); - //scf.setSecure(isSecure); - scf.setName(CBConstants.CB_SESSION_COOKIE_NAME); + cookieConfig.setPath(CBApplication.getInstance().getRootURI()); +// cookieConfig.setSecure(application.getServerURL().startsWith("https")); + cookieConfig.setHttpOnly(true); + cookieConfig.setName(CBConstants.CB_SESSION_COOKIE_NAME); } public void contextDestroyed(ServletContextEvent sce) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java new file mode 100644 index 0000000000..b8539264f7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java @@ -0,0 +1,29 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.jetty; + +import io.cloudbeaver.server.GQLApplicationAdapter; +import org.eclipse.jetty.ee10.servlet.SessionHandler; + +public class CBSessionHandler extends SessionHandler { + static final int ONE_MINUTE = 60; + private final GQLApplicationAdapter application; + + public CBSessionHandler(GQLApplicationAdapter application) { + this.application = application; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSymLinkContentAllowedAliasChecker.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSymLinkContentAllowedAliasChecker.java new file mode 100644 index 0000000000..ecfc108878 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSymLinkContentAllowedAliasChecker.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.jetty; + +import org.eclipse.jetty.server.AliasCheck; +import org.eclipse.jetty.util.resource.Resource; +import org.jkiss.code.NotNull; + +import java.nio.file.Path; + +public class CBSymLinkContentAllowedAliasChecker implements AliasCheck { + @NotNull + private final Path contentRootPath; + + public CBSymLinkContentAllowedAliasChecker(@NotNull Path contentRootPath) { + this.contentRootPath = contentRootPath; + } + + @Override + public boolean checkAlias(String pathInContext, Resource resource) { + Path resourcePath = resource.getPath(); + return resourcePath != null && resourcePath.startsWith(contentRootPath); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java deleted file mode 100644 index 053749b7d8..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.cloudbeaver.server.jobs; - -import io.cloudbeaver.server.CBPlatform; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.runtime.AbstractJob; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; - -public abstract class PeriodicSystemJob extends AbstractJob { - - @NotNull - protected final CBPlatform platform; - private final long periodMs; - - public PeriodicSystemJob(@NotNull String name, @NotNull CBPlatform platform, long periodMs) { - super(name); - this.platform = platform; - this.periodMs = periodMs; - - setUser(false); - setSystem(true); - } - - @Override - protected IStatus run(@NotNull DBRProgressMonitor monitor) { - if (platform.isShuttingDown()) { - return Status.OK_STATUS; - } - - doJob(monitor); - - // If the platform is still running after the job is completed, reschedule the job - if (!platform.isShuttingDown()) { - scheduleMonitor(); - } - - return Status.OK_STATUS; - } - - protected abstract void doJob(@NotNull DBRProgressMonitor monitor); - - public void scheduleMonitor() { - schedule(periodMs); - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java index 5f2d9a6eaa..01aaffb15a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java @@ -16,23 +16,27 @@ */ package io.cloudbeaver.server.jobs; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.service.session.WebSessionManager; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.app.DBPPlatform; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.PeriodicJob; -public class SessionStateJob extends PeriodicSystemJob { +public class SessionStateJob extends PeriodicJob { private static final Log log = Log.getLog(SessionStateJob.class); private static final int PERIOD_MS = 30_000; // once per 30 seconds + private final WebSessionManager sessionManager; - public SessionStateJob(@NotNull CBPlatform platform) { + public SessionStateJob(@NotNull DBPPlatform platform, WebSessionManager sessionManager) { super("Session state sender", platform, PERIOD_MS); + this.sessionManager = sessionManager; } @Override protected void doJob(@NotNull DBRProgressMonitor monitor) { try { - platform.getSessionManager().sendSessionsStates(); + sessionManager.sendSessionsStates(); } catch (Exception e) { log.error("Error sending session state", e); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebDataSourceMonitorJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebDataSourceMonitorJob.java index 5a244b6156..961bf38ce6 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebDataSourceMonitorJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebDataSourceMonitorJob.java @@ -16,9 +16,9 @@ */ package io.cloudbeaver.server.jobs; +import io.cloudbeaver.server.AppWebSessionManager; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBPlatform; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.DBPDataSource; import org.jkiss.dbeaver.model.app.DBPPlatform; @@ -34,14 +34,19 @@ * Web data source monitor job. */ public class WebDataSourceMonitorJob extends DataSourceMonitorJob { + private final AppWebSessionManager sessionManager; - public WebDataSourceMonitorJob(DBPPlatform platform) { + public WebDataSourceMonitorJob( + @NotNull DBPPlatform platform, + @NotNull AppWebSessionManager sessionManager + ) { super(platform); + this.sessionManager = sessionManager; } @Override protected void doJob() { - Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); + Collection allSessions = sessionManager.getAllActiveSessions(); allSessions.parallelStream().forEach(s -> { checkDataSourceAliveInWorkspace(s.getWorkspace(), s.getLastAccessTimeMillis()); }); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java index 11fc79c569..e73dacdf62 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java @@ -16,26 +16,30 @@ */ package io.cloudbeaver.server.jobs; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.service.session.WebSessionManager; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.app.DBPPlatform; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.PeriodicJob; /** * WebSessionMonitorJob */ -public class WebSessionMonitorJob extends PeriodicSystemJob { +public class WebSessionMonitorJob extends PeriodicJob { private static final Log log = Log.getLog(WebSessionMonitorJob.class); private static final int MONITOR_INTERVAL = 10000; // once per 10 seconds + private final WebSessionManager sessionManager; - public WebSessionMonitorJob(@NotNull CBPlatform platform) { + public WebSessionMonitorJob(@NotNull DBPPlatform platform, @NotNull WebSessionManager sessionManager) { super("Web session monitor", platform, MONITOR_INTERVAL); + this.sessionManager = sessionManager; } @Override protected void doJob(@NotNull DBRProgressMonitor monitor) { try { - platform.getSessionManager().expireIdleSessions(); + sessionManager.expireIdleSessions(); } catch (Exception e) { log.error("Error on expire idle sessions", e); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java index 11ad45034e..232d57f3f9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java @@ -20,26 +20,23 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.auth.CBAuthConstants; import io.cloudbeaver.auth.SMAuthProviderFederated; +import io.cloudbeaver.model.config.CBAppConfig; +import io.cloudbeaver.model.config.CBServerConfig; import io.cloudbeaver.model.session.WebActionParameters; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; import io.cloudbeaver.registry.WebHandlerRegistry; import io.cloudbeaver.registry.WebServletHandlerDescriptor; -import io.cloudbeaver.server.CBAppConfig; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; -import io.cloudbeaver.server.CBServerConfig; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.http.HttpContent; -import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.server.ResourceService; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.util.resource.Resource; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; @@ -48,9 +45,13 @@ import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; -import java.io.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.Enumeration; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; @WebServlet(urlPatterns = "/") @@ -62,8 +63,11 @@ public class CBStaticServlet extends DefaultServlet { private static final Log log = Log.getLog(CBStaticServlet.class); - public CBStaticServlet() { - super(makeResourceService()); + @NotNull + private final Path contentRoot; + + public CBStaticServlet(@NotNull Path contentRoot) { + this.contentRoot = contentRoot; } @Override @@ -95,7 +99,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } catch (DBWebException e) { log.error("Error reading websession", e); } - super.doGet(request, response); + patchStaticContentIfNeeded(request, response); } private void performAutoLoginIfNeeded(HttpServletRequest request, WebSession webSession) { @@ -189,46 +193,40 @@ private boolean processSessionStart(HttpServletRequest request, HttpServletRespo return false; } - private static ResourceService makeResourceService() { - ResourceService resourceService = new ProxyResourceService(); - resourceService.setCacheControl(new HttpField(HttpHeader.CACHE_CONTROL, "public, max-age=" + STATIC_CACHE_SECONDS)); - return resourceService; - } - + private void patchStaticContentIfNeeded(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String pathInContext = request.getServletPath(); - private static class ProxyResourceService extends ResourceService { - @Override - protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, HttpContent content, Enumeration reqRanges) throws IOException { - String resourceName = content.getResource().getName(); - if (resourceName.endsWith("index.html") || resourceName.endsWith("sso.html")) { - return patchIndexHtml(response, content); - } - return super.sendData(request, response, include, content, reqRanges); + if ("/".equals(pathInContext)) { + pathInContext = "index.html"; } - private boolean patchIndexHtml(HttpServletResponse response, HttpContent content) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Resource resource = content.getResource(); - File file = resource.getFile(); - try (InputStream fis = new FileInputStream(file)) { - IOUtils.copyStream(fis, baos); - } - String indexContents = baos.toString(StandardCharsets.UTF_8); - CBServerConfig serverConfig = CBApplication.getInstance().getServerConfiguration(); - indexContents = indexContents - .replace("{ROOT_URI}", serverConfig.getRootURI()) - .replace("{STATIC_CONTENT}", serverConfig.getStaticContent()); - byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); - - putHeaders(response, content, indexBytes.length); - // Disable cache for index.html - response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); - response.setHeader(HttpHeader.EXPIRES.toString(), "0"); - - response.getOutputStream().write(indexBytes); + if (pathInContext == null || !pathInContext.endsWith("index.html") + && !pathInContext.endsWith("sso.html") + && !pathInContext.endsWith("ssoError.html") + ) { + super.doGet(request, response); + return; + } - return true; + if (pathInContext.startsWith("/")) { + pathInContext = pathInContext.substring(1); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + var filePath = contentRoot.resolve(pathInContext); + try (InputStream fis = Files.newInputStream(filePath)) { + IOUtils.copyStream(fis, baos); } + String indexContents = baos.toString(StandardCharsets.UTF_8); + CBServerConfig serverConfig = CBApplication.getInstance().getServerConfiguration(); + indexContents = indexContents + .replace("{ROOT_URI}", serverConfig.getRootURI()) + .replace("{STATIC_CONTENT}", serverConfig.getStaticContent()); + byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); + + // Disable cache for index.html + response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); + response.setHeader(HttpHeader.EXPIRES.toString(), "0"); + response.getOutputStream().write(ByteBuffer.wrap(indexBytes)); } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java index ceb4fdf748..78cd34091c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStatusServlet.java @@ -19,12 +19,12 @@ import com.google.gson.stream.JsonWriter; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBConstants; +import io.cloudbeaver.server.CBPlatform; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.jkiss.dbeaver.DBException; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.utils.GeneralUtils; @@ -46,7 +46,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t infoMap.put("health", "ok"); infoMap.put("product.name", GeneralUtils.getProductName()); infoMap.put("product.version", GeneralUtils.getProductVersion().toString()); - CBApplication.getInstance().getStatusInfo(infoMap); + CBPlatform.getInstance().getApplication().getStatusInfo(infoMap); try (JsonWriter writer = new JsonWriter(response.getWriter())) { JSONUtils.serializeMap(writer, infoMap); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java index 807814ea19..2dac868a86 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBAbstractWebSocket.java @@ -17,29 +17,31 @@ package io.cloudbeaver.server.websockets; import com.google.gson.Gson; -import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import org.eclipse.jetty.websocket.api.Callback; +import org.eclipse.jetty.websocket.api.Session; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.websocket.WSUtils; import org.jkiss.dbeaver.model.websocket.event.WSEvent; -import java.io.IOException; - -public class CBAbstractWebSocket extends WebSocketAdapter { +public class CBAbstractWebSocket extends Session.Listener.AbstractAutoDemanding { private static final Log log = Log.getLog(CBAbstractWebSocket.class); protected static final Gson gson = WSUtils.gson; public void handleEvent(WSEvent event) { - if (isNotConnected()) { + if (!isOpen()) { return; } - try { - getRemote().sendString(gson.toJson(event)); - } catch (IOException e) { - handleEventException(e); - } + Session session = getSession(); + session.sendText(gson.toJson(event), new Callback() { + @Override + public void fail(Throwable e) { + handleEventException(e); + } + }); + } - protected void handleEventException(Exception e) { + protected void handleEventException(Throwable e) { log.error("Failed to send websocket message", e); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java index 697011c5d4..b99ff76e93 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java @@ -20,8 +20,8 @@ import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.websocket.CBWebSessionEventHandler; +import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.WriteCallback; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.websocket.event.WSClientEvent; @@ -36,7 +36,7 @@ public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessi @NotNull private final BaseWebSession webSession; @NotNull - private final WriteCallback callback; + private final Callback callback; public CBEventsWebSocket(@NotNull BaseWebSession webSession) { this.webSession = webSession; @@ -45,8 +45,8 @@ public CBEventsWebSocket(@NotNull BaseWebSession webSession) { } @Override - public void onWebSocketConnect(Session session) { - super.onWebSocketConnect(session); + public void onWebSocketOpen(Session session) { + super.onWebSocketOpen(session); this.webSession.addEventHandler(this); handleEvent(new WSSocketConnectedEvent(webSession.getApplication().getApplicationRunId())); log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session"); @@ -109,7 +109,7 @@ public void handleWebSessionEvent(WSEvent event) { super.handleEvent(event); } @Override - protected void handleEventException(Exception e) { + protected void handleEventException(Throwable e) { super.handleEventException(e); webSession.addSessionError(e); } @@ -120,7 +120,7 @@ public BaseWebSession getWebSession() { } @NotNull - public WriteCallback getCallback() { + public Callback getCallback() { return callback; } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java index ec294713eb..643197d7e3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBExpiredSessionWebSocket.java @@ -21,8 +21,8 @@ public class CBExpiredSessionWebSocket extends CBAbstractWebSocket { @Override - public void onWebSocketConnect(Session session) { - super.onWebSocketConnect(session); + public void onWebSocketOpen(Session session) { + super.onWebSocketOpen(session); handleEvent(new WSAccessTokenExpiredEvent()); close(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java index 02c730dd10..06cd85f1c1 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java @@ -16,20 +16,21 @@ */ package io.cloudbeaver.server.websockets; +import io.cloudbeaver.server.AppWebSessionManager; import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebHeadlessSession; +import io.cloudbeaver.model.session.WebHttpRequestInfo; import io.cloudbeaver.server.CBPlatform; -import io.cloudbeaver.service.session.WebSessionManager; -import jakarta.servlet.http.HttpServletRequest; -import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; -import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; -import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.server.ServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.ServerUpgradeResponse; +import org.eclipse.jetty.websocket.server.WebSocketCreator; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException; -import org.jkiss.dbeaver.runtime.DBWorkbench; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -38,12 +39,12 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -public class CBJettyWebSocketManager implements JettyWebSocketCreator { +public class CBJettyWebSocketManager implements WebSocketCreator { private static final Log log = Log.getLog(CBJettyWebSocketManager.class); private final Map> socketBySessionId = new ConcurrentHashMap<>(); - private final WebSessionManager webSessionManager; + private final AppWebSessionManager webSessionManager; - public CBJettyWebSocketManager(@NotNull WebSessionManager webSessionManager) { + public CBJettyWebSocketManager(@NotNull AppWebSessionManager webSessionManager) { this.webSessionManager = webSessionManager; new WebSocketPingPongJob(CBPlatform.getInstance(), this).scheduleMonitor(); @@ -51,17 +52,22 @@ public CBJettyWebSocketManager(@NotNull WebSessionManager webSessionManager) { @Nullable @Override - public Object createWebSocket(@NotNull JettyServerUpgradeRequest request, JettyServerUpgradeResponse resp) { - var httpRequest = request.getHttpServletRequest(); - var webSession = webSessionManager.getOrRestoreSession(httpRequest); + public Object createWebSocket(@NotNull ServerUpgradeRequest request, ServerUpgradeResponse resp, Callback callback) { + var webSession = webSessionManager.getOrRestoreSession(request); + var requestInfo = new WebHttpRequestInfo( + request.getId(), + request.getAttribute("locale"), + Request.getRemoteAddr(request), + request.getHeaders().get("User-Agent") + ); if (webSession != null) { - webSession.updateSessionParameters(httpRequest); + webSession.updateSessionParameters(requestInfo); // web client session return createNewEventsWebSocket(webSession); } // possible desktop client session try { - var headlessSession = createHeadlessSession(httpRequest); + var headlessSession = createHeadlessSession(request); if (headlessSession == null) { log.debug("Couldn't create headless session"); return null; @@ -86,21 +92,21 @@ private CBEventsWebSocket createNewEventsWebSocket(@NotNull BaseWebSession webSe } @Nullable - private WebHeadlessSession createHeadlessSession(@NotNull HttpServletRequest request) throws DBException { - var httpSession = request.getSession(false); - if (httpSession == null) { + private WebHeadlessSession createHeadlessSession(@NotNull Request request) throws DBException { + var requestSession = request.getSession(false); + if (requestSession == null) { log.debug("CloudBeaver web session not exist, try to create headless session"); } else { - log.debug("CloudBeaver session not found with id " + httpSession.getId() + ", try to create headless session"); + log.debug("CloudBeaver session not found with id " + requestSession.getId() + ", try to create headless session"); } - return webSessionManager.getHeadlessSession(request, true); + return webSessionManager.getHeadlessSession(request, requestSession, true); } public void sendPing() { //remove expired sessions socketBySessionId.entrySet() .removeIf(entry -> { - entry.getValue().removeIf(ws -> !ws.isConnected()); + entry.getValue().removeIf(ws -> !ws.isOpen()); return webSessionManager.getSession(entry.getKey()) == null || entry.getValue().isEmpty(); } @@ -115,7 +121,7 @@ public void sendPing() { var webSockets = entry.getValue(); for (CBEventsWebSocket webSocket : webSockets) { try { - webSocket.getRemote().sendPing( + webSocket.getSession().sendPing( ByteBuffer.wrap("cb-ping".getBytes(StandardCharsets.UTF_8)), webSocket.getCallback() ); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java index 8530e963c1..b19741df9a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2022 DBeaver Corp and others + * Copyright (C) 2010-2024 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,10 @@ import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebHeadlessSession; -import org.eclipse.jetty.websocket.api.WriteCallback; +import org.eclipse.jetty.websocket.api.Callback; import org.jkiss.code.NotNull; -public class WebSocketPingPongCallback implements WriteCallback { +public class WebSocketPingPongCallback implements Callback { @NotNull private final BaseWebSession webSession; @@ -30,7 +30,7 @@ public WebSocketPingPongCallback(@NotNull BaseWebSession webSession) { } @Override - public void writeSuccess() { + public void succeed() { if (webSession instanceof WebHeadlessSession) { webSession.touchSession(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceBindingWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceBindingWebSocket.java new file mode 100644 index 0000000000..9d62ef7a5f --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceBindingWebSocket.java @@ -0,0 +1,29 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service; + +import io.cloudbeaver.model.app.WebApplication; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; + +public interface DBWServiceBindingWebSocket extends DBWServiceBinding { + default boolean isApplicable(@NotNull WebApplication application) { + return true; + } + + void addWebSockets(@NotNull APPLICATION application, @NotNull DBWWebSocketContext context) throws DBException; +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWWebSocketContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWWebSocketContext.java new file mode 100644 index 0000000000..4da0e61f05 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWWebSocketContext.java @@ -0,0 +1,28 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service; + +import org.eclipse.jetty.websocket.api.Configurable; +import org.eclipse.jetty.websocket.server.WebSocketCreator; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; + +import java.util.function.Function; + +public interface DBWWebSocketContext { + void addWebSocket(@NotNull String mapping, @NotNull Function configurator) throws DBException; +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java index 7a85a39e97..e6609823b2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java @@ -27,6 +27,7 @@ import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.WebDataSourceUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.jkiss.code.NotNull; @@ -136,7 +137,7 @@ public static WebSession findWebSession(DataFetchingEnvironment env, boolean err @NotNull public static WebConnectionInfo getWebConnection(WebSession session, String projectId, String connectionId) throws DBWebException { - return session.getWebConnectionInfo(projectId, connectionId); + return WebDataSourceUtils.getWebConnectionInfo(session, projectId, connectionId); } private class ServiceInvocationHandler implements InvocationHandler { @@ -229,7 +230,7 @@ private void checkObjectActionPermissions(Method method, WebProjectAction object if (project == null) { throw new DBException("Project not found:" + projectId); } - RMProject rmProject = project.getRmProject(); + RMProject rmProject = project.getRMProject(); for (String reqProjectPermission : requireProjectPermissions) { if (!rmProject.hasProjectPermission(reqProjectPermission)) { @@ -250,7 +251,7 @@ private void checkServicePermissions(Method method, WebActionSet actionSet) thro } private void checkActionPermissions(@NotNull Method method, @NotNull WebAction webAction) throws DBWebException { - CBApplication application = CBApplication.getInstance(); + var application = CBPlatform.getInstance().getApplication(); if (application.isInitializationMode() && webAction.initializationRequired()) { String message = "Server initialization in progress: " + String.join(",", application.getInitActions().values()) + ".\nDo not restart the server."; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java index c6644a5223..3768ff0617 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java @@ -1,7 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.cloudbeaver.service; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; @@ -27,13 +44,13 @@ public abstract class WebServiceServletBase extends HttpServlet { .setPrettyPrinting() .create(); - private final CBApplication application; + private final WebApplication application; - public WebServiceServletBase(CBApplication application) { + public WebServiceServletBase(WebApplication application) { this.application = application; } - public CBApplication getApplication() { + public WebApplication getApplication() { return application; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java index a04c44847f..ea6b0264b8 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java @@ -29,7 +29,6 @@ import io.cloudbeaver.service.DBWBindingContext; import io.cloudbeaver.service.WebServiceBindingBase; import io.cloudbeaver.service.core.impl.WebServiceCore; -import io.cloudbeaver.service.session.WebSessionManager; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -50,7 +49,7 @@ public WebServiceBindingCore() { @Override public void bindWiring(DBWBindingContext model) throws DBWebException { CBPlatform platform = CBPlatform.getInstance(); - WebSessionManager sessionManager = platform.getSessionManager(); + var sessionManager = platform.getSessionManager(); model.getQueryType() .dataFetcher("serverConfig", env -> getService(env).getServerConfig()) .dataFetcher("productSettings", env -> getService(env).getProductSettings(getWebSession(env))) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index f77c7cf6ab..77a5a40fc7 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -17,10 +17,7 @@ package io.cloudbeaver.service.core.impl; -import io.cloudbeaver.DBWConstants; -import io.cloudbeaver.DBWebException; -import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.*; import io.cloudbeaver.model.*; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebHandlerRegistry; @@ -29,7 +26,7 @@ import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.core.DBWServiceCore; import io.cloudbeaver.service.security.SMUtils; -import io.cloudbeaver.service.session.WebSessionManager; +import io.cloudbeaver.utils.WebAppUtils; import io.cloudbeaver.utils.WebConnectionFolderUtils; import io.cloudbeaver.utils.WebDataSourceUtils; import io.cloudbeaver.utils.WebEventUtils; @@ -118,15 +115,17 @@ public List getUserConnections( return Collections.singletonList(connectionInfo); } } - var stream = webSession.getConnections().stream(); + var stream = webSession.getAccessibleProjects().stream(); if (projectId != null) { - stream = stream.filter(c -> c.getProjectId().equals(projectId)); + stream = stream.filter(c -> c.getId().equals(projectId)); } if (projectIds != null) { - stream = stream.filter(c -> projectIds.contains(c.getProjectId())); + stream = stream.filter(c -> projectIds.contains(c.getId())); } Set applicableDrivers = WebServiceUtils.getApplicableDriversIds(); - return stream.filter(c -> applicableDrivers.contains(c.getDataSourceContainer().getDriver().getId())) + return stream + .flatMap(p -> p.getConnections().stream()) + .filter(c -> applicableDrivers.contains(c.getDataSourceContainer().getDriver().getId())) .toList(); } @@ -154,27 +153,30 @@ public List getTemplateDataSources() throws DBWebException public List getTemplateConnections( @NotNull WebSession webSession, @Nullable String projectId ) throws DBWebException { + if (webSession.getApplication().isDistributed()) { + return List.of(); + } List result = new ArrayList<>(); if (projectId == null) { - for (DBPProject project : webSession.getAccessibleProjects()) { + for (WebSessionProjectImpl project : webSession.getAccessibleProjects()) { getTemplateConnectionsFromProject(webSession, project, result); } } else { - DBPProject project = getProjectById(webSession, projectId); + WebSessionProjectImpl project = getProjectById(webSession, projectId); getTemplateConnectionsFromProject(webSession, project, result); } - webSession.filterAccessibleConnections(result); return result; } private void getTemplateConnectionsFromProject( @NotNull WebSession webSession, - @NotNull DBPProject project, + @NotNull WebSessionProjectImpl project, List result ) { DBPDataSourceRegistry registry = project.getDataSourceRegistry(); for (DBPDataSourceContainer ds : registry.getDataSources()) { if (ds.isTemplate() && + project.getDataSourceFilter().filter(ds) && CBPlatform.getInstance().getApplicableDrivers().contains(ds.getDriver())) { result.add(new WebConnectionInfo(webSession, ds)); } @@ -208,7 +210,7 @@ private List getConnectionFoldersFromProject( @Override public String[] getSessionPermissions(@NotNull WebSession webSession) throws DBWebException { - if (CBApplication.getInstance().isConfigurationMode()) { + if (WebAppUtils.getWebApplication().isConfigurationMode()) { return new String[]{ DBWConstants.PERMISSION_ADMIN }; @@ -291,7 +293,7 @@ public boolean touchSession(@NotNull HttpServletRequest request, @NotNull HttpSe @Deprecated public WebSession updateSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws DBWebException { - WebSessionManager sessionManager = CBPlatform.getInstance().getSessionManager(); + var sessionManager = CBPlatform.getInstance().getSessionManager(); sessionManager.touchSession(request, response); return sessionManager.getWebSession(request, response, true); } @@ -317,9 +319,11 @@ public boolean changeSessionLanguage(@NotNull WebSession webSession, String loca @Override public WebConnectionInfo getConnectionState( - WebSession webSession, @Nullable String projectId, String connectionId + @NotNull WebSession webSession, + @Nullable String projectId, + @NotNull String connectionId ) throws DBWebException { - return webSession.getWebConnectionInfo(projectId, connectionId); + return WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, connectionId); } @@ -334,7 +338,7 @@ public WebConnectionInfo initConnection( @Nullable Boolean sharedCredentials, @Nullable String selectedSecretId ) throws DBWebException { - WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, connectionId); + WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, connectionId); connectionInfo.setSavedCredentials(authProperties, networkCredentials); var dataSourceContainer = (DataSourceDescriptor) connectionInfo.getDataSourceContainer(); @@ -430,11 +434,11 @@ public WebConnectionInfo createConnection( @Nullable String projectId, @NotNull WebConnectionConfig connectionConfig ) throws DBWebException { - var project = getProjectById(webSession, projectId); - var rmProject = project.getRmProject(); + WebSessionProjectImpl project = getProjectById(webSession, projectId); + var rmProject = project.getRMProject(); if (rmProject.getType() == RMProjectType.USER && !webSession.hasPermission(DBWConstants.PERMISSION_ADMIN) - && !CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections() + && !WebAppUtils.getWebApplication().getAppConfiguration().isSupportsCustomConnections() ) { throw new DBWebException("New connection create is restricted by server configuration"); } @@ -460,8 +464,7 @@ public WebConnectionInfo createConnection( throw new DBWebException("Failed to create connection", e); } - WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, newDataSource); - webSession.addConnection(connectionInfo); + WebConnectionInfo connectionInfo = project.addConnection(newDataSource); webSession.addInfoMessage("New connection was created - " + WebServiceUtils.getConnectionContainerInfo( newDataSource)); WebEventUtils.addDataSourceUpdatedEvent( @@ -485,9 +488,8 @@ public WebConnectionInfo updateConnection( // if (!CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections()) { // throw new DBWebException("Connection edit is restricted by server configuration"); // } - DBPDataSourceRegistry sessionRegistry = getProjectById(webSession, projectId).getDataSourceRegistry(); - WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, config.getConnectionId()); + WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, config.getConnectionId()); DBPDataSourceContainer dataSource = connectionInfo.getDataSourceContainer(); webSession.addInfoMessage("Update connection - " + WebServiceUtils.getConnectionContainerInfo(dataSource)); var oldDataSource = new DataSourceDescriptor((DataSourceDescriptor) dataSource, dataSource.getRegistry()); @@ -500,6 +502,8 @@ public WebConnectionInfo updateConnection( dataSource.setDescription(config.getDescription()); } + WebSessionProjectImpl project = getProjectById(webSession, projectId); + DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); dataSource.setFolder(config.getFolder() != null ? sessionRegistry.getFolder(config.getFolder()) : null); if (config.isDefaultAutoCommit() != null) { dataSource.setDefaultAutoCommit(config.isDefaultAutoCommit()); @@ -576,7 +580,7 @@ private WSDataSourceProperty getDatasourceEventProperty( public boolean deleteConnection( @NotNull WebSession webSession, @Nullable String projectId, @NotNull String connectionId ) throws DBWebException { - WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, connectionId); + WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, connectionId); if (connectionInfo.getDataSourceContainer().getProject() != getProjectById(webSession, projectId)) { throw new DBWebException("Global connection '" + connectionInfo.getName() + "' configuration cannot be deleted"); } @@ -600,7 +604,8 @@ public WebConnectionInfo createConnectionFromTemplate( @NotNull String templateId, @Nullable String connectionName ) throws DBWebException { - DBPDataSourceRegistry templateRegistry = getProjectById(webSession, projectId).getDataSourceRegistry(); + WebSessionProjectImpl project = getProjectById(webSession, projectId); + DBPDataSourceRegistry templateRegistry = project.getDataSourceRegistry(); DBPDataSourceContainer dataSourceTemplate = templateRegistry.getDataSource(templateId); if (dataSourceTemplate == null) { throw new DBWebException("Template data source '" + templateId + "' not found"); @@ -623,9 +628,7 @@ public WebConnectionInfo createConnectionFromTemplate( throw new DBWebException(e.getMessage(), e); } - WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, newDataSource); - webSession.addConnection(connectionInfo); - return connectionInfo; + return project.addConnection(newDataSource); } @Override @@ -637,7 +640,8 @@ public WebConnectionInfo copyConnectionFromNode( ) throws DBWebException { try { DBNModel navigatorModel = webSession.getNavigatorModel(); - DBPDataSourceRegistry dataSourceRegistry = getProjectById(webSession, projectId).getDataSourceRegistry(); + WebSessionProjectImpl project = getProjectById(webSession, projectId); + DBPDataSourceRegistry dataSourceRegistry = project.getDataSourceRegistry(); DBNNode srcNode = navigatorModel.getNodeByPath(webSession.getProgressMonitor(), nodePath); if (srcNode == null) { @@ -663,9 +667,8 @@ public WebConnectionInfo copyConnectionFromNode( dataSourceRegistry.addDataSource(newDataSource); - WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, newDataSource); dataSourceRegistry.checkForErrors(); - webSession.addConnection(connectionInfo); + WebConnectionInfo connectionInfo = project.addConnection(newDataSource); WebEventUtils.addDataSourceUpdatedEvent( webSession.getProjectById(projectId), webSession, @@ -688,7 +691,7 @@ public WebConnectionInfo testConnection( connectionConfig.setSaveCredentials(true); // It is used in createConnectionFromConfig DataSourceDescriptor dataSource = (DataSourceDescriptor) WebDataSourceUtils.getLocalOrGlobalDataSource( - CBApplication.getInstance(), webSession, projectId, connectionId); + webSession, projectId, connectionId); WebProjectImpl project = getProjectById(webSession, projectId); DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); @@ -824,12 +827,13 @@ private WebConnectionInfo closeAndDeleteConnection( @NotNull String connectionId, boolean forceDelete ) throws DBWebException { - WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, connectionId); + WebSessionProjectImpl project = getProjectById(webSession, projectId); + WebConnectionInfo connectionInfo = project.getWebConnectionInfo(connectionId); DBPDataSourceContainer dataSourceContainer = connectionInfo.getDataSourceContainer(); boolean disconnected = WebDataSourceUtils.disconnectDataSource(webSession, dataSourceContainer); if (forceDelete) { - DBPDataSourceRegistry registry = getProjectById(webSession, projectId).getDataSourceRegistry(); + DBPDataSourceRegistry registry = project.getDataSourceRegistry(); registry.removeDataSource(dataSourceContainer); try { registry.checkForErrors(); @@ -841,7 +845,7 @@ private WebConnectionInfo closeAndDeleteConnection( } throw new DBWebException("Failed to delete connection", e); } - webSession.removeConnection(connectionInfo); + project.removeConnection(dataSourceContainer); } else { // Just reset saved credentials connectionInfo.clearCache(); @@ -854,7 +858,7 @@ private WebConnectionInfo closeAndDeleteConnection( @Override public List getProjects(@NotNull WebSession session) { var customConnectionsEnabled = - CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections() + WebAppUtils.getWebApplication().getAppConfiguration().isSupportsCustomConnections() || SMUtils.isRMAdmin(session); return session.getAccessibleProjects().stream() .map(pr -> new WebProjectInfo(session, pr, customConnectionsEnabled)) @@ -955,7 +959,7 @@ public boolean deleteConnectionFolder( public WebConnectionInfo setConnectionNavigatorSettings( WebSession webSession, @Nullable String projectId, String id, DBNBrowseSettings settings ) throws DBWebException { - WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, id); + WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, id); DataSourceDescriptor dataSourceDescriptor = ((DataSourceDescriptor) connectionInfo.getDataSourceContainer()); dataSourceDescriptor.setNavigatorSettings(settings); dataSourceDescriptor.persistConfiguration(); @@ -984,8 +988,8 @@ public WebProductSettings getProductSettings(@NotNull WebSession webSession) { return new WebProductSettings(webSession, ProductSettingsRegistry.getInstance().getSettings()); } - private WebProjectImpl getProjectById(WebSession webSession, String projectId) throws DBWebException { - WebProjectImpl project = webSession.getProjectById(projectId); + private WebSessionProjectImpl getProjectById(WebSession webSession, String projectId) throws DBWebException { + WebSessionProjectImpl project = webSession.getProjectById(projectId); if (project == null) { throw new DBWebException("Project '" + projectId + "' not found"); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java index 7d251b5755..7fe7cd1d01 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebDatabaseObjectInfo.java @@ -16,15 +16,17 @@ */ package io.cloudbeaver.service.navigator; +import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.WebPropertyInfo; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.service.security.SMUtils; +import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.meta.Property; -import org.jkiss.dbeaver.model.navigator.DBNDataSource; -import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; +import org.jkiss.dbeaver.model.rm.RMProjectPermission; import org.jkiss.dbeaver.model.struct.*; import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog; import org.jkiss.dbeaver.model.struct.rdb.DBSSchema; @@ -91,9 +93,22 @@ public WebPropertyInfo[] getProperties() { @Property public WebPropertyInfo[] filterProperties(@Nullable WebPropertyFilter filter) { + if (object instanceof DBPDataSourceContainer container && !isDataSourceEditable(container)) { + // If user cannot edit a connection, then return only name + filter = new WebPropertyFilter(); + filter.setFeatures(List.of(DBConstants.PROP_FEATURE_NAME)); + } return WebServiceUtils.getObjectFilteredProperties(session, object, filter); } + private boolean isDataSourceEditable(@NotNull DBPDataSourceContainer container) { + WebProjectImpl project = session.getProjectById(container.getProject().getId()); + if (project == null) { + return false; + } + return SMUtils.hasProjectPermission(session, project.getRMProject(), RMProjectPermission.DATA_SOURCES_EDIT); + } + /////////////////////////////////// // Advanced @@ -183,8 +198,7 @@ private void getObjectFeatures(DBSObject object, List features) { features.add(OBJECT_FEATURE_OBJECT_CONTAINER); try { Class childType = objectContainer.getPrimaryChildType(null); - Collection childrenCollection = objectContainer.getChildren(session.getProgressMonitor()); - if (DBSTable.class.isAssignableFrom(childType) && childrenCollection != null) { + if (DBSTable.class.isAssignableFrom(childType)) { features.add(OBJECT_FEATURE_ENTITY_CONTAINER); } } catch (Exception e) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java index 6f22110e9f..511b0743f6 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java @@ -36,8 +36,6 @@ import org.jkiss.dbeaver.model.navigator.*; import org.jkiss.dbeaver.model.navigator.fs.DBNFileSystem; import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; -import org.jkiss.dbeaver.model.navigator.meta.DBXTreeFolder; -import org.jkiss.dbeaver.model.navigator.meta.DBXTreeItem; import org.jkiss.dbeaver.model.navigator.meta.DBXTreeNode; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.rm.RMProjectPermission; @@ -113,7 +111,7 @@ public String getPlainName() { // for renaming node @Property public String getProjectId() { - DBPProject ownerProject = node.getOwnerProject(); + DBPProject ownerProject = node.getOwnerProjectOrNull(); return ownerProject == null ? null : ownerProject.getId(); } @@ -121,11 +119,11 @@ public String getProjectId() { @Deprecated public String getFullName() { String nodeName; - if (node instanceof DBNDatabaseNode && !(node instanceof DBNDataSource)) { - DBSObject object = ((DBNDatabaseNode) node).getObject(); + if (node instanceof DBNDatabaseNode dbNode && !(node instanceof DBNDataSource)) { + DBSObject object = dbNode.getObject(); nodeName = DBUtils.getObjectFullName(object, DBPEvaluationContext.UI); - } else if (node instanceof DBNDataSource) { - DBPDataSourceContainer object = ((DBNDataSource) node).getDataSourceContainer(); + } else if (node instanceof DBNDataSource dataSource) { + DBPDataSourceContainer object = dataSource.getDataSourceContainer(); nodeName = object.getName(); } else { nodeName = node.getNodeTargetName(); @@ -182,18 +180,20 @@ public boolean isHasChildren() { @Association public String[] getFeatures() { List features = new ArrayList<>(); + boolean isLeaf = false; if (node instanceof DBNDatabaseItem) { features.add(NODE_FEATURE_ITEM); DBSObject object = ((DBNDatabaseItem) node).getObject(); if (object instanceof DBSEntity || object instanceof DBSProcedure) { features.add(NODE_FEATURE_LEAF); + isLeaf = true; } } if (node instanceof DBNContainer) { features.add(NODE_FEATURE_CONTAINER); } boolean isShared = false; - if (node instanceof DBNDatabaseNode) { + if (node instanceof DBNDatabaseNode && !isLeaf) { if (node instanceof DBNDataSource dataSource) { if (dataSource.getDataSourceContainer().getDataSource() != null) { boolean hasNonFolderNode = DBXTreeNode.hasNonFolderNode(dataSource.getMeta().getChildren(null)); @@ -235,7 +235,7 @@ public String[] getFeatures() { if (objectManager != null && objectManager.canDeleteObject(object)) { features.add(NODE_FEATURE_CAN_DELETE); } - if (objectManager instanceof DBEObjectRenamer && ((DBEObjectRenamer) objectManager).canRenameObject(object)) { + if (objectManager instanceof DBEObjectRenamer renamer && renamer.canRenameObject(object)) { if (!object.getDataSource().getContainer().getNavigatorSettings().isShowOnlyEntities()) { features.add(NODE_FEATURE_CAN_RENAME); } @@ -259,7 +259,7 @@ private boolean hasNodePermission(RMProjectPermission permission) { if (project == null) { return false; } - RMProject rmProject = project.getRmProject(); + RMProject rmProject = project.getRMProject(); return SMUtils.hasProjectPermission(session, rmProject, permission); } @@ -282,9 +282,10 @@ private boolean isDistributedSpecialFolderNode() { @Property public WebPropertyInfo[] getNodeDetails() throws DBWebException { - if (node instanceof DBPObjectWithDetails) { + if (node instanceof DBPObjectWithDetails objectWithDetails) { try { - DBPObject objectDetails = ((DBPObjectWithDetails) node).getObjectDetails(session.getProgressMonitor(), session.getSessionContext(), node); + DBPObject objectDetails = objectWithDetails.getObjectDetails( + session.getProgressMonitor(), session.getSessionContext(), node); if (objectDetails != null) { return WebServiceUtils.getObjectProperties(session, objectDetails); } @@ -301,8 +302,8 @@ public WebPropertyInfo[] getNodeDetails() throws DBWebException { @Property public WebDatabaseObjectInfo getObject() { - if (node instanceof DBNDatabaseNode) { - DBSObject object = ((DBNDatabaseNode) node).getObject(); + if (node instanceof DBNDatabaseNode dbNode) { + DBSObject object = dbNode.getObject(); return object == null ? null : new WebDatabaseObjectInfo(session, object); } return null; @@ -320,10 +321,10 @@ public String getObjectId() { @Property public DBSObjectFilter getFilter() throws DBWebException { - if (!(node instanceof DBNDatabaseNode)) { + if (!(node instanceof DBNDatabaseNode dbNode)) { throw new DBWebException("Invalid navigator node type: " + node.getClass().getName()); } - DBSObjectFilter filter = ((DBNDatabaseNode) node).getNodeFilter(((DBNDatabaseNode) node).getItemsMeta(), true); + DBSObjectFilter filter = dbNode.getNodeFilter(dbNode.getItemsMeta(), true); return filter == null || filter.isEmpty() || !filter.isEnabled() ? null : filter; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index 28f54520c8..67efb63def 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -142,7 +142,7 @@ public List getNavigatorNodeChildren( return result.subList(offset, result.size()); } } catch (DBException e) { - throw new DBWebException(e); + throw new DBWebException(e.getMessage(), e); } } @@ -228,11 +228,12 @@ public boolean setNavigatorNodeFilter( filter.setExclude(exclude); } filter.setEnabled(true); - ((DBNDatabaseNode) node).setNodeFilter( - ((DBNDatabaseNode) node).getItemsMeta(), filter, true); - if (hasNodeEditPermission(webSession, node, ((WebProjectImpl) node.getOwnerProject()).getRmProject())) { - // Save settings - ((DBNDatabaseNode) node).getDataSourceContainer().persistConfiguration(); + if (node instanceof DBNDatabaseNode dbNode) { + dbNode.setNodeFilter(dbNode.getItemsMeta(), filter, true); + if (hasNodeEditPermission(webSession, node, ((WebProjectImpl) node.getOwnerProject()).getRMProject())) { + // Save settings + dbNode.getDataSourceContainer().persistConfiguration(); + } } } catch (DBException e) { if (e instanceof DBWebException) { @@ -255,14 +256,14 @@ public boolean refreshNavigatorNode( if (node == null) { throw new DBWebException("Navigator node '" + nodePath + "' not found"); } - if (node instanceof DBNDataSource) { + if (node instanceof DBNDataSource dbnDataSource) { // Do not refresh entire tree - just clear child nodes // Otherwise refresh may fail if navigator settings were changed. - DBPDataSource dataSource = ((DBNDataSource) node).getDataSource(); - if (dataSource instanceof DBPRefreshableObject) { - ((DBPRefreshableObject) dataSource).refreshObject(monitor); + DBPDataSource dataSource = dbnDataSource.getDataSource(); + if (dataSource instanceof DBPRefreshableObject refreshableObject) { + refreshableObject.refreshObject(monitor); } - ((DBNDataSource) node).cleanupNode(); + dbnDataSource.cleanupNode(); } else if (node instanceof DBNLocalFolder) { // Refresh can't be applied to the local folder node return true; @@ -425,7 +426,7 @@ private String renameConnectionFolder(@NotNull WebSession session, DBNNode node, List siblings = Arrays.stream( ((DBNLocalFolder) node).getLogicalParent().getChildren(session.getProgressMonitor())) .filter(n -> n instanceof DBNLocalFolder) - .map(DBNNode::getName).collect(Collectors.toList()); + .map(DBNNode::getName).toList(); if (siblings.contains(newName)) { throw new DBWebException("Name " + newName + " is unavailable or invalid"); } @@ -570,7 +571,7 @@ public int deleteNodes( private void checkProjectEditAccess(DBNNode node, WebSession session) throws DBException { BaseWebProjectImpl project = (BaseWebProjectImpl) node.getOwnerProject(); - if (project == null || !hasNodeEditPermission(session, node, project.getRmProject())) { + if (project == null || !hasNodeEditPermission(session, node, project.getRMProject())) { throw new DBException("Access denied"); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java index 40dc20ae00..92017155d1 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java @@ -18,18 +18,19 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.auth.SMTokenCredentialProvider; -import io.cloudbeaver.model.session.BaseWebSession; -import io.cloudbeaver.model.session.WebHeadlessSession; -import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.model.session.WebSessionAuthProcessor; +import io.cloudbeaver.server.AppWebSessionManager; +import io.cloudbeaver.model.session.*; import io.cloudbeaver.registry.WebHandlerRegistry; import io.cloudbeaver.registry.WebSessionHandlerDescriptor; import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBConstants; import io.cloudbeaver.server.events.WSWebUtils; import io.cloudbeaver.service.DBWSessionHandler; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Session; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -47,7 +48,7 @@ /** * Web session manager */ -public class WebSessionManager { +public class WebSessionManager implements AppWebSessionManager { private static final Log log = Log.getLog(WebSessionManager.class); @@ -61,6 +62,7 @@ public WebSessionManager(CBApplication application) { /** * Closes Web Session, associated to HttpSession from {@code request} */ + @Override public BaseWebSession closeSession(@NotNull HttpServletRequest request) { HttpSession session = request.getSession(); if (session != null) { @@ -82,20 +84,27 @@ protected CBApplication getApplication() { } @Deprecated - public boolean touchSession(@NotNull HttpServletRequest request, - @NotNull HttpServletResponse response) throws DBWebException { + public boolean touchSession( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response + ) throws DBWebException { WebSession webSession = getWebSession(request, response, false); - webSession.updateSessionParameters(request); + var requestInfo = new WebHttpRequestInfo(request); + webSession.updateSessionParameters(requestInfo); webSession.updateInfo(!request.getSession().isNew()); return true; } + @Override @NotNull - public WebSession getWebSession(@NotNull HttpServletRequest request, - @NotNull HttpServletResponse response) throws DBWebException { + public WebSession getWebSession( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response + ) throws DBWebException { return getWebSession(request, response, true); } + @Override @NotNull public WebSession getWebSession( @NotNull HttpServletRequest request, @@ -109,14 +118,14 @@ public WebSession getWebSession( var baseWebSession = sessionMap.get(sessionId); if (baseWebSession == null && CBApplication.getInstance().isConfigurationMode()) { try { - webSession = createWebSessionImpl(request); + webSession = createWebSessionImpl(new WebHttpRequestInfo(request)); } catch (DBException e) { throw new DBWebException("Failed to create web session", e); } sessionMap.put(sessionId, webSession); } else if (baseWebSession == null) { try { - webSession = createWebSessionImpl(request); + webSession = createWebSessionImpl(new WebHttpRequestInfo(request)); } catch (DBException e) { throw new DBWebException("Failed to create web session", e); } @@ -154,13 +163,15 @@ public WebSession getWebSession( * @return WebSession object or null, if session expired or invalid */ @Nullable - public WebSession getOrRestoreSession(@NotNull HttpServletRequest request) { - var httpSession = request.getSession(); - if (httpSession == null) { + public WebSession getOrRestoreSession(@NotNull Request request) { + var sessionIdCookie = Request.getCookies(request).stream().filter( + c -> c.getName().equals(CBConstants.CB_SESSION_COOKIE_NAME) + ).findAny().orElse(null); + if (sessionIdCookie == null) { log.debug("Http session is null. No Web Session returned"); return null; } - var sessionId = httpSession.getId(); + var sessionId = sessionIdCookie.getValue(); WebSession webSession; synchronized (sessionMap) { if (sessionMap.containsKey(sessionId)) { @@ -178,7 +189,12 @@ public WebSession getOrRestoreSession(@NotNull HttpServletRequest request) { return null; } - webSession = createWebSessionImpl(request); + webSession = createWebSessionImpl(new WebHttpRequestInfo( + request.getId(), + request.getAttribute("locale"), + Request.getRemoteAddr(request), + request.getHeaders().get("User-Agent") + )); restorePreviousUserSession(webSession, oldAuthInfo); sessionMap.put(sessionId, webSession); @@ -212,7 +228,7 @@ private void restorePreviousUserSession( } @NotNull - protected WebSession createWebSessionImpl(@NotNull HttpServletRequest request) throws DBException { + protected WebSession createWebSessionImpl(@NotNull WebHttpRequestInfo request) throws DBException { return new WebSession(request, application, getSessionHandlers()); } @@ -223,6 +239,7 @@ protected Map getSessionHandlers() { .collect(Collectors.toMap(WebSessionHandlerDescriptor::getId, WebSessionHandlerDescriptor::getInstance)); } + @Override @Nullable public BaseWebSession getSession(@NotNull String sessionId) { synchronized (sessionMap) { @@ -230,6 +247,7 @@ public BaseWebSession getSession(@NotNull String sessionId) { } } + @Override @Nullable public WebSession findWebSession(HttpServletRequest request) { String sessionId = request.getSession().getId(); @@ -242,6 +260,7 @@ public WebSession findWebSession(HttpServletRequest request) { } } + @Override public WebSession findWebSession(HttpServletRequest request, boolean errorOnNoFound) throws DBWebException { WebSession webSession = findWebSession(request); if (webSession != null) { @@ -274,6 +293,7 @@ public void expireIdleSessions() { } } + @Override public Collection getAllActiveSessions() { synchronized (sessionMap) { return new ArrayList<>(sessionMap.values()); @@ -281,16 +301,15 @@ public Collection getAllActiveSessions() { } @Nullable - public WebHeadlessSession getHeadlessSession(HttpServletRequest request, boolean create) throws DBException { - String smAccessToken = request.getHeader(WSConstants.WS_AUTH_HEADER); + public WebHeadlessSession getHeadlessSession(Request request, Session session, boolean create) throws DBException { + String smAccessToken = request.getHeaders().get(WSConstants.WS_AUTH_HEADER); if (CommonUtils.isEmpty(smAccessToken)) { return null; } synchronized (sessionMap) { - var httpSession = request.getSession(); var tempCredProvider = new SMTokenCredentialProvider(smAccessToken); SMAuthPermissions authPermissions = application.createSecurityController(tempCredProvider).getTokenPermissions(); - var sessionId = httpSession != null ? httpSession.getId() + var sessionId = session != null ? session.getId() : authPermissions.getSessionId(); var existSession = sessionMap.get(sessionId); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java index fbfced3597..2d3a9e5fa5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDataLOBReceiver.java @@ -20,7 +20,7 @@ import io.cloudbeaver.server.CBConstants; import io.cloudbeaver.server.CBPlatform; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.exec.*; +import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; import org.jkiss.dbeaver.model.sql.DBQuotaException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java index 5ea8b52716..34f70e363f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java @@ -20,6 +20,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; @@ -29,7 +30,7 @@ import jakarta.servlet.annotation.MultipartConfig; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.json.JSONUtils; @@ -60,7 +61,7 @@ public class WebSQLFileLoaderServlet extends WebServiceServletBase { .setPrettyPrinting() .create(); - public WebSQLFileLoaderServlet(CBApplication application) { + public WebSQLFileLoaderServlet(WebApplication application) { super(application); } @@ -84,7 +85,7 @@ protected void processServiceRequest( .resolve(session.getSessionId()); MultipartConfigElement multiPartConfig = new MultipartConfigElement(tempFolder.toString()); - request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, multiPartConfig); + request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, multiPartConfig); Map variables = gson.fromJson(request.getParameter(REQUEST_PARAM_VARIABLES), MAP_STRING_OBJECT_TYPE); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java index d49d4ec414..a58ab10961 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java @@ -259,6 +259,8 @@ public WebSQLExecuteInfo processQuery( } } }); + } else { + executeInfo.setResults(new WebSQLQueryResults[0]); } } catch (DBException e) { throw new DBWebException("Error executing query", e); @@ -793,6 +795,10 @@ public Object convertInputCellValue(DBCSession session, DBDAttributeBinding upda cellRawValue, false, true); + //FIXME: fix array editing for nosql databases + if (realCellValue == null && cellRawValue != null && updateAttribute.getDataKind() == DBPDataKind.ARRAY) { + throw new DBCException("Array update is not supported"); + } } catch (DBCException e) { //checks if this function is used only for script generation if (justGenerateScript) { @@ -1138,7 +1144,8 @@ private static DBCExecutionPurpose resolveQueryPurpose(DBDDataFilter filter) { return filter.hasFilters() ? DBCExecutionPurpose.USER_FILTERED : DBCExecutionPurpose.USER; } - private Object setCellRowValue(Object cellRow, WebSession webSession, DBCSession dbcSession, DBDAttributeBinding allAttributes, boolean withoutExecution) { + private Object setCellRowValue(Object cellRow, WebSession webSession, DBCSession dbcSession, DBDAttributeBinding allAttributes, boolean withoutExecution) + throws DBException { if (cellRow instanceof Map) { Map variables = (Map) cellRow; if (variables.get(FILE_ID) != null) { @@ -1151,14 +1158,10 @@ private Object setCellRowValue(Object cellRow, WebSession webSession, DBCSession var file = Files.newInputStream(path); return convertInputCellValue(dbcSession, allAttributes, file, withoutExecution); } catch (IOException | DBCException e) { - return new DBException(e.getMessage()); + throw new DBException(e.getMessage()); } } } - try { - return convertInputCellValue(dbcSession, allAttributes, cellRow, withoutExecution); - } catch (DBCException e) { - return new DBException(e.getMessage()); - } + return convertInputCellValue(dbcSession, allAttributes, cellRow, withoutExecution); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataContainer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataContainer.java index b8ab7b7af7..1529bbde64 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataContainer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataContainer.java @@ -26,11 +26,11 @@ import org.jkiss.dbeaver.model.data.DBDDataFilter; import org.jkiss.dbeaver.model.data.DBDDataReceiver; import org.jkiss.dbeaver.model.exec.*; -import org.jkiss.dbeaver.model.impl.sql.SQLQueryTransformerCount; import org.jkiss.dbeaver.model.sql.SQLQuery; import org.jkiss.dbeaver.model.sql.SQLScriptContext; import org.jkiss.dbeaver.model.sql.SQLSyntaxManager; import org.jkiss.dbeaver.model.sql.data.SQLQueryDataContainer; +import org.jkiss.dbeaver.model.sql.transformers.SQLQueryTransformerCount; import org.jkiss.dbeaver.model.struct.DBSDataContainer; import org.jkiss.dbeaver.model.struct.DBSObject; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java index 6a5800cb20..89bdc1e5f1 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java @@ -17,7 +17,7 @@ package io.cloudbeaver.service.sql; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.utils.WebAppUtils; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; @@ -58,7 +58,9 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver { this.contextInfo = contextInfo; this.dataContainer = dataContainer; this.dataFormat = dataFormat; - rowLimit = CBApplication.getInstance().getAppConfiguration().getResourceQuota(WebSQLConstants.QUOTA_PROP_ROW_LIMIT); + rowLimit = WebAppUtils.getWebApplication() + .getAppConfiguration() + .getResourceQuota(WebSQLConstants.QUOTA_PROP_ROW_LIMIT); } public WebSQLQueryResultSet getResultSet() { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java index a0d231525e..42652e9843 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLResultServlet.java @@ -1,22 +1,39 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.cloudbeaver.service.sql; import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.servlets.CBStaticServlet; import io.cloudbeaver.service.WebServiceServletBase; -import org.eclipse.jetty.server.Request; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.Log; -import org.jkiss.utils.CommonUtils; -import org.jkiss.utils.IOUtils; - import jakarta.servlet.MultipartConfigElement; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.MultipartConfig; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.Part; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; + import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -35,14 +52,14 @@ public class WebSQLResultServlet extends WebServiceServletBase { private final DBWServiceSQL sqlService; - public WebSQLResultServlet(CBApplication application, DBWServiceSQL sqlService) { + public WebSQLResultServlet(WebApplication application, DBWServiceSQL sqlService) { super(application); this.sqlService = sqlService; } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG); + request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG); String fileName = UUID.randomUUID().toString(); for (Part part : request.getParts()) { part.write(WebSQLDataLOBReceiver.DATA_EXPORT_FOLDER + "/" + fileName); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java index 667539783d..9bf207fa4c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java @@ -16,11 +16,11 @@ */ package io.cloudbeaver.service.sql; +import io.cloudbeaver.model.app.WebAppConfiguration; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebServiceRegistry; -import io.cloudbeaver.server.CBAppConfig; -import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.utils.CBModelConstants; +import io.cloudbeaver.utils.WebAppUtils; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.*; @@ -151,9 +151,11 @@ private static Object serializeContentValue(WebSession session, DBDContent value if (ContentUtils.isTextContent(value)) { String stringValue = ContentUtils.getContentStringValue(session.getProgressMonitor(), value); int textPreviewMaxLength = CommonUtils.toInt( - CBApplication.getInstance().getAppConfiguration().getResourceQuota( - WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH, - WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH)); + WebAppUtils.getWebApplication() + .getAppConfiguration() + .getResourceQuota(WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH), + WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH + ); if (stringValue != null && stringValue.length() > textPreviewMaxLength) { stringValue = stringValue.substring(0, textPreviewMaxLength); } @@ -164,12 +166,11 @@ private static Object serializeContentValue(WebSession session, DBDContent value if (binaryValue != null) { byte[] previewValue = binaryValue; // gets parameters from the configuration file - CBAppConfig config = CBApplication.getInstance().getAppConfiguration(); + WebAppConfiguration config = WebAppUtils.getWebApplication().getAppConfiguration(); // the max length of the text preview int textPreviewMaxLength = CommonUtils.toInt( config.getResourceQuota( - WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH, - WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH)); + WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH), WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH); if (previewValue.length > textPreviewMaxLength) { previewValue = Arrays.copyOf(previewValue, textPreviewMaxLength); } @@ -177,8 +178,8 @@ private static Object serializeContentValue(WebSession session, DBDContent value // the max length of the binary preview int binaryPreviewMaxLength = CommonUtils.toInt( config.getResourceQuota( - WebSQLConstants.QUOTA_PROP_BINARY_PREVIEW_MAX_LENGTH, - WebSQLConstants.BINARY_PREVIEW_MAX_LENGTH)); + WebSQLConstants.QUOTA_PROP_BINARY_PREVIEW_MAX_LENGTH), + WebSQLConstants.BINARY_PREVIEW_MAX_LENGTH); byte[] inlineValue = binaryValue; if (inlineValue.length > binaryPreviewMaxLength) { inlineValue = Arrays.copyOf(inlineValue, textPreviewMaxLength); @@ -214,9 +215,11 @@ private static Object serializeGeometryValue(DBGeometry value) { */ public static Object serializeStringValue(Object value) { int textPreviewMaxLength = CommonUtils.toInt( - CBApplication.getInstance().getAppConfiguration().getResourceQuota( - WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH, - WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH)); + WebAppUtils.getWebApplication() + .getAppConfiguration() + .getResourceQuota(WebSQLConstants.QUOTA_PROP_TEXT_PREVIEW_MAX_LENGTH), + WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH + ); String stringValue = value.toString(); if (stringValue.length() < textPreviewMaxLength) { return value.toString(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index 60c471f348..f2bc4e41ad 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -19,6 +19,7 @@ import graphql.schema.DataFetchingEnvironment; import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.service.DBWBindingContext; @@ -39,7 +40,7 @@ /** * Web service implementation */ -public class WebServiceBindingSQL extends WebServiceBindingBase implements DBWServiceBindingServlet { +public class WebServiceBindingSQL extends WebServiceBindingBase implements DBWServiceBindingServlet { public WebServiceBindingSQL() { super(DBWServiceSQL.class, new WebServiceSQL(), "schema/service.sql.graphqls"); @@ -299,7 +300,7 @@ public static WebSQLContextInfo getSQLContext(WebSQLProcessor processor, String } @Override - public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException { + public void addServlets(WebApplication application, DBWServletContext servletContext) throws DBException { servletContext.addServlet( "sqlResultValueViewer", new WebSQLResultServlet(application, getServiceImpl()), @@ -312,6 +313,11 @@ public void addServlets(CBApplication application, DBWServletContext servletCont ); } + @Override + public boolean isApplicable(WebApplication application) { + return application.isMultiuser(); + } + private static class WebSQLConfiguration { private final Map processors = new HashMap<>(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 45bad1c55d..cc512d8a9a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -85,7 +85,7 @@ public WebSQLContextInfo[] listContexts( WebConnectionInfo webConnection = WebServiceBindingBase.getWebConnection(session, projectId, connectionId); conToRead.add(webConnection); } else { - conToRead.addAll(session.getConnections()); + conToRead.addAll(session.getAccessibleProjects().stream().flatMap(p -> p.getConnections().stream()).toList()); } List contexts = new ArrayList<>(); diff --git a/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF index a91459c9bc..f0e730e532 100644 --- a/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Administration Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true -Bundle-Version: 1.0.103.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.108.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.admin/pom.xml b/server/bundles/io.cloudbeaver.service.admin/pom.xml index c7ad4e19dd..80411660ad 100644 --- a/server/bundles/io.cloudbeaver.service.admin/pom.xml +++ b/server/bundles/io.cloudbeaver.service.admin/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.admin - 1.0.103-SNAPSHOT + 1.0.108-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index 559b9b90e4..20f53605dd 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -89,6 +89,7 @@ type AdminAuthProviderConfiguration { redirectLink: String metadataLink: String acsLink: String + entityIdLink: String @since(version: "24.2.1") } type WebFeatureSet { diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java index 0ba9adf93e..e459b2d2a0 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java @@ -16,7 +16,7 @@ */ package io.cloudbeaver.service.admin; -import io.cloudbeaver.server.CBAppConfig; +import io.cloudbeaver.model.config.CBAppConfig; import io.cloudbeaver.server.CBApplication; import org.jkiss.dbeaver.model.data.json.JSONUtils; diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java index 973f010f6d..8a9bca7de0 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java @@ -202,6 +202,9 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { @Override public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException { + if(!application.isMultiuser()) { + return; + } servletContext.addServlet("adminLogs", new WebAdminLogsServlet(application), application.getServicesURI() + "logs/*"); } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java index 711acc19b1..8292df8c2c 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java @@ -16,9 +16,9 @@ */ package io.cloudbeaver.service.admin.impl; +import io.cloudbeaver.model.config.CBAppConfig; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.utils.ConfigurationUtils; -import io.cloudbeaver.server.CBAppConfig; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.admin.AdminConnectionSearchInfo; diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index bfbd2778b2..48e78992a3 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -22,11 +22,15 @@ import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.model.WebPropertyInfo; +import io.cloudbeaver.model.config.CBAppConfig; +import io.cloudbeaver.model.config.CBServerConfig; import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebUser; import io.cloudbeaver.registry.*; -import io.cloudbeaver.server.*; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBConstants; +import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.DBWServiceServerConfigurator; import io.cloudbeaver.service.admin.*; import io.cloudbeaver.service.security.SMUtils; @@ -39,6 +43,7 @@ import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.auth.AuthInfo; +import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; import org.jkiss.dbeaver.model.rm.RMProjectType; @@ -72,6 +77,9 @@ public class WebServiceAdmin implements DBWServiceAdmin { public AdminUserInfo getUserById(@NotNull WebSession webSession, @NotNull String userId) throws DBWebException { try { SMUser smUser = webSession.getAdminSecurityController().getUserById(userId); + if (smUser == null) { + throw new DBException("User '" + userId + "' not found"); + } return new AdminUserInfo(webSession, new WebUser(smUser)); } catch (Exception e) { throw new DBWebException("Error getting user - " + userId, e); @@ -540,7 +548,7 @@ public boolean configureServer(WebSession webSession, Map params appConfig.setAdminCredentialsSaveEnabled(config.isAdminCredentialsSaveEnabled()); appConfig.setEnabledFeatures(config.getEnabledFeatures().toArray(new String[0])); // custom logic for enabling embedded drivers - appConfig.updateDisabledDriversConfig(config.getDisabledDrivers()); + updateDisabledDriversConfig(appConfig, config.getDisabledDrivers()); appConfig.setResourceManagerEnabled(config.isResourceManagerEnabled()); if (CommonUtils.isEmpty(config.getEnabledAuthProviders())) { @@ -618,6 +626,36 @@ public boolean configureServer(WebSession webSession, Map params return true; } + // we disable embedded drivers by default and enable it in enabled drivers list + // that's why we need so complicated logic for disabling drivers + private void updateDisabledDriversConfig(CBAppConfig appConfig, String[] disabledDriversConfig) { + Set disabledIds = new LinkedHashSet<>(Arrays.asList(disabledDriversConfig)); + Set enabledIds = new LinkedHashSet<>(Arrays.asList(appConfig.getEnabledDrivers())); + + // remove all disabled embedded drivers from enabled drivers list + enabledIds.removeAll(disabledIds); + + // enable embedded driver if it is not in disabled drivers list + for (String driverId : appConfig.getDisabledDrivers()) { + if (disabledIds.contains(driverId)) { + // driver is also disabled + continue; + } + // driver is removed from disabled list + // we need to enable if it is embedded + try { + DBPDriver driver = WebServiceUtils.getDriverById(driverId); + if (driver.isEmbedded()) { + enabledIds.add(driverId); + } + } catch (DBWebException e) { + log.error("Failed to find driver by id", e); + } + } + appConfig.setDisabledDrivers(disabledDriversConfig); + appConfig.setEnabledDrivers(enabledIds.toArray(String[]::new)); + } + @Override public boolean setDefaultNavigatorSettings(WebSession webSession, DBNBrowseSettings settings) throws DBWebException { CBApplication.getInstance().getAppConfiguration().setDefaultNavigatorSettings(settings); diff --git a/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF index 025840bf4e..d6d81abf2f 100644 --- a/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Authentication Bundle-SymbolicName: io.cloudbeaver.service.auth;singleton:=true -Bundle-Version: 1.0.103.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.108.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.auth/pom.xml b/server/bundles/io.cloudbeaver.service.auth/pom.xml index 2cee174fa8..7ec5e3be44 100644 --- a/server/bundles/io.cloudbeaver.service.auth/pom.xml +++ b/server/bundles/io.cloudbeaver.service.auth/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.auth - 1.0.103-SNAPSHOT + 1.0.108-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index eb5b4b4d43..f1a86e224e 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -42,6 +42,7 @@ type AuthProviderConfiguration { signOutLink: String metadataLink: String acsLink: String + entityIdLink: String @since(version: "24.2.1") } type AuthProviderCredentialsProfile { @@ -136,6 +137,9 @@ type UserInfo { configurationParameters: Object! # User teams teams: [UserTeamInfo!]! + + @since(version: "24.2.3") + isAnonymous: Boolean! } type UserTeamInfo { diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/ReverseProxyConfigurator.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/ReverseProxyConfigurator.java index 5bda8362a2..8420f4e8ba 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/ReverseProxyConfigurator.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/ReverseProxyConfigurator.java @@ -71,7 +71,7 @@ private void migrateConfiguration( smReverseProxyProviderConfiguration.setProvider(RPAuthProvider.AUTH_PROVIDER); smReverseProxyProviderConfiguration.setDisplayName("Reverse Proxy"); smReverseProxyProviderConfiguration.setDescription( - "Automatically created provider after changing Reverse Proxy configuration way in 23.3.4 version" + "This provider was created automatically" ); smReverseProxyProviderConfiguration .setIconURL(""); Map parameters = new HashMap<>(); diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java index 8e9c44e00c..d7cbaf650c 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java @@ -69,6 +69,9 @@ public List getAuthTokens() { @Property public List getLinkedAuthProviders() throws DBWebException { + if (isAnonymous()) { + return List.of(); + } if (linkedProviders == null) { try { linkedProviders = session.getSecurityController().getCurrentUserLinkedProviders(); @@ -104,4 +107,9 @@ public List getTeams() throws DBWebException { return List.of(); } } + + @Property + public boolean isAnonymous() { + return session.getUser() == null; + } } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index ca7466b619..2ea5cdcd91 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -21,6 +21,7 @@ import io.cloudbeaver.auth.SMSignOutLinkProvider; import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.model.WebPropertyInfo; +import io.cloudbeaver.model.app.WebAppConfiguration; import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionAuthProcessor; @@ -189,7 +190,12 @@ public WebLogoutInfo authLogout( @Override public WebUserInfo activeUser(@NotNull WebSession webSession) throws DBWebException { if (webSession.getUser() == null) { - return null; + WebAppConfiguration appConfiguration = webSession.getApplication().getAppConfiguration(); + if (!appConfiguration.isAnonymousAccessEnabled()) { + return null; + } + SMUser anonymous = new SMUser("anonymous", true, null); + return new WebUserInfo(webSession, new WebUser(anonymous)); } try { // Read user from security controller. It will also read meta parameters diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java index 9f31bf115f..42d2024e83 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java @@ -40,7 +40,7 @@ public class LocalServletHandler extends AbstractActionServletHandler { @Override public boolean handleRequest(Servlet servlet, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { - if (URI_PREFIX.equals(WebAppUtils.removeSideSlashes(request.getPathInfo()))) { + if (URI_PREFIX.equals(WebAppUtils.removeSideSlashes(request.getServletPath()))) { try { WebSession webSession = CBPlatform.getInstance().getSessionManager().getWebSession(request, response, true); createActionFromParams(webSession, request, response); diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalSessionHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalSessionHandler.java index 6707fe84ec..c2261f8e5f 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalSessionHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalSessionHandler.java @@ -54,10 +54,9 @@ protected void openDatabaseConsole(WebSession webSession, CBServerAction action) String connectionId = action.getParameter(LocalServletHandler.PARAM_CONNECTION_ID); String connectionName = action.getParameter(LocalServletHandler.PARAM_CONNECTION_NAME); String connectionURL = action.getParameter(LocalServletHandler.PARAM_CONNECTION_URL); - Stream stream = webSession.getConnections().stream(); - if (projectId != null) { - stream = stream.filter(c -> c.getProjectId().equals(projectId)); - } + Stream stream = webSession.getAccessibleProjects().stream() + .filter(c -> projectId == null || c.getId().equals(projectId)) + .flatMap(p -> p.getConnections().stream()); if (connectionId != null) { stream = stream.filter(c -> c.getId().equals(connectionId)); } else if (connectionName != null) { diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF index 63190e4490..025209c00b 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Data Transfer Bundle-SymbolicName: io.cloudbeaver.service.data.transfer;singleton:=true -Bundle-Version: 1.0.104.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.109.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml b/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml index 3f11db754e..081944d88c 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml +++ b/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.data.transfer - 1.0.104-SNAPSHOT + 1.0.109-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/WebServiceBindingDataTransfer.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/WebServiceBindingDataTransfer.java index 5c6daa5356..10151d5467 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/WebServiceBindingDataTransfer.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/WebServiceBindingDataTransfer.java @@ -66,6 +66,9 @@ public void bindWiring(DBWBindingContext model) { @Override public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException { + if (!application.isMultiuser()) { + return; + } servletContext.addServlet( "dataTransfer", new WebDataTransferServlet(application, getServiceImpl()), diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferImportServlet.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferImportServlet.java index 5aad6558cc..f645d07404 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferImportServlet.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferImportServlet.java @@ -42,7 +42,9 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; @MultipartConfig public class WebDataTransferImportServlet extends WebServiceServletBase { @@ -86,7 +88,7 @@ protected void processServiceRequest( throw new IllegalArgumentException("Missing required parameters"); } - WebConnectionInfo webConnectionInfo = session.getWebConnectionInfo(projectId, connectionId); + WebConnectionInfo webConnectionInfo = session.getAccessibleProjectById(projectId).getWebConnectionInfo(connectionId); WebSQLProcessor processor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); WebSQLContextInfo webSQLContextInfo = processor.getContext(contextId); diff --git a/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF index aaef196873..0257ce1394 100644 --- a/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - File System Bundle-SymbolicName: io.cloudbeaver.service.fs;singleton:=true -Bundle-Version: 1.0.21.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.26.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.fs/pom.xml b/server/bundles/io.cloudbeaver.service.fs/pom.xml index 9a6c5ccc60..ad762c4ee9 100644 --- a/server/bundles/io.cloudbeaver.service.fs/pom.xml +++ b/server/bundles/io.cloudbeaver.service.fs/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.fs - 1.0.21-SNAPSHOT + 1.0.26-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java index d4d616dace..2d8b5515fe 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java @@ -118,6 +118,9 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { @Override public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException { + if (!application.isMultiuser()) { + return; + } servletContext.addServlet( "fileSystems", new WebFSServlet(application, getServiceImpl()), diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java index 53ac1a5dad..fa2514c98f 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java @@ -27,7 +27,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.Part; -import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; @@ -79,7 +79,7 @@ private void doGet(WebSession session, HttpServletRequest request, HttpServletRe private void doPost(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { // we need to set this attribute to get parts - request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, new MultipartConfigElement("")); + request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, new MultipartConfigElement("")); Map variables = getVariables(request); String parentNodePath = JSONUtils.getString(variables, "toParentNodePath"); if (CommonUtils.isEmpty(parentNodePath)) { diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml b/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml index 4abd568426..e908b7dc2a 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml @@ -12,16 +12,27 @@ - + + + + - - + diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java index 4a86e2fb4e..7bdee38129 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java @@ -25,6 +25,7 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPObject; import org.jkiss.dbeaver.model.auth.SMSession; import org.jkiss.dbeaver.model.data.json.JSONUtils; @@ -34,19 +35,22 @@ import org.jkiss.utils.CommonUtils; import javax.naming.Context; +import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.Stream; public class LdapAuthProvider implements SMAuthProviderExternal, SMBruteForceProtected { + private static final Log log = Log.getLog(LdapAuthProvider.class); + public LdapAuthProvider() { } + @NotNull @Override public Map authExternalUser( @NotNull DBRProgressMonitor monitor, @@ -56,41 +60,113 @@ public Map authExternalUser( if (providerConfig == null) { throw new DBException("LDAP provider config is null"); } - String userName = JSONUtils.getString(authParameters, LdapConstants.CRED_USERNAME); + String userName = JSONUtils.getString(authParameters, LdapConstants.CRED_USER_DN); if (CommonUtils.isEmpty(userName)) { - throw new DBException("LDAP user name is empty"); + throw new DBException("LDAP user dn is empty"); } String password = JSONUtils.getString(authParameters, LdapConstants.CRED_PASSWORD); if (CommonUtils.isEmpty(password)) { throw new DBException("LDAP password is empty"); } - String unit = CommonUtils.nullIfEmpty(JSONUtils.getString(authParameters, LdapConstants.CRED_UNITS)); LdapSettings ldapSettings = new LdapSettings(providerConfig); - Hashtable environment = new Hashtable<>(); - environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + Hashtable environment = creteAuthEnvironment(ldapSettings); - var ldapProviderUrl = "ldap://" + ldapSettings.getHost() + ":" + ldapSettings.getPort(); - environment.put(Context.PROVIDER_URL, ldapProviderUrl); - environment.put(Context.SECURITY_AUTHENTICATION, "simple"); + String fullUserDN = userName; + + if (!fullUserDN.startsWith(ldapSettings.getUserIdentifierAttr())) { + fullUserDN = String.join("=", ldapSettings.getUserIdentifierAttr(), userName); + } + if (CommonUtils.isNotEmpty(ldapSettings.getBaseDN()) && !fullUserDN.endsWith(ldapSettings.getBaseDN())) { + fullUserDN = String.join(",", fullUserDN, ldapSettings.getBaseDN()); + } - String cn = "cn=" + userName; - var principal = Stream.of(cn, unit, ldapSettings.getBaseDN()) - .filter(CommonUtils::isNotEmpty) - .collect(Collectors.joining(",")); + validateUserAccess(fullUserDN, ldapSettings); - environment.put(Context.SECURITY_PRINCIPAL, principal); + environment.put(Context.SECURITY_PRINCIPAL, fullUserDN); environment.put(Context.SECURITY_CREDENTIALS, password); + DirContext context = null; try { - DirContext context = new InitialDirContext(environment); - context.close(); + context = new InitialDirContext(environment); Map userData = new HashMap<>(); - userData.put(LdapConstants.CRED_USERNAME, userName); + userData.put(LdapConstants.CRED_USERNAME, findUserNameFromDN(fullUserDN, ldapSettings)); userData.put(LdapConstants.CRED_SESSION_ID, UUID.randomUUID()); return userData; } catch (Exception e) { throw new DBException("LDAP authentication failed: " + e.getMessage(), e); + } finally { + try { + if (context != null) { + context.close(); + } + } catch (NamingException e) { + log.warn("Error closing LDAP user context", e); + } + } + } + + private void validateUserAccess(@NotNull String fullUserDN, @NotNull LdapSettings ldapSettings) throws DBException { + if ( + CommonUtils.isEmpty(ldapSettings.getFilter()) + || CommonUtils.isEmpty(ldapSettings.getBindUserDN()) + || CommonUtils.isEmpty(ldapSettings.getBindUserPassword()) + ) { + return; + } + + var environment = creteAuthEnvironment(ldapSettings); + environment.put(Context.SECURITY_PRINCIPAL, ldapSettings.getBindUserDN()); + environment.put(Context.SECURITY_CREDENTIALS, ldapSettings.getBindUserPassword()); + DirContext bindUserContext = null; + try { + bindUserContext = new InitialDirContext(environment); + + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setTimeLimit(30_000); + var searchResult = bindUserContext.search(fullUserDN, ldapSettings.getFilter(), searchControls); + if (!searchResult.hasMore()) { + throw new DBException("Access denied"); + } + } catch (DBException e) { + throw e; + } catch (Exception e) { + throw new DBException("LDAP user access validation by filter failed: " + e.getMessage(), e); + } finally { + if (bindUserContext != null) { + try { + bindUserContext.close(); + } catch (NamingException e) { + log.warn("Error closing LDAP bind user context", e); + } + } + } + } + + @NotNull + private static Hashtable creteAuthEnvironment(LdapSettings ldapSettings) { + Hashtable environment = new Hashtable<>(); + environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + + environment.put(Context.PROVIDER_URL, ldapSettings.getLdapProviderUrl()); + environment.put(Context.SECURITY_AUTHENTICATION, "simple"); + return environment; + } + + @NotNull + private String findUserNameFromDN(@NotNull String fullUserDN, @NotNull LdapSettings ldapSettings) + throws DBException { + String userId = null; + for (String dn : fullUserDN.split(",")) { + if (dn.startsWith(ldapSettings.getUserIdentifierAttr() + "=")) { + userId = dn.split("=")[1]; + break; + } + } + if (userId == null) { + throw new DBException("Failed to determinate userId from user DN: " + fullUserDN); } + return userId; } @NotNull diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java index 02fcfbea6d..dac3e989b1 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java @@ -20,10 +20,14 @@ public interface LdapConstants { String PARAM_HOST = "ldap-host"; String PARAM_PORT = "ldap-port"; String PARAM_DN = "ldap-dn"; + String PARAM_BIND_USER = "ldap-bind-user"; + String PARAM_BIND_USER_PASSWORD = "ldap-bind-user-pwd"; + String PARAM_FILTER = "ldap-filter"; + String PARAM_USER_IDENTIFIER_ATTR = "ldap-identifier-attr"; String CRED_USERNAME = "user"; + String CRED_USER_DN = "user-dn"; String CRED_PASSWORD = "password"; - String CRED_UNITS = "units"; String CRED_SESSION_ID = "session-id"; } diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java index 9bce30ee14..538e2df1b3 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java @@ -28,13 +28,26 @@ public class LdapSettings { @NotNull private final String baseDN; private final int port; + @NotNull + private final String userIdentifierAttr; + private final String bindUser; + private final String bindUserPassword; + private final String filter; + - protected LdapSettings(SMAuthProviderCustomConfiguration providerConfiguration) { + protected LdapSettings( + SMAuthProviderCustomConfiguration providerConfiguration + ) { this.providerConfiguration = providerConfiguration; this.host = providerConfiguration.getParameter(LdapConstants.PARAM_HOST); this.port = CommonUtils.isNotEmpty(providerConfiguration.getParameter(LdapConstants.PARAM_PORT)) ? Integer.parseInt( providerConfiguration.getParameter(LdapConstants.PARAM_PORT)) : 389; this.baseDN = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_DN, ""); + this.userIdentifierAttr = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_USER_IDENTIFIER_ATTR, + "cn"); + this.bindUser = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_BIND_USER, ""); + this.bindUserPassword = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_BIND_USER_PASSWORD, ""); + this.filter = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_FILTER, ""); } @@ -51,4 +64,25 @@ public String getHost() { public int getPort() { return port; } + + public String getLdapProviderUrl() { + return "ldap://" + getHost() + ":" + getPort(); + } + + @NotNull + public String getUserIdentifierAttr() { + return userIdentifierAttr; + } + + public String getBindUserDN() { + return bindUser; + } + + public String getBindUserPassword() { + return bindUserPassword; + } + + public String getFilter() { + return filter; + } } diff --git a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF index 867baf2c04..92add5dc98 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Metadata Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true -Bundle-Version: 1.0.107.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.112.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.metadata/pom.xml b/server/bundles/io.cloudbeaver.service.metadata/pom.xml index d89f059707..2115c701fc 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/pom.xml +++ b/server/bundles/io.cloudbeaver.service.metadata/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.metadata - 1.0.107-SNAPSHOT + 1.0.112-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF index 41e2d2849b..eb9b59eb3f 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Resource manager NIO implementation Bundle-SymbolicName: io.cloudbeaver.service.rm.nio;singleton:=true -Bundle-Version: 1.0.21.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.26.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml b/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml index 220c6d4113..fae03f750e 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml +++ b/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.rm.nio - 1.0.21-SNAPSHOT + 1.0.26-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF index fa6dbedafb..4263eaeed0 100644 --- a/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Resource manager Bundle-SymbolicName: io.cloudbeaver.service.rm;singleton:=true -Bundle-Version: 1.0.56.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.61.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.rm/pom.xml b/server/bundles/io.cloudbeaver.service.rm/pom.xml index 564a6496fa..3b0f64b47b 100644 --- a/server/bundles/io.cloudbeaver.service.rm/pom.xml +++ b/server/bundles/io.cloudbeaver.service.rm/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.rm - 1.0.56-SNAPSHOT + 1.0.61-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java index 8af78a4819..4c412ecbd8 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java @@ -214,7 +214,7 @@ public boolean moveResource(@NotNull WebSession webSession, WSResourceProperty.NAME); return true; } catch (Exception e) { - throw new DBWebException("Error moving resource " + oldResourcePath, e); + throw new DBWebException(e.getMessage(), e); } } diff --git a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF index a7d81b179c..f3de2d795c 100644 --- a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: Cloudbeaver Web Service - Security Bundle-Vendor: DBeaver Corp Bundle-SymbolicName: io.cloudbeaver.service.security;singleton:=true -Bundle-Version: 1.0.59.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.64.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.security/pom.xml b/server/bundles/io.cloudbeaver.service.security/pom.xml index 08b8d0ca0c..9f682a8392 100644 --- a/server/bundles/io.cloudbeaver.service.security/pom.xml +++ b/server/bundles/io.cloudbeaver.service.security/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.security - 1.0.59-SNAPSHOT + 1.0.64-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index 7b21ede7fb..f8ac85aa1c 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -21,9 +21,9 @@ import com.google.gson.reflect.TypeToken; import io.cloudbeaver.DBWConstants; import io.cloudbeaver.auth.*; -import io.cloudbeaver.model.app.WebAppConfiguration; import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.model.app.WebAuthConfiguration; +import io.cloudbeaver.model.config.SMControllerConfiguration; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; import io.cloudbeaver.registry.WebMetaParametersRegistry; @@ -515,7 +515,7 @@ public SMUser[] findUsers(@NotNull SMUserFilter filter) try (PreparedStatement dbStat = dbCon.prepareStatement( database.normalizeTableNames("SELECT USER_ID,IS_ACTIVE,DEFAULT_AUTH_ROLE FROM {table_prefix}CB_USER" + buildUsersFilter(filter) + "\nORDER BY USER_ID " + getOffsetLimitPart(filter)))) { - int parameterIndex = setUsersFilterValues(dbStat, filter, 1); + setUsersFilterValues(dbStat, filter, 1); try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { @@ -531,14 +531,11 @@ public SMUser[] findUsers(@NotNull SMUserFilter filter) } readSubjectsMetas(dbCon, SMSubjectType.user, filter.getUserIdMask(), result); - StringBuilder teamsSql = new StringBuilder() - .append("SELECT USER_ID,TEAM_ID FROM {table_prefix}CB_USER_TEAM") - .append("\n") - .append("WHERE USER_ID IN (") - .append(SQLUtils.generateParamList(result.size())) - .append(")"); + String teamsSql = + "SELECT USER_ID,TEAM_ID FROM {table_prefix}CB_USER_TEAM\n" + + "WHERE USER_ID IN (" + SQLUtils.generateParamList(result.size()) + ")"; // Read teams - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(teamsSql.toString()))) { + try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(teamsSql))) { int parameterIndex = 1; for (String userId : result.keySet()) { dbStat.setString(parameterIndex++, userId); @@ -574,7 +571,7 @@ private String buildUsersFilter(SMUserFilter filter) { if (filter.getEnabledState() != null) { whereParts.add("IS_ACTIVE=?"); } - if (whereParts.size() > 0) { + if (!whereParts.isEmpty()) { where.append(whereParts.stream().collect(Collectors.joining(" AND ", " WHERE ", ""))); } return where.toString(); @@ -858,7 +855,7 @@ public void setUserCredentials( String encodedValue = CommonUtils.toString(cred.getValue()); encodedValue = property.getEncryption().encrypt(userId, encodedValue); return new String[]{propertyName, encodedValue}; - }).collect(Collectors.toList()); + }).toList(); } catch (Exception e) { throw new DBCException(e.getMessage(), e); } @@ -1037,21 +1034,19 @@ public String[] getUserLinkedProviders(@NotNull String userId) throws DBExceptio @NotNull @Override - public SMPropertyDescriptor[] getMetaParametersBySubjectType(SMSubjectType subjectType) throws DBException { + public SMPropertyDescriptor[] getMetaParametersBySubjectType(SMSubjectType subjectType) { // First add global metas List props = new ArrayList<>( WebMetaParametersRegistry.getInstance().getMetaParameters(subjectType)); // Add metas from enabled auth providers - WebAppConfiguration appConfiguration = application.getAppConfiguration(); - if (appConfiguration instanceof WebAuthConfiguration) { - for (String apId : ((WebAuthConfiguration) appConfiguration).getEnabledAuthProviders()) { - WebAuthProviderDescriptor ap = WebAuthProviderRegistry.getInstance().getAuthProvider(apId); - if (ap != null) { - List metaProps = ap.getMetaParameters(SMSubjectType.team); - if (!CommonUtils.isEmpty(metaProps)) { - props.addAll(metaProps); - } + WebAuthConfiguration authConfiguration = application.getAuthConfiguration(); + for (String apId : authConfiguration.getEnabledAuthProviders()) { + WebAuthProviderDescriptor ap = WebAuthProviderRegistry.getInstance().getAuthProvider(apId); + if (ap != null) { + List metaProps = ap.getMetaParameters(SMSubjectType.team); + if (!CommonUtils.isEmpty(metaProps)) { + props.addAll(metaProps); } } } @@ -1071,9 +1066,10 @@ public SMTeam[] readAllTeams() throws DBCException { String defaultUserTeam = getDefaultUserTeam(); Map teams = new LinkedHashMap<>(); String query = database.normalizeTableNames( - "SELECT T.*, S.IS_SECRET_STORAGE FROM {table_prefix}CB_TEAM T, " + - "{table_prefix}CB_AUTH_SUBJECT S " + - "WHERE T.TEAM_ID IN (S.SUBJECT_ID, ?) ORDER BY TEAM_ID"); + """ + SELECT T.*, S.IS_SECRET_STORAGE FROM {table_prefix}CB_TEAM T, \ + {table_prefix}CB_AUTH_SUBJECT S \ + WHERE T.TEAM_ID IN (S.SUBJECT_ID, ?) ORDER BY TEAM_ID"""); try (PreparedStatement dbPreparedStatement = dbCon.prepareStatement(query)) { dbPreparedStatement.setString(1, defaultUserTeam); try (ResultSet dbResult = dbPreparedStatement.executeQuery()) { @@ -1083,9 +1079,11 @@ public SMTeam[] readAllTeams() throws DBCException { } } } - query = database.normalizeTableNames("SELECT SUBJECT_ID,PERMISSION_ID\n" + - "FROM {table_prefix}CB_AUTH_PERMISSIONS AP, {table_prefix}CB_TEAM R\n" + - "WHERE AP.SUBJECT_ID IN (R.TEAM_ID,?)\n"); + query = database.normalizeTableNames(""" + SELECT SUBJECT_ID,PERMISSION_ID + FROM {table_prefix}CB_AUTH_PERMISSIONS AP, {table_prefix}CB_TEAM R + WHERE AP.SUBJECT_ID IN (R.TEAM_ID,?) + """); try (PreparedStatement dbPreparedStatement = dbCon.prepareStatement(query)) { dbPreparedStatement.setString(1, defaultUserTeam); try (ResultSet dbResult = dbPreparedStatement.executeQuery()) { @@ -1422,7 +1420,7 @@ private String createSmSession( @NotNull Map parameters, @NotNull SMSessionType sessionType, Connection dbCon - ) throws SQLException, DBException { + ) throws SQLException { var sessionId = UUID.randomUUID().toString(); try (PreparedStatement dbStat = dbCon.prepareStatement( database.normalizeTableNames( @@ -1754,11 +1752,10 @@ private void updateAuthStatus( SMAuthConfigurationReference providerId = entry.getKey(); String authJson = gson.toJson(entry.getValue()); boolean configIdExist = providerId.getAuthProviderConfigurationId() != null; - var sqlBuilder = new StringBuilder(); - sqlBuilder.append("UPDATE {table_prefix}CB_AUTH_ATTEMPT_INFO SET AUTH_STATE=? ") - .append("WHERE AUTH_ID=? AND AUTH_PROVIDER_ID=? AND ") - .append(configIdExist ? "AUTH_PROVIDER_CONFIGURATION_ID=?" : "AUTH_PROVIDER_CONFIGURATION_ID IS NULL"); - try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder.toString()))) { + String sqlBuilder = "UPDATE {table_prefix}CB_AUTH_ATTEMPT_INFO SET AUTH_STATE=? " + + "WHERE AUTH_ID=? AND AUTH_PROVIDER_ID=? AND " + + (configIdExist ? "AUTH_PROVIDER_CONFIGURATION_ID=?" : "AUTH_PROVIDER_CONFIGURATION_ID IS NULL"); + try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder))) { dbStat.setString(1, authJson); dbStat.setString(2, authId); dbStat.setString(3, providerId.getAuthProviderId()); @@ -1848,16 +1845,14 @@ private SMAuthInfo getAuthStatus(@NotNull String authId, boolean readExpiredData if (authProviderConfiguration != null) { WebAuthProviderDescriptor authProviderDescriptor = getAuthProvider(authProviderId); var authProviderInstance = authProviderDescriptor.getInstance(); - if (SMAuthProviderFederated.class.isAssignableFrom(authProviderInstance.getClass())) { - signInLink = buildRedirectLink(((SMAuthProviderFederated) authProviderInstance).getRedirectLink( + if (authProviderInstance instanceof SMAuthProviderFederated providerFederated) { + signInLink = buildRedirectLink(providerFederated.getRedirectLink( authProviderConfiguration, Map.of()), authId); - var userCustomSignOutLink = - ((SMAuthProviderFederated) authProviderInstance).getUserSignOutLink( - application.getAuthConfiguration() - .getAuthProviderConfiguration(authProviderConfiguration), - authProviderData); - signOutLink = userCustomSignOutLink; + signOutLink = providerFederated.getUserSignOutLink( + application.getAuthConfiguration() + .getAuthProviderConfiguration(authProviderConfiguration), + authProviderData); } } @@ -1867,16 +1862,13 @@ private SMAuthInfo getAuthStatus(@NotNull String authId, boolean readExpiredData } if (smAuthStatus != SMAuthStatus.SUCCESS) { - switch (smAuthStatus) { - case IN_PROGRESS: - return SMAuthInfo.inProgress(authId, signInLink, signOutLink, authData, isMainAuth, forceSessionsLogout); - case ERROR: - return SMAuthInfo.error(authId, authError, isMainAuth, errorCode); - case EXPIRED: - return SMAuthInfo.expired(authId, readExpiredData ? authData : Map.of(), isMainAuth); - default: - throw new SMException("Unknown auth status:" + smAuthStatus); - } + return switch (smAuthStatus) { + case IN_PROGRESS -> + SMAuthInfo.inProgress(authId, signInLink, signOutLink, authData, isMainAuth, forceSessionsLogout); + case ERROR -> SMAuthInfo.error(authId, authError, isMainAuth, errorCode); + case EXPIRED -> SMAuthInfo.expired(authId, readExpiredData ? authData : Map.of(), isMainAuth); + default -> throw new SMException("Unknown auth status:" + smAuthStatus); + }; } SMTokens smTokens = findTokenBySmSession(smSessionId); @@ -2090,7 +2082,7 @@ public SMAuthInfo finishAuthentication(@NotNull String authId) throws DBExceptio return finishAuthentication(authInfo, false, authInfo.isForceSessionsLogout()); } - private SMAuthInfo finishAuthentication( + protected SMAuthInfo finishAuthentication( @NotNull SMAuthInfo authInfo, boolean isSyncAuth, boolean forceSessionsLogout @@ -2114,7 +2106,7 @@ private SMAuthInfo finishAuthentication( String activeUserId = null; if (!isMainAuthSession) { var accessToken = findTokenBySmSession(authAttemptSessionInfo.getSmSessionId()).getSmAccessToken(); - //this is an additional authorization and we should to return the original permissions and userId + //this is an additional authorization, and we should to return the original permissions and userId permissions = getTokenPermissions(accessToken); activeUserId = permissions.getUserId(); } @@ -2260,10 +2252,9 @@ private void autoUpdateUserTeams( String userId, SMTeam[] allTeams ) throws DBCException { - if (!(authProvider.getInstance() instanceof SMAuthProviderAssigner)) { + if (!(authProvider.getInstance() instanceof SMAuthProviderAssigner authProviderAssigner)) { return; } - SMAuthProviderAssigner authProviderAssigner = (SMAuthProviderAssigner) authProvider.getInstance(); String externalTeamIdMetadataFieldName = authProviderAssigner.getExternalTeamIdMetadataFieldName(); if (!CommonUtils.isEmpty(externalTeamIdMetadataFieldName)) { @@ -2583,8 +2574,9 @@ private SMAuthPermissions getTokenPermissions(@NotNull String token) throws DBEx String authRole; try (Connection dbCon = database.openConnection(); PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("SELECT USER_ID, EXPIRATION_TIME, SESSION_ID, AUTH_ROLE FROM {table_prefix}CB_AUTH_TOKEN " + - "WHERE TOKEN_ID=?")); + database.normalizeTableNames(""" + SELECT USER_ID, EXPIRATION_TIME, SESSION_ID, AUTH_ROLE FROM {table_prefix}CB_AUTH_TOKEN \ + WHERE TOKEN_ID=?""")) ) { dbStat.setString(1, token); try (var dbResult = dbStat.executeQuery()) { @@ -2608,17 +2600,14 @@ private SMAuthPermissions getTokenPermissions(@NotNull String token) throws DBEx @Override public SMAuthProviderDescriptor[] getAvailableAuthProviders() throws DBException { - if (!(application.getAppConfiguration() instanceof WebAuthConfiguration)) { - throw new DBException("Web application doesn't support external authentication"); - } - WebAuthConfiguration appConfiguration = (WebAuthConfiguration) application.getAppConfiguration(); + WebAuthConfiguration appConfiguration = application.getAuthConfiguration(); Set customConfigurations = appConfiguration.getAuthCustomConfigurations(); List providers = WebAuthProviderRegistry.getInstance().getAuthProviders().stream() .filter(ap -> !ap.isTrusted() && appConfiguration.isAuthProviderEnabled(ap.getId()) && (!ap.isConfigurable() || hasProviderConfiguration(ap, customConfigurations))) - .map(WebAuthProviderDescriptor::createDescriptorBean).collect(Collectors.toList()); + .map(WebAuthProviderDescriptor::createDescriptorBean).toList(); if (!CommonUtils.isEmpty(customConfigurations)) { // Attach custom configs to providers @@ -3009,12 +2998,12 @@ public List getObjectPermissionGrants( var grantedPermissionsBySubjectId = new HashMap(); try (Connection dbCon = database.openConnection()) { try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames( - "SELECT OP.SUBJECT_ID,S.SUBJECT_TYPE, OP.PERMISSION\n" + - "FROM {table_prefix}CB_OBJECT_PERMISSIONS OP, {table_prefix}CB_AUTH_SUBJECT S\n" + - "WHERE S.SUBJECT_ID = OP.SUBJECT_ID AND OP.OBJECT_TYPE=? AND OP.OBJECT_ID=?"))) { + """ + SELECT OP.SUBJECT_ID,S.SUBJECT_TYPE, OP.PERMISSION + FROM {table_prefix}CB_OBJECT_PERMISSIONS OP, {table_prefix}CB_AUTH_SUBJECT S + WHERE S.SUBJECT_ID = OP.SUBJECT_ID AND OP.OBJECT_TYPE=? AND OP.OBJECT_ID=?"""))) { dbStat.setString(1, smObjectType.name()); dbStat.setString(2, objectId); - List result = new ArrayList<>(); try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { String subjectId = dbResult.getString(1); @@ -3063,7 +3052,7 @@ public List getSubjectObjectPermissionGrants(@NotNull } return grantedPermissionsByObjectId.values().stream() .map(SMObjectPermissionsGrant.Builder::build) - .collect(Collectors.toList()); + .toList(); } } catch (SQLException e) { @@ -3145,7 +3134,7 @@ private void deleteAuthSubject(Connection dbCon, String subjectId) throws SQLExc } } - private WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws DBCException { + protected WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws DBCException { WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(authProviderId); if (authProvider == null) { throw new DBCException("Auth provider not found: " + authProviderId); @@ -3211,11 +3200,10 @@ public void clearOldAuthAttemptInfo() throws DBException { public Set getFilteredSubjects(Set allSubjects) { try (Connection dbCon = database.openConnection()) { Set result = new HashSet<>(); - var sqlBuilder = new StringBuilder("SELECT SUBJECT_ID FROM {table_prefix}CB_AUTH_SUBJECT U ") - .append("WHERE SUBJECT_ID IN (") - .append(SQLUtils.generateParamList(allSubjects.size())) - .append(")"); - try (var dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder.toString()))) { + String sqlBuilder = + "SELECT SUBJECT_ID FROM {table_prefix}CB_AUTH_SUBJECT U " + + "WHERE SUBJECT_ID IN (" + SQLUtils.generateParamList(allSubjects.size()) + ")"; + try (var dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder))) { int parameterIndex = 1; for (String subjectId : allSubjects) { dbStat.setString(parameterIndex++, subjectId); @@ -3235,7 +3223,6 @@ public Set getFilteredSubjects(Set allSubjects) { private SMSubjectType getSubjectType(@NotNull String subjectId) { try (Connection dbCon = database.openConnection()) { - Set result = new HashSet<>(); String sqlBuilder = "SELECT SUBJECT_TYPE FROM {table_prefix}CB_AUTH_SUBJECT U WHERE SUBJECT_ID = ?"; try (var dbStat = dbCon.prepareStatement(database.normalizeTableNames(sqlBuilder))) { dbStat.setString(1, subjectId); diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java index 6c7b581b13..1b1a7405fa 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java @@ -18,8 +18,9 @@ import io.cloudbeaver.auth.NoAuthCredentialsProvider; import io.cloudbeaver.model.app.WebAuthApplication; +import io.cloudbeaver.model.config.SMControllerConfiguration; +import io.cloudbeaver.model.config.WebDatabaseConfig; import io.cloudbeaver.service.security.db.CBDatabase; -import io.cloudbeaver.service.security.db.WebDatabaseConfig; import io.cloudbeaver.service.security.internal.ClearAuthAttemptInfoJob; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/bruteforce/BruteForceUtils.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/bruteforce/BruteForceUtils.java index fecf508ec6..9a0bb08e27 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/bruteforce/BruteForceUtils.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/bruteforce/BruteForceUtils.java @@ -16,7 +16,7 @@ */ package io.cloudbeaver.service.security.bruteforce; -import io.cloudbeaver.service.security.SMControllerConfiguration; +import io.cloudbeaver.model.config.SMControllerConfiguration; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthStatus; diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java index 0c373781e4..3484e47d52 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabase.java @@ -18,8 +18,10 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.Strictness; import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; import io.cloudbeaver.model.app.WebApplication; +import io.cloudbeaver.model.config.WebDatabaseConfig; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderRegistry; import io.cloudbeaver.utils.WebAppUtils; @@ -300,7 +302,9 @@ CBDatabaseInitialData getInitialData() throws DBException { initialDataPath = WebAppUtils.getRelativePath( databaseConfiguration.getInitialDataConfiguration(), application.getHomeDirectory()); try (Reader reader = new InputStreamReader(new FileInputStream(initialDataPath), StandardCharsets.UTF_8)) { - Gson gson = new GsonBuilder().setLenient().create(); + Gson gson = new GsonBuilder() + .setStrictness(Strictness.LENIENT) + .create(); return gson.fromJson(reader, CBDatabaseInitialData.class); } catch (Exception e) { throw new DBException("Error loading initial data configuration", e); diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/utils/DBConfigurationUtils.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/utils/DBConfigurationUtils.java index 8d1ac5d980..d025c18221 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/utils/DBConfigurationUtils.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/utils/DBConfigurationUtils.java @@ -16,7 +16,7 @@ */ package io.cloudbeaver.service.security.internal.utils; -import io.cloudbeaver.service.security.db.WebDatabaseConfig; +import io.cloudbeaver.model.config.WebDatabaseConfig; import org.jkiss.code.Nullable; import org.jkiss.utils.CommonUtils; diff --git a/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF index 46a21cc5e9..3eb22bf581 100644 --- a/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: CloudBeaver SLF4j Binding Bundle-SymbolicName: io.cloudbeaver.slf4j;singleton:=true -Bundle-Version: 1.0.19.qualifier -Bundle-Release-Date: 20240819 +Bundle-Version: 1.0.24.qualifier +Bundle-Release-Date: 20241104 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.slf4j/pom.xml b/server/bundles/io.cloudbeaver.slf4j/pom.xml index 6045e7e856..bcc71fa113 100644 --- a/server/bundles/io.cloudbeaver.slf4j/pom.xml +++ b/server/bundles/io.cloudbeaver.slf4j/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.slf4j - 1.0.19-SNAPSHOT + 1.0.24-SNAPSHOT eclipse-plugin diff --git a/server/drivers/db2-jt400/pom.xml b/server/drivers/db2-jt400/pom.xml index 5158c8c7f2..05cdc02a0a 100644 --- a/server/drivers/db2-jt400/pom.xml +++ b/server/drivers/db2-jt400/pom.xml @@ -18,7 +18,7 @@ net.sf.jt400 jt400 - 10.5 + 20.0.7 diff --git a/server/drivers/mysql/pom.xml b/server/drivers/mysql/pom.xml index 7f993d3be7..76959e3ab1 100644 --- a/server/drivers/mysql/pom.xml +++ b/server/drivers/mysql/pom.xml @@ -20,6 +20,10 @@ mysql-connector-j 8.2.0 + + com.google.protobuf + protobuf-java + diff --git a/server/drivers/pom.xml b/server/drivers/pom.xml index 39fecc0302..06766cf06e 100644 --- a/server/drivers/pom.xml +++ b/server/drivers/pom.xml @@ -1,7 +1,12 @@ 4.0.0 - io.cloudbeaver + + io.cloudbeaver + cloudbeaver + 1.0.0-SNAPSHOT + ../ + drivers 1.0.0 pom diff --git a/server/drivers/sqlserver/pom.xml b/server/drivers/sqlserver/pom.xml index 2ae93c9fe7..71138b49aa 100644 --- a/server/drivers/sqlserver/pom.xml +++ b/server/drivers/sqlserver/pom.xml @@ -18,7 +18,7 @@ com.microsoft.sqlserver mssql-jdbc - 8.2.0.jre8 + 12.8.0.jre11 diff --git a/server/features/io.cloudbeaver.server.feature/pom.xml b/server/features/io.cloudbeaver.server.feature/pom.xml index df176583f2..5cb3dc1d02 100644 --- a/server/features/io.cloudbeaver.server.feature/pom.xml +++ b/server/features/io.cloudbeaver.server.feature/pom.xml @@ -10,6 +10,6 @@ ../ io.cloudbeaver.server.feature - 24.1.5-SNAPSHOT + 24.2.4-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.ws.feature/feature.xml b/server/features/io.cloudbeaver.ws.feature/feature.xml index e69666f3a5..d771aa8a70 100644 --- a/server/features/io.cloudbeaver.ws.feature/feature.xml +++ b/server/features/io.cloudbeaver.ws.feature/feature.xml @@ -2,7 +2,7 @@ @@ -15,7 +15,6 @@ - diff --git a/server/features/io.cloudbeaver.ws.feature/pom.xml b/server/features/io.cloudbeaver.ws.feature/pom.xml index e3b43cfd37..c2f998b10c 100644 --- a/server/features/io.cloudbeaver.ws.feature/pom.xml +++ b/server/features/io.cloudbeaver.ws.feature/pom.xml @@ -10,6 +10,6 @@ ../ io.cloudbeaver.ws.feature - 1.0.57-SNAPSHOT + 1.0.62-SNAPSHOT eclipse-feature diff --git a/server/pom.xml b/server/pom.xml index fc94b4b30f..d8fb5d9272 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -19,19 +19,28 @@ CloudBeaver CE - 24.1.5 + 24.2.4 bundles features - drivers - test - - - product + + + + full-build + !plain-api-server + + drivers + + product + test + + + + diff --git a/server/product/aggregate/build-full.cmd b/server/product/aggregate/build-full.cmd index 6aacb1a7a6..c660606cb6 100644 --- a/server/product/aggregate/build-full.cmd +++ b/server/product/aggregate/build-full.cmd @@ -1,6 +1,6 @@ @echo off set MAVEN_OPTS=-Xmx2048m -call mvn clean install -Dheadless-platform +call mvn clean install -Dheadless-platform -T 1C pause diff --git a/server/product/web-server/CloudbeaverServer.product b/server/product/web-server/CloudbeaverServer.product index 8e03bd0a5a..1cd6808668 100644 --- a/server/product/web-server/CloudbeaverServer.product +++ b/server/product/web-server/CloudbeaverServer.product @@ -2,7 +2,7 @@ diff --git a/server/product/web-server/pom.xml b/server/product/web-server/pom.xml index c0faddf5e7..a0a46f5953 100644 --- a/server/product/web-server/pom.xml +++ b/server/product/web-server/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../ - 24.1.5-SNAPSHOT + 24.2.4-SNAPSHOT web-server eclipse-repository Cloudbeaver Server Product diff --git a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF index 12ada095bf..29e094999e 100644 --- a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF +++ b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF @@ -8,7 +8,6 @@ Bundle-Vendor: DBeaver Corp Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Require-Bundle: org.eclipse.core.runtime, - org.eclipse.core.resources, org.junit, org.mockito.mockito-core, org.apache.felix.scr, diff --git a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf index cd8725bd8d..4511bd491c 100644 --- a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf +++ b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf @@ -1,48 +1,48 @@ { server: { serverPort: "${CLOUDBEAVER_TEST_PORT:18978}", - serverName: "CloudBeaver CE Test Server", + serverName: "${CLOUDBEAVER_SERVER_NAME:CloudBeaver CE Test Server}", - workspaceLocation: "workspace", + workspaceLocation: "${CLOUDBEAVER_WORKSPACE_LOCATION:workspace}", contentRoot: "workspace/web", driversLocation: "../../../deploy/", - rootURI: "/", + rootURI: "${CLOUDBEAVER_ROOT_URI:/}", serviceURI: "/api/", productSettings: {}, - expireSessionAfterPeriod: 1800000, + expireSessionAfterPeriod: "${CLOUDBEAVER_EXPIRE_SESSION_AFTER_PERIOD:1800000}", - develMode: false, + develMode: "${CLOUDBEAVER_DEVEL_MODE:false}", sm: { enableBruteForceProtection: "${CLOUDBEAVER_BRUTE_FORCE_PROTECTION_ENABLED:false}" }, database: { - driver="h2_embedded_v2", - url: "jdbc:h2:mem:testdb", + driver: "${CLOUDBEAVER_DB_DRIVER:h2_embedded_v2}", + url: "${CLOUDBEAVER_DB_URL:jdbc:h2:mem:testdb}", - createDatabase: true, + createDatabase: "${CLOUDBEAVER_CREATE_DATABASE:true}", - initialDataConfiguration: "workspace/conf/initial-data.conf", + initialDataConfiguration: "${CLOUDBEAVER_DB_INITIAL_DATA:workspace/conf/initial-data.conf}", pool: { - minIdleConnections: 4, - maxIdleConnections: 10, - maxConnections: 100, - validationQuery: "SELECT 1" + minIdleConnections: "${CLOUDBEAVER_DB_MIN_IDLE_CONNECTIONS:4}", + maxIdleConnections: "${CLOUDBEAVER_DB_MAX_IDLE_CONNECTIONS:10}", + maxConnections: "${CLOUDBEAVER_DB_MAX_CONNECTIONS:100}", + validationQuery: "${CLOUDBEAVER_DB_VALIDATION_QUERY:SELECT 1}" } } }, app: { - anonymousAccessEnabled: true, - anonymousUserRole: "user", - defaultUserTeam: "user", - supportsCustomConnections: true, - enableReverseProxyAuth: true, + anonymousAccessEnabled: "${CLOUDBEAVER_APP_ANONYMOUS_ACCESS_ENABLED:true}", + anonymousUserRole: user, + defaultUserTeam: "${CLOUDBEAVER_APP_DEFAULT_USER_TEAM:user}", + supportsCustomConnections: "${CLOUDBEAVER_APP_SUPPORTS_CUSTOM_CONNECTIONS:true}", + enableReverseProxyAuth: "${CLOUDBEAVER_APP_ENABLE_REVERSE_PROXY_AUTH:true}", enabledAuthProviders: [ "local", "reverseProxy" @@ -52,13 +52,12 @@ ], resourceQuotas: { - dataExportFileSizeLimit: 10000000, - sqlMaxRunningQueries: 100, - sqlResultSetRowsLimit: 100000, - sqlResultSetMemoryLimit: 2000000, - sqlTextPreviewMaxLength: 4096, - sqlBinaryPreviewMaxLength: 261120, - sqlQueryTimeout: 5 + dataExportFileSizeLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_DATA_EXPORT_FILE_SIZE_LIMIT:10000000}", + sqlMaxRunningQueries: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_MAX_RUNNING_QUERIES:100}", + sqlResultSetRowsLimit: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_RESULT_SET_ROWS_LIMIT:100000}", + sqlTextPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_TEXT_PREVIEW_MAX_LENGTH:4096}", + sqlBinaryPreviewMaxLength: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_BINARY_PREVIEW_MAX_LENGTH:261120}", + sqlQueryTimeout: "${CLOUDBEAVER_RESOURCE_QUOTA_SQL_QUERY_TIMEOUT:5}" }, disabledDrivers: [ diff --git a/webapp/package.json b/webapp/package.json index f16b36fbc3..8ff63b4aed 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,7 @@ { "name": "cloudbeaver-ce", "version": "1.0.0", + "type": "module", "private": true, "workspaces": { "packages": [ @@ -35,7 +36,7 @@ "@testing-library/user-event": "^14", "@types/react": "^18", "@types/react-dom": "^18", - "concurrently": "^8", + "concurrently": "^9", "husky": "^9", "lerna": "^5", "mobx": "^6", diff --git a/webapp/packages/core-administration/package.json b/webapp/packages/core-administration/package.json index 4ca22be149..fb46112aef 100644 --- a/webapp/packages/core-administration/package.json +++ b/webapp/packages/core-administration/package.json @@ -1,5 +1,6 @@ { "name": "@cloudbeaver/core-administration", + "type": "module", "sideEffects": [ "src/**/*.css", "src/**/*.scss", diff --git a/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts b/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts index 422e719751..1cae5f2989 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts @@ -8,13 +8,18 @@ import { makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; -import { Executor, IExecutor, IExecutorHandler } from '@cloudbeaver/core-executor'; +import { Executor, type IExecutor, type IExecutorHandler } from '@cloudbeaver/core-executor'; import type { RouterState } from '@cloudbeaver/core-routing'; -import { filterConfigurationWizard } from './filterConfigurationWizard'; -import { AdministrationItemType, IAdministrationItem, IAdministrationItemOptions, IAdministrationItemSubItem } from './IAdministrationItem'; -import type { IAdministrationItemRoute } from './IAdministrationItemRoute'; -import { orderAdministrationItems } from './orderAdministrationItems'; +import { filterConfigurationWizard } from './filterConfigurationWizard.js'; +import { + AdministrationItemType, + type IAdministrationItem, + type IAdministrationItemOptions, + type IAdministrationItemSubItem, +} from './IAdministrationItem.js'; +import type { IAdministrationItemRoute } from './IAdministrationItemRoute.js'; +import { orderAdministrationItems } from './orderAdministrationItems.js'; interface IActivationData { screen: IAdministrationItemRoute; @@ -112,14 +117,14 @@ export class AdministrationItemService { return onlyActive.name; } - return items[0].name; + return items[0]?.name || null; } getAdministrationItemRoute(state: RouterState, configurationMode = false): IAdministrationItemRoute { return { - item: state.params.item || this.getDefaultItem(configurationMode), - sub: state.params.sub || null, - param: state.params.param || null, + item: state.params['item'] || this.getDefaultItem(configurationMode), + sub: state.params['sub'] || null, + param: state.params['param'] || null, }; } @@ -162,7 +167,7 @@ export class AdministrationItemService { }; const index = this.items.push(item); - return this.items[index - 1]; + return this.items[index - 1]!; } async activate(screen: IAdministrationItemRoute, configurationWizard: boolean, outside: boolean, outsideAdminPage: boolean): Promise { @@ -241,7 +246,7 @@ export class AdministrationItemService { if (item === items.length) { break; } - await items[item].configurationWizardOptions?.onLoad?.(); + await items[item]?.configurationWizardOptions?.onLoad?.(); item++; } } diff --git a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts index 8b1ec7a058..dce7254076 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IRouteParams } from './IRouteParams'; +import type { IRouteParams } from './IRouteParams.js'; export enum AdministrationItemType { Default, diff --git a/webapp/packages/core-administration/src/AdministrationItem/filterConfigurationWizard.ts b/webapp/packages/core-administration/src/AdministrationItem/filterConfigurationWizard.ts index f070fb1745..816d7b3d1f 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/filterConfigurationWizard.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/filterConfigurationWizard.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { AdministrationItemType, IAdministrationItem } from './IAdministrationItem'; +import { AdministrationItemType, type IAdministrationItem } from './IAdministrationItem.js'; export function filterConfigurationWizard(configurationWizard: boolean) { return (item: IAdministrationItem) => diff --git a/webapp/packages/core-administration/src/AdministrationItem/orderAdministrationItems.ts b/webapp/packages/core-administration/src/AdministrationItem/orderAdministrationItems.ts index 6ebb678278..06599f76ab 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/orderAdministrationItems.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/orderAdministrationItems.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IAdministrationItem } from './IAdministrationItem'; +import type { IAdministrationItem } from './IAdministrationItem.js'; export function orderAdministrationItems(configuration: boolean) { return (itemA: IAdministrationItem, itemB: IAdministrationItem): number => { diff --git a/webapp/packages/core-administration/src/AdministrationLocaleService.ts b/webapp/packages/core-administration/src/AdministrationLocaleService.ts index 8febc531c1..ba5c98f6a8 100644 --- a/webapp/packages/core-administration/src/AdministrationLocaleService.ts +++ b/webapp/packages/core-administration/src/AdministrationLocaleService.ts @@ -14,24 +14,22 @@ export class AdministrationLocaleService extends Bootstrap { super(); } - register(): void | Promise { + override register(): void { this.localizationService.addProvider(this.provider.bind(this)); } - load(): void | Promise {} - private async provider(locale: string) { switch (locale) { case 'ru': - return (await import('./locales/ru')).default; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; case 'fr': - return (await import('./locales/fr')).default; + return (await import('./locales/fr.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index 9c8ff331f1..30828e7841 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -9,16 +9,16 @@ import { computed, makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { Executor, type IExecutor } from '@cloudbeaver/core-executor'; import { EAdminPermission, PermissionsService, ServerConfigResource, SessionPermissionsResource } from '@cloudbeaver/core-root'; -import { RouterState, ScreenService } from '@cloudbeaver/core-routing'; +import { type RouterState, ScreenService } from '@cloudbeaver/core-routing'; import { StorageService } from '@cloudbeaver/core-storage'; -import { DefaultValueGetter, GlobalConstants, MetadataMap, schema } from '@cloudbeaver/core-utils'; +import { type DefaultValueGetter, GlobalConstants, MetadataMap, schema } from '@cloudbeaver/core-utils'; -import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService'; -import type { IAdministrationItemRoute } from '../AdministrationItem/IAdministrationItemRoute'; -import type { IRouteParams } from '../AdministrationItem/IRouteParams'; -import { ADMINISTRATION_SCREEN_STATE_SCHEMA, type IAdministrationScreenInfo } from './IAdministrationScreenState'; +import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService.js'; +import type { IAdministrationItemRoute } from '../AdministrationItem/IAdministrationItemRoute.js'; +import type { IRouteParams } from '../AdministrationItem/IRouteParams.js'; +import { ADMINISTRATION_SCREEN_STATE_SCHEMA, type IAdministrationScreenInfo } from './IAdministrationScreenState.js'; const ADMINISTRATION_INFO = 'administration_info'; @@ -111,6 +111,21 @@ export class AdministrationScreenService { this.permissionsResource.onDataUpdate.addPostHandler(() => { this.checkPermissions(this.screenService.routerService.state); }); + + this.screenService.routeChange.addHandler(this.onRouteChange.bind(this)); + } + + private async onRouteChange() { + // this is need for this.isConfigurationMode + await this.serverConfigResource.load(); + + if (!this.isAdministrationPageActive) { + return; + } + + if (!this.activeScreen || !this.administrationItemService.getItem(this.activeScreen.item, this.isConfigurationMode)) { + this.navigateToRoot(); + } } getRouteName(item?: string, sub?: string, param?: string) { @@ -213,7 +228,7 @@ export class AdministrationScreenService { } async handleCanDeActivate(fromState: RouterState, toState: RouterState): Promise { - if (!fromState.params.item) { + if (!fromState.params['item']) { return true; } @@ -228,7 +243,7 @@ export class AdministrationScreenService { } async handleCanActivate(toState: RouterState, fromState: RouterState): Promise { - if (!toState.params.item) { + if (!toState.params['item']) { return false; } diff --git a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreenService.ts new file mode 100644 index 0000000000..fb7091f711 --- /dev/null +++ b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreenService.ts @@ -0,0 +1,43 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Dependency, injectable } from '@cloudbeaver/core-di'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { ScreenService } from '@cloudbeaver/core-routing'; + +import { AdministrationScreenService } from '../AdministrationScreenService.js'; +import { ConfigurationWizardService } from './ConfigurationWizardService.js'; + +@injectable() +export class ConfigurationWizardScreenService extends Dependency { + constructor( + private readonly administrationScreenService: AdministrationScreenService, + private readonly screenService: ScreenService, + private readonly serverConfigResource: ServerConfigResource, + private readonly configurationWizardService: ConfigurationWizardService, + ) { + super(); + this.screenService.routeChange.addHandler(this.onRouteChange.bind(this)); + } + + private async onRouteChange() { + // this is need for this.isConfigurationMode + await this.serverConfigResource.load(); + + if (!this.administrationScreenService.isConfigurationMode) { + return; + } + + const isCurrentStepAvailable = + this.configurationWizardService.currentStep && + this.configurationWizardService.isStepAvailable(this.configurationWizardService.currentStep.name); + + if (!isCurrentStepAvailable) { + this.administrationScreenService.navigateToRoot(); + } + } +} diff --git a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts index d40cc8a3e2..186a4c89d4 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts @@ -10,11 +10,11 @@ import { computed, makeObservable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { AdministrationItemService, filterHiddenAdministrationItem } from '../../AdministrationItem/AdministrationItemService'; -import { filterConfigurationWizard } from '../../AdministrationItem/filterConfigurationWizard'; -import type { IAdministrationItem } from '../../AdministrationItem/IAdministrationItem'; -import { orderAdministrationItems } from '../../AdministrationItem/orderAdministrationItems'; -import { AdministrationScreenService } from '../AdministrationScreenService'; +import { AdministrationItemService, filterHiddenAdministrationItem } from '../../AdministrationItem/AdministrationItemService.js'; +import { filterConfigurationWizard } from '../../AdministrationItem/filterConfigurationWizard.js'; +import type { IAdministrationItem } from '../../AdministrationItem/IAdministrationItem.js'; +import { orderAdministrationItems } from '../../AdministrationItem/orderAdministrationItems.js'; +import { AdministrationScreenService } from '../AdministrationScreenService.js'; @injectable() export class ConfigurationWizardService { @@ -86,10 +86,6 @@ export class ConfigurationWizardService { } isStepAvailable(name: string): boolean { - if (this.currentStep?.name === name) { - return true; - } - for (const step of this.steps) { if (step.name === name) { return true; @@ -151,7 +147,7 @@ export class ConfigurationWizardService { } if (this.currentStepIndex - 1 >= 0) { - const step = this.steps[this.currentStepIndex - 1]; + const step = this.steps[this.currentStepIndex - 1]!; this.administrationScreenService.navigateTo(step.name, step.configurationWizardOptions?.defaultRoute); } } diff --git a/webapp/packages/core-administration/src/DataContext/DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE.ts b/webapp/packages/core-administration/src/DataContext/DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE.ts index b0948244b9..d2e158b088 100644 --- a/webapp/packages/core-administration/src/DataContext/DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE.ts +++ b/webapp/packages/core-administration/src/DataContext/DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE.ts @@ -7,6 +7,6 @@ */ import { createDataContext } from '@cloudbeaver/core-data-context'; -import type { IAdministrationItemRoute } from '../AdministrationItem/IAdministrationItemRoute'; +import type { IAdministrationItemRoute } from '../AdministrationItem/IAdministrationItemRoute.js'; export const DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE = createDataContext('AdministrationItemRoute'); diff --git a/webapp/packages/core-administration/src/PermissionsResource.ts b/webapp/packages/core-administration/src/PermissionsResource.ts index f49f8020e9..3a3fd145e9 100644 --- a/webapp/packages/core-administration/src/PermissionsResource.ts +++ b/webapp/packages/core-administration/src/PermissionsResource.ts @@ -8,7 +8,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { CachedMapAllKey, CachedMapResource, resourceKeyList } from '@cloudbeaver/core-resource'; import { SessionDataResource } from '@cloudbeaver/core-root'; -import { AdminObjectGrantInfoFragment, AdminPermissionInfoFragment, GraphQLService } from '@cloudbeaver/core-sdk'; +import { type AdminObjectGrantInfoFragment, type AdminPermissionInfoFragment, GraphQLService } from '@cloudbeaver/core-sdk'; export type PermissionInfo = AdminPermissionInfoFragment; export type AdminObjectGrantInfo = AdminObjectGrantInfoFragment; diff --git a/webapp/packages/core-administration/src/index.ts b/webapp/packages/core-administration/src/index.ts index 672bf369c6..d1708c2bc8 100644 --- a/webapp/packages/core-administration/src/index.ts +++ b/webapp/packages/core-administration/src/index.ts @@ -1,12 +1,19 @@ -export * from './manifest'; -export * from './AdministrationItem/AdministrationItemService'; -export * from './AdministrationItem/filterConfigurationWizard'; -export * from './AdministrationItem/IAdministrationItem'; -export * from './AdministrationItem/IAdministrationItemRoute'; -export * from './AdministrationItem/IRouteParams'; -export * from './AdministrationItem/orderAdministrationItems'; -export * from './AdministrationScreen/AdministrationScreenService'; -export * from './AdministrationScreen/ConfigurationWizard/ConfigurationWizardService'; -export * from './DataContext/DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE'; -export * from './AdministrationLocaleService'; -export * from './PermissionsResource'; +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +export * from './manifest.js'; +export * from './AdministrationItem/AdministrationItemService.js'; +export * from './AdministrationItem/filterConfigurationWizard.js'; +export * from './AdministrationItem/IAdministrationItem.js'; +export * from './AdministrationItem/IAdministrationItemRoute.js'; +export * from './AdministrationItem/IRouteParams.js'; +export * from './AdministrationItem/orderAdministrationItems.js'; +export * from './AdministrationScreen/AdministrationScreenService.js'; +export * from './AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.js'; +export * from './DataContext/DATA_CONTEXT_ADMINISTRATION_ITEM_ROUTE.js'; +export * from './AdministrationLocaleService.js'; +export * from './PermissionsResource.js'; diff --git a/webapp/packages/core-administration/src/locales/zh.ts b/webapp/packages/core-administration/src/locales/zh.ts index a83ba9a2ee..7b8cc21cf1 100644 --- a/webapp/packages/core-administration/src/locales/zh.ts +++ b/webapp/packages/core-administration/src/locales/zh.ts @@ -1,7 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ export default [ ['administration_menu_enter', '管理'], ['administration_menu_back', '返回应用'], ['administration_configuration_wizard_title', '初始化服务器配置'], - ['administration_configuration_wizard_finish_success_title', 'Server configured'], - ['administration_configuration_wizard_finish_success_message', 'You can log-in as administrator in order to set up additional parameters.'], + ['administration_configuration_wizard_finish_success_title', '服务器已完成配置'], + ['administration_configuration_wizard_finish_success_message', '您可以使用管理员登录后进行更多参数配置。'], ]; diff --git a/webapp/packages/core-administration/src/manifest.ts b/webapp/packages/core-administration/src/manifest.ts index e3c9c4bdd6..af2c95191e 100644 --- a/webapp/packages/core-administration/src/manifest.ts +++ b/webapp/packages/core-administration/src/manifest.ts @@ -13,10 +13,11 @@ export const coreAdministrationManifest: PluginManifest = { }, providers: [ - () => import('./AdministrationItem/AdministrationItemService').then(m => m.AdministrationItemService), - () => import('./PermissionsResource').then(m => m.PermissionsResource), - () => import('./AdministrationScreen/AdministrationScreenService').then(m => m.AdministrationScreenService), - () => import('./AdministrationScreen/ConfigurationWizard/ConfigurationWizardService').then(m => m.ConfigurationWizardService), - () => import('./AdministrationLocaleService').then(m => m.AdministrationLocaleService), + () => import('./AdministrationItem/AdministrationItemService.js').then(m => m.AdministrationItemService), + () => import('./PermissionsResource.js').then(m => m.PermissionsResource), + () => import('./AdministrationScreen/AdministrationScreenService.js').then(m => m.AdministrationScreenService), + () => import('./AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.js').then(m => m.ConfigurationWizardService), + () => import('./AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreenService.js').then(m => m.ConfigurationWizardScreenService), + () => import('./AdministrationLocaleService.js').then(m => m.AdministrationLocaleService), ], }; diff --git a/webapp/packages/core-app/package.json b/webapp/packages/core-app/package.json index c2eda7cecb..62e0321797 100644 --- a/webapp/packages/core-app/package.json +++ b/webapp/packages/core-app/package.json @@ -1,5 +1,6 @@ { "name": "@cloudbeaver/core-app", + "type": "module", "sideEffects": [ "src/**/*.css", "src/**/*.scss", diff --git a/webapp/packages/core-app/src/AppLocaleService.ts b/webapp/packages/core-app/src/AppLocaleService.ts index 6d835fb52a..8d5fb6df62 100644 --- a/webapp/packages/core-app/src/AppLocaleService.ts +++ b/webapp/packages/core-app/src/AppLocaleService.ts @@ -14,24 +14,22 @@ export class AppLocaleService extends Bootstrap { super(); } - register(): void | Promise { + override register(): void { this.localizationService.addProvider(this.provider.bind(this)); } - load(): void | Promise {} - private async provider(locale: string) { switch (locale) { case 'ru': - return (await import('./locales/ru')).default; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; case 'fr': - return (await import('./locales/fr')).default; + return (await import('./locales/fr.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/core-app/src/AppScreen/AppScreen.tsx b/webapp/packages/core-app/src/AppScreen/AppScreen.tsx index 8b364ee9e1..62bcb6ab1f 100644 --- a/webapp/packages/core-app/src/AppScreen/AppScreen.tsx +++ b/webapp/packages/core-app/src/AppScreen/AppScreen.tsx @@ -10,8 +10,8 @@ import { memo } from 'react'; import { Loader, Placeholder } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { AppScreenService } from './AppScreenService'; -import { Main } from './Main'; +import { AppScreenService } from './AppScreenService.js'; +import { Main } from './Main.js'; export const AppScreen = memo(function AppScreen() { const appScreenService = useService(AppScreenService); diff --git a/webapp/packages/core-app/src/AppScreen/AppScreenBootstrap.ts b/webapp/packages/core-app/src/AppScreen/AppScreenBootstrap.ts index 7e7a71395e..2875ea25b4 100644 --- a/webapp/packages/core-app/src/AppScreen/AppScreenBootstrap.ts +++ b/webapp/packages/core-app/src/AppScreen/AppScreenBootstrap.ts @@ -6,11 +6,11 @@ * you may not use this file except in compliance with the License. */ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { Executor, type IExecutor } from '@cloudbeaver/core-executor'; import { ScreenService } from '@cloudbeaver/core-routing'; -import { AppScreen } from './AppScreen'; -import { AppScreenService } from './AppScreenService'; +import { AppScreen } from './AppScreen.js'; +import { AppScreenService } from './AppScreenService.js'; @injectable() export class AppScreenBootstrap extends Bootstrap { @@ -21,7 +21,7 @@ export class AppScreenBootstrap extends Bootstrap { this.activation = new Executor(); } - register(): void { + override register(): void { this.screenService.create({ name: AppScreenService.screenName, routes: [{ name: AppScreenService.screenName, path: '/' }], @@ -32,6 +32,4 @@ export class AppScreenBootstrap extends Bootstrap { }, }); } - - load(): void | Promise {} } diff --git a/webapp/packages/core-app/src/AppScreen/Main.tsx b/webapp/packages/core-app/src/AppScreen/Main.tsx index dcc56e93db..514dc8a2e2 100644 --- a/webapp/packages/core-app/src/AppScreen/Main.tsx +++ b/webapp/packages/core-app/src/AppScreen/Main.tsx @@ -12,7 +12,7 @@ import { useService } from '@cloudbeaver/core-di'; import { LeftBarPanelService, SideBarPanel, SideBarPanelService } from '@cloudbeaver/core-ui'; import style from './Main.module.css'; -import { RightArea } from './RightArea'; +import { RightArea } from './RightArea.js'; export const Main = observer(function Main() { const styles = useS(style); diff --git a/webapp/packages/core-app/src/AppScreen/RightArea.tsx b/webapp/packages/core-app/src/AppScreen/RightArea.tsx index 470a220660..34097d383f 100644 --- a/webapp/packages/core-app/src/AppScreen/RightArea.tsx +++ b/webapp/packages/core-app/src/AppScreen/RightArea.tsx @@ -23,7 +23,7 @@ import { import { useService } from '@cloudbeaver/core-di'; import { OptionsPanelService } from '@cloudbeaver/core-ui'; -import { AppScreenService } from './AppScreenService'; +import { AppScreenService } from './AppScreenService.js'; import style from './RightArea.module.css'; interface Props { @@ -40,8 +40,12 @@ export const RightArea = observer(function RightArea({ className }) { const toolsDisabled = appScreenService.rightAreaBottom.getDisplayed({}).length === 0; + function close() { + optionsPanelService.close(); + } + return ( - + @@ -61,7 +65,7 @@ export const RightArea = observer(function RightArea({ className }) { - optionsPanelService.close()} /> + ); diff --git a/webapp/packages/core-app/src/Body.tsx b/webapp/packages/core-app/src/Body.tsx index 831aa61315..03fcc3698b 100644 --- a/webapp/packages/core-app/src/Body.tsx +++ b/webapp/packages/core-app/src/Body.tsx @@ -20,8 +20,8 @@ import { DNDProvider } from '@cloudbeaver/core-ui'; import { useAppVersion } from '@cloudbeaver/core-version'; import style from './Body.module.css'; -import { useAppHeight } from './useAppHeight'; -import { useClientActivity } from './useClientActivity'; +import { useAppHeight } from './useAppHeight.js'; +import { useClientActivity } from './useClientActivity.js'; export const Body = observer(function Body() { // const serverConfigLoader = useResource(Body, ServerConfigResource, undefined); @@ -41,7 +41,7 @@ export const Body = observer(function Body() { if (ref.current) { document.body.className = ref.current.className; } - document.documentElement.dataset.backendVersion = backendVersion; + document.documentElement.dataset['backendVersion'] = backendVersion; }); useAppHeight(); @@ -50,7 +50,17 @@ export const Body = observer(function Body() { return ( -

+
{Screen && } diff --git a/webapp/packages/core-app/src/BodyLazy.ts b/webapp/packages/core-app/src/BodyLazy.ts index 3d8043c34d..e717806089 100644 --- a/webapp/packages/core-app/src/BodyLazy.ts +++ b/webapp/packages/core-app/src/BodyLazy.ts @@ -7,4 +7,4 @@ */ import { importLazyComponent } from '@cloudbeaver/core-blocks'; -export const BodyLazy = importLazyComponent(() => import('./Body').then(m => m.Body)); +export const BodyLazy = importLazyComponent(() => import('./Body.js').then(m => m.Body)); diff --git a/webapp/packages/core-app/src/index.ts b/webapp/packages/core-app/src/index.ts index 51841f691c..e7c12637ba 100644 --- a/webapp/packages/core-app/src/index.ts +++ b/webapp/packages/core-app/src/index.ts @@ -1,11 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ // Services -export * from './AppScreen/AppScreenService'; -export * from './AppScreen/AppScreenBootstrap'; +export * from './AppScreen/AppScreenService.js'; +export * from './AppScreen/AppScreenBootstrap.js'; -export * from './AppLocaleService'; +export * from './AppLocaleService.js'; // components -export * from './BodyLazy'; +export * from './BodyLazy.js'; // Interfaces -export * from './manifest'; +export * from './manifest.js'; diff --git a/webapp/packages/core-app/src/manifest.ts b/webapp/packages/core-app/src/manifest.ts index e857c64f6c..0c64908dd9 100644 --- a/webapp/packages/core-app/src/manifest.ts +++ b/webapp/packages/core-app/src/manifest.ts @@ -13,8 +13,8 @@ export const coreAppManifest: PluginManifest = { }, providers: [ - () => import('./AppScreen/AppScreenService').then(m => m.AppScreenService), - () => import('./AppScreen/AppScreenBootstrap').then(m => m.AppScreenBootstrap), - () => import('./AppLocaleService').then(m => m.AppLocaleService), + () => import('./AppScreen/AppScreenService.js').then(m => m.AppScreenService), + () => import('./AppScreen/AppScreenBootstrap.js').then(m => m.AppScreenBootstrap), + () => import('./AppLocaleService.js').then(m => m.AppLocaleService), ], }; diff --git a/webapp/packages/core-authentication/package.json b/webapp/packages/core-authentication/package.json index bff33346db..2cfe0dce7e 100644 --- a/webapp/packages/core-authentication/package.json +++ b/webapp/packages/core-authentication/package.json @@ -1,5 +1,6 @@ { "name": "@cloudbeaver/core-authentication", + "type": "module", "sideEffects": [ "src/**/*.css", "src/**/*.scss", @@ -41,7 +42,7 @@ "@cloudbeaver/core-settings": "^0", "@cloudbeaver/tests-runner": "^0", "@jest/globals": "^29", - "@testing-library/jest-dom": "^6", + "@types/jest": "^29", "msw": "^2", "typescript": "^5" } diff --git a/webapp/packages/core-authentication/src/ADMIN_USERNAME_MIN_LENGTH.ts b/webapp/packages/core-authentication/src/ADMIN_USERNAME_MIN_LENGTH.ts new file mode 100644 index 0000000000..fff36b1d12 --- /dev/null +++ b/webapp/packages/core-authentication/src/ADMIN_USERNAME_MIN_LENGTH.ts @@ -0,0 +1,9 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export const ADMIN_USERNAME_MIN_LENGTH = 6; diff --git a/webapp/packages/core-authentication/src/AppAuthService.ts b/webapp/packages/core-authentication/src/AppAuthService.ts index e76d207d2b..0e653faf10 100644 --- a/webapp/packages/core-authentication/src/AppAuthService.ts +++ b/webapp/packages/core-authentication/src/AppAuthService.ts @@ -6,19 +6,17 @@ * you may not use this file except in compliance with the License. */ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-executor'; -import { CachedDataResourceKey, CachedResource, getCachedDataResourceLoaderState } from '@cloudbeaver/core-resource'; +import { Executor, ExecutorInterrupter, type IExecutor } from '@cloudbeaver/core-executor'; +import { type CachedDataResourceKey, CachedResource, getCachedDataResourceLoaderState } from '@cloudbeaver/core-resource'; import { ServerConfigResource } from '@cloudbeaver/core-root'; import type { ILoadableState } from '@cloudbeaver/core-utils'; -import { UserInfoResource } from './UserInfoResource'; +import { UserInfoResource } from './UserInfoResource.js'; @injectable() export class AppAuthService extends Bootstrap { get authenticated(): boolean { - const user = this.userInfoResource.data; - - return this.serverConfigResource.anonymousAccessEnabled || this.serverConfigResource.configurationMode || user !== null; + return this.serverConfigResource.configurationMode || this.userInfoResource.hasAccess(); } get loaders(): ILoadableState[] { @@ -56,9 +54,9 @@ export class AppAuthService extends Bootstrap { throw new Error("Can't configure Authentication"); } - const user = await this.userInfoResource.load(); + await this.userInfoResource.load(); - return !this.serverConfigResource.configurationMode && !this.serverConfigResource.anonymousAccessEnabled && user === null; + return !this.serverConfigResource.configurationMode && !this.userInfoResource.hasAccess(); } async authUser(): Promise { @@ -68,8 +66,4 @@ export class AppAuthService extends Bootstrap { await this.auth.execute(state); return state; } - - register(): void {} - - load(): void {} } diff --git a/webapp/packages/core-authentication/src/AuthConfigurationParametersResource.ts b/webapp/packages/core-authentication/src/AuthConfigurationParametersResource.ts index 19f223b322..c5359f36f6 100644 --- a/webapp/packages/core-authentication/src/AuthConfigurationParametersResource.ts +++ b/webapp/packages/core-authentication/src/AuthConfigurationParametersResource.ts @@ -9,8 +9,8 @@ import { injectable } from '@cloudbeaver/core-di'; import { CachedMapResource, isResourceAlias, type ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import { EAdminPermission, SessionDataResource, SessionPermissionsResource } from '@cloudbeaver/core-root'; import { - AuthProviderConfigurationParametersFragment, - GetAuthProviderConfigurationParametersQueryVariables, + type AuthProviderConfigurationParametersFragment, + type GetAuthProviderConfigurationParametersQueryVariables, GraphQLService, } from '@cloudbeaver/core-sdk'; diff --git a/webapp/packages/core-authentication/src/AuthConfigurationsResource.ts b/webapp/packages/core-authentication/src/AuthConfigurationsResource.ts index 14960db42b..cec81b6319 100644 --- a/webapp/packages/core-authentication/src/AuthConfigurationsResource.ts +++ b/webapp/packages/core-authentication/src/AuthConfigurationsResource.ts @@ -18,9 +18,9 @@ import { ResourceKeyUtils, } from '@cloudbeaver/core-resource'; import { EAdminPermission, SessionPermissionsResource } from '@cloudbeaver/core-root'; -import { AdminAuthProviderConfiguration, GetAuthProviderConfigurationsQueryVariables, GraphQLService } from '@cloudbeaver/core-sdk'; +import { type AdminAuthProviderConfiguration, type GetAuthProviderConfigurationsQueryVariables, GraphQLService } from '@cloudbeaver/core-sdk'; -import type { AuthProviderConfiguration } from './AuthProvidersResource'; +import type { AuthProviderConfiguration } from './AuthProvidersResource.js'; const NEW_CONFIGURATION_SYMBOL = Symbol('new-configuration'); @@ -106,7 +106,7 @@ export class AuthConfigurationsResource extends CachedMapResource; export type AuthProviderConfiguration = NonNullable; @@ -31,8 +31,13 @@ export class AuthProvidersResource extends CachedMapResource provider.configurable); } + get enabledConfigurableAuthProviders(): AuthProvider[] { + const enabledProviders = new Set(this.serverConfigResource.data?.enabledAuthProviders); + + return this.configurable.filter(provider => enabledProviders.has(provider.id)); + } + constructor( - private readonly authSettingsService: AuthSettingsService, private readonly graphQLService: GraphQLService, private readonly serverConfigResource: ServerConfigResource, private readonly authConfigurationsResource: AuthConfigurationsResource, @@ -64,7 +69,7 @@ export class AuthProvidersResource extends CachedMapResource [ // { - // key: 'disableAnonymousAccess', + // key: 'core.authentication.disableAnonymousAccess', + // access: { + // scope: ['server'], + // }, // type: ESettingsValueType.Checkbox, // name: 'settings_authentication_disable_anonymous_access_name', // description: 'settings_authentication_disable_anonymous_access_description', diff --git a/webapp/packages/core-authentication/src/LocaleService.ts b/webapp/packages/core-authentication/src/LocaleService.ts index f8618985f9..a55cfec20e 100644 --- a/webapp/packages/core-authentication/src/LocaleService.ts +++ b/webapp/packages/core-authentication/src/LocaleService.ts @@ -14,24 +14,22 @@ export class LocaleService extends Bootstrap { super(); } - register(): void | Promise { + override register(): void { this.localizationService.addProvider(this.provider.bind(this)); } - load(): void | Promise {} - private async provider(locale: string) { switch (locale) { case 'ru': - return (await import('./locales/ru')).default; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; case 'zh': - return (await import('./locales/zh')).default; + return (await import('./locales/zh.js')).default; case 'fr': - return (await import('./locales/fr')).default; + return (await import('./locales/fr.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/core-authentication/src/TeamInfoMetaParametersResource.ts b/webapp/packages/core-authentication/src/TeamInfoMetaParametersResource.ts new file mode 100644 index 0000000000..f1e213544d --- /dev/null +++ b/webapp/packages/core-authentication/src/TeamInfoMetaParametersResource.ts @@ -0,0 +1,78 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { CachedMapAllKey, CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; + +import type { TeamMetaParameter } from './TeamMetaParametersResource.js'; +import { TeamsResource } from './TeamsResource.js'; + +@injectable() +export class TeamInfoMetaParametersResource extends CachedMapResource { + constructor( + private readonly graphQLService: GraphQLService, + private readonly teamsResource: TeamsResource, + ) { + super(); + + this.sync(this.teamsResource); + this.teamsResource.onItemDelete.addHandler(this.delete.bind(this)); + } + + protected async loader(param: ResourceKey): Promise> { + const all = this.aliases.isAlias(param, CachedMapAllKey); + const teamsList: [string, TeamMetaParameter][] = []; + + await ResourceKeyUtils.forEachAsync(param, async key => { + let teamId: string | undefined; + + if (!isResourceAlias(key)) { + teamId = key; + } + + const { teams } = await this.graphQLService.sdk.getTeamsListMetaParameters({ + teamId, + }); + + if (!teams.length) { + throw new Error(`Team ${teamId} not found`); + } + + const metaParameters = teams[0]?.metaParameters; + + if (teamId) { + teamsList.push([teamId, metaParameters]); + } + }); + + const key = resourceKeyList(teamsList.map(([teamId]) => teamId)); + const value = teamsList.map(([_, metaParameters]) => metaParameters); + + if (all) { + this.replace(key, value); + } else { + this.set(key, value); + } + + return this.data; + } + + async setMetaParameters(teamId: string, parameters: Record): Promise { + await this.performUpdate(teamId, [], async () => { + await this.graphQLService.sdk.saveTeamMetaParameters({ teamId, parameters }); + + if (this.data) { + this.data.set(teamId, parameters as TeamMetaParameter); + } + }); + } + + protected validateKey(key: string): boolean { + return typeof key === 'string'; + } +} diff --git a/webapp/packages/core-authentication/src/TeamMetaParametersResource.ts b/webapp/packages/core-authentication/src/TeamMetaParametersResource.ts index dd8d85ec31..118952f9f6 100644 --- a/webapp/packages/core-authentication/src/TeamMetaParametersResource.ts +++ b/webapp/packages/core-authentication/src/TeamMetaParametersResource.ts @@ -8,7 +8,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { CachedDataResource } from '@cloudbeaver/core-resource'; import { SessionResource } from '@cloudbeaver/core-root'; -import { GraphQLService, ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { GraphQLService, type ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; export type TeamMetaParameter = ObjectPropertyInfo; diff --git a/webapp/packages/core-authentication/src/TeamsManagerService.ts b/webapp/packages/core-authentication/src/TeamsManagerService.ts index e213ae62bb..c4a20912bd 100644 --- a/webapp/packages/core-authentication/src/TeamsManagerService.ts +++ b/webapp/packages/core-authentication/src/TeamsManagerService.ts @@ -7,7 +7,7 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { TeamsResource } from './TeamsResource'; +import { TeamsResource } from './TeamsResource.js'; @injectable() export class TeamsManagerService { diff --git a/webapp/packages/core-authentication/src/TeamsResource.ts b/webapp/packages/core-authentication/src/TeamsResource.ts index 08373dfec0..832c1d880c 100644 --- a/webapp/packages/core-authentication/src/TeamsResource.ts +++ b/webapp/packages/core-authentication/src/TeamsResource.ts @@ -16,13 +16,13 @@ import { ResourceKeyUtils, } from '@cloudbeaver/core-resource'; import { - AdminConnectionGrantInfo, - AdminTeamInfoFragment, - AdminUserTeamGrantInfo, - GetTeamsListQueryVariables, + type AdminConnectionGrantInfo, + type AdminTeamInfoFragment, + type AdminUserTeamGrantInfo, + type GetTeamsListQueryVariables, GraphQLService, } from '@cloudbeaver/core-sdk'; -import { isArraysEqual, UndefinedToNull } from '@cloudbeaver/core-utils'; +import { isArraysEqual, type UndefinedToNull } from '@cloudbeaver/core-utils'; const NEW_TEAM_SYMBOL = Symbol('new-team'); @@ -38,12 +38,11 @@ export class TeamsResource extends CachedMapResource { + async createTeam({ teamId, teamPermissions, teamName, description }: TeamInfo): Promise { const response = await this.graphQLService.sdk.createTeam({ teamId, teamName, description, - ...this.getDefaultIncludes(), ...this.getIncludesMap(teamId), }); @@ -55,24 +54,21 @@ export class TeamsResource extends CachedMapResource { + async updateTeam({ teamId, teamPermissions, teamName, description }: TeamInfo): Promise { const { team } = await this.graphQLService.sdk.updateTeam({ teamId, teamName, description, - ...this.getDefaultIncludes(), ...this.getIncludesMap(teamId), }); this.set(team.teamId, team); - await this.setMetaParameters(team.teamId, metaParameters); await this.setSubjectPermissions(team.teamId, teamPermissions); this.markOutdated(team.teamId); @@ -94,7 +90,11 @@ export class TeamsResource extends CachedMapResource { const { team } = await this.graphQLService.sdk.getTeamGrantedUsers({ teamId }); - return team[0].grantedUsersInfo.map(user => ({ userId: user.userId, teamRole: user.teamRole ?? null })); + + if (!team.length) { + throw new Error('Team not found'); + } + return team[0]!.grantedUsersInfo.map(user => ({ userId: user.userId, teamRole: user.teamRole ?? null })); } async getSubjectConnectionAccess(subjectId: string): Promise { @@ -119,10 +119,6 @@ export class TeamsResource extends CachedMapResource): Promise { - await this.graphQLService.sdk.saveTeamMetaParameters({ teamId, parameters }); - } - protected async loader(originalKey: ResourceKey, includes?: string[]): Promise> { const all = this.aliases.isAlias(originalKey, CachedMapAllKey); const teamsList: TeamInfo[] = []; @@ -136,7 +132,6 @@ export class TeamsResource extends CachedMapResource { + override async load(): Promise { await this.userInfoResource.load(); } } diff --git a/webapp/packages/core-authentication/src/UserDataService.ts b/webapp/packages/core-authentication/src/UserDataService.ts index fcaa223d89..85c2e69e25 100644 --- a/webapp/packages/core-authentication/src/UserDataService.ts +++ b/webapp/packages/core-authentication/src/UserDataService.ts @@ -11,7 +11,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { StorageService } from '@cloudbeaver/core-storage'; import { TempMap } from '@cloudbeaver/core-utils'; -import { UserInfoResource } from './UserInfoResource'; +import { UserInfoResource } from './UserInfoResource.js'; @injectable() export class UserDataService { diff --git a/webapp/packages/core-authentication/src/UserInfoMetaParametersResource.ts b/webapp/packages/core-authentication/src/UserInfoMetaParametersResource.ts new file mode 100644 index 0000000000..2cb361f05e --- /dev/null +++ b/webapp/packages/core-authentication/src/UserInfoMetaParametersResource.ts @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { CachedDataResource, type ResourceKey } from '@cloudbeaver/core-resource'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; + +import { UserInfoResource } from './UserInfoResource.js'; +import type { UserMetaParameter } from './UserMetaParametersResource.js'; + +@injectable() +export class UserInfoMetaParametersResource extends CachedDataResource { + constructor( + private readonly graphQLService: GraphQLService, + private readonly userInfoResource: UserInfoResource, + ) { + super(() => undefined, undefined); + + this.sync(this.userInfoResource); + } + + protected async loader(param: ResourceKey): Promise { + const { user } = await this.graphQLService.sdk.getActiveUserMetaParameters(); + + return user?.metaParameters; + } +} diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index 132327173a..0537b2569b 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -8,15 +8,22 @@ import { computed, makeObservable, runInAction } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; -import { AutoRunningTask, ISyncExecutor, ITask, SyncExecutor, whileTask } from '@cloudbeaver/core-executor'; +import { AutoRunningTask, type ISyncExecutor, type ITask, SyncExecutor, whileTask } from '@cloudbeaver/core-executor'; import { CachedDataResource, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import { SessionResource } from '@cloudbeaver/core-root'; -import { AuthInfo, AuthLogoutQuery, AuthStatus, GetActiveUserQueryVariables, GraphQLService, UserInfo } from '@cloudbeaver/core-sdk'; - -import { AUTH_PROVIDER_LOCAL_ID } from './AUTH_PROVIDER_LOCAL_ID'; -import { AuthProviderService } from './AuthProviderService'; -import type { ELMRole } from './ELMRole'; -import type { IAuthCredentials } from './IAuthCredentials'; +import { + type AuthInfo, + type AuthLogoutQuery, + AuthStatus, + type GetActiveUserQueryVariables, + GraphQLService, + type UserInfo, +} from '@cloudbeaver/core-sdk'; + +import { AUTH_PROVIDER_LOCAL_ID } from './AUTH_PROVIDER_LOCAL_ID.js'; +import { AuthProviderService } from './AuthProviderService.js'; +import type { ELMRole } from './ELMRole.js'; +import type { IAuthCredentials } from './IAuthCredentials.js'; export type UserInfoIncludes = GetActiveUserQueryVariables; @@ -53,7 +60,7 @@ export class UserInfoResource extends CachedDataResource null, undefined, ['customIncludeOriginDetails', 'includeConfigurationParameters']); + super(() => null, undefined, ['includeConfigurationParameters']); this.onUserChange = new SyncExecutor(); this.onException = new SyncExecutor(); @@ -69,6 +76,18 @@ export class UserInfoResource extends CachedDataResource { + constructor( + private readonly graphQLService: GraphQLService, + private readonly usersResource: UsersResource, + ) { + super(); + + this.sync(this.usersResource); + this.usersResource.onItemDelete.addHandler(this.delete.bind(this)); + } + + async setMetaParameters(userId: string, parameters: Record): Promise { + await this.performUpdate(userId, undefined, async () => { + await this.graphQLService.sdk.saveUserMetaParameters({ userId, parameters }); + + if (this.data) { + this.data.set(userId, parameters as UserMetaParameter); + } + }); + } + + protected async loader(originalKey: ResourceKey): Promise> { + const all = this.aliases.isAlias(originalKey, CachedMapAllKey); + const keys = resourceKeyList([]); + + if (all) { + throw new Error('Loading all users is prohibited'); + } + + const userMetaParametersList: UserMetaParameter[] = []; + + await ResourceKeyUtils.forEachAsync(originalKey, async key => { + let userId: string | undefined; + + if (!isResourceAlias(key)) { + userId = key; + } + + if (userId !== undefined) { + const { user } = await this.graphQLService.sdk.getAdminUserMetaParameters({ + userId, + }); + + keys.push(userId); + userMetaParametersList.push(user.metaParameters); + } + }); + + this.set(keys, userMetaParametersList); + + return this.data; + } + + protected validateKey(key: string): boolean { + return typeof key === 'string'; + } +} diff --git a/webapp/packages/core-authentication/src/UsersOriginDetailsResource.ts b/webapp/packages/core-authentication/src/UsersOriginDetailsResource.ts new file mode 100644 index 0000000000..8ab8d596f6 --- /dev/null +++ b/webapp/packages/core-authentication/src/UsersOriginDetailsResource.ts @@ -0,0 +1,61 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { CachedMapAllKey, CachedMapResource, isResourceAlias, type ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { type AdminOriginDetailsFragment, GraphQLService } from '@cloudbeaver/core-sdk'; + +import { UsersResource } from './UsersResource.js'; + +@injectable() +export class UsersOriginDetailsResource extends CachedMapResource { + constructor( + private readonly graphQLService: GraphQLService, + private readonly usersResource: UsersResource, + ) { + super(); + + this.sync(this.usersResource); + this.usersResource.onItemDelete.addHandler(this.delete.bind(this)); + } + + protected async loader(originalKey: ResourceKey): Promise> { + const all = this.aliases.isAlias(originalKey, CachedMapAllKey); + const keys = resourceKeyList([]); + + if (all) { + throw new Error('Loading all users is prohibited'); + } + + const userMetaParametersList: AdminOriginDetailsFragment[] = []; + + await ResourceKeyUtils.forEachAsync(originalKey, async key => { + let userId: string | undefined; + + if (!isResourceAlias(key)) { + userId = key; + } + + if (userId !== undefined) { + const { user } = await this.graphQLService.sdk.getAdminUserOriginDetails({ + userId, + }); + + keys.push(userId); + userMetaParametersList.push(user); + } + }); + + this.set(keys, userMetaParametersList); + + return this.data; + } + + protected validateKey(key: string): boolean { + return typeof key === 'string'; + } +} diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index 8155178da0..f129e6e0a9 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -5,14 +5,15 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { runInAction } from 'mobx'; + import { injectable } from '@cloudbeaver/core-di'; import { - CACHED_RESOURCE_DEFAULT_PAGE_LIMIT, - CACHED_RESOURCE_DEFAULT_PAGE_OFFSET, CachedMapAllKey, CachedMapResource, CachedResourceOffsetPageKey, CachedResourceOffsetPageListKey, + getOffsetPageKeyInfo, isResourceAlias, type ResourceKey, resourceKeyList, @@ -22,12 +23,18 @@ import { ResourceKeyUtils, } from '@cloudbeaver/core-resource'; import { EAdminPermission, ServerConfigResource, SessionPermissionsResource } from '@cloudbeaver/core-root'; -import { AdminConnectionGrantInfo, AdminUserInfo, AdminUserInfoFragment, GetUsersListQueryVariables, GraphQLService } from '@cloudbeaver/core-sdk'; - -import { AUTH_PROVIDER_LOCAL_ID } from './AUTH_PROVIDER_LOCAL_ID'; -import { AuthInfoService } from './AuthInfoService'; -import { AuthProviderService } from './AuthProviderService'; -import type { IAuthCredentials } from './IAuthCredentials'; +import { + type AdminConnectionGrantInfo, + type AdminUserInfo, + type AdminUserInfoFragment, + type GetUsersListQueryVariables, + GraphQLService, +} from '@cloudbeaver/core-sdk'; + +import { AUTH_PROVIDER_LOCAL_ID } from './AUTH_PROVIDER_LOCAL_ID.js'; +import { AuthInfoService } from './AuthInfoService.js'; +import { AuthProviderService } from './AuthProviderService.js'; +import type { IAuthCredentials } from './IAuthCredentials.js'; const NEW_USER_SYMBOL = Symbol('new-user'); @@ -53,6 +60,7 @@ export const UsersResourceNewUsers = resourceKeyListAlias('@users-resource/new-u interface UserCreateOptions { userId: string; authRole?: string; + enabled?: boolean; } @injectable() @@ -130,16 +138,11 @@ export class UsersResource extends CachedMapResource): Promise { - await this.graphQLService.sdk.saveUserMetaParameters({ userId, parameters }); - } - - async create({ userId, authRole }: UserCreateOptions): Promise { + async create({ userId, authRole, enabled }: UserCreateOptions): Promise { const { user } = await this.graphQLService.sdk.createUser({ userId, authRole, - enabled: false, - ...this.getDefaultIncludes(), + enabled: enabled ?? false, ...this.getIncludesMap(userId), }); @@ -218,13 +221,13 @@ export class UsersResource extends CachedMapResource): Promise { + async deleteUsers(key: ResourceKeySimple): Promise { await ResourceKeyUtils.forEachAsync(key, async key => { if (this.isActiveUser(key)) { throw new Error("You can't delete current logged user"); } await this.graphQLService.sdk.deleteUser({ userId: key }); - super.delete(key); + this.delete(key); }); } @@ -240,6 +243,7 @@ export class UsersResource extends CachedMapResource[] = []; await ResourceKeyUtils.forEachAsync(originalKey, async key => { let userId: string | undefined; @@ -251,25 +255,16 @@ export class UsersResource extends CachedMapResource user.userId), + users.length === limit, + ]); } }); const key = resourceKeyList(usersList.map(user => user.userId)); - this.set(key, usersList); + runInAction(() => { + this.set(key, usersList); + for (const pageArgs of pages) { + this.offsetPagination.setPage(...pageArgs); + } + }); return this.data; } - private getDefaultIncludes(): UserResourceIncludes { - return { - customIncludeOriginDetails: false, - includeMetaParameters: false, - }; - } - - protected dataSet(key: string, value: AdminUserInfoFragment): void { + protected override dataSet(key: string, value: AdminUserInfoFragment): void { const oldValue = this.data.get(key); super.dataSet(key, { ...oldValue, ...value }); } diff --git a/webapp/packages/core-authentication/src/__custom_mocks__/mockAuthentication.ts b/webapp/packages/core-authentication/src/__custom_mocks__/mockAuthentication.ts index 58c9983935..9175c4a5ce 100644 --- a/webapp/packages/core-authentication/src/__custom_mocks__/mockAuthentication.ts +++ b/webapp/packages/core-authentication/src/__custom_mocks__/mockAuthentication.ts @@ -7,7 +7,7 @@ */ import type { graphql } from 'msw'; -import { mockGetActiveUser } from './resolvers/mockGetActiveUser'; +import { mockGetActiveUser } from './resolvers/mockGetActiveUser.js'; export function mockAuthentication(endpoint: ReturnType) { return [endpoint.query('getActiveUser', mockGetActiveUser)]; diff --git a/webapp/packages/core-authentication/src/__custom_mocks__/resolvers/mockGetActiveUser.ts b/webapp/packages/core-authentication/src/__custom_mocks__/resolvers/mockGetActiveUser.ts index 99fab2f88b..d32f4957a6 100644 --- a/webapp/packages/core-authentication/src/__custom_mocks__/resolvers/mockGetActiveUser.ts +++ b/webapp/packages/core-authentication/src/__custom_mocks__/resolvers/mockGetActiveUser.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { GraphQLResolverExtras, GraphQLResponseBody, HttpResponse, ResponseResolver } from 'msw'; +import { type GraphQLResolverExtras, type GraphQLResponseBody, HttpResponse, type ResponseResolver } from 'msw'; import type { GetActiveUserQuery, GetActiveUserQueryVariables } from '@cloudbeaver/core-sdk'; diff --git a/webapp/packages/core-authentication/src/index.ts b/webapp/packages/core-authentication/src/index.ts index ec66902c08..5305ceadce 100644 --- a/webapp/packages/core-authentication/src/index.ts +++ b/webapp/packages/core-authentication/src/index.ts @@ -5,26 +5,32 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -export * from './manifest'; -export * from './ELMRole'; -export * from './AppAuthService'; -export * from './AUTH_PROVIDER_LOCAL_ID'; -export * from './AuthInfoService'; -export * from './AuthProviderService'; -export * from './AuthProvidersResource'; -export * from './AuthRolesResource'; -export * from './AuthSettingsService'; -export * from './DATA_CONTEXT_USER'; -export * from './IAuthCredentials'; -export * from './AuthConfigurationsResource'; -export * from './AuthConfigurationParametersResource'; -export * from './TeamsManagerService'; -export * from './TeamsResource'; -export * from './UserDataService'; -export * from './UserInfoResource'; -export * from './UserMetaParametersResource'; -export * from './UsersResource'; -export * from './TeamMetaParametersResource'; -export * from './AUTH_SETTINGS_GROUP'; -export * from './PasswordPolicyService'; -export * from './TeamRolesResource'; + +export * from './manifest.js'; +export * from './ELMRole.js'; +export * from './AppAuthService.js'; +export * from './AUTH_PROVIDER_LOCAL_ID.js'; +export * from './AuthInfoService.js'; +export * from './AuthProviderService.js'; +export * from './AuthProvidersResource.js'; +export * from './AuthRolesResource.js'; +export * from './AuthSettingsService.js'; +export * from './DATA_CONTEXT_USER.js'; +export * from './IAuthCredentials.js'; +export * from './AuthConfigurationsResource.js'; +export * from './AuthConfigurationParametersResource.js'; +export * from './TeamsManagerService.js'; +export * from './TeamsResource.js'; +export * from './UserDataService.js'; +export * from './UserInfoResource.js'; +export * from './UserMetaParametersResource.js'; +export * from './UsersMetaParametersResource.js'; +export * from './UsersResource.js'; +export * from './UsersOriginDetailsResource.js'; +export * from './UserInfoMetaParametersResource.js'; +export * from './TeamMetaParametersResource.js'; +export * from './AUTH_SETTINGS_GROUP.js'; +export * from './PasswordPolicyService.js'; +export * from './TeamRolesResource.js'; +export * from './TeamInfoMetaParametersResource.js'; +export * from './ADMIN_USERNAME_MIN_LENGTH.js'; diff --git a/webapp/packages/core-authentication/src/locales/zh.ts b/webapp/packages/core-authentication/src/locales/zh.ts index 859671e833..58ed2efad6 100644 --- a/webapp/packages/core-authentication/src/locales/zh.ts +++ b/webapp/packages/core-authentication/src/locales/zh.ts @@ -1,10 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ export default [ - ['core_authentication_auth_settings_group', 'Authentication'], - ['settings_authentication_disable_anonymous_access_name', 'Disable anonymous access'], - ['settings_authentication_disable_anonymous_access_description', 'Disable anonymous access function'], + ['core_authentication_auth_settings_group', '认证'], + ['settings_authentication_disable_anonymous_access_name', '禁用匿名访问'], + ['settings_authentication_disable_anonymous_access_description', '禁用匿名访问功能'], - ['core_authentication_password_policy_min_length', 'Password must be at least {arg:min} characters long'], - ['core_authentication_password_policy_upper_lower_case', 'Password must contain both upper and lower case letters'], - ['core_authentication_password_policy_min_digits', 'Password must contain at least {arg:min} digits'], - ['core_authentication_password_policy_min_special_characters', 'Password must contain at least {arg:min} special characters'], + ['core_authentication_password_policy_min_length', '密码长度需要至少 {arg:min} 个字符'], + ['core_authentication_password_policy_upper_lower_case', '密码必须包含大写和小写字母'], + ['core_authentication_password_policy_min_digits', '密码至少需要包含 {arg:min} 个数字'], + ['core_authentication_password_policy_min_special_characters', '密码至少需要包含 {arg:min} 个特殊字符'], ]; diff --git a/webapp/packages/core-authentication/src/manifest.ts b/webapp/packages/core-authentication/src/manifest.ts index f5db16b02a..49539690e2 100644 --- a/webapp/packages/core-authentication/src/manifest.ts +++ b/webapp/packages/core-authentication/src/manifest.ts @@ -13,24 +13,28 @@ export const coreAuthenticationManifest: PluginManifest = { }, providers: [ - () => import('./AppAuthService').then(m => m.AppAuthService), - () => import('./AuthConfigurationParametersResource').then(m => m.AuthConfigurationParametersResource), - () => import('./AuthConfigurationsResource').then(m => m.AuthConfigurationsResource), - () => import('./AuthInfoService').then(m => m.AuthInfoService), - () => import('./AuthProviderService').then(m => m.AuthProviderService), - () => import('./AuthProvidersResource').then(m => m.AuthProvidersResource), - () => import('./AuthRolesResource').then(m => m.AuthRolesResource), - () => import('./AuthSettingsService').then(m => m.AuthSettingsService), - () => import('./LocaleService').then(m => m.LocaleService), - () => import('./PasswordPolicyService').then(m => m.PasswordPolicyService), - () => import('./TeamMetaParametersResource').then(m => m.TeamMetaParametersResource), - () => import('./TeamsManagerService').then(m => m.TeamsManagerService), - () => import('./TeamsResource').then(m => m.TeamsResource), - () => import('./TeamRolesResource').then(m => m.TeamRolesResource), - () => import('./UserConfigurationBootstrap').then(m => m.UserConfigurationBootstrap), - () => import('./UserDataService').then(m => m.UserDataService), - () => import('./UserInfoResource').then(m => m.UserInfoResource), - () => import('./UserMetaParametersResource').then(m => m.UserMetaParametersResource), - () => import('./UsersResource').then(m => m.UsersResource), + () => import('./AppAuthService.js').then(m => m.AppAuthService), + () => import('./AuthConfigurationParametersResource.js').then(m => m.AuthConfigurationParametersResource), + () => import('./AuthConfigurationsResource.js').then(m => m.AuthConfigurationsResource), + () => import('./AuthInfoService.js').then(m => m.AuthInfoService), + () => import('./AuthProviderService.js').then(m => m.AuthProviderService), + () => import('./AuthProvidersResource.js').then(m => m.AuthProvidersResource), + () => import('./AuthRolesResource.js').then(m => m.AuthRolesResource), + () => import('./AuthSettingsService.js').then(m => m.AuthSettingsService), + () => import('./LocaleService.js').then(m => m.LocaleService), + () => import('./PasswordPolicyService.js').then(m => m.PasswordPolicyService), + () => import('./TeamMetaParametersResource.js').then(m => m.TeamMetaParametersResource), + () => import('./TeamsManagerService.js').then(m => m.TeamsManagerService), + () => import('./TeamsResource.js').then(m => m.TeamsResource), + () => import('./TeamRolesResource.js').then(m => m.TeamRolesResource), + () => import('./UserConfigurationBootstrap.js').then(m => m.UserConfigurationBootstrap), + () => import('./UserInfoMetaParametersResource.js').then(m => m.UserInfoMetaParametersResource), + () => import('./UserDataService.js').then(m => m.UserDataService), + () => import('./UserInfoResource.js').then(m => m.UserInfoResource), + () => import('./UsersOriginDetailsResource.js').then(m => m.UsersOriginDetailsResource), + () => import('./UserMetaParametersResource.js').then(m => m.UserMetaParametersResource), + () => import('./UsersMetaParametersResource.js').then(m => m.UsersMetaParametersResource), + () => import('./TeamInfoMetaParametersResource.js').then(m => m.TeamInfoMetaParametersResource), + () => import('./UsersResource.js').then(m => m.UsersResource), ], }; diff --git a/webapp/packages/core-blocks/package.json b/webapp/packages/core-blocks/package.json index 6b589ea587..4448edf506 100644 --- a/webapp/packages/core-blocks/package.json +++ b/webapp/packages/core-blocks/package.json @@ -1,5 +1,6 @@ { "name": "@cloudbeaver/core-blocks", + "type": "module", "sideEffects": [ "src/**/*.css", "src/**/*.scss", @@ -33,6 +34,7 @@ "mobx": "^6", "mobx-react-lite": "^4", "react": "^18", + "react-hotkeys-hook": "^4", "reakit": "^1", "reakit-utils": "^0" }, @@ -48,12 +50,12 @@ "@cloudbeaver/core-utils": "^0", "@cloudbeaver/tests-runner": "^0", "@jest/globals": "^29", - "@testing-library/jest-dom": "^6", "@testing-library/react": "^16", + "@types/jest": "^29", "@types/react": "^18", "mobx": "^6", "react": "^18", "typescript": "^5", "typescript-plugin-css-modules": "^5" } -} \ No newline at end of file +} diff --git a/webapp/packages/core-blocks/src/ActionIconButton.tsx b/webapp/packages/core-blocks/src/ActionIconButton.tsx index 59f879667d..32e44520ad 100644 --- a/webapp/packages/core-blocks/src/ActionIconButton.tsx +++ b/webapp/packages/core-blocks/src/ActionIconButton.tsx @@ -8,9 +8,9 @@ import { observer } from 'mobx-react-lite'; import style from './ActionIconButton.module.css'; -import { IconButton, type IconButtonProps } from './IconButton'; -import { s } from './s'; -import { useS } from './useS'; +import { IconButton, type IconButtonProps } from './IconButton.js'; +import { s } from './s.js'; +import { useS } from './useS.js'; export interface ActionIconButtonProps extends IconButtonProps { primary?: boolean; diff --git a/webapp/packages/core-blocks/src/AppRefreshButton.tsx b/webapp/packages/core-blocks/src/AppRefreshButton.tsx index 351700b821..7ca985a0ce 100644 --- a/webapp/packages/core-blocks/src/AppRefreshButton.tsx +++ b/webapp/packages/core-blocks/src/AppRefreshButton.tsx @@ -24,7 +24,7 @@ export const AppRefreshButton: React.FC = function AppRefreshButton({ cl } return ( - ); diff --git a/webapp/packages/core-blocks/src/BlocksLocaleService.ts b/webapp/packages/core-blocks/src/BlocksLocaleService.ts index 74224944b2..9026444e25 100644 --- a/webapp/packages/core-blocks/src/BlocksLocaleService.ts +++ b/webapp/packages/core-blocks/src/BlocksLocaleService.ts @@ -14,22 +14,22 @@ export class BlocksLocaleService extends Bootstrap { super(); } - register(): void | Promise { + override register(): void { this.localizationService.addProvider(this.provider.bind(this)); } - load(): void | Promise {} - private async provider(locale: string) { switch (locale) { case 'ru': - return (await import('./locales/ru')).default; + return (await import('./locales/ru.js')).default; case 'it': - return (await import('./locales/it')).default; + return (await import('./locales/it.js')).default; + case 'zh': + return (await import('./locales/zh.js')).default; case 'fr': - return (await import('./locales/fr')).default; + return (await import('./locales/fr.js')).default; default: - return (await import('./locales/en')).default; + return (await import('./locales/en.js')).default; } } } diff --git a/webapp/packages/core-blocks/src/Button.tsx b/webapp/packages/core-blocks/src/Button.tsx index e96edc9143..5ba253b03b 100644 --- a/webapp/packages/core-blocks/src/Button.tsx +++ b/webapp/packages/core-blocks/src/Button.tsx @@ -9,12 +9,12 @@ import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import style from './Button.module.css'; -import { IconOrImage } from './IconOrImage'; -import { Loader } from './Loader/Loader'; -import { s } from './s'; -import { useObjectRef } from './useObjectRef'; -import { useObservableRef } from './useObservableRef'; -import { useS } from './useS'; +import { IconOrImage } from './IconOrImage.js'; +import { Loader } from './Loader/Loader.js'; +import { s } from './s.js'; +import { useObjectRef } from './useObjectRef.js'; +import { useObservableRef } from './useObservableRef.js'; +import { useS } from './useS.js'; type ButtonMod = Array<'raised' | 'unelevated' | 'outlined' | 'secondary'>; @@ -72,12 +72,6 @@ export const Button = observer(function Button({ ['click'], ); - function handleEnter(event: React.KeyboardEvent) { - if (event.key === 'Enter') { - event.currentTarget.click(); - } - } - loading = state.loading || loading; if (loading) { @@ -89,7 +83,6 @@ export const Button = observer(function Button({
)} {this.canRefresh && ( -
+
@@ -109,12 +109,12 @@ export class ErrorBoundary extends React.ComponentLoading...}> {onClose && ( -
+
)} {this.canRefresh && ( -
+
)} diff --git a/webapp/packages/core-blocks/src/ErrorDetailsDialog/ErrorDetailsDialog.tsx b/webapp/packages/core-blocks/src/ErrorDetailsDialog/ErrorDetailsDialog.tsx index 955d089531..51da956639 100644 --- a/webapp/packages/core-blocks/src/ErrorDetailsDialog/ErrorDetailsDialog.tsx +++ b/webapp/packages/core-blocks/src/ErrorDetailsDialog/ErrorDetailsDialog.tsx @@ -10,27 +10,28 @@ import { useCallback, useMemo } from 'react'; import type { DialogComponent } from '@cloudbeaver/core-dialogs'; -import { Button } from '../Button'; -import { CommonDialogBody } from '../CommonDialog/CommonDialog/CommonDialogBody'; -import { CommonDialogFooter } from '../CommonDialog/CommonDialog/CommonDialogFooter'; -import { CommonDialogHeader } from '../CommonDialog/CommonDialog/CommonDialogHeader'; -import { CommonDialogWrapper } from '../CommonDialog/CommonDialog/CommonDialogWrapper'; -import { Textarea } from '../FormControls/Textarea'; -import { Iframe } from '../Iframe'; -import { useTranslate } from '../localization/useTranslate'; -import { s } from '../s'; -import { useClipboard } from '../useClipboard'; -import { useS } from '../useS'; +import { Button } from '../Button.js'; +import { CommonDialogBody } from '../CommonDialog/CommonDialog/CommonDialogBody.js'; +import { CommonDialogFooter } from '../CommonDialog/CommonDialog/CommonDialogFooter.js'; +import { CommonDialogHeader } from '../CommonDialog/CommonDialog/CommonDialogHeader.js'; +import { CommonDialogWrapper } from '../CommonDialog/CommonDialog/CommonDialogWrapper.js'; +import { Textarea } from '../FormControls/Textarea.js'; +import { Iframe } from '../Iframe.js'; +import { useTranslate } from '../localization/useTranslate.js'; +import { s } from '../s.js'; +import { useClipboard } from '../useClipboard.js'; +import { useS } from '../useS.js'; import style from './ErrorDetailsDialog.module.css'; -import { ErrorModel, IErrorInfo } from './ErrorModel'; +import { ErrorModel, type IErrorInfo } from './ErrorModel.js'; function DisplayErrorInfo({ error }: { error: IErrorInfo }) { const styles = useS(style); + const translate = useTranslate(); return ( <>
- {error.isHtml ?