853 lines
30 KiB
Python
853 lines
30 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Literal
|
|
from fastapi import APIRouter, Depends, Query, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_core.auth import ApiPrincipal, require_any_scope
|
|
from govoplan_files.backend.change_tracking import (
|
|
FILES_CONNECTOR_CREDENTIALS_COLLECTION,
|
|
FILES_CONNECTOR_POLICIES_COLLECTION,
|
|
FILES_CONNECTOR_PROFILES_COLLECTION,
|
|
)
|
|
from govoplan_files.backend.schemas import (
|
|
FileConnectorCredentialCreateRequest,
|
|
FileConnectorCredentialResponse,
|
|
FileConnectorCredentialsResponse,
|
|
FileConnectorCredentialUpdateRequest,
|
|
FileConnectorDiscoveryRequest,
|
|
FileConnectorDiscoveryResponse,
|
|
FileConnectorSettingsDeltaResponse,
|
|
FileConnectorPolicyEvaluateRequest,
|
|
FileConnectorPolicyEvaluateResponse,
|
|
FileConnectorPolicyResponse,
|
|
FileConnectorPolicyUpdateRequest,
|
|
FileConnectorProfileCreateRequest,
|
|
FileConnectorProfileResponse,
|
|
FileConnectorProfilesResponse,
|
|
FileConnectorProviderResponse,
|
|
FileConnectorProvidersResponse,
|
|
)
|
|
from govoplan_core.db.session import get_session
|
|
from govoplan_files.backend.storage.common import FileStorageError
|
|
from govoplan_files.backend.storage.connector_credential_store import (
|
|
create_connector_credential_row,
|
|
get_connector_credential_row,
|
|
list_database_connector_credentials,
|
|
update_connector_credential_row,
|
|
)
|
|
from govoplan_files.backend.storage.connector_credential_deletion import (
|
|
delete_connector_credential_row,
|
|
)
|
|
from govoplan_files.backend.storage.connector_browse import (
|
|
ConnectorBrowseError,
|
|
ConnectorBrowseUnsupported,
|
|
browse_connector_profile,
|
|
)
|
|
from govoplan_files.backend.storage.connector_deployment import (
|
|
connector_effective_endpoint_url,
|
|
reject_api_controlled_deployment_references,
|
|
)
|
|
from govoplan_files.backend.storage.connector_profile_store import (
|
|
connector_profile_from_row,
|
|
create_connector_profile_row,
|
|
)
|
|
from govoplan_files.backend.storage.connector_providers import (
|
|
connector_provider_descriptors,
|
|
)
|
|
from govoplan_files.backend.storage.connector_policy import (
|
|
ConnectorAccessRequest,
|
|
ConnectorPolicyDenied,
|
|
connector_policy_decision,
|
|
connector_policy_sources_from_payload,
|
|
)
|
|
from govoplan_files.backend.storage.connector_policy_store import (
|
|
connector_policy_response,
|
|
set_connector_policy,
|
|
)
|
|
|
|
|
|
from govoplan_files.backend.route_support import (
|
|
FILES_CONNECTOR_CREDENTIAL_RESOURCE,
|
|
FILES_CONNECTOR_POLICY_RESOURCE,
|
|
FILES_CONNECTOR_PROFILE_RESOURCE,
|
|
_audit_connector_discovery_attempt,
|
|
_can_read_disabled_connector_profiles,
|
|
_connector_credential_response,
|
|
_connector_policy_error,
|
|
_credential_row_for_profile,
|
|
_discovery_profile_from_payload,
|
|
_ensure_campaign_file_access,
|
|
_ensure_connector_configuration_allowed,
|
|
_ensure_connector_credential_configuration_allowed,
|
|
_ensure_connector_local_policy_allowed,
|
|
_file_connector_policy_resource_id,
|
|
_file_connector_settings_entries,
|
|
_http_error,
|
|
_record_connector_settings_change,
|
|
_require_connector_credential_write,
|
|
_require_connector_policy_read,
|
|
_require_connector_profile_write,
|
|
_same_endpoint,
|
|
_visible_connector_profiles,
|
|
_webdav_discovery_candidates,
|
|
)
|
|
from govoplan_files.backend.services.connector_settings_delta import (
|
|
_full_file_connector_settings_delta_response,
|
|
_incremental_file_connector_settings_delta_response,
|
|
)
|
|
|
|
router = APIRouter(prefix="/files", tags=["files"])
|
|
|
|
|
|
@router.post(
|
|
"/connector-policy/evaluate", response_model=FileConnectorPolicyEvaluateResponse
|
|
)
|
|
def evaluate_connector_policy(
|
|
payload: FileConnectorPolicyEvaluateRequest,
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope("files:file:read", "files:file:upload", "files:file:download")
|
|
),
|
|
):
|
|
del principal
|
|
sources = connector_policy_sources_from_payload(
|
|
[item.model_dump(mode="json") for item in payload.policy_sources]
|
|
)
|
|
request = ConnectorAccessRequest.from_provenance(
|
|
payload.source_provenance.model_dump(mode="json", exclude_none=True),
|
|
operation=payload.operation,
|
|
)
|
|
return FileConnectorPolicyEvaluateResponse(
|
|
decision=connector_policy_decision(request, sources).to_dict()
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/connectors/settings/delta", response_model=FileConnectorSettingsDeltaResponse
|
|
)
|
|
def connector_settings_delta(
|
|
scope_type: str = Query(default="tenant"),
|
|
scope_id: str | None = Query(default=None),
|
|
provider: str | None = None,
|
|
campaign_id: str | None = None,
|
|
include_disabled: bool = False,
|
|
include_inactive: bool = False,
|
|
owner_type: Literal["user", "group"] | None = None,
|
|
owner_id: str | None = None,
|
|
since: str | None = None,
|
|
limit: int = Query(default=100, ge=1, le=500),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:admin", "system:settings:read", "admin:settings:read"
|
|
)
|
|
),
|
|
):
|
|
scope_type = scope_type.strip().casefold()
|
|
_require_connector_policy_read(principal, scope_type)
|
|
try:
|
|
if since is None:
|
|
return _full_file_connector_settings_delta_response(
|
|
session,
|
|
principal,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
provider=provider,
|
|
campaign_id=campaign_id,
|
|
include_disabled=include_disabled,
|
|
include_inactive=include_inactive,
|
|
owner_type=owner_type,
|
|
owner_id=owner_id,
|
|
)
|
|
entries, has_more = _file_connector_settings_entries(
|
|
session, tenant_id=principal.tenant_id, since=since, limit=limit
|
|
)
|
|
if entries is None:
|
|
return _full_file_connector_settings_delta_response(
|
|
session,
|
|
principal,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
provider=provider,
|
|
campaign_id=campaign_id,
|
|
include_disabled=include_disabled,
|
|
include_inactive=include_inactive,
|
|
owner_type=owner_type,
|
|
owner_id=owner_id,
|
|
)
|
|
return _incremental_file_connector_settings_delta_response(
|
|
session,
|
|
principal,
|
|
entries=entries,
|
|
has_more=has_more,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
provider=provider,
|
|
campaign_id=campaign_id,
|
|
include_disabled=include_disabled,
|
|
include_inactive=include_inactive,
|
|
owner_type=owner_type,
|
|
owner_id=owner_id,
|
|
)
|
|
except (FileStorageError, ValueError, json.JSONDecodeError) as exc:
|
|
raise _http_error(exc) from exc
|
|
|
|
|
|
@router.get("/connectors/providers", response_model=FileConnectorProvidersResponse)
|
|
def list_connector_providers(
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope("files:file:read", "files:file:upload", "files:file:admin")
|
|
),
|
|
):
|
|
del principal
|
|
return FileConnectorProvidersResponse(
|
|
providers=[
|
|
FileConnectorProviderResponse(**item.to_response())
|
|
for item in connector_provider_descriptors()
|
|
]
|
|
)
|
|
|
|
|
|
@router.post("/connectors/discover", response_model=FileConnectorDiscoveryResponse)
|
|
def discover_connector_endpoint(
|
|
payload: FileConnectorDiscoveryRequest,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:admin", "system:settings:write", "admin:settings:write"
|
|
)
|
|
),
|
|
):
|
|
try:
|
|
reject_api_controlled_deployment_references(
|
|
password_env=payload.credentials.password_env,
|
|
token_env=payload.credentials.token_env,
|
|
secret_ref=payload.credentials.secret_ref,
|
|
metadata=payload.metadata,
|
|
)
|
|
except ValueError as exc:
|
|
raise _http_error(exc) from exc
|
|
if payload.provider not in {"webdav", "nextcloud"}:
|
|
return FileConnectorDiscoveryResponse(
|
|
provider=payload.provider,
|
|
endpoint_url=None,
|
|
base_path=payload.base_path,
|
|
status="unsupported",
|
|
message=f"Discovery is not implemented for {payload.provider} connectors yet",
|
|
)
|
|
candidates: list[dict[str, str]] = []
|
|
for endpoint_url in _webdav_discovery_candidates(payload):
|
|
profile = _discovery_profile_from_payload(
|
|
payload, endpoint_url, principal=principal
|
|
)
|
|
try:
|
|
_ensure_connector_configuration_allowed(
|
|
session,
|
|
principal,
|
|
connector_id=None,
|
|
credential_id=None,
|
|
provider=profile.provider,
|
|
endpoint_url=endpoint_url,
|
|
base_path=profile.base_path,
|
|
scope_type="tenant",
|
|
scope_id=principal.tenant_id,
|
|
operation="discover",
|
|
)
|
|
_audit_connector_discovery_attempt(
|
|
session,
|
|
principal,
|
|
provider=profile.provider,
|
|
endpoint_url=endpoint_url,
|
|
base_path=profile.base_path,
|
|
)
|
|
browse_connector_profile(profile, path=payload.base_path or "")
|
|
except ConnectorPolicyDenied as exc:
|
|
raise _connector_policy_error(exc) from exc
|
|
except (
|
|
ConnectorBrowseError,
|
|
ConnectorBrowseUnsupported,
|
|
OSError,
|
|
ValueError,
|
|
json.JSONDecodeError,
|
|
) as exc:
|
|
message = str(exc)
|
|
if "credentials were rejected" in message.casefold():
|
|
if payload.require_valid_credentials:
|
|
candidates.append(
|
|
{
|
|
"endpoint_url": endpoint_url,
|
|
"status": "credentials_rejected",
|
|
"message": "The endpoint exists, but the credentials were rejected.",
|
|
}
|
|
)
|
|
return FileConnectorDiscoveryResponse(
|
|
provider=payload.provider,
|
|
endpoint_url=endpoint_url,
|
|
base_path=payload.base_path,
|
|
status="credentials_rejected",
|
|
message="The endpoint was found, but login failed with these credentials.",
|
|
candidates=candidates,
|
|
metadata={"discovered_by": "webdav-auth-challenge"},
|
|
)
|
|
candidates.append(
|
|
{
|
|
"endpoint_url": endpoint_url,
|
|
"status": "found",
|
|
"message": "The endpoint exists, but credentials are required or were rejected.",
|
|
}
|
|
)
|
|
return FileConnectorDiscoveryResponse(
|
|
provider=payload.provider,
|
|
endpoint_url=endpoint_url,
|
|
base_path=payload.base_path,
|
|
status="found",
|
|
message="The endpoint was found. Add working credentials before saving or testing the connection.",
|
|
candidates=candidates,
|
|
metadata={"discovered_by": "webdav-auth-challenge"},
|
|
)
|
|
candidates.append(
|
|
{"endpoint_url": endpoint_url, "status": "failed", "message": message}
|
|
)
|
|
continue
|
|
status_value = (
|
|
"usable" if _same_endpoint(endpoint_url, payload.endpoint_url) else "found"
|
|
)
|
|
message = (
|
|
"The supplied URL is directly usable."
|
|
if status_value == "usable"
|
|
else "A usable connector endpoint was discovered."
|
|
)
|
|
candidates.append(
|
|
{"endpoint_url": endpoint_url, "status": status_value, "message": message}
|
|
)
|
|
return FileConnectorDiscoveryResponse(
|
|
provider=payload.provider,
|
|
endpoint_url=endpoint_url,
|
|
base_path=payload.base_path,
|
|
status=status_value,
|
|
message=message,
|
|
candidates=candidates,
|
|
metadata={"discovered_by": "webdav-propfind"},
|
|
)
|
|
return FileConnectorDiscoveryResponse(
|
|
provider=payload.provider,
|
|
endpoint_url=None,
|
|
base_path=payload.base_path,
|
|
status="not_found",
|
|
message="No usable WebDAV endpoint was found for this server URL.",
|
|
candidates=candidates,
|
|
)
|
|
|
|
|
|
@router.get("/connectors/credentials", response_model=FileConnectorCredentialsResponse)
|
|
def list_connector_credentials(
|
|
provider: str | None = None,
|
|
include_disabled: bool = False,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:admin", "system:settings:read", "admin:settings:read"
|
|
)
|
|
),
|
|
):
|
|
provider_norm = provider.strip().casefold() if provider else None
|
|
credentials = list_database_connector_credentials(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
include_disabled=include_disabled
|
|
and _can_read_disabled_connector_profiles(principal),
|
|
)
|
|
return FileConnectorCredentialsResponse(
|
|
credentials=[
|
|
FileConnectorCredentialResponse(**credential.to_response())
|
|
for credential in credentials
|
|
if provider_norm is None or credential.provider in {None, provider_norm}
|
|
]
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/connectors/policies/{scope_type}", response_model=FileConnectorPolicyResponse
|
|
)
|
|
def read_connector_policy(
|
|
scope_type: str,
|
|
scope_id: str | None = Query(default=None),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:admin", "system:settings:read", "admin:settings:read"
|
|
)
|
|
),
|
|
):
|
|
_require_connector_policy_read(principal, scope_type)
|
|
try:
|
|
return FileConnectorPolicyResponse(
|
|
**connector_policy_response(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
)
|
|
)
|
|
except (FileStorageError, ValueError, json.JSONDecodeError) as exc:
|
|
raise _http_error(exc, not_found=True) from exc
|
|
|
|
|
|
@router.put(
|
|
"/connectors/policies/{scope_type}", response_model=FileConnectorPolicyResponse
|
|
)
|
|
def write_connector_policy(
|
|
scope_type: str,
|
|
payload: FileConnectorPolicyUpdateRequest,
|
|
scope_id: str | None = Query(default=None),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:admin", "system:settings:write", "admin:settings:write"
|
|
)
|
|
),
|
|
):
|
|
_require_connector_profile_write(principal, scope_type)
|
|
try:
|
|
set_connector_policy(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
user_id=principal.user.id,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
policy=payload.policy,
|
|
)
|
|
_record_connector_settings_change(
|
|
session,
|
|
collection=FILES_CONNECTOR_POLICIES_COLLECTION,
|
|
resource_type=FILES_CONNECTOR_POLICY_RESOURCE,
|
|
resource_id=_file_connector_policy_resource_id(scope_type, scope_id),
|
|
operation="updated",
|
|
principal=principal,
|
|
tenant_id=None
|
|
if scope_type.strip().casefold() == "system"
|
|
else principal.tenant_id,
|
|
payload={"scope_type": scope_type.strip().casefold(), "scope_id": scope_id},
|
|
)
|
|
session.commit()
|
|
return FileConnectorPolicyResponse(
|
|
**connector_policy_response(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
)
|
|
)
|
|
except (FileStorageError, ValueError, json.JSONDecodeError) as exc:
|
|
session.rollback()
|
|
raise _http_error(exc) from exc
|
|
|
|
|
|
@router.post(
|
|
"/connectors/credentials",
|
|
response_model=FileConnectorCredentialResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
)
|
|
def create_connector_credential(
|
|
payload: FileConnectorCredentialCreateRequest,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:admin", "system:settings:write", "admin:settings:write"
|
|
)
|
|
),
|
|
):
|
|
_require_connector_credential_write(principal, payload.scope_type)
|
|
credentials = payload.credentials
|
|
try:
|
|
_ensure_connector_credential_configuration_allowed(
|
|
session,
|
|
principal,
|
|
credential_id=payload.id,
|
|
provider=payload.provider,
|
|
scope_type=payload.scope_type,
|
|
scope_id=payload.scope_id,
|
|
operation="configure_credentials",
|
|
)
|
|
_ensure_connector_local_policy_allowed(
|
|
session,
|
|
principal,
|
|
scope_type=payload.scope_type,
|
|
scope_id=payload.scope_id,
|
|
policy=payload.policy,
|
|
)
|
|
row = create_connector_credential_row(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
user_id=principal.user.id,
|
|
credential_id=payload.id,
|
|
label=payload.label,
|
|
provider=payload.provider,
|
|
scope_type=payload.scope_type,
|
|
scope_id=payload.scope_id,
|
|
enabled=payload.enabled,
|
|
credential_mode=payload.credential_mode,
|
|
username=credentials.username,
|
|
password=credentials.password,
|
|
token=credentials.token,
|
|
password_env=credentials.password_env,
|
|
token_env=credentials.token_env,
|
|
secret_ref=credentials.secret_ref,
|
|
policy=payload.policy,
|
|
metadata=payload.metadata,
|
|
)
|
|
_record_connector_settings_change(
|
|
session,
|
|
collection=FILES_CONNECTOR_CREDENTIALS_COLLECTION,
|
|
resource_type=FILES_CONNECTOR_CREDENTIAL_RESOURCE,
|
|
resource_id=row.id,
|
|
operation="created",
|
|
principal=principal,
|
|
tenant_id=row.tenant_id,
|
|
payload={
|
|
"scope_type": row.scope_type,
|
|
"scope_id": row.scope_id,
|
|
"provider": row.provider,
|
|
},
|
|
)
|
|
session.commit()
|
|
session.refresh(row)
|
|
return _connector_credential_response(row)
|
|
except ConnectorPolicyDenied as exc:
|
|
session.rollback()
|
|
raise _connector_policy_error(exc) from exc
|
|
except (FileStorageError, ValueError, json.JSONDecodeError) as exc:
|
|
session.rollback()
|
|
raise _http_error(exc) from exc
|
|
|
|
|
|
@router.get(
|
|
"/connectors/credentials/{credential_id}",
|
|
response_model=FileConnectorCredentialResponse,
|
|
)
|
|
def get_connector_credential(
|
|
credential_id: str,
|
|
include_disabled: bool = False,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:admin", "system:settings:read", "admin:settings:read"
|
|
)
|
|
),
|
|
):
|
|
try:
|
|
row = get_connector_credential_row(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
credential_id=credential_id,
|
|
include_disabled=include_disabled
|
|
and _can_read_disabled_connector_profiles(principal),
|
|
)
|
|
return _connector_credential_response(row)
|
|
except FileStorageError as exc:
|
|
raise _http_error(exc, not_found=True) from exc
|
|
|
|
|
|
@router.patch(
|
|
"/connectors/credentials/{credential_id}",
|
|
response_model=FileConnectorCredentialResponse,
|
|
)
|
|
def update_connector_credential(
|
|
credential_id: str,
|
|
payload: FileConnectorCredentialUpdateRequest,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:admin", "system:settings:write", "admin:settings:write"
|
|
)
|
|
),
|
|
):
|
|
try:
|
|
row = get_connector_credential_row(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
credential_id=credential_id,
|
|
include_disabled=True,
|
|
)
|
|
except FileStorageError as exc:
|
|
raise _http_error(exc, not_found=True) from exc
|
|
_require_connector_credential_write(principal, row.scope_type)
|
|
credentials = payload.credentials
|
|
try:
|
|
provider = payload.provider if payload.provider is not None else row.provider
|
|
_ensure_connector_credential_configuration_allowed(
|
|
session,
|
|
principal,
|
|
credential_id=row.id,
|
|
provider=provider,
|
|
scope_type=row.scope_type,
|
|
scope_id=row.scope_id,
|
|
operation="configure_credentials",
|
|
)
|
|
if payload.policy is not None:
|
|
_ensure_connector_local_policy_allowed(
|
|
session,
|
|
principal,
|
|
scope_type=row.scope_type,
|
|
scope_id=row.scope_id,
|
|
policy=payload.policy,
|
|
)
|
|
update_connector_credential_row(
|
|
session,
|
|
row,
|
|
user_id=principal.user.id,
|
|
label=payload.label,
|
|
provider=payload.provider,
|
|
enabled=payload.enabled,
|
|
credential_mode=payload.credential_mode,
|
|
username=credentials.username if credentials else None,
|
|
password=credentials.password if credentials else None,
|
|
token=credentials.token if credentials else None,
|
|
password_env=credentials.password_env if credentials else None,
|
|
token_env=credentials.token_env if credentials else None,
|
|
secret_ref=credentials.secret_ref if credentials else None,
|
|
clear_password=payload.clear_password,
|
|
clear_token=payload.clear_token,
|
|
policy=payload.policy,
|
|
metadata=payload.metadata,
|
|
)
|
|
_record_connector_settings_change(
|
|
session,
|
|
collection=FILES_CONNECTOR_CREDENTIALS_COLLECTION,
|
|
resource_type=FILES_CONNECTOR_CREDENTIAL_RESOURCE,
|
|
resource_id=row.id,
|
|
operation="updated",
|
|
principal=principal,
|
|
tenant_id=row.tenant_id,
|
|
payload={
|
|
"scope_type": row.scope_type,
|
|
"scope_id": row.scope_id,
|
|
"provider": row.provider,
|
|
},
|
|
)
|
|
session.commit()
|
|
session.refresh(row)
|
|
return _connector_credential_response(row)
|
|
except ConnectorPolicyDenied as exc:
|
|
session.rollback()
|
|
raise _connector_policy_error(exc) from exc
|
|
except (FileStorageError, ValueError, json.JSONDecodeError) as exc:
|
|
session.rollback()
|
|
raise _http_error(exc) from exc
|
|
|
|
|
|
@router.delete(
|
|
"/connectors/credentials/{credential_id}",
|
|
response_model=FileConnectorCredentialResponse,
|
|
)
|
|
def deactivate_connector_credential(
|
|
credential_id: str,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:admin", "system:settings:write", "admin:settings:write"
|
|
)
|
|
),
|
|
):
|
|
try:
|
|
row = get_connector_credential_row(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
credential_id=credential_id,
|
|
include_disabled=True,
|
|
)
|
|
except FileStorageError as exc:
|
|
raise _http_error(exc, not_found=True) from exc
|
|
_require_connector_credential_write(principal, row.scope_type)
|
|
try:
|
|
deletion = delete_connector_credential_row(
|
|
session,
|
|
row,
|
|
deletion_reason="api_delete",
|
|
user_id=principal.user.id,
|
|
api_key_id=principal.api_key.id if principal.api_key else None,
|
|
)
|
|
if deletion.changed:
|
|
_record_connector_settings_change(
|
|
session,
|
|
collection=FILES_CONNECTOR_CREDENTIALS_COLLECTION,
|
|
resource_type=FILES_CONNECTOR_CREDENTIAL_RESOURCE,
|
|
resource_id=row.id,
|
|
operation="deleted",
|
|
principal=principal,
|
|
tenant_id=row.tenant_id,
|
|
payload={
|
|
"scope_type": row.scope_type,
|
|
"scope_id": row.scope_id,
|
|
"provider": row.provider,
|
|
},
|
|
)
|
|
for profile in deletion.affected_profiles:
|
|
_record_connector_settings_change(
|
|
session,
|
|
collection=FILES_CONNECTOR_PROFILES_COLLECTION,
|
|
resource_type=FILES_CONNECTOR_PROFILE_RESOURCE,
|
|
resource_id=profile.id,
|
|
operation="updated",
|
|
principal=principal,
|
|
tenant_id=profile.tenant_id,
|
|
payload={
|
|
"scope_type": profile.scope_type,
|
|
"scope_id": profile.scope_id,
|
|
"provider": profile.provider,
|
|
"reason": "credential_deleted",
|
|
},
|
|
)
|
|
session.commit()
|
|
session.refresh(row)
|
|
return _connector_credential_response(row)
|
|
except FileStorageError as exc:
|
|
session.rollback()
|
|
raise _http_error(exc) from exc
|
|
except Exception:
|
|
session.rollback()
|
|
raise
|
|
|
|
|
|
@router.get("/connectors/profiles", response_model=FileConnectorProfilesResponse)
|
|
def list_connector_profiles(
|
|
provider: str | None = None,
|
|
campaign_id: str | None = None,
|
|
include_disabled: bool = False,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:read",
|
|
"files:file:upload",
|
|
"files:file:download",
|
|
"files:file:admin",
|
|
"system:settings:read",
|
|
"admin:settings:read",
|
|
)
|
|
),
|
|
):
|
|
try:
|
|
if campaign_id:
|
|
_ensure_campaign_file_access(session, principal, campaign_id)
|
|
profiles = _visible_connector_profiles(
|
|
session,
|
|
principal,
|
|
provider=provider,
|
|
campaign_id=campaign_id,
|
|
include_disabled=include_disabled
|
|
and _can_read_disabled_connector_profiles(principal),
|
|
include_admin_scopes=_can_read_disabled_connector_profiles(principal),
|
|
include_effective_policy=False,
|
|
)
|
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
raise _http_error(exc) from exc
|
|
return FileConnectorProfilesResponse(
|
|
profiles=[
|
|
FileConnectorProfileResponse(**profile.to_response())
|
|
for profile in profiles
|
|
]
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/connectors/profiles",
|
|
response_model=FileConnectorProfileResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
)
|
|
def create_connector_profile(
|
|
payload: FileConnectorProfileCreateRequest,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(
|
|
require_any_scope(
|
|
"files:file:admin", "system:settings:write", "admin:settings:write"
|
|
)
|
|
),
|
|
):
|
|
_require_connector_profile_write(principal, payload.scope_type)
|
|
credentials = payload.credentials
|
|
try:
|
|
credential_row = _credential_row_for_profile(
|
|
session,
|
|
principal,
|
|
credential_profile_id=payload.credential_profile_id,
|
|
provider=payload.provider,
|
|
profile_id=payload.id,
|
|
scope_type=payload.scope_type,
|
|
scope_id=payload.scope_id,
|
|
)
|
|
_ensure_connector_configuration_allowed(
|
|
session,
|
|
principal,
|
|
connector_id=payload.id,
|
|
credential_id=payload.credential_profile_id,
|
|
provider=payload.provider,
|
|
endpoint_url=connector_effective_endpoint_url(
|
|
provider=payload.provider,
|
|
endpoint_url=payload.endpoint_url,
|
|
metadata=payload.metadata,
|
|
),
|
|
base_path=payload.base_path,
|
|
scope_type=payload.scope_type,
|
|
scope_id=payload.scope_id,
|
|
operation="configure",
|
|
)
|
|
_ensure_connector_local_policy_allowed(
|
|
session,
|
|
principal,
|
|
scope_type=payload.scope_type,
|
|
scope_id=payload.scope_id,
|
|
policy=payload.policy,
|
|
)
|
|
row = create_connector_profile_row(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
user_id=principal.user.id,
|
|
profile_id=payload.id,
|
|
label=payload.label,
|
|
provider=payload.provider,
|
|
scope_type=payload.scope_type,
|
|
scope_id=payload.scope_id,
|
|
endpoint_url=payload.endpoint_url,
|
|
base_path=payload.base_path,
|
|
enabled=payload.enabled,
|
|
credential_profile_id=payload.credential_profile_id,
|
|
credential_mode=payload.credential_mode,
|
|
username=credentials.username,
|
|
password=credentials.password,
|
|
token=credentials.token,
|
|
password_env=credentials.password_env,
|
|
token_env=credentials.token_env,
|
|
secret_ref=credentials.secret_ref,
|
|
capabilities=payload.capabilities,
|
|
policy=payload.policy,
|
|
metadata=payload.metadata,
|
|
)
|
|
_record_connector_settings_change(
|
|
session,
|
|
collection=FILES_CONNECTOR_PROFILES_COLLECTION,
|
|
resource_type=FILES_CONNECTOR_PROFILE_RESOURCE,
|
|
resource_id=row.id,
|
|
operation="created",
|
|
principal=principal,
|
|
tenant_id=row.tenant_id,
|
|
payload={
|
|
"scope_type": row.scope_type,
|
|
"scope_id": row.scope_id,
|
|
"provider": row.provider,
|
|
},
|
|
)
|
|
session.commit()
|
|
session.refresh(row)
|
|
return FileConnectorProfileResponse(
|
|
**connector_profile_from_row(
|
|
row, credential_row=credential_row
|
|
).to_response()
|
|
)
|
|
except ConnectorPolicyDenied as exc:
|
|
session.rollback()
|
|
raise _connector_policy_error(exc) from exc
|
|
except (FileStorageError, ValueError, json.JSONDecodeError) as exc:
|
|
session.rollback()
|
|
raise _http_error(exc) from exc
|