diff --git a/netengine/spec/__init__.py b/netengine/spec/__init__.py index c4ef1e8..812d590 100644 --- a/netengine/spec/__init__.py +++ b/netengine/spec/__init__.py @@ -1 +1,10 @@ """Spec parsing and models for NetEngine declarative specifications.""" + +from netengine.spec.authority import Authority, AuthorityKind, AuthorityScope, AuthoritySource + +__all__ = [ + "Authority", + "AuthorityKind", + "AuthorityScope", + "AuthoritySource", +] diff --git a/netengine/spec/authority.py b/netengine/spec/authority.py new file mode 100644 index 0000000..d76dcbf --- /dev/null +++ b/netengine/spec/authority.py @@ -0,0 +1,55 @@ +"""Foundational authority primitives for NetEngine specifications.""" + +from enum import Enum +from pydantic import Field + +from netengine.spec.models import SpecModel + + +class AuthorityKind(str, Enum): + """Kinds of authority recognized by NetEngine specs.""" + + WORLD_ROOT = "world_root" + ROOT_NAMING = "root_naming" + NUMBERING = "numbering" + DOMAIN_REGISTRY = "domain_registry" + REGISTRAR = "registrar" + TRUST = "trust" + PLATFORM_IDENTITY = "platform_identity" + INWORLD_IDENTITY = "inworld_identity" + TRANSIT = "transit" + MAIL = "mail" + SERVICE_CATALOG = "service_catalog" + + +class AuthorityScope(str, Enum): + """Scope within which an authority is valid.""" + + WORLD = "world" + PLATFORM = "platform" + INWORLD = "inworld" + ORG = "org" + BOUNDARY = "boundary" + PEER = "peer" + EXTERNAL = "external" + + +class AuthoritySource(str, Enum): + """Origin for an authority definition.""" + + LOCAL = "local" + MIRRORED = "mirrored" + IMPORTED_PEER = "imported_peer" + EXTERNAL = "external" + + +class Authority(SpecModel): + """Foundational model describing who controls a spec authority surface.""" + + id: str = Field(...) + kind: AuthorityKind = Field(...) + scope: AuthorityScope = Field(...) + operator: str = Field(...) + controls: list[str] = Field(...) + description: str | None = Field(default=None) + source: AuthoritySource = Field(default=AuthoritySource.LOCAL) diff --git a/tests/test_authority_spec.py b/tests/test_authority_spec.py new file mode 100644 index 0000000..63ef75e --- /dev/null +++ b/tests/test_authority_spec.py @@ -0,0 +1,47 @@ +"""Tests for foundational authority spec primitives.""" + +import pytest +from pydantic import ValidationError + +from netengine.spec import Authority, AuthorityKind, AuthorityScope, AuthoritySource + + +def test_authority_defaults_to_local_source() -> None: + authority = Authority( + id="world-root", + kind=AuthorityKind.WORLD_ROOT, + scope=AuthorityScope.WORLD, + operator="root-operator", + controls=["dns.root", "pki.root_ca"], + ) + + assert authority.source == AuthoritySource.LOCAL + assert authority.description is None + + +def test_authority_accepts_enum_values() -> None: + authority = Authority( + id="mail-authority", + kind="mail", + scope="org", + operator="mail-ops", + controls=["mx.example.internal"], + source="mirrored", + ) + + assert authority.kind == AuthorityKind.MAIL + assert authority.scope == AuthorityScope.ORG + assert authority.source == AuthoritySource.MIRRORED + + +def test_authority_model_is_frozen() -> None: + authority = Authority( + id="registry-authority", + kind=AuthorityKind.DOMAIN_REGISTRY, + scope=AuthorityScope.INWORLD, + operator="registry-ops", + controls=["domains"], + ) + + with pytest.raises(ValidationError): + authority.operator = "other-operator"