321 lines
12 KiB
Python
321 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from collections.abc import Iterable, Mapping
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from govoplan_core.core.policy import normalize_policy_scope_type, policy_source_path
|
|
from govoplan_files.backend.storage.connector_policy import ConnectorPolicySource, connector_policy_sources_from_payload
|
|
|
|
|
|
_ENV_JSON_KEYS = ("GOVOPLAN_FILES_CONNECTOR_PROFILES_JSON", "FILES_CONNECTOR_PROFILES_JSON")
|
|
_ENV_FILE_KEYS = ("GOVOPLAN_FILES_CONNECTOR_PROFILES_FILE", "FILES_CONNECTOR_PROFILES_FILE")
|
|
_SUPPORTED_PROVIDERS = {"seafile", "nextcloud", "webdav", "smb", "s3", "sharepoint", "onedrive", "nfs", "dms", "generic"}
|
|
_INLINE_SECRET_FIELDS = (
|
|
"password",
|
|
"token",
|
|
"api_key",
|
|
"access_key",
|
|
"access_key_id",
|
|
"secret_key",
|
|
"secret_access_key",
|
|
"session_token",
|
|
)
|
|
|
|
|
|
def supported_connector_providers() -> set[str]:
|
|
return set(_SUPPORTED_PROVIDERS)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ConnectorProfile:
|
|
id: str
|
|
label: str
|
|
provider: str
|
|
scope_type: str = "system"
|
|
scope_id: str | None = None
|
|
endpoint_url: str | None = None
|
|
base_path: str | None = None
|
|
enabled: bool = True
|
|
credential_profile_id: str | None = None
|
|
credential_profile_label: str | None = None
|
|
credential_mode: str = "none"
|
|
username: str | None = None
|
|
password_env: str | None = None
|
|
token_env: str | None = None
|
|
secret_ref: str | None = None
|
|
password_value: str | None = field(default=None, repr=False)
|
|
token_value: str | None = field(default=None, repr=False)
|
|
has_inline_secret: bool = False
|
|
capabilities: tuple[str, ...] = ()
|
|
policy_sources: tuple[ConnectorPolicySource, ...] = ()
|
|
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
source_kind: str = "settings"
|
|
|
|
@property
|
|
def source_path(self) -> str:
|
|
return policy_source_path(self.scope_type, self.scope_id)
|
|
|
|
@property
|
|
def credential_source(self) -> str | None:
|
|
if self.credential_profile_id:
|
|
return "credential_profile"
|
|
if self.secret_ref:
|
|
return "secret_ref"
|
|
if self.token_value or self.password_value:
|
|
return "stored"
|
|
if self.token_env:
|
|
return "token_env"
|
|
if self.password_env:
|
|
return "password_env"
|
|
if self.has_inline_secret:
|
|
return "inline"
|
|
return None
|
|
|
|
@property
|
|
def credentials_configured(self) -> bool:
|
|
mode = self.credential_mode.casefold()
|
|
if mode in {"", "none", "anonymous"}:
|
|
return True
|
|
if self.secret_ref or self.has_inline_secret or self.password_value or self.token_value:
|
|
return True
|
|
if self.token_env:
|
|
return bool(os.environ.get(self.token_env))
|
|
if self.password_env:
|
|
return bool(os.environ.get(self.password_env))
|
|
return False
|
|
|
|
def to_response(self) -> dict[str, Any]:
|
|
return {
|
|
"id": self.id,
|
|
"label": self.label,
|
|
"provider": self.provider,
|
|
"endpoint_url": self.endpoint_url,
|
|
"base_path": self.base_path,
|
|
"enabled": self.enabled,
|
|
"scope_type": self.scope_type,
|
|
"scope_id": self.scope_id,
|
|
"source_path": self.source_path,
|
|
"credential_profile_id": self.credential_profile_id,
|
|
"credential_profile_label": self.credential_profile_label,
|
|
"credential_mode": self.credential_mode,
|
|
"credential_source": self.credential_source,
|
|
"credentials_configured": self.credentials_configured,
|
|
"username": self.username,
|
|
"capabilities": list(self.capabilities),
|
|
"policy_sources": [_policy_source_response(source) for source in self.policy_sources],
|
|
"metadata": dict(self.metadata),
|
|
"source_kind": self.source_kind,
|
|
}
|
|
|
|
|
|
def connector_profiles_from_settings(settings: object | None = None) -> list[ConnectorProfile]:
|
|
raw = _raw_profiles_payload(settings)
|
|
if raw in (None, ""):
|
|
return []
|
|
payload = json.loads(raw) if isinstance(raw, str) else raw
|
|
return connector_profiles_from_payload(payload)
|
|
|
|
|
|
def connector_profiles_from_payload(value: object) -> list[ConnectorProfile]:
|
|
if isinstance(value, Mapping):
|
|
raw_profiles = value.get("profiles", [])
|
|
elif isinstance(value, list):
|
|
raw_profiles = value
|
|
else:
|
|
raise ValueError("Connector profiles must be a JSON object with profiles or a list")
|
|
if not isinstance(raw_profiles, list):
|
|
raise ValueError("Connector profiles payload requires a profiles list")
|
|
return [_profile_from_mapping(item) for item in raw_profiles if isinstance(item, Mapping)]
|
|
|
|
|
|
def _profile_from_mapping(value: Mapping[str, Any]) -> ConnectorProfile:
|
|
profile_id = _profile_id_from_mapping(value)
|
|
provider = _provider_from_mapping(value)
|
|
scope_type, scope_id = _scope_from_mapping(value)
|
|
credentials = _credentials_from_mapping(value)
|
|
mode = _clean(value.get("credential_mode") or value.get("auth_type") or credentials.get("mode") or credentials.get("type")) or "none"
|
|
profile = _profile_from_normalized_mapping(
|
|
value,
|
|
profile_id=profile_id,
|
|
provider=provider,
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
credential_mode=mode,
|
|
credentials=credentials,
|
|
)
|
|
return ConnectorProfile(
|
|
id=profile.id,
|
|
label=profile.label,
|
|
provider=profile.provider,
|
|
scope_type=profile.scope_type,
|
|
scope_id=profile.scope_id,
|
|
endpoint_url=profile.endpoint_url,
|
|
base_path=profile.base_path,
|
|
enabled=profile.enabled,
|
|
credential_profile_id=profile.credential_profile_id,
|
|
credential_profile_label=profile.credential_profile_label,
|
|
credential_mode=profile.credential_mode,
|
|
username=profile.username,
|
|
password_env=profile.password_env,
|
|
token_env=profile.token_env,
|
|
secret_ref=profile.secret_ref,
|
|
password_value=profile.password_value,
|
|
token_value=profile.token_value,
|
|
has_inline_secret=profile.has_inline_secret,
|
|
capabilities=profile.capabilities,
|
|
policy_sources=tuple(_policy_sources_from_profile(value, profile)),
|
|
metadata=profile.metadata,
|
|
source_kind="settings",
|
|
)
|
|
|
|
|
|
def _profile_id_from_mapping(value: Mapping[str, Any]) -> str:
|
|
profile_id = _clean(value.get("id") or value.get("connector_id") or value.get("name"))
|
|
if not profile_id:
|
|
raise ValueError("Connector profiles require id")
|
|
return profile_id
|
|
|
|
|
|
def _provider_from_mapping(value: Mapping[str, Any]) -> str:
|
|
provider = (_clean(value.get("provider") or value.get("type")) or "generic").casefold()
|
|
if provider not in _SUPPORTED_PROVIDERS:
|
|
raise ValueError(f"Unsupported connector provider: {provider}")
|
|
return provider
|
|
|
|
|
|
def _scope_from_mapping(value: Mapping[str, Any]) -> tuple[str, str | None]:
|
|
scope_type = normalize_policy_scope_type(str(value.get("scope_type") or "system"))
|
|
scope_id = _clean(value.get("scope_id"))
|
|
if scope_type == "system":
|
|
return scope_type, None
|
|
if not scope_id:
|
|
raise ValueError(f"{scope_type} connector profiles require scope_id")
|
|
return scope_type, scope_id
|
|
|
|
|
|
def _credentials_from_mapping(value: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
credentials = value.get("credentials")
|
|
return credentials if isinstance(credentials, Mapping) else {}
|
|
|
|
|
|
def _profile_from_normalized_mapping(
|
|
value: Mapping[str, Any],
|
|
*,
|
|
profile_id: str,
|
|
provider: str,
|
|
scope_type: str,
|
|
scope_id: str | None,
|
|
credential_mode: str,
|
|
credentials: Mapping[str, Any],
|
|
) -> ConnectorProfile:
|
|
return ConnectorProfile(
|
|
id=profile_id,
|
|
label=_clean(value.get("label") or value.get("name")) or profile_id,
|
|
provider=provider,
|
|
endpoint_url=_clean(value.get("endpoint_url") or value.get("base_url") or value.get("url")),
|
|
base_path=_clean(value.get("base_path") or value.get("path") or value.get("root")),
|
|
enabled=_bool(value.get("enabled"), default=True),
|
|
scope_type=scope_type,
|
|
scope_id=scope_id,
|
|
credential_profile_id=_clean(value.get("credential_profile_id") or value.get("credential_id")),
|
|
credential_profile_label=_clean(value.get("credential_profile_label") or value.get("credential_label")),
|
|
credential_mode=credential_mode,
|
|
username=_clean(value.get("username") or credentials.get("username")),
|
|
password_env=_clean(value.get("password_env") or credentials.get("password_env")),
|
|
token_env=_clean(value.get("token_env") or credentials.get("token_env")),
|
|
secret_ref=_clean(value.get("secret_ref") or credentials.get("secret_ref")),
|
|
password_value=_clean(value.get("password") or credentials.get("password")),
|
|
token_value=_clean(value.get("token") or credentials.get("token")),
|
|
has_inline_secret=any(_clean(value.get(field) or credentials.get(field)) for field in _INLINE_SECRET_FIELDS),
|
|
capabilities=tuple(_string_list(value.get("capabilities"))),
|
|
metadata=_public_metadata(value.get("metadata")),
|
|
)
|
|
|
|
|
|
def _raw_profiles_payload(settings: object | None) -> object:
|
|
for key in _ENV_JSON_KEYS:
|
|
value = os.environ.get(key)
|
|
if value:
|
|
return value
|
|
for key in _ENV_FILE_KEYS:
|
|
path = os.environ.get(key)
|
|
if path:
|
|
with open(path, encoding="utf-8") as handle:
|
|
return handle.read()
|
|
if settings is not None:
|
|
value = getattr(settings, "files_connector_profiles_json", None)
|
|
if value:
|
|
return value
|
|
value = getattr(settings, "files_connector_profiles", None)
|
|
if value:
|
|
return value
|
|
return None
|
|
|
|
|
|
def _policy_sources_from_profile(value: Mapping[str, Any], profile: ConnectorProfile) -> list[ConnectorPolicySource]:
|
|
raw_sources: list[Mapping[str, Any]] = []
|
|
policy = value.get("policy")
|
|
if isinstance(policy, Mapping):
|
|
raw_sources.append({
|
|
"scope_type": profile.scope_type,
|
|
"scope_id": profile.scope_id,
|
|
"label": profile.label,
|
|
"policy": policy,
|
|
})
|
|
configured_sources = value.get("policy_sources") or value.get("connector_policy_sources")
|
|
if configured_sources is not None:
|
|
raw = connector_policy_sources_from_payload(configured_sources)
|
|
raw_sources.extend(_policy_source_response(source) for source in raw)
|
|
return connector_policy_sources_from_payload(raw_sources)
|
|
|
|
|
|
def _policy_source_response(source: ConnectorPolicySource) -> dict[str, Any]:
|
|
return {
|
|
"scope_type": source.scope_type,
|
|
"scope_id": source.scope_id,
|
|
"label": source.label,
|
|
"policy": dict(source.policy or {}),
|
|
}
|
|
|
|
|
|
def _public_metadata(value: object) -> Mapping[str, Any]:
|
|
if not isinstance(value, Mapping):
|
|
return {}
|
|
return {
|
|
str(key): item
|
|
for key, item in value.items()
|
|
if str(key).strip().casefold() not in _INLINE_SECRET_FIELDS
|
|
}
|
|
|
|
|
|
def _string_list(value: object) -> list[str]:
|
|
if value is None:
|
|
return []
|
|
if isinstance(value, str):
|
|
values: Iterable[object] = value.split(",")
|
|
elif isinstance(value, Iterable) and not isinstance(value, (bytes, bytearray, Mapping)):
|
|
values = value
|
|
else:
|
|
values = (value,)
|
|
return [str(item).strip() for item in values if str(item).strip()]
|
|
|
|
|
|
def _bool(value: object, *, default: bool) -> bool:
|
|
if value is None:
|
|
return default
|
|
if isinstance(value, bool):
|
|
return value
|
|
if isinstance(value, str):
|
|
return value.strip().casefold() not in {"0", "false", "no", "off"}
|
|
return bool(value)
|
|
|
|
|
|
def _clean(value: object) -> str | None:
|
|
if value is None:
|
|
return None
|
|
text = str(value).strip()
|
|
return text or None
|