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/tenant ORM models when they need labels, group membership, default
access provisioning, counts, audit actor labels, or tenant metadata. 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 ### Named Interface Contracts
Capabilities are runtime objects. Named interface contracts are compatibility 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 allowed, but an installed provider with an incompatible version blocks
activation because the integration would otherwise bind to an unsafe API. 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` - `mail.campaign_delivery`
- `campaigns.access` - `notifications.dispatch`
- `campaigns.delivery_tasks` - `poll.availability_matrix`, `poll.option_selection`,
- `campaigns.mail_policy_context` `poll.response_collection`, `poll.signed_participation`,
- `campaigns.policy_context` `poll.workflow_context`
- `campaigns.retention` - `rest.function_publication`
- `scheduling.candidate_slots`, `scheduling.decision_handoff`
- `soap.operation_publication`
Core validates named interface contracts in three places: 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" IDENTITY_MODULE_ID = "identity"
CAPABILITY_IDENTITY_DIRECTORY = "identity.directory" CAPABILITY_IDENTITY_DIRECTORY = "identity.directory"
CAPABILITY_IDENTITY_SEARCH = "identity.search"
IdentityStatus = Literal["active", "inactive", "suspended"] IdentityStatus = Literal["active", "inactive", "suspended"]
@@ -44,3 +45,15 @@ class IdentityDirectory(Protocol):
def accounts_for_identity(self, identity_id: str) -> Sequence[IdentityAccountLinkRef]: 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 from govoplan_organizations.backend.directory import SqlOrganizationDirectory
identity_directory = SqlIdentityDirectory() identity_directory = SqlIdentityDirectory()
idm_directory = SqlIdmDirectory()
organization_directory = SqlOrganizationDirectory() 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("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] 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.auth import ApiPrincipal, get_api_principal
from govoplan_core.core.access import PrincipalRef 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.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_core.db.base import Base
from govoplan_identity.backend.directory import SqlIdentityDirectory
from govoplan_identity.backend.db.models import Identity, IdentityAccountLink from govoplan_identity.backend.db.models import Identity, IdentityAccountLink
from govoplan_idm.backend.db.models import IdmOrganizationFunctionAssignment, IdmTenantSettings 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.api.v1.routes import router as idm_router
from govoplan_idm.backend.manifest import get_manifest 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 govoplan_organizations.backend.db.models import OrganizationFunction, OrganizationUnit
from tests.db_isolation import temporary_database from tests.db_isolation import temporary_database
@@ -50,7 +57,37 @@ class IdmApiTests(unittest.TestCase):
) )
def _app(self, scopes: set[str]) -> FastAPI: 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 = FastAPI()
app.state.govoplan_registry = registry
app.include_router(idm_router, prefix="/api/v1") app.include_router(idm_router, prefix="/api/v1")
app.dependency_overrides[get_api_principal] = lambda: self._principal(scopes) app.dependency_overrides[get_api_principal] = lambda: self._principal(scopes)
return app return app