Add governed tabular and LDAP address sources
This commit is contained in:
@@ -15,6 +15,12 @@ from govoplan_core.core.contact_points import (
|
||||
from govoplan_core.core.distribution_lists import DistributionSourceReference
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_addresses.backend.carddav import AddressCardDAVError
|
||||
from govoplan_addresses.backend.ldap import AddressLdapError
|
||||
from govoplan_addresses.backend.ldap_schemas import (
|
||||
AddressLdapConnectionRequest,
|
||||
AddressLdapDiscoveryResponse,
|
||||
AddressLdapSourceCreateRequest,
|
||||
)
|
||||
from govoplan_addresses.backend.db.models import (
|
||||
AddressBook,
|
||||
AddressList,
|
||||
@@ -29,6 +35,27 @@ from govoplan_addresses.backend.db.models import (
|
||||
ContactPointQualityDecision,
|
||||
ContactPostalAddress,
|
||||
)
|
||||
from govoplan_addresses.backend.import_schemas import (
|
||||
AddressImportCommitRequest,
|
||||
AddressImportPreviewRequest,
|
||||
AddressImportProfileCreateRequest,
|
||||
AddressImportProfileListResponse,
|
||||
AddressImportProfileResponse,
|
||||
AddressImportProfileUpdateRequest,
|
||||
AddressImportRollbackRequest,
|
||||
AddressImportRunResponse,
|
||||
)
|
||||
from govoplan_addresses.backend.imports import (
|
||||
apply_address_import,
|
||||
create_import_profile,
|
||||
get_import_run,
|
||||
import_run_payload,
|
||||
list_import_profiles,
|
||||
preview_address_import,
|
||||
retire_import_profile,
|
||||
rollback_address_import,
|
||||
update_import_profile,
|
||||
)
|
||||
from govoplan_addresses.backend.capabilities import (
|
||||
AddressesContactPointResolutionCapability,
|
||||
AddressesContactWriterCapability,
|
||||
@@ -112,6 +139,7 @@ from govoplan_addresses.backend.service import (
|
||||
create_address_list,
|
||||
create_address_list_entry,
|
||||
create_carddav_sync_source,
|
||||
create_ldap_sync_source,
|
||||
create_contact,
|
||||
create_contact_channel_rule,
|
||||
create_contact_quality_decision,
|
||||
@@ -124,6 +152,7 @@ from govoplan_addresses.backend.service import (
|
||||
delete_contact,
|
||||
delete_sync_source,
|
||||
discover_carddav_address_books,
|
||||
discover_ldap_base_dns,
|
||||
export_address_book_vcard,
|
||||
export_contact_vcard,
|
||||
end_contact_channel_rule,
|
||||
@@ -1341,6 +1370,55 @@ def api_discover_carddav_address_books(
|
||||
raise _error(AddressBookError(str(exc))) from exc
|
||||
|
||||
|
||||
@router.post("/ldap/discover", response_model=AddressLdapDiscoveryResponse)
|
||||
def api_discover_ldap_base_dns(
|
||||
payload: AddressLdapConnectionRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:sync:write")
|
||||
try:
|
||||
return AddressLdapDiscoveryResponse(
|
||||
base_dns=list(discover_ldap_base_dns(session, principal, payload))
|
||||
)
|
||||
except (AddressBookError, AddressLdapError) as exc:
|
||||
raise _error(AddressBookError(str(exc))) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/address-books/{book_id}/ldap/sources",
|
||||
response_model=AddressSyncSourceResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_ldap_sync_source(
|
||||
book_id: str,
|
||||
payload: AddressLdapSourceCreateRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:sync:write")
|
||||
try:
|
||||
sync_source = create_ldap_sync_source(session, principal, book_id, payload)
|
||||
_audit_address_sync(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.sync_source_created",
|
||||
object_type="address_sync_source",
|
||||
object_id=sync_source.id,
|
||||
details={
|
||||
"address_book_id": book_id,
|
||||
"connector_type": "ldap",
|
||||
"sync_direction": "read_only",
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(sync_source)
|
||||
return _sync_source_response(sync_source)
|
||||
except (AddressBookError, AddressLdapError) as exc:
|
||||
session.rollback()
|
||||
raise _error(AddressBookError(str(exc))) from exc
|
||||
|
||||
|
||||
@router.get("/credentials", response_model=AddressCredentialEnvelopeListResponse)
|
||||
def api_list_address_credentials(
|
||||
source_id: str | None = Query(default=None),
|
||||
@@ -1381,7 +1459,7 @@ def api_create_carddav_sync_source(
|
||||
session.commit()
|
||||
session.refresh(sync_source)
|
||||
return _sync_source_response(sync_source)
|
||||
except (AddressBookError, AddressCardDAVError) as exc:
|
||||
except (AddressBookError, AddressCardDAVError, AddressLdapError) as exc:
|
||||
session.rollback()
|
||||
raise _error(AddressBookError(str(exc))) from exc
|
||||
|
||||
@@ -1428,7 +1506,7 @@ def api_preview_sync_source(
|
||||
)
|
||||
session.commit()
|
||||
return _sync_plan_response(plan)
|
||||
except (AddressBookError, AddressCardDAVError) as exc:
|
||||
except (AddressBookError, AddressCardDAVError, AddressLdapError) as exc:
|
||||
session.rollback()
|
||||
raise _error(AddressBookError(str(exc))) from exc
|
||||
|
||||
@@ -1460,9 +1538,41 @@ def api_run_sync_source(
|
||||
)
|
||||
session.commit()
|
||||
return _sync_plan_response(plan)
|
||||
except (AddressBookError, AddressCardDAVError) as exc:
|
||||
except (AddressBookError, AddressCardDAVError, AddressLdapError) as exc:
|
||||
message = str(exc)
|
||||
session.rollback()
|
||||
raise _error(AddressBookError(str(exc))) from exc
|
||||
try:
|
||||
finish_sync_attempt(
|
||||
session,
|
||||
principal,
|
||||
sync_source_id,
|
||||
AddressSyncAttemptFinishRequest(
|
||||
status="failed",
|
||||
error=message,
|
||||
diagnostic={
|
||||
"severity": "error",
|
||||
"code": "connector_unavailable",
|
||||
"message": message,
|
||||
"retryable": True,
|
||||
"stage": "read",
|
||||
},
|
||||
),
|
||||
)
|
||||
record_sync_diagnostic(
|
||||
session,
|
||||
principal,
|
||||
sync_source_id,
|
||||
AddressSyncDiagnosticCreateRequest(
|
||||
severity="error",
|
||||
code="connector_unavailable",
|
||||
message=message,
|
||||
details={"retryable": True, "stage": "read"},
|
||||
),
|
||||
)
|
||||
session.commit()
|
||||
except Exception:
|
||||
session.rollback()
|
||||
raise _error(AddressBookError(message)) from exc
|
||||
|
||||
|
||||
@router.post("/address-books/{book_id}/sync-sources", response_model=AddressSyncSourceResponse, status_code=status.HTTP_201_CREATED)
|
||||
@@ -1981,6 +2091,196 @@ def api_import_address_book_vcards(
|
||||
raise _error(AddressBookError(str(exc))) from exc
|
||||
|
||||
|
||||
@router.get("/import-profiles", response_model=AddressImportProfileListResponse)
|
||||
def api_list_address_import_profiles(
|
||||
include_history: bool = Query(default=False),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
return AddressImportProfileListResponse(
|
||||
profiles=[
|
||||
AddressImportProfileResponse.model_validate(item)
|
||||
for item in list_import_profiles(session, principal, include_history=include_history)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.post("/import-profiles", response_model=AddressImportProfileResponse, status_code=status.HTTP_201_CREATED)
|
||||
def api_create_address_import_profile(
|
||||
payload: AddressImportProfileCreateRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:write")
|
||||
try:
|
||||
profile = create_import_profile(session, principal, payload)
|
||||
session.flush()
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.import_profile_created",
|
||||
object_type="address_import_profile",
|
||||
object_id=profile.profile_key,
|
||||
details={"version": profile.version, "source_format": profile.source_format, "scope_type": profile.scope_type},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(profile)
|
||||
return AddressImportProfileResponse.model_validate(profile)
|
||||
except AddressBookError as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.patch("/import-profiles/{profile_id}", response_model=AddressImportProfileResponse)
|
||||
def api_update_address_import_profile(
|
||||
profile_id: str,
|
||||
payload: AddressImportProfileUpdateRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:write")
|
||||
try:
|
||||
profile = update_import_profile(session, principal, profile_id, payload)
|
||||
session.flush()
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.import_profile_versioned",
|
||||
object_type="address_import_profile",
|
||||
object_id=profile.profile_key,
|
||||
details={"version": profile.version, "source_format": profile.source_format},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(profile)
|
||||
return AddressImportProfileResponse.model_validate(profile)
|
||||
except AddressBookError as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.delete("/import-profiles/{profile_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def api_retire_address_import_profile(
|
||||
profile_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:write")
|
||||
try:
|
||||
retire_import_profile(session, principal, profile_id)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.import_profile_retired",
|
||||
object_type="address_import_profile",
|
||||
object_id=profile_id,
|
||||
)
|
||||
session.commit()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
except AddressBookError as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/address-books/{book_id}/imports/preview",
|
||||
response_model=AddressImportRunResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_preview_address_import(
|
||||
book_id: str,
|
||||
payload: AddressImportPreviewRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:write")
|
||||
try:
|
||||
run = preview_address_import(session, principal, book_id, payload)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.import_previewed",
|
||||
object_type="address_import_run",
|
||||
object_id=run.id,
|
||||
details={"input_hash": run.input_hash, "plan_hash": run.plan_hash, "statistics": run.statistics},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(run)
|
||||
return AddressImportRunResponse.model_validate(import_run_payload(run))
|
||||
except AddressBookError as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/imports/{run_id}", response_model=AddressImportRunResponse)
|
||||
def api_get_address_import_run(
|
||||
run_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:read")
|
||||
try:
|
||||
return AddressImportRunResponse.model_validate(
|
||||
import_run_payload(get_import_run(session, principal, run_id))
|
||||
)
|
||||
except AddressBookError as exc:
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/imports/{run_id}/apply", response_model=AddressImportRunResponse)
|
||||
def api_apply_address_import(
|
||||
run_id: str,
|
||||
payload: AddressImportCommitRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:write")
|
||||
try:
|
||||
run = apply_address_import(session, principal, run_id, expected_plan_hash=payload.expected_plan_hash)
|
||||
session.flush()
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.import_applied",
|
||||
object_type="address_import_run",
|
||||
object_id=run.id,
|
||||
details={"input_hash": run.input_hash, "plan_hash": run.plan_hash, "statistics": run.statistics},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(run)
|
||||
return AddressImportRunResponse.model_validate(import_run_payload(run))
|
||||
except AddressBookError as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/imports/{run_id}/rollback", response_model=AddressImportRunResponse)
|
||||
def api_rollback_address_import(
|
||||
run_id: str,
|
||||
payload: AddressImportRollbackRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "addresses:contact:write")
|
||||
try:
|
||||
run = rollback_address_import(session, principal, run_id, payload)
|
||||
session.flush()
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="addresses.import_rolled_back",
|
||||
object_type="address_import_run",
|
||||
object_id=run.id,
|
||||
details={"reason": payload.reason, "plan_hash": run.plan_hash},
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(run)
|
||||
return AddressImportRunResponse.model_validate(import_run_payload(run))
|
||||
except AddressBookError as exc:
|
||||
session.rollback()
|
||||
raise _error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/address-books/{book_id}/vcards/export")
|
||||
def api_export_address_book_vcards(
|
||||
book_id: str,
|
||||
|
||||
Reference in New Issue
Block a user