Skip to content

Cache

authkeys.cache

TTL cache for resolved authorized keys.

Entries are keyed by (uid, source_name) and store the serialized keys plus a timestamp. :class:AuthKeysCache applies the TTL; backends only persist and retrieve (value, timestamp) pairs.

Backends are found by name: memory and file are built in, and an installed distribution adds one through the authkeys.caches entry-point group.

CacheEntry = Tuple[str, float] module-attribute

CacheKey = Tuple[str, str] module-attribute

LOGGER = logging.getLogger('authkeys.cache') module-attribute

__all__ = ['AuthKeysCache', 'CacheBackend', 'CacheEntry', 'CacheKey', 'FileCache', 'MemoryCache', 'cache_names', 'get_cache', 'register_cache'] module-attribute

AuthKeysCache(backend, ttl=1, negative_ttl=None, max_stale=None)

TTL cache with a distinct (shorter) TTL for negative (empty) results.

ttl is the positive TTL. negative_ttl (defaults to ttl) bounds a cached "no keys" result so a newly-added key appears sooner. An errored resolution is never stored here -- only genuine results -- so expired_on_error never resurrects an outage as "empty". max_stale (seconds, None for no bound) limits how long past its TTL an entry may still be served with include_expired.

Source code in src/authkeys/cache.py
272
273
274
275
276
277
278
279
280
281
282
def __init__(
    self,
    backend: CacheBackend,
    ttl: int = 1,
    negative_ttl: "Optional[int]" = None,
    max_stale: "Optional[int]" = None,
) -> None:
    self.ttl = ttl
    self.negative_ttl = ttl if negative_ttl is None else negative_ttl
    self.max_stale = max_stale
    self.backend = backend

backend = backend instance-attribute

max_stale = max_stale instance-attribute

negative_ttl = ttl if negative_ttl is None else negative_ttl instance-attribute

ttl = ttl instance-attribute

__getitem__(key)

Source code in src/authkeys/cache.py
315
316
def __getitem__(self, key: CacheKey) -> "Optional[str]":
    return self.get(key, include_expired=False)

__setitem__(key, value)

Source code in src/authkeys/cache.py
318
319
320
def __setitem__(self, key: CacheKey, value: str) -> None:
    # Empty string => negative (known-empty) entry with the negative TTL.
    self.set(key, value, negative=(value == ""))

get(key, *, include_expired=False, ttl=None)

Source code in src/authkeys/cache.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def get(
    self,
    key: CacheKey,
    *,
    include_expired: bool = False,
    ttl: "Optional[int]" = None,
) -> "Optional[str]":
    result = self.backend[key]
    if not result:
        return None
    value, stored_at = result
    is_negative = value == _NEGATIVE_MARKER
    effective = (
        self.negative_ttl if is_negative else (self.ttl if ttl is None else ttl)
    )
    if self._fresh(stored_at, effective) or (
        include_expired
        and (
            self.max_stale is None
            or self._fresh(stored_at, effective + self.max_stale)
        )
    ):
        return "" if is_negative else value
    return None

set(key, value, *, negative=False)

Source code in src/authkeys/cache.py
312
313
def set(self, key: CacheKey, value: str, *, negative: bool = False) -> None:
    self.backend[key] = _NEGATIVE_MARKER if negative else value

CacheBackend

Persist (value, timestamp) pairs keyed by :data:CacheKey.

options names the [cache] keys a backend reads besides the shared ones; any other key is reported as unknown.

Enumeration/deletion (keys/__delitem__/sweep) are optional; a backend that can't support them (e.g. an in-memory one across processes) raises NotImplementedError so the authkeys cache subcommand can report the limitation gracefully.

options = frozenset() class-attribute

__delitem__(key)

Source code in src/authkeys/cache.py
79
80
def __delitem__(self, key: CacheKey) -> None:  # pragma: no cover
    raise NotImplementedError

__getitem__(key)

Source code in src/authkeys/cache.py
70
71
def __getitem__(self, key: CacheKey) -> "Optional[CacheEntry]":  # pragma: no cover
    raise NotImplementedError

__setitem__(key, value)

Source code in src/authkeys/cache.py
73
74
def __setitem__(self, key: CacheKey, value: str) -> None:  # pragma: no cover
    raise NotImplementedError

from_config(options) classmethod

Build the backend from the [cache] keys in :attr:options.

Source code in src/authkeys/cache.py
65
66
67
68
@classmethod
def from_config(cls, options: Mapping[str, Any]) -> "CacheBackend":
    """Build the backend from the ``[cache]`` keys in :attr:`options`."""
    return cls()

keys()

Source code in src/authkeys/cache.py
76
77
def keys(self) -> "Iterable[CacheKey]":  # pragma: no cover
    raise NotImplementedError

sweep(*, max_age=None, max_entries=None)

Evict expired/excess entries; return the number removed.

Source code in src/authkeys/cache.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def sweep(
    self, *, max_age: "Optional[float]" = None, max_entries: "Optional[int]" = None
) -> int:
    """Evict expired/excess entries; return the number removed."""
    removed = 0
    entries = [(k, self[k]) for k in self.keys()]
    entries = [(k, e) for k, e in entries if e]
    if max_age is not None:
        cutoff = time() - max_age
        for k, e in list(entries):
            if e[1] < cutoff:
                del self[k]
                removed += 1
                entries.remove((k, e))
    if max_entries is not None and len(entries) > max_entries:
        # Drop the oldest first.
        entries.sort(key=lambda ke: ke[1][1])
        for k, _e in entries[: len(entries) - max_entries]:
            del self[k]
            removed += 1
    return removed

FileCache(path)

Bases: CacheBackend

Cache keys under <path>/<source>/<uid>; mtime is the timestamp.

On POSIX an entry, its source directory or the root that is not owned by the running user, or that group or others may write, is ignored: whoever can write there could plant keys for any user.

Source code in src/authkeys/cache.py
146
147
148
def __init__(self, path: "str | Path") -> None:
    self.path = Path(path)
    self._warned: "Set[str]" = set()

options = frozenset({'path'}) class-attribute

path = Path(path) instance-attribute

__delitem__(key)

Source code in src/authkeys/cache.py
250
251
252
253
def __delitem__(self, key: CacheKey) -> None:
    cached = self.cached_entry_path(key)
    if cached and cached.exists():
        cached.unlink()

__getitem__(key)

Source code in src/authkeys/cache.py
207
208
209
210
211
212
213
214
215
216
def __getitem__(self, key: CacheKey) -> "Optional[CacheEntry]":
    cached = self.cached_entry_path(key)
    if (
        cached
        and cached.exists()
        and self._trusted(cached.parent)
        and self._trusted(cached)
    ):
        return cached.read_text(), cached.stat().st_mtime
    return None

__setitem__(key, value)

Source code in src/authkeys/cache.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
def __setitem__(self, key: CacheKey, value: str) -> None:
    cached = self.cached_entry_path(key)
    if not cached or not self._trusted(cached.parent):
        return
    # Write atomically (temp file in the same dir + os.replace) so a concurrent
    # reader -- including a separate sshd-spawned process, which the in-process
    # lock does not cover -- never sees a truncated entry.
    import tempfile

    fd, tmp = tempfile.mkstemp(dir=str(cached.parent), prefix=".tmp-")
    try:
        os.write(fd, value.encode("utf-8"))
        os.fsync(fd)
    finally:
        os.close(fd)
    try:
        os.chmod(tmp, 0o600)
    except OSError:
        pass
    os.replace(tmp, str(cached))

cached_entry_path(key)

Return the entry's path, creating its source directory.

Raises ValueError for a uid or source that is not a plain name, and returns None when the cache root is missing or not trusted.

Source code in src/authkeys/cache.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def cached_entry_path(self, key: CacheKey) -> "Optional[Path]":
    """Return the entry's path, creating its source directory.

    Raises ``ValueError`` for a uid or source that is not a plain name, and
    returns ``None`` when the cache root is missing or not trusted.
    """
    uid, src = key
    _check_component("uid", uid)
    _check_component("source", src)
    if not (self.path and self.path.exists()) or not self._trusted(self.path):
        return None
    src_cache = self.path / src
    if not self._within_root(src_cache / uid):
        raise ValueError(f"cache entry escapes the cache root: {key!r}")
    # Cache entries reveal which principals a host trusts -> keep them private.
    src_cache.mkdir(exist_ok=True)
    try:
        src_cache.chmod(0o700)
    except OSError:
        pass
    return src_cache / uid

from_config(options) classmethod

Use the first existing directory of path (a path or a list of them).

Source code in src/authkeys/cache.py
173
174
175
176
177
178
179
180
181
182
183
@classmethod
def from_config(cls, options: Mapping[str, Any]) -> "FileCache":
    """Use the first existing directory of ``path`` (a path or a list of them)."""
    paths = options.get("path", [])
    if isinstance(paths, str):
        paths = [paths]
    for path in paths:
        path = Path(path)
        if path.exists():
            return cls(path)
    raise ConfigError("cache.path names no existing directory for the file cache")

keys()

Source code in src/authkeys/cache.py
239
240
241
242
243
244
245
246
247
248
def keys(self) -> "Iterable[CacheKey]":
    if not (self.path and self.path.exists()):
        return []
    found = []
    for src_dir in self.path.iterdir():
        if src_dir.is_dir():
            for entry in src_dir.iterdir():
                if entry.is_file() and not entry.name.startswith(".tmp-"):
                    found.append((entry.name, src_dir.name))
    return found

MemoryCache()

Bases: CacheBackend

Per-process dictionary; nothing survives the process.

Source code in src/authkeys/cache.py
108
109
def __init__(self) -> None:
    self.db: "dict[CacheKey, CacheEntry]" = {}

db = {} instance-attribute

__delitem__(key)

Source code in src/authkeys/cache.py
120
121
def __delitem__(self, key: CacheKey) -> None:
    self.db.pop(key, None)

__getitem__(key)

Source code in src/authkeys/cache.py
111
112
def __getitem__(self, key: CacheKey) -> "Optional[CacheEntry]":
    return self.db.get(key)

__setitem__(key, value)

Source code in src/authkeys/cache.py
114
115
def __setitem__(self, key: CacheKey, value: str) -> None:
    self.db[key] = (value, time())

keys()

Source code in src/authkeys/cache.py
117
118
def keys(self) -> "Iterable[CacheKey]":
    return list(self.db.keys())

cache_names()

The registered cache backend names, sorted.

Source code in src/authkeys/cache.py
346
347
348
349
def cache_names() -> "Tuple[str, ...]":
    """The registered cache backend names, sorted."""
    _load_plugins()
    return tuple(sorted(_CACHES))

get_cache(name)

The cache backend registered as name; ConfigError listing the names.

Source code in src/authkeys/cache.py
352
353
354
355
356
357
358
359
360
def get_cache(name: str) -> "Type[CacheBackend]":
    """The cache backend registered as ``name``; ``ConfigError`` listing the names."""
    _load_plugins()
    try:
        return _CACHES[name]
    except KeyError:
        raise ConfigError(
            f"unknown cache backend {name!r}; registered: {', '.join(cache_names())}"
        ) from None

register_cache(name, cls)

Register a cache backend under name; a name taken by another class is a ValueError. Returns cls.

Source code in src/authkeys/cache.py
327
328
329
330
331
332
333
def register_cache(name: str, cls: "Type[CacheBackend]") -> "Type[CacheBackend]":
    """Register a cache backend under ``name``; a name taken by another class is a
    ``ValueError``. Returns ``cls``."""
    if _CACHES.get(name, cls) is not cls:
        raise ValueError(f"cache backend {name!r} is already registered")
    _CACHES[name] = cls
    return cls