mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
Final-review minors:
- is_user_supplied_secret: a verbatim match needs a remainder of at least
8 characters, so "Bearer key" no longer counts as supplied because the goal
says "key".
- is_credential_key: Ocp-Apim-Subscription-Key ("subscription" key prefix),
and cookie / credential / credentials / bearer as last segments. No
"header" segment -- accept_header stays non-credential.
- A bare Bearer/Basic/Token word is kept as an auth scheme only on an
Authorization / Proxy-Authorization line; under any other credential key
(X-Auth-Token: token, api_key: token) it is replaced whole.
- endpoint_variable_names also collects a start template inside the parsed
host (https://{{#s.host#}}/v1), via the shared core/dify_builder/urls.py;
path/query templates stay mockable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
3.7 KiB
Python
85 lines
3.7 KiB
Python
"""Whether a header/param/field key names a credential.
|
|
|
|
Pure, no I/O, stdlib-only -- ``core/dify_builder`` must not import from
|
|
``services``, so this is the single shared home for ``is_credential_key``
|
|
(moved here in the Task 3 review fix round) and its params-only sibling
|
|
``is_credential_param_key``. Both ``core/dify_builder/handlers_fix.py``
|
|
(deciding which start variables a mocked/prefilled form must skip) and
|
|
``services/dify_builder/agent/build.py`` (deciding which literals to ground)
|
|
need the SAME rule, so there is one definition; ``user_supplied.py`` imports
|
|
and re-exports both so existing ``services``-side callers see no API change.
|
|
"""
|
|
|
|
import re
|
|
|
|
# A dict/field key's last word-segment that, by itself, names a credential
|
|
# regardless of what comes before it (so "access_token" and "auth" both
|
|
# qualify, but "token_limit" does not -- "limit" is the last segment).
|
|
_CREDENTIAL_LAST_SEGMENTS = {
|
|
"authorization",
|
|
"auth",
|
|
"token",
|
|
"secret",
|
|
"password",
|
|
"passwd",
|
|
"cookie",
|
|
"credential",
|
|
"credentials",
|
|
"bearer",
|
|
}
|
|
|
|
# A last segment of "key" only counts as a credential when the segment (or
|
|
# merged word) before it names what kind of key it is -- otherwise ordinary
|
|
# fields like "key_points" would match on "key" alone. ("subscription" is
|
|
# Azure API Management's ``Ocp-Apim-Subscription-Key``.) There is
|
|
# deliberately no "header" last segment: ``accept_header`` is not a secret.
|
|
_CREDENTIAL_KEY_PREFIXES = {"api", "access", "secret", "private", "auth", "subscription"}
|
|
|
|
# Splits an identifier into camelCase words: an uppercase run immediately
|
|
# followed by "Xy" (e.g. the "API" in "APIKey"), or an optional leading
|
|
# capital plus a run of lowercase/digits (e.g. "api", "Key", "3").
|
|
_CAMEL_SEGMENT_RE = re.compile(r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z0-9]+|[A-Z]+|[0-9]+")
|
|
|
|
|
|
def _key_segments(key: str) -> list[str]:
|
|
"""Lowercase word-segments of a dict/field key, split on ``_``, ``-``,
|
|
``.``, whitespace, and camelCase boundaries -- so ``x-api-key``,
|
|
``apiKey`` and ``api_key`` all segment to ``["api", "key"]``."""
|
|
segments: list[str] = []
|
|
for part in re.split(r"[_\-.\s]+", key or ""):
|
|
if not part:
|
|
continue
|
|
segments.extend(match.lower() for match in _CAMEL_SEGMENT_RE.findall(part))
|
|
return segments
|
|
|
|
|
|
def is_credential_key(key: str) -> bool:
|
|
"""True iff ``key`` names a credential, decided by its LAST segment --
|
|
not by containing a credential-ish substring anywhere. So
|
|
``password_policy``, ``token_limit``, ``session_token_expiry`` and
|
|
``max_tokens`` are NOT credential keys, but ``api_key``, ``apiKey``,
|
|
``x-api-key``, ``access_token``, ``Authorization``, ``Cookie`` and
|
|
``Ocp-Apim-Subscription-Key`` are.
|
|
"""
|
|
segments = _key_segments(key)
|
|
if not segments:
|
|
return False
|
|
last = segments[-1]
|
|
if last in _CREDENTIAL_LAST_SEGMENTS or last == "apikey":
|
|
return True
|
|
return last == "key" and len(segments) >= 2 and segments[-2] in _CREDENTIAL_KEY_PREFIXES
|
|
|
|
|
|
def is_credential_param_key(key: str) -> bool:
|
|
"""``is_credential_key`` PLUS a bare ``key`` (case-insensitive, the WHOLE
|
|
key -- not merely its last segment): a raw ``?key=...`` query parameter is
|
|
a common way an API names its API key, which ``is_credential_key`` alone
|
|
misses (its last-segment rule requires a qualifying prefix like
|
|
``api``/``access`` before a bare ``key``). Params-only -- a header simply
|
|
named ``key`` is not a realistic credential header the way a query param
|
|
is, so this rule is not applied to headers. ``cache_key``/``key_points``
|
|
still segment to more than just ``key`` and stay non-credential, same as
|
|
under ``is_credential_key``.
|
|
"""
|
|
return is_credential_key(key) or (key or "").strip().lower() == "key"
|