feat(identity): define the search provider contract

This commit is contained in:
2026-07-20 20:03:15 +02:00
parent 57ec960f40
commit 1a29e75db4
4 changed files with 80 additions and 8 deletions

View File

@@ -126,6 +126,15 @@ Feature modules should prefer these capabilities over direct reads of
access/tenant ORM models when they need labels, group membership, default
access provisioning, counts, audit actor labels, or tenant metadata.
Other stable runtime capabilities currently include:
- `identity.directory` and `identity.search`
- `organizations.directory`
- `idm.directory`
- `calendar.outbox` and `calendar.scheduling`
- `poll.scheduling`
- `notifications.dispatch`
### Named Interface Contracts
Capabilities are runtime objects. Named interface contracts are compatibility
@@ -147,15 +156,25 @@ intended for SemVer major-version lines. Missing optional interfaces are
allowed, but an installed provider with an incompatible version blocks
activation because the integration would otherwise bind to an unsafe API.
Current named interfaces:
Current named interfaces, generated from the source manifests by the workspace
contract checks, are:
- `files.campaign_attachments`
- `addresses.contact_writer`, `addresses.lookup`, `addresses.recipient_source`
- `calendar.outbox`, `calendar.scheduling`
- `campaigns.access`, `campaigns.delivery_tasks`,
`campaigns.mail_policy_context`, `campaigns.policy_context`,
`campaigns.retention`
- `dist_lists.expand`, `dist_lists.source`, `dist_lists.writer`
- `evaluation.feedback`, `evaluation.result_aggregation`, `evaluation.scoring`
- `files.access`, `files.campaign_attachments`
- `mail.campaign_delivery`
- `campaigns.access`
- `campaigns.delivery_tasks`
- `campaigns.mail_policy_context`
- `campaigns.policy_context`
- `campaigns.retention`
- `notifications.dispatch`
- `poll.availability_matrix`, `poll.option_selection`,
`poll.response_collection`, `poll.signed_participation`,
`poll.workflow_context`
- `rest.function_publication`
- `scheduling.candidate_slots`, `scheduling.decision_handoff`
- `soap.operation_publication`
Core validates named interface contracts in three places:

View File

@@ -7,6 +7,7 @@ from typing import Literal, Protocol, runtime_checkable
IDENTITY_MODULE_ID = "identity"
CAPABILITY_IDENTITY_DIRECTORY = "identity.directory"
CAPABILITY_IDENTITY_SEARCH = "identity.search"
IdentityStatus = Literal["active", "inactive", "suspended"]
@@ -44,3 +45,15 @@ class IdentityDirectory(Protocol):
def accounts_for_identity(self, identity_id: str) -> Sequence[IdentityAccountLinkRef]:
...
@runtime_checkable
class IdentitySearchProvider(Protocol):
def search_identities(
self,
query: str | None = None,
*,
include_inactive: bool = False,
limit: int = 25,
) -> Sequence[IdentityRef]:
...

View File

@@ -396,8 +396,11 @@ class IdentityOrganizationContractTests(unittest.TestCase):
from govoplan_organizations.backend.directory import SqlOrganizationDirectory
identity_directory = SqlIdentityDirectory()
idm_directory = SqlIdmDirectory()
organization_directory = SqlOrganizationDirectory()
idm_directory = SqlIdmDirectory(
identities=identity_directory,
organizations=organization_directory,
)
self.assertEqual("Directory Person", identity_directory.get_identity("identity-directory").display_name) # type: ignore[union-attr]
self.assertEqual("identity-directory", identity_directory.identity_for_account("account-directory").id) # type: ignore[union-attr]

View File

@@ -12,11 +12,18 @@ from govoplan_access.backend.db.models import Account, User
from govoplan_core.auth import ApiPrincipal, get_api_principal
from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.configuration_control import configuration_control_snapshot, create_configuration_change_request
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, CAPABILITY_IDENTITY_SEARCH
from govoplan_core.core.modules import ModuleContext, ModuleManifest
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.core.runtime import clear_runtime, configure_runtime
from govoplan_core.db.base import Base
from govoplan_identity.backend.directory import SqlIdentityDirectory
from govoplan_identity.backend.db.models import Identity, IdentityAccountLink
from govoplan_idm.backend.db.models import IdmOrganizationFunctionAssignment, IdmTenantSettings
from govoplan_idm.backend.api.v1.routes import router as idm_router
from govoplan_idm.backend.manifest import get_manifest
from govoplan_organizations.backend.directory import SqlOrganizationDirectory
from govoplan_organizations.backend.db.models import OrganizationFunction, OrganizationUnit
from tests.db_isolation import temporary_database
@@ -50,7 +57,37 @@ class IdmApiTests(unittest.TestCase):
)
def _app(self, scopes: set[str]) -> FastAPI:
identity_directory = SqlIdentityDirectory()
organization_directory = SqlOrganizationDirectory()
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="identity",
name="Identity",
version="test",
capability_factories={
CAPABILITY_IDENTITY_DIRECTORY: lambda context: identity_directory,
CAPABILITY_IDENTITY_SEARCH: lambda context: identity_directory,
},
)
)
registry.register(
ModuleManifest(
id="organizations",
name="Organizations",
version="test",
capability_factories={
CAPABILITY_ORGANIZATION_DIRECTORY: lambda context: organization_directory,
},
)
)
context = ModuleContext(registry=registry, settings=object())
registry.configure_capability_context(context)
configure_runtime(context)
self.addCleanup(clear_runtime)
app = FastAPI()
app.state.govoplan_registry = registry
app.include_router(idm_router, prefix="/api/v1")
app.dependency_overrides[get_api_principal] = lambda: self._principal(scopes)
return app