Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ results
site

deploy/compose/cache
deploy/compose/images
deploy/compose/presentation
deploy/compose/source-cache

Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions .github/workflows/github-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
if: github.event.pull_request.merged == true && !contains(github.event.pull_request.title, 'skip-release')
uses: libops/actions/.github/workflows/bump-release.yaml@ef667db8c16533a257d841e75df5c3388152b2d7 # main
with:
workflow_file: build-push.yaml
prefix: v
permissions:
contents: write
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ RUN rm -rf \
&& groupadd --system triplet \
&& useradd --system --gid triplet --uid 100 --home-dir /nonexistent --shell /usr/sbin/nologin triplet

WORKDIR /var/lib/triplet
RUN mkdir -p /var/lib/triplet/cache /var/lib/triplet/testdata/images \
&& chown -R triplet:triplet /var/lib/triplet
COPY --chown=triplet:triplet deploy/compose/images/ /var/lib/triplet/testdata/images/

COPY --from=build /out/triplet /usr/local/bin/triplet
COPY --from=build /out/triplet-healthcheck /usr/local/bin/triplet-healthcheck
COPY config.example.yaml /etc/triplet/config.yaml
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ All image processing is done by [libvips] through [govips].
docker run -p 8080:8080 ghcr.io/libops/triplet:main
```

Then try the bundled sample image:

```bash
curl http://localhost:8080/iiif/3/sample.png/info.json
```

## Documentation

The project documentation lives at <https://libops.github.io/triplet>.
Expand Down
85 changes: 41 additions & 44 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ vips:
- VipsForeignLoadPdf

iiif:
# Optional shared CORS allowlist for IIIF Presentation, Search, Auth, and
# Image unless iiif.image.allowed_origins is set. Entries must be exact
# Optional shared CORS allowlist for IIIF Presentation and Image unless
# iiif.image.allowed_origins is set. Entries must be exact
# origins (`https://viewer.example.edu`) or `*`.
# When empty, no Access-Control-Allow-Origin header is emitted.
# allowed_origins:
Expand All @@ -77,10 +77,13 @@ iiif:
max_source_pixels: 250000000
# Refuse or stop spooling encoded source files larger than this many bytes
# when the source is not already available as a file path. 0 disables.
max_source_bytes: 1073741824
# Refuse encoded derivatives larger than this many bytes after libvips
# export. 0 disables.
max_derivative_bytes: 536870912
max_source_bytes: 1GiB
# Per-request encoded response limit. Refuse one generated derivative if it
# is larger than this many bytes after libvips export. This protects the
# server from returning or caching a single unexpectedly huge response.
# This is not the total cache size; see cache.max_bytes for the aggregate
# filesystem derivative-cache budget. 0 disables.
max_derivative_bytes: 512MiB
# Bound concurrent libvips jobs across image derivatives and info probes.
max_concurrent_transforms: 4
# Advertise additional transform limits in info.json so clients can avoid
Expand All @@ -91,8 +94,14 @@ iiif:
# Cantaloupe behavior. `normalize` converts to sRGB/gray. `none` skips
# profile conversion and strips metadata where the output codec supports it.
color_management: preserve
# `auto` uses random access for region crops and sequential access for
# full/resize requests. You can force sequential or random for profiling.
# How libvips should read source pixels from disk or spooled source files.
# `auto` is the production default: it uses random access for region crops
# and sequential access for full-image or resize requests. Sequential access
# streams forward and can reduce memory and I/O for whole-image reads, but it
# is a poor fit for tile/region workloads that need pixels from arbitrary
# offsets. Random access is better for crops and tiled viewers, but can do
# unnecessary work for simple full-image derivatives. Force `sequential` or
# `random` only when profiling a specific deployment or source format.
load_access: auto
# Cache info.json dimensions by identifier plus source mtime/size.
info_dimension_cache: true
Expand All @@ -119,20 +128,9 @@ iiif:
# Bearer token. Prefer injecting this from the environment.
write_enabled: false
# write_token: ${TRIPLET_PRESENTATION_WRITE_TOKEN}
search:
# IIIF Content Search 2.0 surface. The default backend is a no-op that
# returns an empty AnnotationPage; indexing adapters are future work.
enabled: false
prefix: /search/v2
auth:
# IIIF Authorization Flow API 2.0 surface. No production authorizer is
# built in yet; permit-all must be explicitly enabled for development.
enabled: false
prefix: /auth/v2
development_permit_all: false

# Identifier resolution. Exactly one source must be the default; additional
# sources are selected by identifier scheme (e.g. `https://…`, `gs://…`).
# sources are selected by identifier scheme (e.g. `https://…`).
sources:
default: file
file:
Expand All @@ -148,10 +146,6 @@ sources:
# - prefix: /system/files
# root: /private
# auth_probe: true
# auth_anonymous_cache_ttl: 720h
# auth_authenticated_cache_ttl: 168h
# auth_error_cache_min_age: 5m
# auth_cache_max_entries: 4096
# - prefix: /fedora
# root: /fcrepo
# ocfl: true
Expand All @@ -161,10 +155,8 @@ sources:
# `auth_probe: true` means Triplet forwards browser Cookie/Authorization
# headers to the original URL and requires 200/206 before reading locally.
# Auth probes are tiered: anonymous is checked first and cached separately;
# credentialed probes only run when anonymous access is denied. Long TTLs
# are explicit access-staleness windows; defaults are short when omitted.
# 403/404 probe results are not cached when Last-Modified is newer than
# auth_error_cache_min_age, avoiding permission-publication races.
# credentialed probes only run when anonymous access is denied. Probe
# decisions inherit sources.http.metadata_cache_ttl.
# Optional HTTP(S) source. When configured alongside the file source,
# `http://...` and `https://...` identifiers are routed here automatically.
#
Expand All @@ -180,27 +172,32 @@ sources:
# allowed_origins: [https://islandora-stage.lib.lehigh.edu]
# allow_private_hosts: false
# request_timeout: 2m
# max_bytes: 52428800
# gcs:
# bucket_url: gs://my-bucket
# prefix: images
# max_bytes: 50MiB
# # Optional in-process metadata cache for remote URL identifiers. This lets
# # derivative cache hits reuse recent ETag/Last-Modified/size metadata
# # instead of making a HEAD or range request to the upstream source every
# # time. During this TTL, Triplet may serve a cached derivative without
# # noticing that the remote source changed or disappeared.
# metadata_cache_ttl: 5m

cache:
# Derivative cache. Configure either a filesystem root or a blob bucket URL.
# Derivative cache. Configure a filesystem root.
root: /var/lib/triplet/cache
# bucket_url: gs://triplet-cache
# prefix: derivatives
# Best-effort eviction target for the file cache. 0 disables size-based
# eviction.
max_bytes: 1073741824
# Optional source cache for fetched source bytes (primarily HTTP
# identifiers). Configure either a filesystem root or a blob bucket URL.
# Best-effort aggregate size target for all cached derivative payload files
# under cache.root. This controls retained cache footprint over time, not the
# size of any single generated response. A write may temporarily exceed this
# target before eviction runs, and metadata sidecar files are not counted.
# 0 disables size-based eviction.
max_bytes: 500GiB
# Optional age limit for derivative entries. Expired entries are removed on
# read and opportunistically during writes. 0 disables age-based eviction.
max_age: 720h
# Optional filesystem source cache for fetched source bytes (primarily HTTP
# identifiers).
# source_root: /var/lib/triplet/source-cache
# source_bucket_url: gs://triplet-source-cache
# source_prefix: sources
# Best-effort eviction target for the source cache. 0 disables size-based
# eviction.
source_max_bytes: 1073741824
source_max_bytes: 1GiB
# When non-zero, stale source-cache hits are served immediately while a
# background refresh fetches a fresh copy for later requests.
source_stale_after: 24h
Expand All @@ -210,7 +207,7 @@ extensions:
# → encoded derivative. Same pipeline as the spec routes.
transform:
enabled: true
max_upload_bytes: 52428800 # 50 MiB
max_upload_bytes: 50MiB
# Non-spec endpoint: POST bytes → mints an opaque identifier resolvable
# via the standard /iiif/3/{id}/... routes.
uploads:
Expand Down
17 changes: 5 additions & 12 deletions deploy/compose/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ iiif:
max_output_pixels: 100000000
allow_unsafe_unlimited_output_pixels: false
max_source_pixels: 250000000
max_source_bytes: 1073741824
max_derivative_bytes: 536870912
max_source_bytes: 1GiB
max_derivative_bytes: 512MiB
max_concurrent_transforms: 4
color_management: preserve
load_access: auto
Expand All @@ -32,13 +32,6 @@ iiif:
root: /var/lib/triplet/presentation
write_enabled: ${TRIPLET_PRESENTATION_WRITE_ENABLED}
write_token: "${TRIPLET_PRESENTATION_WRITE_TOKEN}"
search:
enabled: false
prefix: /search/v2
auth:
enabled: false
prefix: /auth/v2
development_permit_all: false

sources:
default: file
Expand All @@ -52,13 +45,13 @@ sources:
# allowed_origins: [https://images.example.org]
# allow_private_hosts: false
# request_timeout: 2m
# max_bytes: 52428800
# max_bytes: 50MiB

cache:
root: /var/lib/triplet/cache
max_bytes: 1073741824
max_bytes: 500GiB
source_root: /var/lib/triplet/source-cache
source_max_bytes: 1073741824
source_max_bytes: 1GiB
source_stale_after: 24h

extensions:
Expand Down
56 changes: 23 additions & 33 deletions docs/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,24 +64,25 @@ sources:
- prefix: /system/files
root: /private
auth_probe: true
auth_anonymous_cache_ttl: 720h
auth_authenticated_cache_ttl: 168h
auth_error_cache_min_age: 5m
auth_cache_max_entries: 4096
http:
allowed_origins:
- https://repository.example.edu
metadata_cache_ttl: 168h
```

The probe answers whether the original source URL would let this request read
the file. Triplet uses that source response as the authority before serving the
local copy or a cached derivative.

Anonymous access is checked first. If the source allows anonymous access,
Triplet caches that anonymous allow decision for the identifier and all callers
can use it until the TTL expires. If anonymous access is denied and the incoming
request has `Cookie` or `Authorization` headers, Triplet checks the source again
with those headers. Credentialed decisions are cached separately by identifier
and by the exact forwarded `Cookie` and `Authorization` header values, so two
different sessions do not share one authenticated auth decision. A repeated
request with the same headers uses the cached decision until its TTL expires.
Anonymous access is checked first. If `sources.http.metadata_cache_ttl` is set
and the source allows anonymous access, Triplet caches that anonymous allow
decision for the identifier and all callers can use it until the TTL expires. If
anonymous access is denied and the incoming request has `Cookie` or
`Authorization` headers, Triplet checks the source again with those headers.
Credentialed decisions are cached separately by identifier and by the exact
forwarded `Cookie` and `Authorization` header values, so two different sessions
do not share one authenticated auth decision. A repeated request with the same
headers uses the cached decision until the TTL expires.

Derivative bytes are shared after authorization. The authorization probe or
auth-cache lookup happens before Triplet serves a derivative-cache hit, but the
Expand Down Expand Up @@ -119,7 +120,7 @@ flowchart TD
deriv -- No --> transform[Transform source] --> store[Store if cacheable] --> serve([Serve derivative])
```

## IIIF Authorization Flow terminology
## Source authorization terminology

Triplet's local URL `auth_probe` is a server-side source authorization check. It
is related to, but not the same thing as, an IIIF Authorization Flow API 2.0
Expand All @@ -137,12 +138,7 @@ Triplet's `auth_probe` uses the same idea internally: before serving a local fil
or a cached derivative, Triplet asks the original source URL what status this
request would receive. The source response remains authoritative.

The IIIF auth service declarations can inform viewers and Presentation API
responses, but they should not be treated as a standalone filesystem bypass
inside the Image API path. A single Image API request does not necessarily carry
the Manifest context that referenced it, manifests can be stale, and multiple
manifests can point at the same image service with different access stories. For
Triplet's local-file shortcut, the safe optimization is still based on the source
For Triplet's local-file shortcut, the safe optimization is based on the source
authorization result:

- If the source allows anonymous access, Triplet can cache that anonymous allow
Expand All @@ -153,11 +149,9 @@ authorization result:

## Auth decision TTLs

Anonymous and authenticated probe decisions default to 5 minutes. Override them
per mapping with `auth_anonymous_cache_ttl` and
`auth_authenticated_cache_ttl`. If either tier-specific value is omitted,
Triplet falls back to `auth_cache_ttl`. If that is also omitted, the tier uses
the 5 minute default.
Anonymous and authenticated probe decisions inherit
`sources.http.metadata_cache_ttl`. Leave the TTL unset or `0` to disable
auth-probe decision caching and recheck the upstream source on every request.

Long TTLs are explicit access-staleness windows. They are appropriate when
repository permissions change rarely or when the mapping is used for content
Expand All @@ -170,14 +164,10 @@ configured TTL. Other upstream errors are not cached.
Negative auth-probe caching is conservative. For 401, 403, and 404 probe
responses, Triplet checks the upstream `Last-Modified` header before caching the
denial. If `Last-Modified` parses and is newer than
`now - auth_error_cache_min_age`, the denial is not cached. This avoids holding a
stale denial while repository access rules or file publication are still
settling. If `Last-Modified` is absent, unparseable, or older than that minimum
age window, the denial can be cached for the configured auth TTL.
`auth_error_cache_min_age` defaults to 5 minutes; increase it when repository
metadata and permissions are known to settle more slowly.

`auth_cache_max_entries` defaults to 4096 entries when omitted.
5 minutes ago, the denial is not cached. This avoids holding a stale denial
while repository access rules or file publication are still settling. If
`Last-Modified` is absent, unparseable, or older than that minimum age window,
the denial can be cached for the configured auth TTL.

The image cache invalidation route also clears matching auth-probe entries when
the source backend supports it.
Expand Down Expand Up @@ -205,5 +195,5 @@ sources:
- https://repository.example.edu
allow_private_hosts: false
request_timeout: 2m
max_bytes: 52428800
max_bytes: 50MiB
```
Loading
Loading