-
-
Notifications
You must be signed in to change notification settings - Fork 11.8k
fix(team-callbacks): report API-registered callbacks from GET /team/{team_id}/callback #35512
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f02f4c1
82dc8fa
582587d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,16 @@ | |
| UserAPIKeyAuth, | ||
| ) | ||
| from litellm.proxy.auth.user_api_key_auth import user_api_key_auth | ||
| from litellm.proxy.common_utils.callback_utils import encrypt_callback_vars | ||
| from litellm.proxy.common_utils.callback_utils import ( | ||
| _CALLBACK_VAR_ENCRYPTED_PREFIX, | ||
| decrypt_callback_vars, | ||
| encrypt_callback_vars, | ||
| is_sensitive_callback_key, | ||
| ) | ||
| from litellm.proxy.litellm_pre_call_utils import ( | ||
| _get_validated_callback_metadata, | ||
| convert_key_logging_metadata_to_callback, | ||
| ) | ||
| from litellm.proxy.management_endpoints.team_endpoints import _verify_team_access | ||
| from litellm.proxy.management_helpers.utils import management_endpoint_wrapper | ||
| from litellm.repositories.team_repository import TeamRepository | ||
|
|
@@ -64,6 +73,76 @@ def _redact_callback_secrets(metadata: Any) -> Any: | |
| return redacted | ||
|
|
||
|
|
||
| def _mask_sensitive_callback_vars(callbacks: TeamCallbackMetadata) -> None: | ||
| """Mask credential-bearing callback vars in place, keeping the rest readable. | ||
|
|
||
| ``callback_vars`` mixes credentials (``langsmith_api_key``, | ||
| ``langfuse_secret_key``, ``gcs_path_service_account``) with plain | ||
| configuration (project names, bucket names, hosts). The configuration is | ||
| what makes a read of this endpoint useful, so only the sensitive keys are | ||
| replaced, using the same marker as the audit-log redaction above. | ||
|
|
||
| A value that still carries the encrypted prefix here failed to decrypt, so | ||
| it is masked too. Handing back a ciphertext blob under a key that is not | ||
| classified as sensitive would give the caller something it cannot use and | ||
| cannot tell apart from a real value. | ||
|
|
||
| Masking in place rather than rebuilding the mapping keeps this under the | ||
| LIT002 mutable-collection-construction budget. It is safe because the only | ||
| caller passes an object it just built from a decrypted deep copy of the | ||
| row, so nothing here is reachable from the team's stored metadata. | ||
| """ | ||
| if not callbacks.callback_vars: | ||
| return | ||
| for key in tuple(callbacks.callback_vars): | ||
| value = callbacks.callback_vars[key] | ||
| if is_sensitive_callback_key(key) or str(value).startswith(_CALLBACK_VAR_ENCRYPTED_PREFIX): | ||
| callbacks.callback_vars[key] = _CALLBACK_VARS_REDACTED | ||
|
|
||
|
|
||
| def _resolve_team_callbacks(team_metadata: object) -> TeamCallbackMetadata: | ||
| """Report the callbacks that are actually in effect for a team. | ||
|
|
||
| A team's callback config can live in either of two metadata slots. | ||
| ``metadata["logging"]`` holds the ``AddTeamCallback`` entries written by | ||
| ``POST /team/{team_id}/callback`` and by the Admin UI, while | ||
| ``metadata["callback_settings"]`` holds the older ``TeamCallbackMetadata`` | ||
| shape. Request-time resolution in ``_get_dynamic_logging_metadata`` treats | ||
| the two as mutually exclusive: a populated ``logging`` slot wins outright | ||
| and ``callback_settings`` is consulted only as the deprecated fallback. | ||
| This reader applies the same precedence so it reports what a request would | ||
| really do. Merging the two instead would report a ``callback_settings`` | ||
| entry as active for a team whose requests never fire it. | ||
|
|
||
| Credential ``callback_vars`` are stored encrypted, so they are decrypted | ||
| before being masked by key; a value encrypted under a key that is no longer | ||
| classified as sensitive would otherwise come back as raw ciphertext. | ||
| """ | ||
| if not isinstance(team_metadata, dict): | ||
| return TeamCallbackMetadata() | ||
|
|
||
| decrypted = decrypt_callback_vars(team_metadata) | ||
| logging_entries = decrypted.get("logging") | ||
|
|
||
| if logging_entries is not None: | ||
| resolved = TeamCallbackMetadata() | ||
| for entry in logging_entries if isinstance(logging_entries, list) else (): | ||
| if not isinstance(entry, dict): | ||
| continue | ||
| callback = _get_validated_callback_metadata(item=entry, source="team-level read") | ||
| if callback is None: | ||
| continue | ||
| resolved = convert_key_logging_metadata_to_callback(data=callback, team_callback_settings_obj=resolved) | ||
| else: | ||
| callback_settings = decrypted.get("callback_settings") | ||
| resolved = ( | ||
| TeamCallbackMetadata(**callback_settings) if isinstance(callback_settings, dict) else TeamCallbackMetadata() | ||
| ) | ||
|
|
||
| _mask_sensitive_callback_vars(resolved) | ||
| return resolved | ||
|
Comment on lines
+95
to
+143
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 New callback resolution code builds results by repeated mutation instead of the required immutable style The newly added resolution helper repeatedly overwrites its accumulator and mutates the credential mapping in place ( Rule source and affected code
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
|
|
||
| def _log_audit_task_exception(task: "asyncio.Task[None]") -> None: | ||
| """Surface a fire-and-forget audit-log task failure. | ||
|
|
||
|
|
@@ -410,6 +489,12 @@ async def get_team_callbacks( | |
|
|
||
| This will return the callback settings for the team with id dbe2f686-a686-4896-864a-4c3924458709 | ||
|
|
||
| Covers callbacks registered through POST /team/{team_id}/callback and the Admin UI as well as | ||
| teams still on the deprecated callback_settings shape, resolved from the team's stored metadata | ||
| with the same precedence used at request time. A key-level logging config overrides the team's | ||
| at request time and is not reflected here. Credential-bearing callback_vars are returned masked | ||
| as `***REDACTED***` | ||
|
|
||
| Returns { | ||
| "status": "success", | ||
| "data": { | ||
|
|
@@ -442,12 +527,7 @@ async def get_team_callbacks( | |
| user_api_key_dict=user_api_key_dict, | ||
| ) | ||
|
|
||
| # Retrieve team callback settings from metadata | ||
| team_metadata = _existing_team.metadata | ||
| team_callback_settings = team_metadata.get("callback_settings", {}) | ||
|
|
||
| # Convert to TeamCallbackMetadata object for consistent structure | ||
| team_callback_settings_obj = TeamCallbackMetadata(**team_callback_settings) | ||
| team_callback_settings_obj = _resolve_team_callbacks(_existing_team.metadata) | ||
|
|
||
| return { | ||
| "status": "success", | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.