"""Loading, validation, and permission evaluation for widget manifests.
A widget describes itself in a ``widget.toml`` file. This module parses and
validates that description and evaluates the host-side ``required_permission``
gate (the forkable, per-user bar) against a resolved address count.
"""
import tomllib
VALID_ORIGINS = ("inhouse", "thirdparty")
VALID_CAPABILITIES = ("public", "engine-backed")
REQUIRED_FIELDS = (
"id",
"name",
"version",
"origin",
"capability",
"required_permission",
)
[docs]
class ManifestError(Exception):
"""Raised when a widget manifest is missing or malformed."""
[docs]
class Manifest:
"""Parsed and validated description of a single widget.
:var Manifest.id: unique widget slug
:type Manifest.id: str
:var Manifest.name: human-readable widget name
:type Manifest.name: str
:var Manifest.version: widget version string
:type Manifest.version: str
:var Manifest.origin: provenance, one of inhouse or thirdparty
:type Manifest.origin: str
:var Manifest.capability: privilege level, one of public or engine-backed
:type Manifest.capability: str
:var Manifest.revenue_account: payout identifier for thirdparty widgets
:type Manifest.revenue_account: str
:var Manifest.required_permission: user-permission gate, integer or band list
:type Manifest.required_permission: int or list
:var Manifest.routes: route declarations served by the widget
:type Manifest.routes: list
:var Manifest.consumers: websocket consumer declarations
:type Manifest.consumers: list
:var Manifest.menu: optional menu entry declaration
:type Manifest.menu: dict
:var Manifest.engine_endpoints: declared privileged engine scopes
:type Manifest.engine_endpoints: list
:var Manifest.data: declared user-context keys the host must inject
:type Manifest.data: list
:var Manifest.hosts: declared external hosts the widget may call
:type Manifest.hosts: list
:var Manifest.assets: static and template directory declarations
:type Manifest.assets: dict
"""
def __init__(self, data):
"""Populate manifest attributes from the parsed mapping.
:param data: parsed and validated widget.toml mapping
:type data: dict
"""
self.id = data["id"]
self.name = data["name"]
self.version = data["version"]
self.origin = data["origin"]
self.capability = data["capability"]
self.revenue_account = data.get("revenue_account", "")
self.required_permission = data["required_permission"]
self.routes = data.get("routes", [])
self.consumers = data.get("consumers", [])
self.menu = data.get("menu")
self.engine_endpoints = data.get("engine_endpoints", [])
self.data = data.get("data", [])
self.hosts = data.get("hosts", [])
self.assets = data.get("assets", {})
[docs]
def load_manifest(path):
"""Load, validate, and return the widget manifest stored at `path`.
:param path: full path to a widget.toml file
:type path: :class:`pathlib.Path`
:var data: parsed widget.toml mapping
:type data: dict
:return: :class:`Manifest`
"""
with open(path, "rb") as manifest_file:
data = tomllib.load(manifest_file)
validate_manifest(data)
return Manifest(data)
[docs]
def validate_manifest(data):
"""Raise :class:`ManifestError` when the manifest mapping is invalid.
:param data: parsed widget.toml mapping
:type data: dict
:var field: currently checked required field name
:type field: str
"""
for field in REQUIRED_FIELDS:
if field not in data:
raise ManifestError(f"Manifest missing required field '{field}'.")
if data["origin"] not in VALID_ORIGINS:
raise ManifestError(f"Invalid origin '{data['origin']}'.")
if data["capability"] not in VALID_CAPABILITIES:
raise ManifestError(f"Invalid capability '{data['capability']}'.")
if data["capability"] == "public" and data.get("engine_endpoints"):
raise ManifestError("A public widget must not declare engine_endpoints.")
if data["capability"] == "engine-backed" and not data.get("engine_endpoints"):
raise ManifestError("An engine-backed widget must declare engine_endpoints.")
_validate_required_permission(data["required_permission"])
def _validate_required_permission(required_permission):
"""Raise :class:`ManifestError` when required_permission is malformed.
:param required_permission: integer or ordered band list
:type required_permission: int or list
:var band: currently checked permission band mapping
:type band: dict
"""
if isinstance(required_permission, bool):
raise ManifestError("required_permission must be an int or a band list.")
if isinstance(required_permission, int):
return
if not isinstance(required_permission, list) or not required_permission:
raise ManifestError("required_permission must be an int or a band list.")
for band in required_permission:
if "max_addresses" not in band or "permission" not in band:
raise ManifestError("Each band needs 'max_addresses' and 'permission'.")
[docs]
def required_permission_for_size(required_permission, size):
"""Return the permission integer required for `size` addresses, or None.
None means `size` exceeds the largest band and access must be denied.
:param required_permission: integer or ordered band list
:type required_permission: int or list
:param size: number of resolved Algorand addresses
:type size: int
:var band: currently evaluated permission band mapping
:type band: dict
:return: int
"""
if isinstance(required_permission, int):
return required_permission
for band in required_permission:
if size <= band["max_addresses"]:
return band["permission"]
return None
[docs]
def can_access(profile_permission, required_permission, size):
"""Return whether a user with `profile_permission` may use the widget.
:param profile_permission: the user's permission integer
:type profile_permission: int
:param required_permission: integer or ordered band list from the manifest
:type required_permission: int or list
:param size: number of resolved Algorand addresses
:type size: int
:var required: permission integer needed for the given size, or None
:type required: int
:return: Boolean
"""
required = required_permission_for_size(required_permission, size)
if required is None:
return False
return profile_permission >= required
[docs]
def addresses_limit_for_permission(required_permission, profile_permission):
"""Return the largest address count `profile_permission` qualifies for.
Returns 0 when the permission clears no band (or for a bare-integer gate),
which the historic widget uses to choose between its denial messages.
:param required_permission: integer or ordered band list from the manifest
:type required_permission: int or list
:param profile_permission: the user's permission integer
:type profile_permission: int
:var allowed: largest qualifying max_addresses, defaulting to zero
:type allowed: int
:var band: currently evaluated permission band mapping
:type band: dict
:return: int
"""
if isinstance(required_permission, int):
return 0
allowed = 0
for band in required_permission:
if profile_permission >= band["permission"]:
allowed = band["max_addresses"]
return allowed