feat(identity): define the search provider contract
This commit is contained in:
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
...
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user