Skip to content

Policy and context

What a load may do beyond parsing, and what a format or source knows about the load it is part of. See Security for how the policy is applied.

anycfg.Policy(allow_commands=False, allow_templates=False, allow_includes=True, sandbox=True, strict=False, confine_to=None, max_includes=1000, max_depth=64, max_bytes=64 * 1024 * 1024, command_timeout=60.0) dataclass

What a load may do beyond parsing the documents it was given.

The default is the untrusted posture: a document may include other files, but cannot run commands, cannot render templates, and interpolation runs sandboxed. Policy.trusted() turns commands and templates on and the sandbox off, for configuration you wrote yourself.

A document can never change the policy: an include mapping cannot carry any of these fields, and every nested load (an include, a command's output) runs under the policy of the load that reached it.

Subclass it to apply your own trust rules. The hooks :meth:check_path, :meth:check_command and :meth:check_template are asked after the fields have allowed something, so a subclass can only narrow them; returning a falsy value refuses. trusted(), untrusted() and replace() keep the subclass::

class LocalOnly(Policy):
    def check_path(self, path):
        return isinstance(path, pathapi.LocalPath)

Loader(policy=LocalOnly())

Attributes:

Name Type Description
allow_commands bool

Run command sources (cmd://, exec://, sh://, script files) instead of raising :class:~anycfg.exceptions.CommandsDisabledError.

allow_templates bool

Render template sources (app.yaml.j2) instead of raising :class:~anycfg.exceptions.TemplatesDisabledError.

allow_includes bool

Honour !include/!load.

sandbox bool

Render templates and interpolate values in a sandbox that blocks Python internals. A template engine that cannot be sandboxed is refused while this is on.

strict bool

An undefined name in a template or interpolation raises.

confine_to ConfineTo

Roots every local file read must fall inside: a sequence of absolute paths, one os.pathsep-separated string, a path, True for the loader's base_dir, or None/False for none. With None the ANYCFG_CONFINE_TO environment variable is read instead.

max_includes int

Most includes one load may resolve.

max_depth int

Deepest nesting of includes.

max_bytes int

Most bytes one load may read, all sources together.

command_timeout '_ty.Optional[float]'

Seconds a command may run before it and its child processes are killed; None waits indefinitely.

allow_commands = False class-attribute instance-attribute

allow_includes = True class-attribute instance-attribute

allow_templates = False class-attribute instance-attribute

command_timeout = 60.0 class-attribute instance-attribute

confine_to = None class-attribute instance-attribute

max_bytes = 64 * 1024 * 1024 class-attribute instance-attribute

max_depth = 64 class-attribute instance-attribute

max_includes = 1000 class-attribute instance-attribute

sandbox = True class-attribute instance-attribute

strict = False class-attribute instance-attribute

check_command(command)

Whether the command source command may run, once allow_commands is on. command is the text a source names (cmd:// and friends) or a script file's path. A refusal is a CommandsDisabledError.

Source code in src/anycfg/_context.py
116
117
118
119
120
def check_command(self, command: str) -> bool:
    """Whether the command source *command* may run, once ``allow_commands``
    is on. *command* is the text a source names (``cmd://`` and friends) or
    a script file's path. A refusal is a `CommandsDisabledError`."""
    return True

check_path(path)

Whether path, a pathapi Path, may be read.

Asked about every path before it is read: a top-level source, an include, a script file, a glob pattern before it is expanded and each match. A str source, top-level or from a document, is always a local path; any other kind of Path comes only from the caller. A refusal is a ConfinementError. Allows everything by default.

Source code in src/anycfg/_context.py
105
106
107
108
109
110
111
112
113
114
def check_path(self, path: _ty.Any) -> bool:
    """Whether *path*, a pathapi ``Path``, may be read.

    Asked about every path before it is read: a top-level source, an
    include, a script file, a glob pattern before it is expanded and each
    match. A ``str`` source, top-level or from a document, is always a
    local path; any other kind of ``Path`` comes only from the caller. A
    refusal is a `ConfinementError`. Allows everything by default.
    """
    return True

check_template(name, engine)

Whether the template file name may render with the tplx engine class, once allow_templates and the sandbox allow it. A refusal is a TemplatesDisabledError.

Source code in src/anycfg/_context.py
122
123
124
125
126
def check_template(self, name: str, engine: type) -> bool:
    """Whether the template file *name* may render with the tplx *engine*
    class, once ``allow_templates`` and the sandbox allow it. A refusal is a
    `TemplatesDisabledError`."""
    return True

replace(**changes)

A copy with changes applied.

Source code in src/anycfg/_context.py
87
88
89
def replace(self, **changes: _ty.Any) -> "Policy":
    """A copy with *changes* applied."""
    return dataclasses.replace(self, **changes)

trusted(**changes) classmethod

Commands and templates allowed, no sandbox, with changes applied.

Source code in src/anycfg/_context.py
78
79
80
81
82
83
84
85
@classmethod
def trusted(cls, **changes: _ty.Any) -> "Policy":
    """Commands and templates allowed, no sandbox, with *changes* applied."""
    values: _ty.Dict[str, _ty.Any] = dict(
        allow_commands=True, allow_templates=True, sandbox=False
    )
    values.update(changes)
    return cls(**values)

untrusted(**changes) classmethod

The default posture, with changes applied.

Source code in src/anycfg/_context.py
73
74
75
76
@classmethod
def untrusted(cls, **changes: _ty.Any) -> "Policy":
    """The default posture, with *changes* applied."""
    return cls(**changes)

using(**settings) classmethod

This class with settings as class attributes: a mixin carrying its settings, as in AllowedSuffixes.using(suffixes={".yaml"}).

Raises:

Type Description
TypeError

For a setting the class does not declare.

Source code in src/anycfg/_context.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
@classmethod
def using(cls, **settings: _ty.Any) -> "_ty.Type[Policy]":
    """This class with *settings* as class attributes: a mixin carrying its
    settings, as in ``AllowedSuffixes.using(suffixes={".yaml"})``.

    Raises:
        TypeError: For a setting the class does not declare.
    """
    from .policies import compose

    return compose(cls, name=cls.__name__, **settings)

Policy mixins

anycfg.policies

Ready-made Policy mixins, combined by inheritance or with :func:compose.

Each mixin narrows one hook and then asks the next class in line (super()), so any number of them stack: a path must pass every check_path, a command every check_command. Their settings are class attributes, set on the combined class::

from anycfg import Loader, policies

class Production(policies.LocalPathsOnly, policies.AllowedSuffixes,
                 policies.AllowedCommands):
    suffixes = {".yaml", ".json"}
    commands = {"vault read -field=password secret/db"}

loader = Loader(policy=Production(allow_commands=True))

or, without a class statement::

Production = policies.compose(
    policies.LocalPathsOnly, policies.AllowedSuffixes, suffixes={".yaml"}
)

A mixin only ever narrows: the policy's fields (allow_commands, allow_templates, sandbox, confine_to) still decide first.

__all__ = ['LocalPathsOnly', 'AllowedSuffixes', 'AllowedCommands', 'AllowedEngines', 'compose'] module-attribute

AllowedCommands(allow_commands=False, allow_templates=False, allow_includes=True, sandbox=True, strict=False, confine_to=None, max_includes=1000, max_depth=64, max_bytes=64 * 1024 * 1024, command_timeout=60.0) dataclass

Bases: Policy

Run only the commands listed in :attr:commands, compared as written.

allow_commands must still be on; this narrows it to an allowlist. A script file is named by its path.

commands = frozenset() class-attribute

check_command(command)

Source code in src/anycfg/policies.py
88
89
def check_command(self, command: str) -> bool:
    return command in self.commands and super().check_command(command)

AllowedEngines(allow_commands=False, allow_templates=False, allow_includes=True, sandbox=True, strict=False, confine_to=None, max_includes=1000, max_depth=64, max_bytes=64 * 1024 * 1024, command_timeout=60.0) dataclass

Bases: Policy

Render template files only with the tplx engines named in :attr:engines.

allow_templates must still be on, and the sandbox still refuses an engine it cannot hold.

engines = frozenset({'jinja'}) class-attribute

check_template(name, engine)

Source code in src/anycfg/policies.py
102
103
104
105
def check_template(self, name: str, engine: type) -> bool:
    return _engine_name(engine) in self.engines and super().check_template(
        name, engine
    )

AllowedSuffixes(allow_commands=False, allow_templates=False, allow_includes=True, sandbox=True, strict=False, confine_to=None, max_includes=1000, max_depth=64, max_bytes=64 * 1024 * 1024, command_timeout=60.0) dataclass

Bases: Policy

Read only files whose name ends with one of :attr:suffixes.

Compared case-insensitively; a glob pattern is judged by its own last component (*.yaml) and each match by its name.

suffixes = frozenset() class-attribute

check_path(path)

Source code in src/anycfg/policies.py
72
73
74
75
def check_path(self, path: _ty.Any) -> bool:
    name = str(getattr(path, "name", path)).lower()
    allowed = any(name.endswith(suffix.lower()) for suffix in self.suffixes)
    return allowed and super().check_path(path)

LocalPathsOnly(allow_commands=False, allow_templates=False, allow_includes=True, sandbox=True, strict=False, confine_to=None, max_includes=1000, max_depth=64, max_bytes=64 * 1024 * 1024, command_timeout=60.0) dataclass

Bases: Policy

Read only local and in-memory paths: a remote pathapi Path is refused.

A document can only ever name local paths, so this vets the paths the caller passes.

check_path(path)

Source code in src/anycfg/policies.py
56
57
58
59
def check_path(self, path: _ty.Any) -> bool:
    from pathapi.mempath import MemPath

    return isinstance(path, (LocalPath, MemPath)) and super().check_path(path)

compose(*mixins, name='ComposedPolicy', **settings)

A Policy subclass combining mixins, with settings as its class attributes.

compose(LocalPathsOnly, AllowedSuffixes, suffixes={".yaml"}). Each setting must be one a mixin declares.

Raises:

Type Description
TypeError

For a mixin that is not a Policy subclass, or an unknown setting.

Source code in src/anycfg/policies.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def compose(
    *mixins: "_ty.Type[Policy]", name: str = "ComposedPolicy", **settings: _ty.Any
) -> "_ty.Type[Policy]":
    """A `Policy` subclass combining *mixins*, with *settings* as its class attributes.

    ``compose(LocalPathsOnly, AllowedSuffixes, suffixes={".yaml"})``. Each
    setting must be one a mixin declares.

    Raises:
        TypeError: For a mixin that is not a `Policy` subclass, or an unknown
            setting.
    """
    for mixin in mixins:
        if not (isinstance(mixin, type) and issubclass(mixin, Policy)):
            raise TypeError(f"compose() takes Policy subclasses, not {mixin!r}")
    known = {
        attribute
        for mixin in mixins
        for klass in mixin.__mro__
        for attribute in getattr(klass, "__annotations__", {})
        if klass is not Policy and klass is not object
    }
    unknown = sorted(set(settings) - known)
    if unknown:
        raise TypeError(
            f"compose() got unknown setting(s) {', '.join(unknown)}; the mixins "
            f"declare: {', '.join(sorted(known)) or 'none'}"
        )
    return type(name, tuple(mixins) or (Policy,), dict(settings))

anycfg.LoadContext(policy=Policy(), encoding='utf-8', origin=None, name=None, chain=(), trail=(), keep_text=False, _loader=None, _state=None) dataclass

What a format or source knows about the load it is part of.

Formats receive it as context=; a format with includes resolves them through :meth:include, which applies the policy, confinement, the budget and cycle detection.

Attributes:

Name Type Description
policy Policy

The load's :class:Policy.

encoding str

The codec bytes are decoded with, and the default for every include that names none of its own.

origin Any

The path the current document was read from, which relative includes resolve against; None for in-memory content.

name '_ty.Optional[str]'

The current document's name, for messages.

chain '_ty.Tuple[str, ...]'

Identities of the documents being loaded, outermost first.

trail '_ty.Tuple[str, ...]'

Their names, for messages.

keep_text bool

The load builds a type (into=): a format that resolves plain scalars by their text (YAML) keeps that text, as anycfg._scalar.ScalarText, for the type to decide.

chain = () class-attribute instance-attribute

encoding = 'utf-8' class-attribute instance-attribute

keep_text = False class-attribute instance-attribute

name = None class-attribute instance-attribute

origin = None class-attribute instance-attribute

policy = dataclasses.field(default_factory=Policy) class-attribute instance-attribute

trail = () class-attribute instance-attribute

derive(**changes)

A copy with changes applied, sharing the load's counters.

Source code in src/anycfg/_context.py
188
189
190
def derive(self, **changes: _ty.Any) -> "LoadContext":
    """A copy with *changes* applied, sharing the load's counters."""
    return dataclasses.replace(self, **changes)

include(source, *more, **options)

Load source (and more, merged in order) as part of this document.

options are the document-settable include options (encoding, default, flatten, merge, merge_options, recursive, format); the caller filters anything else out.

Raises:

Type Description
ConfigValueError

Without a loader, as for a direct format call.

Source code in src/anycfg/_context.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def include(self, source: _ty.Any, *more: _ty.Any, **options: _ty.Any) -> _ty.Any:
    """Load *source* (and *more*, merged in order) as part of this document.

    *options* are the document-settable include options (``encoding``,
    ``default``, ``flatten``, ``merge``, ``merge_options``, ``recursive``,
    ``format``); the caller filters anything else out.

    Raises:
        ConfigValueError: Without a loader, as for a direct format call.
    """
    if self._loader is None:
        raise ConfigValueError(
            "includes are only available while an anycfg Loader is loading"
        )
    return self._loader.include(self, (source,) + more, options)