mirror of
https://github.com/langgenius/dify.git
synced 2026-03-10 09:56:13 +08:00
226 lines
8.3 KiB
Python
226 lines
8.3 KiB
Python
import logging
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
|
|
from configs import dify_config
|
|
from services.enterprise.base import EnterpriseRequest
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_WORKSPACE_JOIN_TIMEOUT_SECONDS = 1.0
|
|
|
|
|
|
class WebAppSettings(BaseModel):
|
|
access_mode: str = Field(
|
|
description="Access mode for the web app. Can be 'public', 'private', 'private_all', 'sso_verified'",
|
|
default="private",
|
|
alias="accessMode",
|
|
)
|
|
|
|
|
|
class WorkspacePermission(BaseModel):
|
|
workspace_id: str = Field(
|
|
description="The ID of the workspace.",
|
|
alias="workspaceId",
|
|
)
|
|
allow_member_invite: bool = Field(
|
|
description="Whether to allow members to invite new members to the workspace.",
|
|
default=False,
|
|
alias="allowMemberInvite",
|
|
)
|
|
allow_owner_transfer: bool = Field(
|
|
description="Whether to allow owners to transfer ownership of the workspace.",
|
|
default=False,
|
|
alias="allowOwnerTransfer",
|
|
)
|
|
|
|
|
|
class DefaultWorkspaceJoinResult(BaseModel):
|
|
"""
|
|
Result of ensuring an account is a member of the enterprise default workspace.
|
|
|
|
- joined=True is idempotent (already a member also returns True)
|
|
- joined=False means enterprise default workspace is not configured or invalid/archived
|
|
"""
|
|
|
|
workspace_id: str = Field(default="", alias="workspaceId")
|
|
joined: bool
|
|
message: str
|
|
|
|
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
|
|
@model_validator(mode="after")
|
|
def _check_workspace_id_when_joined(self) -> "DefaultWorkspaceJoinResult":
|
|
if self.joined and not self.workspace_id:
|
|
raise ValueError("workspace_id must be non-empty when joined is True")
|
|
return self
|
|
|
|
|
|
def try_join_default_workspace(account_id: str) -> None:
|
|
"""
|
|
Enterprise-only side-effect: ensure account is a member of the default workspace.
|
|
|
|
This is a best-effort integration. Failures must not block user registration.
|
|
"""
|
|
|
|
if not dify_config.ENTERPRISE_ENABLED:
|
|
return
|
|
|
|
try:
|
|
result = EnterpriseService.join_default_workspace(account_id=account_id)
|
|
if result.joined:
|
|
logger.info(
|
|
"Joined enterprise default workspace for account %s (workspace_id=%s)",
|
|
account_id,
|
|
result.workspace_id,
|
|
)
|
|
else:
|
|
logger.info(
|
|
"Skipped joining enterprise default workspace for account %s (message=%s)",
|
|
account_id,
|
|
result.message,
|
|
)
|
|
except Exception:
|
|
logger.warning("Failed to join enterprise default workspace for account %s", account_id, exc_info=True)
|
|
|
|
|
|
class EnterpriseService:
|
|
@classmethod
|
|
def get_info(cls):
|
|
return EnterpriseRequest.send_request("GET", "/info")
|
|
|
|
@classmethod
|
|
def get_workspace_info(cls, tenant_id: str):
|
|
return EnterpriseRequest.send_request("GET", f"/workspace/{tenant_id}/info")
|
|
|
|
@classmethod
|
|
def join_default_workspace(cls, *, account_id: str) -> DefaultWorkspaceJoinResult:
|
|
"""
|
|
Call enterprise inner API to add an account to the default workspace.
|
|
|
|
NOTE: EnterpriseRequest.base_url is expected to already include the `/inner/api` prefix,
|
|
so the endpoint here is `/default-workspace/members`.
|
|
"""
|
|
|
|
# Ensure we are sending a UUID-shaped string (enterprise side validates too).
|
|
try:
|
|
uuid.UUID(account_id)
|
|
except ValueError as e:
|
|
raise ValueError(f"account_id must be a valid UUID: {account_id}") from e
|
|
|
|
data = EnterpriseRequest.send_request(
|
|
"POST",
|
|
"/default-workspace/members",
|
|
json={"account_id": account_id},
|
|
timeout=DEFAULT_WORKSPACE_JOIN_TIMEOUT_SECONDS,
|
|
raise_for_status=True,
|
|
)
|
|
if not isinstance(data, dict):
|
|
raise ValueError("Invalid response format from enterprise default workspace API")
|
|
if "joined" not in data or "message" not in data:
|
|
raise ValueError("Invalid response payload from enterprise default workspace API")
|
|
return DefaultWorkspaceJoinResult.model_validate(data)
|
|
|
|
@classmethod
|
|
def get_app_sso_settings_last_update_time(cls) -> datetime:
|
|
data = EnterpriseRequest.send_request("GET", "/sso/app/last-update-time")
|
|
if not data:
|
|
raise ValueError("No data found.")
|
|
try:
|
|
# parse the UTC timestamp from the response
|
|
return datetime.fromisoformat(data)
|
|
except ValueError as e:
|
|
raise ValueError(f"Invalid date format: {data}") from e
|
|
|
|
@classmethod
|
|
def get_workspace_sso_settings_last_update_time(cls) -> datetime:
|
|
data = EnterpriseRequest.send_request("GET", "/sso/workspace/last-update-time")
|
|
if not data:
|
|
raise ValueError("No data found.")
|
|
try:
|
|
# parse the UTC timestamp from the response
|
|
return datetime.fromisoformat(data)
|
|
except ValueError as e:
|
|
raise ValueError(f"Invalid date format: {data}") from e
|
|
|
|
class WorkspacePermissionService:
|
|
@classmethod
|
|
def get_permission(cls, workspace_id: str):
|
|
if not workspace_id:
|
|
raise ValueError("workspace_id must be provided.")
|
|
data = EnterpriseRequest.send_request("GET", f"/workspaces/{workspace_id}/permission")
|
|
if not data or "permission" not in data:
|
|
raise ValueError("No data found.")
|
|
return WorkspacePermission.model_validate(data["permission"])
|
|
|
|
class WebAppAuth:
|
|
@classmethod
|
|
def is_user_allowed_to_access_webapp(cls, user_id: str, app_id: str):
|
|
params = {"userId": user_id, "appId": app_id}
|
|
data = EnterpriseRequest.send_request("GET", "/webapp/permission", params=params)
|
|
|
|
return data.get("result", False)
|
|
|
|
@classmethod
|
|
def batch_is_user_allowed_to_access_webapps(cls, user_id: str, app_ids: list[str]):
|
|
if not app_ids:
|
|
return {}
|
|
body = {"userId": user_id, "appIds": app_ids}
|
|
data = EnterpriseRequest.send_request("POST", "/webapp/permission/batch", json=body)
|
|
if not data:
|
|
raise ValueError("No data found.")
|
|
return data.get("permissions", {})
|
|
|
|
@classmethod
|
|
def get_app_access_mode_by_id(cls, app_id: str) -> WebAppSettings:
|
|
if not app_id:
|
|
raise ValueError("app_id must be provided.")
|
|
params = {"appId": app_id}
|
|
data = EnterpriseRequest.send_request("GET", "/webapp/access-mode/id", params=params)
|
|
if not data:
|
|
raise ValueError("No data found.")
|
|
return WebAppSettings.model_validate(data)
|
|
|
|
@classmethod
|
|
def batch_get_app_access_mode_by_id(cls, app_ids: list[str]) -> dict[str, WebAppSettings]:
|
|
if not app_ids:
|
|
return {}
|
|
body = {"appIds": app_ids}
|
|
data: dict[str, str] = EnterpriseRequest.send_request("POST", "/webapp/access-mode/batch/id", json=body)
|
|
if not data:
|
|
raise ValueError("No data found.")
|
|
|
|
if not isinstance(data["accessModes"], dict):
|
|
raise ValueError("Invalid data format.")
|
|
|
|
ret = {}
|
|
for key, value in data["accessModes"].items():
|
|
curr = WebAppSettings()
|
|
curr.access_mode = value
|
|
ret[key] = curr
|
|
|
|
return ret
|
|
|
|
@classmethod
|
|
def update_app_access_mode(cls, app_id: str, access_mode: str):
|
|
if not app_id:
|
|
raise ValueError("app_id must be provided.")
|
|
if access_mode not in ["public", "private", "private_all"]:
|
|
raise ValueError("access_mode must be either 'public', 'private', or 'private_all'")
|
|
|
|
data = {"appId": app_id, "accessMode": access_mode}
|
|
|
|
response = EnterpriseRequest.send_request("POST", "/webapp/access-mode", json=data)
|
|
|
|
return response.get("result", False)
|
|
|
|
@classmethod
|
|
def cleanup_webapp(cls, app_id: str):
|
|
if not app_id:
|
|
raise ValueError("app_id must be provided.")
|
|
|
|
params = {"appId": app_id}
|
|
EnterpriseRequest.send_request("DELETE", "/webapp/clean", params=params)
|