mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 13:51:25 +08:00
347 lines
14 KiB
Python
347 lines
14 KiB
Python
from base64 import b64encode
|
|
from collections.abc import Callable
|
|
from functools import wraps
|
|
from hashlib import sha1
|
|
from hmac import compare_digest
|
|
from hmac import new as hmac_new
|
|
|
|
from flask import abort, current_app, g, request
|
|
from flask.globals import request_ctx
|
|
|
|
from configs import dify_config
|
|
from core.db.session_factory import session_factory
|
|
from libs.exception import BaseHTTPException
|
|
from models.model import EndUser
|
|
from services.knowledge_fs.feature_service import get_knowledge_fs_features
|
|
|
|
|
|
class InnerApiUnauthorizedError(BaseHTTPException):
|
|
error_code = "inner_api_unauthorized"
|
|
description = "Unauthorized."
|
|
code = 401
|
|
|
|
|
|
def inner_api_only[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
|
"""Restrict access to callers authenticated with the shared inner API key."""
|
|
|
|
@wraps(view)
|
|
def decorated(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
if not dify_config.INNER_API:
|
|
abort(404)
|
|
|
|
inner_api_key = request.headers.get("X-Inner-Api-Key")
|
|
if not inner_api_key or inner_api_key != dify_config.INNER_API_KEY:
|
|
raise InnerApiUnauthorizedError()
|
|
|
|
return view(*args, **kwargs)
|
|
|
|
return decorated
|
|
|
|
|
|
def enterprise_inner_api_only[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
|
return inner_api_only(view)
|
|
|
|
|
|
def enterprise_inner_api_user_auth[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
|
"""Inject an EndUser for valid inner API HMAC auth, otherwise pass the request through unchanged."""
|
|
|
|
@wraps(view)
|
|
def decorated(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
if not dify_config.INNER_API:
|
|
return view(*args, **kwargs)
|
|
|
|
# get header 'X-Inner-Api-Key'
|
|
authorization = request.headers.get("Authorization")
|
|
if not authorization:
|
|
return view(*args, **kwargs)
|
|
|
|
parts = authorization.split(":")
|
|
if len(parts) != 2:
|
|
return view(*args, **kwargs)
|
|
|
|
user_id, token = parts
|
|
if " " in user_id:
|
|
user_id = user_id.split(" ")[1]
|
|
|
|
inner_api_key = request.headers.get("X-Inner-Api-Key", "")
|
|
|
|
data_to_sign = f"DIFY {user_id}"
|
|
|
|
signature = hmac_new(inner_api_key.encode("utf-8"), data_to_sign.encode("utf-8"), sha1)
|
|
signature_base64 = b64encode(signature.digest()).decode("utf-8")
|
|
|
|
if signature_base64 != token:
|
|
return view(*args, **kwargs)
|
|
|
|
with session_factory.create_session() as session:
|
|
kwargs["user"] = session.get(EndUser, user_id)
|
|
return view(*args, **kwargs)
|
|
|
|
return decorated
|
|
|
|
|
|
def plugin_inner_api_only[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
|
@wraps(view)
|
|
def decorated(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
if not dify_config.PLUGIN_DAEMON_KEY:
|
|
abort(404)
|
|
|
|
# get header 'X-Inner-Api-Key'
|
|
inner_api_key = request.headers.get("X-Inner-Api-Key")
|
|
if not inner_api_key or inner_api_key != dify_config.INNER_API_KEY_FOR_PLUGIN:
|
|
abort(404)
|
|
|
|
return view(*args, **kwargs)
|
|
|
|
return decorated
|
|
|
|
|
|
def agent_inner_api_only[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
|
"""Temporary alias for agent-backend inner API callers.
|
|
|
|
Agent tool and knowledge calls currently share the same trusted
|
|
`dify-agent -> Dify API` transport credentials as the plugin inner bridge.
|
|
Keep the wrapper name agent-specific so the controller surface does not grow
|
|
more plugin-only semantics while auth settings stay shared.
|
|
"""
|
|
|
|
return plugin_inner_api_only(view)
|
|
|
|
|
|
class AgentRuntimeRefused(BaseHTTPException):
|
|
error_code = "native_agent_control_refused"
|
|
description = "Native Agent control is unavailable."
|
|
code = 409
|
|
|
|
|
|
def agent_endpoint_api_only[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
|
"""Share a controller body between separately authenticated Resource wrappers.
|
|
|
|
The runtime Resource's outer guard creates this request-local admission. A
|
|
legacy Resource always uses its original plugin key and never calls lineage.
|
|
"""
|
|
|
|
@wraps(view)
|
|
def legacy(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
if "X-Dify-Agent-Dispatch" in request.headers:
|
|
raise AgentRuntimeRefused()
|
|
body = request.get_json(silent=True) if request.is_json else None
|
|
if isinstance(body, dict) and (
|
|
any(
|
|
body.get(key) is not None
|
|
for key in (
|
|
"native_dispatch",
|
|
"native_test_ref",
|
|
"native_parent_node",
|
|
"parent_node",
|
|
"ref",
|
|
)
|
|
)
|
|
or body.get("execution_mode") == "builder_test"
|
|
):
|
|
raise AgentRuntimeRefused()
|
|
if any(
|
|
key in request.args
|
|
for key in (
|
|
"native_dispatch",
|
|
"native_test_ref",
|
|
"native_parent_node",
|
|
"parent_node",
|
|
"ref",
|
|
)
|
|
):
|
|
raise AgentRuntimeRefused()
|
|
return view(*args, **kwargs)
|
|
|
|
authenticated_legacy = plugin_inner_api_only(legacy)
|
|
|
|
@wraps(view)
|
|
def decorated(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
if g.get("agent_runtime_admitted", False) and g.get("agent_runtime_request") is request_ctx.request:
|
|
return view(*args, **kwargs)
|
|
return authenticated_legacy(*args, **kwargs)
|
|
|
|
from libs.external_api import document_errors
|
|
|
|
return document_errors(decorated, AgentRuntimeRefused)
|
|
|
|
|
|
def agent_runtime_api_only(boundary: str):
|
|
"""Authenticate the Agent service, then prove actual persisted Run lineage.
|
|
|
|
Only used as the outer method decorator of explicit runtime Resources. No
|
|
body flag, legacy credential, or absent envelope can select ordinary mode.
|
|
"""
|
|
|
|
def decorate(view):
|
|
@wraps(view)
|
|
def guarded(*args, **kwargs):
|
|
from controllers.inner_api.agent.runtime_selection import (
|
|
parse_agent_run_selection,
|
|
)
|
|
from runtime_contracts.builder.execution import (
|
|
BuilderExecutionPolicyError,
|
|
)
|
|
from services.dify_builder.native_testing.agent_lineage import (
|
|
AgentServicePrincipal,
|
|
)
|
|
|
|
token = dify_config.AGENT_BACKEND_API_TOKEN
|
|
other_credentials = (
|
|
dify_config.INNER_API_KEY_FOR_PLUGIN,
|
|
dify_config.INNER_API_KEY,
|
|
dify_config.PLUGIN_DAEMON_KEY,
|
|
)
|
|
authorization = request.headers.get("Authorization", "")
|
|
if (
|
|
not token
|
|
or not token.strip()
|
|
or any(
|
|
credential and compare_digest(token.encode(), credential.encode())
|
|
for credential in other_credentials
|
|
)
|
|
or "X-Inner-Api-Key" in request.headers
|
|
or len(request.headers.getlist("Authorization")) != 1
|
|
or not authorization.startswith("Bearer ")
|
|
or not compare_digest(authorization[7:].encode(), token.encode())
|
|
):
|
|
raise InnerApiUnauthorizedError()
|
|
principal = AgentServicePrincipal()
|
|
try:
|
|
selection = parse_agent_run_selection(request.headers)
|
|
if boundary in {"prepare-run", "prepared-config"}:
|
|
from dify_agent.protocol.native_testing import (
|
|
MAX_AGENT_CONTROL_BYTES,
|
|
AgentPrepareRunPayload,
|
|
AgentReadPreparedConfigPayload,
|
|
)
|
|
|
|
from controllers.inner_api.agent.native_testing import _parse
|
|
|
|
_parse(
|
|
AgentPrepareRunPayload if boundary == "prepare-run" else AgentReadPreparedConfigPayload,
|
|
limit=MAX_AGENT_CONTROL_BYTES,
|
|
)
|
|
lineage = current_app.extensions.get("native_agent_endpoint_lineage")
|
|
if lineage is None:
|
|
raise BuilderExecutionPolicyError("native_endpoint_lineage_unavailable")
|
|
body = request.get_json(silent=True) if request.is_json else None
|
|
raw = body if isinstance(body, dict) else request.args.to_dict(flat=True)
|
|
caller = raw.get("caller", raw)
|
|
if not isinstance(caller, dict):
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
recovery_read = None
|
|
if boundary == "native-result-read":
|
|
from dify_agent.protocol.native_testing import MAX_AGENT_CONTROL_BYTES, AgentReadResultPayload
|
|
|
|
from controllers.inner_api.agent.native_testing import _parse
|
|
|
|
try:
|
|
payload = _parse(AgentReadResultPayload, limit=MAX_AGENT_CONTROL_BYTES)
|
|
except ValueError:
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch") from None
|
|
recovery_read = lineage.read_recovery_result(
|
|
principal=principal, selection=selection, payload=payload
|
|
)
|
|
context = (
|
|
recovery_read
|
|
if recovery_read is not None
|
|
else lineage.resolve(
|
|
principal=principal,
|
|
selection=selection,
|
|
boundary="native-control" if boundary == "native-result-read" else boundary,
|
|
caller=caller,
|
|
)
|
|
)
|
|
if context is None:
|
|
if (
|
|
boundary
|
|
in {
|
|
"prepare-run",
|
|
"prepared-config",
|
|
"native-control",
|
|
"native-settlement",
|
|
"native-result-read",
|
|
}
|
|
or raw.get("native_dispatch") is not None
|
|
or "X-Dify-Agent-Dispatch" in request.headers
|
|
):
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
elif recovery_read is not None:
|
|
if raw.get("ref") != recovery_read.ref.model_dump(mode="json"):
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
elif boundary == "config":
|
|
from controllers.inner_api.agent.runtime_selection import parse_agent_config_dispatch
|
|
|
|
if context.native_test_profile != "confirmed" or not (
|
|
request.method == "GET"
|
|
or (request.method == "POST" and request.path.endswith("/download-request"))
|
|
):
|
|
raise BuilderExecutionPolicyError("native_endpoint_dependency_unavailable")
|
|
envelope = parse_agent_config_dispatch(request.headers)
|
|
if envelope.ref != context.ref:
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
g.agent_runtime_dispatch = envelope
|
|
elif boundary in {"prepare-run", "prepared-config"}:
|
|
if selection.parent_node is None or raw.get("parent_node") != selection.parent_node.model_dump(
|
|
mode="json"
|
|
):
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
if boundary == "prepare-run" and (
|
|
raw.get("agent_run_id") != selection.agent_run_id
|
|
or raw.get("request_digest") != selection.request_digest
|
|
):
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
if boundary == "prepared-config" and raw.get("config_ref") != context.config.config_ref:
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
elif boundary in {"native-control", "native-settlement", "native-result-read"}:
|
|
if raw.get("ref") != context.ref.model_dump(mode="json"):
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
elif boundary in {
|
|
"model",
|
|
"core_tool",
|
|
"knowledge",
|
|
"file_upload",
|
|
"file_download",
|
|
}:
|
|
from dify_agent.adapters.native_testing.client import (
|
|
AgentPhysicalDispatchPayload,
|
|
)
|
|
|
|
try:
|
|
envelope = AgentPhysicalDispatchPayload.model_validate(raw.get("native_dispatch"))
|
|
except ValueError:
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch") from None
|
|
if envelope.ref != context.ref:
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
# The inherited controller validates final target/arguments
|
|
# at its shared preflight; its physical owner remains closed.
|
|
else:
|
|
raise BuilderExecutionPolicyError("native_endpoint_dependency_unavailable")
|
|
except BuilderExecutionPolicyError:
|
|
raise AgentRuntimeRefused() from None
|
|
# g can outlive one request under an explicitly pushed app context.
|
|
# Bind this admission to the actual request object, not only g.
|
|
g.agent_runtime_request = request_ctx.request
|
|
g.agent_runtime_principal = principal
|
|
g.agent_runtime_selection = selection
|
|
g.agent_runtime_context = context
|
|
g.agent_runtime_admitted = True
|
|
return view(*args, **kwargs)
|
|
|
|
return guarded
|
|
|
|
return decorate
|
|
|
|
|
|
def knowledge_fs_inner_api_only[**P, R](view: Callable[P, R]) -> Callable[P, R]:
|
|
"""Authenticate the trusted KnowledgeFS process while the product is available."""
|
|
|
|
@wraps(view)
|
|
def enabled_view(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
if not get_knowledge_fs_features().enabled:
|
|
abort(404)
|
|
return view(*args, **kwargs)
|
|
|
|
return plugin_inner_api_only(enabled_view)
|