feat: expose sanctions screening integration
This commit is contained in:
152
src/govoplan_core/core/sanctions.py
Normal file
152
src/govoplan_core/core/sanctions.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
|
SANCTIONS_SNAPSHOT_CONTRACT_VERSION = "1"
|
||||||
|
CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS = (
|
||||||
|
"connectors.sanctionsSnapshotProvider"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SanctionsSnapshotReference:
|
||||||
|
ref: str
|
||||||
|
tenant_id: str
|
||||||
|
provider_id: str
|
||||||
|
publisher: str
|
||||||
|
jurisdiction: str
|
||||||
|
list_type: str
|
||||||
|
source_id: str
|
||||||
|
source_version: str
|
||||||
|
publication_at: datetime | None
|
||||||
|
effective_at: datetime | None
|
||||||
|
acquired_at: datetime
|
||||||
|
content_type: str
|
||||||
|
byte_count: int
|
||||||
|
sha256: str
|
||||||
|
parser_version: str
|
||||||
|
raw_evidence_ref: str
|
||||||
|
connector_run_id: str
|
||||||
|
signature_evidence: Mapping[str, object] = field(
|
||||||
|
default_factory=dict
|
||||||
|
)
|
||||||
|
licence_notes: str | None = None
|
||||||
|
trust_notes: str | None = None
|
||||||
|
transport_evidence: Mapping[str, object] = field(
|
||||||
|
default_factory=dict
|
||||||
|
)
|
||||||
|
contract_version: str = SANCTIONS_SNAPSHOT_CONTRACT_VERSION
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.contract_version != SANCTIONS_SNAPSHOT_CONTRACT_VERSION:
|
||||||
|
raise ValueError(
|
||||||
|
"Unsupported sanctions snapshot contract version."
|
||||||
|
)
|
||||||
|
required = (
|
||||||
|
self.ref,
|
||||||
|
self.tenant_id,
|
||||||
|
self.provider_id,
|
||||||
|
self.publisher,
|
||||||
|
self.jurisdiction,
|
||||||
|
self.list_type,
|
||||||
|
self.source_id,
|
||||||
|
self.source_version,
|
||||||
|
self.content_type,
|
||||||
|
self.sha256,
|
||||||
|
self.parser_version,
|
||||||
|
self.raw_evidence_ref,
|
||||||
|
self.connector_run_id,
|
||||||
|
)
|
||||||
|
if not all(value.strip() for value in required):
|
||||||
|
raise ValueError(
|
||||||
|
"Sanctions snapshot references require complete provenance."
|
||||||
|
)
|
||||||
|
if self.byte_count < 1:
|
||||||
|
raise ValueError(
|
||||||
|
"Sanctions snapshot evidence must not be empty."
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
len(self.sha256) != 64
|
||||||
|
or any(character not in "0123456789abcdef" for character in self.sha256)
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"Sanctions snapshot SHA-256 evidence is invalid."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SanctionsSnapshotPayload:
|
||||||
|
snapshot: SanctionsSnapshotReference
|
||||||
|
content: bytes
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if len(self.content) != self.snapshot.byte_count:
|
||||||
|
raise ValueError(
|
||||||
|
"Sanctions snapshot content length does not match metadata."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class SanctionsSnapshotProvider(Protocol):
|
||||||
|
def list_snapshots(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
limit: int = 100,
|
||||||
|
) -> Sequence[SanctionsSnapshotReference]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_snapshot(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
snapshot_ref: str,
|
||||||
|
) -> SanctionsSnapshotReference | None:
|
||||||
|
...
|
||||||
|
|
||||||
|
def read_snapshot(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
principal: object,
|
||||||
|
*,
|
||||||
|
snapshot_ref: str,
|
||||||
|
) -> SanctionsSnapshotPayload:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def sanctions_snapshot_provider(
|
||||||
|
registry: object | None,
|
||||||
|
) -> SanctionsSnapshotProvider | None:
|
||||||
|
if (
|
||||||
|
registry is None
|
||||||
|
or not hasattr(registry, "has_capability")
|
||||||
|
or not hasattr(registry, "capability")
|
||||||
|
or not registry.has_capability(
|
||||||
|
CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
provider = registry.capability(
|
||||||
|
CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
provider
|
||||||
|
if isinstance(provider, SanctionsSnapshotProvider)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS",
|
||||||
|
"SANCTIONS_SNAPSHOT_CONTRACT_VERSION",
|
||||||
|
"SanctionsSnapshotPayload",
|
||||||
|
"SanctionsSnapshotProvider",
|
||||||
|
"SanctionsSnapshotReference",
|
||||||
|
"sanctions_snapshot_provider",
|
||||||
|
]
|
||||||
@@ -49,8 +49,8 @@ class Settings(BaseSettings):
|
|||||||
default=(
|
default=(
|
||||||
"tenancy,organizations,identity,idm,access,admin,dashboard,policy,"
|
"tenancy,organizations,identity,idm,access,admin,dashboard,policy,"
|
||||||
"audit,campaigns,files,mail,calendar,poll,scheduling,connectors,"
|
"audit,campaigns,files,mail,calendar,poll,scheduling,connectors,"
|
||||||
"datasources,dataflow,workflow,views,search,postbox,notifications,"
|
"datasources,dataflow,workflow,views,search,risk_compliance,"
|
||||||
"docs,ops"
|
"postbox,notifications,docs,ops"
|
||||||
),
|
),
|
||||||
alias="ENABLED_MODULES",
|
alias="ENABLED_MODULES",
|
||||||
)
|
)
|
||||||
|
|||||||
79
tests/test_sanctions_contract.py
Normal file
79
tests/test_sanctions_contract.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import hashlib
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_core.core.sanctions import (
|
||||||
|
CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS,
|
||||||
|
SanctionsSnapshotPayload,
|
||||||
|
SanctionsSnapshotProvider,
|
||||||
|
SanctionsSnapshotReference,
|
||||||
|
sanctions_snapshot_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _Provider:
|
||||||
|
def list_snapshots(self, session, principal, *, limit=100):
|
||||||
|
del session, principal, limit
|
||||||
|
return ()
|
||||||
|
|
||||||
|
def get_snapshot(self, session, principal, *, snapshot_ref):
|
||||||
|
del session, principal, snapshot_ref
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read_snapshot(self, session, principal, *, snapshot_ref):
|
||||||
|
del session, principal, snapshot_ref
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
|
||||||
|
class _Registry:
|
||||||
|
def __init__(self, provider):
|
||||||
|
self.provider = provider
|
||||||
|
|
||||||
|
def has_capability(self, name):
|
||||||
|
return name == CAPABILITY_CONNECTORS_SANCTIONS_SNAPSHOTS
|
||||||
|
|
||||||
|
def capability(self, name):
|
||||||
|
return self.provider if self.has_capability(name) else None
|
||||||
|
|
||||||
|
|
||||||
|
class SanctionsContractTests(unittest.TestCase):
|
||||||
|
def test_snapshot_evidence_is_versioned_and_bounded(self) -> None:
|
||||||
|
content = b"<CONSOLIDATED_LIST/>"
|
||||||
|
snapshot = SanctionsSnapshotReference(
|
||||||
|
ref="sanctions-snapshot:snapshot-1",
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
provider_id="un.security_council",
|
||||||
|
publisher="United Nations Security Council",
|
||||||
|
jurisdiction="UN",
|
||||||
|
list_type="consolidated",
|
||||||
|
source_id="unsc-consolidated",
|
||||||
|
source_version="2026-07-22",
|
||||||
|
publication_at=None,
|
||||||
|
effective_at=None,
|
||||||
|
acquired_at=datetime.now(timezone.utc),
|
||||||
|
content_type="application/xml",
|
||||||
|
byte_count=len(content),
|
||||||
|
sha256=hashlib.sha256(content).hexdigest(),
|
||||||
|
parser_version="unsc-xml-v1",
|
||||||
|
raw_evidence_ref="connector-evidence:snapshot-1",
|
||||||
|
connector_run_id="run-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = SanctionsSnapshotPayload(
|
||||||
|
snapshot=snapshot,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
provider = _Provider()
|
||||||
|
|
||||||
|
self.assertEqual(content, payload.content)
|
||||||
|
self.assertIsInstance(provider, SanctionsSnapshotProvider)
|
||||||
|
self.assertIs(
|
||||||
|
provider,
|
||||||
|
sanctions_snapshot_provider(_Registry(provider)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
21
webui/package-lock.json
generated
21
webui/package-lock.json
generated
@@ -26,6 +26,7 @@
|
|||||||
"@govoplan/organizations-webui": "file:../../govoplan-organizations/webui",
|
"@govoplan/organizations-webui": "file:../../govoplan-organizations/webui",
|
||||||
"@govoplan/policy-webui": "file:../../govoplan-policy/webui",
|
"@govoplan/policy-webui": "file:../../govoplan-policy/webui",
|
||||||
"@govoplan/postbox-webui": "file:../../govoplan-postbox/webui",
|
"@govoplan/postbox-webui": "file:../../govoplan-postbox/webui",
|
||||||
|
"@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui",
|
||||||
"@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui",
|
"@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui",
|
||||||
"@govoplan/search-webui": "file:../../govoplan-search/webui",
|
"@govoplan/search-webui": "file:../../govoplan-search/webui",
|
||||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||||
@@ -378,6 +379,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../../govoplan-risk-compliance/webui": {
|
||||||
|
"name": "@govoplan/risk-compliance-webui",
|
||||||
|
"version": "0.1.8",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.14",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": ">=7.18.2 <8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"../../govoplan-scheduling/webui": {
|
"../../govoplan-scheduling/webui": {
|
||||||
"name": "@govoplan/scheduling-webui",
|
"name": "@govoplan/scheduling-webui",
|
||||||
"version": "0.1.11",
|
"version": "0.1.11",
|
||||||
@@ -1243,6 +1260,10 @@
|
|||||||
"resolved": "../../govoplan-postbox/webui",
|
"resolved": "../../govoplan-postbox/webui",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@govoplan/risk-compliance-webui": {
|
||||||
|
"resolved": "../../govoplan-risk-compliance/webui",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@govoplan/scheduling-webui": {
|
"node_modules/@govoplan/scheduling-webui": {
|
||||||
"resolved": "../../govoplan-scheduling/webui",
|
"resolved": "../../govoplan-scheduling/webui",
|
||||||
"link": true
|
"link": true
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
"@govoplan/ops-webui": "file:../../govoplan-ops/webui",
|
"@govoplan/ops-webui": "file:../../govoplan-ops/webui",
|
||||||
"@govoplan/policy-webui": "file:../../govoplan-policy/webui",
|
"@govoplan/policy-webui": "file:../../govoplan-policy/webui",
|
||||||
"@govoplan/postbox-webui": "file:../../govoplan-postbox/webui",
|
"@govoplan/postbox-webui": "file:../../govoplan-postbox/webui",
|
||||||
|
"@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui",
|
||||||
"@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui",
|
"@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui",
|
||||||
"@govoplan/search-webui": "file:../../govoplan-search/webui",
|
"@govoplan/search-webui": "file:../../govoplan-search/webui",
|
||||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Activity, Bell, BookUser, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, Form, Inbox, LayoutDashboard, LayoutTemplate, Mail, Mails, RadioTower, Shield, Users, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
|
import { Activity, Bell, BookUser, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, Form, Inbox, LayoutDashboard, LayoutTemplate, Mail, Mails, RadioTower, Shield, ShieldCheck, Users, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
|
||||||
import installedWebModules from "virtual:govoplan-installed-modules";
|
import installedWebModules from "virtual:govoplan-installed-modules";
|
||||||
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule } from "../types";
|
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule } from "../types";
|
||||||
import {
|
import {
|
||||||
@@ -68,6 +68,7 @@ const iconByName: Record<string, LucideIcon> = {
|
|||||||
operator: RadioTower,
|
operator: RadioTower,
|
||||||
"radio-tower": RadioTower,
|
"radio-tower": RadioTower,
|
||||||
reports: ClipboardPenLine,
|
reports: ClipboardPenLine,
|
||||||
|
"shield-check": ShieldCheck,
|
||||||
templates: LayoutTemplate,
|
templates: LayoutTemplate,
|
||||||
users: Users,
|
users: Users,
|
||||||
waypoints: Waypoints,
|
waypoints: Waypoints,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const defaultWebModulePackages = [
|
|||||||
"@govoplan/ops-webui",
|
"@govoplan/ops-webui",
|
||||||
"@govoplan/policy-webui",
|
"@govoplan/policy-webui",
|
||||||
"@govoplan/postbox-webui",
|
"@govoplan/postbox-webui",
|
||||||
|
"@govoplan/risk-compliance-webui",
|
||||||
"@govoplan/scheduling-webui",
|
"@govoplan/scheduling-webui",
|
||||||
"@govoplan/views-webui",
|
"@govoplan/views-webui",
|
||||||
"@govoplan/workflow-webui"
|
"@govoplan/workflow-webui"
|
||||||
@@ -129,6 +130,7 @@ export default defineConfig({
|
|||||||
fileURLToPath(new URL('../../govoplan-ops/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-ops/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-policy/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-policy/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-postbox/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-postbox/webui', import.meta.url)),
|
||||||
|
fileURLToPath(new URL('../../govoplan-risk-compliance/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-scheduling/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-scheduling/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url))
|
fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url))
|
||||||
|
|||||||
Reference in New Issue
Block a user