chore: sync GovOPlaN module split state
This commit is contained in:
272
src/govoplan_files/backend/storage/connector_profiles.py
Normal file
272
src/govoplan_files/backend/storage/connector_profiles.py
Normal file
@@ -0,0 +1,272 @@
|
||||
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", "nfs", "dms", "generic"}
|
||||
_INLINE_SECRET_FIELDS = ("password", "token", "api_key", "access_key", "secret_key")
|
||||
|
||||
|
||||
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 = _clean(value.get("id") or value.get("connector_id") or value.get("name"))
|
||||
if not profile_id:
|
||||
raise ValueError("Connector profiles require id")
|
||||
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}")
|
||||
scope_type = normalize_policy_scope_type(str(value.get("scope_type") or "system"))
|
||||
scope_id = _clean(value.get("scope_id"))
|
||||
if scope_type == "system":
|
||||
scope_id = None
|
||||
elif not scope_id:
|
||||
raise ValueError(f"{scope_type} connector profiles require scope_id")
|
||||
|
||||
credentials = value.get("credentials")
|
||||
credentials = credentials if isinstance(credentials, Mapping) else {}
|
||||
mode = _clean(value.get("credential_mode") or value.get("auth_type") or credentials.get("mode") or credentials.get("type")) or "none"
|
||||
profile = 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=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")),
|
||||
)
|
||||
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 _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
|
||||
Reference in New Issue
Block a user