5 Commits

5 changed files with 190 additions and 186 deletions

View File

@@ -213,7 +213,7 @@ enforcement until Redis recovers.
| Setting | Default | Notes | | Setting | Default | Notes |
| --- | --- | --- | | --- | --- | --- |
| `GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS` | `true` in dev/test, otherwise `false` | Deployment-wide decision. Set `true` only when connectors, mail, SMB, or object storage must reach internal addresses. | | `GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS` | `true` in dev/test, otherwise `false` | Deployment-wide decision. Set `true` only when pinned HTTP(S), DAV, SMTP, or IMAP transports must reach internal addresses. It does not enable an SDK transport that cannot pin every peer. |
| `GOVOPLAN_CONNECTOR_MAX_STRUCTURED_RESPONSE_BYTES` | `16777216` (16 MiB) | Maximum buffered JSON, XML, iCalendar, vCard, catalog, and connector error response. | | `GOVOPLAN_CONNECTOR_MAX_STRUCTURED_RESPONSE_BYTES` | `16777216` (16 MiB) | Maximum buffered JSON, XML, iCalendar, vCard, catalog, and connector error response. |
| `GOVOPLAN_CONNECTOR_MAX_FILE_TRANSFER_BYTES` | `536870912` (512 MiB) | Hard upper bound for a single remote file; module upload limits may be lower. | | `GOVOPLAN_CONNECTOR_MAX_FILE_TRANSFER_BYTES` | `536870912` (512 MiB) | Hard upper bound for a single remote file; module upload limits may be lower. |
| `GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST` | empty | Comma-separated exact environment names usable by deployment-owned connector profiles. Tenant/API-managed profiles cannot select process variables, even when a name is listed. | | `GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST` | empty | Comma-separated exact environment names usable by deployment-owned connector profiles. Tenant/API-managed profiles cannot select process variables, even when a name is listed. |
@@ -226,12 +226,12 @@ origin.
The urllib, HTTPX/httpcore, SMTP, and IMAP transports resolve, validate, and The urllib, HTTPX/httpcore, SMTP, and IMAP transports resolve, validate, and
connect to the same approved address record while retaining the original host connect to the same approved address record while retaining the original host
for HTTP Host, TLS SNI, and certificate verification. SMB uses an approved for HTTP Host, TLS SNI, and certificate verification. Live SMB and S3 access
numeric connection target in public-only mode. The S3 SDK cannot provide the fails closed in both public-only and private-network deployments: the current
same peer and redirect guarantee, so S3 connector endpoints fail closed while SDK transports cannot pin every initial and secondary peer or revalidate every
application-enforced public-only egress is required. Production deployments SDK-managed redirect/referral. An explicit IP endpoint does not bypass this
should still enforce the same decision at their worker/container egress firewall rule. Production deployments should still enforce the same decision at their
or outbound proxy as a second boundary. worker/container egress firewall or outbound proxy as a second boundary.
File connector TLS verification may be disabled only in dev/test. A custom CA File connector TLS verification may be disabled only in dev/test. A custom CA
bundle must be an existing regular file whose resolved absolute path is listed bundle must be an existing regular file whose resolved absolute path is listed

View File

@@ -13,9 +13,11 @@ The meta repository's
[Connected Governance Platform Roadmap](https://git.add-ideas.de/add-ideas/govoplan/src/branch/main/docs/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md) [Connected Governance Platform Roadmap](https://git.add-ideas.de/add-ideas/govoplan/src/branch/main/docs/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
describes the corresponding cross-product stakeholder visions, configurable describes the corresponding cross-product stakeholder visions, configurable
service and operating configurations, connected outcome stories, and service and operating configurations, connected outcome stories, and
capability horizons. Its product horizons are canonical; the waves below give capability horizons. The selected five-stage delivery sequence and its gates
their technical dependency order. This Core roadmap remains the technical are in the meta repository's
sequencing and module-routing companion. [Reference Journey Program](https://git.add-ideas.de/add-ideas/govoplan/src/branch/main/docs/REFERENCE_JOURNEY_PROGRAM.md).
Those product documents are canonical; this Core roadmap remains their
technical sequencing and module-routing companion.
## Product Thesis ## Product Thesis
@@ -121,7 +123,8 @@ pattern exists.
## Focus Rules ## Focus Rules
1. Build one reference journey per wave. 1. Build one selected reference journey stage at a time; a later capability
cluster is not an active program merely because it appears below.
2. Do not implement a module because the repository exists. 2. Do not implement a module because the repository exists.
3. Do not add module-to-module imports for optional behavior. 3. Do not add module-to-module imports for optional behavior.
4. Every new domain module must justify its own semantics beyond `cases`, 4. Every new domain module must justify its own semantics beyond `cases`,
@@ -146,11 +149,15 @@ pattern exists.
| Internal work queues and tasks | `govoplan-tasks` | | Internal work queues and tasks | `govoplan-tasks` |
| Appointment proposals and booking | `govoplan-appointments`, `govoplan-calendar` | | Appointment proposals and booking | `govoplan-appointments`, `govoplan-calendar` |
| Postbox, email, and notifications | `govoplan-postbox`, `govoplan-mail`, `govoplan-notifications` | | Postbox, email, and notifications | `govoplan-postbox`, `govoplan-mail`, `govoplan-notifications` |
| Canonical subjects and account links | `govoplan-identity` |
| Organizational structures, units, and functions | `govoplan-organizations` |
| Identity-to-function assignments and directory synchronization | `govoplan-idm` |
| Identity trust, device keys, and encrypted postbox key contracts | `govoplan-identity-trust`, `govoplan-access`, `govoplan-postbox` | | Identity trust, device keys, and encrypted postbox key contracts | `govoplan-identity-trust`, `govoplan-access`, `govoplan-postbox` |
| Service directory/catalog | `govoplan-portal` | | Service directory/catalog | `govoplan-portal` |
| Permit/document generation | `govoplan-templates`, `govoplan-dms` | | Permit/document generation | `govoplan-templates`, `govoplan-dms` |
| Payment capture and accounting handoff | `govoplan-payments`, `govoplan-ledger` | | Payment capture and accounting handoff | `govoplan-payments`, `govoplan-ledger` |
| Roles, permissions, tenants, policy, audit | `govoplan-access`, `govoplan-tenancy`, `govoplan-policy`, `govoplan-audit` | | Authentication projection, roles, permissions, acting context | `govoplan-access` |
| Tenants, policy, and audit | `govoplan-tenancy`, `govoplan-policy`, `govoplan-audit` |
| External software integration | `govoplan-connectors` | | External software integration | `govoplan-connectors` |
| Recurring extraction and transformation | possible future `govoplan-datasources`, possible future `govoplan-dataflow` | | Recurring extraction and transformation | possible future `govoplan-datasources`, possible future `govoplan-dataflow` |
| Reports, BI, and management visibility | `govoplan-reporting` | | Reports, BI, and management visibility | `govoplan-reporting` |
@@ -197,56 +204,82 @@ an editor applies a high-impact configuration change.
## Reference Journeys ## Reference Journeys
The roadmap should be driven by three journeys. The active sequence is selected. Workflow remains deliberately deferred and is
not a dependency of these journeys.
### Journey 1: Permit To Payment ### Journey 1: Campaign Demonstration Composition
This is the primary public-administration journey. Campaign is the first complete proof of modular composition. Campaign owns
intent, recipient snapshots, personalization, execution state, and delivery
evidence. Mail owns reusable profiles, credentials, protocol policy, and
provider execution; Campaign stores only a selected profile reference. Files
owns storage, connector profiles, file policy, and provenance.
1. A person applies for a permit through the public portal. The technical gate is a pinned Campaign/Mail/Files composition with central
2. The applicant uploads required files and submits structured form data. UI, adaptive user/admin/operator/integration documentation, target SMTP/IMAP
3. Submission creates a case, a workflow instance, and an internal task. and file-provider evidence, and explicit test/send/resend/retry/reconciliation
4. Completing the task creates a postbox message, a notification, and an email semantics. Readers must not receive backend paths, worker claims, secrets, or
notification with an appointment proposal. raw provider diagnostics.
5. The applicant accepts an appointment, which updates the calendar and the
workflow state.
6. During the appointment, the case is opened and the permit is generated from
a governed template.
7. The payment is processed and linked to the case and accounting handoff.
8. The permit, payment evidence, communication history, audit trail, retention
state, and records evidence remain available according to policy.
This journey proves the platform can coordinate modules without core knowing ### Journey 2: Function-Bound Postbox Delivery
module internals.
### Journey 2: Training To Certificate Postbox accepts delivery to an addressable postbox or a function in an
organizational unit. Organizations owns units and functions, Identity owns
subjects, IDM owns identity-to-function assignments and upstream sync, and
Access resolves current roles, delegation, acting context, and permission.
This is the best university-administration and internal-administration journey. Campaign consumes a typed delivery-target capability without importing Postbox
or identity internals. Reassignment changes future access without moving the
message; vacancy or ambiguous acting context fails visibly; delivery, access,
and correction remain auditable.
1. Course or training offer is planned. ### Journey 3: Data-Backed Templates, Reports, And Deep Launch
2. Room, trainer, resource, and capacity are booked.
3. Participants register or are assigned.
4. Attendance is tracked.
5. Certificate or participation confirmation is issued.
6. Evidence remains available through records, files, audit, and docs.
This journey keeps `booking`, `resources`, `learning`, and `certificates` An authenticated user follows an opaque, short-lived launch reference from HIS
focused instead of becoming broad ERP replacements. or another specialist system. GovOPlaN re-authorizes the actor, resolves a
curated data context server-side, displays source/freshness/version, and renders
one reproducible document and report.
### Journey 3: Report To Resolution Templates owns definition/version/schema/rendering, Reporting owns source
selection/parameters/execution/export, Files owns generated bytes, and
connectors own protocol access. URLs do not carry credentials, arbitrary SQL,
or trusted raw personal data. Retries are idempotent and generation evidence
connects source, snapshot/reference, transformation, definition, parameters,
output checksum, actor, and policy.
This is the internal operations and municipal issue-reporting journey. ### Journey 4: Governed University BI Path
1. A person reports an issue. Starting from the Journey 3 source contract, one bounded university dataset is
2. The issue is triaged into helpdesk, facilities, assets, or a case. catalogued, staged by snapshot or watermark, validated, transformed through a
3. Work is assigned, tracked, and escalated. versioned lineage graph, and exposed as a policy-aware analytical data product.
4. Evidence, communication, and status updates are preserved. The result must preserve official-key mappings, organizational and reporting
5. Reports show workload, SLA, recurring problems, and completion. date semantics, quality findings, quarantine/replay, transparent calculation,
and reproducible promotion between development, test, and production.
This journey prevents `helpdesk`, `issue-reporting`, `facilities`, and `assets` Reporting consumes the product. Create `govoplan-datasources` or
from becoming disconnected ticket silos. `govoplan-dataflow` only after the concrete path proves repeated ownership that
does not belong to connectors, Reporting, or the producing domain module.
## Roadmap Waves ### Journey 5: Collaborative Document Lifecycle
An uploaded or generated artifact becomes a DMS document. Files continues to
own bytes; DMS owns identity, versions, renditions, editing sessions, locks,
comments, review, approval, comparison, and recovery; a collaboration connector
owns provider-specific protocol behavior; Records owns later classification,
hold, archive, and disposal.
The gate requires no silent lost updates, short-lived and currently authorized
editing sessions, idempotent authenticated callbacks, visible uncertain saves,
immutable accepted renditions, and a Records-ready handoff with stable content
and provenance.
## Capability Dependency Waves
The waves below remain a dependency and ownership catalogue for the wider
product vision. They are not the active delivery order. The five selected
journeys above and the meta roadmap decide what is implemented now; other
clusters remain dormant until a selected journey consumes them or they are
explicitly reprioritized.
### Wave 0: Platform Spine ### Wave 0: Platform Spine
@@ -256,9 +289,13 @@ Refine:
- `govoplan-core`: module discovery, capabilities, events, migrations, release - `govoplan-core`: module discovery, capabilities, events, migrations, release
catalog, configuration package runtime, WebUI shell. catalog, configuration package runtime, WebUI shell.
- `govoplan-access`: identities, sessions, API keys, users, groups, roles, - `govoplan-identity`: canonical identities and account links.
memberships, function assignments, delegation, RBAC decisions. - `govoplan-organizations`: organizational structures, units, and functions.
- `govoplan-tenancy`: tenant and organizational-unit boundaries. - `govoplan-idm`: identity-to-function assignments, directory synchronization,
preview, conflicts, and reconciliation.
- `govoplan-access`: sessions, API keys, users, groups, roles, memberships,
function-to-role projection, delegation, acting context, and RBAC decisions.
- `govoplan-tenancy`: tenant lifecycle and tenant boundaries.
- `govoplan-identity-trust`: initial trust contracts for device keys, public key - `govoplan-identity-trust`: initial trust contracts for device keys, public key
directory, assurance, and later encrypted postbox key access. directory, assurance, and later encrypted postbox key access.
- `govoplan-policy`: policy sources, policy decisions, retention inputs. - `govoplan-policy`: policy sources, policy decisions, retention inputs.
@@ -540,19 +577,28 @@ Before a module becomes release-included, it needs:
## Technical Dependency Order Summary ## Technical Dependency Order Summary
When the meta roadmap selects one of these product programs, use this order to Use this active order while respecting the ownership and implementation gates
respect shared-spine dependencies. The list is not an active schedule or a in the capability waves:
decision to start every program horizontally.
1. Stabilize the platform spine. 1. Keep the platform/release spine green and extend connector, identity,
2. Deliver permit-to-payment MVP. external-effect, provenance, documentation, focused-view, recovery, and
3. Build booking and resource operations. version contracts only as the current journey requires.
4. Add learning and certificates. 2. Complete and package Campaign with Mail-owned profiles, Files, target
5. Add issue reporting and helpdesk. delivery/recovery, central UI, and adaptive documentation.
6. Add records, DMS, search, and transparency. 3. Implement function-bound Postbox delivery through
7. Add procurement, contracts, grants, and finance handoff. OrganizationsIdentityIDMAccess and consume it from Campaign through a
8. Add committee and consultation workflows. typed capability.
9. Expand integration, dataflow, reporting, and operations. 4. Implement one data-backed Templates/Reporting path and safe HIS-style deep
launch.
5. Extend that concrete source into one governed university analytical data
product before generalizing data-source or dataflow ownership.
6. Implement Files-backed DMS versions and one provider-neutral collaborative
editing lifecycle, then connect Records handoff.
7. Maintain already integrated Calendar/Scheduling/Poll and other foundations;
activate another capability cluster only when the current journey needs it
or the product roadmap explicitly reprioritizes it.
8. Resume Workflow only by explicit product decision and constrain it with
stable actions from one demonstrated package.
## Deliberate Deferrals ## Deliberate Deferrals
@@ -560,7 +606,8 @@ Defer these until a reference journey proves the need:
- full ERP replacement - full ERP replacement
- native project management beyond connector support - native project management beyond connector support
- broad BI/dataflow platform - an unbounded general-purpose dataflow platform; the bounded governed BI
reference journey is selected
- every possible public-sector protocol adapter - every possible public-sector protocol adapter
- rich LMS behavior beyond training administration - rich LMS behavior beyond training administration
- full qualified digital signing/trust services beyond the identity-trust and - full qualified digital signing/trust services beyond the identity-trust and
@@ -657,19 +704,24 @@ Release composition and tag-only repository handling are documented in
## Next Practical Work ## Next Practical Work
The next planning step should create or update Gitea issues for Wave 0 and Wave The active cross-product story is
1 only. Later waves should stay as roadmap context until the permit-to-payment [`add-ideas/govoplan#14`](https://git.add-ideas.de/add-ideas/govoplan/issues/14).
MVP is demonstrable. Module repositories own implementation issues; do not clone their state here.
Recommended immediate issue buckets: Immediate issue buckets:
- platform spine hardening - fail-closed connector destination pinning and private-network deployment
- configuration package preflight and rollback control for every real transport
- forms-runtime MVP - Mail-profile-only Campaign authoring/build/delivery and safe legacy failure
- portal submission MVP - immediate audited secret deletion when a provider/profile is removed
- cases/workflow/tasks integration MVP - Campaign central-component and role-safe UI acceptance
- template-generated decision document - adaptive Campaign, Mail, and Files task/process/admin/operator/integration/
- postbox/notification handoff security/acceptance documentation
- appointment/booking handoff - target SMTP/IMAP, file-provider, queue/reconciliation, install/upgrade, and
- payment evidence handoff restore proof for the pinned Campaign reference composition
- configured documentation for the reference process
Once that gate is demonstrable, activate the existing Postbox model, access,
API, inbox, and Campaign integration issues. Templates/Reporting, governed BI,
and DMS collaboration remain durable selected direction, but should be
decomposed only as the preceding stage stabilizes or a bounded independent
contract can be implemented without pre-deciding target-system choices.

View File

@@ -84,12 +84,28 @@ def validate_unpinned_sdk_http_url(
"""Fail closed when an SDK cannot connect to a prevalidated DNS answer.""" """Fail closed when an SDK cannot connect to a prevalidated DNS answer."""
active_policy = policy or outbound_http_policy() active_policy = policy or outbound_http_policy()
validated = validate_outbound_http_url(value, label=label, policy=active_policy) validate_outbound_http_url(value, label=label, policy=active_policy)
if active_policy.allow_private_networks: _raise_unpinned_transport(label)
return validated
def validate_unpinned_sdk_host(
hostname: str,
*,
port: int,
label: str,
policy: OutboundHttpPolicy | None = None,
) -> None:
"""Validate a host and then reject an SDK that may select another peer."""
active_policy = policy or outbound_http_policy()
validate_outbound_host(hostname, port=port, label=label, policy=active_policy)
_raise_unpinned_transport(label)
def _raise_unpinned_transport(label: str) -> None:
raise OutboundHttpBlocked( raise OutboundHttpBlocked(
f"{label} uses an SDK that cannot pin connection peers or revalidate every SDK-managed redirect; " f"{label} uses an SDK that cannot pin every connection peer or revalidate SDK-managed redirects/referrals; "
"it is disabled while application-enforced public-only egress is required" "it is disabled until that transport supports connection-time DNS/IP pinning"
) )
@@ -156,25 +172,6 @@ def create_outbound_connection(
raise OutboundHttpBlocked(f"{label} hostname did not resolve to a usable address") raise OutboundHttpBlocked(f"{label} hostname did not resolve to a usable address")
def pinned_outbound_hostname(
hostname: str,
*,
port: int,
label: str = "Connector host",
policy: OutboundHttpPolicy | None = None,
) -> str:
"""Return the original allowed host or a public numeric connection target."""
active_policy = policy or outbound_http_policy()
host = str(hostname).strip().rstrip(".")
records = _resolved_address_records(host, port=port, label=label, policy=active_policy)
if active_policy.allow_private_networks:
return host
addresses = tuple(dict.fromkeys(str(item[4][0]).split("%", 1)[0] for item in records if item[4]))
ipv4 = next((address for address in addresses if ":" not in address), None)
return ipv4 or addresses[0]
def build_outbound_http_opener(*handlers: urllib.request.BaseHandler) -> urllib.request.OpenerDirector: def build_outbound_http_opener(*handlers: urllib.request.BaseHandler) -> urllib.request.OpenerDirector:
"""Build a proxy-free urllib opener whose sockets use approved addresses.""" """Build a proxy-free urllib opener whose sockets use approved addresses."""

View File

@@ -2341,74 +2341,13 @@ class ApiSmokeTests(unittest.TestCase):
self.assertEqual(unchanged["file"]["id"], nextcloud_file["id"]) self.assertEqual(unchanged["file"]["id"], nextcloud_file["id"])
self.assertEqual(unchanged["current_version_id"], synced["current_version_id"]) self.assertEqual(unchanged["current_version_id"], synced["current_version_id"])
case = self with patch("govoplan_files.backend.storage.connector_browse._smbclient_module") as smb_sdk:
class _FakeSmbStat:
st_size = 18
st_mtime = 1783425600
st_mtime_ns = 1783425600000000000
class _FakeSmbEntry:
def __init__(self, name: str, is_dir: bool, size: int = 18) -> None:
self.name = name
self._is_dir = is_dir
self._stat = _FakeSmbStat()
self._stat.st_size = size
def is_dir(self) -> bool:
return self._is_dir
def stat(self):
return self._stat
class _FakeSmbScandir:
def __enter__(self):
return iter([
_FakeSmbEntry("Archive", True, 0),
_FakeSmbEntry("smb-notice.txt", False, 18),
])
def __exit__(self, exc_type, exc, traceback):
return False
class _FakeSmbFile:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, traceback):
return False
def read(self, size: int) -> bytes:
case.assertGreater(size, 18)
return b"smb connector data"
class _FakeSmbClient:
def scandir(self, path: str, **kwargs):
case.assertEqual(path, r"\\127.0.0.1\files\root\shared")
case.assertEqual(kwargs["port"], 1445)
case.assertEqual(kwargs["username"], "govoplan")
case.assertEqual(kwargs["password"], "super-secret-smb")
case.assertTrue(kwargs["require_signing"])
return _FakeSmbScandir()
def stat(self, path: str, **kwargs):
case.assertEqual(path, r"\\127.0.0.1\files\root\shared\smb-notice.txt")
case.assertEqual(kwargs["port"], 1445)
return _FakeSmbStat()
def open_file(self, path: str, mode: str, **kwargs):
case.assertEqual(path, r"\\127.0.0.1\files\root\shared\smb-notice.txt")
case.assertEqual(mode, "rb")
case.assertEqual(kwargs["password"], "super-secret-smb")
return _FakeSmbFile()
fake_smb = _FakeSmbClient()
with patch("govoplan_files.backend.storage.connector_browse._smbclient_module", return_value=fake_smb):
smb_browse = self.client.get("/api/v1/files/connectors/profiles/tenant-smb/browse?path=shared", headers=headers) smb_browse = self.client.get("/api/v1/files/connectors/profiles/tenant-smb/browse?path=shared", headers=headers)
self.assertEqual(smb_browse.status_code, 200, smb_browse.text) self.assertEqual(smb_browse.status_code, 400, smb_browse.text)
self.assertEqual([(item["kind"], item["path"]) for item in smb_browse.json()["items"]], [("folder", "shared/Archive"), ("file", "shared/smb-notice.txt")]) self.assertIn("redirects/referrals", smb_browse.json()["detail"])
smb_sdk.assert_not_called()
with patch("govoplan_files.backend.storage.connector_imports._smbclient_module", return_value=fake_smb): with patch("govoplan_files.backend.storage.connector_imports._smbclient_module") as smb_import_sdk:
smb_import = self.client.post( smb_import = self.client.post(
"/api/v1/files/connectors/profiles/tenant-smb/import", "/api/v1/files/connectors/profiles/tenant-smb/import",
headers=headers, headers=headers,
@@ -2420,12 +2359,9 @@ class ApiSmokeTests(unittest.TestCase):
"target_folder": "imports", "target_folder": "imports",
}, },
) )
self.assertEqual(smb_import.status_code, 200, smb_import.text) self.assertEqual(smb_import.status_code, 400, smb_import.text)
smb_file = smb_import.json()["files"][0] self.assertIn("redirects/referrals", smb_import.json()["detail"])
self.assertEqual(smb_file["display_path"], "imports/smb-notice.txt") smb_import_sdk.assert_not_called()
self.assertEqual(smb_file["source_provenance"]["provider"], "smb")
self.assertEqual(smb_file["source_provenance"]["external_id"], "files:shared/smb-notice.txt")
self.assertEqual(smb_file["source_provenance"]["metadata"]["share"], "files")
filtered = self.client.get("/api/v1/files/connectors/profiles?provider=smb", headers=headers) filtered = self.client.get("/api/v1/files/connectors/profiles?provider=smb", headers=headers)
self.assertEqual(filtered.status_code, 200, filtered.text) self.assertEqual(filtered.status_code, 200, filtered.text)

View File

@@ -15,6 +15,7 @@ from govoplan_core.security.outbound_http import (
create_outbound_connection, create_outbound_connection,
outbound_http_policy, outbound_http_policy,
validate_outbound_http_url, validate_outbound_http_url,
validate_unpinned_sdk_host,
validate_unpinned_sdk_http_url, validate_unpinned_sdk_http_url,
) )
@@ -135,18 +136,36 @@ class HttpFetchTests(unittest.TestCase):
self.assertEqual(2, resolver.call_count) self.assertEqual(2, resolver.call_count)
socket_factory.assert_not_called() socket_factory.assert_not_called()
def test_unpinned_sdk_endpoints_fail_closed_for_dns_hosts(self) -> None: def test_unpinned_sdk_endpoints_fail_closed_in_public_and_private_modes(self) -> None:
policy = outbound_http_policy({"APP_ENV": "production"}) for allow_private, address in ((False, "93.184.216.34"), (True, "10.0.0.5")):
with self.subTest(allow_private=allow_private):
policy = outbound_http_policy(
{
"APP_ENV": "production",
"GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": str(allow_private).lower(),
}
)
with patch( with patch(
"govoplan_core.security.outbound_http.socket.getaddrinfo", "govoplan_core.security.outbound_http.socket.getaddrinfo",
return_value=[(2, 1, 6, "", ("93.184.216.34", 443))], return_value=[(2, 1, 6, "", (address, 443))],
), self.assertRaisesRegex(OutboundHttpBlocked, "cannot pin"): ), self.assertRaisesRegex(OutboundHttpBlocked, "until that transport supports.*DNS/IP pinning"):
validate_unpinned_sdk_http_url( validate_unpinned_sdk_http_url(
"https://objects.example.test", "https://objects.example.test",
label="S3 endpoint", label="S3 endpoint",
policy=policy, policy=policy,
) )
def test_unpinned_host_sdk_fails_closed_for_dns_and_explicit_ip_targets(self) -> None:
policy = outbound_http_policy(
{"APP_ENV": "production", "GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": "true"}
)
for host in ("files.internal.example", "10.0.0.5"):
with self.subTest(host=host), patch(
"govoplan_core.security.outbound_http.socket.getaddrinfo",
return_value=[(2, 1, 6, "", ("10.0.0.5", 445))],
), self.assertRaisesRegex(OutboundHttpBlocked, "redirects/referrals.*DNS/IP pinning"):
validate_unpinned_sdk_host(host, port=445, label="SMB endpoint", policy=policy)
def test_core_redirects_strip_credentials_cross_origin_and_reject_https_downgrades(self) -> None: def test_core_redirects_strip_credentials_cross_origin_and_reject_https_downgrades(self) -> None:
import urllib.request import urllib.request