msflib.scope¶
Part of the msflib core package.
msflib.scope
¶
Unified scope system for MSFLib.
Provides a canonical, extensible scope model for multi-tenant data access control.
ScopeCompiler handles generic scope transformations (to string, segments, or mapping). Domain-specific transformations (e.g., checkpoint keys, retrieval filters, storage keys) belong in the subsystem classes that use them (e.g., CheckpointScope, factories).
Quick start::
from msflib.scope import (
ScopeEnvelope,
PrincipalContext,
ScopeCompiler,
DimensionRegistry,
create_default_registry,
)
registry = create_default_registry()
compiler = ScopeCompiler(registry=registry)
envelope = ScopeEnvelope(
dimensions={"tenant_id": "42", "workspace_id": "7"},
principal=PrincipalContext(account_id="user-1"),
)
# Compile to canonical scope string
scope_string = compiler.compile_scope_string(envelope, operation="retrieval.search")
# Compile to canonical scope segments tuple
segments = compiler.compile_scope_segments(envelope, operation="retrieval.search")
# Compile to canonical scope mapping
scope_map = compiler.compile_scope_map(envelope, operation="retrieval.search")
ScopeCompiler(registry: DimensionRegistry | None = None, profile_registry: ProfileRegistry | None = None, string_delimiter: str = '/', string_style: str = 'key_value_pairs', sentinel: str = _NONE_SENTINEL, lock_registry: bool = False)
¶
Translates a :class:ScopeEnvelope into various sink-specific formats.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
registry
|
DimensionRegistry | None
|
Dimension registry used to resolve dimension specs. |
None
|
profile_registry
|
ProfileRegistry | None
|
Operation profile registry used for validation. |
None
|
register_profile(name: str, profile: dict[str, Any]) -> None
¶
Register (or replace) an operation profile by name.
get_profile(name: str) -> dict[str, Any]
¶
Return the profile dict for name.
validate_profile(envelope: ScopeEnvelope, profile_name: str) -> None
¶
Validate that envelope satisfies profile_name.
Raises :exc:~msflib.scope.errors.ProfileValidationError on failure.
get_operation_policy_config(operation: str) -> dict[str, Any]
¶
Return policy-related profile settings for operation.
This keeps policy evaluation concerns outside sink projection.
compile_scope_map(envelope: ScopeEnvelope, *, operation: str, use_sentinel: bool = True) -> dict[str, str] | dict[str, str | None]
¶
compile_scope_map(envelope: ScopeEnvelope, *, operation: str, use_sentinel: Literal[True] = True) -> dict[str, str]
compile_scope_map(envelope: ScopeEnvelope, *, operation: str, use_sentinel: Literal[False]) -> dict[str, str | None]
Return canonical scope as an ordered key/value mapping.
Pass use_sentinel=False to keep null/absent required_nullable
dimensions as None instead of substituting the compiler's
sentinel — for consumers that need to distinguish "explicitly null"
from a real value without comparing against the sentinel string.
compile_scope_segments(envelope: ScopeEnvelope, *, operation: str) -> tuple[str, ...]
¶
Return canonical scope as flat ordered segments: (k1, v1, k2, v2, ...).
compile_scope_string(envelope: ScopeEnvelope, *, operation: str, delimiter: str | None = None, style: str | None = None, order: tuple[str, ...] | None = None) -> str
¶
Return canonical scope as a delimited flat segment string.
compile_scope_items(envelope: ScopeEnvelope, *, operation: str, use_sentinel: bool = True) -> tuple[tuple[str, str], ...] | tuple[tuple[str, str | None], ...]
¶
compile_scope_items(envelope: ScopeEnvelope, *, operation: str, use_sentinel: Literal[True] = True) -> tuple[tuple[str, str], ...]
compile_scope_items(envelope: ScopeEnvelope, *, operation: str, use_sentinel: Literal[False]) -> tuple[tuple[str, str | None], ...]
Build canonical scope items from a profile-derived contract.
Contract semantics:
- required: must be present and non-null.
- required_nullable: always projected, with null/absent replaced by
sentinel (or left as None when use_sentinel=False).
- optional: projected only when the envelope actually supplies a
non-null value for that dimension; absent optional dimensions are
omitted entirely (no sentinel filler).
ConstraintViolationError(message: str, decision: ScopeDecision | None = None)
¶
Bases: ScopeError
Raised when a constraint evaluator denies access.
Attach the :class:~msflib.scope.model.ScopeDecision for structured
error handling without re-evaluating the constraint chain.
DimensionConflictError
¶
Bases: DimensionError
Raised when a duplicate or conflicting dimension is registered.
DimensionError
¶
Bases: ScopeError
Base for dimension registration and lookup errors.
ProfileValidationError
¶
Bases: ScopeError
Raised when a scope does not satisfy an operation profile's requirements.
ScopeError
¶
Bases: Exception
Base exception for all scope-related errors.
ScopeValidationError
¶
Bases: ScopeError
Raised when scope dimensions fail validation.
Typically indicates missing or malformed required dimensions before compiling to a sink-specific format.
UnknownDimensionError
¶
Bases: DimensionError
Raised when a dimension key or alias is not in the registry.
DimensionSpec(canonical_key: str, aliases: frozenset[str] = frozenset(), normalizer: Callable[[Any], str] | None = None, validator: Callable[[str | None], bool] | None = None, key_alias: str | None = None, priority: int = 0, sensitive: bool = False)
dataclass
¶
Metadata for a single scope dimension.
Attributes:
| Name | Type | Description |
|---|---|---|
canonical_key |
str
|
Primary name used in :class: |
aliases |
frozenset[str]
|
Alternative names that resolve to this dimension. |
normalizer |
Callable[[Any], str] | None
|
Callable |
validator |
Callable[[str | None], bool] | None
|
Optional callable |
key_alias |
str | None
|
Optional compact key name for string serialization. |
priority |
int
|
Higher values appear first in |
sensitive |
bool
|
Sensitive dimensions are redacted in audit summaries. |
PrincipalContext(account_id: str, roles: frozenset[str] = frozenset(), claims: Mapping[str, Any] = dict(), request_id: str | None = None, timestamp: datetime = (lambda: datetime.now(timezone.utc))())
dataclass
¶
Immutable representation of the authenticated caller.
Attributes:
| Name | Type | Description |
|---|---|---|
account_id |
str
|
The caller's system-wide account identifier (user). |
roles |
frozenset[str]
|
Frozenset of role names granted to this principal. |
claims |
Mapping[str, Any]
|
Arbitrary JWT/session claims forwarded from the auth layer. |
request_id |
str | None
|
Optional tracing identifier for the originating request. |
timestamp |
datetime
|
When the principal context was captured. |
has_role(role: str) -> bool
¶
Return True if role is in this principal's role set.
ScopeDecision(allowed: bool, reason_code: str = '', constraint_name: str = '', metadata: Mapping[str, Any] = dict())
dataclass
¶
Result of evaluating one or more :class:ScopeConstraint instances.
Attributes:
| Name | Type | Description |
|---|---|---|
allowed |
bool
|
|
reason_code |
str
|
Machine-readable code describing the outcome. |
constraint_name |
str
|
Name of the constraint that produced this decision. |
metadata |
Mapping[str, Any]
|
Optional structured data about the decision. |
allow(*, reason_code: str = 'allowed', constraint_name: str = '', metadata: dict[str, Any] | None = None) -> ScopeDecision
classmethod
¶
Factory for an allow decision.
deny(*, reason_code: str, constraint_name: str = '', metadata: dict[str, Any] | None = None) -> ScopeDecision
classmethod
¶
Factory for a deny decision.
to_dict() -> dict[str, Any]
¶
Serialize decision to dict for logging/events.
ScopeEnvelope(dimensions: Mapping[str, str | None], attributes: Mapping[str, Any] = dict(), principal: PrincipalContext | None = None, metadata: Mapping[str, Any] = dict())
dataclass
¶
Canonical, immutable scope context for a single operation.
dimensions holds normalised values keyed by canonical dimension name.
A dimension key that is absent means "not specified".
A dimension key present with a None value means "explicitly null"
(e.g. workspace_id=None is the workspace-less bucket per decision D3).
Attributes:
| Name | Type | Description |
|---|---|---|
dimensions |
Mapping[str, str | None]
|
Mapping of canonical dimension key → normalised string (or None). |
attributes |
Mapping[str, Any]
|
Non-dimension metadata (e.g. operation hints, flags). |
principal |
PrincipalContext | None
|
Authenticated caller context; may be |
metadata |
Mapping[str, Any]
|
Request-level tracing and observability metadata. |
get_dim(key: str) -> str | None
¶
Return the normalised value for key, or None if absent or null.
has_dim(key: str) -> bool
¶
Return True if key is present (even with a None value).
to_dict(*, registry: DimensionRegistry | None = None, redact_sensitive: bool = False) -> dict[str, Any]
¶
Serialise to a plain dict (suitable for JSON / logging).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
registry
|
DimensionRegistry | None
|
Optional dimension registry used to determine sensitive dimensions for redaction. |
None
|
redact_sensitive
|
bool
|
If |
False
|
canonical_items(*, order: tuple[str, ...], sentinel: str | None) -> tuple[tuple[str, str | None], ...]
¶
Return ordered key/value scope pairs with sentinel substitution.
This provides a canonical shape that can be transformed into any sink
representation by simple joins/splits. Pass sentinel=None to keep
null/absent values as None instead of substituting a filler.
canonical_segments(*, order: tuple[str, ...], sentinel: str) -> tuple[str, ...]
¶
Return flat ordered segments: (k1, v1, k2, v2, ...).
from_dict(data: dict[str, Any], *, registry: DimensionRegistry | None = None) -> ScopeEnvelope
classmethod
¶
Construct from a plain dict, optionally resolving aliases via registry.
When registry is provided, aliases are resolved to canonical keys and values are normalised using each dimension's normalizer function. Without a registry, values are still validated via _default_normalizer (which rejects booleans, empty strings, and collections).
ChannelConstraint(required_channel: str, *, deny_without_channel: bool = True)
¶
Validates that the request channel matches the required channel.
Channel is read from scope.attributes['channel'] first and falls back to
context['current_channel'] when absent.
ConstraintEvaluator(constraints: list[ScopeConstraint] | None = None, enforcement_level: str = 'log-only')
¶
Evaluates an ordered list of constraints and returns a composite decision.
Constraints are evaluated in order until the first deny. The
enforcement_level controls what happens after that deny:
- ``log-only``: return *allow* (violations are logged but not enforced).
- ``shadow``: return *deny* (violations are logged; callers observe the
denial without an exception being raised).
- ``enforce``: raise :class:`ConstraintViolationError` on the first deny.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constraints
|
list[ScopeConstraint] | None
|
Ordered list of :class: |
None
|
enforcement_level
|
str
|
One of |
'log-only'
|
last_decisions: list[ScopeDecision]
property
¶
Return decisions from the most recent :meth:evaluate call.
evaluate(scope: ScopeEnvelope, context: dict[str, Any] | None = None) -> ScopeDecision
¶
Run all constraints in order; return first deny or final allow.
Side-effects:
- Stores decisions in :attr:last_decisions for audit.
- Logs violations at WARNING level.
- Emits scope.decision events and records metrics for the first deny
(including enforce raises) and for the final effective decision
(an unconditional allow, a shadow deny, or a log-only override).
A log-only override therefore emits and records both the original
deny and the overridden allow.
The enforcement_level controls what happens on a deny:
- log-only: Log and return allow.
- shadow: Log and return deny (but do not raise).
- enforce: Log and raise :class:ConstraintViolationError.
DimensionMatchConstraint(required_match: dict[str, str] | None = None, *, constraint_name: str | None = None)
¶
Checks that dimension values in the scope match those in context.
Pass required_match as a mapping of {canonical_dim_key: context_key}.
If the context key is present and the scope has that dimension, the values
must match exactly. Missing context keys are skipped (not treated as deny).
Example::
constraint = DimensionMatchConstraint(
required_match={"tenant_id": "current_tenant_id"},
)
If scope.get_dim("tenant_id") != context["current_tenant_id"], the
constraint returns a deny decision.
MembershipConstraint(*, deny_without_principal: bool = True, fail_open: bool = False)
¶
Stub: validates that the principal is a member of the scoped workspace.
Production implementations should inject a membership service or DB session
via the context dict (key: membership_service).
By default (fail_open=False), missing or misconfigured services fail closed (deny) for security. Set fail_open=True to allow in shadow/log-only modes when the service is unavailable (e.g., for testing or during deployment).
If no principal is attached and the scope targets a concrete workspace, this constraint denies by default to avoid unauthenticated bypasses.
RoleConstraint(required_role: str, *, deny_without_principal: bool = True)
¶
Stub: validates that the principal has the required role.
Requires the scope envelope to carry a PrincipalContext with a roles
set. The required_role is checked against principal.roles.
If no principal is attached, behaviour depends on deny_without_principal:
- True (default): deny.
- False: allow (useful for service-to-service calls where role is implicit).
ScopeConstraint
¶
Bases: Protocol
Protocol that every scope constraint must implement.
Implementations should be stateless (or thread-safe) so they can be shared across requests.
evaluate(scope: ScopeEnvelope, context: dict[str, Any]) -> ScopeDecision
¶
Evaluate the constraint against scope and context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
ScopeEnvelope
|
The immutable scope envelope for the current operation. |
required |
context
|
dict[str, Any]
|
Runtime lookup context (e.g. DB session, service clients, request headers). Constraints must not mutate it. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
A |
ScopeDecision
|
class: |
ScopeDecisionMetrics(total_allowed: int = 0, total_denied: int = 0, by_constraint: dict[str, int] = (lambda: defaultdict(int))(), by_reason_code: dict[str, int] = (lambda: defaultdict(int))(), by_enforcement_level: dict[str, int] = (lambda: defaultdict(int))())
dataclass
¶
Aggregated metrics for scope decision outcomes.
Attributes:
| Name | Type | Description |
|---|---|---|
total_allowed |
int
|
Count of decisions with allowed=True. |
total_denied |
int
|
Count of decisions with allowed=False. |
by_constraint |
dict[str, int]
|
Breakdown of denials by constraint name. |
by_reason_code |
dict[str, int]
|
Breakdown of denials by reason_code. |
by_enforcement_level |
dict[str, int]
|
Breakdown of denials by enforcement_level. |
to_dict() -> dict[str, Any]
¶
Serialize metrics to dict.
ScopeMetricsCollector()
¶
Thread-safe collector for scope decision metrics.
Can be used to track decisions across all ConstraintEvaluator instances for observability and debugging.
ProfileRegistry(*, load_built_ins: bool = True)
¶
Manages operation profiles used for scope validation.
Usage::
registry = ProfileRegistry()
registry.register(
"my_op",
{
"required": ["tenant_id"],
"required_nullable": ["workspace_id"],
"optional": [],
},
)
profile = registry.get("my_op")
Built-in profiles (retrieval.search, memory.put, etc.) are
automatically loaded at construction time.
register(name: str, profile: dict[str, Any]) -> None
¶
Register (or replace) a profile.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Operation name (e.g. |
required |
profile
|
dict[str, Any]
|
Dict with |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If name is empty or profile is not a dict. |
ProfileValidationError
|
If profile declares overlapping required and required_nullable dimensions. |
get(name: str) -> dict[str, Any]
¶
Return the profile for name.
Raises:
| Type | Description |
|---|---|
ProfileValidationError
|
If name is not registered. |
has(name: str) -> bool
¶
Return True if name is registered.
list_all() -> list[str]
¶
Return sorted list of registered profile names.
DimensionRegistry()
¶
Central registry of scope dimension specifications.
Usage::
registry = DimensionRegistry()
registry.register(DimensionSpec(canonical_key="project_id", priority=25))
registry.lock()
canonical = registry.resolve_alias("project_id") # "project_id"
value = registry.normalize("project_id", 42) # "42"
After :meth:lock is called, :meth:register raises :exc:RuntimeError.
register(spec: DimensionSpec) -> None
¶
Register spec. Raises on duplicate canonical key or alias conflict.
get(key: str) -> DimensionSpec
¶
Return the spec for key (canonical or alias).
Raises :exc:~msflib.scope.errors.UnknownDimensionError if not found.
resolve_alias(key: str) -> str
¶
Return the canonical key for key (which may be canonical, alias, or key_alias).
Raises :exc:~msflib.scope.errors.UnknownDimensionError if unknown.
has(key: str) -> bool
¶
Return True if key is a known canonical key, alias, or key_alias.
list_all() -> list[DimensionSpec]
¶
Return all registered specs sorted by priority (highest first).
normalize(key: str, value: Any) -> str | None
¶
Normalise value using the spec for key.
Returns None when value is None (explicit null).
validate(key: str, value: str | None) -> bool
¶
Return True if value passes the spec's validator for key.
lock() -> None
¶
Lock the registry. Further register() calls will raise.
is_locked() -> bool
¶
Return True if the registry has been locked.
build_context_scope(*, tenant_id: int | None = None, workspace_id: int | None = None, account_id: int | None = None) -> ScopeEnvelope
¶
Build the caller's context ScopeEnvelope from tier identifiers.
Each id comes from a get_current_*-style dependency (see
get_scope_dependencies); entity-specific dimensions are layered on
later by domain code.
get_scope_dependencies(*, get_current_tenant: Callable | None = None, get_current_workspace: Callable | None = None, get_current_active_user: Callable | None = None, get_current_account: Callable | None = None) -> DependencyNamespace
¶
Return reusable FastAPI dependencies yielding the caller's ScopeEnvelope.
One factory takes the site's callables and returns a DependencyNamespace
(the module deps convention, see auth.get_account_dependencies). Each
member is a distinct named scope shape — pick the one matching the tiers
an endpoint acts in rather than parameterizing a single dependency.
get_current_tenant resolves the caller's tenant the same way
get_current_workspace/get_current_account resolve theirs: as a
request-scoped FastAPI dependency returning a model instance (.id
read off it below), validated at the endpoint boundary instead of a bare
ambient string threaded in from deep inside action/service code. Pass
msflib.tenancy.deps.get_tenant_dependencies(...).get_current_tenant
once modules/tenancy is mounted. Omit it (the default) for a site
with no tenant concept yet -- every scope below then carries no
tenant_id dimension at all.
Which identity callables to pass reduces to one question: does this site
have a membership entity distinct from the account — a per-workspace row
with its own status and role, resolved via auth.get_user_dependencies?
- Yes — pass
get_current_workspace+get_current_active_user.get_current_active_useris what actually verifies membership (the account_id/workspace_id join and per-workspace active/kicked/banned status live there — seeauth.get_current_user); workspace resolution alone does not check it, so this pair cannot be replaced by a bareget_current_account+get_current_workspace.get_account_scopethen carries all three tiers, andget_workspace_scopeis also available. -
No — pass
get_current_account(fromauth.get_account_dependencies— typicallyget_current_active_account, so an inactive account can't reach a scoped endpoint). Whether a workspace tier exists at all is then a second, independent choice: -
No workspace concept: omit
get_current_workspace. Onlyget_account_scope/get_tenant_scopeare exposed —get_workspace_scopehas no meaning without workspaces. - Workspace exists as an attribute of the account (e.g. an
Account.current_workspace_idfield), not a separate membership row: also passget_current_workspace.get_workspace_scopebecomes available too, since a workspace tier now exists — but nothing here verifies the account belongs to that workspace, because there is no membership row to check membership against.
Returns:
| Type | Description |
|---|---|
DependencyNamespace exposing:
|
|
get_scope_metrics() -> ScopeDecisionMetrics
¶
Get current scope decision metrics.
reset_scope_metrics() -> None
¶
Reset metrics (useful for testing).
create_default_registry() -> DimensionRegistry
¶
Create a new :class:DimensionRegistry pre-loaded with built-in dimensions.
compiler
¶
Scope compiler: translates a :class:ScopeEnvelope to sink-specific formats.
Each compile_* method is deterministic and derived from the same canonical
ordered scope shape to keep transformation costs low.
ScopeCompiler(registry: DimensionRegistry | None = None, profile_registry: ProfileRegistry | None = None, string_delimiter: str = '/', string_style: str = 'key_value_pairs', sentinel: str = _NONE_SENTINEL, lock_registry: bool = False)
¶
Translates a :class:ScopeEnvelope into various sink-specific formats.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
registry
|
DimensionRegistry | None
|
Dimension registry used to resolve dimension specs. |
None
|
profile_registry
|
ProfileRegistry | None
|
Operation profile registry used for validation. |
None
|
register_profile(name: str, profile: dict[str, Any]) -> None
¶
Register (or replace) an operation profile by name.
get_profile(name: str) -> dict[str, Any]
¶
Return the profile dict for name.
validate_profile(envelope: ScopeEnvelope, profile_name: str) -> None
¶
Validate that envelope satisfies profile_name.
Raises :exc:~msflib.scope.errors.ProfileValidationError on failure.
get_operation_policy_config(operation: str) -> dict[str, Any]
¶
Return policy-related profile settings for operation.
This keeps policy evaluation concerns outside sink projection.
compile_scope_map(envelope: ScopeEnvelope, *, operation: str, use_sentinel: bool = True) -> dict[str, str] | dict[str, str | None]
¶
compile_scope_map(envelope: ScopeEnvelope, *, operation: str, use_sentinel: Literal[True] = True) -> dict[str, str]
compile_scope_map(envelope: ScopeEnvelope, *, operation: str, use_sentinel: Literal[False]) -> dict[str, str | None]
Return canonical scope as an ordered key/value mapping.
Pass use_sentinel=False to keep null/absent required_nullable
dimensions as None instead of substituting the compiler's
sentinel — for consumers that need to distinguish "explicitly null"
from a real value without comparing against the sentinel string.
compile_scope_segments(envelope: ScopeEnvelope, *, operation: str) -> tuple[str, ...]
¶
Return canonical scope as flat ordered segments: (k1, v1, k2, v2, ...).
compile_scope_string(envelope: ScopeEnvelope, *, operation: str, delimiter: str | None = None, style: str | None = None, order: tuple[str, ...] | None = None) -> str
¶
Return canonical scope as a delimited flat segment string.
compile_scope_items(envelope: ScopeEnvelope, *, operation: str, use_sentinel: bool = True) -> tuple[tuple[str, str], ...] | tuple[tuple[str, str | None], ...]
¶
compile_scope_items(envelope: ScopeEnvelope, *, operation: str, use_sentinel: Literal[True] = True) -> tuple[tuple[str, str], ...]
compile_scope_items(envelope: ScopeEnvelope, *, operation: str, use_sentinel: Literal[False]) -> tuple[tuple[str, str | None], ...]
Build canonical scope items from a profile-derived contract.
Contract semantics:
- required: must be present and non-null.
- required_nullable: always projected, with null/absent replaced by
sentinel (or left as None when use_sentinel=False).
- optional: projected only when the envelope actually supplies a
non-null value for that dimension; absent optional dimensions are
omitted entirely (no sentinel filler).
deps
¶
Request-scoped FastAPI dependencies yielding the caller's ScopeEnvelope.
Endpoints that act on an entire scope should pull it in as a function parameter via these dependencies rather than assembling envelopes in endpoint bodies — the chosen shape then shows up in the endpoint signature.
build_context_scope(*, tenant_id: int | None = None, workspace_id: int | None = None, account_id: int | None = None) -> ScopeEnvelope
¶
Build the caller's context ScopeEnvelope from tier identifiers.
Each id comes from a get_current_*-style dependency (see
get_scope_dependencies); entity-specific dimensions are layered on
later by domain code.
get_scope_dependencies(*, get_current_tenant: Callable | None = None, get_current_workspace: Callable | None = None, get_current_active_user: Callable | None = None, get_current_account: Callable | None = None) -> DependencyNamespace
¶
Return reusable FastAPI dependencies yielding the caller's ScopeEnvelope.
One factory takes the site's callables and returns a DependencyNamespace
(the module deps convention, see auth.get_account_dependencies). Each
member is a distinct named scope shape — pick the one matching the tiers
an endpoint acts in rather than parameterizing a single dependency.
get_current_tenant resolves the caller's tenant the same way
get_current_workspace/get_current_account resolve theirs: as a
request-scoped FastAPI dependency returning a model instance (.id
read off it below), validated at the endpoint boundary instead of a bare
ambient string threaded in from deep inside action/service code. Pass
msflib.tenancy.deps.get_tenant_dependencies(...).get_current_tenant
once modules/tenancy is mounted. Omit it (the default) for a site
with no tenant concept yet -- every scope below then carries no
tenant_id dimension at all.
Which identity callables to pass reduces to one question: does this site
have a membership entity distinct from the account — a per-workspace row
with its own status and role, resolved via auth.get_user_dependencies?
- Yes — pass
get_current_workspace+get_current_active_user.get_current_active_useris what actually verifies membership (the account_id/workspace_id join and per-workspace active/kicked/banned status live there — seeauth.get_current_user); workspace resolution alone does not check it, so this pair cannot be replaced by a bareget_current_account+get_current_workspace.get_account_scopethen carries all three tiers, andget_workspace_scopeis also available. -
No — pass
get_current_account(fromauth.get_account_dependencies— typicallyget_current_active_account, so an inactive account can't reach a scoped endpoint). Whether a workspace tier exists at all is then a second, independent choice: -
No workspace concept: omit
get_current_workspace. Onlyget_account_scope/get_tenant_scopeare exposed —get_workspace_scopehas no meaning without workspaces. - Workspace exists as an attribute of the account (e.g. an
Account.current_workspace_idfield), not a separate membership row: also passget_current_workspace.get_workspace_scopebecomes available too, since a workspace tier now exists — but nothing here verifies the account belongs to that workspace, because there is no membership row to check membership against.
Returns:
| Type | Description |
|---|---|
DependencyNamespace exposing:
|
|
errors
¶
Exceptions for the unified scope system.
ScopeError
¶
Bases: Exception
Base exception for all scope-related errors.
ScopeValidationError
¶
Bases: ScopeError
Raised when scope dimensions fail validation.
Typically indicates missing or malformed required dimensions before compiling to a sink-specific format.
ConstraintViolationError(message: str, decision: ScopeDecision | None = None)
¶
Bases: ScopeError
Raised when a constraint evaluator denies access.
Attach the :class:~msflib.scope.model.ScopeDecision for structured
error handling without re-evaluating the constraint chain.
DimensionError
¶
Bases: ScopeError
Base for dimension registration and lookup errors.
DimensionConflictError
¶
Bases: DimensionError
Raised when a duplicate or conflicting dimension is registered.
UnknownDimensionError
¶
Bases: DimensionError
Raised when a dimension key or alias is not in the registry.
ProfileValidationError
¶
Bases: ScopeError
Raised when a scope does not satisfy an operation profile's requirements.
model
¶
Core data models for the unified scope system.
Models are frozen dataclasses. Mapping fields are defensively copied and wrapped in read-only proxies so callers cannot mutate top-level scope, principal, or decision mappings in place.
DimensionSpec(canonical_key: str, aliases: frozenset[str] = frozenset(), normalizer: Callable[[Any], str] | None = None, validator: Callable[[str | None], bool] | None = None, key_alias: str | None = None, priority: int = 0, sensitive: bool = False)
dataclass
¶
Metadata for a single scope dimension.
Attributes:
| Name | Type | Description |
|---|---|---|
canonical_key |
str
|
Primary name used in :class: |
aliases |
frozenset[str]
|
Alternative names that resolve to this dimension. |
normalizer |
Callable[[Any], str] | None
|
Callable |
validator |
Callable[[str | None], bool] | None
|
Optional callable |
key_alias |
str | None
|
Optional compact key name for string serialization. |
priority |
int
|
Higher values appear first in |
sensitive |
bool
|
Sensitive dimensions are redacted in audit summaries. |
PrincipalContext(account_id: str, roles: frozenset[str] = frozenset(), claims: Mapping[str, Any] = dict(), request_id: str | None = None, timestamp: datetime = (lambda: datetime.now(timezone.utc))())
dataclass
¶
Immutable representation of the authenticated caller.
Attributes:
| Name | Type | Description |
|---|---|---|
account_id |
str
|
The caller's system-wide account identifier (user). |
roles |
frozenset[str]
|
Frozenset of role names granted to this principal. |
claims |
Mapping[str, Any]
|
Arbitrary JWT/session claims forwarded from the auth layer. |
request_id |
str | None
|
Optional tracing identifier for the originating request. |
timestamp |
datetime
|
When the principal context was captured. |
has_role(role: str) -> bool
¶
Return True if role is in this principal's role set.
ScopeDecision(allowed: bool, reason_code: str = '', constraint_name: str = '', metadata: Mapping[str, Any] = dict())
dataclass
¶
Result of evaluating one or more :class:ScopeConstraint instances.
Attributes:
| Name | Type | Description |
|---|---|---|
allowed |
bool
|
|
reason_code |
str
|
Machine-readable code describing the outcome. |
constraint_name |
str
|
Name of the constraint that produced this decision. |
metadata |
Mapping[str, Any]
|
Optional structured data about the decision. |
allow(*, reason_code: str = 'allowed', constraint_name: str = '', metadata: dict[str, Any] | None = None) -> ScopeDecision
classmethod
¶
Factory for an allow decision.
deny(*, reason_code: str, constraint_name: str = '', metadata: dict[str, Any] | None = None) -> ScopeDecision
classmethod
¶
Factory for a deny decision.
to_dict() -> dict[str, Any]
¶
Serialize decision to dict for logging/events.
ScopeEnvelope(dimensions: Mapping[str, str | None], attributes: Mapping[str, Any] = dict(), principal: PrincipalContext | None = None, metadata: Mapping[str, Any] = dict())
dataclass
¶
Canonical, immutable scope context for a single operation.
dimensions holds normalised values keyed by canonical dimension name.
A dimension key that is absent means "not specified".
A dimension key present with a None value means "explicitly null"
(e.g. workspace_id=None is the workspace-less bucket per decision D3).
Attributes:
| Name | Type | Description |
|---|---|---|
dimensions |
Mapping[str, str | None]
|
Mapping of canonical dimension key → normalised string (or None). |
attributes |
Mapping[str, Any]
|
Non-dimension metadata (e.g. operation hints, flags). |
principal |
PrincipalContext | None
|
Authenticated caller context; may be |
metadata |
Mapping[str, Any]
|
Request-level tracing and observability metadata. |
get_dim(key: str) -> str | None
¶
Return the normalised value for key, or None if absent or null.
has_dim(key: str) -> bool
¶
Return True if key is present (even with a None value).
to_dict(*, registry: DimensionRegistry | None = None, redact_sensitive: bool = False) -> dict[str, Any]
¶
Serialise to a plain dict (suitable for JSON / logging).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
registry
|
DimensionRegistry | None
|
Optional dimension registry used to determine sensitive dimensions for redaction. |
None
|
redact_sensitive
|
bool
|
If |
False
|
canonical_items(*, order: tuple[str, ...], sentinel: str | None) -> tuple[tuple[str, str | None], ...]
¶
Return ordered key/value scope pairs with sentinel substitution.
This provides a canonical shape that can be transformed into any sink
representation by simple joins/splits. Pass sentinel=None to keep
null/absent values as None instead of substituting a filler.
canonical_segments(*, order: tuple[str, ...], sentinel: str) -> tuple[str, ...]
¶
Return flat ordered segments: (k1, v1, k2, v2, ...).
from_dict(data: dict[str, Any], *, registry: DimensionRegistry | None = None) -> ScopeEnvelope
classmethod
¶
Construct from a plain dict, optionally resolving aliases via registry.
When registry is provided, aliases are resolved to canonical keys and values are normalised using each dimension's normalizer function. Without a registry, values are still validated via _default_normalizer (which rejects booleans, empty strings, and collections).
policy
¶
Constraint framework for the unified scope system.
Constraints are evaluated in registration order. The first deny wins.
All decisions are auditable via :class:ScopeDecision.
ScopeDecisionMetrics(total_allowed: int = 0, total_denied: int = 0, by_constraint: dict[str, int] = (lambda: defaultdict(int))(), by_reason_code: dict[str, int] = (lambda: defaultdict(int))(), by_enforcement_level: dict[str, int] = (lambda: defaultdict(int))())
dataclass
¶
Aggregated metrics for scope decision outcomes.
Attributes:
| Name | Type | Description |
|---|---|---|
total_allowed |
int
|
Count of decisions with allowed=True. |
total_denied |
int
|
Count of decisions with allowed=False. |
by_constraint |
dict[str, int]
|
Breakdown of denials by constraint name. |
by_reason_code |
dict[str, int]
|
Breakdown of denials by reason_code. |
by_enforcement_level |
dict[str, int]
|
Breakdown of denials by enforcement_level. |
to_dict() -> dict[str, Any]
¶
Serialize metrics to dict.
ScopeMetricsCollector()
¶
Thread-safe collector for scope decision metrics.
Can be used to track decisions across all ConstraintEvaluator instances for observability and debugging.
ScopeConstraint
¶
Bases: Protocol
Protocol that every scope constraint must implement.
Implementations should be stateless (or thread-safe) so they can be shared across requests.
evaluate(scope: ScopeEnvelope, context: dict[str, Any]) -> ScopeDecision
¶
Evaluate the constraint against scope and context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
ScopeEnvelope
|
The immutable scope envelope for the current operation. |
required |
context
|
dict[str, Any]
|
Runtime lookup context (e.g. DB session, service clients, request headers). Constraints must not mutate it. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
A |
ScopeDecision
|
class: |
DimensionMatchConstraint(required_match: dict[str, str] | None = None, *, constraint_name: str | None = None)
¶
Checks that dimension values in the scope match those in context.
Pass required_match as a mapping of {canonical_dim_key: context_key}.
If the context key is present and the scope has that dimension, the values
must match exactly. Missing context keys are skipped (not treated as deny).
Example::
constraint = DimensionMatchConstraint(
required_match={"tenant_id": "current_tenant_id"},
)
If scope.get_dim("tenant_id") != context["current_tenant_id"], the
constraint returns a deny decision.
MembershipConstraint(*, deny_without_principal: bool = True, fail_open: bool = False)
¶
Stub: validates that the principal is a member of the scoped workspace.
Production implementations should inject a membership service or DB session
via the context dict (key: membership_service).
By default (fail_open=False), missing or misconfigured services fail closed (deny) for security. Set fail_open=True to allow in shadow/log-only modes when the service is unavailable (e.g., for testing or during deployment).
If no principal is attached and the scope targets a concrete workspace, this constraint denies by default to avoid unauthenticated bypasses.
RoleConstraint(required_role: str, *, deny_without_principal: bool = True)
¶
Stub: validates that the principal has the required role.
Requires the scope envelope to carry a PrincipalContext with a roles
set. The required_role is checked against principal.roles.
If no principal is attached, behaviour depends on deny_without_principal:
- True (default): deny.
- False: allow (useful for service-to-service calls where role is implicit).
ChannelConstraint(required_channel: str, *, deny_without_channel: bool = True)
¶
Validates that the request channel matches the required channel.
Channel is read from scope.attributes['channel'] first and falls back to
context['current_channel'] when absent.
ConstraintEvaluator(constraints: list[ScopeConstraint] | None = None, enforcement_level: str = 'log-only')
¶
Evaluates an ordered list of constraints and returns a composite decision.
Constraints are evaluated in order until the first deny. The
enforcement_level controls what happens after that deny:
- ``log-only``: return *allow* (violations are logged but not enforced).
- ``shadow``: return *deny* (violations are logged; callers observe the
denial without an exception being raised).
- ``enforce``: raise :class:`ConstraintViolationError` on the first deny.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constraints
|
list[ScopeConstraint] | None
|
Ordered list of :class: |
None
|
enforcement_level
|
str
|
One of |
'log-only'
|
last_decisions: list[ScopeDecision]
property
¶
Return decisions from the most recent :meth:evaluate call.
evaluate(scope: ScopeEnvelope, context: dict[str, Any] | None = None) -> ScopeDecision
¶
Run all constraints in order; return first deny or final allow.
Side-effects:
- Stores decisions in :attr:last_decisions for audit.
- Logs violations at WARNING level.
- Emits scope.decision events and records metrics for the first deny
(including enforce raises) and for the final effective decision
(an unconditional allow, a shadow deny, or a log-only override).
A log-only override therefore emits and records both the original
deny and the overridden allow.
The enforcement_level controls what happens on a deny:
- log-only: Log and return allow.
- shadow: Log and return deny (but do not raise).
- enforce: Log and raise :class:ConstraintViolationError.
get_scope_metrics() -> ScopeDecisionMetrics
¶
Get current scope decision metrics.
reset_scope_metrics() -> None
¶
Reset metrics (useful for testing).
profiles
¶
Operation profile registry and built-in profiles.
A profile defines the required/optional dimensions and policy-level enforcement settings for a named operation. The compiler validates envelopes against profiles before compiling.
ProfileRegistry(*, load_built_ins: bool = True)
¶
Manages operation profiles used for scope validation.
Usage::
registry = ProfileRegistry()
registry.register(
"my_op",
{
"required": ["tenant_id"],
"required_nullable": ["workspace_id"],
"optional": [],
},
)
profile = registry.get("my_op")
Built-in profiles (retrieval.search, memory.put, etc.) are
automatically loaded at construction time.
register(name: str, profile: dict[str, Any]) -> None
¶
Register (or replace) a profile.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Operation name (e.g. |
required |
profile
|
dict[str, Any]
|
Dict with |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If name is empty or profile is not a dict. |
ProfileValidationError
|
If profile declares overlapping required and required_nullable dimensions. |
get(name: str) -> dict[str, Any]
¶
Return the profile for name.
Raises:
| Type | Description |
|---|---|
ProfileValidationError
|
If name is not registered. |
has(name: str) -> bool
¶
Return True if name is registered.
list_all() -> list[str]
¶
Return sorted list of registered profile names.
registry
¶
Dimension registry for the unified scope system.
Scope dimensions are registered at application start-up. The registry can
be locked (via :meth:lock) to prevent further registrations at runtime;
locking is optional and caller-controlled.
DimensionRegistry()
¶
Central registry of scope dimension specifications.
Usage::
registry = DimensionRegistry()
registry.register(DimensionSpec(canonical_key="project_id", priority=25))
registry.lock()
canonical = registry.resolve_alias("project_id") # "project_id"
value = registry.normalize("project_id", 42) # "42"
After :meth:lock is called, :meth:register raises :exc:RuntimeError.
register(spec: DimensionSpec) -> None
¶
Register spec. Raises on duplicate canonical key or alias conflict.
get(key: str) -> DimensionSpec
¶
Return the spec for key (canonical or alias).
Raises :exc:~msflib.scope.errors.UnknownDimensionError if not found.
resolve_alias(key: str) -> str
¶
Return the canonical key for key (which may be canonical, alias, or key_alias).
Raises :exc:~msflib.scope.errors.UnknownDimensionError if unknown.
has(key: str) -> bool
¶
Return True if key is a known canonical key, alias, or key_alias.
list_all() -> list[DimensionSpec]
¶
Return all registered specs sorted by priority (highest first).
normalize(key: str, value: Any) -> str | None
¶
Normalise value using the spec for key.
Returns None when value is None (explicit null).
validate(key: str, value: str | None) -> bool
¶
Return True if value passes the spec's validator for key.
lock() -> None
¶
Lock the registry. Further register() calls will raise.
is_locked() -> bool
¶
Return True if the registry has been locked.
create_default_registry() -> DimensionRegistry
¶
Create a new :class:DimensionRegistry pre-loaded with built-in dimensions.