Emitting PREMIS Rights Statements for Restricted Collections
Restricted and embargoed cultural-heritage material fails to stay restricted for one recurring reason: the rules that govern access live in a finding aid, a deed of gift, or a curator’s memory, while the software that serves the object knows nothing about them. A donor agreement that seals a personal-papers collection until 2035, a statutory closure on records containing personal data, a licence that permits research viewing but forbids redistribution — each of these is a machine-enforceable constraint expressed as human prose, and prose does not gate a download. This guide, part of the PREMIS Metadata Mapping area of the OAIS architecture, shows how to emit the PREMIS Rights entity so that every access decision is driven by structured, versioned rights metadata rather than by a human reading a note. The goal is a single source of truth the access layer can query at request time, returning allow or deny against a specific act, requester, and date.
Root-Cause Analysis of Access-Control Failures
When a restricted object is served to someone who should never have seen it, or a collection stays dark years after its embargo lapsed, the fault almost always traces to one of three structural gaps between the rights record and the enforcement point.
- Rights recorded only in prose. The restriction exists as free text — “closed for 25 years from date of deposit”, “third-party copyright, seek permission” — in a scope-and-content note or a spreadsheet cell. No system can evaluate it, so enforcement falls to whichever archivist happens to field the request. The rule is real but invisible to the dissemination path that produces a DIP.
- No link between the rights and the object or the act. Even where rights are recorded structurally, they frequently float free of the objects they govern and say nothing about which act they permit. A statement that a collection is “restricted” does not tell the access layer whether it may generate a low-resolution reading-room derivative while forbidding a bulk export, so the system defaults to all-or-nothing and usually errs toward exposure.
- Embargo end-dates that are not machine-readable. The single most common silent failure. An end-date buried in a sentence (“opens twenty-five years after the death of the donor”) cannot be compared to
todayby any scheduler, so embargoes never lift automatically and never trip automatically. Restricted material leaks the day a manual review is skipped; open material stays needlessly dark for years.
The PREMIS Rights entity closes all three gaps at once. It binds a rightsStatement to the objects it governs through a linkingObjectIdentifier, names the legal rightsBasis, and expresses each permission as a rightsGranted block carrying the act, any restriction, and a termOfGrant with explicit start and end dates the access layer can compare against the clock. The flow below traces a single request through that evaluation.
Every gate maps to a field on the rightsGranted block; a request is permitted only when the act is granted, the requester clears any restriction, and the current date sits inside an active term of grant.
Anatomy of the PREMIS Rights Entity
PREMIS models Rights as one of its four semantic units, alongside Object, Event, and Agent. A rightsStatement is the emittable record. Its shape is stable across the Library of Congress PREMIS Data Dictionary: a rightsBasis that names the legal authority, a repeatable rightsGranted block describing what may be done, and linking identifiers that bind the statement to concrete objects and to the agent asserting it. The fields you must populate depend on which basis you assert, because each basis carries its own evidentiary sub-block.
| rightsBasis value | Meaning | Required companion fields |
|---|---|---|
copyright |
Rights flow from copyright law | copyrightStatus, copyrightJurisdiction, copyrightStatusDeterminationDate |
license |
A licence or grant permits use | licenseIdentifier, licenseTerms, licenseNote |
statute |
A statutory closure or mandate applies | statuteJurisdiction, statuteCitation, statuteInformationDeterminationDate |
donor |
A deed of gift or donor agreement governs access | otherRightsBasis = donorAgreement, otherRightsApplicableDates, otherRightsNote |
Every basis, whatever its companion fields, feeds the same rightsGranted block. That block is where enforcement actually lives: an act drawn from a controlled set — disseminate, replicate, migrate, modify, delete — an optional restriction string that narrows who or how, and a termOfGrant carrying startDate and endDate. An open-ended grant sets endDate to OPEN; an embargo sets it to the ISO-8601 date the closure lifts. Because disseminate and delete are distinct acts, a single statement can permit reading-room dissemination while withholding the right to delete, which is exactly the granularity the prose-only approach cannot express. These rights records sit beside the descriptive metadata you already emit when you map Dublin Core to PREMIS for archival objects, completing the object’s provenance with an enforceable access dimension.
Step-by-Step Resolution: Modelling and Evaluating Rights
The resolution has two moving parts. First, model the rightsStatement as a validated data structure so a malformed or half-specified restriction can never be persisted. Second, build an evaluator the access layer calls at request time, which reduces the statement to a single allow-or-deny decision and logs it. The pydantic models below enforce the field-level invariants — a controlled act vocabulary, a term whose end date is either OPEN or a real date, a basis that is one of the four above.
from __future__ import annotations
import logging
from datetime import date
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field, field_validator
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("archival.rights")
class RightsBasis(str, Enum):
COPYRIGHT = "copyright"
LICENSE = "license"
STATUTE = "statute"
DONOR = "donor"
class Act(str, Enum):
DISSEMINATE = "disseminate"
REPLICATE = "replicate"
MIGRATE = "migrate"
MODIFY = "modify"
DELETE = "delete"
class TermOfGrant(BaseModel):
"""Machine-readable start/end of a grant. end_date is None for an open term."""
start_date: date
end_date: Optional[date] = Field(
default=None, description="None means OPEN — no expiry on the grant."
)
@field_validator("end_date")
@classmethod
def end_after_start(cls, v: Optional[date], info) -> Optional[date]:
start = info.data.get("start_date")
if v is not None and start is not None and v < start:
raise ValueError("termOfGrant end_date precedes start_date")
return v
class RightsGranted(BaseModel):
act: Act
restriction: Optional[str] = None
term: TermOfGrant
class RightsStatement(BaseModel):
"""A single PREMIS rightsStatement bound to one or more objects."""
rights_statement_id: str
basis: RightsBasis
granted: list[RightsGranted]
linking_object_ids: list[str] = Field(min_length=1)
linking_agent_id: str
note: Optional[str] = None
With the shape guaranteed, the evaluator implements the three gates from the diagram in order. It never trusts prose: it compares the requested act against the granted acts, tests the requester against the restriction predicate, and checks the request date against the term. Each outcome is logged with structured fields so the decision is auditable long after it is made.
class AccessDecision(BaseModel):
permitted: bool
reason: str
rights_statement_id: Optional[str] = None
def evaluate_disseminate(
statement: RightsStatement,
requester_role: str,
request_date: date,
open_roles: frozenset[str] = frozenset({"reading_room", "staff"}),
) -> AccessDecision:
"""Decide whether `requester_role` may disseminate an object at `request_date`.
Returns the first failing gate as the deny reason, or a permit if all pass.
"""
log_ctx = {
"rights_statement_id": statement.rights_statement_id,
"basis": statement.basis.value,
"requester_role": requester_role,
"request_date": request_date.isoformat(),
}
grant = next(
(g for g in statement.granted if g.act is Act.DISSEMINATE), None
)
if grant is None:
logger.warning("access_denied", extra={**log_ctx, "gate": "act_not_granted"})
return AccessDecision(
permitted=False,
reason="disseminate act is not granted",
rights_statement_id=statement.rights_statement_id,
)
if grant.restriction and requester_role not in open_roles:
logger.warning("access_denied", extra={**log_ctx, "gate": "restriction"})
return AccessDecision(
permitted=False,
reason=f"restriction blocks role '{requester_role}': {grant.restriction}",
rights_statement_id=statement.rights_statement_id,
)
term = grant.term
if request_date < term.start_date:
logger.warning("access_denied", extra={**log_ctx, "gate": "before_term"})
return AccessDecision(
permitted=False,
reason=f"grant not yet active until {term.start_date.isoformat()}",
rights_statement_id=statement.rights_statement_id,
)
if term.end_date is not None and request_date > term.end_date:
logger.warning("access_denied", extra={**log_ctx, "gate": "after_term"})
return AccessDecision(
permitted=False,
reason=f"grant expired on {term.end_date.isoformat()}",
rights_statement_id=statement.rights_statement_id,
)
logger.info("access_permitted", extra={**log_ctx, "gate": "all_passed"})
return AccessDecision(
permitted=True,
reason="disseminate granted, unrestricted for role, within active term",
rights_statement_id=statement.rights_statement_id,
)
A worked embargo makes the behaviour concrete. The statement below asserts a donor agreement that seals a personal-papers collection until 1 January 2035 — the date the public dissemination grant becomes active. The same evaluator denies a request today and permits it automatically once the clock passes that date, with no manual review and no missed opening.
def demo() -> None:
embargo = RightsStatement(
rights_statement_id="rights-mss-0421",
basis=RightsBasis.DONOR,
linking_object_ids=["aip-mss-0421"],
linking_agent_id="agent-collections-dept",
note="Deed of gift: closed to the public until 2035-01-01.",
granted=[
RightsGranted(
act=Act.DISSEMINATE,
restriction=None,
term=TermOfGrant(start_date=date(2035, 1, 1), end_date=None),
)
],
)
today = evaluate_disseminate(embargo, "public", date.today())
after_lift = evaluate_disseminate(embargo, "public", date(2035, 6, 1))
logger.info(
"demo_result",
extra={
"today_permitted": today.permitted,
"after_lift_permitted": after_lift.permitted,
},
)
if __name__ == "__main__":
demo()
Validation and Verification
Confirm the rights layer is doing its job with three independent checks rather than trusting that the models compiled.
- Round-trip the serialization. Emit each
rightsStatementas both PREMIS XML and JSON (statement.model_dump_json()), re-parse it, and assert equality. A statement that does not survive a round-trip cannot be a reliable system of record, and a silent field drop is how an embargo end-date disappears. - Assert the negative and positive paths. For every restricted object, write a test that a
publicrequest today returnspermitted=Falseand that a request dated one day after the embargo end returnspermitted=True. The boundary — the day the term flips — is where manual processes fail and where the evaluator must be provably correct. - Inspect the emitted event. A dissemination that passes should leave a durable trace. Fold the permit decision and
rights_statement_idinto a PREMIS event through Preservation Action Logging, so an auditor can reconstruct why an object was released, not merely that it was. The access layer that consumes these decisions is governed by the wider Digital Preservation Security Policies, which define the roles the evaluator tests against.
Edge Cases and Gotchas
- Death-plus-N and event-relative embargoes. “Opens twenty-five years after the death of the donor” has no fixed date until the triggering event is recorded. Store the term as
OPENwith a machine-readableotherRightsNote, and resolve theendDateonly when the death date is confirmed — never leave the object open by default while the trigger is pending. - Overlapping statements on one object. An item can carry both a
copyrightstatement and adonorembargo. Evaluate every applicable statement and apply the most restrictive outcome; a permit from one basis must not override a deny from another. - Timezone and date-only comparisons. Embargo dates are calendar dates, not timestamps. Compare
datetodate(as above), notdatetime, or a request at 23:00 UTC on the boundary day can flip depending on the server’s zone and release material a day early. - Legacy prose that never had a basis. Retrofitted collections often carry a restriction note with no legal basis attached. Do not guess a
rightsBasis; recordotherRightsBasiswith the original note verbatim and flag the object for a rights review rather than fabricating acopyrightStatus.
Frequently Asked Questions
Why encode rights in PREMIS rather than in an access-control list?
An access-control list records who may act; it does not record why, until when, or on what legal basis. PREMIS Rights carries the basis, the term of grant, and the linking identifiers that bind the rule to the object and the asserting agent, so the same record both drives enforcement and satisfies an auditor asking for provenance. The ACL is derived from the rights statement at request time — it is an output of evaluation, not the source of truth.
How do embargo end-dates become machine-actionable?
By storing them as a termOfGrant with ISO-8601 startDate and endDate values that the evaluator compares against the request date. An open-ended grant uses a null or OPEN end date. Because the comparison is a date operation rather than a human reading a sentence, an embargo lifts automatically the moment the clock passes the end date, and a still-active embargo trips automatically on every request in the meantime.
Which rightsBasis should I use for a donor agreement?
Use donor (recorded through PREMIS’s otherRightsBasis as donorAgreement), not copyright or license. A deed of gift is a contractual restriction between the depositor and the repository, distinct from any copyright the material also carries. Modelling it separately lets an object hold both a donor embargo and a copyright status, which the evaluator resolves by applying the most restrictive outcome.
Can one statement permit viewing but forbid download or deletion?
Yes, and that separation is the point. Each permitted act is a distinct rightsGranted block, so a statement can grant disseminate with a low-resolution restriction while granting no delete act at all. The access layer permits only the acts explicitly granted, so an unlisted act — bulk export, deletion — is denied by default rather than by omission.
Related
- PREMIS Metadata Mapping — the parent area: how PREMIS Object, Event, Agent, and Rights entities are modelled and serialized across the archive.
- How to Map Dublin Core to PREMIS for Archival Objects — the descriptive-to-preservation crosswalk that sits alongside the rights records emitted here.
- Preservation Action Logging — how a permit or deny decision becomes a durable, auditable PREMIS event.