Skip to content

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

may(scope: str, zone: Zone) -> bool

Whether this principal may perform scope on zone.

may_touch

may_touch(path: str) -> bool

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.

provide_principal async

provide_principal(
    request: Request,
    db_session: NamedDependency[AsyncSession],
) -> Principal

Litestar dependency: the caller, or a 401 before the handler runs.

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

headroom(replacing: int = 0) -> int | None

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

refuse_if_over(
    room: int | None, size: int, replacing: int = 0
) -> None

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

retry_after(key: str, now: float | None = None) -> int

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.

check

check(request: Request, principal: Principal) -> None

Spend one write from this token's allowance, or refuse the request.

Called where the principal is built, which is the one place both faces have in common and is before any handler has done anything.