265 lines
8.1 KiB
Python
265 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from govoplan_core.core.access import (
|
|
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
|
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
|
)
|
|
from govoplan_core.core.module_guards import (
|
|
drop_table_retirement_provider,
|
|
persistent_table_uninstall_guard,
|
|
)
|
|
from govoplan_core.core.modules import (
|
|
DocumentationLink,
|
|
DocumentationTopic,
|
|
FrontendModule,
|
|
FrontendRoute,
|
|
MigrationSpec,
|
|
ModuleContext,
|
|
ModuleInterfaceProvider,
|
|
ModuleManifest,
|
|
PermissionDefinition,
|
|
RoleTemplate,
|
|
)
|
|
from govoplan_core.core.provider_governance import declared_module_architecture
|
|
from govoplan_core.core.search import (
|
|
CAPABILITY_SEARCH_INDEX_WRITER,
|
|
SearchProviderRegistration,
|
|
)
|
|
from govoplan_core.core.views import ViewSurface
|
|
from govoplan_core.db.base import Base
|
|
from govoplan_search.backend.db import models as search_models
|
|
|
|
|
|
MODULE_ID = "search"
|
|
MODULE_NAME = "Search"
|
|
MODULE_VERSION = "0.1.14"
|
|
READ_SCOPE = "search:result:read"
|
|
INDEX_SCOPE = "search:index:write"
|
|
ADMIN_SCOPE = "search:index:admin"
|
|
|
|
|
|
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
|
module_id, resource, action = scope.split(":", 2)
|
|
return PermissionDefinition(
|
|
scope=scope,
|
|
label=label,
|
|
description=description,
|
|
category="Search",
|
|
level="tenant",
|
|
module_id=module_id,
|
|
resource=resource,
|
|
action=action,
|
|
)
|
|
|
|
|
|
PERMISSIONS = (
|
|
_permission(
|
|
READ_SCOPE,
|
|
"Search available content",
|
|
"Search content the current principal is authorized to read.",
|
|
),
|
|
_permission(
|
|
INDEX_SCOPE,
|
|
"Write search index entries",
|
|
"Publish and remove module-owned entries in the search index.",
|
|
),
|
|
_permission(
|
|
ADMIN_SCOPE,
|
|
"Administer search index",
|
|
"Inspect providers and rebuild tenant search indexes.",
|
|
),
|
|
)
|
|
|
|
ROLE_TEMPLATES = (
|
|
RoleTemplate(
|
|
slug="search_user",
|
|
name="Search user",
|
|
description="Search content available to the current account.",
|
|
permissions=(READ_SCOPE,),
|
|
default_authenticated=True,
|
|
),
|
|
RoleTemplate(
|
|
slug="search_manager",
|
|
name="Search manager",
|
|
description="Search content and administer indexing.",
|
|
permissions=(READ_SCOPE, INDEX_SCOPE, ADMIN_SCOPE),
|
|
),
|
|
)
|
|
|
|
|
|
def _service(context: ModuleContext):
|
|
from govoplan_search.backend.service import SearchIndexService
|
|
|
|
return SearchIndexService(context.registry)
|
|
|
|
|
|
def _router(_context: ModuleContext):
|
|
from govoplan_search.backend.router import router
|
|
|
|
return router
|
|
|
|
|
|
manifest = ModuleManifest(
|
|
id=MODULE_ID,
|
|
name=MODULE_NAME,
|
|
version=MODULE_VERSION,
|
|
optional_dependencies=(
|
|
"access",
|
|
"views",
|
|
"connectors",
|
|
"wiki",
|
|
"projects",
|
|
"tickets",
|
|
"cases",
|
|
),
|
|
required_capabilities=(
|
|
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
|
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
|
),
|
|
provides_interfaces=(
|
|
ModuleInterfaceProvider(name="search.provider", version="1.0.0"),
|
|
ModuleInterfaceProvider(name="search.index_writer", version="1.1.0"),
|
|
ModuleInterfaceProvider(name="search.source", version="1.0.0"),
|
|
),
|
|
permissions=PERMISSIONS,
|
|
role_templates=ROLE_TEMPLATES,
|
|
route_factory=_router,
|
|
frontend=FrontendModule(
|
|
module_id=MODULE_ID,
|
|
package_name="@govoplan/search-webui",
|
|
routes=(
|
|
FrontendRoute(
|
|
path="/search",
|
|
component="SearchPage",
|
|
required_any=(READ_SCOPE,),
|
|
order=12,
|
|
),
|
|
),
|
|
view_surfaces=(
|
|
ViewSurface(
|
|
id="search.global",
|
|
module_id=MODULE_ID,
|
|
kind="selector",
|
|
label="Global search",
|
|
description="Search entry in the title bar.",
|
|
order=10,
|
|
),
|
|
ViewSurface(
|
|
id="search.results",
|
|
module_id=MODULE_ID,
|
|
kind="route",
|
|
label="Search results",
|
|
order=20,
|
|
),
|
|
ViewSurface(
|
|
id="search.admin.index",
|
|
module_id=MODULE_ID,
|
|
kind="section",
|
|
label="Search index administration",
|
|
order=30,
|
|
),
|
|
),
|
|
),
|
|
migration_spec=MigrationSpec(
|
|
module_id=MODULE_ID,
|
|
metadata=Base.metadata,
|
|
script_location=str(Path(__file__).with_name("migrations") / "versions"),
|
|
retirement_supported=True,
|
|
retirement_provider=drop_table_retirement_provider(
|
|
search_models.SearchIndexChangeQueue,
|
|
search_models.SearchIndexAclToken,
|
|
search_models.SearchIndexDocument,
|
|
search_models.SearchIndexState,
|
|
label="Search",
|
|
),
|
|
retirement_notes=(
|
|
"Destructive retirement removes the derived search index. "
|
|
"Source module data remains authoritative."
|
|
),
|
|
),
|
|
uninstall_guard_providers=(
|
|
persistent_table_uninstall_guard(
|
|
search_models.SearchIndexChangeQueue,
|
|
search_models.SearchIndexDocument,
|
|
search_models.SearchIndexState,
|
|
label="Search index",
|
|
),
|
|
),
|
|
capability_factories={
|
|
CAPABILITY_SEARCH_INDEX_WRITER: _service,
|
|
},
|
|
search_providers=(
|
|
SearchProviderRegistration(
|
|
id="search.index",
|
|
factory=_service,
|
|
order=10,
|
|
),
|
|
),
|
|
documentation=(
|
|
DocumentationTopic(
|
|
id="search.global-and-contextual",
|
|
title="Global and contextual search",
|
|
summary=(
|
|
"Search authorized native and connected objects from one "
|
|
"permission-aware interface."
|
|
),
|
|
body=(
|
|
"Search works with the built-in database index and can aggregate "
|
|
"optional providers. Source modules announce searchable types, "
|
|
"context scopes, and ACL-aware index entries. External engines "
|
|
"remain optional adapters. F3 or the title-bar field opens the "
|
|
"keyboard-navigable search overlay; filters never broaden the "
|
|
"current principal's source permissions. Provider failures are "
|
|
"shown as partial diagnostics without discarding safe results."
|
|
" Search administrators can inspect native source coverage, "
|
|
"process queued changes, reconcile enabled modules, and run "
|
|
"bounded source rebuilds from Administration. Quarantined "
|
|
"changes remain visible until repaired and reconciled."
|
|
),
|
|
layer="available",
|
|
documentation_types=("admin", "user"),
|
|
audience=("administrator", "user"),
|
|
related_modules=("connectors", "views"),
|
|
links=(
|
|
DocumentationLink(
|
|
label="Search interface pattern audit",
|
|
href="govoplan-search/docs/INTERFACE_PATTERN_MIGRATION.md",
|
|
kind="repository",
|
|
),
|
|
),
|
|
order=12,
|
|
),
|
|
),
|
|
architecture=declared_module_architecture(
|
|
layer="governance_accountability",
|
|
kind="runtime",
|
|
maturity="vertical_slice",
|
|
documentation_ref="README.md",
|
|
test_ref="tests/test_postgres_search.py",
|
|
known_limits=("The built-in PostgreSQL index is implemented; optional OpenSearch target evidence is not.",),
|
|
supported_authority_modes=("external_mirror",),
|
|
owned_concepts=("derived search index", "search ACL projection", "index change queue"),
|
|
non_owned_concepts=("source object", "source authorization", "external search engine"),
|
|
recovery_docs=("README.md",),
|
|
security_docs=("README.md",),
|
|
operations_docs=("README.md",),
|
|
),
|
|
)
|
|
|
|
|
|
def get_manifest() -> ModuleManifest:
|
|
return manifest
|
|
|
|
|
|
__all__ = [
|
|
"ADMIN_SCOPE",
|
|
"INDEX_SCOPE",
|
|
"MODULE_ID",
|
|
"MODULE_VERSION",
|
|
"READ_SCOPE",
|
|
"get_manifest",
|
|
"manifest",
|
|
]
|