Compare commits
5 Commits
ae74189588
...
77f8d15d17
| Author | SHA1 | Date | |
|---|---|---|---|
| 77f8d15d17 | |||
| fda99d40eb | |||
| 5ab1af803b | |||
| 0845e99cf6 | |||
| 28a0a596a6 |
@@ -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
|
||||||
|
|||||||
@@ -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.
|
Organizations–Identity–IDM–Access 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.
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,17 +136,35 @@ 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 patch(
|
with self.subTest(allow_private=allow_private):
|
||||||
"govoplan_core.security.outbound_http.socket.getaddrinfo",
|
policy = outbound_http_policy(
|
||||||
return_value=[(2, 1, 6, "", ("93.184.216.34", 443))],
|
{
|
||||||
), self.assertRaisesRegex(OutboundHttpBlocked, "cannot pin"):
|
"APP_ENV": "production",
|
||||||
validate_unpinned_sdk_http_url(
|
"GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": str(allow_private).lower(),
|
||||||
"https://objects.example.test",
|
}
|
||||||
label="S3 endpoint",
|
)
|
||||||
policy=policy,
|
with patch(
|
||||||
)
|
"govoplan_core.security.outbound_http.socket.getaddrinfo",
|
||||||
|
return_value=[(2, 1, 6, "", (address, 443))],
|
||||||
|
), self.assertRaisesRegex(OutboundHttpBlocked, "until that transport supports.*DNS/IP pinning"):
|
||||||
|
validate_unpinned_sdk_http_url(
|
||||||
|
"https://objects.example.test",
|
||||||
|
label="S3 endpoint",
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user