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.

CacheEntry = Tuple[str, float] module-attribute

CacheKey = Tuple[str, str] module-attribute

AuthKeysCache(backend, ttl=1, negative_ttl=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".

Source code in src/authkeys/cache.py
171
172
173
174
175
176
177
178
179
def __init__(
    self,
    backend: AuthKeysCacheBackend,
    ttl: int = 1,
    negative_ttl: "Optional[int]" = None,
) -> None:
    self.ttl = ttl
    self.negative_ttl = ttl if negative_ttl is None else negative_ttl
    self.backend = backend

backend = backend 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
206
207
def __getitem__(self, key: CacheKey) -> "Optional[str]":
    return self.get(key, include_expired=False)

__setitem__(key, value)

Source code in src/authkeys/cache.py
209
210
211
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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 include_expired or self._fresh(stored_at, effective):
        return "" if is_negative else value
    return None

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

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

AuthKeysCacheBackend

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

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.

__delitem__(key)

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

__getitem__(key)

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

__setitem__(key, value)

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

from_config(config) classmethod

Source code in src/authkeys/cache.py
27
28
29
@classmethod
def from_config(cls, config) -> "AuthKeysCacheBackend":
    return cls()

keys()

Source code in src/authkeys/cache.py
37
38
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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

AuthKeysCacheFileBackend(path)

Bases: AuthKeysCacheBackend

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

Source code in src/authkeys/cache.py
86
87
def __init__(self, path: "str | Path") -> None:
    self.path = Path(path)

path = Path(path) instance-attribute

__delitem__(key)

Source code in src/authkeys/cache.py
151
152
153
154
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
112
113
114
115
116
def __getitem__(self, key: CacheKey) -> "Optional[CacheEntry]":
    cached = self.cached_entry_path(key)
    if cached and cached.exists():
        return cached.read_text(), cached.stat().st_mtime
    return None

__setitem__(key, value)

Source code in src/authkeys/cache.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def __setitem__(self, key: CacheKey, value: str) -> None:
    cached = self.cached_entry_path(key)
    if not cached:
        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 os
    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)

Source code in src/authkeys/cache.py
 99
100
101
102
103
104
105
106
107
108
109
110
def cached_entry_path(self, key: CacheKey) -> "Optional[Path]":
    uid, src = key
    if not (self.path and self.path.exists()):
        return None
    src_cache = self.path / src
    # 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(config) classmethod

Source code in src/authkeys/cache.py
89
90
91
92
93
94
95
96
97
@classmethod
def from_config(cls, config) -> "AuthKeysCacheFileBackend":
    for path in config.getlist("path", []):
        path = Path(path)
        if path.exists():
            return cls(path)
    raise FileNotFoundError(
        "No existing cache 'path' configured for the file cache backend"
    )

keys()

Source code in src/authkeys/cache.py
140
141
142
143
144
145
146
147
148
149
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

AuthKeysCacheMemBackend()

Bases: AuthKeysCacheBackend

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

db = {} instance-attribute

__delitem__(key)

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

__getitem__(key)

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

__setitem__(key, value)

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

keys()

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