Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion api/controllers/console/workspace/tool_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,11 @@ def post(self, provider):
current_user, current_tenant_id = current_account_with_tenant()
args = parser_default_cred.parse_args()
return BuiltinToolManageService.set_default_provider(
tenant_id=current_tenant_id, user_id=current_user.id, provider=provider, id=args["id"]
tenant_id=current_tenant_id,
user_id=current_user.id,
provider=provider,
id=args["id"],
account=current_user,
)


Expand Down
30 changes: 23 additions & 7 deletions api/services/tools/builtin_tools_manage_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import logging
from collections.abc import Mapping
from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from models.account import Account

from sqlalchemy import exists, select
from sqlalchemy.orm import Session
Expand Down Expand Up @@ -406,20 +409,33 @@ def delete_builtin_tool_provider(tenant_id: str, provider: str, credential_id: s
return {"result": "success"}

@staticmethod
def set_default_provider(tenant_id: str, user_id: str, provider: str, id: str):
def set_default_provider(tenant_id: str, user_id: str, provider: str, id: str, account: "Account | None" = None):
"""
set default provider
"""
with Session(db.engine) as session:
# get provider
target_provider = session.query(BuiltinToolProvider).filter_by(id=id).first()
# get provider (verify tenant ownership to prevent IDOR)
target_provider = session.query(BuiltinToolProvider).filter_by(id=id, tenant_id=tenant_id).first()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The set_default_provider method currently lacks validation to ensure the provider name from the request matches the provider attribute of the credential being updated. This logic flaw could allow an attacker to bypass the intended clearing mechanism and create multiple default credentials for a single provider, leading to unpredictable behavior. While the inclusion of tenant_id=tenant_id correctly prevents IDOR vulnerabilities, the provider validation is still crucial for maintaining data integrity and preventing unintended credential assignments.

            # get provider (verify tenant ownership and provider match to prevent IDOR and logic flaws)
            target_provider = session.query(BuiltinToolProvider).filter_by(id=id, tenant_id=tenant_id, provider=provider).first()

if target_provider is None:
raise ValueError("provider not found")

# clear default provider
session.query(BuiltinToolProvider).filter_by(
tenant_id=tenant_id, user_id=user_id, provider=provider, is_default=True
).update({"is_default": False})
if dify_config.ENTERPRISE_ENABLED:
# Enterprise: verify admin permission for tenant-wide operation
from models.account import TenantAccountRole

if account and not TenantAccountRole.is_privileged_role(account.current_role):
raise ValueError("Only workspace admins/owners can set default credentials in enterprise mode")
Comment on lines +427 to +428
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The permission check if account and not TenantAccountRole.is_privileged_role(account.current_role): is a high-priority security measure. It correctly restricts the ability to set tenant-wide default credentials to only privileged roles (admins/owners) in enterprise mode, preventing unauthorized changes.

# Enterprise: clear ALL defaults for this provider in the tenant
# (regardless of user_id, since enterprise credentials may have different user_id)
session.query(BuiltinToolProvider).filter_by(
tenant_id=tenant_id, provider=provider, is_default=True
).update({"is_default": False})
Comment on lines +431 to +433
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This change correctly implements the core logic for enterprise mode, clearing all is_default=True flags for the specified provider within the tenant. This directly resolves the problem described in EE-1511, where credentials synced from global might have different user_ids, leading to multiple active defaults.

else:
# Non-enterprise: only clear defaults for the current user
session.query(BuiltinToolProvider).filter_by(
tenant_id=tenant_id, user_id=user_id, provider=provider, is_default=True
).update({"is_default": False})

# set new default provider
target_provider.is_default = True
Expand Down
Loading