A digital asset is never publishable on the strength of its descriptive record alone. Before an image reaches a public viewer, a museum has to answer a legal question in machine-readable form: who holds rights to this object, under what statement, in which jurisdiction, and until when. This page defines the layer that answers that question automatically. It takes the normalized object records handed over by the core architecture and collection taxonomy layer, resolves each one’s ambiguous rights prose into a canonical URI, computes copyright status against jurisdictional rules, routes embargoed and restricted assets away from open publication, and attaches a conformant rights block to every delivered IIIF manifest. It is the compliance gate the rest of the workflow depends on, and its governing rule is simple: when rights metadata is missing or ambiguous, the pipeline defaults to restricted, never to open.
Three institutional roles meet at this gate. The collections manager needs the guarantee that no asset is exposed beyond the access tier its donor agreement or copyright status permits, and that every routing decision is auditable if a rightsholder ever objects. The Python automation engineer needs an executable rights contract — a model that fails loudly on an unrecognized license string rather than silently publishing an in-copyright work as public domain. The DAMS administrator needs deterministic, idempotent routing so that re-running a batch never double-publishes an embargoed record or races an expiration trigger. This layer is fed validated records from the record ingestion and sync workflows by way of the taxonomy core, and it is the final decision point before an asset becomes visible to an aggregator or a public viewer.
Architecture Overview
The rights layer is a deterministic sequence of gates, not a scoring heuristic dressed up as one. A canonical record enters carrying a rights_tier and a free-text or partially structured rights assertion. Normalization resolves that assertion to a canonical rights URI — a RightsStatements.org statement or a Creative Commons license — using longest-match resolution against a version-controlled registry. Copyright computation applies jurisdictional duration rules to any record whose status is undetermined. A routing gate then dispatches each record by the intersection of its resolved status and its access tier: public-domain and open-licensed assets to publication, embargoed assets to a time-bound controller, ambiguous or unresolved assets to a human review queue. Only records that clear routing reach IIIF manifest generation with a rights block attached. Every arrow below is an explicit decision with a recorded outcome.
The discipline that separates a defensible rights pipeline from a liability is that no stage guesses. Normalization either resolves an assertion to a canonical URI or routes it to review — it never invents a license. Copyright computation runs only against records whose status is genuinely undetermined, and it emits a confidence-scored result, not a boolean it cannot justify. The routing gate treats a missing status as restricted by default. The sections that follow specify the standards each gate honours, the canonical Python that models a rights record, and the workflows that implement each decision.
Standards Landscape
Rights automation is only as trustworthy as the identifiers it emits. A free-text “public domain” note is unenforceable and unaggregatable; a resolvable URI is both. Every record that leaves this layer must carry a rights value drawn from one of the standards below, asserted at the stage where this architecture enforces it. The table maps each standard to its pipeline stage, the version this layer targets, and the role it plays.
| Standard | Version | Enforced at stage | Use in this architecture |
|---|---|---|---|
| RightsStatements.org | 1.0 | Normalization / validation | Canonical URIs for the twelve standardized rights statements (in copyright, no known copyright, undetermined, etc.) asserted on every record before publication |
| Creative Commons licenses | 3.0 / 4.0 | Normalization | Resolving free-text license notes to canonical CC URIs, with strict version disambiguation between 3.0 and 4.0 |
| LIDO | 1.1 | Interchange / mapping | lido:rightsWork and lido:rightsResource carry the rights statement, holder, and credit line in the harvestable envelope |
| IIIF Presentation API | 3.0 | Delivery | The manifest rights property holds a single license or rights-statement URI; access enforcement is delegated to the IIIF Auth API |
| Dublin Core / DCTERMS | DCMI 2020 | Mapping | dc:rights and dcterms:rightsHolder provide the baseline crosswalk for cross-aggregator interoperability |
| Getty ULAN | Linked Open Data (current) | Normalization | Resolving named rightsholders and creators to stable authority URIs for life-plus-term computation |
Version pinning is load-bearing here in a way it is not elsewhere. Creative Commons 3.0 and 4.0 differ in their treatment of moral rights and database rights, so a normalizer that collapses them to a single URI silently mislabels the license — resolution must preserve the exact version string. IIIF 3.0 admits exactly one URI in the rights property and delegates access control entirely to the separate Auth API, so a delivery layer that tries to encode tiered access in the manifest itself produces a payload that fails in a compliant viewer; the canonical rules live in the IIIF Presentation API 3.0 specification. RightsStatements.org URIs are the only values a cross-institutional aggregator will treat as authoritative, so the normalization stage must reject any rights value that is not either a RightsStatements.org URI or a canonical CC URI. LIDO 1.1 element placement for the rights block is defined at lido-schema.org.
Core Implementation Pattern
The canonical task for this layer is the rights record itself: a strict Pydantic v2 model that functions as an executable contract at the normalization gate. It resolves a free-text rights assertion to a canonical URI through a version-controlled registry, refuses to emit a status it cannot justify, and defaults to restricted access whenever the assertion is missing or unrecognized. The model below uses Python 3.9+ syntax (PEP 604 | unions, match) and the Pydantic v2 API (model_config, field_validator, model_dump).
from __future__ import annotations
import re
from datetime import date
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field, field_validator
RIGHTS_URI = re.compile(r"^https?://rightsstatements\.org/vocab/\w+/1\.0/$")
CC_URI = re.compile(r"^https://creativecommons\.org/licenses/[\w-]+/[34]\.0/$")
# Longest-match first so "cc-by-nc-nd" resolves before "cc-by".
LICENSE_REGISTRY: dict[str, str] = {
"cc-by-nc-nd-4.0": "https://creativecommons.org/licenses/by-nc-nd/4.0/",
"cc-by-sa-4.0": "https://creativecommons.org/licenses/by-sa/4.0/",
"cc-by-4.0": "https://creativecommons.org/licenses/by/4.0/",
"public-domain": "http://rightsstatements.org/vocab/NoC-US/1.0/",
"in-copyright": "http://rightsstatements.org/vocab/InC/1.0/",
"undetermined": "http://rightsstatements.org/vocab/UND/1.0/",
}
class AccessTier(str, Enum):
OPEN = "open"
RESTRICTED = "restricted"
EMBARGOED = "embargoed"
class RightsRecord(BaseModel):
"""The rights contract enforced at the normalization gate."""
model_config = ConfigDict(strict=True, extra="forbid")
object_id: str = Field(min_length=1)
rights_assertion: str | None = None # raw free-text note
rights_uri: str | None = None # resolved canonical URI
jurisdiction: str = "US"
creator_death_year: int | None = None # for life-plus-term compute
embargo_until: date | None = None
access_tier: AccessTier = AccessTier.RESTRICTED # conservative default
@field_validator("rights_uri")
@classmethod
def _must_be_canonical(cls, v: str | None) -> str | None:
if v is None:
return None
if not (RIGHTS_URI.match(v) or CC_URI.match(v)):
raise ValueError(f"not a canonical rights URI: {v!r}")
return v.rstrip("/") + "/"
def resolve_rights(record: RightsRecord) -> RightsRecord:
"""Resolve a free-text assertion to a canonical URI and access tier.
Never invents a license: an unrecognized assertion leaves rights_uri
unset and the access_tier at its conservative RESTRICTED default,
which the routing gate treats as 'send to review'.
"""
if record.rights_uri is not None:
return record # already canonical, nothing to resolve
token = (record.rights_assertion or "").strip().lower().replace(" ", "-")
# Longest-match first prevents "cc-by" shadowing "cc-by-nc-nd".
for key in sorted(LICENSE_REGISTRY, key=len, reverse=True):
if key in token:
uri = LICENSE_REGISTRY[key]
tier = AccessTier.OPEN if "creativecommons" in uri else AccessTier.RESTRICTED
return record.model_copy(update={"rights_uri": uri, "access_tier": tier})
return record # unresolved -> stays RESTRICTED, routes to reviewTwo design decisions carry most of the weight. First, strict=True with extra="forbid" means a drifted export cannot smuggle an unexpected field past the gate, and the rights_uri validator pattern-checks every value against RightsStatements.org and Creative Commons so a malformed or non-canonical URI is rejected outright. Second, resolve_rights never fabricates a result: an assertion it cannot match to the registry leaves rights_uri unset and access_tier at its RESTRICTED default, which the routing gate reads as “hold for human review.” Longest-match resolution — sorting registry keys by length before scanning — is what prevents a bare cc-by token from shadowing the more restrictive cc-by-nc-nd license buried in the same free-text note.
Explore the Licensing Workflows
This architecture is implemented across the workflows below, each owning one decision in the flow above.
Determining copyright status. Automating copyright status checks is the deepest treatment of the computation gate: normalizing fragmented rights metadata, applying jurisdictional duration rules — the US rolling publication cutoff, the EU life-plus-seventy calculation — and evaluating explicit RightsStatements.org assertions against institutional policy before any status is emitted. It is the upstream signal that every other workflow in this section depends on, and it covers the field-level crosswalk from RightsStatements.org to collection fields.
Routing open licenses. Routing Creative Commons licenses covers the normalization of free-text license notes into canonical CC URIs with strict 3.0-versus-4.0 disambiguation, thread-safe dead-letter routing for unresolvable inputs, and version-aware dispatch to IIIF and LIDO serializers. It goes deep on automating CC BY-NC-ND tagging in Python for the non-derivative, non-commercial edge cases that most often route incorrectly.
Tuning the public-domain boundary. Threshold tuning for public domain is the decision layer that turns a confidence score over creation dates, life-plus terms, and provenance evidence into a routing outcome — automatic public-domain release, manual review, or fallback. It separates scoring logic from routing logic so legal counsel can adjust the cutoff without a redeploy, and treats orphan works in digital collections as their own conservative branch.
Enforcing time-bound access. Implementing embargo workflows defines the temporal controller for donor-agreement and statutory embargoes: asynchronous evaluators that poll metadata under strict UTC normalization, idempotent publish events that never fire twice, and a state machine that always re-checks rights on expiry rather than auto-publishing. It details setting date-based embargo triggers for the boundary-calculation logic.
Enforcing donor terms. Copyright status is not the only constraint on publication — a gift can carry its own restrictions. Automating donor agreement terms turns deed-of-gift and donor-agreement language into machine-actionable policy, modelling access, credit, and embargo constraints that can override an otherwise-open copyright status, and covers parsing deed-of-gift restrictions from free-text clauses.
State Model & Event-Driven Routing
Rights automation integrates with the delivery layer through an event-driven state machine rather than a synchronous batch. Each record transitions through explicit states — pending_review, rights_resolved, licensed, published, or flagged — and every transition emits an event that downstream consumers act on, such as thumbnail generation or IIIF access-token issuance. Modelling routing as a state machine rather than a chain of function calls is what makes each decision auditable after the fact: the state history is the compliance record.
The resolver evaluates creation dates, authorship metadata, and institutional policy before advancing a record out of pending_review. Records it cannot resolve deterministically transition to flagged and enter a human review queue rather than being forced forward. A record only reaches published after it has been both resolved and licensed, and a flagged record can only rejoin the flow through rights_resolved after manual clearance — there is no path that skips review. This maintains compliance while keeping automated throughput high on the records that resolve cleanly.
Integration Boundaries
This layer is the last gate before publication, and its contracts are what keep rights enforcement decoupled from both modelling and delivery. On the inbound side, the core architecture and collection taxonomy layer hands over a CanonicalObjectRecord carrying a rights_tier and a rights assertion; this layer owns everything from normalization onward. The handoff contract is that the taxonomy core guarantees a well-formed record with a rights assertion present, and this layer guarantees that the record is either resolved to a canonical URI or routed to review — the two never blur. A change to the vocabulary resolver never forces a change to the object schema, and vice versa.
On the outbound side, a resolved record is not delivered raw. Its rights_uri becomes the single value of the IIIF manifest rights property, while its access_tier drives the separate IIIF Auth API service that actually gates the image — the manifest advertises the rights, the auth layer enforces the access. Embargoed records never reach the delivery boundary until their controller fires an expiry event that re-checks rights and only then advances them to published. The result is a single directional flow — canonical record → resolved rights → routed access → IIIF manifest + auth service — where each stage owns exactly one decision and hands a well-defined contract to the next.
Operational Checklist
Before promoting this rights layer to production, confirm every gate below. These are the checks that keep an in-copyright work from ever being published as public domain.
- The
RightsRecordmodel runs withstrict=Trueandextra="forbid", andrights_uriis pattern-validated against both RightsStatements.org and canonical Creative Commons URIs. -
access_tierdefaults torestricted; a record with a missing or unresolved rights assertion is never published open. - Licence resolution uses longest-match-first ordering so a more permissive token never shadows a more restrictive one in the same free-text note.
- Creative Commons resolution preserves the exact 3.0 versus 4.0 version string; the two are never collapsed to one URI.
- Copyright computation runs only against records whose status is genuinely undetermined, emits a confidence-scored result, and routes low-confidence outcomes to review rather than guessing.
- Unresolvable rights assertions route to a review queue with the original payload and a structured reason — never dropped, never defaulted to open.
- Embargo evaluation normalizes all dates to UTC and re-checks rights on expiry before advancing a record to
published. - Publish events are idempotent; re-running a batch never double-publishes or races an expiration trigger.
- The IIIF manifest carries a single
rightsURI; tiered access is enforced through the IIIF Auth API service, not encoded in the manifest. - Every state transition and routing decision is written to an immutable audit log so any rights objection can be reconstructed.
Failure Modes
The failures below recur across institutions running rights automation. Each has a deterministic remediation and a workflow that treats it in depth.
| Failure pattern | Root cause | Remediation |
|---|---|---|
| In-copyright work published as public domain | Missing status defaulted to open instead of restricted | Default access_tier to restricted and route unresolved records to review; see automating copyright status checks |
| Permissive licence overrides a restrictive one | Substring match found cc-by before cc-by-nc-nd |
Sort registry keys longest-first before matching; see automating CC BY-NC-ND tagging in Python |
| Wrong Creative Commons version asserted | Resolver collapsed 3.0 and 4.0 to one URI | Preserve the exact version string in the registry; see routing Creative Commons licenses |
| Orphan work auto-released as public domain | Confidence threshold too permissive for missing author data | Route low-confidence scores to a conservative branch; see handling orphan works in digital collections |
| Embargoed asset exposed before its end date | Evaluation used local time or fired non-idempotently | Normalize to UTC and make publish events idempotent; see setting date-based embargo triggers |
| Rights block missing or unparseable in the manifest | Non-canonical URI or access logic pushed into the manifest | Emit one canonical rights URI and delegate access to the Auth API; see mapping RightsStatements.org to collection fields |
Related
- Automating Copyright Status Checks — jurisdictional status computation
- Routing Creative Commons Licenses — free-text to canonical CC URIs
- Threshold Tuning for Public Domain — confidence-scored routing cutoffs
- Implementing Embargo Workflows — time-bound access control
- Automating Donor Agreement Terms — deed-of-gift restriction policy
- IIIF Image Delivery & Manifest Generation — downstream manifest rights blocks
- Core Architecture & Collection Taxonomy — upstream canonical records