Files
dify/api/extensions/ext_application_services.py
林玮 (Jade Lin) 70a513fc62 fix(api): scope hosted credits by tenant plan (#41131)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-24 06:35:42 +00:00

345 lines
16 KiB
Python

"""Composition root for application services used by transport adapters."""
import json
from dataclasses import dataclass
from typing import cast
import httpx
from flask import Flask, current_app
from pydantic import ValidationError
from sqlalchemy.orm import Session, sessionmaker
from configs import dify_config
from constants.dsl_version import CURRENT_APP_DSL_VERSION
from core.db.session_factory import get_session_maker
from core.schemas.schema_manager import SchemaManager
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 repositories.account_activation_repository import SQLAlchemyAccountActivationRepository
from repositories.account_integration_repository import SQLAlchemyAccountIntegrationRepository
from repositories.account_repository import SQLAlchemyAccountRepository
from repositories.app_definition_query_repository import AppDefinitionQueryRepository
from repositories.data_source_api_key_auth_repository import SQLAlchemyDataSourceApiKeyAuthBindingRepository
from repositories.explore_banner_query_repository import ExploreBannerQueryRepository
from repositories.installation_state_repository import InstallationStateRepository
from repositories.recommended_app_catalog_repository import DatabaseRecommendedAppCatalogRepository
from repositories.tag_repository import TagRepository
from repositories.trial_app_query_repository import TrialAppQueryRepository
from repositories.trial_app_usage_repository import TrialAppUsageRepository
from repositories.webapp_access_query_repository import WebAppAccessQueryRepository
from repositories.workspace_member_query_repository import WorkspaceMemberQueryRepository
from repositories.workspace_query_repository import WorkspaceQueryRepository
from services.account_activation_adapters import (
BillingAccountActivationEligibility,
BillingWorkspaceMembershipCache,
DeploymentWorkspaceInvitePolicy,
RegisterServiceInvitationTokenStore,
)
from services.account_activation_service import AccountActivationService
from services.account_avatar_file_gateway import SQLAlchemyAccountAvatarFileGateway
from services.account_avatar_service import AccountAvatarService
from services.account_billing_adapters import (
BillingAccountDeletionFeedbackGateway,
BillingAccountEducationGateway,
)
from services.account_change_email_adapters import (
BillingAccountEmailPolicyGateway,
CeleryChangeEmailNotificationGateway,
RateLimiterChangeEmailSendLimiter,
RedisChangeEmailSecurityGateway,
SecureChangeEmailCodeGenerator,
TokenManagerChangeEmailTokenGateway,
)
from services.account_change_email_service import AccountChangeEmailService
from services.account_deletion_adapters import (
CeleryAccountDeletionScheduler,
CeleryAccountDeletionVerificationNotifier,
EnterpriseAccountDeletionSyncGateway,
TokenManagerAccountDeletionVerificationGateway,
)
from services.account_deletion_feedback_service import AccountDeletionFeedbackService
from services.account_deletion_service import AccountDeletionService
from services.account_education_service import AccountEducationService
from services.account_initialization_service import AccountInitializationService
from services.account_integration_service import AccountIntegrationService
from services.account_password_hasher import LegacyAccountPasswordHasher
from services.account_password_service import AccountPasswordService
from services.account_profile_service import AccountProfileService
from services.app_definition_query_service import AppDefinitionQueryService
from services.auth.data_source_api_key_auth_gateways import (
ProviderApiKeyAuthCredentialValidator,
TenantApiKeyAuthCredentialEncryptor,
)
from services.auth.data_source_api_key_auth_service import DataSourceApiKeyAuthService
from services.billing_portal_service import BillingPortalService
from services.billing_service import BillingService
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.partner_tenant_binding_service import PartnerTenantBindingService
from services.recommended_app_catalog_gateway import (
BuiltinRecommendedAppCatalogGateway,
RecommendedAppCatalogRouter,
RemoteRecommendedAppCatalogGateway,
)
from services.recommended_app_query_service import RecommendedAppQueryService
from services.schema_definition_service import SchemaDefinitionService
from services.setup_adapters import RedisSetupLock, RegisterServiceAccountProvisioner
from services.setup_service import SetupService
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.webapp_access_query_service import (
WebAppAccessQueryService,
WebAppAccessUnavailableError,
)
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
_EXTENSION_KEY = "application_services"
def _get_enterprise_webapp_access_mode(app_id: str) -> WebAppAccessMode:
try:
settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
except (EnterpriseServiceError, httpx.RequestError, json.JSONDecodeError, UnicodeDecodeError, ValidationError) as e:
raise WebAppAccessUnavailableError from e
try:
return WebAppAccessMode(settings.access_mode)
except ValueError as e:
raise WebAppAccessUnavailableError from e
def _is_user_allowed_to_access_webapp(user_id: str, app_id: str) -> bool:
try:
return EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_id)
except (EnterpriseServiceError, httpx.RequestError, json.JSONDecodeError, UnicodeDecodeError) as e:
raise WebAppAccessUnavailableError from e
@dataclass(frozen=True, slots=True)
class AccountServices:
avatar: AccountAvatarService
change_email: AccountChangeEmailService
deletion: AccountDeletionService
deletion_feedback: AccountDeletionFeedbackService
education: AccountEducationService
initialization: AccountInitializationService
integrations: AccountIntegrationService
password: AccountPasswordService
profile: AccountProfileService
@dataclass(frozen=True, slots=True)
class ApplicationServices:
accounts: AccountServices
account_activation: AccountActivationService
app_definitions: AppDefinitionQueryService
billing_portal: BillingPortalService
data_source_api_key_auth: DataSourceApiKeyAuthService
webapp_access: WebAppAccessQueryService
web_app_runtime: WebAppRuntimeQueryService
explore_banner_queries: ExploreBannerQueryService
schema_definitions: SchemaDefinitionService
setup: SetupService
feature_queries: FeatureQueryService
init_validation: InitValidationService
partner_tenant_bindings: PartnerTenantBindingService
recommended_app_queries: RecommendedAppQueryService
trial_app_usage: TrialAppUsageRecorder
workspace_queries: WorkspaceQueryService
workspace_member_queries: WorkspaceMemberQueryService
tags: TagApplicationService
def build_application_services(
*,
database_client: sessionmaker[Session],
deployment_edition: DeploymentEdition,
initialization_password: str,
redis: RedisClientWrapper,
) -> ApplicationServices:
installation_state = InstallationStateRepository(session_factory=database_client)
data_source_api_key_auth_bindings = SQLAlchemyDataSourceApiKeyAuthBindingRepository(session_factory=database_client)
app_definition_repository = AppDefinitionQueryRepository(session_factory=database_client)
feature_gateway = FeatureServiceGateway()
accounts = SQLAlchemyAccountRepository(session_factory=database_client)
integrations = SQLAlchemyAccountIntegrationRepository(session_factory=database_client)
trial_app_enabled = FeatureService.is_trial_app_enabled()
database_catalog = DatabaseRecommendedAppCatalogRepository(session_factory=database_client, redis=redis)
builtin_catalog = BuiltinRecommendedAppCatalogGateway()
remote_catalog = RemoteRecommendedAppCatalogGateway()
recommended_app_catalog = RecommendedAppCatalogRouter(
remote=remote_catalog,
database=database_catalog,
builtin=builtin_catalog,
)
workspace_query_repository = WorkspaceQueryRepository(session_factory=database_client)
return ApplicationServices(
accounts=AccountServices(
avatar=AccountAvatarService(
files=SQLAlchemyAccountAvatarFileGateway(session_factory=database_client),
),
change_email=AccountChangeEmailService(
accounts=accounts,
tokens=TokenManagerChangeEmailTokenGateway(),
codes=SecureChangeEmailCodeGenerator(),
notifications=CeleryChangeEmailNotificationGateway(),
send_limits=RateLimiterChangeEmailSendLimiter(
rate_limiter=RateLimiter(
prefix="change_email_rate_limit",
max_attempts=1,
time_window=60,
redis_client=redis,
)
),
security=RedisChangeEmailSecurityGateway(
redis=redis,
email_send_ip_limit_per_minute=dify_config.EMAIL_SEND_IP_LIMIT_PER_MINUTE,
verification_failure_limit=5,
verification_lockout_duration=dify_config.CHANGE_EMAIL_LOCKOUT_DURATION,
),
email_policy=BillingAccountEmailPolicyGateway(
billing_enabled=deployment_edition == DeploymentEdition.CLOUD,
),
),
deletion=AccountDeletionService(
accounts=accounts,
memberships=workspace_query_repository,
verification=TokenManagerAccountDeletionVerificationGateway(),
notifications=CeleryAccountDeletionVerificationNotifier(
rate_limiter=RateLimiter(
prefix="email_code_account_deletion_rate_limit",
max_attempts=1,
time_window=60,
redis_client=redis,
)
),
synchronization=EnterpriseAccountDeletionSyncGateway(),
scheduler=CeleryAccountDeletionScheduler(),
),
deletion_feedback=AccountDeletionFeedbackService(
feedback=BillingAccountDeletionFeedbackGateway(),
),
education=AccountEducationService(
accounts=accounts,
education=BillingAccountEducationGateway(),
),
initialization=AccountInitializationService(
accounts=accounts,
invitation_required=deployment_edition == DeploymentEdition.CLOUD,
now=naive_utc_now,
),
integrations=AccountIntegrationService(integrations=integrations),
password=AccountPasswordService(
accounts=accounts,
passwords=LegacyAccountPasswordHasher(),
),
profile=AccountProfileService(accounts=accounts),
),
account_activation=AccountActivationService(
tokens=RegisterServiceInvitationTokenStore(),
accounts=SQLAlchemyAccountActivationRepository(session_factory=database_client),
workspace_policy=DeploymentWorkspaceInvitePolicy(),
eligibility=BillingAccountActivationEligibility(
enabled=deployment_edition == DeploymentEdition.CLOUD,
),
membership_cache=BillingWorkspaceMembershipCache(
enabled=deployment_edition == DeploymentEdition.CLOUD,
),
),
app_definitions=AppDefinitionQueryService(
definitions=app_definition_repository,
builtin_icon_url_prefix=(
dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/"
),
),
billing_portal=BillingPortalService(
accounts=accounts,
get_subscription=BillingService.get_subscription,
get_invoices=BillingService.get_invoices,
),
data_source_api_key_auth=DataSourceApiKeyAuthService(
bindings=data_source_api_key_auth_bindings,
validator=ProviderApiKeyAuthCredentialValidator(),
encryptor=TenantApiKeyAuthCredentialEncryptor(),
),
webapp_access=WebAppAccessQueryService(
access=WebAppAccessQueryRepository(session_factory=database_client),
webapp_auth_enabled=FeatureService.is_webapp_auth_enabled(),
access_mode_for_app=_get_enterprise_webapp_access_mode,
is_user_allowed_for_app=_is_user_allowed_to_access_webapp,
),
web_app_runtime=WebAppRuntimeQueryService(
runtime=app_definition_repository,
file_service=FileService(session_factory=database_client),
workspace_features=feature_gateway.get_workspace_features,
files_url=dify_config.FILES_URL,
),
explore_banner_queries=ExploreBannerQueryService(
banners=ExploreBannerQueryRepository(session_factory=database_client),
enabled=FeatureService.is_explore_banner_enabled(),
),
schema_definitions=SchemaDefinitionService(source_factory=SchemaManager),
setup=SetupService(
state=installation_state,
accounts=RegisterServiceAccountProvisioner(session_factory=database_client),
lock=RedisSetupLock(client=redis),
setup_required=deployment_edition != DeploymentEdition.CLOUD,
),
feature_queries=FeatureQueryService(
features=feature_gateway,
app_dsl_version=CURRENT_APP_DSL_VERSION,
),
init_validation=InitValidationService(
state=installation_state,
validation_required=(deployment_edition != DeploymentEdition.CLOUD and bool(initialization_password)),
expected_password=initialization_password,
),
partner_tenant_bindings=PartnerTenantBindingService(
sync_bindings=BillingService.sync_partner_tenants_bindings,
),
recommended_app_queries=RecommendedAppQueryService(
catalog=recommended_app_catalog,
trial_apps=TrialAppQueryRepository(session_factory=database_client),
trial_enabled=trial_app_enabled,
),
trial_app_usage=TrialAppUsageRepository(session_factory=database_client),
workspace_queries=WorkspaceQueryService(
workspaces=workspace_query_repository,
plans=DeploymentWorkspacePlanGateway(),
),
workspace_member_queries=WorkspaceMemberQueryService(
members=WorkspaceMemberQueryRepository(
session_factory=database_client,
),
roles=DeploymentWorkspaceMemberRoleResolver(),
),
tags=TagApplicationService(
tags=TagRepository(session_factory=database_client),
),
)
def init_app(app: Flask) -> None:
app.extensions[_EXTENSION_KEY] = build_application_services(
database_client=get_session_maker(),
deployment_edition=dify_config.DEPLOYMENT_EDITION,
initialization_password=dify_config.INIT_PASSWORD,
redis=redis_client,
)
def application_services() -> ApplicationServices:
"""Return the application services bound to the current Flask app."""
return cast(ApplicationServices, current_app.extensions[_EXTENSION_KEY])