abilian_cdn.policy¶
What a caller may do, how much they may store, and how fast they may write.
policy.auth¶
abilian_cdn.policy.auth ¶
Who is calling, and what they may do.
Authentication happens once, at the request boundary, and produces a
Principal. Everything past that point asks the principal rather than
re-deriving permission from the token row.
Principal
dataclass
¶
An authenticated caller and the limits of what it may do.
may_touch ¶
Whether this principal may address this object path.
Separate from may because it is a different question asked at a
different moment: may is answered once the zone is known, this one
once the path is. A token restricted to releases/ passes the first
and fails the second for index.html.
ZoneUnavailableError ¶
Bases: Exception
A zone this caller cannot have, and why — but not how to say so.
The rule is one rule; the wording is the caller's. The token API answers
404 and 403 in JSON, the S3 face answers NoSuchBucket and AccessDenied
in XML, and neither should be re-deriving who may touch what.
zone_for
async
¶
zone_for(
session: AsyncSession,
slug: str,
principal: Principal,
scope: str,
) -> Zone
The zone this request names, if the caller may do scope to it.
A zone belonging to another organisation is MISSING, not UNSCOPED:
whether a zone exists is not something an unrelated tenant gets to learn
from the difference between two error codes.
narrowed ¶
narrowed(
requested: str, principal: Principal
) -> str | None
The prefix a listing should actually use, or DISJOINT.
A listing is the other half of the prefix restriction: refusing to write
outside releases/ while listing the whole zone still hands over every
path in it. An unrestricted token lists what it asked for; a restricted
one is floored at its own prefix, and one asking about a different branch
entirely is told no rather than quietly given nothing.
authenticate
async
¶
authenticate(
credential: str, session: AsyncSession
) -> Principal
Turn a bearer credential into a principal, or refuse.
Refusals are indistinguishable from each other by design: a malformed credential, an unknown key id, a wrong secret and an expired token all produce the same 401.
policy.quotas¶
abilian_cdn.policy.quotas ¶
How much a tenant may store, and how much it already does.
Soft in the sense that nothing is deleted when a cap is passed, hard at the door: an upload that would go over is refused before the bytes are transferred, because a quota discovered after a 2 GB transfer costs the same as no quota at all.
Egress is deliberately not capped. It could only be enforced on the read path, where the cost is a lookup per request, and the question people actually ask — "how much have we sent this month" — is already answered by the statistics page.
OverQuotaError ¶
Bases: ClientException
An upload that would take the zone or the organisation past its cap.
Usage
dataclass
¶
What one zone holds, and what its organisation holds around it.
Both, because a write is checked against both and the settings page shows both — and because reading them apart means two round trips for one answer.
headroom ¶
How many more bytes may be written, or None where nothing caps it.
replacing is the size of the object about to be overwritten, which
the write frees: without it, re-uploading the same file to a full
zone would be refused, and that is the one case where refusing helps
nobody.
usage
async
¶
usage(session: AsyncSession, zone: Zone) -> Usage
Both totals and both caps, in one query.
refuse_if_over ¶
Raise unless size fits in what is left.
A write that frees at least as much as it takes always fits, whatever the headroom says. Without that, a zone pushed over its cap — by an operator lowering it, or setting one on a zone that already holds more — could never be shrunk back under: replacing a large object with a small one would be refused, and deleting the file would be the only way down.
policy.ratelimit¶
abilian_cdn.policy.ratelimit ¶
A ceiling on how fast one token may write.
Writes only, and per token rather than per address: a write costs an
object-store round trip and a database row, and the caller is already
identified by the time one starts. Reads are left to whatever proxy sits in
front — the delivery path is the one that must not touch shared state per
request, and nginx's limit_req does that better than a Python dict.
A token bucket rather than a counter per window: a deploy that uploads two hundred files in four seconds is normal, a client sending that every second is not, and a fixed window either refuses the first or allows the second.
RateLimited ¶
Bases: ClientException
One token writing faster than its allowance.
Limiter
dataclass
¶
How many writes a token may make per minute.
A full minute's worth may arrive at once and the rest refills steadily, which is one number to explain rather than two: a deploy pushing a thousand files is what the allowance is for, and a client doing it every minute is what it is against.
Zero turns it off, which is what an installation behind its own gateway wants.
ponytail: one bucket table per process. With more than one web process each gets its own allowance — the wrong arithmetic but the right order of magnitude; move the table to Redis if we ever run more than one. The dict is bounded by the number of tokens in the installation, which is small enough that nothing prunes it.
retry_after ¶
Seconds the caller must wait, or 0 when the write may go ahead.
Taking and reporting in one call on purpose: two methods would be two places to forget that asking is itself what spends the allowance.