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.

Rights gate — from canonical record to a rights-blocked IIIF manifest A deterministic sequence of gates. A canonical record is normalized to a canonical rights URI, its copyright status is computed when undetermined, and a routing gate dispatches it by the intersection of resolved status and access tier: public-domain and open records publish, embargoed records go to a time-bound controller, and ambiguous or unresolved records go to a human review queue. Only routed records reach the IIIF manifest with a rights block attached, and embargoed records join it only when their controller fires on expiry. Missing or ambiguous rights default to restricted, never to open. Rights gate — every arrow is a recorded decision; the default is restricted Canonical record rights_tier · rights prose Normalize resolve to canonical URI Status determined? Copyright compute jurisdiction + dates Route by tier × status Publish IIIF manifest + rights block Review queue Embargo controller resolved unresolved no yes public domain / open embargoed ambiguous on expiry

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).

python
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 review

Two 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.

Record lifecycle — pending_review to published, with a flagged review branch An event-driven state machine. Each record enters at pending_review; the resolver advances resolvable records to rights_resolved and routes ambiguous ones to flagged. A flagged record can only re-enter the flow through rights_resolved after manual clearance. From rights_resolved a record moves to licensed once a license is assigned, then to published once published, which is terminal. No transition skips resolution or licensing, so the state history itself is the compliance record. Record lifecycle — no path reaches published without resolve + license pending_review rights_resolved licensed published flagged resolver evaluates assign license publish ambiguous after manual review

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 RightsRecord model runs with strict=True and extra="forbid", and rights_uri is pattern-validated against both RightsStatements.org and canonical Creative Commons URIs.
  • access_tier defaults to restricted; 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 rights URI; 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