mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts: # api/services/feature_service.py # api/tests/unit_tests/controllers/console/workspace/test_members.py # api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py # api/tests/unit_tests/services/test_system_feature_service_knowledge_fs.py # pnpm-lock.yaml
This commit is contained in:
commit
cd7e1be7cb
@ -12,7 +12,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "lts"
|
||||
"version": "24.20.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-extra/features/npm-package:1": {
|
||||
"package": "typescript",
|
||||
@ -46,4 +46,4 @@
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
}
|
||||
}
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -28,6 +28,7 @@
|
||||
|
||||
# E2E
|
||||
/e2e/ @lyzno1
|
||||
/.github/workflows/web-e2e.yml @lyzno1
|
||||
|
||||
# Backend (default owner, more specific rules below will override)
|
||||
/api/ @QuantumGhost
|
||||
|
||||
2
.github/actions/setup-web/action.yml
vendored
2
.github/actions/setup-web/action.yml
vendored
@ -11,6 +11,6 @@ runs:
|
||||
- name: Setup Vite+
|
||||
uses: voidzero-dev/setup-vp@1b32467adbe183473499fd9d5d372c3ed9641754 # v1.18.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
node-version-file: package.json
|
||||
cache: true
|
||||
run-install: true
|
||||
|
||||
1
.github/labeler.yml
vendored
1
.github/labeler.yml
vendored
@ -6,7 +6,6 @@ web:
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- '.nvmrc'
|
||||
|
||||
e2e:
|
||||
- changed-files:
|
||||
|
||||
3
.github/workflows/main-ci.yml
vendored
3
.github/workflows/main-ci.yml
vendored
@ -82,7 +82,6 @@ jobs:
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'lint.config.ts'
|
||||
- '.npmrc'
|
||||
- '.nvmrc'
|
||||
- '.github/workflows/cli-tests.yml'
|
||||
- '.github/actions/setup-web/**'
|
||||
web:
|
||||
@ -91,7 +90,6 @@ jobs:
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- '.nvmrc'
|
||||
- '.github/workflows/main-ci.yml'
|
||||
- '.github/workflows/web-tests.yml'
|
||||
- '.github/actions/setup-web/**'
|
||||
@ -105,7 +103,6 @@ jobs:
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- '.nvmrc'
|
||||
- 'docker/docker-compose.middleware.yaml'
|
||||
- 'docker/envs/middleware.env.example'
|
||||
- '.github/workflows/web-e2e.yml'
|
||||
|
||||
1
.github/workflows/post-merge.yml
vendored
1
.github/workflows/post-merge.yml
vendored
@ -39,7 +39,6 @@ jobs:
|
||||
- 'e2e/tsx-register.js'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.nvmrc'
|
||||
- '.github/workflows/post-merge.yml'
|
||||
- '.github/workflows/web-e2e.yml'
|
||||
- '.github/actions/setup-web/**'
|
||||
|
||||
2
.github/workflows/style.yml
vendored
2
.github/workflows/style.yml
vendored
@ -114,7 +114,6 @@ jobs:
|
||||
pnpm-workspace.yaml
|
||||
knip.config.ts
|
||||
scripts/check-web-production-unused-after-knip-fix.mjs
|
||||
.nvmrc
|
||||
.github/workflows/style.yml
|
||||
.github/actions/setup-web/**
|
||||
|
||||
@ -170,7 +169,6 @@ jobs:
|
||||
package.json
|
||||
pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
.nvmrc
|
||||
vite.config.ts
|
||||
lint.config.ts
|
||||
eslint.config.mjs
|
||||
|
||||
2
.github/workflows/tool-test-sdks.yaml
vendored
2
.github/workflows/tool-test-sdks.yaml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
node-version-file: package.json
|
||||
cache: ''
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
|
||||
|
||||
2
.github/workflows/translate-i18n-claude.yml
vendored
2
.github/workflows/translate-i18n-claude.yml
vendored
@ -162,7 +162,7 @@ jobs:
|
||||
|
||||
- name: Run Claude Code for Translation Sync
|
||||
if: steps.context.outputs.CHANGED_FILES != ''
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1.0.210
|
||||
uses: anthropics/claude-code-action@833fb0f8c9f6686b33d963a8bae0a94f4936ab2a # v1.0.211
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
278
.github/workflows/web-e2e.yml
vendored
278
.github/workflows/web-e2e.yml
vendored
@ -12,13 +12,196 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
core-build:
|
||||
name: Prepare Core E2E Web Build
|
||||
if: ${{ !inputs.run-external-runtime }}
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup web dependencies
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
- name: Run E2E support unit tests
|
||||
working-directory: ./e2e
|
||||
run: vp run test:unit
|
||||
|
||||
- name: Build production Web app
|
||||
working-directory: ./e2e
|
||||
env:
|
||||
E2E_FORCE_WEB_BUILD: '1'
|
||||
run: vp run e2e:web:build
|
||||
|
||||
- name: Package Web build
|
||||
run: >-
|
||||
tar -cf e2e-web-build.tar -C web
|
||||
.next/BUILD_ID
|
||||
.next/e2e-web-build.sha256
|
||||
.next/standalone
|
||||
.next/static
|
||||
|
||||
- name: Upload Web build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: core-e2e-web-build
|
||||
path: e2e-web-build.tar
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 7
|
||||
|
||||
chromium-full:
|
||||
name: Chromium Full (${{ matrix.shard }}/3)
|
||||
if: ${{ !inputs.run-external-runtime }}
|
||||
needs: core-build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3]
|
||||
env:
|
||||
E2E_ADMIN_EMAIL: e2e-admin@example.com
|
||||
E2E_ADMIN_NAME: E2E Admin
|
||||
E2E_ADMIN_PASSWORD: E2eAdmin12345
|
||||
E2E_INIT_PASSWORD: E2eInit12345
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup web dependencies
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: '3.12'
|
||||
cache-dependency-glob: api/uv.lock
|
||||
|
||||
- name: Install API dependencies
|
||||
run: uv sync --project api --dev
|
||||
|
||||
- name: Download Web build
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: core-e2e-web-build
|
||||
|
||||
- name: Extract Web build
|
||||
run: tar -xf e2e-web-build.tar -C web
|
||||
|
||||
- name: Install Chromium
|
||||
timeout-minutes: 15
|
||||
working-directory: ./e2e
|
||||
run: vp run e2e:install:ci:chromium
|
||||
|
||||
- name: Run Chromium full shard
|
||||
working-directory: ./e2e
|
||||
run: vp run e2e:full -- --shard ${{ matrix.shard }}/3
|
||||
|
||||
- name: Upload Cucumber report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: cucumber-report-chromium-${{ matrix.shard }}
|
||||
path: e2e/cucumber-report
|
||||
if-no-files-found: ignore
|
||||
overwrite: true
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload E2E logs
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-logs-chromium-${{ matrix.shard }}
|
||||
path: e2e/.logs/*.log
|
||||
if-no-files-found: ignore
|
||||
include-hidden-files: true
|
||||
overwrite: true
|
||||
retention-days: 7
|
||||
|
||||
webkit-smoke:
|
||||
name: WebKit Browser Smoke
|
||||
if: ${{ !inputs.run-external-runtime }}
|
||||
needs: core-build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
E2E_ADMIN_EMAIL: e2e-admin@example.com
|
||||
E2E_ADMIN_NAME: E2E Admin
|
||||
E2E_ADMIN_PASSWORD: E2eAdmin12345
|
||||
E2E_BROWSER: webkit
|
||||
E2E_INIT_PASSWORD: E2eInit12345
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup web dependencies
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: '3.12'
|
||||
cache-dependency-glob: api/uv.lock
|
||||
|
||||
- name: Install API dependencies
|
||||
run: uv sync --project api --dev
|
||||
|
||||
- name: Download Web build
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: core-e2e-web-build
|
||||
|
||||
- name: Extract Web build
|
||||
run: tar -xf e2e-web-build.tar -C web
|
||||
|
||||
- name: Install WebKit
|
||||
timeout-minutes: 15
|
||||
working-directory: ./e2e
|
||||
run: vp run e2e:install:ci:webkit
|
||||
|
||||
- name: Run WebKit browser smoke
|
||||
working-directory: ./e2e
|
||||
run: vp run e2e:full -- --tags '@browser-smoke'
|
||||
|
||||
- name: Upload Cucumber report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: cucumber-report-webkit
|
||||
path: e2e/cucumber-report
|
||||
if-no-files-found: ignore
|
||||
overwrite: true
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload E2E logs
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-logs-webkit
|
||||
path: e2e/.logs/*.log
|
||||
if-no-files-found: ignore
|
||||
include-hidden-files: true
|
||||
overwrite: true
|
||||
retention-days: 7
|
||||
|
||||
external-runtime:
|
||||
name: Web Full-Stack E2E
|
||||
if: ${{ inputs.run-external-runtime }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@ -41,82 +224,12 @@ jobs:
|
||||
- name: Install API dependencies
|
||||
run: uv sync --project api --dev
|
||||
|
||||
- name: Run E2E support unit tests
|
||||
if: ${{ !inputs.run-external-runtime }}
|
||||
working-directory: ./e2e
|
||||
run: vp run test:unit
|
||||
|
||||
- name: Install Playwright browsers for core E2E
|
||||
if: ${{ !inputs.run-external-runtime }}
|
||||
timeout-minutes: 15
|
||||
working-directory: ./e2e
|
||||
run: vp run e2e:install:ci
|
||||
|
||||
- name: Install Chromium for external runtime E2E
|
||||
if: ${{ inputs.run-external-runtime }}
|
||||
- name: Install Chromium
|
||||
timeout-minutes: 15
|
||||
working-directory: ./e2e
|
||||
run: vp run e2e:install:ci:chromium
|
||||
|
||||
- name: Run isolated source-api and built-web Cucumber E2E tests
|
||||
if: ${{ !inputs.run-external-runtime }}
|
||||
working-directory: ./e2e
|
||||
env:
|
||||
E2E_ADMIN_EMAIL: e2e-admin@example.com
|
||||
E2E_ADMIN_NAME: E2E Admin
|
||||
E2E_ADMIN_PASSWORD: E2eAdmin12345
|
||||
E2E_FORCE_WEB_BUILD: '1'
|
||||
E2E_INIT_PASSWORD: E2eInit12345
|
||||
run: vp run e2e:full
|
||||
|
||||
- name: Preserve Chromium E2E report and logs
|
||||
if: ${{ !cancelled() && !inputs.run-external-runtime }}
|
||||
run: |
|
||||
if [[ -d e2e/cucumber-report ]]; then
|
||||
mv e2e/cucumber-report e2e/cucumber-report-non-external
|
||||
fi
|
||||
if [[ -d e2e/.logs ]]; then
|
||||
mv e2e/.logs e2e/.logs-non-external
|
||||
fi
|
||||
|
||||
- name: Run WebKit keyboard and browser smoke tests
|
||||
if: ${{ !inputs.run-external-runtime }}
|
||||
working-directory: ./e2e
|
||||
env:
|
||||
E2E_ADMIN_EMAIL: e2e-admin@example.com
|
||||
E2E_ADMIN_NAME: E2E Admin
|
||||
E2E_ADMIN_PASSWORD: E2eAdmin12345
|
||||
E2E_BROWSER: webkit
|
||||
E2E_INIT_PASSWORD: E2eInit12345
|
||||
run: |
|
||||
teardown_webkit_smoke() {
|
||||
local run_status=$?
|
||||
trap - EXIT
|
||||
if ! vp run e2e:middleware:down; then
|
||||
echo "::error title=E2E teardown failed::WebKit smoke middleware did not shut down cleanly."
|
||||
if [[ "$run_status" -eq 0 ]]; then
|
||||
run_status=1
|
||||
fi
|
||||
fi
|
||||
exit "$run_status"
|
||||
}
|
||||
|
||||
trap teardown_webkit_smoke EXIT
|
||||
vp run e2e:middleware:up
|
||||
vp run e2e -- --tags '@browser-smoke'
|
||||
|
||||
- name: Preserve WebKit E2E report and logs
|
||||
if: ${{ !cancelled() && !inputs.run-external-runtime }}
|
||||
run: |
|
||||
if [[ -d e2e/cucumber-report ]]; then
|
||||
mv e2e/cucumber-report e2e/cucumber-report-webkit
|
||||
fi
|
||||
if [[ -d e2e/.logs ]]; then
|
||||
mv e2e/.logs e2e/.logs-webkit
|
||||
fi
|
||||
|
||||
- name: Run prepared and external runtime E2E tests
|
||||
if: ${{ inputs.run-external-runtime }}
|
||||
working-directory: ./e2e
|
||||
env:
|
||||
E2E_ADMIN_EMAIL: e2e-admin@example.com
|
||||
@ -149,10 +262,8 @@ jobs:
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: cucumber-report
|
||||
path: |
|
||||
e2e/cucumber-report
|
||||
e2e/cucumber-report-non-external
|
||||
e2e/cucumber-report-webkit
|
||||
path: e2e/cucumber-report
|
||||
overwrite: true
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload E2E logs
|
||||
@ -160,18 +271,17 @@ jobs:
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-logs
|
||||
path: |
|
||||
e2e/.logs/*.log
|
||||
e2e/.logs-non-external/*.log
|
||||
e2e/.logs-webkit/*.log
|
||||
path: e2e/.logs/*.log
|
||||
include-hidden-files: true
|
||||
overwrite: true
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload E2E seed report
|
||||
if: ${{ !cancelled() && inputs.run-external-runtime }}
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-seed-report
|
||||
path: e2e/seed-report
|
||||
if-no-files-found: ignore
|
||||
overwrite: true
|
||||
retention-days: 7
|
||||
|
||||
@ -7,3 +7,9 @@ Dify is an open-source platform for building LLM applications, agentic workflows
|
||||
- Run backend commands through `uv run --project api <command>`.
|
||||
- Backend integration tests are CI-only and are not expected to run locally.
|
||||
- Keep `docker/.env.example` limited to variables required for a default Docker Compose deployment to start. Put optional and provider-specific settings in the matching `docker/envs/*.env.example` file; `docker/.env` overrides those service-specific env files.
|
||||
|
||||
## Frontend Workflow
|
||||
|
||||
- For truncated text disclosure and native `title` decisions, follow [Truncated Text Disclosure].
|
||||
|
||||
[Truncated Text Disclosure]: web/docs/truncated-text-disclosure.md
|
||||
|
||||
@ -65,7 +65,7 @@ ignore_imports =
|
||||
core.app.workflow.layers.persistence -> services.workflow.inspector_events
|
||||
core.datasource.datasource_manager -> services.datasource_provider_service
|
||||
core.helper.credential_utils -> services.enterprise.plugin_manager_service
|
||||
core.helper.credential_utils -> services.feature_service
|
||||
core.helper.credential_utils -> services.system_feature_service
|
||||
core.indexing_runner -> services.vector_space_admission_service
|
||||
core.mcp.auth_client -> services
|
||||
core.provider_manager -> services.credential_permission_service
|
||||
@ -99,9 +99,9 @@ ignore_imports =
|
||||
core.workflow.nodes.agent_v2.workspace_retirement_layer -> tasks.collect_agent_resources_task
|
||||
libs.device_flow_security -> controllers.openapi._models
|
||||
libs.device_flow_security -> services.entities.feature_entities
|
||||
libs.device_flow_security -> services.feature_service
|
||||
libs.device_flow_security -> services.system_feature_service
|
||||
libs.email_i18n -> services.entities.feature_entities
|
||||
libs.email_i18n -> services.feature_service
|
||||
libs.email_i18n -> services.system_feature_service
|
||||
libs.external_api -> core
|
||||
libs.external_api -> core.errors.error
|
||||
libs.external_api -> extensions.ext_logging
|
||||
@ -117,7 +117,6 @@ ignore_imports =
|
||||
libs.oauth_bearer -> models
|
||||
libs.rsa -> extensions.ext_storage
|
||||
libs.workspace_permission -> services.enterprise.enterprise_service
|
||||
libs.workspace_permission -> services.feature_service
|
||||
services.account_service -> controllers
|
||||
services.account_service -> controllers.console.error
|
||||
services.app_generate_service -> controllers.console.app.workflow
|
||||
@ -387,6 +386,37 @@ forbidden_modules =
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:inner-mail-service-boundary]
|
||||
name = Inner mail application service is framework and implementation neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
services.inner_mail_service
|
||||
forbidden_modules =
|
||||
configs
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
repositories
|
||||
sqlalchemy
|
||||
tasks
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:web-passport-service-boundary]
|
||||
name = Web passport application service is framework and persistence neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
services.web_passport_service
|
||||
forbidden_modules =
|
||||
configs
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
repositories
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:account-activation-service-boundary]
|
||||
name = Account activation application service is framework and persistence neutral
|
||||
type = forbidden
|
||||
|
||||
@ -53,15 +53,14 @@ WORKDIR /app/api
|
||||
|
||||
# Create non-root user
|
||||
ARG dify_uid=1001
|
||||
ARG NODE_MAJOR=22
|
||||
ARG NODE_PACKAGE_VERSION=22.21.0-1nodesource1
|
||||
ARG NODE_PACKAGE_VERSION=24.20.0-1nodesource1
|
||||
ARG NODESOURCE_KEY_FPR=6F71F525282841EEDAF851B42F59B5F99B1BE0B4
|
||||
RUN groupadd -r -g ${dify_uid} dify && \
|
||||
useradd -r -u ${dify_uid} -g ${dify_uid} -s /bin/bash dify && \
|
||||
chown -R dify:dify /app
|
||||
|
||||
RUN \
|
||||
apt-get update \
|
||||
RUN NODE_MAJOR="${NODE_PACKAGE_VERSION%%.*}" \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
|
||||
@ -7,7 +7,7 @@ from werkzeug.exceptions import NotFound
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.common.session import with_session
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
@ -527,8 +527,7 @@ class AgentComposerApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_EDIT)
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
|
||||
@ -7,7 +7,6 @@ from pydantic import AliasChoices, BaseModel, Field, field_validator
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.schema import (
|
||||
query_params_from_model,
|
||||
query_params_from_request,
|
||||
@ -33,6 +32,7 @@ from controllers.console.app.app import (
|
||||
from controllers.console.app.app import (
|
||||
UpdateAppPayload as GenericUpdateAppPayload,
|
||||
)
|
||||
from controllers.console.app.wraps import agent_manage_required_for_agent_app
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
@ -79,11 +79,9 @@ from services.agent.observability_service import (
|
||||
)
|
||||
from services.agent.roster_service import AgentRosterService
|
||||
from services.app_service import AgentAppPublicationCounts, AppListParams, AppService, CreateAppParams
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.entities.agent_entities import ComposerSavePayload, RosterListQuery
|
||||
from services.feature_service import FeatureService
|
||||
from tasks.initialize_created_app_rbac_access_task import initialize_created_app_rbac_access_task
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
AgentPublicationStatus = Literal["published", "drafts"]
|
||||
|
||||
@ -390,7 +388,7 @@ def _serialize_agent_app_detail(
|
||||
"""
|
||||
|
||||
app_model = AppService().get_app(app_model, session=session)
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
if SystemFeatureService.is_webapp_auth_enabled():
|
||||
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
|
||||
app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined]
|
||||
|
||||
@ -687,15 +685,6 @@ class AgentAppListApi(Resource):
|
||||
)
|
||||
|
||||
app = AppService().create_app(current_tenant_id, params, current_user, session=session)
|
||||
if dify_config.RBAC_ENABLED:
|
||||
enterprise_rbac_service.RBACService.AppAccess.replace_whitelist(
|
||||
current_tenant_id,
|
||||
current_user.id,
|
||||
str(app.id),
|
||||
enterprise_rbac_service.ReplaceMemberBindings(automatic_include_workspace_members=True),
|
||||
)
|
||||
initialize_created_app_rbac_access_task.delay(current_tenant_id, current_user.id, app_id=app.id)
|
||||
|
||||
return _serialize_agent_app_detail(session, app, current_user=current_user), 201
|
||||
|
||||
|
||||
@ -995,8 +984,7 @@ class AgentApiStatusApi(Resource):
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
@model_validate(AgentApiStatusPayload)
|
||||
@ -1014,10 +1002,9 @@ class AgentApiKeyListApi(BaseApiKeyListResource):
|
||||
token_prefix = "app-"
|
||||
|
||||
@console_ns.response(200, "Agent service API keys", console_ns.models[ApiKeyList.__name__])
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@with_current_tenant_id
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, tenant_id: str, agent_id: UUID) -> dict[str, object]:
|
||||
app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
@ -1027,8 +1014,7 @@ class AgentApiKeyListApi(BaseApiKeyListResource):
|
||||
@console_ns.response(400, "Maximum keys exceeded")
|
||||
@with_current_tenant_id
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, agent_id: UUID) -> tuple[dict[str, object], int]:
|
||||
app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
@ -1047,8 +1033,7 @@ class AgentApiKeyApi(BaseApiKeyResource):
|
||||
@console_ns.response(204, "Agent service API key deleted")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.AGENT_MANAGE, resource_required=False)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@with_session
|
||||
def delete(
|
||||
self,
|
||||
|
||||
@ -23,7 +23,7 @@ from controllers.common.schema import (
|
||||
)
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.error import AppNotFoundError
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
@ -153,7 +153,7 @@ class AgentAppSandboxInfoResource(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
@ -183,7 +183,7 @@ class AgentAppSandboxListResource(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
@ -214,7 +214,7 @@ class AgentAppSandboxReadResource(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
@ -245,7 +245,7 @@ class AgentAppSandboxDownloadResource(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
@model_validate(AgentSandboxDownloadPayload)
|
||||
|
||||
@ -77,7 +77,7 @@ from services.entities.knowledge_entities.knowledge_entities import (
|
||||
WeightVectorSetting,
|
||||
)
|
||||
from services.errors.account import NoPermissionError
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
from tasks.initialize_created_app_rbac_access_task import initialize_created_app_rbac_access_task
|
||||
|
||||
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
|
||||
@ -516,7 +516,7 @@ class AppImportResponse(ResponseModel):
|
||||
|
||||
|
||||
def _enrich_app_list_items(session: Session, *, apps: Sequence[App], tenant_id: str) -> None:
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
if SystemFeatureService.is_webapp_auth_enabled():
|
||||
app_ids = [str(app.id) for app in apps]
|
||||
res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids)
|
||||
if len(res) != len(app_ids):
|
||||
@ -877,7 +877,7 @@ class AppApi(Resource):
|
||||
|
||||
app_model = app_service.get_app(app_model, session=session)
|
||||
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
if SystemFeatureService.is_webapp_auth_enabled():
|
||||
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
|
||||
app_model.access_mode = app_setting.access_mode
|
||||
|
||||
@ -1002,7 +1002,7 @@ class AppCopyApi(Resource):
|
||||
session.commit()
|
||||
|
||||
# Inherit web app permission from original app
|
||||
if result.app_id and FeatureService.get_system_features().webapp_auth.enabled:
|
||||
if result.app_id and SystemFeatureService.is_webapp_auth_enabled():
|
||||
try:
|
||||
# Get the original app's access mode
|
||||
original_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_model.id)
|
||||
@ -1173,8 +1173,7 @@ class AppSiteStatus(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@agent_manage_required_for_agent_app
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_RELEASE_AND_VERSION)
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
@model_validate(AppSiteStatusPayload)
|
||||
|
||||
@ -31,7 +31,7 @@ from services.app_dsl_service import (
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.entities.dsl_entities import CheckDependenciesResult, ImportStatus
|
||||
from services.errors.account import NoPermissionError
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
from .. import console_ns
|
||||
from .permission_keys import get_app_permission_keys
|
||||
@ -127,7 +127,7 @@ class AppImportApi(Resource):
|
||||
result.app_id,
|
||||
)
|
||||
|
||||
if result.app_id and FeatureService.get_system_features().webapp_auth.enabled:
|
||||
if result.app_id and SystemFeatureService.is_webapp_auth_enabled():
|
||||
# update web app setting as private
|
||||
EnterpriseService.WebAppAuth.update_app_access_mode(result.app_id, "private")
|
||||
# Return appropriate status code based on result
|
||||
|
||||
@ -24,7 +24,7 @@ from controllers.console.app.error import (
|
||||
ProviderNotInitializeError,
|
||||
ProviderQuotaExceededError,
|
||||
)
|
||||
from controllers.console.app.wraps import get_app_model, with_session
|
||||
from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model, with_session
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
@ -263,7 +263,7 @@ class AgentChatMessageApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_TEST_AND_RUN)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
@ -292,7 +292,7 @@ class AgentBuildChatFinalizeApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_TEST_AND_RUN)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
|
||||
@ -21,7 +21,7 @@ from controllers.console.app.error import (
|
||||
ProviderNotInitializeError,
|
||||
ProviderQuotaExceededError,
|
||||
)
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model
|
||||
from controllers.console.explore.error import AppSuggestedQuestionsAfterAnswerDisabledError
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
@ -174,7 +174,7 @@ class AgentChatMessageListApi(Resource):
|
||||
@account_initialization_required
|
||||
@setup_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@agent_manage_required_for_agent_app(scene=RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
|
||||
@ -8,7 +8,6 @@ from constants.languages import supported_language
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.app.error import AppNotFoundError
|
||||
from controllers.console.app.wraps import agent_manage_required_for_agent_app
|
||||
from controllers.console.flask_admission import console_account_admission
|
||||
from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
@ -108,8 +107,8 @@ class AppSite(Resource):
|
||||
allowed_roles=_APP_SITE_EDIT_ROLES,
|
||||
rbac_resource_scope=RBACResourceScope.APP,
|
||||
rbac_permission=RBACPermission.APP_RELEASE_AND_VERSION,
|
||||
agent_manage_fallback=True,
|
||||
)
|
||||
@agent_manage_required_for_agent_app
|
||||
@model_validate(AppSiteUpdatePayload)
|
||||
def post(
|
||||
self,
|
||||
@ -139,8 +138,8 @@ class AppSiteAccessTokenReset(Resource):
|
||||
allowed_roles=_APP_SITE_TOKEN_RESET_ROLES,
|
||||
rbac_resource_scope=RBACResourceScope.APP,
|
||||
rbac_permission=RBACPermission.APP_RELEASE_AND_VERSION,
|
||||
agent_manage_fallback=True,
|
||||
)
|
||||
@agent_manage_required_for_agent_app
|
||||
def post(self, request_context: RequestContext, app_id: UUID):
|
||||
try:
|
||||
site = application_services().app_sites.reset_access_token(request_context, str(app_id))
|
||||
|
||||
@ -12,20 +12,22 @@ from typing import cast, overload
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.session import with_session
|
||||
from controllers.common.wraps import RBACPermission, RBACResourceScope, enforce_rbac_access
|
||||
from controllers.common.wraps import RBACPermission, RBACResourceScope, _extract_resource_id, enforce_rbac_access
|
||||
from controllers.console.app.error import AppNotFoundError
|
||||
from extensions.ext_application_services import application_services
|
||||
from extensions.ext_database import db
|
||||
from libs.login import current_account_with_tenant
|
||||
from models import App, AppMode
|
||||
from models.agent import AgentScope
|
||||
from models.agent import Agent, AgentScope
|
||||
from services.app_service import AppService
|
||||
|
||||
__all__ = [
|
||||
"agent_manage_required_for_agent_app",
|
||||
"enforce_agent_manage_or_app_scene",
|
||||
"get_app_model",
|
||||
"get_previewable_app_model",
|
||||
"with_session",
|
||||
@ -57,43 +59,116 @@ def _load_previewable_app_model(session: Session, app_id: str) -> App | None:
|
||||
return AppService.get_normal_app_by_id(app_id, session)
|
||||
|
||||
|
||||
def agent_manage_required_for_agent_app[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
"""Gate generic app management routes that target an Agent App.
|
||||
def _agent_app_binding(app_id: str) -> Agent | None:
|
||||
app_model = _load_app_model_from_scoped_session(app_id)
|
||||
if app_model is None:
|
||||
return None
|
||||
return app_model.agent_app_binding_with_session(session=db.session(), include_archived=True)
|
||||
|
||||
A hidden workflow-only backing App only reuses the App runtime and is not
|
||||
part of the general app management plane, so generic routes reject it
|
||||
outright. Managing a roster Agent App mutates the roster Agent behind it
|
||||
(rename/icon sync, archive, API enablement), so it additionally requires
|
||||
workspace ``agent.manage`` on top of the route's existing App permission
|
||||
checks when RBAC is enabled. A no-op for non-agent Apps. Must be placed
|
||||
above ``get_app_model`` so the ``app_id`` path parameter is still present.
|
||||
"""
|
||||
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
raw_app_id = kwargs.get("app_id") or kwargs.get("resource_id")
|
||||
if raw_app_id is not None:
|
||||
app_model = _load_app_model_from_scoped_session(str(raw_app_id))
|
||||
binding = (
|
||||
app_model.agent_app_binding_with_session(session=db.session(), include_archived=True)
|
||||
if app_model is not None
|
||||
else None
|
||||
def _reject_hidden_agent_backing_app(path_args: dict[str, object]) -> None:
|
||||
raw_app_id = path_args.get("app_id") or path_args.get("resource_id")
|
||||
if raw_app_id is None:
|
||||
return
|
||||
binding = _agent_app_binding(str(raw_app_id))
|
||||
if binding is not None and binding.scope == AgentScope.WORKFLOW_ONLY:
|
||||
raise AppNotFoundError()
|
||||
|
||||
|
||||
def enforce_agent_manage_or_app_scene(
|
||||
*,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
scene: RBACPermission,
|
||||
path_args: dict[str, object],
|
||||
) -> None:
|
||||
# Must run before the RBAC_ENABLED check below: a hidden workflow-only
|
||||
# backing App has to stay unreachable regardless of RBAC_ENABLED.
|
||||
_reject_hidden_agent_backing_app(path_args)
|
||||
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
return
|
||||
|
||||
binding = _agent_app_binding(_extract_resource_id(RBACResourceScope.APP, tenant_id, path_args))
|
||||
|
||||
if binding is not None:
|
||||
if binding.scope == AgentScope.WORKFLOW_ONLY:
|
||||
raise AppNotFoundError()
|
||||
try:
|
||||
enforce_rbac_access(
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
resource_type=RBACResourceScope.WORKSPACE,
|
||||
scene=RBACPermission.AGENT_MANAGE,
|
||||
resource_required=False,
|
||||
)
|
||||
if binding is not None:
|
||||
if binding.scope == AgentScope.WORKFLOW_ONLY:
|
||||
raise AppNotFoundError()
|
||||
if dify_config.RBAC_ENABLED:
|
||||
current_user, current_tenant_id = current_account_with_tenant()
|
||||
enforce_rbac_access(
|
||||
tenant_id=current_tenant_id,
|
||||
account_id=current_user.id,
|
||||
resource_type=RBACResourceScope.WORKSPACE,
|
||||
scene=RBACPermission.AGENT_MANAGE,
|
||||
resource_required=False,
|
||||
)
|
||||
return view(*args, **kwargs)
|
||||
return
|
||||
except Forbidden:
|
||||
pass # not an agent.manage holder — fall through to the normal scene check
|
||||
|
||||
return decorated
|
||||
enforce_rbac_access(
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
resource_type=RBACResourceScope.APP,
|
||||
scene=scene,
|
||||
path_args=path_args,
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
def agent_manage_required_for_agent_app[**P, R](view: Callable[P, R]) -> Callable[P, R]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def agent_manage_required_for_agent_app[**P, R](
|
||||
view: None = None, *, scene: RBACPermission | None = None
|
||||
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
||||
|
||||
|
||||
def agent_manage_required_for_agent_app[**P, R](
|
||||
view: Callable[P, R] | None = None, *, scene: RBACPermission | None = None
|
||||
) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
# Must sit above get_app_model in the decorator stack — get_app_model
|
||||
# deletes app_id from kwargs, and this decorator needs it.
|
||||
# TODO: this is a workaround, remove this after ACL for agent app is available
|
||||
def decorator(view_func: Callable[P, R]) -> Callable[P, R]:
|
||||
@wraps(view_func)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
if scene is not None:
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
_reject_hidden_agent_backing_app(kwargs)
|
||||
return view_func(*args, **kwargs)
|
||||
current_user, current_tenant_id = current_account_with_tenant()
|
||||
enforce_agent_manage_or_app_scene(
|
||||
tenant_id=current_tenant_id,
|
||||
account_id=current_user.id,
|
||||
scene=scene,
|
||||
path_args=kwargs,
|
||||
)
|
||||
return view_func(*args, **kwargs)
|
||||
|
||||
raw_app_id = kwargs.get("app_id") or kwargs.get("resource_id")
|
||||
if raw_app_id is not None:
|
||||
binding = _agent_app_binding(str(raw_app_id))
|
||||
if binding is not None:
|
||||
if binding.scope == AgentScope.WORKFLOW_ONLY:
|
||||
raise AppNotFoundError()
|
||||
if dify_config.RBAC_ENABLED:
|
||||
current_user, current_tenant_id = current_account_with_tenant()
|
||||
enforce_rbac_access(
|
||||
tenant_id=current_tenant_id,
|
||||
account_id=current_user.id,
|
||||
resource_type=RBACResourceScope.WORKSPACE,
|
||||
scene=RBACPermission.AGENT_MANAGE,
|
||||
resource_required=False,
|
||||
)
|
||||
return view_func(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
if view is None:
|
||||
return decorator
|
||||
return decorator(view)
|
||||
|
||||
|
||||
def _get_injected_session(args: tuple[object, ...]) -> Session | None:
|
||||
|
||||
@ -25,7 +25,7 @@ from services.entities.auth_entities import (
|
||||
ForgotPasswordResetPayload,
|
||||
ForgotPasswordSendPayload,
|
||||
)
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
|
||||
class ForgotPasswordEmailResponse(BaseModel):
|
||||
@ -87,7 +87,7 @@ class ForgotPasswordSendEmailApi(Resource):
|
||||
account=account,
|
||||
email=normalized_email,
|
||||
language=language,
|
||||
is_allow_register=FeatureService.get_system_features().is_allow_register,
|
||||
is_allow_register=SystemFeatureService.is_registration_allowed(),
|
||||
)
|
||||
|
||||
return {"result": "success", "data": token}
|
||||
@ -198,6 +198,6 @@ class ForgotPasswordResetApi(Resource):
|
||||
# Create workspace if needed
|
||||
if (
|
||||
not TenantService.get_join_tenants(account, session=db.session())
|
||||
and FeatureService.is_workspace_creation_allowed()
|
||||
and SystemFeatureService.is_workspace_creation_allowed()
|
||||
):
|
||||
TenantService.create_owner_tenant(account, session=db.session())
|
||||
|
||||
@ -81,7 +81,7 @@ from services.errors.account import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
from services.turnstile_service import (
|
||||
EMAIL_CODE_VERIFY_ACTION,
|
||||
TurnstileChallengeRejectedError,
|
||||
@ -201,8 +201,8 @@ class LoginApi(Resource):
|
||||
tenants = TenantService.get_join_tenants(account, session=db.session())
|
||||
if len(tenants) == 0:
|
||||
if (
|
||||
FeatureService.is_workspace_creation_allowed()
|
||||
and not FeatureService.get_license().workspaces.is_available()
|
||||
SystemFeatureService.is_workspace_creation_allowed()
|
||||
and not SystemFeatureService.get_license().workspaces.is_available()
|
||||
):
|
||||
raise WorkspacesLimitExceeded()
|
||||
else:
|
||||
@ -272,7 +272,7 @@ class ResetPasswordSendEmailApi(Resource):
|
||||
email=normalized_email,
|
||||
account=account,
|
||||
language=language,
|
||||
is_allow_register=FeatureService.get_system_features().is_allow_register,
|
||||
is_allow_register=SystemFeatureService.is_registration_allowed(),
|
||||
)
|
||||
|
||||
return SimpleResultDataResponse(result="success", data=token).model_dump(mode="json")
|
||||
@ -313,7 +313,7 @@ class EmailCodeLoginSendEmailApi(Resource):
|
||||
raise AccountInFreezeError() from exc
|
||||
|
||||
if account is None:
|
||||
if FeatureService.get_system_features().is_allow_register:
|
||||
if SystemFeatureService.is_registration_allowed():
|
||||
token = AccountService.send_email_code_login_email(email=normalized_email, language=language)
|
||||
else:
|
||||
raise AccountNotFound()
|
||||
@ -398,10 +398,10 @@ class EmailCodeLoginApi(Resource):
|
||||
if account:
|
||||
tenants = TenantService.get_join_tenants(account, session=db.session())
|
||||
if not tenants:
|
||||
workspaces = FeatureService.get_license().workspaces
|
||||
workspaces = SystemFeatureService.get_license().workspaces
|
||||
if not workspaces.is_available():
|
||||
raise WorkspacesLimitExceeded()
|
||||
if not FeatureService.is_workspace_creation_allowed():
|
||||
if not SystemFeatureService.is_workspace_creation_allowed():
|
||||
raise NotAllowedCreateWorkspace()
|
||||
else:
|
||||
TenantService.create_owner_tenant(account, session=db.session())
|
||||
|
||||
@ -36,7 +36,7 @@ from services.errors.account import (
|
||||
EmailDomainSuspendedError as EmailDomainSuspendedRegistrationError,
|
||||
)
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
from .. import console_ns
|
||||
|
||||
@ -310,7 +310,7 @@ def _generate_account(
|
||||
if account:
|
||||
tenants = TenantService.get_join_tenants(account, session=db.session())
|
||||
if not tenants:
|
||||
if not FeatureService.is_workspace_creation_allowed():
|
||||
if not SystemFeatureService.is_workspace_creation_allowed():
|
||||
raise WorkSpaceNotAllowedCreateError()
|
||||
else:
|
||||
TenantService.create_owner_tenant(account, session=db.session())
|
||||
@ -318,7 +318,7 @@ def _generate_account(
|
||||
if not account:
|
||||
normalized_email = user_info.email.lower()
|
||||
oauth_new_user = True
|
||||
if not FeatureService.get_system_features().is_allow_register:
|
||||
if not SystemFeatureService.is_registration_allowed():
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
freeze_type = BillingService.get_email_freeze_type(normalized_email)
|
||||
if freeze_type:
|
||||
|
||||
@ -17,7 +17,7 @@ from controllers.console.wraps import (
|
||||
with_current_user,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from fields.dataset_fields import DatasetDetailResponse
|
||||
from fields.dataset_fields import DatasetDetailResponse, dataset_detail_response_source
|
||||
from libs.helper import dump_response
|
||||
from libs.login import login_required
|
||||
from models import Account
|
||||
@ -116,6 +116,7 @@ class CreateEmptyRagPipelineDatasetApi(Resource):
|
||||
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
session = db.session()
|
||||
dataset = DatasetService.create_empty_rag_pipeline_dataset(
|
||||
tenant_id=current_tenant_id,
|
||||
rag_pipeline_dataset_create_entity=RagPipelineDatasetCreateEntity(
|
||||
@ -129,6 +130,6 @@ class CreateEmptyRagPipelineDatasetApi(Resource):
|
||||
permission=DatasetPermissionEnum.ONLY_ME,
|
||||
partial_member_list=None,
|
||||
),
|
||||
session=db.session(),
|
||||
session=session,
|
||||
)
|
||||
return dump_response(DatasetDetailResponse, dataset), 201
|
||||
return dump_response(DatasetDetailResponse, dataset_detail_response_source(dataset, session=session)), 201
|
||||
|
||||
@ -18,7 +18,7 @@ from extensions.ext_database import db
|
||||
from libs.login import current_account_with_tenant, login_required
|
||||
from models import AccountTrialAppRecord, App, InstalledApp, TrialApp
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
|
||||
def installed_app_required[**P, R](view: Callable[Concatenate[InstalledApp, P], R] | None = None):
|
||||
@ -55,8 +55,7 @@ def user_allowed_to_access_app[**P, R](view: Callable[Concatenate[InstalledApp,
|
||||
@wraps(view)
|
||||
def decorated(installed_app: InstalledApp, *args: P.args, **kwargs: P.kwargs):
|
||||
current_user, _ = current_account_with_tenant()
|
||||
feature = FeatureService.get_system_features()
|
||||
if feature.webapp_auth.enabled:
|
||||
if SystemFeatureService.is_webapp_auth_enabled():
|
||||
app_id = installed_app.app_id
|
||||
res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(
|
||||
user_id=str(current_user.id),
|
||||
|
||||
@ -125,7 +125,7 @@ class SystemFeatureApi(Resource):
|
||||
Authentication configuration must be available before the authentication flow can be selected.
|
||||
Authenticated license detail is served separately by SystemFeatureLicenseApi.
|
||||
"""
|
||||
return dump_response(SystemFeatureModel, application_services().feature_queries.get_system_features())
|
||||
return dump_response(SystemFeatureModel, application_services().feature_queries.get_public_system_features())
|
||||
|
||||
|
||||
@console_ns.route("/system-features/license")
|
||||
|
||||
@ -9,6 +9,7 @@ from werkzeug.exceptions import Forbidden
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.wraps import enforce_rbac_access
|
||||
from controllers.console.app.wraps import enforce_agent_manage_or_app_scene
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
enable_change_email,
|
||||
@ -22,7 +23,7 @@ from libs.login import current_account_with_tenant, login_required
|
||||
from machinery.context import RequestContext
|
||||
from machinery.errors import AdmissionConfigurationError
|
||||
from models.account import TenantAccountRole
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
|
||||
def console_email_registration_admission[T, **P, R](
|
||||
@ -32,8 +33,10 @@ def console_email_registration_admission[T, **P, R](
|
||||
|
||||
@wraps(view)
|
||||
def check_registration_features(self: T, /, *args: P.args, **kwargs: P.kwargs) -> R:
|
||||
features = FeatureService.get_system_features()
|
||||
if not features.enable_email_password_login or not features.is_allow_register:
|
||||
if (
|
||||
not SystemFeatureService.is_email_password_login_enabled()
|
||||
or not SystemFeatureService.is_registration_allowed()
|
||||
):
|
||||
abort(403)
|
||||
return view(self, *args, **kwargs)
|
||||
|
||||
@ -50,6 +53,7 @@ def console_account_admission[T, **P, R](
|
||||
rbac_resource_scope: RBACResourceScope | None = None,
|
||||
rbac_permission: RBACPermission | None = None,
|
||||
rbac_resource_required: bool = True,
|
||||
agent_manage_fallback: bool = False,
|
||||
) -> Callable[
|
||||
[Callable[Concatenate[T, RequestContext, P], R]],
|
||||
Callable[Concatenate[T, P], R | Response],
|
||||
@ -64,6 +68,10 @@ def console_account_admission[T, **P, R](
|
||||
|
||||
if (rbac_resource_scope is None) != (rbac_permission is None):
|
||||
raise AdmissionConfigurationError("RBAC resource scope and permission must be configured together")
|
||||
if agent_manage_fallback and rbac_resource_scope != RBACResourceScope.APP:
|
||||
raise AdmissionConfigurationError("agent_manage_fallback requires rbac_resource_scope=RBACResourceScope.APP")
|
||||
if agent_manage_fallback and not rbac_resource_required:
|
||||
raise AdmissionConfigurationError("agent_manage_fallback requires rbac_resource_required=True")
|
||||
|
||||
def decorator(
|
||||
view: Callable[Concatenate[T, RequestContext, P], R],
|
||||
@ -76,14 +84,22 @@ def console_account_admission[T, **P, R](
|
||||
if allowed_roles is not None and not dify_config.RBAC_ENABLED and account.role not in allowed_roles:
|
||||
raise Forbidden()
|
||||
if rbac_resource_scope is not None and rbac_permission is not None:
|
||||
enforce_rbac_access(
|
||||
tenant_id=tenant_id,
|
||||
account_id=account.id,
|
||||
resource_type=rbac_resource_scope,
|
||||
scene=rbac_permission,
|
||||
resource_required=rbac_resource_required,
|
||||
path_args=kwargs,
|
||||
)
|
||||
if agent_manage_fallback:
|
||||
enforce_agent_manage_or_app_scene(
|
||||
tenant_id=tenant_id,
|
||||
account_id=account.id,
|
||||
scene=rbac_permission,
|
||||
path_args=kwargs,
|
||||
)
|
||||
else:
|
||||
enforce_rbac_access(
|
||||
tenant_id=tenant_id,
|
||||
account_id=account.id,
|
||||
resource_type=rbac_resource_scope,
|
||||
scene=rbac_permission,
|
||||
resource_required=rbac_resource_required,
|
||||
path_args=kwargs,
|
||||
)
|
||||
request_context = RequestContext(
|
||||
account_id=account.id,
|
||||
active_workspace_id=tenant_id,
|
||||
|
||||
@ -45,6 +45,7 @@ from models.account import Account, TenantAccountJoin, TenantAccountRole
|
||||
from services.account_service import AccountService, RegisterService, TenantService
|
||||
from services.errors.account import AccountAlreadyInTenantError
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
|
||||
class MemberInvitePayload(BaseModel):
|
||||
@ -185,7 +186,7 @@ def _check_member_invite_limits(tenant_id: str, new_member_count: int, new_accou
|
||||
if workspace_members.enabled is True and not workspace_members.is_available(new_member_count):
|
||||
raise WorkspaceMembersLimitExceeded()
|
||||
if new_account_count > 0:
|
||||
seats = FeatureService.get_license().seats
|
||||
seats = SystemFeatureService.get_license().seats
|
||||
if not seats.is_available(new_account_count):
|
||||
raise SeatsLimitExceeded()
|
||||
return
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import io
|
||||
from typing import Any, Literal
|
||||
|
||||
from flask import request, send_file
|
||||
from flask import send_file
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
@ -15,6 +15,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@ -154,10 +155,8 @@ class ModelProviderListApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str):
|
||||
payload = request.args.to_dict(flat=True)
|
||||
args = ParserModelList.model_validate(payload)
|
||||
|
||||
@model_validate(ParserModelList)
|
||||
def get(self, args: ParserModelList, tenant_id: str):
|
||||
model_provider_service = ModelProviderService()
|
||||
provider_list = model_provider_service.get_provider_list(tenant_id=tenant_id, model_type=args.model_type)
|
||||
|
||||
@ -212,12 +211,10 @@ class ModelProviderCredentialApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str, provider: str):
|
||||
# if credential_id is not provided, return current used credential
|
||||
payload = request.args.to_dict(flat=True)
|
||||
args = ParserCredentialId.model_validate(payload)
|
||||
|
||||
@model_validate(ParserCredentialId)
|
||||
def get(self, args: ParserCredentialId, tenant_id: str, provider: str):
|
||||
model_provider_service = ModelProviderService()
|
||||
# if credential_id is not provided, return current used credential
|
||||
credentials = model_provider_service.get_provider_credential(
|
||||
tenant_id=tenant_id, provider=provider, credential_id=args.credential_id
|
||||
)
|
||||
@ -232,10 +229,8 @@ class ModelProviderCredentialApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider: str):
|
||||
payload = console_ns.payload or {}
|
||||
args = ParserCredentialCreate.model_validate(payload)
|
||||
|
||||
@model_validate(ParserCredentialCreate)
|
||||
def post(self, args: ParserCredentialCreate, current_tenant_id: str, provider: str):
|
||||
model_provider_service = ModelProviderService()
|
||||
|
||||
try:
|
||||
@ -258,10 +253,8 @@ class ModelProviderCredentialApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def put(self, current_tenant_id: str, provider: str):
|
||||
payload = console_ns.payload or {}
|
||||
args = ParserCredentialUpdate.model_validate(payload)
|
||||
|
||||
@model_validate(ParserCredentialUpdate)
|
||||
def put(self, args: ParserCredentialUpdate, current_tenant_id: str, provider: str):
|
||||
model_provider_service = ModelProviderService()
|
||||
|
||||
try:
|
||||
@ -285,10 +278,8 @@ class ModelProviderCredentialApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, provider: str):
|
||||
payload = console_ns.payload or {}
|
||||
args = ParserCredentialDelete.model_validate(payload)
|
||||
|
||||
@model_validate(ParserCredentialDelete)
|
||||
def delete(self, args: ParserCredentialDelete, current_tenant_id: str, provider: str):
|
||||
model_provider_service = ModelProviderService()
|
||||
model_provider_service.remove_provider_credential(
|
||||
tenant_id=current_tenant_id, provider=provider, credential_id=args.credential_id
|
||||
@ -307,10 +298,8 @@ class ModelProviderCredentialSwitchApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider: str):
|
||||
payload = console_ns.payload or {}
|
||||
args = ParserCredentialSwitch.model_validate(payload)
|
||||
|
||||
@model_validate(ParserCredentialSwitch)
|
||||
def post(self, args: ParserCredentialSwitch, current_tenant_id: str, provider: str):
|
||||
service = ModelProviderService()
|
||||
service.switch_active_provider_credential(
|
||||
tenant_id=current_tenant_id,
|
||||
@ -332,10 +321,8 @@ class ModelProviderValidateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider: str):
|
||||
payload = console_ns.payload or {}
|
||||
args = ParserCredentialValidate.model_validate(payload)
|
||||
|
||||
@model_validate(ParserCredentialValidate)
|
||||
def post(self, args: ParserCredentialValidate, current_tenant_id: str, provider: str):
|
||||
tenant_id = current_tenant_id
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
@ -388,10 +375,8 @@ class PreferredProviderTypeUpdateApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
payload = console_ns.payload or {}
|
||||
args = ParserPreferredProviderType.model_validate(payload)
|
||||
|
||||
@model_validate(ParserPreferredProviderType)
|
||||
def post(self, args: ParserPreferredProviderType, tenant_id: str, provider: str):
|
||||
model_provider_service = ModelProviderService()
|
||||
model_provider_service.switch_preferred_provider(
|
||||
tenant_id=tenant_id, provider=provider, preferred_provider_type=args.preferred_provider_type
|
||||
|
||||
@ -19,6 +19,7 @@ from controllers.common.wraps import (
|
||||
from controllers.console.auth.error import AuthenticationFailedError, EmailCodeError
|
||||
from controllers.console.workspace.error import AccountNotInitializedError
|
||||
from enums import CloudPlan, DeploymentEdition
|
||||
from extensions.ext_application_services import application_services
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from libs.encryption import FieldEncryption
|
||||
@ -31,6 +32,7 @@ from services.billing_service import BillingService
|
||||
from services.entities.feature_entities import LicenseStatus
|
||||
from services.feature_service import FeatureService
|
||||
from services.operation_service import OperationService, UtmInfo
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
from .error import NotInitValidateError, NotSetupError, UnauthorizedAndForceLogout
|
||||
|
||||
@ -183,7 +185,7 @@ def cloud_edition_billing_resource_check[**P, R](resource: str) -> Callable[[Cal
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||
return view(*args, **kwargs)
|
||||
|
||||
vector_space = FeatureService.get_vector_space(current_tenant_id)
|
||||
vector_space = application_services().feature_queries.get_workspace_vector_space(current_tenant_id)
|
||||
if 0 < vector_space.limit <= vector_space.size:
|
||||
abort(
|
||||
403,
|
||||
@ -330,8 +332,11 @@ def setup_required[R](view: Callable[..., R]) -> Callable[..., R]:
|
||||
def enterprise_license_required[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||
settings = FeatureService.get_system_features()
|
||||
if settings.license.status in [LicenseStatus.INACTIVE, LicenseStatus.EXPIRED, LicenseStatus.LOST]:
|
||||
if SystemFeatureService.get_license_status() in [
|
||||
LicenseStatus.INACTIVE,
|
||||
LicenseStatus.EXPIRED,
|
||||
LicenseStatus.LOST,
|
||||
]:
|
||||
raise UnauthorizedAndForceLogout("Your license is invalid. Please contact your administrator.")
|
||||
|
||||
return view(*args, **kwargs)
|
||||
@ -342,8 +347,7 @@ def enterprise_license_required[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
def email_password_login_enabled[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||
features = FeatureService.get_system_features()
|
||||
if features.enable_email_password_login:
|
||||
if SystemFeatureService.is_email_password_login_enabled():
|
||||
return view(*args, **kwargs)
|
||||
|
||||
# otherwise, return 403
|
||||
@ -355,8 +359,7 @@ def email_password_login_enabled[**P, R](view: Callable[P, R]) -> Callable[P, R]
|
||||
def enable_change_email[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||
features = FeatureService.get_system_features()
|
||||
if features.enable_change_email:
|
||||
if SystemFeatureService.is_change_email_enabled():
|
||||
return view(*args, **kwargs)
|
||||
|
||||
# otherwise, return 403
|
||||
@ -372,7 +375,11 @@ def is_allow_transfer_owner[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
|
||||
_, current_tenant_id = current_account_with_tenant()
|
||||
# Check both billing/plan level and workspace policy level permissions
|
||||
check_workspace_owner_transfer_permission(current_tenant_id)
|
||||
features = application_services().feature_queries.get_workspace_features(current_tenant_id)
|
||||
check_workspace_owner_transfer_permission(
|
||||
current_tenant_id,
|
||||
owner_transfer_allowed=features.is_allow_transfer_workspace,
|
||||
)
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
@ -6,8 +6,9 @@ from pydantic import BaseModel, Field
|
||||
from controllers.common.schema import register_schema_model
|
||||
from controllers.console.wraps import setup_required
|
||||
from controllers.inner_api import inner_api_ns
|
||||
from controllers.inner_api.wraps import billing_inner_api_only, enterprise_inner_api_only
|
||||
from tasks.mail_inner_task import send_inner_email_task
|
||||
from controllers.inner_api.wraps import inner_api_only
|
||||
from extensions.ext_application_services import application_services
|
||||
from services.entities.mail_entities import InnerMailMessage
|
||||
|
||||
|
||||
class InnerMailPayload(BaseModel):
|
||||
@ -28,25 +29,27 @@ class BaseMail(Resource):
|
||||
@inner_api_ns.expect(inner_api_ns.models[InnerMailPayload.__name__])
|
||||
def post(self):
|
||||
args = InnerMailPayload.model_validate(inner_api_ns.payload or {})
|
||||
send_inner_email_task.delay(
|
||||
to=args.to,
|
||||
subject=args.subject,
|
||||
body=args.body,
|
||||
substitutions=args.substitutions, # type: ignore
|
||||
application_services().inner_mail.send(
|
||||
InnerMailMessage(
|
||||
recipients=tuple(args.to),
|
||||
subject=args.subject,
|
||||
body=args.body,
|
||||
substitutions=args.substitutions,
|
||||
)
|
||||
)
|
||||
return {"message": "success"}, 200
|
||||
|
||||
|
||||
@inner_api_ns.route("/enterprise/mail")
|
||||
class EnterpriseMail(BaseMail):
|
||||
method_decorators = [setup_required, enterprise_inner_api_only]
|
||||
|
||||
@inner_api_ns.doc("send_enterprise_mail")
|
||||
@inner_api_ns.doc(description="Send internal email for enterprise features")
|
||||
@inner_api_ns.expect(inner_api_ns.models[InnerMailPayload.__name__])
|
||||
@inner_api_ns.doc(
|
||||
responses={200: "Email sent successfully", 401: "Unauthorized - invalid API key", 404: "Service not available"}
|
||||
)
|
||||
@inner_api_only
|
||||
@setup_required
|
||||
def post(self):
|
||||
"""Send internal email for enterprise features.
|
||||
|
||||
@ -61,14 +64,14 @@ class EnterpriseMail(BaseMail):
|
||||
|
||||
@inner_api_ns.route("/billing/mail")
|
||||
class BillingMail(BaseMail):
|
||||
method_decorators = [setup_required, billing_inner_api_only]
|
||||
|
||||
@inner_api_ns.doc("send_billing_mail")
|
||||
@inner_api_ns.doc(description="Send internal email for billing notifications")
|
||||
@inner_api_ns.expect(inner_api_ns.models[InnerMailPayload.__name__])
|
||||
@inner_api_ns.doc(
|
||||
responses={200: "Email sent successfully", 401: "Unauthorized - invalid API key", 404: "Service not available"}
|
||||
)
|
||||
@inner_api_only
|
||||
@setup_required
|
||||
def post(self):
|
||||
"""Send internal email for billing notifications.
|
||||
|
||||
|
||||
@ -35,10 +35,6 @@ def inner_api_only[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
return decorated
|
||||
|
||||
|
||||
def billing_inner_api_only[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
return inner_api_only(view)
|
||||
|
||||
|
||||
def enterprise_inner_api_only[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
return inner_api_only(view)
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ from controllers.openapi.auth.data import AuthData, RequestContext
|
||||
from enums import DeploymentEdition
|
||||
from libs.oauth_bearer import Scope, TokenType
|
||||
from services.enterprise.enterprise_service import WebAppAccessMode
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
CondFn = Callable[[RequestContext, AuthData | None], bool]
|
||||
|
||||
@ -50,7 +50,7 @@ EDITION_COMMUNITY = config_cond(lambda: dify_config.DEPLOYMENT_EDITION == Deploy
|
||||
EDITION_ENTERPRISE = config_cond(lambda: dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE)
|
||||
EDITION_CLOUD = config_cond(lambda: dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD)
|
||||
|
||||
WEBAPP_AUTH_ENABLED = config_cond(lambda: FeatureService.get_system_features().webapp_auth.enabled)
|
||||
WEBAPP_AUTH_ENABLED = config_cond(lambda: SystemFeatureService.is_webapp_auth_enabled())
|
||||
|
||||
WEBAPP_RUN_SCOPED = request_cond(lambda ctx: ctx.scope == Scope.APPS_RUN)
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ from libs.oauth_bearer import (
|
||||
)
|
||||
from models.account import TenantAccountRole
|
||||
from services.entities.feature_entities import LicenseStatus
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
|
||||
class AuthPipeline:
|
||||
@ -265,8 +265,11 @@ def _subject_type_str(identity: Any) -> str | None:
|
||||
|
||||
|
||||
def _check_license() -> None:
|
||||
settings = FeatureService.get_system_features()
|
||||
if settings.license.status in {LicenseStatus.INACTIVE, LicenseStatus.EXPIRED, LicenseStatus.LOST}:
|
||||
if SystemFeatureService.get_license_status() in {
|
||||
LicenseStatus.INACTIVE,
|
||||
LicenseStatus.EXPIRED,
|
||||
LicenseStatus.LOST,
|
||||
}:
|
||||
raise Forbidden("license_invalid")
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from flask_restx.api import HTTPStatus
|
||||
from pydantic import BaseModel, Field, TypeAdapter
|
||||
@ -207,9 +206,9 @@ class AnnotationListApi(Resource):
|
||||
)
|
||||
@validate_app_token
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, app_model: App):
|
||||
@model_validate(AnnotationListQuery)
|
||||
def get(self, query: AnnotationListQuery, session: Session, app_model: App):
|
||||
"""List annotations for the application."""
|
||||
query = AnnotationListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(
|
||||
app_model.id, query.page, query.limit, query.keyword, session
|
||||
|
||||
@ -2,7 +2,6 @@ from datetime import datetime
|
||||
from typing import Annotated, Any, Literal
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, TypeAdapter, WithJsonSchema, field_validator
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
@ -185,7 +184,8 @@ class ConversationApi(Resource):
|
||||
service_api_ns.models[ConversationInfiniteScrollPagination.__name__],
|
||||
)
|
||||
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
|
||||
def get(self, app_model: App, end_user: EndUser):
|
||||
@model_validate(ConversationListQuery)
|
||||
def get(self, query_args: ConversationListQuery, app_model: App, end_user: EndUser):
|
||||
"""List all conversations for the current user.
|
||||
|
||||
Supports pagination using last_id and limit parameters.
|
||||
@ -194,7 +194,6 @@ class ConversationApi(Resource):
|
||||
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}:
|
||||
raise NotChatAppError()
|
||||
|
||||
query_args = ConversationListQuery.model_validate(request.args.to_dict())
|
||||
last_id = query_args.last_id or None
|
||||
|
||||
try:
|
||||
@ -343,7 +342,8 @@ class ConversationVariablesApi(Resource):
|
||||
service_api_ns.models[ConversationVariableInfiniteScrollPaginationResponse.__name__],
|
||||
)
|
||||
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
|
||||
def get(self, app_model: App, end_user: EndUser, conversation_id: UUID):
|
||||
@model_validate(ConversationVariablesQuery)
|
||||
def get(self, query_args: ConversationVariablesQuery, app_model: App, end_user: EndUser, conversation_id: UUID):
|
||||
"""List all variables for a conversation.
|
||||
|
||||
Conversational variables are only available for chat applications.
|
||||
@ -355,7 +355,6 @@ class ConversationVariablesApi(Resource):
|
||||
|
||||
conversation_id_str = str(conversation_id)
|
||||
|
||||
query_args = ConversationVariablesQuery.model_validate(request.args.to_dict())
|
||||
last_id = query_args.last_id or None
|
||||
|
||||
try:
|
||||
|
||||
@ -2,7 +2,7 @@ import logging
|
||||
from urllib.parse import quote
|
||||
from uuid import UUID
|
||||
|
||||
from flask import Response, request
|
||||
from flask import Response
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
@ -10,6 +10,7 @@ from sqlalchemy import select
|
||||
from controllers.common.fields import BinaryFileResponse
|
||||
from controllers.common.file_response import enforce_download_for_html
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_model, register_schema_model
|
||||
from controllers.console.wraps import model_validate
|
||||
from controllers.service_api import service_api_ns
|
||||
from controllers.service_api.app.error import (
|
||||
FileAccessDeniedError,
|
||||
@ -86,7 +87,8 @@ class FilePreviewApi(Resource):
|
||||
)
|
||||
@service_api_ns.response(200, "File retrieved successfully")
|
||||
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
|
||||
def get(self, app_model: App, end_user: EndUser, file_id: UUID):
|
||||
@model_validate(FilePreviewQuery)
|
||||
def get(self, args: FilePreviewQuery, app_model: App, end_user: EndUser, file_id: UUID):
|
||||
"""
|
||||
Preview/Download a file that was uploaded via Service API.
|
||||
|
||||
@ -95,9 +97,6 @@ class FilePreviewApi(Resource):
|
||||
"""
|
||||
file_id_str = str(file_id)
|
||||
|
||||
# Parse query parameters
|
||||
args = FilePreviewQuery.model_validate(request.args.to_dict())
|
||||
|
||||
# Validate file ownership and get file objects
|
||||
_, upload_file = self._validate_file_ownership(file_id_str, app_model.id)
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import logging
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, TypeAdapter, WithJsonSchema
|
||||
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
|
||||
@ -102,7 +101,8 @@ class MessageListApi(Resource):
|
||||
service_api_ns.models[MessageInfiniteScrollPagination.__name__],
|
||||
)
|
||||
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
|
||||
def get(self, app_model: App, end_user: EndUser):
|
||||
@model_validate(MessageListQuery)
|
||||
def get(self, query_args: MessageListQuery, app_model: App, end_user: EndUser):
|
||||
"""List messages in a conversation.
|
||||
|
||||
Retrieves messages with pagination support using first_id.
|
||||
@ -111,7 +111,6 @@ class MessageListApi(Resource):
|
||||
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}:
|
||||
raise NotChatAppError()
|
||||
|
||||
query_args = MessageListQuery.model_validate(request.args.to_dict())
|
||||
conversation_id = query_args.conversation_id
|
||||
first_id = query_args.first_id or None
|
||||
|
||||
@ -212,12 +211,12 @@ class AppGetFeedbacksApi(Resource):
|
||||
service_api_ns.models[AppFeedbackListResponse.__name__],
|
||||
)
|
||||
@validate_app_token
|
||||
def get(self, app_model: App):
|
||||
@model_validate(FeedbackListQuery)
|
||||
def get(self, query_args: FeedbackListQuery, app_model: App):
|
||||
"""Get all feedbacks for the application.
|
||||
|
||||
Returns paginated list of all feedback submitted for messages in this app.
|
||||
"""
|
||||
query_args = FeedbackListQuery.model_validate(request.args.to_dict())
|
||||
feedbacks = MessageService.get_all_messages_feedbacks(
|
||||
app_model, page=query_args.page, limit=query_args.limit, session=db.session()
|
||||
)
|
||||
|
||||
@ -23,6 +23,7 @@ from controllers.service_api.schema import (
|
||||
USER_REQUIRED_ATTR,
|
||||
)
|
||||
from enums import CloudPlan, DeploymentEdition
|
||||
from extensions.ext_application_services import application_services
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from libs.login import current_user
|
||||
@ -196,7 +197,7 @@ def cloud_edition_billing_resource_check[**P, R](
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||
return view(*args, **kwargs)
|
||||
|
||||
vector_space = FeatureService.get_vector_space(api_token.tenant_id)
|
||||
vector_space = application_services().feature_queries.get_workspace_vector_space(api_token.tenant_id)
|
||||
if vector_space.usage_unknown:
|
||||
features = FeatureService.get_features(api_token.tenant_id, exclude_vector_space=True)
|
||||
if features.billing.enabled and features.billing.subscription.plan == CloudPlan.SANDBOX:
|
||||
|
||||
@ -32,5 +32,5 @@ class SystemFeatureApi(Resource):
|
||||
"""
|
||||
return dump_response(
|
||||
SystemFeatureModel,
|
||||
application_services().feature_queries.get_system_features(),
|
||||
application_services().feature_queries.get_public_system_features(),
|
||||
)
|
||||
|
||||
@ -1,27 +1,22 @@
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, select
|
||||
from werkzeug.exceptions import NotFound, Unauthorized
|
||||
|
||||
from configs import dify_config
|
||||
from constants import HEADER_NAME_APP_CODE
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.error import WebAppAuthRequiredError
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_application_services import application_services
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import dump_response
|
||||
from libs.passport import PassportService
|
||||
from libs.token import extract_webapp_access_token
|
||||
from models.enums import EndUserType
|
||||
from models.model import App, EndUser, Site
|
||||
from services.feature_service import FeatureService
|
||||
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
|
||||
from services.entities.passport_entities import WebPassportRequest
|
||||
from services.web_passport_service import (
|
||||
WebPassportAuthenticationRequiredError,
|
||||
WebPassportNotFoundError,
|
||||
WebPassportUnauthorizedError,
|
||||
)
|
||||
|
||||
|
||||
class PassportQuery(BaseModel):
|
||||
@ -40,7 +35,7 @@ register_response_schema_models(web_ns, PassportAccessTokenResponse)
|
||||
|
||||
@web_ns.route("/passport")
|
||||
class PassportResource(Resource):
|
||||
"""Base resource for passport."""
|
||||
"""Issue an authentication passport for a deployed web application."""
|
||||
|
||||
@web_ns.doc("get_passport")
|
||||
@web_ns.doc(description="Get authentication passport for web application access")
|
||||
@ -54,207 +49,23 @@ class PassportResource(Resource):
|
||||
)
|
||||
@web_ns.response(200, "Passport retrieved successfully", web_ns.models[PassportAccessTokenResponse.__name__])
|
||||
def get(self):
|
||||
system_features = FeatureService.get_system_features()
|
||||
app_code = request.headers.get(HEADER_NAME_APP_CODE)
|
||||
user_id = request.args.get("user_id")
|
||||
access_token = extract_webapp_access_token(request)
|
||||
if app_code is None:
|
||||
raise Unauthorized("X-App-Code header is missing.")
|
||||
if system_features.webapp_auth.enabled:
|
||||
enterprise_user_decoded = decode_enterprise_webapp_user_id(access_token)
|
||||
app_auth_type = WebAppAuthService.get_app_auth_type(app_code=app_code, session=db.session())
|
||||
if app_auth_type != WebAppAuthType.PUBLIC:
|
||||
if not enterprise_user_decoded:
|
||||
raise WebAppAuthRequiredError()
|
||||
return dump_response(
|
||||
PassportAccessTokenResponse,
|
||||
exchange_token_for_existing_web_user(
|
||||
app_code=app_code, enterprise_user_decoded=enterprise_user_decoded, auth_type=app_auth_type
|
||||
),
|
||||
)
|
||||
|
||||
# get site from db and check if it is normal
|
||||
site = db.session.scalar(select(Site).where(Site.code == app_code, Site.status == "normal"))
|
||||
if not site:
|
||||
raise NotFound()
|
||||
# get app from db and check if it is normal and enable_site
|
||||
app_model = db.session.scalar(select(App).where(App.id == site.app_id))
|
||||
if not app_model or app_model.status != "normal" or not app_model.enable_site:
|
||||
raise NotFound()
|
||||
|
||||
if user_id:
|
||||
end_user = db.session.scalar(
|
||||
select(EndUser).where(EndUser.app_id == app_model.id, EndUser.session_id == user_id)
|
||||
)
|
||||
|
||||
if end_user:
|
||||
pass
|
||||
else:
|
||||
end_user = EndUser(
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=app_model.id,
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=True,
|
||||
session_id=user_id,
|
||||
)
|
||||
db.session.add(end_user)
|
||||
db.session.commit()
|
||||
else:
|
||||
end_user = EndUser(
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=app_model.id,
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=True,
|
||||
session_id=generate_session_id(),
|
||||
)
|
||||
db.session.add(end_user)
|
||||
db.session.commit()
|
||||
|
||||
payload = {
|
||||
"iss": site.app_id,
|
||||
"sub": "Web API Passport",
|
||||
"app_id": site.app_id,
|
||||
"app_code": app_code,
|
||||
"end_user_id": end_user.id,
|
||||
}
|
||||
|
||||
tk = PassportService().issue(payload)
|
||||
|
||||
return dump_response(PassportAccessTokenResponse, {"access_token": tk})
|
||||
|
||||
|
||||
def decode_enterprise_webapp_user_id(jwt_token: str | None) -> dict[str, Any] | None:
|
||||
"""
|
||||
Decode the enterprise user session from the Authorization header.
|
||||
"""
|
||||
if not jwt_token:
|
||||
return None
|
||||
|
||||
decoded: dict[str, Any] = PassportService().verify(jwt_token)
|
||||
source = decoded.get("token_source")
|
||||
if not source or source != "webapp_login_token":
|
||||
raise Unauthorized("Invalid token source. Expected 'webapp_login_token'.")
|
||||
return decoded
|
||||
|
||||
|
||||
def exchange_token_for_existing_web_user(
|
||||
app_code: str, enterprise_user_decoded: dict[str, Any], auth_type: WebAppAuthType
|
||||
):
|
||||
"""
|
||||
Exchange a token for an existing web user session.
|
||||
"""
|
||||
user_id = enterprise_user_decoded.get("user_id")
|
||||
end_user_id = enterprise_user_decoded.get("end_user_id")
|
||||
session_id = enterprise_user_decoded.get("session_id")
|
||||
user_auth_type = enterprise_user_decoded.get("auth_type")
|
||||
exchanged_token_expires_unix = enterprise_user_decoded.get("exp")
|
||||
|
||||
if not user_auth_type:
|
||||
raise Unauthorized("Missing auth_type in the token.")
|
||||
|
||||
site = db.session.scalar(select(Site).where(Site.code == app_code, Site.status == "normal"))
|
||||
if not site:
|
||||
raise NotFound()
|
||||
|
||||
app_model = db.session.scalar(select(App).where(App.id == site.app_id))
|
||||
if not app_model or app_model.status != "normal" or not app_model.enable_site:
|
||||
raise NotFound()
|
||||
|
||||
match auth_type:
|
||||
case WebAppAuthType.PUBLIC:
|
||||
return _exchange_for_public_app_token(app_model, site, enterprise_user_decoded)
|
||||
case WebAppAuthType.EXTERNAL:
|
||||
if user_auth_type != "external":
|
||||
raise WebAppAuthRequiredError("Please login as external user.")
|
||||
case WebAppAuthType.INTERNAL:
|
||||
if user_auth_type != "internal":
|
||||
raise WebAppAuthRequiredError("Please login as internal user.")
|
||||
|
||||
end_user = None
|
||||
if end_user_id:
|
||||
end_user = db.session.scalar(select(EndUser).where(EndUser.id == end_user_id))
|
||||
if session_id:
|
||||
end_user = db.session.scalar(
|
||||
select(EndUser).where(
|
||||
EndUser.session_id == session_id,
|
||||
EndUser.tenant_id == app_model.tenant_id,
|
||||
EndUser.app_id == app_model.id,
|
||||
)
|
||||
query = PassportQuery.model_validate(request.args.to_dict(flat=True))
|
||||
passport_request = WebPassportRequest(
|
||||
app_code=app_code,
|
||||
user_session_id=query.user_id,
|
||||
access_token=extract_webapp_access_token(request),
|
||||
)
|
||||
if not end_user:
|
||||
if not session_id:
|
||||
raise NotFound("Missing session_id for existing web user.")
|
||||
end_user = EndUser(
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=app_model.id,
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=True,
|
||||
session_id=session_id,
|
||||
)
|
||||
db.session.add(end_user)
|
||||
db.session.commit()
|
||||
try:
|
||||
result = application_services().web_passport.issue(passport_request)
|
||||
except WebPassportAuthenticationRequiredError as exc:
|
||||
raise WebAppAuthRequiredError(str(exc)) from exc
|
||||
except WebPassportUnauthorizedError as exc:
|
||||
raise Unauthorized(str(exc)) from exc
|
||||
except WebPassportNotFoundError as exc:
|
||||
raise NotFound(str(exc) or None) from exc
|
||||
|
||||
exp = int((datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES)).timestamp())
|
||||
if exchanged_token_expires_unix:
|
||||
exp = int(exchanged_token_expires_unix)
|
||||
|
||||
payload = {
|
||||
"iss": site.id,
|
||||
"sub": "Web API Passport",
|
||||
"app_id": site.app_id,
|
||||
"app_code": site.code,
|
||||
"user_id": user_id,
|
||||
"end_user_id": end_user.id,
|
||||
"auth_type": user_auth_type,
|
||||
"granted_at": int(datetime.now(UTC).timestamp()),
|
||||
"token_source": "webapp",
|
||||
"exp": exp,
|
||||
}
|
||||
token: str = PassportService().issue(payload)
|
||||
return {"access_token": token}
|
||||
|
||||
|
||||
def _exchange_for_public_app_token(app_model, site, token_decoded):
|
||||
user_id = token_decoded.get("user_id")
|
||||
end_user = None
|
||||
if user_id:
|
||||
end_user = db.session.scalar(
|
||||
select(EndUser).where(EndUser.app_id == app_model.id, EndUser.session_id == user_id)
|
||||
)
|
||||
|
||||
if not end_user:
|
||||
end_user = EndUser(
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=app_model.id,
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=True,
|
||||
session_id=generate_session_id(),
|
||||
)
|
||||
|
||||
db.session.add(end_user)
|
||||
db.session.commit()
|
||||
|
||||
payload = {
|
||||
"iss": site.app_id,
|
||||
"sub": "Web API Passport",
|
||||
"app_id": site.app_id,
|
||||
"app_code": site.code,
|
||||
"end_user_id": end_user.id,
|
||||
}
|
||||
|
||||
tk = PassportService().issue(payload)
|
||||
|
||||
return {"access_token": tk}
|
||||
|
||||
|
||||
def generate_session_id():
|
||||
"""
|
||||
Generate a unique session ID.
|
||||
"""
|
||||
while True:
|
||||
session_id = str(uuid.uuid4())
|
||||
existing_count = db.session.scalar(
|
||||
select(func.count()).select_from(EndUser).where(EndUser.session_id == session_id)
|
||||
)
|
||||
if existing_count == 0:
|
||||
return session_id
|
||||
return dump_response(PassportAccessTokenResponse, {"access_token": result.access_token})
|
||||
|
||||
@ -18,7 +18,7 @@ from libs.token import extract_webapp_passport
|
||||
from models.model import App, EndUser, Site
|
||||
from services.app_service import AppService
|
||||
from services.enterprise.enterprise_service import EnterpriseService, WebAppAccessMode, WebAppSettings
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
from services.webapp_auth_service import WebAppAuthService
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ def validate_jwt_token[**P, R](
|
||||
|
||||
|
||||
def decode_jwt_token(app_code: str | None = None, user_id: str | None = None) -> tuple[App, EndUser]:
|
||||
system_features = FeatureService.get_system_features()
|
||||
webapp_auth_enabled = SystemFeatureService.is_webapp_auth_enabled()
|
||||
if not app_code:
|
||||
app_code = str(request.headers.get(HEADER_NAME_APP_CODE))
|
||||
try:
|
||||
@ -75,21 +75,19 @@ def decode_jwt_token(app_code: str | None = None, user_id: str | None = None) ->
|
||||
# for enterprise webapp auth
|
||||
app_web_auth_enabled = False
|
||||
webapp_settings = None
|
||||
if system_features.webapp_auth.enabled:
|
||||
if webapp_auth_enabled:
|
||||
app_id = AppService.get_app_id_by_code(app_code, session=db.session())
|
||||
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
|
||||
if not webapp_settings:
|
||||
raise NotFound("Web app settings not found.")
|
||||
app_web_auth_enabled = webapp_settings.access_mode != WebAppAccessMode.PUBLIC
|
||||
|
||||
_validate_webapp_token(decoded, app_web_auth_enabled, system_features.webapp_auth.enabled)
|
||||
_validate_user_accessibility(
|
||||
decoded, app_code, app_web_auth_enabled, system_features.webapp_auth.enabled, webapp_settings
|
||||
)
|
||||
_validate_webapp_token(decoded, app_web_auth_enabled, webapp_auth_enabled)
|
||||
_validate_user_accessibility(decoded, app_code, app_web_auth_enabled, webapp_auth_enabled, webapp_settings)
|
||||
|
||||
return app_model, end_user
|
||||
except Unauthorized as e:
|
||||
if system_features.webapp_auth.enabled:
|
||||
if webapp_auth_enabled:
|
||||
if not app_code:
|
||||
raise Unauthorized("Please re-login to access the web app.")
|
||||
app_id = AppService.get_app_id_by_code(app_code, session=db.session())
|
||||
|
||||
@ -69,9 +69,9 @@ def check_credential_policy_compliance(
|
||||
CheckCredentialPolicyComplianceRequest,
|
||||
PluginManagerService,
|
||||
)
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
if not FeatureService.is_plugin_manager_enabled() or not credential_id:
|
||||
if not SystemFeatureService.is_plugin_manager_enabled() or not credential_id:
|
||||
return
|
||||
|
||||
# Check if credential exists in database first (if requested)
|
||||
|
||||
@ -69,7 +69,7 @@ from services.enterprise.plugin_manager_service import (
|
||||
)
|
||||
from services.entities.feature_entities import PluginInstallationPermissionModel, PluginInstallationScope
|
||||
from services.errors.plugin import PluginInstallationForbiddenError
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_provider_entities_adapter: TypeAdapter[list[PluginModelProviderDeclaration]] = TypeAdapter(
|
||||
@ -667,7 +667,7 @@ class PluginService:
|
||||
@staticmethod
|
||||
def _get_plugin_installation_permission() -> PluginInstallationPermissionModel:
|
||||
"""Resolve the validated policy and reject deny-all before any installation side effect."""
|
||||
permission = FeatureService.get_plugin_installation_permission()
|
||||
permission = SystemFeatureService.get_plugin_installation_permission()
|
||||
if permission.plugin_installation_scope == PluginInstallationScope.NONE:
|
||||
raise PluginInstallationForbiddenError("Installing plugins is not allowed")
|
||||
return permission
|
||||
|
||||
@ -6,9 +6,17 @@ from typing import Any
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.agent.publish_visibility import workflow_callable_active_snapshot_filter
|
||||
from core.workflow.graph_topology import WorkflowGraphTopology
|
||||
from graphon.enums import BuiltinNodeTypes
|
||||
from models.agent import Agent, AgentConfigSnapshot, AgentStatus, WorkflowAgentBindingType, WorkflowAgentNodeBinding
|
||||
from models.agent import (
|
||||
Agent,
|
||||
AgentConfigSnapshot,
|
||||
AgentScope,
|
||||
AgentStatus,
|
||||
WorkflowAgentBindingType,
|
||||
WorkflowAgentNodeBinding,
|
||||
)
|
||||
from models.agent_config_entities import (
|
||||
AgentFileRefConfig,
|
||||
AgentHumanContactConfig,
|
||||
@ -127,21 +135,28 @@ class WorkflowAgentNodeValidator:
|
||||
topology: _WorkflowGraphTopology | None = None,
|
||||
require_agent_model: bool = True,
|
||||
) -> None:
|
||||
"""Validate binding ownership, publication state, Agent Soul, and node-job references."""
|
||||
|
||||
if binding.agent_id is None:
|
||||
raise WorkflowAgentNodeValidationError(f"Workflow Agent node {binding.node_id} is missing agent binding.")
|
||||
|
||||
agent = session.scalar(
|
||||
select(Agent)
|
||||
.where(
|
||||
Agent.tenant_id == binding.tenant_id,
|
||||
Agent.id == binding.agent_id,
|
||||
)
|
||||
.limit(1)
|
||||
agent_stmt = select(Agent).where(
|
||||
Agent.tenant_id == binding.tenant_id,
|
||||
Agent.id == binding.agent_id,
|
||||
)
|
||||
if agent is None or agent.status == AgentStatus.ARCHIVED:
|
||||
raise WorkflowAgentNodeValidationError(
|
||||
f"Workflow Agent node {binding.node_id} references an unavailable agent."
|
||||
if binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT:
|
||||
agent_stmt = agent_stmt.where(
|
||||
Agent.scope == AgentScope.ROSTER,
|
||||
workflow_callable_active_snapshot_filter(),
|
||||
)
|
||||
agent = session.scalar(agent_stmt.limit(1))
|
||||
if agent is None or agent.status == AgentStatus.ARCHIVED:
|
||||
availability = (
|
||||
"an unavailable or unpublished roster agent"
|
||||
if binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT
|
||||
else "an unavailable agent"
|
||||
)
|
||||
raise WorkflowAgentNodeValidationError(f"Workflow Agent node {binding.node_id} references {availability}.")
|
||||
|
||||
snapshot_id = (
|
||||
agent.active_config_snapshot_id
|
||||
|
||||
@ -3,7 +3,9 @@
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from typing import cast
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
from flask import Flask, current_app
|
||||
@ -19,6 +21,7 @@ from enums import DeploymentEdition, WebAppAccessMode
|
||||
from extensions.ext_redis import RedisClientWrapper, redis_client
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from libs.helper import RateLimiter
|
||||
from libs.passport import PassportService
|
||||
from repositories.account_activation_repository import SQLAlchemyAccountActivationRepository
|
||||
from repositories.account_integration_repository import SQLAlchemyAccountIntegrationRepository
|
||||
from repositories.account_repository import SQLAlchemyAccountRepository
|
||||
@ -35,6 +38,7 @@ from repositories.step_by_step_tour_repository import SQLAlchemyStepByStepTourSt
|
||||
from repositories.tag_repository import TagRepository
|
||||
from repositories.trial_app_query_repository import TrialAppQueryRepository
|
||||
from repositories.trial_app_usage_repository import TrialAppUsageRepository
|
||||
from repositories.web_passport_repository import WebPassportRepository
|
||||
from repositories.webapp_access_query_repository import WebAppAccessQueryRepository
|
||||
from repositories.workflow_run_archive_repository import WorkflowRunArchiveBundleQueryRepository
|
||||
from repositories.workspace_member_query_repository import WorkspaceMemberQueryRepository
|
||||
@ -101,10 +105,10 @@ from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.errors.enterprise import EnterpriseServiceError
|
||||
from services.explore_banner_query_service import ExploreBannerQueryService
|
||||
from services.feature_query_service import FeatureQueryService
|
||||
from services.feature_service import FeatureService
|
||||
from services.feature_service_gateway import FeatureServiceGateway
|
||||
from services.file_service import FileService
|
||||
from services.init_validation_service import InitValidationService
|
||||
from services.inner_mail_service import InnerMailService
|
||||
from services.notification_gateway import BillingNotificationGateway
|
||||
from services.notification_service import NotificationService
|
||||
from services.notion_data_source_gateway import NotionDataSourceGateway
|
||||
@ -126,9 +130,15 @@ from services.schema_definition_service import SchemaDefinitionService
|
||||
from services.setup_adapters import RedisSetupLock, RegisterServiceAccountProvisioner
|
||||
from services.setup_service import SetupService
|
||||
from services.step_by_step_tour_service import StepByStepTourService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
from services.tag_application_service import TagApplicationService
|
||||
from services.trial_app_usage import TrialAppUsageRecorder
|
||||
from services.web_app_runtime_query_service import WebAppRuntimeQueryService
|
||||
from services.web_passport_gateways import (
|
||||
DeploymentWebPassportAuthGateway,
|
||||
PassportTokenGateway,
|
||||
)
|
||||
from services.web_passport_service import WebPassportService
|
||||
from services.webapp_access_query_service import (
|
||||
WebAppAccessQueryService,
|
||||
WebAppAccessUnavailableError,
|
||||
@ -138,6 +148,7 @@ from services.workspace_member_query_service import WorkspaceMemberQueryService
|
||||
from services.workspace_member_role_resolver import DeploymentWorkspaceMemberRoleResolver
|
||||
from services.workspace_plan_gateway import DeploymentWorkspacePlanGateway
|
||||
from services.workspace_query_service import WorkspaceQueryService
|
||||
from tasks.mail_inner_task import enqueue_inner_mail
|
||||
|
||||
_EXTENSION_KEY = "application_services"
|
||||
|
||||
@ -200,6 +211,8 @@ class ApplicationServices:
|
||||
workflow_run_archives: WorkflowRunArchiveService
|
||||
workspace_queries: WorkspaceQueryService
|
||||
workspace_member_queries: WorkspaceMemberQueryService
|
||||
inner_mail: InnerMailService
|
||||
web_passport: WebPassportService
|
||||
tags: TagApplicationService
|
||||
workflow_statistics: WorkflowStatisticQueryService
|
||||
|
||||
@ -257,7 +270,7 @@ def build_application_services(
|
||||
feature_gateway = FeatureServiceGateway()
|
||||
accounts = SQLAlchemyAccountRepository(session_factory=database_client)
|
||||
integrations = SQLAlchemyAccountIntegrationRepository(session_factory=database_client)
|
||||
trial_app_enabled = FeatureService.is_trial_app_enabled()
|
||||
trial_app_enabled = SystemFeatureService.is_trial_app_enabled()
|
||||
database_catalog = DatabaseRecommendedAppCatalogRepository(session_factory=database_client, redis=redis)
|
||||
builtin_catalog = BuiltinRecommendedAppCatalogGateway()
|
||||
remote_catalog = RemoteRecommendedAppCatalogGateway()
|
||||
@ -409,7 +422,7 @@ def build_application_services(
|
||||
data_source_oauth=_build_data_source_oauth_services(database_client=database_client),
|
||||
webapp_access=WebAppAccessQueryService(
|
||||
access=WebAppAccessQueryRepository(session_factory=database_client),
|
||||
webapp_auth_enabled=FeatureService.is_webapp_auth_enabled(),
|
||||
webapp_auth_enabled=SystemFeatureService.is_webapp_auth_enabled(deployment_edition=deployment_edition),
|
||||
access_mode_for_app=_get_enterprise_webapp_access_mode,
|
||||
is_user_allowed_for_app=_is_user_allowed_to_access_webapp,
|
||||
),
|
||||
@ -421,7 +434,7 @@ def build_application_services(
|
||||
),
|
||||
explore_banner_queries=ExploreBannerQueryService(
|
||||
banners=ExploreBannerQueryRepository(session_factory=database_client),
|
||||
enabled=FeatureService.is_explore_banner_enabled(),
|
||||
enabled=SystemFeatureService.is_explore_banner_enabled(),
|
||||
),
|
||||
schema_definitions=SchemaDefinitionService(source_factory=SchemaManager),
|
||||
setup=SetupService(
|
||||
@ -475,6 +488,20 @@ def build_application_services(
|
||||
),
|
||||
roles=DeploymentWorkspaceMemberRoleResolver(),
|
||||
),
|
||||
inner_mail=InnerMailService(dispatch=enqueue_inner_mail),
|
||||
web_passport=WebPassportService(
|
||||
passports=WebPassportRepository(
|
||||
session_factory=database_client,
|
||||
generate_session_id=lambda: str(uuid4()),
|
||||
),
|
||||
auth=DeploymentWebPassportAuthGateway(
|
||||
webapp_auth_enabled=SystemFeatureService.is_webapp_auth_enabled(deployment_edition=deployment_edition),
|
||||
get_app_access_mode=EnterpriseService.WebAppAuth.get_app_access_mode_by_id,
|
||||
),
|
||||
tokens=PassportTokenGateway(passport=PassportService()),
|
||||
now=lambda: datetime.now(UTC),
|
||||
access_token_expire_minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||
),
|
||||
tags=TagApplicationService(
|
||||
tags=TagRepository(session_factory=database_client),
|
||||
),
|
||||
|
||||
@ -18,7 +18,7 @@ from werkzeug.exceptions import NotFound
|
||||
from libs import jws
|
||||
from libs.token import is_secure
|
||||
from services.entities.feature_entities import LicenseStatus
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -40,8 +40,7 @@ def enterprise_only[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
|
||||
@wraps(view)
|
||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||
settings = FeatureService.get_system_features()
|
||||
if settings.license.status not in _EE_ENABLED_STATUSES:
|
||||
if SystemFeatureService.get_license_status() not in _EE_ENABLED_STATUSES:
|
||||
raise NotFound()
|
||||
return view(*args, **kwargs)
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from extensions.ext_mail import mail
|
||||
from services.entities.feature_entities import BrandingModel
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
|
||||
class EmailType(StrEnum):
|
||||
@ -136,7 +136,7 @@ class FeatureBrandingService:
|
||||
|
||||
def get_branding_config(self) -> BrandingModel:
|
||||
"""Get branding configuration from feature service."""
|
||||
return FeatureService.get_system_features().branding
|
||||
return SystemFeatureService.get_branding()
|
||||
|
||||
|
||||
class EmailSender(Protocol):
|
||||
|
||||
@ -3,7 +3,7 @@ Workspace permission helper functions.
|
||||
|
||||
These helpers check both billing/plan level and workspace-specific policy level permissions.
|
||||
Checks are performed at two levels:
|
||||
1. Billing/plan level - via FeatureService (e.g., SANDBOX plan restrictions)
|
||||
1. Billing/plan level - via an injected owner-transfer policy value
|
||||
2. Workspace policy level - via EnterpriseService (admin-configured per workspace)
|
||||
"""
|
||||
|
||||
@ -14,7 +14,6 @@ from werkzeug.exceptions import Forbidden
|
||||
from configs import dify_config
|
||||
from enums import DeploymentEdition
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -45,7 +44,11 @@ def check_workspace_member_invite_permission(workspace_id: str) -> None:
|
||||
logger.exception("Failed to check workspace invite permission for %s", workspace_id)
|
||||
|
||||
|
||||
def check_workspace_owner_transfer_permission(workspace_id: str) -> None:
|
||||
def check_workspace_owner_transfer_permission(
|
||||
workspace_id: str,
|
||||
*,
|
||||
owner_transfer_allowed: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Check if workspace allows owner transfer at both billing and policy levels.
|
||||
|
||||
@ -55,12 +58,12 @@ def check_workspace_owner_transfer_permission(workspace_id: str) -> None:
|
||||
|
||||
Args:
|
||||
workspace_id: The workspace ID to check permissions for
|
||||
owner_transfer_allowed: Whether the workspace plan permits ownership transfer
|
||||
|
||||
Raises:
|
||||
Forbidden: If either billing plan or workspace policy prohibits ownership transfer
|
||||
"""
|
||||
features = FeatureService.get_features(workspace_id, exclude_vector_space=True)
|
||||
if not features.is_allow_transfer_workspace:
|
||||
if not owner_transfer_allowed:
|
||||
raise Forbidden("Your current plan does not allow workspace ownership transfer")
|
||||
|
||||
# Check the enterprise workspace policy only in the Enterprise edition.
|
||||
|
||||
@ -212,17 +212,9 @@ class Dataset(Base):
|
||||
enable_api = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true"))
|
||||
is_multimodal = mapped_column(sa.Boolean, default=False, nullable=False, server_default=sa.text("false"))
|
||||
|
||||
@property
|
||||
def total_documents(self) -> int:
|
||||
return self.get_total_documents(session=db.session())
|
||||
|
||||
def get_total_documents(self, *, session: Session) -> int:
|
||||
return self.get_document_count(session=session)
|
||||
|
||||
@property
|
||||
def total_available_documents(self) -> int:
|
||||
return self.get_total_available_documents(session=db.session())
|
||||
|
||||
def get_total_available_documents(self, *, session: Session) -> int:
|
||||
return (
|
||||
session.scalar(
|
||||
@ -258,20 +250,12 @@ class Dataset(Base):
|
||||
def get_created_by_account(self, *, session: Session) -> Account | None:
|
||||
return session.get(Account, self.created_by)
|
||||
|
||||
@property
|
||||
def author_name(self) -> str | None:
|
||||
return self.get_author_name(session=db.session())
|
||||
|
||||
def get_author_name(self, *, session: Session) -> str | None:
|
||||
account = self.get_created_by_account(session=session)
|
||||
if account:
|
||||
return account.name
|
||||
return None
|
||||
|
||||
@property
|
||||
def latest_process_rule(self):
|
||||
return self.get_latest_process_rule(session=db.session())
|
||||
|
||||
def get_latest_process_rule(self, *, session: Session) -> "DatasetProcessRule | None":
|
||||
return session.scalar(
|
||||
select(DatasetProcessRule)
|
||||
@ -391,10 +375,6 @@ class Dataset(Base):
|
||||
|
||||
return tags or []
|
||||
|
||||
@property
|
||||
def external_knowledge_info(self) -> dict[str, Any] | None:
|
||||
return self.get_external_knowledge_info(session=db.session())
|
||||
|
||||
def get_external_knowledge_info(self, *, session: Session) -> dict[str, Any] | None:
|
||||
if self.provider != "external":
|
||||
return None
|
||||
@ -974,17 +954,15 @@ class DocumentSegment(TypeBase):
|
||||
"""Load the owning document with the caller-owned database session."""
|
||||
return session.get(Document, self.document_id)
|
||||
|
||||
@property
|
||||
def previous_segment(self):
|
||||
return db.session.scalar(
|
||||
def previous_segment(self, session: Session) -> "DocumentSegment | None":
|
||||
return session.scalar(
|
||||
select(DocumentSegment).where(
|
||||
DocumentSegment.document_id == self.document_id, DocumentSegment.position == self.position - 1
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def next_segment(self):
|
||||
return db.session.scalar(
|
||||
def next_segment(self, session: Session) -> "DocumentSegment | None":
|
||||
return session.scalar(
|
||||
select(DocumentSegment).where(
|
||||
DocumentSegment.document_id == self.document_id, DocumentSegment.position == self.position + 1
|
||||
)
|
||||
@ -1204,9 +1182,8 @@ class AppDatasetJoin(TypeBase):
|
||||
DateTime, nullable=False, server_default=sa.func.current_timestamp(), init=False
|
||||
)
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
return db.session.get(App, self.app_id)
|
||||
def app(self, session: Session) -> App | None:
|
||||
return session.get(App, self.app_id)
|
||||
|
||||
|
||||
class DatasetQuery(TypeBase):
|
||||
|
||||
@ -217,7 +217,7 @@ storage = [
|
||||
############################################################
|
||||
# [ Tools ] dependency group
|
||||
############################################################
|
||||
tools = ["cloudscraper>=1.2.71,<2.0.0", "nltk>=3.10.0,<4.0.0"]
|
||||
tools = ["cloudscraper>=1.2.71,<2.0.0", "nltk>=3.10.3,<4.0.0"]
|
||||
|
||||
############################################################
|
||||
# [ VDB ] workspace plugins — hollow packages under providers/vdb/*
|
||||
|
||||
150
api/repositories/web_passport_repository.py
Normal file
150
api/repositories/web_passport_repository.py
Normal file
@ -0,0 +1,150 @@
|
||||
"""SQLAlchemy persistence adapter for web passport issuance."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from models.enums import AppStatus, EndUserType
|
||||
from models.model import App, EndUser, Site
|
||||
from services.entities.passport_entities import EndUserRecord, WebAppRecord, WebPassportEndUserResolution
|
||||
|
||||
|
||||
class WebPassportRepository:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
session_factory: sessionmaker[Session],
|
||||
generate_session_id: Callable[[], str],
|
||||
) -> None:
|
||||
self._session_factory = session_factory
|
||||
self._generate_session_id = generate_session_id
|
||||
|
||||
def get_active_web_app(self, app_code: str) -> WebAppRecord | None:
|
||||
stmt = self._active_web_app_stmt(app_code).limit(1)
|
||||
with self._session_factory() as session:
|
||||
row = session.execute(stmt).one_or_none()
|
||||
if row is None:
|
||||
return None
|
||||
site_id, app_id, tenant_id, persisted_app_code = row
|
||||
return WebAppRecord(
|
||||
site_id=str(site_id),
|
||||
app_id=str(app_id),
|
||||
tenant_id=str(tenant_id),
|
||||
app_code=str(persisted_app_code),
|
||||
)
|
||||
|
||||
def is_web_app_active(self, app: WebAppRecord) -> bool:
|
||||
with self._session_factory() as session:
|
||||
return self._is_web_app_active(session, app)
|
||||
|
||||
def resolve_standard_end_user(
|
||||
self,
|
||||
app: WebAppRecord,
|
||||
session_id: str | None,
|
||||
) -> WebPassportEndUserResolution:
|
||||
with self._session_factory.begin() as session:
|
||||
if not self._is_web_app_active(session, app):
|
||||
return WebPassportEndUserResolution(app_active=False, end_user=None)
|
||||
|
||||
if session_id:
|
||||
end_user = self._find_end_user_by_session_id(session, app, session_id)
|
||||
if end_user is not None:
|
||||
return WebPassportEndUserResolution(app_active=True, end_user=end_user)
|
||||
else:
|
||||
session_id = self._generate_unique_session_id(session)
|
||||
|
||||
end_user = self._create_anonymous_end_user(session, app, session_id)
|
||||
return WebPassportEndUserResolution(app_active=True, end_user=end_user)
|
||||
|
||||
def resolve_authenticated_end_user(
|
||||
self,
|
||||
app: WebAppRecord,
|
||||
*,
|
||||
end_user_id: str | None,
|
||||
session_id: str | None,
|
||||
) -> WebPassportEndUserResolution:
|
||||
with self._session_factory.begin() as session:
|
||||
if not self._is_web_app_active(session, app):
|
||||
return WebPassportEndUserResolution(app_active=False, end_user=None)
|
||||
|
||||
end_user = None
|
||||
if session_id:
|
||||
end_user = self._find_end_user_by_session_id(session, app, session_id)
|
||||
if end_user is None:
|
||||
end_user = self._create_anonymous_end_user(session, app, session_id)
|
||||
elif end_user_id:
|
||||
end_user = self._find_end_user_by_id(session, app, end_user_id)
|
||||
|
||||
return WebPassportEndUserResolution(app_active=True, end_user=end_user)
|
||||
|
||||
@staticmethod
|
||||
def _active_web_app_stmt(app_code: str):
|
||||
return (
|
||||
select(Site.id, App.id, App.tenant_id, Site.code)
|
||||
.join(App, App.id == Site.app_id)
|
||||
.where(
|
||||
Site.code == app_code,
|
||||
Site.status == AppStatus.NORMAL,
|
||||
App.status == AppStatus.NORMAL,
|
||||
App.enable_site.is_(True),
|
||||
)
|
||||
)
|
||||
|
||||
def _is_web_app_active(self, session: Session, app: WebAppRecord) -> bool:
|
||||
stmt = self._active_web_app_stmt(app.app_code).where(
|
||||
Site.id == app.site_id,
|
||||
App.id == app.app_id,
|
||||
App.tenant_id == app.tenant_id,
|
||||
)
|
||||
return session.execute(stmt.limit(1)).one_or_none() is not None
|
||||
|
||||
@staticmethod
|
||||
def _find_end_user_by_id(session: Session, app: WebAppRecord, end_user_id: str) -> EndUserRecord | None:
|
||||
persisted_id = session.scalar(
|
||||
select(EndUser.id).where(
|
||||
EndUser.id == end_user_id,
|
||||
EndUser.tenant_id == app.tenant_id,
|
||||
EndUser.app_id == app.app_id,
|
||||
)
|
||||
)
|
||||
return EndUserRecord(id=persisted_id) if persisted_id is not None else None
|
||||
|
||||
@staticmethod
|
||||
def _find_end_user_by_session_id(
|
||||
session: Session,
|
||||
app: WebAppRecord,
|
||||
session_id: str,
|
||||
) -> EndUserRecord | None:
|
||||
end_user_id = session.scalar(
|
||||
select(EndUser.id).where(
|
||||
EndUser.session_id == session_id,
|
||||
EndUser.tenant_id == app.tenant_id,
|
||||
EndUser.app_id == app.app_id,
|
||||
)
|
||||
)
|
||||
return EndUserRecord(id=end_user_id) if end_user_id is not None else None
|
||||
|
||||
@staticmethod
|
||||
def _create_anonymous_end_user(
|
||||
session: Session,
|
||||
app: WebAppRecord,
|
||||
session_id: str,
|
||||
) -> EndUserRecord:
|
||||
end_user = EndUser(
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.app_id,
|
||||
type=EndUserType.BROWSER,
|
||||
is_anonymous=True,
|
||||
session_id=session_id,
|
||||
)
|
||||
session.add(end_user)
|
||||
session.flush()
|
||||
return EndUserRecord(id=end_user.id)
|
||||
|
||||
def _generate_unique_session_id(self, session: Session) -> str:
|
||||
while True:
|
||||
session_id = self._generate_session_id()
|
||||
stmt = select(func.count()).select_from(EndUser).where(EndUser.session_id == session_id)
|
||||
if not session.scalar(stmt):
|
||||
return session_id
|
||||
@ -79,8 +79,8 @@ from services.errors.account import (
|
||||
SeatsLimitExceededError,
|
||||
)
|
||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
||||
from services.feature_service import FeatureService
|
||||
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
from services.telemetry_service import CommunityTelemetryService
|
||||
from tasks.mail_change_mail_task import (
|
||||
send_change_mail_completed_notification_task,
|
||||
@ -446,7 +446,7 @@ class AccountService:
|
||||
session: Session,
|
||||
) -> Account:
|
||||
"""Create an account, preferring explicit user timezone over language-derived defaults."""
|
||||
if not FeatureService.get_system_features().is_allow_register and not is_setup:
|
||||
if not SystemFeatureService.is_registration_allowed() and not is_setup:
|
||||
from controllers.console.error import AccountNotFound
|
||||
|
||||
raise AccountNotFound()
|
||||
@ -458,7 +458,7 @@ class AccountService:
|
||||
# account into another workspace does not pass through here and costs no seat.
|
||||
# get_license() carries the full license payload that server-side enforcement needs;
|
||||
# the public system-features endpoint exposes only license status.
|
||||
if not FeatureService.get_license().seats.is_available():
|
||||
if not SystemFeatureService.get_license().seats.is_available():
|
||||
raise SeatsLimitExceededError("licensed seats limit exceeded")
|
||||
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and BillingService.is_email_in_freeze(email):
|
||||
@ -1139,7 +1139,7 @@ class TenantService:
|
||||
session: Session,
|
||||
) -> Tenant:
|
||||
"""Create tenant"""
|
||||
if not FeatureService.is_workspace_creation_allowed() and not is_setup and not is_from_dashboard:
|
||||
if not SystemFeatureService.is_workspace_creation_allowed() and not is_setup and not is_from_dashboard:
|
||||
from controllers.console.error import NotAllowedCreateWorkspace
|
||||
|
||||
raise NotAllowedCreateWorkspace()
|
||||
@ -1202,10 +1202,10 @@ class TenantService:
|
||||
owner. It persists the legacy membership before creating the matching
|
||||
RBAC role binding, then makes the workspace current for the account.
|
||||
"""
|
||||
if not FeatureService.is_workspace_creation_allowed() and not is_setup and not is_from_dashboard:
|
||||
if not SystemFeatureService.is_workspace_creation_allowed() and not is_setup and not is_from_dashboard:
|
||||
raise WorkSpaceNotAllowedCreateError()
|
||||
|
||||
workspaces = FeatureService.get_license().workspaces
|
||||
workspaces = SystemFeatureService.get_license().workspaces
|
||||
if not workspaces.is_available():
|
||||
raise WorkspacesLimitExceededError()
|
||||
|
||||
@ -1988,9 +1988,9 @@ class RegisterService:
|
||||
AccountService.link_account_integrate(provider, open_id, account, session=session)
|
||||
|
||||
if (
|
||||
FeatureService.is_workspace_creation_allowed()
|
||||
SystemFeatureService.is_workspace_creation_allowed()
|
||||
and create_workspace_required
|
||||
and FeatureService.get_license().workspaces.is_available()
|
||||
and SystemFeatureService.get_license().workspaces.is_available()
|
||||
):
|
||||
try:
|
||||
TenantService.create_owner_tenant(account, session=session)
|
||||
|
||||
@ -44,7 +44,7 @@ from services.agent.workspace_service import AgentWorkspaceNotFoundError, AgentW
|
||||
from services.app_service import AppService, CreateAppParams
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.entities.agent_entities import RosterAgentCreatePayload, RosterAgentUpdatePayload
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
from tasks.collect_agent_resources_task import enqueue_agent_resource_collection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -1120,7 +1120,7 @@ class AgentRosterService:
|
||||
source_include_draft=not source_agent.active_config_is_published,
|
||||
)
|
||||
self._session.commit()
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
if SystemFeatureService.is_webapp_auth_enabled():
|
||||
try:
|
||||
original_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(source_app.id)
|
||||
access_mode = original_settings.access_mode
|
||||
|
||||
@ -49,8 +49,8 @@ from services.agent.workspace_service import AgentWorkspaceService
|
||||
from services.billing_service import BillingService
|
||||
from services.enterprise import rbac_service as enterprise_rbac_service
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.feature_service import FeatureService
|
||||
from services.openapi.visibility import apply_openapi_gate, is_openapi_visible
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
from services.tag_service import TagService
|
||||
from tasks.collect_agent_resources_task import enqueue_agent_resource_collection
|
||||
from tasks.remove_app_and_related_data_task import remove_app_and_related_data_task
|
||||
@ -703,7 +703,7 @@ class AppService:
|
||||
app.id,
|
||||
)
|
||||
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
if SystemFeatureService.is_webapp_auth_enabled():
|
||||
# update web app setting as private
|
||||
EnterpriseService.WebAppAuth.update_app_access_mode(app.id, "private")
|
||||
|
||||
@ -1155,7 +1155,7 @@ class AppService:
|
||||
)
|
||||
|
||||
# clean up web app settings
|
||||
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||
if SystemFeatureService.is_webapp_auth_enabled():
|
||||
EnterpriseService.WebAppAuth.cleanup_webapp(app.id)
|
||||
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
|
||||
|
||||
@ -478,7 +478,6 @@ _LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS: list[str] = [
|
||||
|
||||
_LEGACY_APP_OWNER_KEYS: list[str] = [
|
||||
"app.acl.preview",
|
||||
"app.acl.access_point_manage",
|
||||
"app.acl.view_layout",
|
||||
"app.acl.test_and_run",
|
||||
"app.acl.edit",
|
||||
@ -494,7 +493,6 @@ _LEGACY_APP_OWNER_KEYS: list[str] = [
|
||||
_LEGACY_APP_ADMIN_KEYS: list[str] = [
|
||||
"app.acl.preview",
|
||||
"app.acl.view_layout",
|
||||
"app.acl.access_point_manage",
|
||||
"app.acl.test_and_run",
|
||||
"app.acl.edit",
|
||||
"app.acl.import_export_dsl",
|
||||
@ -509,7 +507,6 @@ _LEGACY_APP_ADMIN_KEYS: list[str] = [
|
||||
|
||||
_LEGACY_APP_EDITOR_KEYS: list[str] = [
|
||||
"app.acl.preview",
|
||||
"app.acl.access_point_manage",
|
||||
"app.acl.view_layout",
|
||||
"app.acl.test_and_run",
|
||||
"app.acl.edit",
|
||||
|
||||
12
api/services/entities/mail_entities.py
Normal file
12
api/services/entities/mail_entities.py
Normal file
@ -0,0 +1,12 @@
|
||||
"""Framework-neutral data contracts for internal mail delivery."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InnerMailMessage:
|
||||
recipients: tuple[str, ...]
|
||||
subject: str
|
||||
body: str
|
||||
substitutions: dict[str, Any] | None = None
|
||||
47
api/services/entities/passport_entities.py
Normal file
47
api/services/entities/passport_entities.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""Framework-neutral data contracts for web passport issuance."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WebAppRecord:
|
||||
site_id: str
|
||||
app_id: str
|
||||
tenant_id: str
|
||||
app_code: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EndUserRecord:
|
||||
id: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WebPassportEndUserResolution:
|
||||
app_active: bool
|
||||
end_user: EndUserRecord | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WebPassportRequest:
|
||||
app_code: str
|
||||
user_session_id: str | None
|
||||
access_token: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WebPassportResult:
|
||||
access_token: str
|
||||
|
||||
|
||||
class WebAppLoginClaims(BaseModel):
|
||||
token_source: str | None = None
|
||||
user_id: str | None = None
|
||||
end_user_id: str | None = None
|
||||
session_id: str | None = None
|
||||
auth_type: str | None = None
|
||||
exp: int | None = None
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
@ -36,10 +36,16 @@ class FeatureQueryService:
|
||||
self._app_dsl_version = app_dsl_version
|
||||
|
||||
def get_features(self, context: RequestContext) -> FeatureModel:
|
||||
return self._features.get_workspace_features(self._require_active_workspace(context))
|
||||
return self.get_workspace_features(self._require_active_workspace(context))
|
||||
|
||||
def get_workspace_features(self, workspace_id: str) -> FeatureModel:
|
||||
return self._features.get_workspace_features(workspace_id)
|
||||
|
||||
def get_vector_space(self, context: RequestContext) -> VectorSpaceLimitationModel:
|
||||
return self._features.get_vector_space(self._require_active_workspace(context))
|
||||
return self.get_workspace_vector_space(self._require_active_workspace(context))
|
||||
|
||||
def get_workspace_vector_space(self, workspace_id: str) -> VectorSpaceLimitationModel:
|
||||
return self._features.get_vector_space(workspace_id)
|
||||
|
||||
def get_trial_models(self, context: RequestContext) -> list[str]:
|
||||
return self._features.get_trial_models(self._require_active_workspace(context))
|
||||
@ -47,7 +53,7 @@ class FeatureQueryService:
|
||||
def get_app_dsl_version(self) -> str:
|
||||
return self._app_dsl_version
|
||||
|
||||
def get_system_features(self) -> SystemFeatureModel:
|
||||
def get_public_system_features(self) -> SystemFeatureModel:
|
||||
return self._features.get_public_system_features()
|
||||
|
||||
def get_license(self) -> LicenseModel:
|
||||
|
||||
@ -1,23 +1,9 @@
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
from configs import dify_config
|
||||
from enums import CloudPlan, DeploymentEdition, HostedTrialProvider
|
||||
from services.billing_service import BillingInfo, BillingService
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.entities import feature_entities
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _EnterprisePluginInstallationPermission(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
plugin_installation_scope: feature_entities.PluginInstallationScope = Field(alias="pluginInstallationScope")
|
||||
restrict_to_marketplace_only: bool = Field(alias="restrictToMarketplaceOnly", strict=True)
|
||||
|
||||
|
||||
class FeatureService:
|
||||
@classmethod
|
||||
@ -102,104 +88,6 @@ class FeatureService:
|
||||
return False
|
||||
return features.billing.enabled and features.billing.subscription.plan.is_paid
|
||||
|
||||
@classmethod
|
||||
def get_system_features(cls) -> feature_entities.SystemFeatureModel:
|
||||
system_features = feature_entities.SystemFeatureModel(deployment_edition=dify_config.DEPLOYMENT_EDITION)
|
||||
system_features.rbac_enabled = dify_config.RBAC_ENABLED
|
||||
|
||||
cls._fulfill_system_params_from_env(system_features)
|
||||
system_features.webapp_auth.enabled = cls.is_webapp_auth_enabled()
|
||||
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
|
||||
system_features.branding.enabled = True
|
||||
system_features.enable_change_email = False
|
||||
cls._fulfill_params_from_enterprise(system_features)
|
||||
|
||||
if dify_config.MARKETPLACE_ENABLED:
|
||||
system_features.enable_marketplace = True
|
||||
|
||||
if dify_config.CREATORS_PLATFORM_FEATURES_ENABLED:
|
||||
system_features.enable_creators_platform = True
|
||||
|
||||
return system_features
|
||||
|
||||
@classmethod
|
||||
def is_workspace_creation_allowed(cls) -> bool:
|
||||
"""Resolve the backend workspace-creation policy, including the Enterprise override."""
|
||||
is_allowed = dify_config.ALLOW_CREATE_WORKSPACE
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
||||
return is_allowed
|
||||
|
||||
enterprise_info = EnterpriseService.get_info()
|
||||
return bool(enterprise_info.get("IsAllowCreateWorkspace", is_allowed))
|
||||
|
||||
@classmethod
|
||||
def is_plugin_manager_enabled(cls) -> bool:
|
||||
"""Return whether Enterprise plugin credential policies must be enforced."""
|
||||
return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE
|
||||
|
||||
@classmethod
|
||||
def get_plugin_installation_permission(cls) -> feature_entities.PluginInstallationPermissionModel:
|
||||
"""Resolve the validated deployment-wide plugin installation policy."""
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
||||
return feature_entities.PluginInstallationPermissionModel()
|
||||
|
||||
return cls._resolve_plugin_installation_permission(EnterpriseService.get_info())
|
||||
|
||||
@classmethod
|
||||
def get_license(cls) -> feature_entities.LicenseModel:
|
||||
"""Return full license detail. Enterprise-only; requires an authenticated caller.
|
||||
|
||||
Non-enterprise deployments have no license, so an unconstrained default
|
||||
(unlimited seats/workspaces) is returned.
|
||||
"""
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
||||
return feature_entities.LicenseModel()
|
||||
license_model = cls._build_license(EnterpriseService.get_info())
|
||||
license_model.license_expiry_notice_enabled = dify_config.ENABLE_LICENSE_EXPIRY_NOTICE
|
||||
return license_model
|
||||
|
||||
@staticmethod
|
||||
def is_explore_banner_enabled() -> bool:
|
||||
return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.ENABLE_EXPLORE_BANNER
|
||||
|
||||
@staticmethod
|
||||
def is_webapp_auth_enabled() -> bool:
|
||||
return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE
|
||||
|
||||
@staticmethod
|
||||
def is_trial_app_enabled() -> bool:
|
||||
return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.ENABLE_TRIAL_APP
|
||||
|
||||
@classmethod
|
||||
def _fulfill_system_params_from_env(cls, system_features: feature_entities.SystemFeatureModel):
|
||||
system_features.enable_email_code_login = dify_config.ENABLE_EMAIL_CODE_LOGIN
|
||||
system_features.enable_email_password_login = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN
|
||||
system_features.enable_social_oauth_login = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN
|
||||
system_features.enable_collaboration_mode = dify_config.ENABLE_COLLABORATION_MODE
|
||||
system_features.is_allow_register = dify_config.ALLOW_REGISTER
|
||||
system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != ""
|
||||
system_features.enable_change_email = dify_config.ENABLE_CHANGE_EMAIL
|
||||
system_features.enable_explore_banner = cls.is_explore_banner_enabled()
|
||||
system_features.enable_learn_app = dify_config.ENABLE_LEARN_APP
|
||||
system_features.webapp_auth.allow_public_access = dify_config.WEBAPP_PUBLIC_ACCESS_ENABLED
|
||||
system_features.enable_step_by_step_tour = dify_config.ENABLE_STEP_BY_STEP_TOUR
|
||||
knowledge_fs_enabled = bool(
|
||||
dify_config.KNOWLEDGE_FS_ENABLED
|
||||
and (
|
||||
dify_config.DEPLOYMENT_EDITION in {DeploymentEdition.CLOUD, DeploymentEdition.ENTERPRISE}
|
||||
or dify_config.KNOWLEDGE_FS_COMMUNITY_DEV_ENABLED
|
||||
)
|
||||
)
|
||||
system_features.knowledge_fs_enabled = knowledge_fs_enabled
|
||||
system_features.knowledge_fs_upload_enabled = bool(
|
||||
knowledge_fs_enabled
|
||||
and dify_config.KNOWLEDGE_FS_BASE_URL
|
||||
and dify_config.KNOWLEDGE_FS_CAPABILITY_V2_ENABLED
|
||||
and dify_config.KNOWLEDGE_FS_CAPABILITY_V2_SIGNING_KID
|
||||
and dify_config.KNOWLEDGE_FS_CAPABILITY_V2_PRIVATE_KEY_PEM
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _fulfill_trial_models_from_env(cls, quota_types: tuple[str, ...] | None = None) -> list[str]:
|
||||
allowed_quota_types = quota_types or ("PAID", "TRIAL")
|
||||
@ -325,125 +213,3 @@ class FeatureService:
|
||||
# but feature API keeps LimitationModel.size as int for compatibility.
|
||||
vector_space.size = int(billing_info["vector_space"]["size"])
|
||||
vector_space.limit = billing_info["vector_space"]["limit"]
|
||||
|
||||
@classmethod
|
||||
def _build_license(cls, enterprise_info: dict) -> feature_entities.LicenseModel:
|
||||
license_model = feature_entities.LicenseModel()
|
||||
if license_info := enterprise_info.get("License"):
|
||||
license_model.status = feature_entities.LicenseStatus(
|
||||
license_info.get("status", feature_entities.LicenseStatus.INACTIVE)
|
||||
)
|
||||
license_model.expired_at = license_info.get("expiredAt", "")
|
||||
if workspaces_info := license_info.get("workspaces"):
|
||||
license_model.workspaces = feature_entities.LicenseLimitationModel(
|
||||
enabled=workspaces_info.get("enabled", False),
|
||||
limit=workspaces_info.get("limit", 0),
|
||||
size=workspaces_info.get("used", 0),
|
||||
)
|
||||
if seats_info := license_info.get("licensedSeats"):
|
||||
license_model.seats = feature_entities.LicenseLimitationModel(
|
||||
enabled=seats_info.get("enabled", False),
|
||||
limit=seats_info.get("limit", 0),
|
||||
size=seats_info.get("used", 0),
|
||||
)
|
||||
return license_model
|
||||
|
||||
@classmethod
|
||||
def _resolve_plugin_installation_permission(
|
||||
cls, enterprise_info: Mapping[str, object]
|
||||
) -> feature_entities.PluginInstallationPermissionModel:
|
||||
if "PluginInstallationPermission" not in enterprise_info:
|
||||
return feature_entities.PluginInstallationPermissionModel()
|
||||
|
||||
try:
|
||||
permission = _EnterprisePluginInstallationPermission.model_validate(
|
||||
enterprise_info["PluginInstallationPermission"]
|
||||
)
|
||||
except ValidationError as exc:
|
||||
# Do not attach the exception because it may contain raw Enterprise configuration values.
|
||||
logger.error( # noqa: TRY400
|
||||
"Invalid Enterprise plugin installation permission; denying all plugin installations: %s",
|
||||
exc.errors(include_input=False),
|
||||
)
|
||||
return feature_entities.PluginInstallationPermissionModel(
|
||||
plugin_installation_scope=feature_entities.PluginInstallationScope.NONE,
|
||||
restrict_to_marketplace_only=True,
|
||||
)
|
||||
|
||||
return feature_entities.PluginInstallationPermissionModel(
|
||||
plugin_installation_scope=permission.plugin_installation_scope,
|
||||
restrict_to_marketplace_only=permission.restrict_to_marketplace_only,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_sso_protocol(value: object, *, field_name: str) -> feature_entities.SSOProtocol | None:
|
||||
if value is None or (isinstance(value, str) and not value.strip()):
|
||||
return None
|
||||
|
||||
if not isinstance(value, str):
|
||||
logger.error("Invalid Enterprise SSO protocol for %s; disabling the protocol", field_name)
|
||||
return None
|
||||
|
||||
try:
|
||||
return feature_entities.SSOProtocol(value)
|
||||
except ValueError:
|
||||
logger.error( # noqa: TRY400
|
||||
"Invalid Enterprise SSO protocol for %s; disabling the protocol", field_name
|
||||
)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _fulfill_params_from_enterprise(cls, features: feature_entities.SystemFeatureModel):
|
||||
enterprise_info = EnterpriseService.get_info()
|
||||
|
||||
if "SSOEnforcedForSignin" in enterprise_info:
|
||||
features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"]
|
||||
|
||||
features.sso_enforced_for_signin_protocol = cls._resolve_sso_protocol(
|
||||
enterprise_info.get("SSOEnforcedForSigninProtocol"),
|
||||
field_name="SSOEnforcedForSigninProtocol",
|
||||
)
|
||||
|
||||
if "EnableEmailCodeLogin" in enterprise_info:
|
||||
features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"]
|
||||
|
||||
if "EnableEmailPasswordLogin" in enterprise_info:
|
||||
features.enable_email_password_login = enterprise_info["EnableEmailPasswordLogin"]
|
||||
|
||||
if "IsAllowRegister" in enterprise_info:
|
||||
features.is_allow_register = enterprise_info["IsAllowRegister"]
|
||||
|
||||
if "EnableAppDeploy" in enterprise_info:
|
||||
features.enable_app_deploy = enterprise_info["EnableAppDeploy"]
|
||||
|
||||
if "Branding" in enterprise_info:
|
||||
features.branding.application_title = enterprise_info["Branding"].get("applicationTitle", "")
|
||||
features.branding.login_page_logo = enterprise_info["Branding"].get("loginPageLogo", "")
|
||||
features.branding.workspace_logo = enterprise_info["Branding"].get("workspaceLogo", "")
|
||||
features.branding.favicon = enterprise_info["Branding"].get("favicon", "")
|
||||
|
||||
if "WebAppAuth" in enterprise_info:
|
||||
features.webapp_auth.allow_sso = enterprise_info["WebAppAuth"].get("allowSso", False)
|
||||
features.webapp_auth.allow_email_code_login = enterprise_info["WebAppAuth"].get(
|
||||
"allowEmailCodeLogin", False
|
||||
)
|
||||
features.webapp_auth.allow_email_password_login = enterprise_info["WebAppAuth"].get(
|
||||
"allowEmailPasswordLogin", False
|
||||
)
|
||||
features.webapp_auth.sso_config.protocol = cls._resolve_sso_protocol(
|
||||
enterprise_info.get("SSOEnforcedForWebProtocol"),
|
||||
field_name="SSOEnforcedForWebProtocol",
|
||||
)
|
||||
|
||||
# SECURITY NOTE: system-features is unauthenticated, so it exposes only license
|
||||
# *status* — enough for the login page to detect an expired/inactive license after
|
||||
# force-logout. Full license detail (expiry, workspace/seat usage) is served
|
||||
# separately by get_license() behind an authenticated endpoint.
|
||||
if license_info := enterprise_info.get("License"):
|
||||
features.license = feature_entities.LicenseStatusModel(
|
||||
status=feature_entities.LicenseStatus(
|
||||
license_info.get("status", feature_entities.LicenseStatus.INACTIVE)
|
||||
)
|
||||
)
|
||||
|
||||
features.plugin_installation_permission = cls._resolve_plugin_installation_permission(enterprise_info)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Feature-query gateway backed by the existing FeatureService."""
|
||||
"""Feature-query gateway combining workspace and deployment feature providers."""
|
||||
|
||||
from typing import override
|
||||
|
||||
@ -10,10 +10,11 @@ from services.entities.feature_entities import (
|
||||
)
|
||||
from services.feature_query_service import FeatureQueryGateway
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
|
||||
class FeatureServiceGateway(FeatureQueryGateway):
|
||||
"""Read dynamic feature resources through FeatureService."""
|
||||
"""Read workspace features from FeatureService and deployment features from SystemFeatureService."""
|
||||
|
||||
@override
|
||||
def get_workspace_features(self, workspace_id: str) -> FeatureModel:
|
||||
@ -29,8 +30,8 @@ class FeatureServiceGateway(FeatureQueryGateway):
|
||||
|
||||
@override
|
||||
def get_public_system_features(self) -> SystemFeatureModel:
|
||||
return FeatureService.get_system_features()
|
||||
return SystemFeatureService.get_public_system_features()
|
||||
|
||||
@override
|
||||
def get_license(self) -> LicenseModel:
|
||||
return FeatureService.get_license()
|
||||
return SystemFeatureService.get_license()
|
||||
|
||||
17
api/services/inner_mail_service.py
Normal file
17
api/services/inner_mail_service.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""Application service for mail received through the inner API."""
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from services.entities.mail_entities import InnerMailMessage
|
||||
|
||||
|
||||
class InnerMailDispatcher(Protocol):
|
||||
def __call__(self, message: InnerMailMessage) -> None: ...
|
||||
|
||||
|
||||
class InnerMailService:
|
||||
def __init__(self, *, dispatch: InnerMailDispatcher) -> None:
|
||||
self._dispatch = dispatch
|
||||
|
||||
def send(self, message: InnerMailMessage) -> None:
|
||||
self._dispatch(message)
|
||||
@ -8,7 +8,7 @@ from libs.helper import escape_like_pattern
|
||||
from models import App, AppModelConfig, InstalledApp, Workflow
|
||||
from models.model import AppMode
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
|
||||
class InstalledAppCursor(BaseModel):
|
||||
@ -118,7 +118,7 @@ class InstalledAppService:
|
||||
escaped_name = escape_like_pattern(normalized_name)
|
||||
stmt = stmt.where(App.name.ilike(f"%{escaped_name}%", escape="\\"))
|
||||
|
||||
webapp_auth_enabled = FeatureService.get_system_features().webapp_auth.enabled
|
||||
webapp_auth_enabled = SystemFeatureService.is_webapp_auth_enabled()
|
||||
scan_size = limit * 2 if webapp_auth_enabled else limit + 1
|
||||
visible_rows: list[tuple[InstalledApp, App]] = []
|
||||
scan_cursor = cursor
|
||||
|
||||
@ -5,8 +5,7 @@ the EE blueprint chain is what gives CE deploys no callers on this surface
|
||||
in practice, but the explicit short-circuit avoids any test/fixture that
|
||||
flips the surface on without flipping the license.
|
||||
|
||||
Reuses ``FeatureService.get_system_features()`` so the license status
|
||||
travels the same path as the console reads.
|
||||
Uses the narrow system license policy shared with Console admission.
|
||||
|
||||
Companion to ``controllers.console.wraps.enterprise_license_required`` —
|
||||
that one is for console (cookie-authed, force-logout 401). This one is
|
||||
@ -24,7 +23,7 @@ from werkzeug.exceptions import Forbidden
|
||||
from configs import dify_config
|
||||
from enums import DeploymentEdition
|
||||
from services.entities.feature_entities import LicenseStatus
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -47,8 +46,8 @@ def license_required[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
||||
|
||||
def _is_license_valid() -> bool:
|
||||
try:
|
||||
features = FeatureService.get_system_features()
|
||||
license_status = SystemFeatureService.get_license_status()
|
||||
except Exception:
|
||||
logger.exception("license_gate: FeatureService.get_system_features failed")
|
||||
logger.exception("license_gate: SystemFeatureService.get_license_status failed")
|
||||
return False
|
||||
return features.license.status in _VALID_LICENSE_STATUSES
|
||||
return license_status in _VALID_LICENSE_STATUSES
|
||||
|
||||
301
api/services/system_feature_service.py
Normal file
301
api/services/system_feature_service.py
Normal file
@ -0,0 +1,301 @@
|
||||
"""Deployment-wide feature policies and the public system-features snapshot."""
|
||||
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
from configs import dify_config
|
||||
from enums import DeploymentEdition
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.entities import feature_entities
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _EnterprisePluginInstallationPermission(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
plugin_installation_scope: feature_entities.PluginInstallationScope = Field(alias="pluginInstallationScope")
|
||||
restrict_to_marketplace_only: bool = Field(alias="restrictToMarketplaceOnly", strict=True)
|
||||
|
||||
|
||||
class SystemFeatureService:
|
||||
"""Resolve deployment-wide policies without exposing the public response DTO internally."""
|
||||
|
||||
@classmethod
|
||||
def get_public_system_features(cls) -> feature_entities.SystemFeatureModel:
|
||||
"""Build the non-sensitive bootstrap snapshot shared by Console and Web."""
|
||||
system_features = feature_entities.SystemFeatureModel(deployment_edition=dify_config.DEPLOYMENT_EDITION)
|
||||
system_features.rbac_enabled = dify_config.RBAC_ENABLED
|
||||
|
||||
cls._fulfill_system_params_from_env(system_features)
|
||||
|
||||
if cls.is_webapp_auth_enabled():
|
||||
system_features.branding.enabled = True
|
||||
system_features.webapp_auth.enabled = True
|
||||
system_features.enable_change_email = False
|
||||
cls._fulfill_params_from_enterprise(system_features)
|
||||
|
||||
if dify_config.MARKETPLACE_ENABLED:
|
||||
system_features.enable_marketplace = True
|
||||
|
||||
if dify_config.CREATORS_PLATFORM_FEATURES_ENABLED:
|
||||
system_features.enable_creators_platform = True
|
||||
|
||||
return system_features
|
||||
|
||||
@classmethod
|
||||
def is_registration_allowed(cls) -> bool:
|
||||
"""Return the effective registration policy, including the Enterprise override."""
|
||||
is_allowed = dify_config.ALLOW_REGISTER
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
||||
return is_allowed
|
||||
|
||||
enterprise_info = EnterpriseService.get_info()
|
||||
return bool(enterprise_info.get("IsAllowRegister", is_allowed))
|
||||
|
||||
@classmethod
|
||||
def is_email_password_login_enabled(cls) -> bool:
|
||||
"""Return the effective password-login policy, including the Enterprise override."""
|
||||
is_enabled = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
||||
return is_enabled
|
||||
|
||||
enterprise_info = EnterpriseService.get_info()
|
||||
return bool(enterprise_info.get("EnableEmailPasswordLogin", is_enabled))
|
||||
|
||||
@staticmethod
|
||||
def is_change_email_enabled() -> bool:
|
||||
"""Return whether Console accounts may change their email address."""
|
||||
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
|
||||
return False
|
||||
return dify_config.ENABLE_CHANGE_EMAIL
|
||||
|
||||
@staticmethod
|
||||
def is_webapp_auth_enabled(*, deployment_edition: DeploymentEdition | None = None) -> bool:
|
||||
"""Return whether deployment-level WebApp authentication integration is enabled."""
|
||||
edition = deployment_edition if deployment_edition is not None else dify_config.DEPLOYMENT_EDITION
|
||||
return edition == DeploymentEdition.ENTERPRISE
|
||||
|
||||
@classmethod
|
||||
def get_license_status(cls) -> feature_entities.LicenseStatus:
|
||||
"""Return the deployment license status used by internal admission policies."""
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
||||
return feature_entities.LicenseStatus.NONE
|
||||
return cls._resolve_license_status(EnterpriseService.get_info())
|
||||
|
||||
@classmethod
|
||||
def get_branding(cls) -> feature_entities.BrandingModel:
|
||||
"""Return the deployment branding used by server-rendered email."""
|
||||
branding = feature_entities.BrandingModel(enabled=cls.is_webapp_auth_enabled())
|
||||
if not branding.enabled:
|
||||
return branding
|
||||
|
||||
enterprise_info = EnterpriseService.get_info()
|
||||
if branding_info := enterprise_info.get("Branding"):
|
||||
branding.application_title = branding_info.get("applicationTitle", "")
|
||||
branding.login_page_logo = branding_info.get("loginPageLogo", "")
|
||||
branding.workspace_logo = branding_info.get("workspaceLogo", "")
|
||||
branding.favicon = branding_info.get("favicon", "")
|
||||
return branding
|
||||
|
||||
@classmethod
|
||||
def is_workspace_creation_allowed(cls) -> bool:
|
||||
"""Resolve the backend workspace-creation policy, including the Enterprise override."""
|
||||
is_allowed = dify_config.ALLOW_CREATE_WORKSPACE
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
||||
return is_allowed
|
||||
|
||||
enterprise_info = EnterpriseService.get_info()
|
||||
return bool(enterprise_info.get("IsAllowCreateWorkspace", is_allowed))
|
||||
|
||||
@staticmethod
|
||||
def is_plugin_manager_enabled() -> bool:
|
||||
"""Return whether Enterprise plugin credential policies must be enforced."""
|
||||
return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE
|
||||
|
||||
@classmethod
|
||||
def get_plugin_installation_permission(cls) -> feature_entities.PluginInstallationPermissionModel:
|
||||
"""Resolve the validated deployment-wide plugin installation policy."""
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
||||
return feature_entities.PluginInstallationPermissionModel()
|
||||
|
||||
return cls._resolve_plugin_installation_permission(EnterpriseService.get_info())
|
||||
|
||||
@classmethod
|
||||
def get_license(cls) -> feature_entities.LicenseModel:
|
||||
"""Return full license detail for authenticated server-side consumers."""
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
||||
return feature_entities.LicenseModel()
|
||||
license_model = cls._build_license(EnterpriseService.get_info())
|
||||
license_model.license_expiry_notice_enabled = dify_config.ENABLE_LICENSE_EXPIRY_NOTICE
|
||||
return license_model
|
||||
|
||||
@staticmethod
|
||||
def is_explore_banner_enabled() -> bool:
|
||||
return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.ENABLE_EXPLORE_BANNER
|
||||
|
||||
@staticmethod
|
||||
def is_trial_app_enabled() -> bool:
|
||||
return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.ENABLE_TRIAL_APP
|
||||
|
||||
@classmethod
|
||||
def _fulfill_system_params_from_env(cls, system_features: feature_entities.SystemFeatureModel) -> None:
|
||||
system_features.enable_email_code_login = dify_config.ENABLE_EMAIL_CODE_LOGIN
|
||||
system_features.enable_email_password_login = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN
|
||||
system_features.enable_social_oauth_login = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN
|
||||
system_features.enable_collaboration_mode = dify_config.ENABLE_COLLABORATION_MODE
|
||||
system_features.is_allow_register = dify_config.ALLOW_REGISTER
|
||||
system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != ""
|
||||
system_features.enable_change_email = dify_config.ENABLE_CHANGE_EMAIL
|
||||
system_features.enable_explore_banner = cls.is_explore_banner_enabled()
|
||||
system_features.enable_learn_app = dify_config.ENABLE_LEARN_APP
|
||||
system_features.webapp_auth.allow_public_access = dify_config.WEBAPP_PUBLIC_ACCESS_ENABLED
|
||||
system_features.enable_step_by_step_tour = dify_config.ENABLE_STEP_BY_STEP_TOUR
|
||||
knowledge_fs_enabled = bool(
|
||||
dify_config.KNOWLEDGE_FS_ENABLED
|
||||
and (
|
||||
dify_config.DEPLOYMENT_EDITION in {DeploymentEdition.CLOUD, DeploymentEdition.ENTERPRISE}
|
||||
or dify_config.KNOWLEDGE_FS_COMMUNITY_DEV_ENABLED
|
||||
)
|
||||
)
|
||||
system_features.knowledge_fs_enabled = knowledge_fs_enabled
|
||||
system_features.knowledge_fs_upload_enabled = bool(
|
||||
knowledge_fs_enabled
|
||||
and dify_config.KNOWLEDGE_FS_BASE_URL
|
||||
and dify_config.KNOWLEDGE_FS_CAPABILITY_V2_ENABLED
|
||||
and dify_config.KNOWLEDGE_FS_CAPABILITY_V2_SIGNING_KID
|
||||
and dify_config.KNOWLEDGE_FS_CAPABILITY_V2_PRIVATE_KEY_PEM
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _fulfill_params_from_enterprise(cls, features: feature_entities.SystemFeatureModel) -> None:
|
||||
enterprise_info = EnterpriseService.get_info()
|
||||
|
||||
if "SSOEnforcedForSignin" in enterprise_info:
|
||||
features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"]
|
||||
|
||||
features.sso_enforced_for_signin_protocol = cls._resolve_sso_protocol(
|
||||
enterprise_info.get("SSOEnforcedForSigninProtocol"),
|
||||
field_name="SSOEnforcedForSigninProtocol",
|
||||
)
|
||||
|
||||
if "EnableEmailCodeLogin" in enterprise_info:
|
||||
features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"]
|
||||
|
||||
if "EnableEmailPasswordLogin" in enterprise_info:
|
||||
features.enable_email_password_login = enterprise_info["EnableEmailPasswordLogin"]
|
||||
|
||||
if "IsAllowRegister" in enterprise_info:
|
||||
features.is_allow_register = enterprise_info["IsAllowRegister"]
|
||||
|
||||
if "EnableAppDeploy" in enterprise_info:
|
||||
features.enable_app_deploy = enterprise_info["EnableAppDeploy"]
|
||||
|
||||
if "Branding" in enterprise_info:
|
||||
features.branding.application_title = enterprise_info["Branding"].get("applicationTitle", "")
|
||||
features.branding.login_page_logo = enterprise_info["Branding"].get("loginPageLogo", "")
|
||||
features.branding.workspace_logo = enterprise_info["Branding"].get("workspaceLogo", "")
|
||||
features.branding.favicon = enterprise_info["Branding"].get("favicon", "")
|
||||
|
||||
if "WebAppAuth" in enterprise_info:
|
||||
features.webapp_auth.allow_sso = enterprise_info["WebAppAuth"].get("allowSso", False)
|
||||
features.webapp_auth.allow_email_code_login = enterprise_info["WebAppAuth"].get(
|
||||
"allowEmailCodeLogin", False
|
||||
)
|
||||
features.webapp_auth.allow_email_password_login = enterprise_info["WebAppAuth"].get(
|
||||
"allowEmailPasswordLogin", False
|
||||
)
|
||||
features.webapp_auth.sso_config.protocol = cls._resolve_sso_protocol(
|
||||
enterprise_info.get("SSOEnforcedForWebProtocol"),
|
||||
field_name="SSOEnforcedForWebProtocol",
|
||||
)
|
||||
|
||||
# The unauthenticated endpoint exposes status only. Full license detail is
|
||||
# served by the authenticated license endpoint.
|
||||
license_status = cls._resolve_license_status(enterprise_info)
|
||||
if license_status != feature_entities.LicenseStatus.NONE:
|
||||
features.license = feature_entities.LicenseStatusModel(
|
||||
status=license_status,
|
||||
)
|
||||
|
||||
features.plugin_installation_permission = cls._resolve_plugin_installation_permission(enterprise_info)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_license_status(enterprise_info: Mapping[str, object]) -> feature_entities.LicenseStatus:
|
||||
license_info = enterprise_info.get("License")
|
||||
if not license_info:
|
||||
return feature_entities.LicenseStatus.NONE
|
||||
if not isinstance(license_info, Mapping):
|
||||
return feature_entities.LicenseStatus.INACTIVE
|
||||
|
||||
status = license_info.get("status", feature_entities.LicenseStatus.INACTIVE)
|
||||
if isinstance(status, feature_entities.LicenseStatus):
|
||||
return status
|
||||
if isinstance(status, str):
|
||||
return feature_entities.LicenseStatus(status)
|
||||
return feature_entities.LicenseStatus.INACTIVE
|
||||
|
||||
@classmethod
|
||||
def _build_license(cls, enterprise_info: dict) -> feature_entities.LicenseModel:
|
||||
license_model = feature_entities.LicenseModel()
|
||||
if license_info := enterprise_info.get("License"):
|
||||
license_model.status = feature_entities.LicenseStatus(
|
||||
license_info.get("status", feature_entities.LicenseStatus.INACTIVE)
|
||||
)
|
||||
license_model.expired_at = license_info.get("expiredAt", "")
|
||||
if workspaces_info := license_info.get("workspaces"):
|
||||
license_model.workspaces = feature_entities.LicenseLimitationModel(
|
||||
enabled=workspaces_info.get("enabled", False),
|
||||
limit=workspaces_info.get("limit", 0),
|
||||
size=workspaces_info.get("used", 0),
|
||||
)
|
||||
if seats_info := license_info.get("licensedSeats"):
|
||||
license_model.seats = feature_entities.LicenseLimitationModel(
|
||||
enabled=seats_info.get("enabled", False),
|
||||
limit=seats_info.get("limit", 0),
|
||||
size=seats_info.get("used", 0),
|
||||
)
|
||||
return license_model
|
||||
|
||||
@classmethod
|
||||
def _resolve_plugin_installation_permission(
|
||||
cls, enterprise_info: Mapping[str, object]
|
||||
) -> feature_entities.PluginInstallationPermissionModel:
|
||||
if "PluginInstallationPermission" not in enterprise_info:
|
||||
return feature_entities.PluginInstallationPermissionModel()
|
||||
|
||||
try:
|
||||
permission = _EnterprisePluginInstallationPermission.model_validate(
|
||||
enterprise_info["PluginInstallationPermission"]
|
||||
)
|
||||
except ValidationError as exc:
|
||||
logger.error( # noqa: TRY400
|
||||
"Invalid Enterprise plugin installation permission; denying all plugin installations: %s",
|
||||
exc.errors(include_input=False),
|
||||
)
|
||||
return feature_entities.PluginInstallationPermissionModel(
|
||||
plugin_installation_scope=feature_entities.PluginInstallationScope.NONE,
|
||||
restrict_to_marketplace_only=True,
|
||||
)
|
||||
|
||||
return feature_entities.PluginInstallationPermissionModel(
|
||||
plugin_installation_scope=permission.plugin_installation_scope,
|
||||
restrict_to_marketplace_only=permission.restrict_to_marketplace_only,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_sso_protocol(value: object, *, field_name: str) -> feature_entities.SSOProtocol | None:
|
||||
if value is None or (isinstance(value, str) and not value.strip()):
|
||||
return None
|
||||
|
||||
if not isinstance(value, str):
|
||||
logger.error("Invalid Enterprise SSO protocol for %s; disabling the protocol", field_name)
|
||||
return None
|
||||
|
||||
try:
|
||||
return feature_entities.SSOProtocol(value)
|
||||
except ValueError:
|
||||
logger.error("Invalid Enterprise SSO protocol for %s; disabling the protocol", field_name) # noqa: TRY400
|
||||
return None
|
||||
49
api/services/web_passport_gateways.py
Normal file
49
api/services/web_passport_gateways.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""Outer gateways used by the web passport application service."""
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import Any
|
||||
|
||||
from werkzeug.exceptions import Unauthorized
|
||||
|
||||
from libs.passport import PassportService
|
||||
from services.enterprise.enterprise_service import PERMISSION_CHECK_MODES, WebAppAccessMode, WebAppSettings
|
||||
from services.web_passport_service import WebAppAuthType, WebPassportUnauthorizedError
|
||||
|
||||
|
||||
class DeploymentWebPassportAuthGateway:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
webapp_auth_enabled: bool,
|
||||
get_app_access_mode: Callable[[str], WebAppSettings],
|
||||
) -> None:
|
||||
self._webapp_auth_enabled = webapp_auth_enabled
|
||||
self._get_app_access_mode = get_app_access_mode
|
||||
|
||||
def is_webapp_auth_enabled(self) -> bool:
|
||||
return self._webapp_auth_enabled
|
||||
|
||||
def get_app_auth_type(self, app_id: str) -> WebAppAuthType:
|
||||
access_mode = self._get_app_access_mode(app_id).access_mode
|
||||
if access_mode == WebAppAccessMode.PUBLIC:
|
||||
return WebAppAuthType.PUBLIC
|
||||
if access_mode in PERMISSION_CHECK_MODES:
|
||||
return WebAppAuthType.INTERNAL
|
||||
if access_mode == WebAppAccessMode.SSO_VERIFIED:
|
||||
return WebAppAuthType.EXTERNAL
|
||||
raise ValueError(f"Unsupported web app access mode: {access_mode}")
|
||||
|
||||
|
||||
class PassportTokenGateway:
|
||||
def __init__(self, *, passport: PassportService) -> None:
|
||||
self._passport = passport
|
||||
|
||||
def verify(self, token: str) -> Mapping[str, Any]:
|
||||
try:
|
||||
return self._passport.verify(token)
|
||||
except Unauthorized as exc:
|
||||
description = exc.description or "Invalid token."
|
||||
raise WebPassportUnauthorizedError(description) from exc
|
||||
|
||||
def issue(self, payload: Mapping[str, Any]) -> str:
|
||||
return self._passport.issue(dict(payload))
|
||||
185
api/services/web_passport_service.py
Normal file
185
api/services/web_passport_service.py
Normal file
@ -0,0 +1,185 @@
|
||||
"""Application service for issuing passports used by deployed web applications."""
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
from enum import StrEnum
|
||||
from typing import Any, Protocol
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from services.entities.passport_entities import (
|
||||
EndUserRecord,
|
||||
WebAppLoginClaims,
|
||||
WebAppRecord,
|
||||
WebPassportEndUserResolution,
|
||||
WebPassportRequest,
|
||||
WebPassportResult,
|
||||
)
|
||||
|
||||
|
||||
class WebAppAuthType(StrEnum):
|
||||
PUBLIC = "public"
|
||||
INTERNAL = "internal"
|
||||
EXTERNAL = "external"
|
||||
|
||||
|
||||
class WebPassportNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class WebPassportUnauthorizedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class WebPassportAuthenticationRequiredError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class WebPassportRepository(Protocol):
|
||||
def get_active_web_app(self, app_code: str) -> WebAppRecord | None: ...
|
||||
|
||||
def is_web_app_active(self, app: WebAppRecord) -> bool: ...
|
||||
|
||||
def resolve_standard_end_user(self, app: WebAppRecord, session_id: str | None) -> WebPassportEndUserResolution: ...
|
||||
|
||||
def resolve_authenticated_end_user(
|
||||
self,
|
||||
app: WebAppRecord,
|
||||
*,
|
||||
end_user_id: str | None,
|
||||
session_id: str | None,
|
||||
) -> WebPassportEndUserResolution: ...
|
||||
|
||||
|
||||
class WebPassportAuthGateway(Protocol):
|
||||
def is_webapp_auth_enabled(self) -> bool: ...
|
||||
|
||||
def get_app_auth_type(self, app_id: str) -> WebAppAuthType: ...
|
||||
|
||||
|
||||
class WebPassportTokenGateway(Protocol):
|
||||
def verify(self, token: str) -> Mapping[str, Any]: ...
|
||||
|
||||
def issue(self, payload: Mapping[str, Any]) -> str: ...
|
||||
|
||||
|
||||
class WebPassportService:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
passports: WebPassportRepository,
|
||||
auth: WebPassportAuthGateway,
|
||||
tokens: WebPassportTokenGateway,
|
||||
now: Callable[[], datetime],
|
||||
access_token_expire_minutes: int,
|
||||
) -> None:
|
||||
self._passports = passports
|
||||
self._auth = auth
|
||||
self._tokens = tokens
|
||||
self._now = now
|
||||
self._access_token_expire_minutes = access_token_expire_minutes
|
||||
|
||||
def issue(self, request: WebPassportRequest) -> WebPassportResult:
|
||||
app = self._passports.get_active_web_app(request.app_code)
|
||||
if app is None:
|
||||
raise WebPassportNotFoundError()
|
||||
|
||||
login_claims: WebAppLoginClaims | None = None
|
||||
if self._auth.is_webapp_auth_enabled():
|
||||
login_claims = self._decode_login_token(request.access_token)
|
||||
auth_type = self._auth.get_app_auth_type(app.app_id)
|
||||
if auth_type != WebAppAuthType.PUBLIC:
|
||||
if login_claims is None:
|
||||
raise WebPassportAuthenticationRequiredError("Web app authentication required.")
|
||||
self._require_active_web_app(app)
|
||||
return self._exchange_enterprise_token(app, login_claims, auth_type)
|
||||
|
||||
end_user = self._resolve_standard_user(app, request.user_session_id)
|
||||
token = self._tokens.issue(
|
||||
{
|
||||
"iss": app.app_id,
|
||||
"sub": "Web API Passport",
|
||||
"app_id": app.app_id,
|
||||
"app_code": app.app_code,
|
||||
"end_user_id": end_user.id,
|
||||
}
|
||||
)
|
||||
return WebPassportResult(access_token=token)
|
||||
|
||||
def _decode_login_token(self, token: str | None) -> WebAppLoginClaims | None:
|
||||
if not token:
|
||||
return None
|
||||
|
||||
decoded = self._tokens.verify(token)
|
||||
try:
|
||||
claims = WebAppLoginClaims.model_validate(decoded)
|
||||
except ValidationError as exc:
|
||||
raise WebPassportUnauthorizedError("Invalid web app login token.") from exc
|
||||
|
||||
if claims.token_source != "webapp_login_token":
|
||||
raise WebPassportUnauthorizedError("Invalid token source. Expected 'webapp_login_token'.")
|
||||
return claims
|
||||
|
||||
def _resolve_standard_user(self, app: WebAppRecord, session_id: str | None) -> EndUserRecord:
|
||||
resolution = self._passports.resolve_standard_end_user(app, session_id)
|
||||
self._require_active_resolution(resolution)
|
||||
if resolution.end_user is None:
|
||||
raise WebPassportNotFoundError()
|
||||
return resolution.end_user
|
||||
|
||||
def _exchange_enterprise_token(
|
||||
self,
|
||||
app: WebAppRecord,
|
||||
claims: WebAppLoginClaims,
|
||||
auth_type: WebAppAuthType,
|
||||
) -> WebPassportResult:
|
||||
user_auth_type = claims.auth_type
|
||||
if not user_auth_type:
|
||||
raise WebPassportUnauthorizedError("Missing auth_type in the token.")
|
||||
|
||||
if auth_type == WebAppAuthType.EXTERNAL and user_auth_type != WebAppAuthType.EXTERNAL:
|
||||
raise WebPassportAuthenticationRequiredError("Please login as external user.")
|
||||
if auth_type == WebAppAuthType.INTERNAL and user_auth_type != WebAppAuthType.INTERNAL:
|
||||
raise WebPassportAuthenticationRequiredError("Please login as internal user.")
|
||||
|
||||
resolution = self._passports.resolve_authenticated_end_user(
|
||||
app,
|
||||
end_user_id=claims.end_user_id,
|
||||
session_id=claims.session_id,
|
||||
)
|
||||
self._require_active_resolution(resolution)
|
||||
if resolution.end_user is None:
|
||||
if not claims.session_id:
|
||||
raise WebPassportNotFoundError("Missing session_id for existing web user.")
|
||||
raise WebPassportNotFoundError()
|
||||
end_user = resolution.end_user
|
||||
|
||||
now = self._now()
|
||||
expires_at = int((now + timedelta(minutes=self._access_token_expire_minutes)).timestamp())
|
||||
if claims.exp:
|
||||
expires_at = int(claims.exp)
|
||||
|
||||
token = self._tokens.issue(
|
||||
{
|
||||
"iss": app.site_id,
|
||||
"sub": "Web API Passport",
|
||||
"app_id": app.app_id,
|
||||
"app_code": app.app_code,
|
||||
"user_id": claims.user_id,
|
||||
"end_user_id": end_user.id,
|
||||
"auth_type": user_auth_type,
|
||||
"granted_at": int(now.timestamp()),
|
||||
"token_source": "webapp",
|
||||
"exp": expires_at,
|
||||
}
|
||||
)
|
||||
return WebPassportResult(access_token=token)
|
||||
|
||||
def _require_active_web_app(self, app: WebAppRecord) -> None:
|
||||
if not self._passports.is_web_app_active(app):
|
||||
raise WebPassportNotFoundError()
|
||||
|
||||
@staticmethod
|
||||
def _require_active_resolution(resolution: WebPassportEndUserResolution) -> None:
|
||||
if not resolution.app_active:
|
||||
raise WebPassportNotFoundError()
|
||||
@ -1,4 +1,3 @@
|
||||
import enum
|
||||
import secrets
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
@ -16,19 +15,11 @@ from models.enums import EndUserType
|
||||
from models.model import App, EndUser, Site
|
||||
from services.account_service import AccountService
|
||||
from services.app_service import AppService
|
||||
from services.enterprise.enterprise_service import PERMISSION_CHECK_MODES, EnterpriseService, WebAppAccessMode
|
||||
from services.enterprise.enterprise_service import PERMISSION_CHECK_MODES, EnterpriseService
|
||||
from services.errors.account import AccountLoginError, AccountNotFoundError, AccountPasswordError
|
||||
from tasks.mail_email_code_login import send_email_code_login_mail_task
|
||||
|
||||
|
||||
class WebAppAuthType(enum.StrEnum):
|
||||
"""Enum for web app authentication types."""
|
||||
|
||||
PUBLIC = "public"
|
||||
INTERNAL = "internal"
|
||||
EXTERNAL = "external"
|
||||
|
||||
|
||||
class WebAppAuthService:
|
||||
"""Service for web app authentication."""
|
||||
|
||||
@ -156,28 +147,3 @@ class WebAppAuthService:
|
||||
if webapp_settings and webapp_settings.access_mode in PERMISSION_CHECK_MODES:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_app_auth_type(
|
||||
cls, app_code: str | None = None, access_mode: str | None = None, *, session: Session
|
||||
) -> WebAppAuthType:
|
||||
"""
|
||||
Get the authentication type for the app based on its access mode.
|
||||
"""
|
||||
if not app_code and not access_mode:
|
||||
raise ValueError("Either app_code or access_mode must be provided.")
|
||||
|
||||
if access_mode:
|
||||
if access_mode == WebAppAccessMode.PUBLIC:
|
||||
return WebAppAuthType.PUBLIC
|
||||
elif access_mode in PERMISSION_CHECK_MODES:
|
||||
return WebAppAuthType.INTERNAL
|
||||
elif access_mode == WebAppAccessMode.SSO_VERIFIED:
|
||||
return WebAppAuthType.EXTERNAL
|
||||
|
||||
if app_code:
|
||||
app_id = AppService.get_app_id_by_code(app_code, session=session)
|
||||
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=app_id)
|
||||
return cls.get_app_auth_type(access_mode=webapp_settings.access_mode, session=session)
|
||||
|
||||
raise ValueError("Could not determine app authentication type.")
|
||||
|
||||
@ -94,6 +94,7 @@ from services.errors.app import (
|
||||
WorkflowHashNotEqualError,
|
||||
WorkflowNotFoundError,
|
||||
)
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
from tasks.new_agent_beta_task import register_new_agent_beta_workflow_publish_after_commit
|
||||
|
||||
|
||||
@ -700,9 +701,7 @@ class WorkflowService:
|
||||
)
|
||||
|
||||
# Validate credentials before publishing, for credential policy check
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
if FeatureService.is_plugin_manager_enabled():
|
||||
if SystemFeatureService.is_plugin_manager_enabled():
|
||||
self._validate_workflow_credentials(draft_workflow, session=session)
|
||||
|
||||
# validate graph structure
|
||||
|
||||
@ -13,6 +13,7 @@ from configs import dify_config
|
||||
from configs.feature import TemplateMode
|
||||
from extensions.ext_mail import mail
|
||||
from libs.email_i18n import get_email_i18n_service
|
||||
from services.entities.mail_entities import InnerMailMessage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -29,7 +30,7 @@ class SandboxedEnvironment(ImmutableSandboxedEnvironment):
|
||||
return super().call(context, obj, *args, **kwargs)
|
||||
|
||||
|
||||
def _render_template_with_strategy(body: str, substitutions: Mapping[str, str]) -> str:
|
||||
def _render_template_with_strategy(body: str, substitutions: Mapping[str, Any]) -> str:
|
||||
mode = dify_config.MAIL_TEMPLATING_MODE
|
||||
timeout = dify_config.MAIL_TEMPLATING_TIMEOUT
|
||||
if mode == TemplateMode.UNSAFE:
|
||||
@ -43,7 +44,7 @@ def _render_template_with_strategy(body: str, substitutions: Mapping[str, str])
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_inner_email_task(to: list[str], subject: str, body: str, substitutions: Mapping[str, str]):
|
||||
def send_inner_email_task(to: list[str], subject: str, body: str, substitutions: Mapping[str, Any]):
|
||||
if not mail.is_inited():
|
||||
return
|
||||
|
||||
@ -60,3 +61,12 @@ def send_inner_email_task(to: list[str], subject: str, body: str, substitutions:
|
||||
logger.info(click.style(f"Send enterprise mail to {to} succeeded: latency: {end_at - start_at}", fg="green"))
|
||||
except Exception:
|
||||
logger.exception("Send enterprise mail to %s failed", to)
|
||||
|
||||
|
||||
def enqueue_inner_mail(message: InnerMailMessage) -> None:
|
||||
send_inner_email_task.delay(
|
||||
to=list(message.recipients),
|
||||
subject=message.subject,
|
||||
body=message.body,
|
||||
substitutions=message.substitutions or {},
|
||||
)
|
||||
|
||||
@ -21,11 +21,11 @@ from tests.test_containers_integration_tests.helpers import generate_valid_passw
|
||||
@pytest.fixture
|
||||
def setup_dependencies() -> Iterator[MagicMock]:
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as feature_service,
|
||||
patch("services.account_service.BillingService") as billing_service,
|
||||
patch("services.account_service.CommunityTelemetryService.report_install") as report_install,
|
||||
):
|
||||
feature_service.get_system_features.return_value.is_allow_register = True
|
||||
feature_service.is_registration_allowed.return_value = True
|
||||
feature_service.get_license.return_value.seats.is_available.return_value = True
|
||||
feature_service.get_license.return_value.workspaces.is_available.return_value = True
|
||||
feature_service.is_workspace_creation_allowed.return_value = True
|
||||
|
||||
@ -38,8 +38,8 @@ def make_account(db_session_with_containers: Session) -> Callable[..., Account]:
|
||||
|
||||
def _make(*, with_owner_tenant: bool = True) -> Account:
|
||||
fake = Faker()
|
||||
with patch("services.account_service.FeatureService") as mock_feature_service:
|
||||
mock_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
with patch("services.account_service.SystemFeatureService") as mock_feature_service:
|
||||
mock_feature_service.is_registration_allowed.return_value = True
|
||||
account = AccountService.create_account(
|
||||
email=fake.email(),
|
||||
name=fake.name(),
|
||||
@ -60,7 +60,7 @@ def add_tenant_for_account(
|
||||
account: Account, *, session: Session, role: str = "normal", name: str = "Second WS"
|
||||
) -> Tenant:
|
||||
"""Create an additional tenant and join ``account`` to it (real service calls)."""
|
||||
with patch("services.account_service.FeatureService") as mock_feature_service:
|
||||
with patch("services.account_service.SystemFeatureService") as mock_feature_service:
|
||||
mock_feature_service.is_workspace_creation_allowed.return_value = True
|
||||
tenant = TenantService.create_tenant(name=name, session=session)
|
||||
TenantService.create_tenant_member(tenant, account, session, role=role)
|
||||
|
||||
@ -51,7 +51,7 @@ def external_deps() -> Generator[dict[str, object], None, None]:
|
||||
patch("services.app_dsl_service.DependenciesAnalysisService") as mock_dependencies_service,
|
||||
patch("services.app_dsl_service.app_was_created") as mock_app_was_created,
|
||||
patch("services.app_service.ModelManager.for_tenant") as mock_model_manager,
|
||||
patch("services.app_service.FeatureService") as mock_feature_service,
|
||||
patch("services.app_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise_service,
|
||||
):
|
||||
mock_workflow_service.return_value.get_draft_workflow.return_value = None
|
||||
@ -65,7 +65,7 @@ def external_deps() -> Generator[dict[str, object], None, None]:
|
||||
mock_model_instance.get_default_model_instance.return_value = None
|
||||
mock_model_instance.get_default_provider_model_name.return_value = ("openai", "gpt-3.5-turbo")
|
||||
|
||||
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
|
||||
mock_feature_service.is_webapp_auth_enabled.return_value = False
|
||||
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
|
||||
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
|
||||
|
||||
@ -74,8 +74,8 @@ def external_deps() -> Generator[dict[str, object], None, None]:
|
||||
|
||||
def _app_and_account(db_session: Session, *, mode: str = "chat") -> tuple[App, Account]:
|
||||
fake = Faker()
|
||||
with patch("services.account_service.FeatureService") as mock_account_feature_service:
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
with patch("services.account_service.SystemFeatureService") as mock_account_feature_service:
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
account = AccountService.create_account(
|
||||
email=fake.email(),
|
||||
name=fake.name(),
|
||||
|
||||
@ -231,7 +231,7 @@ class TestDecodeJwtToken:
|
||||
@patch("controllers.web.wraps._validate_webapp_token")
|
||||
@patch("controllers.web.wraps.EnterpriseService.WebAppAuth.get_app_access_mode_by_id")
|
||||
@patch("controllers.web.wraps.AppService.get_app_id_by_code")
|
||||
@patch("controllers.web.wraps.FeatureService.get_system_features")
|
||||
@patch("controllers.web.wraps.SystemFeatureService.is_webapp_auth_enabled")
|
||||
@patch("controllers.web.wraps.PassportService")
|
||||
@patch("controllers.web.wraps.extract_webapp_passport")
|
||||
def test_happy_path(
|
||||
@ -254,7 +254,7 @@ class TestDecodeJwtToken:
|
||||
"app_id": app_model.id,
|
||||
"end_user_id": end_user.id,
|
||||
}
|
||||
mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False))
|
||||
mock_features.return_value = False
|
||||
|
||||
with app.test_request_context("/", headers={"X-App-Code": site.code}):
|
||||
result_app, result_user = decode_jwt_token()
|
||||
@ -262,17 +262,17 @@ class TestDecodeJwtToken:
|
||||
assert result_app.id == app_model.id
|
||||
assert result_user.id == end_user.id
|
||||
|
||||
@patch("controllers.web.wraps.FeatureService.get_system_features")
|
||||
@patch("controllers.web.wraps.SystemFeatureService.is_webapp_auth_enabled")
|
||||
@patch("controllers.web.wraps.extract_webapp_passport")
|
||||
def test_missing_token_raises_unauthorized(self, mock_extract: MagicMock, mock_features: MagicMock, app) -> None:
|
||||
mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False))
|
||||
mock_features.return_value = False
|
||||
mock_extract.return_value = None
|
||||
|
||||
with app.test_request_context("/", headers={"X-App-Code": "code1"}):
|
||||
with pytest.raises(Unauthorized):
|
||||
decode_jwt_token()
|
||||
|
||||
@patch("controllers.web.wraps.FeatureService.get_system_features")
|
||||
@patch("controllers.web.wraps.SystemFeatureService.is_webapp_auth_enabled")
|
||||
@patch("controllers.web.wraps.PassportService")
|
||||
@patch("controllers.web.wraps.extract_webapp_passport")
|
||||
def test_missing_app_raises_not_found(
|
||||
@ -289,13 +289,13 @@ class TestDecodeJwtToken:
|
||||
"app_id": non_existent_id,
|
||||
"end_user_id": str(uuid4()),
|
||||
}
|
||||
mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False))
|
||||
mock_features.return_value = False
|
||||
|
||||
with app.test_request_context("/", headers={"X-App-Code": "code1"}):
|
||||
with pytest.raises(NotFound):
|
||||
decode_jwt_token()
|
||||
|
||||
@patch("controllers.web.wraps.FeatureService.get_system_features")
|
||||
@patch("controllers.web.wraps.SystemFeatureService.is_webapp_auth_enabled")
|
||||
@patch("controllers.web.wraps.PassportService")
|
||||
@patch("controllers.web.wraps.extract_webapp_passport")
|
||||
def test_disabled_site_raises_bad_request(
|
||||
@ -314,13 +314,13 @@ class TestDecodeJwtToken:
|
||||
"app_id": app_model.id,
|
||||
"end_user_id": end_user.id,
|
||||
}
|
||||
mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False))
|
||||
mock_features.return_value = False
|
||||
|
||||
with app.test_request_context("/", headers={"X-App-Code": site.code}):
|
||||
with pytest.raises(BadRequest, match="Site is disabled"):
|
||||
decode_jwt_token()
|
||||
|
||||
@patch("controllers.web.wraps.FeatureService.get_system_features")
|
||||
@patch("controllers.web.wraps.SystemFeatureService.is_webapp_auth_enabled")
|
||||
@patch("controllers.web.wraps.PassportService")
|
||||
@patch("controllers.web.wraps.extract_webapp_passport")
|
||||
def test_missing_end_user_raises_not_found(
|
||||
@ -340,13 +340,13 @@ class TestDecodeJwtToken:
|
||||
"app_id": app_model.id,
|
||||
"end_user_id": non_existent_eu,
|
||||
}
|
||||
mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False))
|
||||
mock_features.return_value = False
|
||||
|
||||
with app.test_request_context("/", headers={"X-App-Code": site.code}):
|
||||
with pytest.raises(NotFound):
|
||||
decode_jwt_token()
|
||||
|
||||
@patch("controllers.web.wraps.FeatureService.get_system_features")
|
||||
@patch("controllers.web.wraps.SystemFeatureService.is_webapp_auth_enabled")
|
||||
@patch("controllers.web.wraps.PassportService")
|
||||
@patch("controllers.web.wraps.extract_webapp_passport")
|
||||
def test_user_id_mismatch_raises_unauthorized(
|
||||
@ -365,7 +365,7 @@ class TestDecodeJwtToken:
|
||||
"app_id": app_model.id,
|
||||
"end_user_id": end_user.id,
|
||||
}
|
||||
mock_features.return_value = SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False))
|
||||
mock_features.return_value = False
|
||||
|
||||
with app.test_request_context("/", headers={"X-App-Code": site.code}):
|
||||
with pytest.raises(Unauthorized, match="expired"):
|
||||
|
||||
@ -626,10 +626,10 @@ class TestKnowledgeRetrievalIntegration:
|
||||
@pytest.fixture
|
||||
def mock_external_service_dependencies():
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
yield {
|
||||
"account_feature_service": mock_account_feature_service,
|
||||
|
||||
@ -49,7 +49,7 @@ class TestDatasetDocumentProperties:
|
||||
db_session_with_containers.add(doc)
|
||||
db_session_with_containers.flush()
|
||||
|
||||
assert dataset.total_documents == 3
|
||||
assert dataset.get_total_documents(session=db_session_with_containers) == 3
|
||||
|
||||
def test_dataset_available_documents_count(self, db_session_with_containers: Session) -> None:
|
||||
"""Test dataset can count available documents."""
|
||||
@ -104,7 +104,7 @@ class TestDatasetDocumentProperties:
|
||||
db_session_with_containers.add_all([doc_available, doc_pending, doc_disabled])
|
||||
db_session_with_containers.flush()
|
||||
|
||||
assert dataset.total_available_documents == 1
|
||||
assert dataset.get_total_available_documents(session=db_session_with_containers) == 1
|
||||
|
||||
def test_dataset_word_count_aggregation(self, db_session_with_containers: Session) -> None:
|
||||
"""Test dataset can aggregate word count from documents."""
|
||||
@ -426,7 +426,7 @@ class TestDocumentSegmentNavigationProperties:
|
||||
db_session_with_containers.flush()
|
||||
|
||||
# Act
|
||||
prev_seg = segment.previous_segment
|
||||
prev_seg = segment.previous_segment(session=db_session_with_containers)
|
||||
|
||||
# Assert
|
||||
assert prev_seg is not None
|
||||
@ -483,7 +483,7 @@ class TestDocumentSegmentNavigationProperties:
|
||||
db_session_with_containers.flush()
|
||||
|
||||
# Act
|
||||
next_seg = segment.next_segment
|
||||
next_seg = segment.next_segment(session=db_session_with_containers)
|
||||
|
||||
# Assert
|
||||
assert next_seg is not None
|
||||
|
||||
@ -30,12 +30,12 @@ class TestAccountService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as mock_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.account_service.BillingService") as mock_billing_service,
|
||||
patch("services.account_service.PassportService") as mock_passport_service,
|
||||
):
|
||||
# Setup default mock returns
|
||||
mock_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_feature_service.is_registration_allowed.return_value = True
|
||||
mock_feature_service.is_workspace_creation_allowed.return_value = True
|
||||
mock_feature_service.get_license.return_value.workspaces.is_available.return_value = True
|
||||
mock_feature_service.get_license.return_value.seats.is_available.return_value = True
|
||||
@ -57,7 +57,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
account = AccountService.create_account(
|
||||
@ -84,7 +84,7 @@ class TestAccountService:
|
||||
email = fake.email()
|
||||
name = fake.name()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
account = AccountService.create_account(
|
||||
@ -108,7 +108,7 @@ class TestAccountService:
|
||||
email = fake.email()
|
||||
name = fake.name()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Test with too short password (assuming minimum length validation)
|
||||
@ -131,7 +131,7 @@ class TestAccountService:
|
||||
email = fake.email()
|
||||
name = fake.name()
|
||||
# Setup mocks to disable registration
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = False
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = False
|
||||
|
||||
with pytest.raises(AccountNotFound): # AccountNotFound exception
|
||||
AccountService.create_account(
|
||||
@ -153,7 +153,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = True
|
||||
dify_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
|
||||
@ -189,7 +189,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account first
|
||||
@ -219,7 +219,7 @@ class TestAccountService:
|
||||
correct_password = generate_valid_password(fake)
|
||||
wrong_password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account first
|
||||
@ -245,7 +245,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
new_password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account without password
|
||||
@ -280,7 +280,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account with pending status
|
||||
@ -309,7 +309,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"feature_service"
|
||||
@ -342,7 +342,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = False
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
@ -366,7 +366,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"feature_service"
|
||||
@ -393,7 +393,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"feature_service"
|
||||
].get_system_features.return_value.license.seats.is_available.return_value = False
|
||||
@ -418,7 +418,7 @@ class TestAccountService:
|
||||
email = fake.email()
|
||||
name = fake.name()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account
|
||||
@ -456,7 +456,7 @@ class TestAccountService:
|
||||
email = fake.email()
|
||||
name = fake.name()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account
|
||||
@ -498,7 +498,7 @@ class TestAccountService:
|
||||
password = generate_valid_password(fake)
|
||||
ip_address = fake.ipv4()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account
|
||||
@ -529,7 +529,7 @@ class TestAccountService:
|
||||
password = generate_valid_password(fake)
|
||||
ip_address = fake.ipv4()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
mock_external_service_dependencies["passport_service"].return_value.issue.return_value = "mock_access_token"
|
||||
|
||||
@ -568,7 +568,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
mock_external_service_dependencies["passport_service"].return_value.issue.return_value = "mock_access_token"
|
||||
|
||||
@ -599,7 +599,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
mock_external_service_dependencies["passport_service"].return_value.issue.return_value = "mock_access_token"
|
||||
|
||||
@ -634,7 +634,7 @@ class TestAccountService:
|
||||
password = generate_valid_password(fake)
|
||||
tenant_name = fake.company()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
mock_external_service_dependencies["passport_service"].return_value.issue.return_value = "new_mock_access_token"
|
||||
|
||||
@ -683,7 +683,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
mock_external_service_dependencies["passport_service"].return_value.issue.return_value = "mock_access_token"
|
||||
|
||||
@ -718,7 +718,7 @@ class TestAccountService:
|
||||
password = generate_valid_password(fake)
|
||||
tenant_name = fake.company()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account
|
||||
@ -759,7 +759,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account
|
||||
@ -788,7 +788,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
mock_external_service_dependencies["passport_service"].return_value.issue.return_value = "mock_jwt_token"
|
||||
|
||||
@ -824,7 +824,7 @@ class TestAccountService:
|
||||
password = generate_valid_password(fake)
|
||||
tenant_name = fake.company()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account
|
||||
@ -859,7 +859,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account
|
||||
@ -900,7 +900,7 @@ class TestAccountService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account
|
||||
@ -946,7 +946,7 @@ class TestTenantService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as mock_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.account_service.BillingService") as mock_billing_service,
|
||||
):
|
||||
# Setup default mock returns
|
||||
@ -2038,12 +2038,12 @@ class TestRegisterService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as mock_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.account_service.BillingService") as mock_billing_service,
|
||||
patch("services.account_service.PassportService") as mock_passport_service,
|
||||
):
|
||||
# Setup default mock returns
|
||||
mock_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_feature_service.is_registration_allowed.return_value = True
|
||||
mock_feature_service.is_workspace_creation_allowed.return_value = True
|
||||
mock_feature_service.get_license.return_value.workspaces.is_available.return_value = True
|
||||
mock_feature_service.get_license.return_value.seats.is_available.return_value = True
|
||||
@ -2066,7 +2066,7 @@ class TestRegisterService:
|
||||
admin_password = generate_valid_password(fake)
|
||||
ip_address = fake.ipv4()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
from models.model import DifySetup
|
||||
@ -2115,7 +2115,7 @@ class TestRegisterService:
|
||||
admin_password = generate_valid_password(fake)
|
||||
ip_address = fake.ipv4()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Mock AccountService.create_account to raise exception
|
||||
@ -2157,7 +2157,7 @@ class TestRegisterService:
|
||||
password = generate_valid_password(fake)
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"feature_service"
|
||||
@ -2199,7 +2199,7 @@ class TestRegisterService:
|
||||
provider = fake.random_element(elements=("google", "github", "microsoft"))
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"feature_service"
|
||||
@ -2246,7 +2246,7 @@ class TestRegisterService:
|
||||
password = generate_valid_password(fake)
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"feature_service"
|
||||
@ -2290,7 +2290,7 @@ class TestRegisterService:
|
||||
password = generate_valid_password(fake)
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = False
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
@ -2327,7 +2327,7 @@ class TestRegisterService:
|
||||
password = generate_valid_password(fake)
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"feature_service"
|
||||
@ -2365,7 +2365,7 @@ class TestRegisterService:
|
||||
password = generate_valid_password(fake)
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Execute registration without workspace creation
|
||||
@ -2404,7 +2404,7 @@ class TestRegisterService:
|
||||
new_member_email = fake.email()
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["feature_service"].is_workspace_creation_allowed.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"feature_service"
|
||||
@ -2476,7 +2476,7 @@ class TestRegisterService:
|
||||
existing_member_password = generate_valid_password(fake)
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant and inviter account
|
||||
@ -2550,7 +2550,7 @@ class TestRegisterService:
|
||||
existing_pending_member_password = generate_valid_password(fake)
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant and inviter account
|
||||
@ -2611,7 +2611,7 @@ class TestRegisterService:
|
||||
new_member_email = fake.email()
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant
|
||||
@ -2644,7 +2644,7 @@ class TestRegisterService:
|
||||
already_in_tenant_password = generate_valid_password(fake)
|
||||
language = fake.random_element(elements=("en-US", "zh-CN"))
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant and inviter account
|
||||
@ -2696,7 +2696,7 @@ class TestRegisterService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant and account
|
||||
@ -2741,7 +2741,7 @@ class TestRegisterService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant and account
|
||||
@ -2789,7 +2789,7 @@ class TestRegisterService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant and account
|
||||
@ -2833,7 +2833,7 @@ class TestRegisterService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant and account
|
||||
@ -2877,7 +2877,7 @@ class TestRegisterService:
|
||||
name = fake.name()
|
||||
password = generate_valid_password(fake)
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant and account
|
||||
@ -2950,7 +2950,7 @@ class TestRegisterService:
|
||||
invalid_tenant_id = fake.uuid4()
|
||||
token = fake.uuid4()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create account
|
||||
@ -3002,7 +3002,7 @@ class TestRegisterService:
|
||||
password = generate_valid_password(fake)
|
||||
token = fake.uuid4()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant and account
|
||||
@ -3054,7 +3054,7 @@ class TestRegisterService:
|
||||
password = generate_valid_password(fake)
|
||||
token = fake.uuid4()
|
||||
# Setup mocks
|
||||
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
|
||||
|
||||
# Create tenant and account
|
||||
|
||||
@ -26,10 +26,10 @@ class TestAgentService:
|
||||
patch("services.agent_service.ToolManager", autospec=True) as mock_tool_manager,
|
||||
patch("services.agent_service.AgentConfigManager", autospec=True) as mock_agent_config_manager,
|
||||
patch("services.agent_service.current_user", create_autospec(Account, instance=True)) as mock_current_user,
|
||||
patch("services.app_service.FeatureService", autospec=True) as mock_feature_service,
|
||||
patch("services.app_service.SystemFeatureService", autospec=True) as mock_feature_service,
|
||||
patch("services.app_service.EnterpriseService", autospec=True) as mock_enterprise_service,
|
||||
patch("services.app_service.ModelManager.for_tenant", autospec=True) as mock_model_manager,
|
||||
patch("services.account_service.FeatureService", autospec=True) as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService", autospec=True) as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns for agent service
|
||||
mock_plugin_agent_client_instance = mock_plugin_agent_client.return_value
|
||||
@ -67,12 +67,12 @@ class TestAgentService:
|
||||
mock_current_user.timezone = "UTC"
|
||||
|
||||
# Setup default mock returns for app service
|
||||
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
|
||||
mock_feature_service.is_webapp_auth_enabled.return_value = False
|
||||
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
|
||||
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
|
||||
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
# Mock ModelManager for model configuration
|
||||
mock_model_instance = mock_model_manager.return_value
|
||||
@ -104,9 +104,7 @@ class TestAgentService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant
|
||||
account = AccountService.create_account(
|
||||
|
||||
@ -21,7 +21,7 @@ class TestAnnotationService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
patch("services.annotation_service.FeatureService") as mock_feature_service,
|
||||
patch("services.annotation_service.add_annotation_to_index_task") as mock_add_task,
|
||||
patch("services.annotation_service.update_annotation_to_index_task") as mock_update_task,
|
||||
@ -70,9 +70,7 @@ class TestAnnotationService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant first
|
||||
from services.account_service import AccountService, TenantService
|
||||
|
||||
@ -17,7 +17,7 @@ class TestAPIBasedExtensionService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
patch("services.api_based_extension_service.APIBasedExtensionRequestor") as mock_requestor,
|
||||
):
|
||||
# Setup default mock returns
|
||||
@ -47,9 +47,7 @@ class TestAPIBasedExtensionService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant
|
||||
account = AccountService.create_account(
|
||||
|
||||
@ -122,7 +122,7 @@ class TestAppDslService:
|
||||
patch("services.app_dsl_service.DependenciesAnalysisService") as mock_dependencies_service,
|
||||
patch("services.app_dsl_service.app_was_created") as mock_app_was_created,
|
||||
patch("services.app_service.ModelManager.for_tenant") as mock_model_manager,
|
||||
patch("services.app_service.FeatureService") as mock_feature_service,
|
||||
patch("services.app_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise_service,
|
||||
):
|
||||
mock_workflow_service.return_value.get_draft_workflow.return_value = None
|
||||
@ -139,7 +139,7 @@ class TestAppDslService:
|
||||
"gpt-3.5-turbo",
|
||||
)
|
||||
|
||||
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
|
||||
mock_feature_service.is_webapp_auth_enabled.return_value = False
|
||||
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
|
||||
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
|
||||
yield {
|
||||
@ -153,8 +153,8 @@ class TestAppDslService:
|
||||
|
||||
def _create_test_app_and_account(self, db_session_with_containers: Session, mock_external_service_dependencies):
|
||||
fake = Faker()
|
||||
with patch("services.account_service.FeatureService") as mock_account_feature_service:
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
with patch("services.account_service.SystemFeatureService") as mock_account_feature_service:
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
account = AccountService.create_account(
|
||||
email=fake.email(),
|
||||
name=fake.name(),
|
||||
|
||||
@ -38,7 +38,7 @@ class TestAppGenerateService:
|
||||
patch(
|
||||
"services.app_generate_service.MessageBasedAppGenerator", autospec=True
|
||||
) as mock_message_based_generator,
|
||||
patch("services.account_service.FeatureService", autospec=True) as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService", autospec=True) as mock_account_feature_service,
|
||||
patch("services.app_generate_service.dify_config") as mock_dify_config,
|
||||
patch("services.quota_service.dify_config") as mock_quota_dify_config,
|
||||
patch("configs.dify_config") as mock_global_dify_config,
|
||||
@ -104,7 +104,7 @@ class TestAppGenerateService:
|
||||
mock_message_based_generator.retrieve_events.return_value = ["workflow_events"]
|
||||
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
# Setup dify_config mock returns
|
||||
mock_dify_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
|
||||
@ -155,9 +155,7 @@ class TestAppGenerateService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant
|
||||
from services.account_service import AccountService, TenantService
|
||||
|
||||
@ -25,18 +25,18 @@ class TestAppService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.app_service.FeatureService") as mock_feature_service,
|
||||
patch("services.app_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise_service,
|
||||
patch("services.app_service.ModelManager.for_tenant") as mock_model_manager,
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns for app service
|
||||
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
|
||||
mock_feature_service.is_webapp_auth_enabled.return_value = False
|
||||
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
|
||||
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
|
||||
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
# Mock ModelManager for model configuration
|
||||
mock_model_instance = mock_model_manager.return_value
|
||||
@ -1252,9 +1252,7 @@ class TestAppService:
|
||||
app_id = app.id
|
||||
|
||||
# Mock webapp auth cleanup
|
||||
mock_external_service_dependencies[
|
||||
"feature_service"
|
||||
].get_system_features.return_value.webapp_auth.enabled = True
|
||||
mock_external_service_dependencies["feature_service"].is_webapp_auth_enabled.return_value = True
|
||||
|
||||
# Mock the async deletion task
|
||||
with patch("services.app_service.remove_app_and_related_data_task") as mock_delete_task:
|
||||
|
||||
@ -14,6 +14,7 @@ from services.entities.feature_entities import (
|
||||
SystemFeatureModel,
|
||||
)
|
||||
from services.feature_service import FeatureService
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
|
||||
|
||||
class TestFeatureService:
|
||||
@ -25,6 +26,7 @@ class TestFeatureService:
|
||||
with (
|
||||
patch("services.feature_service.BillingService") as mock_billing_service,
|
||||
patch("services.feature_service.EnterpriseService") as mock_enterprise_service,
|
||||
patch("services.system_feature_service.EnterpriseService", new=mock_enterprise_service),
|
||||
):
|
||||
# Setup default mock returns for BillingService
|
||||
mock_billing_service.get_info.return_value = {
|
||||
@ -273,7 +275,7 @@ class TestFeatureService:
|
||||
# Arrange: Setup test data with proper config
|
||||
tenant_id = self._create_test_tenant_id()
|
||||
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = True
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = True
|
||||
@ -285,7 +287,7 @@ class TestFeatureService:
|
||||
mock_config.MAIL_TYPE = "smtp"
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -345,7 +347,7 @@ class TestFeatureService:
|
||||
- The response structure adheres to the public schema for unauthenticated clients.
|
||||
"""
|
||||
# Arrange: Setup test data with exact same config as success test
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = True
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = True
|
||||
@ -357,7 +359,7 @@ class TestFeatureService:
|
||||
mock_config.PLUGIN_MAX_PACKAGE_SIZE = 100
|
||||
|
||||
# Act: Execute the public (unauthenticated) system-features call
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
|
||||
# Assert: Basic structure
|
||||
assert result is not None
|
||||
@ -399,11 +401,11 @@ class TestFeatureService:
|
||||
- Detail withheld from the public system-features model is present here.
|
||||
"""
|
||||
# Arrange
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
|
||||
# Act
|
||||
result = FeatureService.get_license()
|
||||
result = SystemFeatureService.get_license()
|
||||
|
||||
# Assert: full license detail is populated
|
||||
assert isinstance(result, LicenseModel)
|
||||
@ -418,10 +420,10 @@ class TestFeatureService:
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""Non-enterprise deployments have no license, so limits are unconstrained."""
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
|
||||
|
||||
result = FeatureService.get_license()
|
||||
result = SystemFeatureService.get_license()
|
||||
|
||||
assert isinstance(result, LicenseModel)
|
||||
assert result.status == LicenseStatus.NONE
|
||||
@ -442,7 +444,7 @@ class TestFeatureService:
|
||||
- Return value correctness and structure
|
||||
"""
|
||||
# Arrange: Setup basic config mock (no enterprise)
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = True
|
||||
@ -456,7 +458,7 @@ class TestFeatureService:
|
||||
mock_config.PLUGIN_MAX_PACKAGE_SIZE = 100
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -640,7 +642,7 @@ class TestFeatureService:
|
||||
- Return value correctness and structure
|
||||
"""
|
||||
# Arrange: Use the Community edition.
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
|
||||
mock_config.MARKETPLACE_ENABLED = True
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -653,7 +655,7 @@ class TestFeatureService:
|
||||
mock_config.PLUGIN_MAX_PACKAGE_SIZE = 50
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -863,7 +865,7 @@ class TestFeatureService:
|
||||
- Return value correctness and structure
|
||||
"""
|
||||
# Arrange: Setup edge case webapp auth mock with proper config
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -879,7 +881,7 @@ class TestFeatureService:
|
||||
}
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -981,7 +983,7 @@ class TestFeatureService:
|
||||
"""
|
||||
|
||||
# Test case 1: Official only scope
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -999,12 +1001,12 @@ class TestFeatureService:
|
||||
}
|
||||
}
|
||||
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
assert result.plugin_installation_permission.plugin_installation_scope == "official_only"
|
||||
assert result.plugin_installation_permission.restrict_to_marketplace_only is True
|
||||
|
||||
# Test case 2: All plugins scope
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -1019,12 +1021,12 @@ class TestFeatureService:
|
||||
"PluginInstallationPermission": {"pluginInstallationScope": "all", "restrictToMarketplaceOnly": False}
|
||||
}
|
||||
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
assert result.plugin_installation_permission.plugin_installation_scope == "all"
|
||||
assert result.plugin_installation_permission.restrict_to_marketplace_only is False
|
||||
|
||||
# Test case 3: Specific partners scope
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -1042,12 +1044,12 @@ class TestFeatureService:
|
||||
}
|
||||
}
|
||||
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
assert result.plugin_installation_permission.plugin_installation_scope == "official_and_specific_partners"
|
||||
assert result.plugin_installation_permission.restrict_to_marketplace_only is False
|
||||
|
||||
# Test case 4: None scope
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -1062,7 +1064,7 @@ class TestFeatureService:
|
||||
"PluginInstallationPermission": {"pluginInstallationScope": "none", "restrictToMarketplaceOnly": True}
|
||||
}
|
||||
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
assert result.plugin_installation_permission.plugin_installation_scope == "none"
|
||||
assert result.plugin_installation_permission.restrict_to_marketplace_only is True
|
||||
|
||||
@ -1120,7 +1122,7 @@ class TestFeatureService:
|
||||
- Return value correctness and structure
|
||||
"""
|
||||
# Arrange: Setup inactive license mock with proper config
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -1140,7 +1142,7 @@ class TestFeatureService:
|
||||
}
|
||||
|
||||
# Act: Execute the authenticated license accessor
|
||||
result = FeatureService.get_license()
|
||||
result = SystemFeatureService.get_license()
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -1169,7 +1171,7 @@ class TestFeatureService:
|
||||
- Return value correctness and structure
|
||||
"""
|
||||
# Arrange: Setup partial enterprise info mock with proper config
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -1187,7 +1189,7 @@ class TestFeatureService:
|
||||
}
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -1297,7 +1299,7 @@ class TestFeatureService:
|
||||
- Return value correctness and structure
|
||||
"""
|
||||
# Arrange: Setup edge case protocols mock with proper config
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -1315,7 +1317,7 @@ class TestFeatureService:
|
||||
}
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -1448,7 +1450,7 @@ class TestFeatureService:
|
||||
- Return value correctness and structure
|
||||
"""
|
||||
# Arrange: Setup expired license mock with proper config
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -1468,7 +1470,7 @@ class TestFeatureService:
|
||||
}
|
||||
|
||||
# Act: Execute the authenticated license accessor
|
||||
result = FeatureService.get_license()
|
||||
result = SystemFeatureService.get_license()
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -1554,7 +1556,7 @@ class TestFeatureService:
|
||||
- Return value correctness and structure
|
||||
"""
|
||||
# Arrange: Setup edge case branding mock with proper config
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -1575,7 +1577,7 @@ class TestFeatureService:
|
||||
}
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
@ -1740,7 +1742,7 @@ class TestFeatureService:
|
||||
- Return value correctness and structure
|
||||
"""
|
||||
# Arrange: Setup lost license mock with proper config
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
with patch("services.system_feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.MARKETPLACE_ENABLED = False
|
||||
mock_config.ENABLE_EMAIL_CODE_LOGIN = False
|
||||
@ -1756,7 +1758,7 @@ class TestFeatureService:
|
||||
}
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = FeatureService.get_system_features()
|
||||
result = SystemFeatureService.get_public_system_features()
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
|
||||
@ -24,7 +24,7 @@ class TestMessageService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
patch("services.message_service.ModelManager.for_tenant") as mock_model_manager,
|
||||
patch("services.message_service.WorkflowService") as mock_workflow_service,
|
||||
patch("services.message_service.AdvancedChatAppConfigManager") as mock_app_config_manager,
|
||||
@ -86,9 +86,7 @@ class TestMessageService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant first
|
||||
from services.account_service import AccountService, TenantService
|
||||
|
||||
@ -20,15 +20,15 @@ class TestOpsService:
|
||||
@pytest.fixture
|
||||
def mock_external_service_dependencies(self):
|
||||
with (
|
||||
patch("services.app_service.FeatureService") as mock_feature_service,
|
||||
patch("services.app_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise_service,
|
||||
patch("services.app_service.ModelManager.for_tenant") as mock_model_manager,
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
):
|
||||
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
|
||||
mock_feature_service.is_webapp_auth_enabled.return_value = False
|
||||
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
|
||||
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
mock_model_instance = mock_model_manager.return_value
|
||||
mock_model_instance.get_default_model_instance.return_value = None
|
||||
mock_model_instance.get_default_provider_model_name.return_value = ("openai", "gpt-3.5-turbo")
|
||||
|
||||
@ -20,12 +20,12 @@ class TestSavedMessageService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
patch("services.app_service.ModelManager.for_tenant") as mock_model_manager,
|
||||
patch("services.saved_message_service.MessageService") as mock_message_service,
|
||||
):
|
||||
# Setup default mock returns
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
# Mock ModelManager for app creation
|
||||
mock_model_instance = mock_model_manager.return_value
|
||||
@ -56,9 +56,7 @@ class TestSavedMessageService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant first
|
||||
from services.account_service import AccountService, TenantService
|
||||
|
||||
@ -27,7 +27,7 @@ class TestTriggerProviderService:
|
||||
patch("services.trigger.trigger_provider_service.TriggerManager") as mock_trigger_manager,
|
||||
patch("services.trigger.trigger_provider_service.redis_client") as mock_redis_client,
|
||||
patch("services.trigger.trigger_provider_service.delete_cache_for_subscription") as mock_delete_cache,
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns
|
||||
mock_provider_controller = MagicMock()
|
||||
@ -42,7 +42,7 @@ class TestTriggerProviderService:
|
||||
mock_redis_client.lock.return_value = mock_lock
|
||||
|
||||
# Setup account feature service mock
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
yield {
|
||||
"trigger_manager": mock_trigger_manager,
|
||||
@ -71,9 +71,7 @@ class TestTriggerProviderService:
|
||||
from services.account_service import AccountService, TenantService
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
mock_external_service_dependencies[
|
||||
"trigger_manager"
|
||||
].get_trigger_provider.return_value = mock_external_service_dependencies["provider_controller"]
|
||||
|
||||
@ -23,18 +23,18 @@ class TestWebConversationService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.app_service.FeatureService") as mock_feature_service,
|
||||
patch("services.app_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise_service,
|
||||
patch("services.app_service.ModelManager.for_tenant") as mock_model_manager,
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns for app service
|
||||
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
|
||||
mock_feature_service.is_webapp_auth_enabled.return_value = False
|
||||
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
|
||||
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
|
||||
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
# Mock ModelManager for model configuration
|
||||
mock_model_instance = mock_model_manager.return_value
|
||||
@ -62,9 +62,7 @@ class TestWebConversationService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant
|
||||
account = AccountService.create_account(
|
||||
|
||||
@ -11,7 +11,7 @@ from libs.password import hash_password
|
||||
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.model import App, Site
|
||||
from services.errors.account import AccountLoginError, AccountNotFoundError, AccountPasswordError
|
||||
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
|
||||
from services.webapp_auth_service import WebAppAuthService
|
||||
from tests.test_containers_integration_tests.helpers import generate_valid_password
|
||||
|
||||
|
||||
@ -825,90 +825,3 @@ class TestWebAppAuthService:
|
||||
WebAppAuthService.is_app_require_permission_check(session=db_session_with_containers)
|
||||
|
||||
assert "Either app_code or app_id must be provided." in str(exc_info.value)
|
||||
|
||||
def test_get_app_auth_type_with_access_mode_public(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test app authentication type for public access mode.
|
||||
|
||||
This test verifies:
|
||||
- Proper authentication type determination for public mode
|
||||
- Correct return value
|
||||
- Mock service integration
|
||||
"""
|
||||
# Arrange: Setup test with public access mode
|
||||
|
||||
# Act: Execute authentication type determination
|
||||
result = WebAppAuthService.get_app_auth_type(access_mode="public", session=db_session_with_containers)
|
||||
|
||||
# Assert: Verify correct result
|
||||
assert result == WebAppAuthType.PUBLIC
|
||||
|
||||
def test_get_app_auth_type_with_access_mode_private(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test app authentication type for private access mode.
|
||||
|
||||
This test verifies:
|
||||
- Proper authentication type determination for private mode
|
||||
- Correct return value
|
||||
- Mock service integration
|
||||
"""
|
||||
# Arrange: Setup test with private access mode
|
||||
|
||||
# Act: Execute authentication type determination
|
||||
result = WebAppAuthService.get_app_auth_type(access_mode="private", session=db_session_with_containers)
|
||||
|
||||
# Assert: Verify correct result
|
||||
assert result == WebAppAuthType.INTERNAL
|
||||
|
||||
def test_get_app_auth_type_with_app_code(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test app authentication type using app code.
|
||||
|
||||
This test verifies:
|
||||
- Proper authentication type determination using app code
|
||||
- Correct return value
|
||||
- Mock service integration
|
||||
"""
|
||||
# Arrange: Setup mock for enterprise service
|
||||
mock_external_service_dependencies["app_service"].get_app_id_by_code.return_value = "mock_app_id"
|
||||
setting = type("MockWebAppAuth", (), {"access_mode": "sso_verified"})()
|
||||
mock_external_service_dependencies[
|
||||
"enterprise_service"
|
||||
].WebAppAuth.get_app_access_mode_by_id.return_value = setting
|
||||
|
||||
# Act: Execute authentication type determination
|
||||
result: WebAppAuthType = WebAppAuthService.get_app_auth_type(
|
||||
app_code="mock_app_code", session=db_session_with_containers
|
||||
)
|
||||
|
||||
# Assert: Verify correct result
|
||||
assert result == WebAppAuthType.EXTERNAL
|
||||
|
||||
# Verify mock service was called correctly
|
||||
mock_external_service_dependencies[
|
||||
"enterprise_service"
|
||||
].WebAppAuth.get_app_access_mode_by_id.assert_called_once_with(app_id="mock_app_id")
|
||||
|
||||
def test_get_app_auth_type_no_parameters(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test app authentication type with no parameters.
|
||||
|
||||
This test verifies:
|
||||
- Proper error handling when no parameters provided
|
||||
- Correct exception type and message
|
||||
"""
|
||||
# Arrange: No parameters provided
|
||||
|
||||
# Act & Assert: Verify proper error handling
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
WebAppAuthService.get_app_auth_type(session=db_session_with_containers)
|
||||
|
||||
assert "Either app_code or access_mode must be provided." in str(exc_info.value)
|
||||
|
||||
@ -8,14 +8,12 @@ from faker import Faker
|
||||
from flask import Flask
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from enums import DeploymentEdition
|
||||
from models.account import Account, Tenant
|
||||
from models.enums import AppTriggerStatus, AppTriggerType
|
||||
from models.model import App
|
||||
from models.trigger import AppTrigger, WorkflowWebhookTrigger
|
||||
from models.workflow import Workflow
|
||||
from services.account_service import AccountService, TenantService
|
||||
from services.entities.feature_entities import SystemFeatureModel
|
||||
from services.trigger.webhook_service import WebhookService
|
||||
from tests.test_containers_integration_tests.helpers import generate_valid_password
|
||||
|
||||
@ -38,16 +36,12 @@ def test_data(
|
||||
"""Persist the webhook graph with account and workspace creation enabled."""
|
||||
|
||||
fake = Faker()
|
||||
system_features = SystemFeatureModel(
|
||||
deployment_edition=DeploymentEdition.COMMUNITY,
|
||||
is_allow_register=True,
|
||||
monkeypatch.setattr(
|
||||
"services.account_service.SystemFeatureService.is_registration_allowed",
|
||||
lambda: True,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"services.account_service.FeatureService.get_system_features",
|
||||
lambda: system_features,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"services.account_service.FeatureService.is_workspace_creation_allowed",
|
||||
"services.account_service.SystemFeatureService.is_workspace_creation_allowed",
|
||||
lambda: True,
|
||||
)
|
||||
account = AccountService.create_account(
|
||||
|
||||
@ -28,18 +28,18 @@ class TestWorkflowAppService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.app_service.FeatureService") as mock_feature_service,
|
||||
patch("services.app_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise_service,
|
||||
patch("services.app_service.ModelManager.for_tenant") as mock_model_manager,
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns for app service
|
||||
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
|
||||
mock_feature_service.is_webapp_auth_enabled.return_value = False
|
||||
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
|
||||
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
|
||||
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
# Mock ModelManager for model configuration
|
||||
mock_model_instance = mock_model_manager.return_value
|
||||
@ -67,9 +67,7 @@ class TestWorkflowAppService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant
|
||||
account = AccountService.create_account(
|
||||
@ -116,9 +114,7 @@ class TestWorkflowAppService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant
|
||||
account = AccountService.create_account(
|
||||
|
||||
@ -25,18 +25,18 @@ class TestWorkflowRunService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.app_service.FeatureService") as mock_feature_service,
|
||||
patch("services.app_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise_service,
|
||||
patch("services.app_service.ModelManager.for_tenant") as mock_model_manager,
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns for app service
|
||||
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
|
||||
mock_feature_service.is_webapp_auth_enabled.return_value = False
|
||||
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
|
||||
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
|
||||
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
# Mock ModelManager for model configuration
|
||||
mock_model_instance = mock_model_manager.return_value
|
||||
@ -64,9 +64,7 @@ class TestWorkflowRunService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant
|
||||
account = AccountService.create_account(
|
||||
|
||||
@ -23,10 +23,10 @@ class TestWorkflowToolManageService:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.app_service.FeatureService") as mock_feature_service,
|
||||
patch("services.app_service.SystemFeatureService") as mock_feature_service,
|
||||
patch("services.app_service.EnterpriseService") as mock_enterprise_service,
|
||||
patch("services.app_service.ModelManager.for_tenant") as mock_model_manager,
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
patch(
|
||||
"services.tools.workflow_tools_manage_service.WorkflowToolProviderController"
|
||||
) as mock_workflow_tool_provider_controller,
|
||||
@ -34,12 +34,12 @@ class TestWorkflowToolManageService:
|
||||
patch("services.tools.workflow_tools_manage_service.ToolTransformService") as mock_tool_transform_service,
|
||||
):
|
||||
# Setup default mock returns for app service
|
||||
mock_feature_service.get_system_features.return_value.webapp_auth.enabled = False
|
||||
mock_feature_service.is_webapp_auth_enabled.return_value = False
|
||||
mock_enterprise_service.WebAppAuth.update_app_access_mode.return_value = None
|
||||
mock_enterprise_service.WebAppAuth.cleanup_webapp.return_value = None
|
||||
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
# Mock ModelManager for model configuration
|
||||
mock_model_instance = mock_model_manager.return_value
|
||||
@ -79,9 +79,7 @@ class TestWorkflowToolManageService:
|
||||
fake = Faker()
|
||||
|
||||
# Setup mocks for account creation
|
||||
mock_external_service_dependencies[
|
||||
"account_feature_service"
|
||||
].get_system_features.return_value.is_allow_register = True
|
||||
mock_external_service_dependencies["account_feature_service"].is_registration_allowed.return_value = True
|
||||
|
||||
# Create account and tenant
|
||||
account = AccountService.create_account(
|
||||
|
||||
@ -37,10 +37,10 @@ class TestCleanNotionDocumentTask:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
yield {
|
||||
"account_feature_service": mock_account_feature_service,
|
||||
|
||||
@ -29,10 +29,10 @@ class TestDealDatasetVectorIndexTask:
|
||||
def mock_external_service_dependencies(self):
|
||||
"""Mock setup for external service dependencies."""
|
||||
with (
|
||||
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||
patch("services.account_service.SystemFeatureService") as mock_account_feature_service,
|
||||
):
|
||||
# Setup default mock returns for account service
|
||||
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||
mock_account_feature_service.is_registration_allowed.return_value = True
|
||||
|
||||
yield {
|
||||
"account_feature_service": mock_account_feature_service,
|
||||
|
||||
@ -41,7 +41,7 @@ from models.trigger import (
|
||||
from models.workflow import Workflow
|
||||
from schedule import workflow_schedule_task
|
||||
from schedule.workflow_schedule_task import poll_workflow_schedules
|
||||
from services import feature_service as feature_service_module
|
||||
from services.system_feature_service import SystemFeatureService
|
||||
from services.trigger import webhook_service
|
||||
from services.trigger.schedule_service import ScheduleService
|
||||
from services.workflow_service import WorkflowService
|
||||
@ -112,7 +112,7 @@ def test_publish_blocks_start_and_trigger_coexistence(
|
||||
workflow_service = WorkflowService()
|
||||
|
||||
monkeypatch.setattr(
|
||||
feature_service_module.FeatureService,
|
||||
SystemFeatureService,
|
||||
"is_plugin_manager_enabled",
|
||||
classmethod(lambda _cls: False),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user