diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 987763d5a6..59c47d6d1f 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 - - - name: Archive build artifacts - uses: actions/upload-artifact@v4 - with: - name: backend-build-artifacts - path: deploy/cloudbeaver - if-no-files-found: error + working-directory: ./cloudbeaver/deploy + + # - name: Archive build artifacts + # uses: actions/upload-artifact@v4 + # with: + # name: backend-build-artifacts + # 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 17bea9c8f1..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..4ee591ae46 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,34 +59,18 @@ jobs: - name: yarn clean if: env.skip_cache == 'true' - uses: borales/actions-yarn@v5 - with: - dir: webapp - cmd: clean + run: yarn clear - - name: yarn install --frozen-lockfile - uses: borales/actions-yarn@v5 - with: - dir: webapp - cmd: install + - run: yarn install --immutable - - name: build - id: build - uses: borales/actions-yarn@v5 - with: - dir: webapp/packages/product-default - cmd: bundle + - 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 - with: - name: frontend-build-artifacts - path: webapp/packages/product-default/lib - if-no-files-found: error + # - name: Archive build artifacts + # uses: actions/upload-artifact@v4 + # with: + # name: frontend-build-artifacts + # path: webapp/packages/product-default/lib + # if-no-files-found: error 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/launch.json b/.vscode/launch.json index 69ae494895..7b5160c2f0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,11 +4,11 @@ { "type": "chrome", "request": "launch", - "name": "Chrome", + "name": "CloudBeaver CE", "url": "http://localhost:8080", "webRoot": "${workspaceFolder}/..", "outFiles": [ - "${workspaceFolder}/../cloudbeaver/packages/**/dist/**/*.{js,jsx}" + "${workspaceFolder}/../cloudbeaver/webapp/packages/**/dist/**/*.{js,jsx}" ], "smartStep": true, "sourceMaps": true, @@ -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 2149dc9704..be604f1cdb 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,39 @@ You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Changelog +### 24.2.5. 2024-11-18 +- Updated user storage mechanism: New user logins are now stored in lowercase to prevent duplicate entries (e.g., "ADMIN" and "admin"). Note: This update does not affect previously created user logins; +- A new setting in Global Preferences was added to restrict data import for non-admin users. + +### 24.2.4. 2024-11-04 +- General: + - Data export: Added the ability to export JSON values as embedded JSON; + - Brazilian Portuguese localization was enhanced (thanks to [brlarini](https://github.com/brlarini)); + - Fixed a proxy issue that excluded the Content-Type header in responses. +- Administration: + - Refreshed design for the User and Teams tab in the Administration panel; + - Added an ability to change a user password even if the user is disabled in a system. +### 24.2.3. 2024-10-21 +- Important update: + - Connections Templates feature is declared as obsolete and will be removed in future releases. +- General: + - Data editor enhancements: Rows with focused cells are specially marked to make it easier to locate a position in large tables; + - DB2i driver has been updated to version 20.0.7; + - The URL mode for PostgreSQL now supports connecting to multiple databases; + - The issue with displaying BLOB data types in DuckDB has been resolved. + +### 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.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.2.0. 2024-09-02 ### Changes since 24.1.0: - General: 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 96% rename from config/sample-databases/DefaultConfiguration/cloudbeaver.conf rename to config/core/cloudbeaver.conf index e063bf739b..9ebad0078f 100644 --- a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf +++ b/config/core/cloudbeaver.conf @@ -1,6 +1,6 @@ { server: { - serverPort: "${CLOUDBEAVER_SERVICE_PORT:8978}", + serverPort: "${CLOUDBEAVER_WEB_SERVER_PORT:8978}", workspaceLocation: "${CLOUDBEAVER_WORKSPACE_LOCATION:workspace}", contentRoot: "web", @@ -21,9 +21,7 @@ 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 }, 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 3c96c0389e..0000000000 --- a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf +++ /dev/null @@ -1,101 +0,0 @@ -{ - server: { - serverPort: "${CLOUDBEAVER_SERVICE_PORT:8978}", - - workspaceLocation: "${CLOUDBEAVER_WORKSPACE_LOCATION:workspace}", - contentRoot: "web", - driversLocation: "drivers", - - rootURI: "${CLOUDBEAVER_ROOT_URI:/}", - 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: "${CLOUDBEAVER_EXPIRE_SESSION_AFTER_PERIOD:1800000}", - - develMode: "${CLOUDBEAVER_DEVEL_MODE: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: "${CLOUDBEAVER_APP_ANONYMOUS_ACCESS_ENABLED:true}", - anonymousUserRole: 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: "${CLOUDBEAVER_APP_FORWARD_PROXY:false}", - - publicCredentialsSaveEnabled: "${CLOUDBEAVER_APP_PUBLIC_CREDENTIALS_SAVE_ENABLED:true}", - adminCredentialsSaveEnabled: "${CLOUDBEAVER_APP_ADMIN_CREDENTIALS_SAVE_ENABLED:true}", - - resourceManagerEnabled: "${CLOUDBEAVER_APP_RESOURCE_MANAGER_ENABLED:true}", - - resourceQuotas: { - 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" - ], - - 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..ba401ce61f 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 @@ -24,6 +18,7 @@ cd ../.. echo "Pull dbeaver platform" [ ! -d dbeaver ] && git clone --depth 1 https://github.com/dbeaver/dbeaver.git [ ! -d dbeaver-common ] && git clone --depth 1 https://github.com/dbeaver/dbeaver-common.git +[ ! -d dbeaver-jdbc-libsql ] && git clone --depth 1 https://github.com/dbeaver/dbeaver-jdbc-libsql.git cd cloudbeaver/deploy @@ -43,20 +38,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..940c64f6da 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 @@ -25,6 +20,8 @@ echo Pull dbeaver platform IF NOT EXIST dbeaver git clone https://github.com/dbeaver/dbeaver.git IF NOT EXIST dbeaver-common git clone https://github.com/dbeaver/dbeaver-common.git +IF NOT EXIST dbeaver-jdbc-libsql git clone https://github.com/dbeaver/dbeaver-jdbc-libsql.git + cd cloudbeaver\deploy echo Build cloudbeaver server @@ -40,32 +37,41 @@ 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 -echo Build static content +echo "Build static content" -cd ..\ +mkdir .\cloudbeaver\web -cd ..\cloudbeaver\webapp +cd ..\webapp call yarn -call yarn lerna bootstrap -call yarn lerna run bundle --no-bail --stream --scope=@cloudbeaver/product-default &::-- -- --env source-map +cd .\packages\product-default +call yarn run bundle + +if %ERRORLEVEL% neq 0 ( + echo 'Application build failed' + exit /b %ERRORLEVEL% +) + +cd ..\..\ +call yarn test + +if %ERRORLEVEL% neq 0 ( + echo 'Frontend tests failed' + exit /b %ERRORLEVEL% +) cd ..\deploy -echo Copy static content +echo "Copy static content" xcopy /E /Q ..\webapp\packages\product-default\lib cloudbeaver\web >NUL -echo Cloudbeaver is ready. Run run-server.bat in cloudbeaver folder to start the server. +echo "Cloudbeaver is ready. Run run-server.bat in cloudbeaver folder to start the server." pause 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/base-java/Dockerfile b/deploy/docker/base-java/Dockerfile index b59a3fc0b9..c0827d7811 100644 --- a/deploy/docker/base-java/Dockerfile +++ b/deploy/docker/base-java/Dockerfile @@ -23,8 +23,7 @@ RUN set -eux; \ tzdata \ # locales ensures proper character encoding and locale-specific behaviors using en_US.UTF-8 locales \ - nano \ - ; \ + nano && \ echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \ locale-gen en_US.UTF-8; \ rm -rf /var/lib/apt/lists/* diff --git a/deploy/docker/cloudbeaver-ce/Dockerfile b/deploy/docker/cloudbeaver-ce/Dockerfile index 95bdcc6812..18a1d20051 100644 --- a/deploy/docker/cloudbeaver-ce/Dockerfile +++ b/deploy/docker/cloudbeaver-ce/Dockerfile @@ -2,12 +2,24 @@ FROM dbeaver/base-java MAINTAINER DBeaver Corp, devops@dbeaver.com -RUN apt-get update; \ - apt-get upgrade -y; - +ENV DBEAVER_GID=8978 +ENV DBEAVER_UID=8978 + +RUN apt-get update && \ + apt-get upgrade -y + +RUN groupadd -g $DBEAVER_GID dbeaver && \ + useradd -g $DBEAVER_GID -M -u $DBEAVER_UID -s /bin/bash dbeaver + COPY cloudbeaver /opt/cloudbeaver +COPY scripts/launch-product.sh /opt/cloudbeaver/launch-product.sh + +RUN chown -R $DBEAVER_UID:$DBEAVER_GID /opt/cloudbeaver EXPOSE 8978 RUN find /opt/cloudbeaver -type d -exec chmod 775 {} \; WORKDIR /opt/cloudbeaver/ -ENTRYPOINT ["./run-server.sh"] + +RUN chmod +x "run-server.sh" "/opt/cloudbeaver/launch-product.sh" + +ENTRYPOINT ["./launch-product.sh"] diff --git a/deploy/scripts/launch-product.sh b/deploy/scripts/launch-product.sh new file mode 100644 index 0000000000..605a4d7a7a --- /dev/null +++ b/deploy/scripts/launch-product.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# This script is needed to change ownership and run the application as user dbeaver during the upgrade from version 24.2.0 + +# Change ownership of the WORKDIR to the dbeaver user and group +# Variables DBEAVER_ are defined in the Dockerfile and exported to the runtime environment +# PWD equals WORKDIR value from product Dockerfile +chown -R $DBEAVER_UID:$DBEAVER_GID $PWD + +# Execute run-server.sh as the dbeaver user with the JAVA_HOME and PATH environment variables +exec su dbeaver -c "JAVA_HOME=$JAVA_HOME PATH=$PATH ./run-server.sh" \ No newline at end of file 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 423b98ad5f..1681d2bbb2 100644 --- a/osgi-app.properties +++ b/osgi-app.properties @@ -30,4 +30,6 @@ testBundlePaths=\ additionalModuleRoots=\ opt; optionalFeatureRepositories=\ - dbeaver/product/repositories \ No newline at end of file + dbeaver/product/repositories +excludeOutputs=\ + cloudbeaver/webapp/node_modules \ No newline at end of file diff --git a/project.deps b/project.deps index a3cf985c1c..2c6c189ff9 100644 --- a/project.deps +++ b/project.deps @@ -1,2 +1,3 @@ dbeaver-common +dbeaver-jdbc-libsql dbeaver \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index 8cd9ac3ac9..0854e65840 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -3,14 +3,13 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Model Bundle-SymbolicName: io.cloudbeaver.model;singleton:=true -Bundle-Version: 1.0.62.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.67.qualifier +Bundle-Release-Date: 20241223 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport, org.jkiss.dbeaver.model;visibility:=reexport, - org.jkiss.dbeaver.model.rcp;visibility:=reexport, org.jkiss.dbeaver.model.sm;visibility:=reexport, org.jkiss.dbeaver.model.event;visibility:=reexport, org.jkiss.dbeaver.model.nio;visibility:=reexport, diff --git a/server/bundles/io.cloudbeaver.model/plugin.xml b/server/bundles/io.cloudbeaver.model/plugin.xml index 9b42539f54..f503391bb6 100644 --- a/server/bundles/io.cloudbeaver.model/plugin.xml +++ b/server/bundles/io.cloudbeaver.model/plugin.xml @@ -4,6 +4,7 @@ + diff --git a/server/bundles/io.cloudbeaver.model/pom.xml b/server/bundles/io.cloudbeaver.model/pom.xml index 421eb0e899..d05a18ffbd 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.62-SNAPSHOT + 1.0.67-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.server.feature.exsd b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.server.feature.exsd new file mode 100644 index 0000000000..93c93336c8 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.server.feature.exsd @@ -0,0 +1,34 @@ + + + + + + + + Web feature + + + + + + + + + + + + + + + + + + + + Web service description + + + + + + 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 d5c4690cd6..ddbd377c4e 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java @@ -23,7 +23,6 @@ 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.utils.CommonUtils; import org.jkiss.utils.Pair; @@ -38,7 +37,6 @@ public abstract class BaseWebProjectImpl extends BaseProjectImpl implements RMCo @NotNull private final Path path; @NotNull - protected final DataSourceFilter dataSourceFilter; private final RMController resourceController; public BaseWebProjectImpl( @@ -46,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 @@ -104,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/DBWebException.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java index f4cc8e50ea..a06626ed51 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/DBWebException.java @@ -22,7 +22,6 @@ import graphql.language.SourceLocation; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.sql.SQLState; -import org.jkiss.dbeaver.utils.GeneralUtils; import org.jkiss.utils.CommonUtils; import java.io.PrintWriter; @@ -100,7 +99,7 @@ public ErrorClassification getErrorType() { @Override public Map getExtensions() { StringWriter buf = new StringWriter(); - GeneralUtils.getRootCause(this).printStackTrace(new PrintWriter(buf, true)); + CommonUtils.getRootCause(this).printStackTrace(new PrintWriter(buf, true)); Map extensions = new LinkedHashMap<>(); String stString = buf.toString(); 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 8cce6d1332..c6c7144198 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java @@ -30,19 +30,22 @@ 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; } @@ -82,8 +85,13 @@ public DBTTaskManager getTaskManager() { 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/model/WebConnectionInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java index 9c5e42c945..c41c22e7fb 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 @@ -42,6 +42,8 @@ import org.jkiss.dbeaver.model.rm.RMConstants; import org.jkiss.dbeaver.model.rm.RMProjectPermission; import org.jkiss.dbeaver.model.runtime.DBRRunnableParametrized; +import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor; +import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.CommonUtils; @@ -356,8 +358,16 @@ public WebPropertyInfo[] getAuthProperties() { @Property public List getNetworkHandlersConfig() { - return dataSourceContainer.getConnectionConfiguration().getHandlers().stream() - .map(WebNetworkHandlerConfig::new).collect(Collectors.toList()); + var registry = NetworkHandlerRegistry.getInstance(); + return dataSourceContainer.getConnectionConfiguration() + .getHandlers() + .stream() + .filter(handlerConf -> { + NetworkHandlerDescriptor descriptor = registry.getDescriptor(handlerConf.getId()); + return descriptor != null && !descriptor.isDesktopHandler(); + }) + .map(WebNetworkHandlerConfig::new) + .collect(Collectors.toList()); } @Property @@ -445,10 +455,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/BaseServerConfigurationController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java index 516ef29e2a..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 @@ -19,19 +19,86 @@ 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 { + 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"); + } @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 0517fef9b1..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,11 +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 org.eclipse.core.runtime.Platform; import org.eclipse.equinox.app.IApplicationContext; import org.jkiss.code.NotNull; @@ -36,7 +32,6 @@ 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.runtime.IVariableResolver; @@ -160,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. @@ -251,6 +233,12 @@ public String getWorkspaceIdProperty() throws DBException { return BaseWorkspaceImpl.readWorkspaceIdProperty(); } + @Override + public Path getWorkspaceDirectory() { + return getServerConfigurationController().getWorkspacePath(); + } + + public String getApplicationId() { try { return getApplicationInstanceId(); @@ -270,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 c06f137b70..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 @@ -39,6 +39,11 @@ default Map getOriginalConfigurationProperties() { return Map.of(); } + @NotNull + Path getWorkspacePath(); + @NotNull Gson getGson(); + + void validateFinalServerConfiguration() throws DBException; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java index e223ba8df4..ca64c38f36 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java @@ -48,7 +48,7 @@ public static Path getPathFromNode(@NotNull WebSession webSession, @NotNull Stri public static DBNPathBase getNodeByPath(@NotNull WebSession webSession, @NotNull String nodePath) throws DBException { DBRProgressMonitor monitor = webSession.getProgressMonitor(); - DBNModel navigatorModel = webSession.getNavigatorModel(); + DBNModel navigatorModel = webSession.getNavigatorModelOrThrow(); DBNNode node = navigatorModel.getNodeByPath(monitor, nodePath); if (!(node instanceof DBNPathBase dbnPath)) { throw new DBWebException("Node '" + nodePath + "' is not found in File Systems"); 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 7500c25460..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 @@ -121,9 +121,9 @@ public DBNNode refreshNode(DBRProgressMonitor monitor, Object source) throws DBE return this; } - @NotNull + @Nullable @Override - public DBPProject getOwnerProject() { + public DBPProject getOwnerProjectOrNull() { List globalProjects = getModel().getModelProjects(); if (globalProjects != null) { for (DBPProject modelProject : globalProjects) { @@ -132,7 +132,7 @@ public DBPProject getOwnerProject() { } } } - throw new IllegalStateException("Project '" + project.getId() + "' not found in workspace"); + return null; } @Nullable 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 0fdc50e6e4..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,10 +203,10 @@ public DBPObject getObjectDetails(@NotNull DBRProgressMonitor monitor, @NotNull return resource; } - @NotNull + @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 4718c448c2..44dc11d08b 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,40 +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; @@ -70,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; @@ -86,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<>(); @@ -99,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(); @@ -131,7 +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 InternalWebProjectImpl(sessionContext, rmProject); + project = new InternalWebProjectImpl(sessionContext, rmProject, getProjectPath(projectId)); projectRegistries.put(projectId, project); } return project; @@ -342,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( @@ -625,10 +433,13 @@ 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())); } + if (!Files.exists(newTargetPath.getParent())) { + throw new DBException("Resource %s doesn't exists".formatted(newTargetPath.getParent().getFileName())); + } try { Files.move(oldTargetPath, newTargetPath); } catch (IOException e) { @@ -804,15 +615,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 @@ -857,14 +659,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); @@ -881,7 +675,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"); } @@ -894,7 +688,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)) { @@ -1138,32 +932,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)); } @@ -1284,22 +1052,4 @@ public static boolean isProjectOwner(String projectId, String userId) { rmProjectName.name.equals(userId); } - - private class InternalWebProjectImpl extends BaseWebProjectImpl { - public InternalWebProjectImpl(SessionContextImpl sessionContext, RMProject rmProject) { - super( - LocalResourceController.this.workspace, - LocalResourceController.this, - sessionContext, - rmProject, - (container) -> true); - } - - @NotNull - @Override - protected DBPDataSourceRegistry createDataSourceRegistry() { - return new DataSourceRegistry(this); - } - } - } 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 8d516be802..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 @@ -63,15 +63,21 @@ public BaseWebSession(@NotNull String id, @NotNull WebApplication application) t 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; } 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 42c1d5c122..d4ee22fbc7 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,10 +19,8 @@ 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; @@ -32,7 +30,6 @@ 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 org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IStatus; @@ -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; @@ -106,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<>(); @@ -129,16 +117,11 @@ public WebSession( @NotNull WebAuthApplication application, @NotNull Map sessionHandlers ) throws DBException { - super(requestInfo.getId(), application); - this.lastAccessTime = this.createTime; - setLocale(CommonUtils.toString(requestInfo.getLocale(), this.locale)); - this.sessionHandlers = sessionHandlers; - //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(); + this(requestInfo.getId(), + CommonUtils.toString(requestInfo.getLocale()), + application, + sessionHandlers + ); updateSessionParameters(requestInfo); } @@ -151,7 +134,7 @@ protected WebSession( super(id, application); this.lastAccessTime = this.createTime; this.sessionHandlers = sessionHandlers; - setLocale(locale); + 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, @@ -171,8 +154,8 @@ public SMSessionPrincipal getSessionPrincipal() { } } - @NotNull - public DBPProject getSingletonProject() { + @Nullable + public WebSessionProjectImpl getSingletonProject() { return getWorkspace().getActiveProject(); } @@ -269,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(); @@ -352,7 +272,7 @@ private void initNavigatorModel() { this.navigatorModel.dispose(); this.navigatorModel = null; } - this.connections.clear(); + this.globalProject = null; loadProjects(); @@ -372,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); } @@ -392,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())); - } - - private boolean isDataSourceAccessible(DBPDataSourceContainer dataSource) { - return dataSource.isExternallyProvided() || - dataSource.isTemporary() || - this.hasPermission(DBWConstants.PERMISSION_ADMIN) || - accessibleConnectionIds.contains(dataSource.getId()); + @NotNull + protected Path getProjectPath(@NotNull RMProject project) throws DBException { + return RMUtils.getProjectPath(project); } - @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 { @@ -460,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(); @@ -484,7 +369,9 @@ private synchronized void refreshSessionAuth() { authAsAnonymousUser(); } else if (getUserId() != null) { userContext.refreshPermissions(); - refreshAccessibleConnectionIds(); + if (globalProject != null) { + globalProject.refreshAccessibleConnectionIds(); + } } } catch (Exception e) { @@ -493,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()) { @@ -538,10 +399,18 @@ public void setLocale(@Nullable String locale) { this.locale = locale != null ? locale : Locale.getDefault().getLanguage(); } + @Nullable public DBNModel getNavigatorModel() { return navigatorModel; } + @NotNull + public DBNModel getNavigatorModelOrThrow() throws DBWebException { + if (navigatorModel != null) { + return navigatorModel; + } + throw new DBWebException("Navigator model is not found in session"); + } /** * Returns and clears progress messages */ @@ -583,69 +452,6 @@ public synchronized void updateSessionParameters(WebHttpRequestInfo requestInfo) 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 { @@ -829,7 +635,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; } @@ -845,7 +651,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); @@ -988,9 +794,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 @@ -999,7 +808,7 @@ public boolean provideAuthParameters( InstanceCreator credTypeAdapter = type -> credentials; Gson credGson = new GsonBuilder() - .setLenient() + .setStrictness(Strictness.LENIENT) .registerTypeAdapter(credentials.getClass(), credTypeAdapter) .create(); @@ -1081,12 +890,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); } @@ -1096,18 +910,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(); } @@ -1132,10 +952,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 @@ -1152,6 +968,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) { @@ -1186,15 +1007,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/WebSessionWorkspace.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionWorkspace.java index f24bf8c90a..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,13 +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; @@ -40,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; @@ -83,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; } @@ -106,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; } @@ -139,21 +138,21 @@ public DBPImage getResourceIcon(DBPAdaptable resourceAdapter) { return null; } - public void setActiveProject(DBPProject activeProject) { - this.activeProject = (WebProjectImpl) activeProject; + 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/registry/WebAuthProviderConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderConfiguration.java index 32f7752fd7..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 diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java index 1ae1b6efea..78a87c8fcb 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebAuthProviderDescriptor.java @@ -54,6 +54,8 @@ public class WebAuthProviderDescriptor extends AbstractDescriptor { private final boolean configurable; private final boolean trusted; private final boolean isPrivate; + private final boolean isAuthHidden; + private final boolean isCaseInsensitive; private final String[] requiredFeatures; private final boolean isRequired; private final String[] types; @@ -67,6 +69,8 @@ public WebAuthProviderDescriptor(IConfigurationElement cfg) { this.trusted = CommonUtils.toBoolean(cfg.getAttribute("trusted")); this.isPrivate = CommonUtils.toBoolean(cfg.getAttribute("private")); this.isRequired = CommonUtils.toBoolean(cfg.getAttribute("required")); + this.isAuthHidden = CommonUtils.toBoolean(cfg.getAttribute("authHidden")); + this.isCaseInsensitive = CommonUtils.toBoolean(cfg.getAttribute("caseInsensitive")); for (IConfigurationElement cfgElement : cfg.getChildren("configuration")) { List properties = WebAuthProviderRegistry.readProperties(cfgElement); @@ -126,6 +130,14 @@ public boolean isRequired() { return isRequired; } + public boolean isAuthHidden() { + return isAuthHidden; + } + + public boolean isCaseInsensitive() { + return isCaseInsensitive; + } + public List getConfigurationParameters() { return new ArrayList<>(configurationParameters.values()); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureDescriptor.java new file mode 100644 index 0000000000..6e04d75767 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureDescriptor.java @@ -0,0 +1,72 @@ +/* + * 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.registry; + +import io.cloudbeaver.DBWFeatureSet; +import io.cloudbeaver.utils.WebAppUtils; +import org.eclipse.core.runtime.IConfigurationElement; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBPImage; +import org.jkiss.dbeaver.model.impl.AbstractContextDescriptor; + +/** + * WebFeatureDescriptor + */ +public class WebServerFeatureDescriptor extends AbstractContextDescriptor implements DBWFeatureSet { + + public static final String EXTENSION_ID = "io.cloudbeaver.server.feature"; //$NON-NLS-1$ + + private final String id; + private final String label; + private final String description; + private final DBPImage icon; + + public WebServerFeatureDescriptor(IConfigurationElement config) + { + super(config); + this.id = config.getAttribute("id"); + this.label = config.getAttribute("label"); + this.description = config.getAttribute("description"); + this.icon = iconToImage(config.getAttribute("icon")); + } + + @NotNull + public String getId() { + return id; + } + + @NotNull + public String getLabel() { + return label; + } + + public String getDescription() { + return description; + } + + @Override + public DBPImage getIcon() { + return icon; + } + + @Override + public boolean isEnabled() { + return true; + } + +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureRegistry.java new file mode 100644 index 0000000000..32a8c7a59c --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebServerFeatureRegistry.java @@ -0,0 +1,68 @@ +/* + * 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.registry; + +import io.cloudbeaver.DBWFeatureSet; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.Platform; +import org.jkiss.dbeaver.Log; + +import java.util.ArrayList; +import java.util.List; + +public class WebServerFeatureRegistry { + + private static final Log log = Log.getLog(WebServerFeatureRegistry.class); + + private static final String TAG_FEATURE = "feature"; //$NON-NLS-1$ + + private static WebServerFeatureRegistry instance = null; + + public synchronized static WebServerFeatureRegistry getInstance() { + if (instance == null) { + instance = new WebServerFeatureRegistry(); + instance.loadExtensions(Platform.getExtensionRegistry()); + } + return instance; + } + + private String[] serverFeatures = new String[0]; + + private WebServerFeatureRegistry() { + } + + private synchronized void loadExtensions(IExtensionRegistry registry) { + IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(WebServerFeatureDescriptor.EXTENSION_ID); + List features = new ArrayList<>(); + for (IConfigurationElement ext : extConfigs) { + if (TAG_FEATURE.equals(ext.getName())) { + features.add( + new WebServerFeatureDescriptor(ext)); + } + } + this.serverFeatures = features + .stream() + .map(DBWFeatureSet::getId) + .toArray(String[]::new); + } + + public String[] getServerFeatures() { + return serverFeatures; + } + +} 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 index ca5d26fd6a..a2ae8040a2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java @@ -17,6 +17,7 @@ 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; @@ -33,6 +34,8 @@ 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; @@ -40,7 +43,7 @@ public abstract class BaseGQLPlatform extends BasePlatformImpl { private static final Log log = Log.getLog(BaseGQLPlatform.class); - public static final String WORK_DATA_FOLDER_NAME = ".work-data"; + public static final String BASE_TEMP_DIR = "dbeaver"; private Path tempFolder; @@ -55,7 +58,7 @@ protected synchronized void initialize() { SecurityProviderUtils.registerSecurityProvider(); // Register properties adapter - this.workspace = new WebGlobalWorkspace(this); + this.workspace = new WebGlobalWorkspace(this, (WebApplication) getApplication()); this.workspace.initializeProjects(); QMUtils.initApplication(this); @@ -92,16 +95,12 @@ public DBPWorkspace getWorkspace() { @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(DBWConstants.WORK_DATA_FOLDER_NAME); - } - if (!Files.exists(tempFolder)) { - try { - Files.createDirectories(tempFolder); - } catch (IOException e) { - log.error("Can't create temp directory " + tempFolder, e); + synchronized (this) { + if (tempFolder == null) { + initTempFolder(monitor); + } } } Path folder = tempFolder.resolve(name); @@ -115,6 +114,21 @@ public Path getTempFolder(@NotNull DBRProgressMonitor monitor, @NotNull String n 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(); 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 index 660f4e9548..6cf1faec56 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalProject.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalProject.java @@ -34,15 +34,15 @@ public class WebGlobalProject extends BaseProjectImpl { private static final Log log = Log.getLog(WebGlobalProject.class); - private final String projectId; + private final String projectName; public WebGlobalProject( @NotNull DBPWorkspace workspace, @Nullable SMSessionContext sessionContext, - @NotNull String projectId + @NotNull String projectName ) { super(workspace, sessionContext); - this.projectId = projectId; + this.projectName = projectName; } @Override @@ -53,13 +53,13 @@ public boolean isVirtual() { @NotNull @Override public String getName() { - return projectId; + return projectName; } @NotNull @Override public Path getAbsolutePath() { - return getWorkspace().getAbsolutePath().resolve(projectId); + return getWorkspace().getAbsolutePath().resolve(projectName); } @Override 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 02a3b4e53d..91ac528f11 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 @@ -17,8 +17,8 @@ package io.cloudbeaver.server; import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.utils.WebAppUtils; -import org.eclipse.core.runtime.Platform; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; @@ -26,10 +26,9 @@ 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.net.URI; -import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; @@ -47,18 +46,14 @@ public class WebGlobalWorkspace extends BaseWorkspaceImpl { protected final Map projects = new LinkedHashMap<>(); private WebGlobalProject globalProject; - public WebGlobalWorkspace(DBPPlatform platform) { - super(platform, Path.of(getWorkspaceURI())); - } + private final WebApplication application; - @NotNull - private static URI getWorkspaceURI() { - String workspacePath = Platform.getInstanceLocation().getURL().toString(); - try { - return new URI(workspacePath); - } catch (URISyntaxException e) { - throw new IllegalStateException("Workspace path is invalid: " + workspacePath, e); - } + public WebGlobalWorkspace( + @NotNull DBPPlatform platform, + @NotNull WebApplication application + ) { + super(platform, application.getWorkspaceDirectory()); + this.application = application; } @Override @@ -66,19 +61,23 @@ 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); + String defaultProjectName = WebAppUtils.getWebApplication().getDefaultProjectName(); + if (CommonUtils.isNotEmpty(defaultProjectName)) { + Path globalProjectPath = getAbsolutePath().resolve(defaultProjectName); + if (!Files.exists(globalProjectPath)) { + try { + Files.createDirectories(globalProjectPath); + } catch (IOException e) { + log.error("Error creating global project path: " + globalProject, e); + } } } globalProject = new WebGlobalProject( this, getAuthContext(), - WebAppUtils.getGlobalProjectId()); + CommonUtils.notEmpty(defaultProjectName) + ); activeProject = globalProject; } @@ -103,7 +102,7 @@ public List getProjects() { @Nullable @Override public BaseProjectImpl getProject(@NotNull String projectName) { - if (globalProject.getId().equals(projectName)) { + if (globalProject != null && globalProject.getId().equals(projectName)) { return globalProject; } return null; 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 93% 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 a40bf64584..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; } @@ -188,7 +185,7 @@ public void save() throws IOException { } 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/utils/WebAppUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java index 607e078071..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); @@ -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 0848b7206b..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,10 +18,9 @@ import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWebException; -import io.cloudbeaver.WebProjectImpl; +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; @@ -38,6 +37,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; public class WebDataSourceUtils { @@ -114,18 +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)) { - WebProjectImpl project = webSession.getProjectById(projectId); + 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) || application.isConfigurationMode())) { + 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; @@ -162,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 f10b82c773..c737550521 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.2.2.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 24.3.1.qualifier +Bundle-Release-Date: 20241223 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 74fb225380..e0d96ac6fc 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.2.2-SNAPSHOT + 24.3.1-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 1a26e7e53e..0c9be71ac0 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.107.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.112.qualifier +Bundle-Release-Date: 20241223 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/plugin.xml b/server/bundles/io.cloudbeaver.resources.drivers.base/plugin.xml index cbd9c57a41..dab5530bc6 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/plugin.xml +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/plugin.xml @@ -54,6 +54,7 @@ + diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml index 8831414e5b..a8c4c28456 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.107-SNAPSHOT + 1.0.112-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 9f073c8022..2c3f7c58fb 100644 --- a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Server Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true -Bundle-Version: 24.2.2.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 24.3.1.qualifier +Bundle-Release-Date: 20241223 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index 73656b40b7..9259eddd80 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -75,4 +75,12 @@ + + + + + + diff --git a/server/bundles/io.cloudbeaver.server/pom.xml b/server/bundles/io.cloudbeaver.server/pom.xml index 1b33a40193..c48c797b5e 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.2.2-SNAPSHOT + 24.3.1-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server/schema/schema.graphqls b/server/bundles/io.cloudbeaver.server/schema/schema.graphqls index 6f8f155a82..191dc13f01 100644 --- a/server/bundles/io.cloudbeaver.server/schema/schema.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/schema.graphqls @@ -2,6 +2,7 @@ scalar Object # Date/Time scalar DateTime +scalar Date input PageInput { limit: Int diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index e89cd4bbfa..cd50318ad2 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -118,6 +118,7 @@ type ProductInfo { licenseInfo: String latestVersionInfo: String + productPurchaseURL: String } type ServerConfig { @@ -156,6 +157,7 @@ type ServerConfig { enabledFeatures: [ID!]! disabledBetaFeatures: [ID!] @since(version: "24.0.5") + serverFeatures: [ID!] @since(version: "24.3.0") enabledAuthProviders: [ID!]! supportedLanguages: [ ServerLanguage! ]! services: [ WebServiceConfig ] @@ -514,18 +516,21 @@ input ConnectionConfig { # Host, port, serverName, databaseName are also stored in mainPropertyValues for custom pages mainPropertyValues: Object @since(version: "24.1.2") - # Connection url jdbc:{driver}://{host}[:{port}]/[{database}] + # Return connection URL jdbc:{driver}://{host}[:{port}]/[{database}] url: String - # Properties + + # Return properties list properties: Object - # Keep-Alive interval + # Return keep-alive interval keepAliveInterval: Int + + # Return auto-commit connection state autocommit: Boolean - # Template connection + # Return template connection state template: Boolean - # Read-onyl connection + # Return read-only connection state readOnly: Boolean # User credentials @@ -536,12 +541,10 @@ input ConnectionConfig { selectedSecretId: ID @since(version: "23.3.5") credentials: Object - # Map of provider properties (name/value) - + # Return map of provider properties (name/value) providerProperties: Object - # Network handlers. Map of id->property map (name/value). - + # Return network handlers configuration. Map of id->property map (name/value). networkHandlersConfig: [NetworkHandlerConfigInput!] #### deprecated fields @@ -565,15 +568,16 @@ input ConnectionConfig { #################################################### extend type Query { - # Returns server config + # Return server config serverConfig: ServerConfig! + # Return product settings productSettings: ProductSettings! @since(version: "24.0.1") - # Returns session state ( initialize if not ) + # Return session state ( initialize if not ) sessionState: SessionInfo! - # Session permissions + # Return session permissions sessionPermissions: [ID]! # Get driver info @@ -581,9 +585,10 @@ extend type Query { authModels: [DatabaseAuthModel!]! networkHandlers: [NetworkHandlerDescriptor!]! - # List of user connections. + # Return list of user connections userConnections( projectId: ID, id: ID, projectIds: [ID!] ): [ ConnectionInfo! ]! - # List of template connections. + + # Return list of template connections by project ID templateConnections( projectId: ID ): [ ConnectionInfo! ]! # List of connection folders @@ -613,21 +618,25 @@ extend type Mutation { # Refresh session connection list refreshSessionConnections: Boolean - # Refreshes session on server and returns its state + # Change session language to specified changeSessionLanguage(locale: String): Boolean - # Create new custom connection. Custom connections exist only within the current session. + # Create new custom connection createConnection( config: ConnectionConfig!, projectId: ID ): ConnectionInfo! + # Update specified connection updateConnection( config: ConnectionConfig!, projectId: ID ): ConnectionInfo! + # Delete specified connection deleteConnection( id: ID!, projectId: ID ): Boolean! + # Create new custom connection from template createConnectionFromTemplate( templateId: ID!, projectId: ID!, connectionName: String ): ConnectionInfo! - # Create new folder + # Create new folder for connections createConnectionFolder(parentFolderPath: ID, folderName: String!, projectId: ID ): ConnectionFolderInfo! + # Delete specified connection folder deleteConnectionFolder( folderPath: ID!, projectId: ID ): Boolean! # Copies connection configuration from node @@ -636,7 +645,7 @@ extend type Mutation { # Test connection configuration. Returns remote server version testConnection( config: ConnectionConfig!, projectId: ID): ConnectionInfo! - # Test connection configuration. Returns remote server version + # Test network handler testNetworkHandler( config: NetworkHandlerConfigInput! ): NetworkEndpointInfo! # Initiate existing connection @@ -646,7 +655,7 @@ extend type Mutation { # Disconnect from database closeConnection( id: ID!, projectId: ID ): ConnectionInfo! - # Changes navigator settings for connection + # Change navigator settings for connection setConnectionNavigatorSettings( id: ID!, projectId: ID, settings: NavigatorSettingsInput!): ConnectionInfo! #### Generic async functions diff --git a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls index 9230aa515b..1e5b7f60ed 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls @@ -57,7 +57,7 @@ type NavigatorNodeInfo { # Node ID - generally a full path to the node from root of tree id: ID! # Node URI - a unique path to a node including all parent nodes - # uri: ID! @since(version: "23.3.1") + uri: ID! @since(version: "23.3.1") # Node human readable name name: String #Node full name @@ -135,7 +135,7 @@ extend type Query { navNodeInfo( nodePath: ID! ): NavigatorNodeInfo! - navRefreshNode( nodePath: ID! ): Boolean + navRefreshNode( nodePath: ID! ): Boolean @deprecated # Use navReloadNode method from Mutation (24.2.4) # contextId currently not using navGetStructContainers( projectId: ID, connectionId: ID!, contextId: ID, catalog: ID ): DatabaseStructContainers! @@ -144,6 +144,8 @@ extend type Query { extend type Mutation { + navReloadNode( nodePath: ID! ): NavigatorNodeInfo! + # Rename node and returns new node name navRenameNode( nodePath: ID!, newName: String! ): String diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index db942decd4..a1d12c3dd5 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -86,6 +86,7 @@ type SQLResultColumn { precision: Int required: Boolean! + autoGenerated: Boolean! readOnly: Boolean! readOnlyStatus: String 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 4c60dd58d8..2d44b8ccde 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java @@ -19,6 +19,7 @@ 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; @@ -30,7 +31,6 @@ 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(); @@ -339,8 +335,8 @@ public static String getConnectionContainerInfo(DBPDataSourceContainer container return container.getName() + " [" + container.getId() + "]"; } - public static void updateConfigAndRefreshDatabases(WebSession session, String projectId) { - DBNProject projectNode = session.getNavigatorModel().getRoot().getProjectNode(session.getProjectById(projectId)); + public static void updateConfigAndRefreshDatabases(WebSession session, String projectId) throws DBWebException { + DBNProject projectNode = session.getNavigatorModelOrThrow().getRoot().getProjectNode(session.getProjectById(projectId)); DBNModel.updateConfigAndRefreshDatabases(projectNode.getDatabases()); } 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 2e3ca7aea1..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 @@ -20,11 +20,16 @@ import io.cloudbeaver.model.config.CBAppConfig; import io.cloudbeaver.model.utils.ConfigurationUtils; 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/WebProductInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java index be799bc9c7..58d8f6b6bd 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java @@ -77,4 +77,10 @@ public String getLatestVersionInfo() { return CommonUtils.notEmpty(product.getProperty("versionUpdateURL")); } + @Property + public String getProductPurchaseURL() { + IProduct product = Platform.getProduct(); + return CommonUtils.notEmpty(product.getProperty("productPurchaseURL")); + } + } 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 02b99023ec..afd236fc54 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,14 +16,18 @@ */ package io.cloudbeaver.model; +import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.model.config.PasswordPolicyConfiguration; +import io.cloudbeaver.registry.WebServerFeatureRegistry; import io.cloudbeaver.registry.WebServiceDescriptor; import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; +import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; +import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings; import org.jkiss.dbeaver.registry.language.PlatformLanguageDescriptor; import org.jkiss.dbeaver.registry.language.PlatformLanguageRegistry; import org.jkiss.dbeaver.runtime.DBWorkbench; @@ -39,15 +43,18 @@ */ public class WebServerConfig { - private final CBApplication application; + private final WebApplication application; - public WebServerConfig(CBApplication application) { + public WebServerConfig(@NotNull WebApplication application) { this.application = application; } @Property public String getName() { - return CommonUtils.notEmpty(application.getServerConfiguration().getServerName()); + if (application instanceof CBApplication cbApp) { + return CommonUtils.notEmpty(cbApp.getServerConfiguration().getServerName()); + } + return ""; } @Property @@ -62,7 +69,10 @@ public String getWorkspaceId() { @Property public String getServerURL() { - return CommonUtils.notEmpty(application.getServerConfiguration().getServerURL()); + if (application instanceof CBApplication cbApp) { + return CommonUtils.notEmpty(cbApp.getServerConfiguration().getServerURL()); + } + return ""; } @Property @@ -78,7 +88,10 @@ public String getHostName() { @Property public String getContainerId() { - return CommonUtils.notEmpty(application.getContainerId()); + if (application instanceof CBApplication cbApp) { + return CommonUtils.notEmpty(cbApp.getContainerId()); + } + return ""; } @Property @@ -93,22 +106,34 @@ public boolean isSupportsCustomConnections() { @Property public boolean isSupportsConnectionBrowser() { - return application.getAppConfiguration().isSupportsConnectionBrowser(); + if (application instanceof CBApplication cbApp) { + return cbApp.getAppConfiguration().isSupportsConnectionBrowser(); + } + return false; } @Property public boolean isSupportsWorkspaces() { - return application.getAppConfiguration().isSupportsUserWorkspaces(); + if (application instanceof CBApplication cbApp) { + return cbApp.getAppConfiguration().isSupportsUserWorkspaces(); + } + return false; } @Property public boolean isPublicCredentialsSaveEnabled() { - return application.getAppConfiguration().isPublicCredentialsSaveEnabled(); + if (application instanceof CBApplication cbApp) { + return cbApp.getAppConfiguration().isPublicCredentialsSaveEnabled(); + } + return false; } @Property public boolean isAdminCredentialsSaveEnabled() { - return application.getAppConfiguration().isAdminCredentialsSaveEnabled(); + if (application instanceof CBApplication cbApp) { + return cbApp.getAppConfiguration().isAdminCredentialsSaveEnabled(); + } + return false; } @Property @@ -118,12 +143,18 @@ public boolean isLicenseRequired() { @Property public boolean isLicenseValid() { - return application.isLicenseValid(); + if (application instanceof CBApplication cbApp) { + return cbApp.isLicenseValid(); + } + return false; } @Property public String getLicenseStatus() { - return application.getLicenseStatus(); + if (application instanceof CBApplication cbApp) { + return cbApp.getLicenseStatus(); + } + return ""; } @Property @@ -138,7 +169,10 @@ public boolean isDevelopmentMode() { @Property public boolean isRedirectOnFederatedAuth() { - return application.getAppConfiguration().isRedirectOnFederatedAuth(); + if (application instanceof CBApplication cbApp) { + return cbApp.getAppConfiguration().isRedirectOnFederatedAuth(); + } + return false; } @Property @@ -148,12 +182,18 @@ public boolean isResourceManagerEnabled() { @Property public long getSessionExpireTime() { - return application.getServerConfiguration().getMaxSessionIdleTime(); + if (application instanceof CBApplication cbApp) { + return cbApp.getServerConfiguration().getMaxSessionIdleTime(); + } + return 0; } @Property public String getLocalHostAddress() { - return application.getLocalHostAddress(); + if (application instanceof CBApplication cbApp) { + return cbApp.getLocalHostAddress(); + } + return ""; } @Property @@ -164,12 +204,24 @@ public String[] getEnabledFeatures() { @Property @Nullable public String[] getDisabledBetaFeatures() { - return application.getAppConfiguration().getDisabledBetaFeatures(); + if (application instanceof CBApplication cbApp) { + return cbApp.getAppConfiguration().getDisabledBetaFeatures(); + } + return new String[0]; + } + + @Property + @NotNull + public String[] getServerFeatures() { + return WebServerFeatureRegistry.getInstance().getServerFeatures(); } @Property public String[] getEnabledAuthProviders() { - return application.getAppConfiguration().getEnabledAuthProviders(); + if (application instanceof CBApplication cbApp) { + return cbApp.getAppConfiguration().getEnabledAuthProviders(); + } + return new String[0]; } @Property @@ -198,12 +250,18 @@ public Map getProductConfiguration() { @Property public DBNBrowseSettings getDefaultNavigatorSettings() { - return application.getAppConfiguration().getDefaultNavigatorSettings(); + if (application instanceof CBApplication cbApp) { + return cbApp.getAppConfiguration().getDefaultNavigatorSettings(); + } + return new DataSourceNavigatorSettings(); } @Property public Map getResourceQuotas() { - return application.getAppConfiguration().getResourceQuotas(); + if (application instanceof CBApplication cbApp) { + return cbApp.getAppConfiguration().getResourceQuotas(); + } + return Map.of(); } @Property @@ -213,7 +271,10 @@ public WebProductInfo getProductInfo() { @Property public String[] getDisabledDrivers() { - return application.getAppConfiguration().getDisabledDrivers(); + if (application instanceof CBApplication cbApp) { + return cbApp.getAppConfiguration().getDisabledDrivers(); + } + return new String[0]; } @Property @@ -223,7 +284,10 @@ public Boolean isDistributed() { @Property public String getDefaultAuthRole() { - return application.getDefaultAuthRole(); + if (application instanceof CBApplication cbApp) { + return cbApp.getDefaultAuthRole(); + } + return ""; } @Property @@ -233,6 +297,9 @@ public String getDefaultUserTeam() { @Property public PasswordPolicyConfiguration getPasswordPolicyConfiguration() { - return application.getSecurityManagerConfiguration().getPasswordPolicyConfiguration(); + if (application instanceof CBApplication cbApp) { + return cbApp.getSecurityManagerConfiguration().getPasswordPolicyConfiguration(); + } + return new PasswordPolicyConfiguration(); } } 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 e9caa0ea1d..242bf2d79c 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 @@ -90,6 +90,11 @@ public boolean isPrivate() { public boolean isRequired() { return descriptor.isRequired(); } + + public boolean isAuthHidden() { + return descriptor.isAuthHidden(); + } + public boolean isAuthRoleProvided(SMAuthProviderCustomConfiguration configuration) { if (descriptor.getInstance() instanceof SMProvisioner provisioner) { return provisioner.isAuthRoleProvided(configuration); 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 f6e36bc2a9..de0f13c5cc 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 @@ -59,17 +59,14 @@ 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; @@ -112,6 +109,8 @@ public static CBApplication getInstance() { private final Map initActions = new ConcurrentHashMap<>(); + private CBJettyServer jettyServer; + public CBApplication() { this.homeDirectory = new File(initHomeFolder()); } @@ -204,6 +203,7 @@ protected void startServer() { if (!loadServerConfiguration()) { return; } + if (CommonUtils.isEmpty(this.getAppConfiguration().getDefaultUserTeam())) { throw new DBException("Default user team must be specified"); } @@ -211,6 +211,7 @@ protected void startServer() { log.error(e); return; } + refreshDisabledDriversConfig(); configurationMode = CommonUtils.isEmpty(getServerConfiguration().getServerName()); @@ -232,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, @@ -306,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()) { @@ -315,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(); @@ -344,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); @@ -353,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); @@ -365,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); } } @@ -452,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(); } @@ -481,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(); } @@ -542,7 +529,7 @@ public synchronized void finishConfiguration( } if (isConfigurationMode()) { - finishSecurityServiceConfiguration(adminName, adminPassword, authInfoList); + finishSecurityServiceConfiguration(adminName.toLowerCase(), adminPassword, authInfoList); } // Save runtime configuration @@ -581,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/CBPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java index 1c4e0fadd5..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 @@ -59,7 +59,7 @@ public class CBPlatform extends BaseGQLPlatform { @Nullable private static GQLApplicationAdapter application = null; - private CBPreferenceStore preferenceStore; + private WebServerPreferenceStore preferenceStore; protected final List applicableDrivers = new ArrayList<>(); public static CBPlatform getInstance() { @@ -77,7 +77,7 @@ public static void setApplication(@NotNull GQLApplicationAdapter application) { protected synchronized void initialize() { long startTime = System.currentTimeMillis(); log.info("Initialize web platform...: "); - this.preferenceStore = new CBPreferenceStore(this, WebPlatformActivator.getInstance().getPreferences()); + this.preferenceStore = new WebServerPreferenceStore(WebPlatformActivator.getInstance().getPreferences()); super.initialize(); refreshApplicableDrivers(); 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 7acde3722b..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,9 +16,7 @@ */ 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.model.config.CBAppConfig; @@ -42,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; @@ -68,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; } @@ -91,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 { @@ -146,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); } @@ -169,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)) { @@ -182,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)) { @@ -248,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); + } } } } @@ -307,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; @@ -324,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) @@ -358,10 +370,9 @@ private synchronized void writeRuntimeConfig(Path runtimeConfigPath, Map productConfiguration) throws DBException { @@ -633,4 +646,9 @@ private String readRootUri(String uri) { 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 7031b941a6..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 @@ -92,6 +92,7 @@ private void savePasswordPolicyConfig(Map originServerConfig, Ma } } + @NotNull @Override protected GsonBuilder getGsonBuilder() { GsonBuilder gsonBuilder = super.getGsonBuilder(); @@ -100,6 +101,4 @@ protected GsonBuilder getGsonBuilder() { return gsonBuilder .registerTypeAdapter(WebDatabaseConfig.class, dbConfigCreator); } - - } 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/WSFolderUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java index 974a16bdb3..7806e116b2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java @@ -20,6 +20,7 @@ import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.navigator.DBNModel; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent; import org.jkiss.utils.CommonUtils; @@ -32,15 +33,19 @@ public class WSFolderUpdatedEventHandlerImpl extends WSAbstractProjectEventHandl @Override protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSDatasourceFolderEvent event) { - if (activeUserSession instanceof WebSession) { - var webSession = (WebSession) activeUserSession; + if (activeUserSession instanceof WebSession webSession) { var project = webSession.getProjectById(event.getProjectId()); if (project == null) { log.debug("Project " + event.getProjectId() + " is not found in session " + webSession.getSessionId()); return; } project.getDataSourceRegistry().refreshConfig(); - webSession.getNavigatorModel().getRoot().getProjectNode(project).getDatabases().refreshChildren(); + DBNModel navigatorModel = webSession.getNavigatorModel(); + if (navigatorModel == null) { + log.debug("Navigator model is not found in session " + webSession.getSessionId()); + return; + } + navigatorModel.getRoot().getProjectNode(project).getDatabases().refreshChildren(); } activeUserSession.addSessionEvent(event); } 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/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); + log.debug("API > " + apiCall + loggerMessage); + } else if (DEBUG) { + log.debug("API > " + query + loggerMessage); } } ExecutionInput executionInput = contextBuilder.build(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLLoggerUtil.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLLoggerUtil.java new file mode 100644 index 0000000000..76ab1a42ab --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLLoggerUtil.java @@ -0,0 +1,109 @@ +/* + * 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.graphql; + +import io.cloudbeaver.model.app.BaseWebApplication; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBPlatform; +import jakarta.servlet.http.HttpServletRequest; +import org.jkiss.code.Nullable; +import org.jkiss.utils.CommonUtils; + +import java.util.Map; +import java.util.Set; + +public class GraphQLLoggerUtil { + + public static final String LOG_API_GRAPHQL_DEBUG_PARAMETER = "log.api.graphql.debug"; + private static final Set PROHIBITED_VARIABLES = + Set.of("password", "config", "parameters", "settings", "licenseText", "credentials", "username"); + + public static String getUserId(HttpServletRequest request) { + WebSession session = getWebSession(request); + if (session == null) { + return null; + } + String userId = session.getUserContext().getUserId(); + if (userId == null && session.getUserContext().isAuthorizedInSecurityManager()) { + return "anonymous"; + } + return userId; + } + + public static String getSessionId(HttpServletRequest request) { + WebSession session = getWebSession(request); + if (session == null) { + return null; + } + return session.getUserContext().getSmSessionId(); + } + + @Nullable + private static WebSession getWebSession(HttpServletRequest request) { + if (request.getSession() == null) { + return null; + } + + if (BaseWebApplication.getInstance() instanceof CBApplication cbApp) { + return (WebSession)cbApp.getSessionManager() + .getSession(request.getSession().getId()); + } else { + return null; + } + } + + public static String buildLoggerMessage(String sessionId, String userId, Map variables) { + StringBuilder loggerMessage = new StringBuilder(" [user: ").append(userId) + .append(", sessionId: ").append(sessionId).append("]"); + + if (CBPlatform.getInstance().getPreferenceStore().getBoolean(LOG_API_GRAPHQL_DEBUG_PARAMETER) + && variables != null + ) { + loggerMessage.append(" [variables] "); + String parsedVariables = parseVarialbes(variables); + if (CommonUtils.isNotEmpty(parsedVariables)) { + loggerMessage.append(parseVarialbes(variables)); + } + } + return loggerMessage.toString(); + } + + private static String parseVarialbes(Map map) { + StringBuilder result = new StringBuilder(); + + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + boolean isProhibited = PROHIBITED_VARIABLES.stream() + .anyMatch(prohibitedKey -> key.toLowerCase().contains(prohibitedKey.toLowerCase())); + + if (isProhibited) { + result.append(key).append(": ").append("******** "); + continue; + } + + if (value instanceof Map) { + result.append(parseVarialbes((Map) value)); + } else { + result.append(key).append(": ").append(value).append(" "); + } + } + return result.toString().trim(); + } +} 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 daa6f8e378..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,18 +16,21 @@ */ package io.cloudbeaver.server.jetty; -import io.cloudbeaver.server.GQLApplicationAdapter; +import io.cloudbeaver.model.config.CBServerConfig; import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.model.config.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.servlets.ProxyResourceHandler; import io.cloudbeaver.server.websockets.CBJettyWebSocketManager; import io.cloudbeaver.service.DBWServiceBindingServlet; -import org.eclipse.jetty.ee10.servlet.*; +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.session.DefaultSessionCache; import org.eclipse.jetty.session.DefaultSessionIdManager; @@ -57,6 +60,7 @@ public class CBJettyServer { } private final CBApplication application; + private Server server; public CBJettyServer(@NotNull CBApplication application) { this.application = application; @@ -65,7 +69,6 @@ public CBJettyServer(@NotNull CBApplication application) { public void runServer() { try { CBServerConfig serverConfiguration = application.getServerConfiguration(); - Server server; int serverPort = serverConfiguration.getServerPort(); String serverHost = serverConfiguration.getServerHost(); Path sslPath = getSslConfigurationPath(); @@ -97,11 +100,12 @@ public void runServer() { 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"); staticServletHolder.setInitParameter("cacheControl", "public, max-age=" + CBStaticServlet.STATIC_CACHE_SECONDS); servletContextHandler.addServlet(staticServletHolder, "/"); - servletContextHandler.insertHandler(new ProxyResourceHandler(Path.of(serverConfiguration.getContentRoot()))); if (Files.isSymbolicLink(contentRootPath)) { servletContextHandler.addAliasCheck(new CBSymLinkContentAllowedAliasChecker(contentRootPath)); @@ -130,11 +134,24 @@ public void runServer() { } } + 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); + } + } + } + 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()) ); } @@ -159,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(); @@ -176,7 +198,7 @@ public void runServer() { } } } - + refreshJettyConfig(); server.start(); server.join(); } catch (Exception e) { @@ -202,6 +224,7 @@ public static void initSessionManager( ) { // Init sessions persistence 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"); @@ -210,6 +233,7 @@ public static 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()); @@ -219,6 +243,19 @@ public static void initSessionManager( DefaultSessionIdManager idMgr = new DefaultSessionIdManager(server); idMgr.setWorkerName(null); server.addBean(idMgr, true); + } + public synchronized void refreshJettyConfig() { + if (server == null) { + return; + } + 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/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/CBSessionHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java index 6fa6d0f7b8..b8539264f7 100644 --- 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 @@ -17,174 +17,13 @@ package io.cloudbeaver.server.jetty; import io.cloudbeaver.server.GQLApplicationAdapter; -import jakarta.servlet.SessionCookieConfig; -import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.SessionHandler; -import java.util.Collections; -import java.util.Locale; -import java.util.Map; -import java.util.TreeMap; - public class CBSessionHandler extends SessionHandler { - private final CBCookieConfig cbCookieConfig; + static final int ONE_MINUTE = 60; private final GQLApplicationAdapter application; public CBSessionHandler(GQLApplicationAdapter application) { - this.cbCookieConfig = new CBCookieConfig(); this.application = application; } - - - @Override - public SessionCookieConfig getSessionCookieConfig() { - return this.cbCookieConfig; - } - - - //mostly copy of org.eclipse.jetty.ee10.servlet.CookieConfig but allows to use dynamic setSecure flag - public final class CBCookieConfig implements SessionCookieConfig { - - @Override - public boolean isSecure() { - var serverUrl = CBSessionHandler.this.application.getServerURL(); - return serverUrl != null && serverUrl.startsWith("https://"); - } - - @Override - public String getComment() { - return getSessionComment(); - } - - @Override - public String getDomain() { - return getSessionDomain(); - } - - @Override - public int getMaxAge() { - return getMaxCookieAge(); - } - - @Override - public void setAttribute(String name, String value) { - checkState(); - String lcase = name.toLowerCase(Locale.ENGLISH); - - switch (lcase) { - case "name" -> setName(value); - case "max-age" -> setMaxAge(value == null ? -1 : Integer.parseInt(value)); - case "comment" -> setComment(value); - case "domain" -> setDomain(value); - case "httponly" -> setHttpOnly(Boolean.parseBoolean(value)); - case "secure" -> setSecure(Boolean.parseBoolean(value)); - case "path" -> setPath(value); - default -> setSessionCookieAttribute(name, value); - } - } - - @Override - public String getAttribute(String name) { - String lcase = name.toLowerCase(Locale.ENGLISH); - return switch (lcase) { - case "name" -> getName(); - case "max-age" -> Integer.toString(getMaxAge()); - case "comment" -> getComment(); - case "domain" -> getDomain(); - case "httponly" -> String.valueOf(isHttpOnly()); - case "secure" -> String.valueOf(isSecure()); - case "path" -> getPath(); - default -> getSessionCookieAttribute(name); - }; - } - - /** - * According to the SessionCookieConfig javadoc, the attributes must also include - * all values set by explicit setters. - * - * @see SessionCookieConfig - */ - @Override - public Map getAttributes() { - Map specials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - specials.put("name", getAttribute("name")); - specials.put("max-age", getAttribute("max-age")); - specials.put("comment", getAttribute("comment")); - specials.put("domain", getAttribute("domain")); - specials.put("httponly", getAttribute("httponly")); - specials.put("secure", getAttribute("secure")); - specials.put("path", getAttribute("path")); - specials.putAll(getSessionCookieAttributes()); - return Collections.unmodifiableMap(specials); - } - - @Override - public String getName() { - return getSessionCookie(); - } - - @Override - public String getPath() { - return getSessionPath(); - } - - @Override - public boolean isHttpOnly() { - return CBSessionHandler.this.isHttpOnly(); - } - - @Override - public void setComment(String comment) { - checkState(); - CBSessionHandler.this.setSessionComment(comment); - } - - @Override - public void setDomain(String domain) { - checkState(); - CBSessionHandler.this.setSessionDomain(domain); - } - - @Override - public void setHttpOnly(boolean httpOnly) { - checkState(); - CBSessionHandler.this.setHttpOnly(httpOnly); - } - - @Override - public void setMaxAge(int maxAge) { - checkState(); - CBSessionHandler.this.setMaxCookieAge(maxAge); - } - - @Override - public void setName(String name) { - checkState(); - CBSessionHandler.this.setSessionCookie(name); - } - - @Override - public void setPath(String path) { - checkState(); - CBSessionHandler.this.setSessionPath(path); - } - - @Override - public void setSecure(boolean secure) { - checkState(); - CBSessionHandler.this.setSecureCookies(secure); - } - - private void checkState() { - //It is allowable to call the CookieConfig.setXX methods after the SessionHandler has started, - //but before the context has fully started. Ie it is allowable for ServletContextListeners - //to call these methods in contextInitialized(). - ServletContextHandler handler = ServletContextHandler.getCurrentServletContextHandler(); - if (handler != null && handler.isAvailable()) - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - - } - } - - } 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 15d4de80d8..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 org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.app.DBPPlatform; -import org.jkiss.dbeaver.model.runtime.AbstractJob; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; - -public abstract class PeriodicSystemJob extends AbstractJob { - - @NotNull - protected final DBPPlatform platform; - private final long periodMs; - - public PeriodicSystemJob(@NotNull String name, @NotNull DBPPlatform 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 354934ebc4..2a03e5262e 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 @@ -21,14 +21,16 @@ 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 { +import java.time.Duration; + +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 DBPPlatform platform, WebSessionManager sessionManager) { - super("Session state sender", platform, PERIOD_MS); + super("Session state sender", platform, Duration.ofSeconds(30)); this.sessionManager = sessionManager; } 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 c17dc270f4..3de046e1a9 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 @@ -21,17 +21,19 @@ 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; + +import java.time.Duration; /** * 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 DBPPlatform platform, @NotNull WebSessionManager sessionManager) { - super("Web session monitor", platform, MONITOR_INTERVAL); + super("Web session monitor", platform, Duration.ofSeconds(10)); this.sessionManager = sessionManager; } 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 e558dd4c75..ee850f61d7 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 @@ -35,14 +35,24 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.http.HttpHeader; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; import org.jkiss.dbeaver.model.auth.SMAuthProvider; import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; +import org.jkiss.dbeaver.utils.MimeTypes; import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; @WebServlet(urlPatterns = "/") @@ -54,6 +64,13 @@ public class CBStaticServlet extends DefaultServlet { private static final Log log = Log.getLog(CBStaticServlet.class); + @NotNull + private final Path contentRoot; + + public CBStaticServlet(@NotNull Path contentRoot) { + this.contentRoot = contentRoot; + } + @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { for (WebServletHandlerDescriptor handler : WebHandlerRegistry.getInstance().getServletHandlers()) { @@ -83,7 +100,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) { @@ -177,4 +194,41 @@ private boolean processSessionStart(HttpServletRequest request, HttpServletRespo return false; } + private void patchStaticContentIfNeeded(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String pathInContext = request.getServletPath(); + + if ("/".equals(pathInContext)) { + pathInContext = "index.html"; + } + + if (pathInContext == null || !pathInContext.endsWith("index.html") + && !pathInContext.endsWith("sso.html") + && !pathInContext.endsWith("ssoError.html") + ) { + super.doGet(request, response); + return; + } + + 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.CONTENT_TYPE.toString(), MimeTypes.TEXT_HTML); + 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/ProxyResourceHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java deleted file mode 100644 index c9c20d874b..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java +++ /dev/null @@ -1,80 +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.servlets; - -import io.cloudbeaver.model.config.CBServerConfig; -import io.cloudbeaver.server.CBApplication; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; -import org.jkiss.code.NotNull; -import org.jkiss.utils.IOUtils; - -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; - -public class ProxyResourceHandler extends Handler.Wrapper { - @NotNull - private final Path contentRoot; - - public ProxyResourceHandler(@NotNull Path contentRoot) { - this.contentRoot = contentRoot; - } - - public boolean handle(Request request, Response response, Callback callback) throws Exception { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - String pathInContext = Request.getPathInContext(request); - - if ("/".equals(pathInContext)) { - pathInContext = "index.html"; - } - - if (pathInContext == null || !pathInContext.endsWith("index.html") - && !pathInContext.endsWith("sso.html") - && !pathInContext.endsWith("ssoError.html") - ) { - return super.handle(request, response, callback); - } - - if (pathInContext.startsWith("/")) { - pathInContext = pathInContext.substring(1); - } - 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.getHeaders().put(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); - response.getHeaders().put(HttpHeader.EXPIRES.toString(), "0"); - - response.write(true, ByteBuffer.wrap(indexBytes), callback); - return true; - } -} 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 2dac868a86..7af0f380c4 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 @@ -25,7 +25,7 @@ public class CBAbstractWebSocket extends Session.Listener.AbstractAutoDemanding { private static final Log log = Log.getLog(CBAbstractWebSocket.class); - protected static final Gson gson = WSUtils.gson; + protected static final Gson gson = WSUtils.clientGson; public void handleEvent(WSEvent event) { if (!isOpen()) { 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 faea6732e0..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,7 +27,7 @@ import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.service.security.SMUtils; -import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.WebDataSourceUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.jkiss.code.NotNull; @@ -137,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 { @@ -230,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)) { 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 f67ce87cc4..22f0166156 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,6 +26,7 @@ import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.core.DBWServiceCore; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.WebAppUtils; import io.cloudbeaver.utils.WebConnectionFolderUtils; import io.cloudbeaver.utils.WebDataSourceUtils; import io.cloudbeaver.utils.WebEventUtils; @@ -78,7 +76,7 @@ public class WebServiceCore implements DBWServiceCore { @Override public WebServerConfig getServerConfig() { - return new WebServerConfig(CBApplication.getInstance()); + return new WebServerConfig(WebAppUtils.getWebApplication()); } @Override @@ -101,6 +99,7 @@ public List getAuthModels(@NotNull WebSession webSession) @Override public List getNetworkHandlers(@NotNull WebSession webSession) { return NetworkHandlerRegistry.getInstance().getDescriptors().stream() + .filter(d -> !d.isDesktopHandler()) .map(d -> new WebNetworkHandlerDescriptor(webSession, d)).collect(Collectors.toList()); } @@ -117,15 +116,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(); } @@ -153,27 +154,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)); } @@ -207,7 +211,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 }; @@ -316,9 +320,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); } @@ -333,7 +339,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(); @@ -429,11 +435,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"); } @@ -459,8 +465,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( @@ -484,9 +489,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()); @@ -499,6 +503,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()); @@ -575,7 +581,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"); } @@ -599,7 +605,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"); @@ -622,9 +629,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 @@ -635,8 +640,9 @@ public WebConnectionInfo copyConnectionFromNode( @NotNull WebConnectionConfig config ) throws DBWebException { try { - DBNModel navigatorModel = webSession.getNavigatorModel(); - DBPDataSourceRegistry dataSourceRegistry = getProjectById(webSession, projectId).getDataSourceRegistry(); + DBNModel navigatorModel = webSession.getNavigatorModelOrThrow(); + WebSessionProjectImpl project = getProjectById(webSession, projectId); + DBPDataSourceRegistry dataSourceRegistry = project.getDataSourceRegistry(); DBNNode srcNode = navigatorModel.getNodeByPath(webSession.getProgressMonitor(), nodePath); if (srcNode == null) { @@ -662,9 +668,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, @@ -687,7 +692,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(); @@ -823,12 +828,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(); @@ -840,7 +846,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(); @@ -853,7 +859,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)) @@ -954,7 +960,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(); @@ -983,8 +989,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/DBWServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/DBWServiceNavigator.java index 0e1a678675..78d0c1fb3a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/DBWServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/DBWServiceNavigator.java @@ -57,9 +57,10 @@ boolean setNavigatorNodeFilter( @Nullable List exclude) throws DBWebException; @WebAction - boolean refreshNavigatorNode( + WebNavigatorNodeInfo refreshNavigatorNode( @NotNull WebSession session, - @NotNull String nodePath) throws DBWebException; + @NotNull String nodePath, + @Nullable Boolean recursive) throws DBWebException; @WebAction WebStructContainers getStructContainers( 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 6e31e3113e..cb38b56b41 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 @@ -229,7 +229,7 @@ public String[] getFeatures() { if (node instanceof DBNDatabaseNode) { boolean canEditDatasources = hasNodePermission(RMProjectPermission.DATA_SOURCES_EDIT); DBSObject object = ((DBNDatabaseNode) node).getObject(); - if (object != null && canEditDatasources) { + if (object != null && canEditDatasources && !DBUtils.isReadOnly(object)) { DBEObjectMaker objectManager = DBWorkbench.getPlatform().getEditorsRegistry().getObjectManager( object.getClass(), DBEObjectMaker.class); if (objectManager != null && objectManager.canDeleteObject(object)) { @@ -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); } @@ -324,8 +324,14 @@ public DBSObjectFilter getFilter() throws DBWebException { if (!(node instanceof DBNDatabaseNode dbNode)) { throw new DBWebException("Invalid navigator node type: " + node.getClass().getName()); } - DBSObjectFilter filter = dbNode.getNodeFilter(dbNode.getItemsMeta(), true); - return filter == null || filter.isEmpty() || !filter.isEnabled() ? null : filter; + try { + DBSObjectFilter filter = dbNode.getNodeFilter( + DBNUtils.getValidItemsMeta(session.getProgressMonitor(), dbNode), + true); + return filter == null || filter.isEmpty() || !filter.isEnabled() ? null : filter; + } catch (DBException e) { + throw new DBWebException(e); + } } @Override diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebServiceBindingNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebServiceBindingNavigator.java index 0e2aad3447..bfc9619808 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebServiceBindingNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebServiceBindingNavigator.java @@ -53,8 +53,9 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { )) .dataFetcher("navRefreshNode", env -> getService(env).refreshNavigatorNode( getWebSession(env), - env.getArgument("nodePath") - )) + env.getArgument("nodePath"), + false + ) != null) .dataFetcher("navGetStructContainers", env -> getService(env).getStructContainers( getProjectReference(env), getWebConnection(env), @@ -63,6 +64,11 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { )); model.getMutationType() + .dataFetcher("navReloadNode", env -> getService(env).refreshNavigatorNode( + getWebSession(env), + env.getArgument("nodePath"), + true + )) .dataFetcher("navSetFolderFilter", env -> getService(env).setNavigatorNodeFilter( getWebSession(env), env.getArgument("nodePath"), 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 96690ea4de..080dbdb795 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 @@ -19,7 +19,6 @@ import io.cloudbeaver.BaseWebProjectImpl; import io.cloudbeaver.DBWebException; -import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.WebCommandContext; import io.cloudbeaver.model.WebConnectionInfo; @@ -43,6 +42,8 @@ import org.jkiss.dbeaver.model.exec.DBCExecutionContext; import org.jkiss.dbeaver.model.exec.DBCExecutionContextDefaults; import org.jkiss.dbeaver.model.navigator.*; +import org.jkiss.dbeaver.model.navigator.meta.DBXTreeItem; +import org.jkiss.dbeaver.model.rm.RMControllerProvider; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.rm.RMProjectPermission; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -82,7 +83,7 @@ public List getNavigatorNodeChildren( DBNNode[] nodeChildren; boolean isRootPath = CommonUtils.isEmpty(parentPath) || "/".equals(parentPath) || ROOT_DATABASES.equals(parentPath); - DBNModel navigatorModel = session.getNavigatorModel(); + DBNModel navigatorModel = session.getNavigatorModelOrThrow(); Set applicableDrivers = WebServiceUtils.getApplicableDriversIds(); if (isRootPath) { DBNRoot rootNode = navigatorModel.getRoot(); @@ -142,7 +143,7 @@ public List getNavigatorNodeChildren( return result.subList(offset, result.size()); } } catch (DBException e) { - throw new DBWebException(e); + throw new DBWebException(e.getMessage(), e); } } @@ -154,8 +155,7 @@ public List getNavigatorNodeParents( try { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNModel navigatorModel = session.getNavigatorModel(); - DBNNode node = navigatorModel.getNodeByPath(monitor, nodePath); + DBNNode node = session.getNavigatorModelOrThrow().getNodeByPath(monitor, nodePath); if (node == null) { throw new DBWebException("Node '" + nodePath + "' not found"); } @@ -197,7 +197,7 @@ public WebNavigatorNodeInfo getNavigatorNodeInfo( try { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNNode node = session.getNavigatorModel().getNodeByPath(monitor, nodePath); + DBNNode node = session.getNavigatorModelOrThrow().getNodeByPath(monitor, nodePath); if (node == null) { throw new DBWebException("Navigator node '" + nodePath + "' not found"); } @@ -216,7 +216,7 @@ public boolean setNavigatorNodeFilter( try { DBRProgressMonitor monitor = webSession.getProgressMonitor(); - DBNNode node = webSession.getNavigatorModel().getNodeByPath(monitor, nodePath); + DBNNode node = webSession.getNavigatorModelOrThrow().getNodeByPath(monitor, nodePath); if (node == null) { throw new DBWebException("Navigator node '" + nodePath + "' not found"); } @@ -229,8 +229,11 @@ public boolean setNavigatorNodeFilter( } filter.setEnabled(true); if (node instanceof DBNDatabaseNode dbNode) { - dbNode.setNodeFilter(dbNode.getItemsMeta(), filter, true); - if (hasNodeEditPermission(webSession, node, ((WebProjectImpl) node.getOwnerProject()).getRmProject())) { + DBXTreeItem itemsMeta = DBNUtils.getValidItemsMeta(webSession.getProgressMonitor(), dbNode); + dbNode.setNodeFilter(itemsMeta, filter, true); + if (node.getOwnerProject() instanceof RMControllerProvider rmControllerProvider && + hasNodeEditPermission(webSession, node, rmControllerProvider.getRMProject()) + ) { // Save settings dbNode.getDataSourceContainer().persistConfiguration(); } @@ -245,14 +248,15 @@ public boolean setNavigatorNodeFilter( } @Override - public boolean refreshNavigatorNode( + public WebNavigatorNodeInfo refreshNavigatorNode( @NotNull WebSession session, - @NotNull String nodePath + @NotNull String nodePath, + @Nullable Boolean recursive ) throws DBWebException { try { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNNode node = session.getNavigatorModel().getNodeByPath(monitor, nodePath); + DBNNode node = session.getNavigatorModelOrThrow().getNodeByPath(monitor, nodePath); if (node == null) { throw new DBWebException("Navigator node '" + nodePath + "' not found"); } @@ -266,11 +270,14 @@ public boolean refreshNavigatorNode( dbnDataSource.cleanupNode(); } else if (node instanceof DBNLocalFolder) { // Refresh can't be applied to the local folder node - return true; + } else if (node instanceof DBNRoot) { + if (recursive != null && recursive) { + node.refreshNode(monitor, this); + } } else { node.refreshNode(monitor, this); } - return true; + return new WebNavigatorNodeInfo(session, node); } catch (DBException e) { throw new DBWebException("Error refreshing navigator node '" + nodePath + "'", e); } @@ -376,10 +383,9 @@ protected List getCatalogs(DBRProgressMonitor monitor, DBSO } @Nullable - protected WebNavigatorNodeInfo getNodeFromObject(WebSession session, DBSObject object){ - DBNModel navigatorModel = session.getNavigatorModel(); + protected WebNavigatorNodeInfo getNodeFromObject(WebSession session, DBSObject object) throws DBWebException { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNNode node = navigatorModel.getNodeByObject(monitor, object, false); + DBNNode node = session.getNavigatorModelOrThrow().getNodeByObject(monitor, object, false); return node == null ? null : new WebNavigatorNodeInfo(session, node); } @@ -393,7 +399,7 @@ public String renameNode( try { DBRProgressMonitor monitor = session.getProgressMonitor(); - DBNNode node = session.getNavigatorModel().getNodeByPath(monitor, nodePath); + DBNNode node = session.getNavigatorModelOrThrow().getNodeByPath(monitor, nodePath); if (node == null) { throw new DBWebException("Navigator node '" + nodePath + "' not found"); } @@ -498,8 +504,9 @@ public int deleteNodes( String projectId = null; boolean containsFolderNodes = false; Map nodes = new LinkedHashMap<>(); + DBNModel model = session.getNavigatorModelOrThrow(); for (String path : nodePaths) { - DBNNode node = session.getNavigatorModel().getNodeByPath(monitor, path); + DBNNode node = model.getNodeByPath(monitor, path); if (node == null) { throw new DBWebException("Navigator node '" + path + "' not found"); } @@ -571,7 +578,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"); } } @@ -594,9 +601,10 @@ public boolean moveNodesToFolder( try { DBRProgressMonitor monitor = session.getProgressMonitor(); DBNNode folderNode; - folderNode = session.getNavigatorModel().getNodeByPath(monitor, folderNodePath); + DBNModel navigatorModel = session.getNavigatorModelOrThrow(); + folderNode = navigatorModel.getNodeByPath(monitor, folderNodePath); for (String path : nodePaths) { - DBNNode node = session.getNavigatorModel().getNodeByPath(monitor, path); + DBNNode node = navigatorModel.getNodeByPath(monitor, path); if (node == null) { throw new DBWebException("Navigator node '" + path + "' not found"); } 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 92017155d1..324c96de6d 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,10 +18,10 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.auth.SMTokenCredentialProvider; -import io.cloudbeaver.server.AppWebSessionManager; import io.cloudbeaver.model.session.*; import io.cloudbeaver.registry.WebHandlerRegistry; import io.cloudbeaver.registry.WebSessionHandlerDescriptor; +import io.cloudbeaver.server.AppWebSessionManager; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBConstants; import io.cloudbeaver.server.events.WSWebUtils; @@ -315,6 +315,14 @@ public WebHeadlessSession getHeadlessSession(Request request, Session session, b var existSession = sessionMap.get(sessionId); if (existSession instanceof WebHeadlessSession) { + var creds = existSession.getUserContext().getActiveUserCredentials(); + if (creds == null || !smAccessToken.equals(creds.getSmAccessToken())) { + existSession.getUserContext().refresh( + smAccessToken, + null, + authPermissions + ); + } return (WebHeadlessSession) existSession; } if (existSession != null) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java index c6a5b1b600..b8b32bd04e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java @@ -123,6 +123,6 @@ public boolean isShowValues() { @Override public SQLCompletionProposalBase createProposal(@NotNull SQLCompletionRequest request, @NotNull String displayString, @NotNull String replacementString, int cursorPosition, @Nullable DBPImage image, @NotNull DBPKeywordType proposalType, @Nullable String description, @Nullable DBPNamedObject object, @NotNull Map params) { - return new SQLCompletionProposalBase(this, request.getWordDetector(), displayString, replacementString, cursorPosition, image, proposalType, description, object, params); + return new SQLCompletionProposalBase(request, displayString, replacementString, cursorPosition, image, proposalType, description, object, params); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java index 1dc81cbad9..f0115c6cef 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java @@ -276,7 +276,7 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException, In public Boolean isAutoCommit() throws DBWebException { DBCExecutionContext context = processor.getExecutionContext(); DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); - if (txnManager == null) { + if (txnManager == null || !txnManager.isSupportsTransactions()) { return null; } try { 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 a58ab10961..436c67ae4c 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 @@ -950,7 +950,7 @@ private void checkDataEditAllowed(DBSEntity dataContainer) throws DBWebException @NotNull public T getDataContainerByNodePath(DBRProgressMonitor monitor, @NotNull String containerPath, Class type) throws DBException { - DBNNode node = webSession.getNavigatorModel().getNodeByPath(monitor, containerPath); + DBNNode node = webSession.getNavigatorModelOrThrow().getNodeByPath(monitor, containerPath); if (node == null) { throw new DBWebException("Container node '" + containerPath + "' not found"); } 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..3a53466590 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; @@ -35,10 +35,7 @@ import org.jkiss.utils.CommonUtils; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; class WebSQLQueryDataReceiver implements DBDDataReceiver { @@ -58,7 +55,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() { @@ -168,8 +167,9 @@ public void fetchEnd(@NotNull DBCSession session, @NotNull DBCResultSet resultSe webResultSet.setSingleEntity(isSingleEntity); - DBDRowIdentifier rowIdentifier = resultsInfo.getDefaultRowIdentifier(); - webResultSet.setHasRowIdentifier(rowIdentifier != null && rowIdentifier.isValidIdentifier()); + Set rowIdentifiers = resultsInfo.getRowIdentifiers(); + boolean hasRowIdentifier = rowIdentifiers.stream().allMatch(DBDRowIdentifier::isValidIdentifier); + webResultSet.setHasRowIdentifier(!rowIdentifiers.isEmpty() && hasRowIdentifier); } private void convertComplexValuesToRelationalView(DBCSession session) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java index 9dfdc218c8..1f03d70e0b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java @@ -105,6 +105,11 @@ public boolean isRequired() { return attrMeta.isRequired(); } + @Property + public boolean isAutoGenerated() { + return attrMeta.isAutoGenerated(); + } + @Property public boolean isReadOnly() { return DBExecUtils.isAttributeReadOnly(attrMeta); 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 2e55d43b34..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.config.CBAppConfig; +import io.cloudbeaver.model.app.WebAppConfiguration; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebServiceRegistry; -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/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index cc52563e20..36ff2f17d7 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 @@ -40,6 +40,7 @@ import org.jkiss.dbeaver.model.exec.trace.DBCTraceDynamic; import org.jkiss.dbeaver.model.exec.trace.DBCTraceProperty; import org.jkiss.dbeaver.model.impl.sql.BasicSQLDialect; +import org.jkiss.dbeaver.model.navigator.DBNModel; import org.jkiss.dbeaver.model.navigator.DBNNode; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.sql.*; @@ -84,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<>(); @@ -241,8 +242,9 @@ public String generateEntityQuery(@NotNull WebSession session, @NotNull String g private List getObjectListFromNodeIds(@NotNull WebSession session, @NotNull List nodePathList) throws DBWebException { try { List objectList = new ArrayList<>(nodePathList.size()); + DBNModel navigatorModel = session.getNavigatorModelOrThrow(); for (String nodePath : nodePathList) { - DBNNode node = session.getNavigatorModel().getNodeByPath(session.getProgressMonitor(), nodePath); + DBNNode node = navigatorModel.getNodeByPath(session.getProgressMonitor(), nodePath); if (node == null) { throw new DBException("Node '" + nodePath + "' not found"); } 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 6b10865c0b..ebff9ca493 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.106.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.111.qualifier +Bundle-Release-Date: 20241223 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 cc6d869b22..93c836ab06 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.106-SNAPSHOT + 1.0.111-SNAPSHOT eclipse-plugin 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 48e78992a3..51c959e209 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 @@ -161,12 +161,13 @@ public AdminUserInfo createUser( if (userName.isEmpty()) { throw new DBWebException("Empty user name"); } - webSession.addInfoMessage("Create new user - " + userName); + String userId = userName.toLowerCase(); + webSession.addInfoMessage("Create new user - " + userId); try { var securityController = webSession.getAdminSecurityController(); - securityController.createUser(userName, Map.of(), enabled, authRole); - var smUser = securityController.getUserById(userName); + securityController.createUser(userId, Map.of(), enabled, authRole); + var smUser = securityController.getUserById(userId); return new AdminUserInfo(webSession, new WebUser(smUser)); } catch (Exception e) { throw new DBWebException("Error creating new user", e); 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 fbc470b0f7..f1ff27028a 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.106.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.111.qualifier +Bundle-Release-Date: 20241223 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 7bff8ea860..75a95071af 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.106-SNAPSHOT + 1.0.111-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 362f5a3549..3e8078ab1c 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -61,6 +61,7 @@ type AuthProviderInfo { defaultProvider: Boolean! trusted: Boolean! private: Boolean! + authHidden: Boolean! @since(version: "24.2.4") supportProvisioning: Boolean! # Configurable providers must be configured first. See configurations field. @@ -137,6 +138,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/RPSessionHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java index 62277e7e89..767823b4c3 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java @@ -98,9 +98,13 @@ public void reverseProxyAuthentication(@NotNull HttpServletRequest request, @Not String firstName = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_FIRST_NAME), RPAuthProvider.X_FIRST_NAME)); String lastName = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_LAST_NAME), RPAuthProvider.X_LAST_NAME)); String fullName = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_FULL_NAME), RPAuthProvider.X_FULL_NAME)); - String logoutUrl = Objects.requireNonNull(configuration).getParameter(RPConstants.PARAM_LOGOUT_URL); - String teamDelimiter = JSONUtils.getString(configuration.getParameters(), - RPConstants.PARAM_TEAM_DELIMITER, "\\|"); + String logoutUrl = null; + String teamDelimiter = DEFAULT_TEAM_DELIMITER; + if (configuration != null) { + logoutUrl = configuration.getParameter(RPConstants.PARAM_LOGOUT_URL); + teamDelimiter = resolveParam(JSONUtils.getString(configuration.getParameters(), + RPConstants.PARAM_TEAM_DELIMITER), DEFAULT_TEAM_DELIMITER); + } List userTeams = teams == null ? null : (teams.isEmpty() ? List.of() : List.of(teams.split(teamDelimiter))); if (userName != null) { try { 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..44b780a3b5 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 @@ -303,5 +309,4 @@ public WebUserInfo setUserConfigurationParameters( throw new DBWebException("Error setting user parameters", e); } } - } 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 f5250bd4cc..5eac42de91 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.107.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.112.qualifier +Bundle-Release-Date: 20241223 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 185a1d5222..bc1b4cb736 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.107-SNAPSHOT + 1.0.112-SNAPSHOT eclipse-plugin 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 c0f575abb5..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 @@ -88,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.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java index 6367944b53..2d26f85dfd 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java @@ -362,7 +362,7 @@ private void importData( processorInstance, properties); DatabaseMappingContainer databaseMappingContainer = - new DatabaseMappingContainer(databaseConsumerSettings, producer.getDatabaseObject()); + new DatabaseMappingContainer(monitor, databaseConsumerSettings, producer.getDatabaseObject(), consumer.getTargetObject()); databaseMappingContainer.getAttributeMappings(monitor); databaseMappingContainer.setTarget(dataContainer); consumer.setContainerMapping(databaseMappingContainer); 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 f87beec385..3b38e865fd 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.24.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.29.qualifier +Bundle-Release-Date: 20241223 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 52d9291e87..e4bc71e98c 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.24-SNAPSHOT + 1.0.29-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java index f02d3ca6da..187184c780 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java @@ -51,7 +51,7 @@ public FSFileSystem[] getAvailableFileSystems(@NotNull WebSession webSession, @N if (project == null) { throw new DBException(MessageFormat.format("Project ''{0}'' is not found in session", projectId)); } - DBNProject projectNode = webSession.getNavigatorModel().getRoot().getProjectNode(project); + DBNProject projectNode = webSession.getNavigatorModelOrThrow().getRoot().getProjectNode(project); if (projectNode == null) { throw new DBException(MessageFormat.format("Project ''{0}'' is not found in navigator model", projectId)); } @@ -78,7 +78,7 @@ public FSFileSystem getFileSystem( @NotNull String nodePath ) throws DBWebException { try { - var node = webSession.getNavigatorModel().getNodeByPath(webSession.getProgressMonitor(), nodePath); + var node = webSession.getNavigatorModelOrThrow().getNodeByPath(webSession.getProgressMonitor(), nodePath); if (!(node instanceof DBNFileSystem fs)) { throw new DBException(MessageFormat.format("Node ''{0}'' is not File System", nodePath)); } 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 fa2514c98f..9a39ba821d 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 @@ -31,7 +31,6 @@ import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; -import org.jkiss.dbeaver.utils.GeneralUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; @@ -100,7 +99,7 @@ private void doPost(WebSession session, HttpServletRequest request, HttpServletR } } catch (Exception e) { throw new DBWebException("File Upload Failed: Unable to Save File to the File System", - GeneralUtils.getRootCause(e)); + CommonUtils.getRootCause(e)); } } } diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.ldap.auth/META-INF/MANIFEST.MF index 983d13ae7d..aeb12d9208 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/META-INF/MANIFEST.MF @@ -1,6 +1,6 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 -Bundle-Vendor: Cloudbeaver LDAP +Bundle-Name: Cloudbeaver Web Service - LDAP Bundle-Vendor: DBeaver Corp Bundle-SymbolicName: io.cloudbeaver.service.ldap.auth;singleton:=true Bundle-Version: 1.0.0.qualifier 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 8f2fa9480d..067992b3c9 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.110.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.115.qualifier +Bundle-Release-Date: 20241223 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 f97daaaaaa..bdcca4f3ad 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.110-SNAPSHOT + 1.0.115-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceBindingMetadata.java b/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceBindingMetadata.java index 9a4221fff6..d135f77666 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceBindingMetadata.java +++ b/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceBindingMetadata.java @@ -52,6 +52,9 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { private DBNNode getNodeFromPath(DataFetchingEnvironment env) throws DBException { WebSession webSession = getWebSession(env); String nodePath = env.getArgument("nodeId"); - return webSession.getNavigatorModel().getNodeByPath(webSession.getProgressMonitor(), nodePath); + if (nodePath == null) { + throw new DBException("Node path is null"); + } + return webSession.getNavigatorModelOrThrow().getNodeByPath(webSession.getProgressMonitor(), nodePath); } } 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 c2fbcd9eac..52a28aaa50 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.24.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.29.qualifier +Bundle-Release-Date: 20241223 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 73a645ff48..67e3719046 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.24-SNAPSHOT + 1.0.29-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java index ed535b60d1..53c60d23b1 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java @@ -28,6 +28,7 @@ import java.net.URI; import java.nio.file.Path; +import java.util.List; public class RMVirtualFileSystem extends AbstractVirtualFileSystem { @NotNull @@ -83,7 +84,7 @@ public Path getPathByURI(@NotNull DBRProgressMonitor monitor, @NotNull URI uri) @NotNull @Override - public DBFVirtualFileSystemRoot[] getRootFolders(DBRProgressMonitor monitor) throws DBException { - return new RMVirtualFileSystemRoot[]{new RMVirtualFileSystemRoot(this, rmProject, rmNioFileSystemProvider)}; + public List getRootFolders(DBRProgressMonitor monitor) throws DBException { + return List.of(new RMVirtualFileSystemRoot(this, rmProject, rmNioFileSystemProvider)); } } 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 31ae2dab47..e0ca21dd10 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.59.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.64.qualifier +Bundle-Release-Date: 20241223 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 a46196fd8e..ad4f1e47f0 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.59-SNAPSHOT + 1.0.64-SNAPSHOT eclipse-plugin 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 be317c8984..e209de2cf9 100644 --- a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF @@ -1,10 +1,10 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 -Bundle-Vendor: Cloudbeaver Web Service - Security +Bundle-Name: Cloudbeaver Web Service - Security Bundle-Vendor: DBeaver Corp Bundle-SymbolicName: io.cloudbeaver.service.security;singleton:=true -Bundle-Version: 1.0.62.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.67.qualifier +Bundle-Release-Date: 20241223 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql index 8186f838a5..1c91b24224 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql @@ -283,3 +283,16 @@ CREATE TABLE {table_prefix}CB_TASKS PRIMARY KEY (TASK_ID) ); + +CREATE TABLE {table_prefix}CB_ACCESS_TOKEN +( + TOKEN_ID VARCHAR(128) NOT NULL, + USER_ID VARCHAR(128) NOT NULL, + TOKEN_NAME VARCHAR(128) NOT NULL, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + EXPIRATION_TIME TIMESTAMP NULL, + + PRIMARY KEY (USER_ID, TOKEN_ID), + UNIQUE (USER_ID, TOKEN_NAME), + FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_USER(USER_ID) ON DELETE CASCADE +); diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_22.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_22.sql new file mode 100644 index 0000000000..201502ce6c --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_22.sql @@ -0,0 +1,12 @@ +CREATE TABLE {table_prefix}CB_ACCESS_TOKEN +( + TOKEN_ID VARCHAR(128) NOT NULL, + USER_ID VARCHAR(128) NOT NULL, + TOKEN_NAME VARCHAR(128) NOT NULL, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + EXPIRATION_TIME TIMESTAMP NULL, + + PRIMARY KEY (USER_ID, TOKEN_ID), + UNIQUE (USER_ID, TOKEN_NAME), + FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_USER(USER_ID) ON DELETE CASCADE +); diff --git a/server/bundles/io.cloudbeaver.service.security/plugin.xml b/server/bundles/io.cloudbeaver.service.security/plugin.xml index e9ef310594..e4b77aab29 100644 --- a/server/bundles/io.cloudbeaver.service.security/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.security/plugin.xml @@ -4,6 +4,7 @@ diff --git a/server/bundles/io.cloudbeaver.service.security/pom.xml b/server/bundles/io.cloudbeaver.service.security/pom.xml index 939a65afd6..d4661f08c4 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.62-SNAPSHOT + 1.0.67-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java index fce0269e82..60cf8809cc 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java @@ -68,7 +68,9 @@ public String validateLocalAuth(@NotNull DBRProgressMonitor monitor, throw new DBException("No user password provided"); } String clientPasswordHash = AuthPropertyEncryption.hash.encrypt(userName, clientPassword); - if (!storedPasswordHash.equals(clientPasswordHash)) { + // we also need to check a hash with lower case (CB-5833) + String clientPasswordHashLowerCase = AuthPropertyEncryption.hash.encrypt(userName.toLowerCase(), clientPassword); + if (!storedPasswordHash.equals(clientPasswordHash) && !clientPasswordHashLowerCase.equals(storedPasswordHash)) { throw new DBException("Invalid user name or password"); } 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 66e7c4e3da..55b1fb1d27 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 @@ -116,6 +116,9 @@ private boolean isSubjectExists(String subjectId) throws DBCException { /////////////////////////////////////////// // Users + /** + * Creates user. Saves user id in database in lower-case. + */ @Override public void createUser( @NotNull String userId, @@ -126,6 +129,7 @@ public void createUser( if (CommonUtils.isEmpty(userId)) { throw new DBCException("Empty user name is not allowed"); } + userId = userId.toLowerCase(); // creating new users only with lowercase if (isSubjectExists(userId)) { throw new DBCException("User or team '" + userId + "' already exists"); } @@ -140,6 +144,9 @@ public void createUser( } } + /** + * Creates user. Saves user id in database as it is. + */ public void createUser( @NotNull Connection dbCon, @NotNull String userId, @@ -838,14 +845,19 @@ public void setUserCredentials( @NotNull String authProviderId, @NotNull Map credentials ) throws DBException { - var existUserByCredentials = findUserByCredentials(getAuthProvider(authProviderId), credentials); + var existUserByCredentials = findUserByCredentials(getAuthProvider(authProviderId), credentials, false); if (existUserByCredentials != null && !existUserByCredentials.equals(userId)) { throw new DBException("Another user is already linked to the specified credentials"); } List transformedCredentials; WebAuthProviderDescriptor authProvider = getAuthProvider(authProviderId); + if (authProvider.isCaseInsensitive() && !isSubjectExists(userId) && isSubjectExists(userId.toLowerCase())) { + log.warn("User with id '" + userId + "' not found, credentials will be set for the user: " + userId.toLowerCase()); + userId = userId.toLowerCase(); + } try { SMAuthCredentialsProfile credProfile = getCredentialProfileByParameters(authProvider, credentials.keySet()); + String finalUserId = userId; transformedCredentials = credentials.entrySet().stream().map(cred -> { String propertyName = cred.getKey(); AuthPropertyDescriptor property = credProfile.getCredentialParameter(propertyName); @@ -853,7 +865,7 @@ public void setUserCredentials( return null; } String encodedValue = CommonUtils.toString(cred.getValue()); - encodedValue = property.getEncryption().encrypt(userId, encodedValue); + encodedValue = property.getEncryption().encrypt(finalUserId, encodedValue); return new String[]{propertyName, encodedValue}; }).toList(); } catch (Exception e) { @@ -906,20 +918,42 @@ public void deleteUserCredentials(@NotNull String userId, @NotNull String authPr } @Nullable - private String findUserByCredentials(WebAuthProviderDescriptor authProvider, Map authParameters) throws DBCException { - Map identCredentials = new LinkedHashMap<>(); + private String findUserByCredentials( + @NotNull WebAuthProviderDescriptor authProvider, + @NotNull Map authParameters, + boolean onlyActive // throws exception if user is inactive + ) throws DBException { + String userId = findUserByCredentials(authProvider, authParameters, onlyActive, false); + if (userId == null && authProvider.isCaseInsensitive()) { + // try to find user id with lower case is auth provider is case-insensitive + return findUserByCredentials(authProvider, authParameters, onlyActive, true); + } + return userId; + } + + @Nullable + private String findUserByCredentials( + @NotNull WebAuthProviderDescriptor authProvider, + @NotNull Map authParameters, + boolean onlyActive, + boolean isCaseInsensitive + ) throws DBCException { + Map identCredentials = new LinkedHashMap<>(); String[] propNames = authParameters.keySet().toArray(new String[0]); for (AuthPropertyDescriptor prop : authProvider.getCredentialParameters(propNames)) { if (prop.isIdentifying()) { String propId = CommonUtils.toString(prop.getId()); - Object paramValue = authParameters.get(propId); - if (paramValue == null) { + if (authParameters.get(propId) == null) { throw new DBCException("Authentication parameter '" + prop.getId() + "' is missing"); } if (prop.getEncryption() == AuthPropertyEncryption.hash) { throw new DBCException("Hash encryption can't be used in identifying credentials"); } - identCredentials.put(propId, paramValue); + String paramValue = CommonUtils.toString(authParameters.get(propId)); + identCredentials.put( + propId, + isCaseInsensitive ? paramValue.toLowerCase() : paramValue + ); } } if (identCredentials.isEmpty()) { @@ -943,9 +977,9 @@ private String findUserByCredentials(WebAuthProviderDescriptor authProvider, Map try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(sql.toString()))) { dbStat.setString(1, authProvider.getId()); int param = 2; - for (Map.Entry credEntry : identCredentials.entrySet()) { + for (Map.Entry credEntry : identCredentials.entrySet()) { dbStat.setString(param++, credEntry.getKey()); - dbStat.setString(param++, CommonUtils.toString(credEntry.getValue())); + dbStat.setString(param++, credEntry.getValue()); } try (ResultSet dbResult = dbStat.executeQuery()) { @@ -961,7 +995,7 @@ private String findUserByCredentials(WebAuthProviderDescriptor authProvider, Map } } - if (userId != null && !isActive) { + if (userId != null && onlyActive && !isActive) { throw new DBCException("User account is locked"); } @@ -976,6 +1010,15 @@ private String findUserByCredentials(WebAuthProviderDescriptor authProvider, Map @Override public Map getUserCredentials(String userId, String authProviderId) throws DBCException { WebAuthProviderDescriptor authProvider = getAuthProvider(authProviderId); + Map creds = getUserCredentials(authProvider, userId); + if (creds.isEmpty() && authProvider.isCaseInsensitive()) { + return getUserCredentials(authProvider, userId.toLowerCase()); + } + return creds; + } + + @NotNull + private Map getUserCredentials(WebAuthProviderDescriptor authProvider, String userId) throws DBCException { try (Connection dbCon = database.openConnection()) { try (PreparedStatement dbStat = dbCon.prepareStatement( database.normalizeTableNames("SELECT CRED_ID,CRED_VALUE FROM {table_prefix}CB_USER_CREDENTIALS\n" + @@ -986,7 +1029,6 @@ public Map getUserCredentials(String userId, String authProvider try (ResultSet dbResult = dbStat.executeQuery()) { Map credentials = new LinkedHashMap<>(); - while (dbResult.next()) { credentials.put(dbResult.getString(1), dbResult.getString(2)); } @@ -1178,6 +1220,7 @@ public void createTeam(String teamId, String name, String description, String gr if (CommonUtils.isEmpty(teamId)) { throw new DBCException("Empty team name is not allowed"); } + teamId = teamId.toLowerCase(); if (isSubjectExists(teamId)) { throw new DBCException("User or team '" + teamId + "' already exists"); } @@ -2082,7 +2125,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 @@ -2405,7 +2448,7 @@ private String findOrCreateExternalUserByCredentials( ) throws DBException { SMAuthProvider smAuthProviderInstance = authProvider.getInstance(); - String userId = findUserByCredentials(authProvider, userCredentials); + String userId = findUserByCredentials(authProvider, userCredentials, true); String userIdFromCredentials; try { userIdFromCredentials = smAuthProviderInstance.validateLocalAuth(progressMonitor, this, providerConfig, userCredentials, null); @@ -2423,13 +2466,20 @@ private String findOrCreateExternalUserByCredentials( return null; } - userId = userIdFromCredentials; + userId = authProvider.isCaseInsensitive() ? userIdFromCredentials.toLowerCase() : userIdFromCredentials; if (!isSubjectExists(userId)) { - createUser(userId, - Map.of(), - true, - resolveUserAuthRole(null, authRole) - ); + log.debug("Create user: " + userId); + try (Connection dbCon = database.openConnection()) { + createUser( + dbCon, + userId, + Map.of(), + true, + resolveUserAuthRole(null, authRole) + ); + } catch (SQLException e) { + throw new DBException("Error saving user in database", e); + } } setUserCredentials(userId, authProvider.getId(), userCredentials); } else if (userId == null) { @@ -2604,7 +2654,7 @@ public SMAuthProviderDescriptor[] getAvailableAuthProviders() throws DBException Set customConfigurations = appConfiguration.getAuthCustomConfigurations(); List providers = WebAuthProviderRegistry.getInstance().getAuthProviders().stream() .filter(ap -> - !ap.isTrusted() && + !ap.isTrusted() && !ap.isAuthHidden() && appConfiguration.isAuthProviderEnabled(ap.getId()) && (!ap.isConfigurable() || hasProviderConfiguration(ap, customConfigurations))) .map(WebAuthProviderDescriptor::createDescriptorBean).toList(); @@ -3134,7 +3184,8 @@ private void deleteAuthSubject(Connection dbCon, String subjectId) throws SQLExc } } - private WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws DBCException { + @NotNull + protected WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws DBCException { WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(authProviderId); if (authProvider == null) { throw new DBCException("Auth provider not found: " + authProviderId); 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 67ced9d5b2..a3c6511de9 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,6 +18,7 @@ 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; @@ -66,7 +67,7 @@ public class CBDatabase extends InternalDB { public static final String SCHEMA_UPDATE_SQL_PATH = "db/cb_schema_update_"; private static final int LEGACY_SCHEMA_VERSION = 1; - private static final int CURRENT_SCHEMA_VERSION = 21; + private static final int CURRENT_SCHEMA_VERSION = 22; private static final String DEFAULT_DB_USER_NAME = "cb-data"; private static final String DEFAULT_DB_PWD_FILE = ".database-credentials.dat"; @@ -255,7 +256,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/db/CBDatabaseInitialData.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabaseInitialData.java index 17c42babb2..7f2879f4f6 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabaseInitialData.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/db/CBDatabaseInitialData.java @@ -17,6 +17,7 @@ package io.cloudbeaver.service.security.db; import org.jkiss.dbeaver.model.security.user.SMTeam; +import org.jkiss.utils.CommonUtils; import java.util.List; @@ -26,7 +27,7 @@ class CBDatabaseInitialData { private List teams; public String getAdminName() { - return adminName; + return CommonUtils.isEmpty(adminName) ? null : adminName.toLowerCase(); } public String getAdminPassword() { diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/ClearAuthAttemptInfoJob.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/ClearAuthAttemptInfoJob.java index 9c700dcca4..8eb28b5721 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/ClearAuthAttemptInfoJob.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/internal/ClearAuthAttemptInfoJob.java @@ -23,7 +23,7 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.CommonUtils; public class ClearAuthAttemptInfoJob extends AbstractJob { @@ -45,7 +45,7 @@ protected IStatus run(DBRProgressMonitor monitor) { securityController.clearOldAuthAttemptInfo(); schedule(CHECK_PERIOD); } catch (DBException e) { - log.error("Error to clear the auth attempt info: " + GeneralUtils.getRootCause(e).getMessage()); + log.error("Error to clear the auth attempt info: " + CommonUtils.getRootCause(e).getMessage()); // Check failed. Re-schedule after 5 seconds schedule(RETRY_PERIOD); } diff --git a/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF index ed1d71160f..98f18aea38 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.22.qualifier -Bundle-Release-Date: 20241007 +Bundle-Version: 1.0.27.qualifier +Bundle-Release-Date: 20241223 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 cbcfbb5d15..79085e1228 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.22-SNAPSHOT + 1.0.27-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/features/io.cloudbeaver.ce.drivers.feature/feature.xml b/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml index 77b67fe77c..e2baca9f05 100644 --- a/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml +++ b/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml b/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml index b2d7dbda84..61d079394c 100644 --- a/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml +++ b/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.ce.drivers.feature - 1.0.130-SNAPSHOT + 1.0.135-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.product.ce.feature/feature.xml b/server/features/io.cloudbeaver.product.ce.feature/feature.xml index 284670b63a..be1f9966ce 100644 --- a/server/features/io.cloudbeaver.product.ce.feature/feature.xml +++ b/server/features/io.cloudbeaver.product.ce.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.product.ce.feature/pom.xml b/server/features/io.cloudbeaver.product.ce.feature/pom.xml index 0b891199c2..1aef885696 100644 --- a/server/features/io.cloudbeaver.product.ce.feature/pom.xml +++ b/server/features/io.cloudbeaver.product.ce.feature/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.product.ce.feature - 24.2.2-SNAPSHOT + 24.3.1-SNAPSHOT eclipse-feature @@ -19,7 +19,6 @@ org.apache.maven.plugins maven-resources-plugin - 2.6 process-product-info diff --git a/server/features/io.cloudbeaver.server.feature/feature.xml b/server/features/io.cloudbeaver.server.feature/feature.xml index 159f75d39a..c9c4d37b8c 100644 --- a/server/features/io.cloudbeaver.server.feature/feature.xml +++ b/server/features/io.cloudbeaver.server.feature/feature.xml @@ -2,7 +2,7 @@ @@ -15,7 +15,6 @@ - diff --git a/server/features/io.cloudbeaver.server.feature/pom.xml b/server/features/io.cloudbeaver.server.feature/pom.xml index 3b8fb58670..c95da44606 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.2.2-SNAPSHOT + 24.3.1-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.ws.feature/feature.xml b/server/features/io.cloudbeaver.ws.feature/feature.xml index 714ab2baf7..611ffa44bb 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 36705b3d43..65a3a13ca2 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.60-SNAPSHOT + 1.0.65-SNAPSHOT eclipse-feature diff --git a/server/pom.xml b/server/pom.xml index 4337a2507b..9a41bbf8ec 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -19,19 +19,28 @@ CloudBeaver CE - 24.2.2 + 24.3.1 bundles features - drivers - test - - - product + + + + full-build + !plain-api-server + + drivers + + product + test + + + + diff --git a/server/product/web-server/CloudbeaverServer.product b/server/product/web-server/CloudbeaverServer.product index 179c684197..0d1dea135f 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 ef175dce7d..1db2b8c995 100644 --- a/server/product/web-server/pom.xml +++ b/server/product/web-server/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../ - 24.2.2-SNAPSHOT + 24.3.1-SNAPSHOT web-server eclipse-repository Cloudbeaver Server Product diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java index d642b44e34..e2bebd4db8 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java @@ -16,15 +16,19 @@ */ package io.cloudbeaver.test.platform; +import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.auth.provider.rp.RPAuthProvider; import io.cloudbeaver.test.WebGQLClient; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.auth.SMAuthStatus; import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.utils.SecurityUtils; import org.junit.Assert; import org.junit.Test; import java.util.List; import java.util.Map; +import java.util.Set; public class AuthenticationTest { private static final String GQL_OPEN_SESSION = """ @@ -39,6 +43,12 @@ mutation openSession($defaultLocale: String) { userId } }"""; + private static final String GQL_AUTH_LOGOUT = """ + query authLogoutExtended($provider: ID, $configuration: ID) { + result: authLogoutExtended(provider: $provider, configuration: $configuration) { + redirectLinks + } + }"""; @Test public void testLoginUser() throws Exception { @@ -47,6 +57,34 @@ public void testLoginUser() throws Exception { Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); } + + @Test + public void testLoginUserWithCamelCase() throws Exception { + WebGQLClient client = CEServerTestSuite.createClient(); + for (String userId : Set.of("Test", "tESt", "tesT", "TEST")) { + Map credsWithCamelCase = getUserCredentials(userId); + // authenticating with user + Map authInfo = CEServerTestSuite.authenticateTestUser(client, credsWithCamelCase); + Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); + Map activeUser = client.sendQuery(GQL_ACTIVE_USER, null); + Assert.assertEquals(userId.toLowerCase(), JSONUtils.getString(activeUser, "userId")); + // making logout + client.sendQuery(GQL_AUTH_LOGOUT, Map.of("provider", "local")); + + activeUser = client.sendQuery(GQL_ACTIVE_USER, null); + Assert.assertNotEquals(userId.toLowerCase(), JSONUtils.getString(activeUser, "userId")); + } + } + + @NotNull + private Map getUserCredentials(@NotNull String userId) throws Exception { + return Map.of( + LocalAuthProvider.CRED_USER, userId, + LocalAuthProvider.CRED_PASSWORD, SecurityUtils.makeDigest("test") + ); + } + + @Test public void testReverseProxyAnonymousModeLogin() throws Exception { WebGQLClient client = CEServerTestSuite.createClient(); diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java index 3ca615b5ea..b133f1baa5 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java @@ -101,11 +101,18 @@ public static WebGQLClient createClient(@NotNull HttpClient httpClient) { } public static Map authenticateTestUser(@NotNull WebGQLClient client) throws Exception { + return authenticateTestUser(client, TEST_CREDENTIALS); + } + + public static Map authenticateTestUser( + @NotNull WebGQLClient client, + @NotNull Map credentials + ) throws Exception { return client.sendQuery( WebGQLClient.GQL_AUTHENTICATE, Map.of( "provider", LocalAuthProvider.PROVIDER_ID, - "credentials", TEST_CREDENTIALS + "credentials", credentials ) ); } 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..0ebcff7979 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts @@ -5,7 +5,9 @@ * 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 { ILoadableState } from '@cloudbeaver/core-utils'; + +import type { IRouteParams } from './IRouteParams.js'; export enum AdministrationItemType { Default, @@ -84,6 +86,7 @@ export interface IAdministrationItemOptions { replace?: IAdministrationItemReplaceOptions; defaultSub?: string; defaultParam?: string; + getLoader?: () => ILoadableState[] | ILoadableState; getDrawerComponent: () => AdministrationItemDrawerComponent; getContentComponent: () => AdministrationItemContentComponent; onLoad?: AdministrationItemEvent; 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 32f949232b..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'; @@ -228,7 +228,7 @@ export class AdministrationScreenService { } async handleCanDeActivate(fromState: RouterState, toState: RouterState): Promise { - if (!fromState.params.item) { + if (!fromState.params['item']) { return true; } @@ -243,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 index e109b98b93..fb7091f711 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreenService.ts @@ -9,8 +9,8 @@ import { Dependency, injectable } from '@cloudbeaver/core-di'; import { ServerConfigResource } from '@cloudbeaver/core-root'; import { ScreenService } from '@cloudbeaver/core-routing'; -import { AdministrationScreenService } from '../AdministrationScreenService'; -import { ConfigurationWizardService } from './ConfigurationWizardService'; +import { AdministrationScreenService } from '../AdministrationScreenService.js'; +import { ConfigurationWizardService } from './ConfigurationWizardService.js'; @injectable() export class ConfigurationWizardScreenService extends Dependency { diff --git a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts index 71133643ad..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 { @@ -147,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/manifest.ts b/webapp/packages/core-administration/src/manifest.ts index 66715a5eb5..af2c95191e 100644 --- a/webapp/packages/core-administration/src/manifest.ts +++ b/webapp/packages/core-administration/src/manifest.ts @@ -13,11 +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('./AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreenService').then(m => m.ConfigurationWizardScreenService), - () => 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-website/src/index.ts b/webapp/packages/core-authentication/src/ADMIN_USERNAME_MIN_LENGTH.ts similarity index 78% rename from webapp/packages/core-website/src/index.ts rename to webapp/packages/core-authentication/src/ADMIN_USERNAME_MIN_LENGTH.ts index 83c0a3d7e6..fff36b1d12 100644 --- a/webapp/packages/core-website/src/index.ts +++ b/webapp/packages/core-authentication/src/ADMIN_USERNAME_MIN_LENGTH.ts @@ -5,5 +5,5 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -export * from './WebsiteLinks'; -export * from './manifest'; + +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 +32,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 +70,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 ea81f6d1b0..f129e6e0a9 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -23,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'); @@ -54,6 +60,7 @@ export const UsersResourceNewUsers = resourceKeyListAlias('@users-resource/new-u interface UserCreateOptions { userId: string; authRole?: string; + enabled?: boolean; } @injectable() @@ -131,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), }); @@ -253,7 +255,6 @@ export class UsersResource extends CachedMapResource) { 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/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 18b5ed7661..9026444e25 100644 --- a/webapp/packages/core-blocks/src/BlocksLocaleService.ts +++ b/webapp/packages/core-blocks/src/BlocksLocaleService.ts @@ -14,24 +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')).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-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 ?