Files
govoplan-reporting/src/govoplan_reporting/backend/definitions.py
T

956 lines
30 KiB
Python

from __future__ import annotations
from collections.abc import Mapping, Sequence
from datetime import datetime
import hashlib
import json
from typing import Any
import uuid
from sqlalchemy import and_, exists, func, or_
from sqlalchemy.orm import Query, Session
from govoplan_core.core.events import (
EventActorRef,
EventObjectRef,
EventTenantRef,
PlatformEvent,
emit_platform_event,
)
from govoplan_core.security.module_permissions import scopes_grant_compatible
from govoplan_reporting.backend.db.models import (
ReportingDefinitionGrant,
ReportingDefinitionIdentity,
ReportingDefinitionRevision,
)
from govoplan_reporting.backend.domain import (
ReportingDefinitionRecord,
definition_from_row,
)
from govoplan_reporting.backend.schemas import validate_definition_payload
READ_SCOPE = "reporting:definition:read"
WRITE_SCOPE = "reporting:definition:write"
ADMIN_SCOPE = "reporting:definition:admin"
DEFINITION_KINDS = frozenset({"dataset", "semantic_model", "report", "quality_plan"})
STATUS_TRANSITIONS = {
"draft": frozenset({"active", "retired"}),
"active": frozenset({"draft", "retired"}),
"retired": frozenset({"draft"}),
}
SUBJECT_KINDS = frozenset(
{
"account",
"identity",
"group",
"role",
"function",
"function_assignment",
"organization_unit",
"service_account",
}
)
class ReportingDefinitionError(ValueError):
pass
def create_definition(
session: Session,
principal: object,
*,
definition_kind: str,
definition_id: str,
definition_key: str,
name: str,
description: str | None,
status: str,
visibility: str,
recorded_at: datetime,
change_reason: str,
payload: Mapping[str, Any],
idempotency_key: str,
) -> ReportingDefinitionRecord:
_require_scope(principal, WRITE_SCOPE)
tenant_id = _principal_tenant(principal)
kind = _definition_kind(definition_kind)
clean_id = _required(definition_id, "Reporting definition identifier", 255)
clean_key = _key(definition_key, "Reporting definition key")
clean_name = _required(name, "Reporting definition name", 500)
clean_description = _optional(description, "Reporting description", 100_000)
clean_status = _status(status)
clean_visibility = _visibility(visibility)
clean_reason = _required(change_reason, "Reporting change reason", 1_000)
_aware(recorded_at, "Reporting recorded_at")
validated_payload = validate_definition_payload(kind, dict(payload))
parent_kind, parent_id, parent_revision = _parent_reference(
kind,
validated_payload,
)
_validate_parent(
session,
tenant_id=tenant_id,
child_status=clean_status,
parent_kind=parent_kind,
parent_id=parent_id,
parent_revision=parent_revision,
)
request = {
"definition_kind": kind,
"definition_id": clean_id,
"definition_key": clean_key,
"name": clean_name,
"description": clean_description,
"status": clean_status,
"visibility": clean_visibility,
"recorded_at": recorded_at,
"change_reason": clean_reason,
"payload": validated_payload,
}
request_sha256 = _sha256(request)
replay = _replay(
session,
tenant_id=tenant_id,
idempotency_key=idempotency_key,
request_sha256=request_sha256,
)
if replay is not None:
return replay
if _identity(session, tenant_id, kind, clean_id) is not None:
raise ReportingDefinitionError("Reporting definition already exists.")
duplicate = (
session.query(ReportingDefinitionIdentity.id)
.filter(
ReportingDefinitionIdentity.tenant_id == tenant_id,
ReportingDefinitionIdentity.definition_kind == kind,
ReportingDefinitionIdentity.definition_key == clean_key,
)
.first()
)
if duplicate is not None:
raise ReportingDefinitionError("Reporting definition key already exists.")
identity = ReportingDefinitionIdentity(
tenant_id=tenant_id,
definition_kind=kind,
definition_id=clean_id,
definition_key=clean_key,
created_by=_actor(principal),
)
session.add(identity)
session.flush()
return _write_revision(
session,
principal,
identity=identity,
current=None,
revision=1,
name=clean_name,
description=clean_description,
status=clean_status,
visibility=clean_visibility,
recorded_at=recorded_at,
change_reason=clean_reason,
payload=validated_payload,
parent_kind=parent_kind,
parent_id=parent_id,
parent_revision=parent_revision,
idempotency_key=idempotency_key,
request_sha256=request_sha256,
operation="created",
)
def update_definition(
session: Session,
principal: object,
*,
definition_kind: str,
definition_id: str,
expected_revision: int,
recorded_at: datetime,
change_reason: str,
idempotency_key: str,
changes: Mapping[str, object],
) -> ReportingDefinitionRecord:
tenant_id = _principal_tenant(principal)
kind = _definition_kind(definition_kind)
current_row = _current_row(
session,
tenant_id=tenant_id,
definition_kind=kind,
definition_id=definition_id,
lock=True,
)
if current_row is None:
raise LookupError("Reporting definition not found.")
if not can_write_definition(
session,
principal,
definition_kind=kind,
definition_id=definition_id,
):
raise PermissionError("Reporting definition write access is denied.")
current = definition_from_row(current_row)
unknown = set(changes) - {
"name",
"description",
"status",
"visibility",
"payload",
}
if unknown:
raise ReportingDefinitionError(
"Unsupported Reporting definition fields: " + ", ".join(sorted(unknown))
)
_aware(recorded_at, "Reporting recorded_at")
clean_reason = _required(change_reason, "Reporting change reason", 1_000)
request_sha256 = _sha256(
{
"definition_kind": kind,
"definition_id": definition_id,
"expected_revision": expected_revision,
"recorded_at": recorded_at,
"change_reason": clean_reason,
"changes": changes,
}
)
replay = _replay(
session,
tenant_id=tenant_id,
idempotency_key=idempotency_key,
request_sha256=request_sha256,
)
if replay is not None:
return replay
if current.revision != expected_revision:
raise ReportingDefinitionError(
"Reporting definition revision conflict: the expected revision is stale."
)
next_status = _status(str(changes.get("status", current.status)))
if (
next_status != current.status
and next_status not in STATUS_TRANSITIONS[current.status]
):
raise ReportingDefinitionError(
f"Cannot move Reporting definition from {current.status!r} to {next_status!r}."
)
next_payload = validate_definition_payload(
kind,
dict(changes.get("payload", current.payload)),
)
parent_kind, parent_id, parent_revision = _parent_reference(kind, next_payload)
_validate_parent(
session,
tenant_id=tenant_id,
child_status=next_status,
parent_kind=parent_kind,
parent_id=parent_id,
parent_revision=parent_revision,
)
identity = _identity(session, tenant_id, kind, definition_id)
if identity is None:
raise ReportingDefinitionError("Reporting definition identity is missing.")
return _write_revision(
session,
principal,
identity=identity,
current=current_row,
revision=current.revision + 1,
name=_required(
changes.get("name", current.name), "Reporting definition name", 500
),
description=_optional(
changes.get("description", current.description),
"Reporting description",
100_000,
),
status=next_status,
visibility=_visibility(str(changes.get("visibility", current.visibility))),
recorded_at=recorded_at,
change_reason=clean_reason,
payload=next_payload,
parent_kind=parent_kind,
parent_id=parent_id,
parent_revision=parent_revision,
idempotency_key=idempotency_key,
request_sha256=request_sha256,
operation="updated" if next_status == current.status else "state_changed",
)
def get_definition(
session: Session,
principal: object,
*,
definition_kind: str,
definition_id: str,
revision: int | None = None,
) -> ReportingDefinitionRecord | None:
tenant_id = _principal_tenant(principal)
kind = _definition_kind(definition_kind)
if not can_read_definition(
session,
principal,
definition_kind=kind,
definition_id=definition_id,
):
return None
query = session.query(ReportingDefinitionRevision).filter(
ReportingDefinitionRevision.tenant_id == tenant_id,
ReportingDefinitionRevision.definition_kind == kind,
ReportingDefinitionRevision.definition_id == definition_id,
)
if revision is None:
query = query.filter(ReportingDefinitionRevision.superseded_at.is_(None))
else:
query = query.filter(ReportingDefinitionRevision.revision == revision)
row = query.order_by(ReportingDefinitionRevision.revision.desc()).first()
return definition_from_row(row) if row is not None else None
def list_definitions(
session: Session,
principal: object,
*,
definition_kinds: Sequence[str] | None = None,
statuses: Sequence[str] | None = None,
query: str = "",
offset: int = 0,
limit: int = 100,
) -> tuple[tuple[ReportingDefinitionRecord, ...], int]:
_require_scope(principal, READ_SCOPE)
if offset < 0 or not 1 <= limit <= 200:
raise ReportingDefinitionError(
"Reporting list offset must be non-negative and limit between 1 and 200."
)
kinds = tuple(dict.fromkeys(definition_kinds or DEFINITION_KINDS))
if any(item not in DEFINITION_KINDS for item in kinds):
raise ReportingDefinitionError("Unsupported Reporting definition kind filter.")
statement = session.query(ReportingDefinitionRevision).filter(
ReportingDefinitionRevision.tenant_id == _principal_tenant(principal),
ReportingDefinitionRevision.superseded_at.is_(None),
ReportingDefinitionRevision.definition_kind.in_(kinds),
)
statement = _filter_accessible(statement, principal)
if statuses:
statement = statement.filter(
ReportingDefinitionRevision.status.in_(tuple(dict.fromkeys(statuses)))
)
clean_query = query.strip().casefold()
if clean_query:
pattern = f"%{clean_query}%"
statement = statement.filter(
or_(
func.lower(ReportingDefinitionRevision.name).like(pattern),
func.lower(ReportingDefinitionRevision.definition_key).like(pattern),
func.lower(ReportingDefinitionRevision.description).like(pattern),
)
)
total = int(statement.with_entities(func.count()).scalar() or 0)
rows = (
statement.order_by(
ReportingDefinitionRevision.definition_kind.asc(),
ReportingDefinitionRevision.name.asc(),
)
.offset(offset)
.limit(limit)
.all()
)
return tuple(definition_from_row(row) for row in rows), total
def definition_history(
session: Session,
principal: object,
*,
definition_kind: str,
definition_id: str,
limit: int = 100,
) -> tuple[ReportingDefinitionRecord, ...]:
if not can_read_definition(
session,
principal,
definition_kind=definition_kind,
definition_id=definition_id,
):
return ()
rows = (
session.query(ReportingDefinitionRevision)
.filter(
ReportingDefinitionRevision.tenant_id == _principal_tenant(principal),
ReportingDefinitionRevision.definition_kind == definition_kind,
ReportingDefinitionRevision.definition_id == definition_id,
)
.order_by(ReportingDefinitionRevision.revision.desc())
.limit(max(1, min(limit, 200)))
.all()
)
return tuple(definition_from_row(row) for row in rows)
def can_read_definition(
session: Session,
principal: object,
*,
definition_kind: str,
definition_id: str,
) -> bool:
if not _has_scope(principal, READ_SCOPE):
return False
return _can_access(
session,
principal,
definition_kind=definition_kind,
definition_id=definition_id,
permission="read",
)
def can_write_definition(
session: Session,
principal: object,
*,
definition_kind: str,
definition_id: str,
) -> bool:
if not _has_scope(principal, WRITE_SCOPE):
return False
return _can_access(
session,
principal,
definition_kind=definition_kind,
definition_id=definition_id,
permission="write",
)
def _write_revision(
session: Session,
principal: object,
*,
identity: ReportingDefinitionIdentity,
current: ReportingDefinitionRevision | None,
revision: int,
name: str,
description: str | None,
status: str,
visibility: str,
recorded_at: datetime,
change_reason: str,
payload: dict[str, Any],
parent_kind: str | None,
parent_id: str | None,
parent_revision: int | None,
idempotency_key: str,
request_sha256: str,
operation: str,
) -> ReportingDefinitionRecord:
if current is not None:
current.superseded_at = recorded_at
content_hash = _sha256(
{
"name": name,
"description": description,
"status": status,
"visibility": visibility,
"parent_kind": parent_kind,
"parent_id": parent_id,
"parent_revision": parent_revision,
"payload": payload,
}
)
event_id = str(uuid.uuid4())
row = ReportingDefinitionRevision(
tenant_id=identity.tenant_id,
identity_id=identity.id,
definition_kind=identity.definition_kind,
definition_id=identity.definition_id,
definition_key=identity.definition_key,
revision=revision,
previous_revision_id=current.id if current else None,
parent_kind=parent_kind,
parent_id=parent_id,
parent_revision=parent_revision,
name=name,
description=description,
status=status,
visibility=visibility,
content_hash=content_hash,
change_reason=change_reason,
idempotency_key=_required(idempotency_key, "Reporting idempotency key", 255),
request_sha256=request_sha256,
event_id=event_id,
recorded_at=recorded_at,
payload=payload,
changed_by=_actor(principal),
)
session.add(row)
session.flush()
_sync_grants(session, row)
event_type = f"reporting.{identity.definition_kind}.{operation}"
emit_platform_event(
session,
PlatformEvent(
event_id=event_id,
type=event_type,
module_id="reporting",
payload={
"definition_kind": identity.definition_kind,
"definition_key": identity.definition_key,
"revision": revision,
"status": status,
"content_hash": content_hash,
"change_reason": change_reason,
},
occurred_at=recorded_at,
actor=EventActorRef(type="account", id=_actor(principal)),
tenant=EventTenantRef(id=identity.tenant_id),
resource=EventObjectRef(
type=f"reporting_{identity.definition_kind}",
id=identity.definition_id,
label=name,
),
classification="restricted" if visibility == "restricted" else "internal",
),
)
return definition_from_row(row)
def _sync_grants(session: Session, row: ReportingDefinitionRevision) -> None:
access_policy = row.payload.get("access_policy")
subjects = (
access_policy.get("subjects", []) if isinstance(access_policy, Mapping) else []
)
desired: dict[tuple[str, str], list[str]] = {}
if not isinstance(subjects, list):
raise ReportingDefinitionError("Report access_policy subjects must be a list.")
for subject in subjects:
if not isinstance(subject, Mapping):
raise ReportingDefinitionError("Report access subjects must be objects.")
kind = str(subject.get("kind") or "")
subject_id = str(subject.get("id") or "").strip()
if kind not in SUBJECT_KINDS or not subject_id:
raise ReportingDefinitionError("Report access subject is invalid.")
permissions = tuple(
dict.fromkeys(str(item) for item in subject.get("permissions", ["read"]))
)
if not permissions or set(permissions) - {"read", "write", "publish", "admin"}:
raise ReportingDefinitionError("Report access permissions are invalid.")
desired[(kind, subject_id)] = list(permissions)
rows = (
session.query(ReportingDefinitionGrant)
.filter(
ReportingDefinitionGrant.tenant_id == row.tenant_id,
ReportingDefinitionGrant.definition_kind == row.definition_kind,
ReportingDefinitionGrant.definition_id == row.definition_id,
)
.all()
)
existing = {(item.subject_kind, item.subject_id): item for item in rows}
for key, item in existing.items():
if key not in desired:
item.active = False
item.source_revision = row.revision
for key, permissions in desired.items():
item = existing.get(key)
if item is None:
session.add(
ReportingDefinitionGrant(
tenant_id=row.tenant_id,
definition_kind=row.definition_kind,
definition_id=row.definition_id,
subject_kind=key[0],
subject_id=key[1],
permissions=permissions,
active=True,
source_revision=row.revision,
)
)
else:
item.permissions = permissions
item.active = True
item.source_revision = row.revision
session.flush()
def _validate_parent(
session: Session,
*,
tenant_id: str,
child_status: str,
parent_kind: str | None,
parent_id: str | None,
parent_revision: int | None,
) -> None:
if parent_kind is None:
return
row = (
session.query(ReportingDefinitionRevision)
.filter(
ReportingDefinitionRevision.tenant_id == tenant_id,
ReportingDefinitionRevision.definition_kind == parent_kind,
ReportingDefinitionRevision.definition_id == parent_id,
ReportingDefinitionRevision.revision == parent_revision,
)
.one_or_none()
)
if row is None:
raise ReportingDefinitionError(
f"Reporting definition references a missing {parent_kind} revision."
)
if child_status == "active" and row.status != "active":
raise ReportingDefinitionError(
f"An active Reporting definition requires an active {parent_kind} revision."
)
def _parent_reference(
definition_kind: str,
payload: Mapping[str, object],
) -> tuple[str | None, str | None, int | None]:
if definition_kind == "semantic_model":
return "dataset", str(payload["dataset_id"]), int(payload["dataset_revision"])
if definition_kind == "report":
return (
"semantic_model",
str(payload["semantic_model_id"]),
int(payload["semantic_model_revision"]),
)
if definition_kind == "quality_plan":
return "dataset", str(payload["dataset_id"]), int(payload["dataset_revision"])
return None, None, None
def _replay(
session: Session,
*,
tenant_id: str,
idempotency_key: str,
request_sha256: str,
) -> ReportingDefinitionRecord | None:
key = _required(idempotency_key, "Reporting idempotency key", 255)
row = (
session.query(ReportingDefinitionRevision)
.filter(
ReportingDefinitionRevision.tenant_id == tenant_id,
ReportingDefinitionRevision.idempotency_key == key,
)
.one_or_none()
)
if row is None:
return None
if row.request_sha256 != request_sha256:
raise ReportingDefinitionError(
"Reporting idempotency conflict: the key belongs to another request."
)
return definition_from_row(row)
def _can_access(
session: Session,
principal: object,
*,
definition_kind: str,
definition_id: str,
permission: str,
) -> bool:
tenant_id = _principal_tenant(principal)
row = _current_row(
session,
tenant_id=tenant_id,
definition_kind=definition_kind,
definition_id=definition_id,
lock=False,
)
if row is None:
return False
if _has_scope(principal, ADMIN_SCOPE):
return True
identity = _identity(session, tenant_id, definition_kind, definition_id)
actor_ids = _actor_ids(principal)
if identity is not None and identity.created_by in actor_ids:
return True
if permission == "read" and row.visibility == "tenant":
return True
subjects = _principal_subjects(principal)
if not subjects:
return False
clauses = [
and_(
ReportingDefinitionGrant.subject_kind == kind,
ReportingDefinitionGrant.subject_id == subject_id,
)
for kind, subject_id in subjects
]
grants = (
session.query(ReportingDefinitionGrant)
.filter(
ReportingDefinitionGrant.tenant_id == tenant_id,
ReportingDefinitionGrant.definition_kind == definition_kind,
ReportingDefinitionGrant.definition_id == definition_id,
ReportingDefinitionGrant.active.is_(True),
or_(*clauses),
)
.all()
)
return any(
permission == "read"
or permission in set(item.permissions or ())
or "admin" in set(item.permissions or ())
for item in grants
)
def _require_scope(principal: object, scope: str) -> None:
if not _has_scope(principal, scope):
raise PermissionError(f"Missing scope: {scope}")
def _filter_accessible(query: Query, principal: object) -> Query:
if _has_scope(principal, ADMIN_SCOPE):
return query
conditions = [ReportingDefinitionRevision.visibility == "tenant"]
actor_ids = _actor_ids(principal)
if actor_ids:
conditions.append(
exists()
.where(
ReportingDefinitionIdentity.id
== ReportingDefinitionRevision.identity_id
)
.where(ReportingDefinitionIdentity.created_by.in_(actor_ids))
)
subjects = _principal_subjects(principal)
if subjects:
conditions.append(
exists()
.where(
ReportingDefinitionGrant.tenant_id
== ReportingDefinitionRevision.tenant_id
)
.where(
ReportingDefinitionGrant.definition_kind
== ReportingDefinitionRevision.definition_kind
)
.where(
ReportingDefinitionGrant.definition_id
== ReportingDefinitionRevision.definition_id
)
.where(ReportingDefinitionGrant.active.is_(True))
.where(
or_(
*(
and_(
ReportingDefinitionGrant.subject_kind == kind,
ReportingDefinitionGrant.subject_id == subject_id,
)
for kind, subject_id in subjects
)
)
)
)
return query.filter(or_(*conditions))
def _current_row(
session: Session,
*,
tenant_id: str,
definition_kind: str,
definition_id: str,
lock: bool,
) -> ReportingDefinitionRevision | None:
query = session.query(ReportingDefinitionRevision).filter(
ReportingDefinitionRevision.tenant_id == tenant_id,
ReportingDefinitionRevision.definition_kind == definition_kind,
ReportingDefinitionRevision.definition_id == definition_id,
ReportingDefinitionRevision.superseded_at.is_(None),
)
if lock:
query = query.with_for_update()
return query.one_or_none()
def _identity(
session: Session,
tenant_id: str,
definition_kind: str,
definition_id: str,
) -> ReportingDefinitionIdentity | None:
return (
session.query(ReportingDefinitionIdentity)
.filter(
ReportingDefinitionIdentity.tenant_id == tenant_id,
ReportingDefinitionIdentity.definition_kind == definition_kind,
ReportingDefinitionIdentity.definition_id == definition_id,
)
.one_or_none()
)
def _principal_subjects(principal: object) -> tuple[tuple[str, str], ...]:
values: list[tuple[str, str]] = []
for kind, attribute in (
("account", "account_id"),
("identity", "identity_id"),
("function_assignment", "acting_assignment_id"),
("service_account", "service_account_id"),
):
value = getattr(principal, attribute, None)
if str(value or "").strip():
values.append((kind, str(value)))
for kind, attribute in (
("group", "group_ids"),
("role", "role_ids"),
("function_assignment", "function_assignment_ids"),
):
values.extend(
(kind, str(value))
for value in getattr(principal, attribute, ()) or ()
if str(value or "").strip()
)
return tuple(dict.fromkeys(values))
def _actor(principal: object) -> str | None:
for value in (
getattr(principal, "account_id", None),
getattr(principal, "identity_id", None),
getattr(principal, "membership_id", None),
):
if str(value or "").strip():
return str(value)
return None
def _actor_ids(principal: object) -> tuple[str, ...]:
user = getattr(principal, "user", None)
return tuple(
dict.fromkeys(
str(value)
for value in (
getattr(principal, "account_id", None),
getattr(principal, "identity_id", None),
getattr(principal, "membership_id", None),
getattr(user, "id", None),
)
if str(value or "").strip()
)
)
def _principal_tenant(principal: object) -> str:
tenant_id = str(getattr(principal, "tenant_id", "") or "").strip()
if not tenant_id:
raise ReportingDefinitionError(
"Reporting operations require a tenant-bound principal."
)
return tenant_id
def _has_scope(principal: object, scope: str) -> bool:
method = getattr(principal, "has", None)
if callable(method):
return bool(method(scope))
return scopes_grant_compatible(
frozenset(getattr(principal, "scopes", ()) or ()),
scope,
)
def _definition_kind(value: str) -> str:
if value not in DEFINITION_KINDS:
raise ReportingDefinitionError(
f"Unsupported Reporting definition kind: {value!r}."
)
return value
def _status(value: str) -> str:
if value not in STATUS_TRANSITIONS:
raise ReportingDefinitionError(f"Unsupported Reporting status: {value!r}.")
return value
def _visibility(value: str) -> str:
if value not in {"tenant", "restricted"}:
raise ReportingDefinitionError(f"Unsupported Reporting visibility: {value!r}.")
return value
def _key(value: object, label: str) -> str:
result = _required(value, label, 120).casefold()
if any(
character not in "abcdefghijklmnopqrstuvwxyz0123456789._-"
for character in result
):
raise ReportingDefinitionError(
f"{label} may contain only letters, digits, dot, underscore, and hyphen."
)
return result
def _required(value: object, label: str, maximum: int) -> str:
result = str(value or "").strip()
if not result:
raise ReportingDefinitionError(f"{label} is required.")
if len(result) > maximum:
raise ReportingDefinitionError(f"{label} is limited to {maximum} characters.")
return result
def _optional(value: object, label: str, maximum: int) -> str | None:
result = str(value or "").strip()
if not result:
return None
if len(result) > maximum:
raise ReportingDefinitionError(f"{label} is limited to {maximum} characters.")
return result
def _aware(value: datetime, label: str) -> None:
if value.tzinfo is None or value.utcoffset() is None:
raise ReportingDefinitionError(f"{label} must include a timezone.")
def _sha256(value: object) -> str:
return hashlib.sha256(
json.dumps(
_json_value(value),
sort_keys=True,
separators=(",", ":"),
ensure_ascii=True,
).encode("utf-8")
).hexdigest()
def _json_value(value: object) -> Any:
if isinstance(value, datetime):
return value.isoformat()
if hasattr(value, "model_dump"):
return _json_value(value.model_dump(mode="json"))
if isinstance(value, Mapping):
return {str(key): _json_value(item) for key, item in value.items()}
if isinstance(value, (list, tuple)):
return [_json_value(item) for item in value]
return value
__all__ = [
"ADMIN_SCOPE",
"DEFINITION_KINDS",
"READ_SCOPE",
"ReportingDefinitionError",
"WRITE_SCOPE",
"can_read_definition",
"can_write_definition",
"create_definition",
"definition_history",
"get_definition",
"list_definitions",
"update_definition",
]