Harden CardDAV credential and network boundaries
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
@@ -33,6 +34,7 @@ from govoplan_addresses.backend.db.models import (
|
||||
)
|
||||
from govoplan_addresses.backend.schemas import (
|
||||
AddressBookCreateRequest,
|
||||
AddressCardDavDiscoveryRequest,
|
||||
AddressListCreateRequest,
|
||||
AddressListEntryCreateRequest,
|
||||
AddressCardDavSourceCreateRequest,
|
||||
@@ -48,6 +50,7 @@ from govoplan_addresses.backend.schemas import (
|
||||
ContactPostalAddressPayload,
|
||||
)
|
||||
from govoplan_addresses.backend.manifest import manifest
|
||||
from govoplan_addresses.backend.router import _sync_source_response
|
||||
from govoplan_addresses.backend.service import (
|
||||
AddressBookError,
|
||||
address_book_contact_counts,
|
||||
@@ -61,6 +64,7 @@ from govoplan_addresses.backend.service import (
|
||||
delete_address_list_entry,
|
||||
delete_contact,
|
||||
delete_sync_source,
|
||||
discover_carddav_address_books,
|
||||
export_address_book_vcard,
|
||||
import_vcards,
|
||||
list_address_list_entries,
|
||||
@@ -81,6 +85,7 @@ from govoplan_addresses.backend.service import (
|
||||
start_sync_attempt,
|
||||
finish_sync_attempt,
|
||||
update_sync_source,
|
||||
resolve_trusted_deployment_carddav_credential_ref,
|
||||
)
|
||||
|
||||
|
||||
@@ -787,6 +792,118 @@ END:VCARD
|
||||
self.assertEqual(contacts[0].display_name, "Ada Remote")
|
||||
self.assertEqual(contacts[0].emails[0].email, "ada.remote@example.local")
|
||||
|
||||
def test_carddav_credentials_are_server_managed_and_public_metadata_is_redacted(self) -> None:
|
||||
book = create_address_book(
|
||||
self.session,
|
||||
self.principal,
|
||||
AddressBookCreateRequest(scope_type="user", name="Secured remote"),
|
||||
)
|
||||
self.session.commit()
|
||||
self.session.refresh(book)
|
||||
|
||||
with self.assertRaisesRegex(AddressBookError, "Caller-supplied credential references"):
|
||||
create_carddav_sync_source(
|
||||
self.session,
|
||||
self.principal,
|
||||
book.id,
|
||||
AddressCardDavSourceCreateRequest(
|
||||
collection_url="https://attacker.example.test/addressbooks/personal/",
|
||||
auth_type="bearer",
|
||||
credential_ref="env:MASTER_KEY_B64",
|
||||
),
|
||||
)
|
||||
with self.assertRaisesRegex(AddressBookError, "Caller-supplied credential references"):
|
||||
discover_carddav_address_books(
|
||||
self.session,
|
||||
self.principal,
|
||||
AddressCardDavDiscoveryRequest(
|
||||
url="https://attacker.example.test/addressbooks/",
|
||||
auth_type="bearer",
|
||||
credential_ref="env:MASTER_KEY_B64",
|
||||
),
|
||||
)
|
||||
with self.assertRaisesRegex(AddressBookError, "server-managed"):
|
||||
create_sync_source(
|
||||
self.session,
|
||||
self.principal,
|
||||
book.id,
|
||||
AddressSyncSourceCreateRequest(
|
||||
connector_type="carddav",
|
||||
display_name="Injected",
|
||||
metadata={"carddav": {"auth_type": "bearer", "credential_ref": "env:DATABASE_URL"}},
|
||||
),
|
||||
)
|
||||
|
||||
source = create_carddav_sync_source(
|
||||
self.session,
|
||||
self.principal,
|
||||
book.id,
|
||||
AddressCardDavSourceCreateRequest(
|
||||
collection_url="https://dav.example.test/addressbooks/personal/",
|
||||
auth_type="basic",
|
||||
username="ada",
|
||||
password="secret",
|
||||
),
|
||||
)
|
||||
stored_auth = dict((source.metadata_ or {})["carddav"])
|
||||
self.assertIn("secret_encrypted", stored_auth)
|
||||
self.assertNotEqual(stored_auth["secret_encrypted"], "secret")
|
||||
|
||||
response_auth = _sync_source_response(source).metadata["carddav"]
|
||||
self.assertNotIn("secret_encrypted", response_auth)
|
||||
self.assertNotIn("credential_ref", response_auth)
|
||||
self.assertTrue(response_auth["has_credential"])
|
||||
|
||||
with self.assertRaisesRegex(AddressBookError, "server-managed"):
|
||||
update_sync_source(
|
||||
self.session,
|
||||
self.principal,
|
||||
source.id,
|
||||
AddressSyncSourceUpdateRequest(
|
||||
metadata={"carddav": {"auth_type": "bearer", "secret_encrypted": "copied-ciphertext"}}
|
||||
),
|
||||
)
|
||||
|
||||
def test_legacy_carddav_env_reference_is_not_resolved_by_runtime_sync(self) -> None:
|
||||
book = create_address_book(
|
||||
self.session,
|
||||
self.principal,
|
||||
AddressBookCreateRequest(scope_type="user", name="Legacy remote"),
|
||||
)
|
||||
self.session.commit()
|
||||
self.session.refresh(book)
|
||||
source = create_sync_source(
|
||||
self.session,
|
||||
self.principal,
|
||||
book.id,
|
||||
AddressSyncSourceCreateRequest(
|
||||
connector_type="carddav",
|
||||
display_name="Legacy",
|
||||
external_address_book_ref="https://attacker.example.test/addressbooks/personal/",
|
||||
),
|
||||
)
|
||||
source.metadata_ = {
|
||||
"carddav": {
|
||||
"auth_type": "bearer",
|
||||
"credential_ref": "env:MASTER_KEY_B64",
|
||||
}
|
||||
}
|
||||
|
||||
with patch.dict("os.environ", {"MASTER_KEY_B64": "must-not-leave-process"}), self.assertRaisesRegex(
|
||||
AddressBookError,
|
||||
"not a server-owned credential",
|
||||
):
|
||||
run_sync_source(self.session, self.principal, source.id)
|
||||
|
||||
def test_trusted_deployment_carddav_env_resolution_is_explicit(self) -> None:
|
||||
with patch.dict("os.environ", {"CARDDAV_DEPLOYMENT_TOKEN": "trusted-token"}):
|
||||
self.assertEqual(
|
||||
resolve_trusted_deployment_carddav_credential_ref("env:CARDDAV_DEPLOYMENT_TOKEN"),
|
||||
"trusted-token",
|
||||
)
|
||||
with self.assertRaisesRegex(AddressBookError, "must use the env: prefix"):
|
||||
resolve_trusted_deployment_carddav_credential_ref("vault:token")
|
||||
|
||||
def test_carddav_two_way_pushes_local_creates_updates_and_deletes(self) -> None:
|
||||
book = create_address_book(self.session, self.principal, AddressBookCreateRequest(scope_type="user", name="Writable Remote"))
|
||||
self.session.commit()
|
||||
|
||||
Reference in New Issue
Block a user