diff --git a/CLAUDE.md b/CLAUDE.md index 03567b6..26b4685 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,13 +35,13 @@ make install # prompts for base URL, keystore path, keystore password and opt Each `install.sh` sources `lib/ldh-app.sh` and is the app's own sequence of steps: -- `ldh admin make-public` — an authorization that makes the app readable by anyone +- `ldh admin make-public` — grants that make the app readable by anyone, the root and everything below it - `ldh_app_import_ns admin/model` — `ldh admin import ontology --file admin/model/ns.ttl`: installs `ns.ttl` as the namespace ontology, replacing the document and clearing the ontology cache - `ldh push --dir . "$LDH_BASE"` — PUTs every RDF file to the document its path spells and uploads every other file into its folder's document - `ldh_app_import_csv . imports.csv` — one `ldh import csv --wait` per manifest row (`query_filename,csv_filename,target,title`) - app-specific steps: `ldh admin create authorization`, `ldh packages add`, `ldh import rdf` -**Re-runs converge but are not clean.** `ldh push` PUTs and the ontology is reset before re-import, so documents and the model end up the same. `make-public`, `create authorization` and every import are POSTs, so each run adds another authorization and another import record. Nothing probes for an earlier install. +**Re-runs converge but are not clean.** `ldh push` PUTs and the ontology is reset before re-import, so documents and the model end up the same. `make-public` writes the same grants again. `create authorization` and every import are POSTs, so each run adds another authorization and another import record. Nothing probes for an earlier install. ### App layout diff --git a/README.md b/README.md index c3f8d99..e01819e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ cd ../LinkedDataHub/cli && mvn package && export PATH="$PWD/bin:$PATH" `make install` prompts for the base URL, the owner's keystore, its password and an optional proxy URL, exports them as the `LDH_*` variables the CLI reads, and runs the app's `install.sh` — every installer sources the shared [`lib/ldh-app.sh`](lib/ldh-app.sh). To install unattended, export the variables and run `./install.sh`. -__Re-running an install converges but is not clean: `ldh push` replaces each document and the namespace ontology is reset before re-import, but `make-public`, `create authorization` and every import are POSTs, so each run adds another authorization and another import record.__ +__Re-running an install converges but is not clean: `ldh push` replaces each document and the namespace ontology is reset before re-import, and `make-public` writes the same grants again, but `create authorization` and every import are POSTs, so each run adds another authorization and another import record.__ ## Apps diff --git a/demo/copenhagen/install.sh b/demo/copenhagen/install.sh index b9d318b..5ed97c5 100755 --- a/demo/copenhagen/install.sh +++ b/demo/copenhagen/install.sh @@ -4,8 +4,8 @@ # # Reads LDH_BASE, LDH_CERT_FILE, LDH_CERT_PASSWORD and optionally LDH_PROXY; `make install` prompts # for them. Re-running converges on the same documents — PUT replaces each one and the ontology is -# reset before re-import — but make-public and every CSV import are POSTs, so each run adds another -# authorization and another import record. +# reset before re-import, and make-public writes the same grants again — but every CSV import is a +# POST, so each run adds another import record. set -euo pipefail app_dir="$(cd "$(dirname "$0")" && pwd)" diff --git a/demo/northwind-traders/install.sh b/demo/northwind-traders/install.sh index b9d318b..5ed97c5 100755 --- a/demo/northwind-traders/install.sh +++ b/demo/northwind-traders/install.sh @@ -4,8 +4,8 @@ # # Reads LDH_BASE, LDH_CERT_FILE, LDH_CERT_PASSWORD and optionally LDH_PROXY; `make install` prompts # for them. Re-running converges on the same documents — PUT replaces each one and the ontology is -# reset before re-import — but make-public and every CSV import are POSTs, so each run adds another -# authorization and another import record. +# reset before re-import, and make-public writes the same grants again — but every CSV import is a +# POST, so each run adds another import record. set -euo pipefail app_dir="$(cd "$(dirname "$0")" && pwd)" diff --git a/demo/unesco-thesaurus/install.sh b/demo/unesco-thesaurus/install.sh index 0e63776..e0d82ae 100755 --- a/demo/unesco-thesaurus/install.sh +++ b/demo/unesco-thesaurus/install.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Installs the app onto a LinkedDataHub instance with the ldh CLI: makes it public, grants -# authenticated agents read access to its items, pushes the document tree with its files, imports the -# taxonomy editor package and imports the UNESCO Thesaurus SKOS data. The ontology, constructors, +# Installs the app onto a LinkedDataHub instance with the ldh CLI: makes it public, pushes the +# document tree with its files, imports the taxonomy editor package and imports the UNESCO Thesaurus +# SKOS data. The ontology, constructors, # constraints and stylesheet all come from the package. # # Reads LDH_BASE, LDH_CERT_FILE, LDH_CERT_PASSWORD and optionally LDH_PROXY; `make install` prompts -# for them. Re-running converges on the same documents — PUT replaces each one — but make-public, -# the authorization and the RDF import are POSTs, so each run adds another of each. +# for them. Re-running converges on the same documents — PUT replaces each one and make-public +# writes the same grants again — but the RDF import is a POST, so each run adds another. set -euo pipefail app_dir="$(cd "$(dirname "$0")" && pwd)" @@ -16,13 +16,6 @@ ldh_app_require_env ldh_app_step "Creating authorization to make the app public" ldh admin make-public -ldh_app_step "Creating authorization for authenticated agents to read items" -ldh admin create authorization \ - --label "Read access to graph items" \ - --agent-class "http://www.w3.org/ns/auth/acl#AuthenticatedAgent" \ - --to-all-in "https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#Item" \ - --read - ldh_app_step "Pushing documents and files" ldh push --dir "$app_dir" "$LDH_BASE" diff --git a/docs/get-started/setup-1.mp4 b/docs/get-started/setup-1.mp4 index d017512..658e55d 100644 Binary files a/docs/get-started/setup-1.mp4 and b/docs/get-started/setup-1.mp4 differ diff --git a/docs/get-started/setup.ttl b/docs/get-started/setup.ttl index 0289504..2964f17 100644 --- a/docs/get-started/setup.ttl +++ b/docs/get-started/setup.ttl @@ -11,7 +11,7 @@ <#content> a ldh:XHTML ; rdf:value """
Set up LinkedDataHub on your local machine with Docker
- +This section assumes you will be running on your local machine, i.e. localhost. If you intend to run it on a different host, change the system base URI.
diff --git a/docs/install.sh b/docs/install.sh index 15b2432..cd7b3b9 100755 --- a/docs/install.sh +++ b/docs/install.sh @@ -4,8 +4,8 @@ # the sources reference. # # Reads LDH_BASE, LDH_CERT_FILE, LDH_CERT_PASSWORD and optionally LDH_PROXY; `make install` prompts -# for them. Re-running converges: PUT replaces each document. make-public is a POST and adds another -# authorization per run. +# for them. Re-running converges: PUT replaces each document and make-public writes the same grants +# again. set -euo pipefail app_dir="$(cd "$(dirname "$0")" && pwd)" diff --git a/docs/reference/administration/acl.ttl b/docs/reference/administration/acl.ttl index 2ef119b..d2bac98 100644 --- a/docs/reference/administration/acl.ttl +++ b/docs/reference/administration/acl.ttl @@ -14,7 +14,7 @@LinkedDataHub access control is based on the W3C ACL ontology.
There are four access modes (classes of operation) that map to HTTP methods:
+There are three access modes (classes of operation) that map to HTTP methods:
| Add information without removing or replacing any. | POST |
- |
| Control | -Change the authorizations that apply to the resource. | -— (no corresponding HTTP method) | -
|---|
acl:Control grants nothing: authorizations are documents in the administration dataspace,
+ so changing them takes acl:Write there.
By default, only members of the owners group have access to the administration dataspace: its built-in - Full control authorization grants the Control mode (along with read and write) - to that group. See Authorizations.
+ Full control authorization grants them read, append and write access to the administration root and everything + below it. See Authorizations.Note that an agent being a member of one of the above groups does not automatically provide it with an authorization. A valid authorization for the whole group has to be present.
An authorization explicitly grants access for an agent or a group - of agents to access a specific end-user dataspace document or a class - of its documents.
-An agent has to be authorized using the Control mode to be - able to log in to the administration dataspace. By default such an - authorization is granted to the owners group.
+ of agents to a specific document (acl:accessTo), to every document below a container (acl:default),
+ or to the resources of a class (acl:accessToClass). Grants only add access:
+ no authorization overrides another.
+ An authorization counts only in its own document, the one whose foaf:primaryTopic it is, and a group's members count
+ only in the group's own document.
To use the administration dataspace, an agent needs access to its + documents like to any others. By default the owners group has it.
The default authorizations for groups and their respective access modes:
| Group | Read access | Write/append access | -Full control | +Administration dataspace | @@ -99,7 +98,7 @@Owners | Read | Write | -Control | +Read, Append, Write | |||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Append | @@ -145,19 +144,47 @@ --read --append --write -||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
http://www.w3.org/ns/auth/acl#mode |
- One link per access mode the agent holds on the requested document (acl:Read, acl:Append, acl:Write,
- acl:Control); historical versions advertise acl:Read at most |
+ One link per access mode the agent holds on the requested document (acl:Read, acl:Append,
+ acl:Write); historical versions advertise acl:Read at most |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
timemap, timegate, original, self |
diff --git a/docs/reference/http-api/documents.ttl b/docs/reference/http-api/documents.ttl
index e04467d..089d439 100644
--- a/docs/reference/http-api/documents.ttl
+++ b/docs/reference/http-api/documents.ttl
@@ -21,7 +21,8 @@
| Media type not supported | |||||||
POST |
- Appends data to a document | -204 No Content |
+ POST |
+ Appends data to a document | +204 No Content |
400 Bad Request |
RDF syntax error | Media type not supported |
|---|---|---|---|---|---|---|---|
422 Unprocessable Entity |
+ 422 Unprocessable Entity |
Constraint violation in the resulting document | |||||
PUT |
- Upserts a document | -200 OK201 Created308 Permanent Redirect |
+ Body states the sioc:has_parent/sioc:has_container of a resource other than the document |
+ ||||
| Malformed IRI in the body (see below) | +|||||||
PUT |
+ Upserts a document | +204 No Content (replaced)201 Created (created)308 Permanent Redirect |
400 Bad Request |
RDF syntax error | |||
| Malformed document URI | |||||||
409 Conflict |
+ The parent of a document being created does not exist | +||||||
413 Payload Too Large |
Request body too large | @@ -88,13 +99,19 @@Media type not supported | |||||
422 Unprocessable Entity |
+ 422 Unprocessable Entity |
Constraint violation in the resulting document | |||||
DELETE |
- Removes the requested document | -204 No Content |
+ Body states the sioc:has_parent/sioc:has_container of a resource other than the document |
+ ||||
| Malformed IRI in the body (see below) | +|||||||
DELETE |
+ Removes the requested document | +204 No Content |
404 Not Found |
Document with request URI not found | Deleting root, owner, or secretary documents is not allowed | ||
PATCH |
- Modifies a document using SPARQL Update | -204 No Content |
- 422 Unprocessable Entity |
- SPARQL update is not a single graph-scoped INSERT/DELETE operation, or removes an rdf:type |
+ 409 Conflict |
+ The container still has documents: delete them first | +|
PATCH |
+ Modifies a document using SPARQL Update | +204 No Content |
+ 422 Unprocessable Entity |
+ SPARQL update is not a single DELETE/INSERT … WHERE or DELETE WHERE operation, uses GRAPH, WITH, USING or USING NAMED, or removes an rdf:type |
|||
| Constraint violation in the updated document | |||||||
Update changes the document's sioc:has_parent/sioc:has_container, or adds one of another resource |
+ |||||||
| Malformed IRI in the body (see below) | +
Any method may additionally answer 403 Forbidden — see Access control. Any method that writes may answer 428 Precondition Required when the request carries no precondition, or 412 Precondition Failed when the one it carries is out of date — see Conditional writes.
A POST or PATCH answers with the ETag of the document it leaves, so the next write can quote it. A PUT answers with no ETag or Last-Modified: the document it stores is not the payload — the server adds its metadata and skolemizes blank nodes — and RFC 9110 allows a PUT response a validator only for a representation saved unchanged. A client that writes again after a PUT reads the tag with HEAD.
An IRI the IRI syntax does not allow — such as <http://[::1> or one with a space in it — is refused with 422 Unprocessable Entity, whether it comes in an RDF body, an RDF/POST form or a SPARQL update. The response names each value as an ldh:URISyntaxViolation, with the value as typed in ldh:violationValue; an RDF body names the first one only, as its parser stops there. A Turtle or N-Triples body with a space or < in an IRI is answered 400 Bad Request instead, as its tokenizer does not report the value.
Since LinkedDataHub 5, the document hierarchy is managed automatically.
By default, LinkedDataHub treats an RDF document as an item by giving it the dh:Item type and attaching it to the parent container using sioc:has_container. If the client wants to create a container instead, it has to explicitly add the dh:Container type on the document resource; the new container will be attached to its parent using sioc:has_parent. In either case, the URI of the new document will be relative to its parent's.
Containment is server-managed: a POST drops the document's sioc:has_parent/sioc:has_container from its body,
+ a PATCH cannot change them, and no write may state the containment of another resource. acl:default
+ grants rely on it.
LinkedDataHub will also manage additional document metadata, such as its owner and creation/modification timestamps.
For example, this HTTP request creating the Northwind regions container (Turtle syntax):
PUT /regions/ HTTP/1.1
Host: localhost:4443
Content-Type: text/turtle
+If-None-Match: *
@prefix dh: <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
@prefix dct: <http://purl.org/dc/terms/> .
@@ -148,7 +181,8 @@ Content-Type: text/turtle
The HTTP request to produce a new item can be empty:
PUT /regions/1/ HTTP/1.1
Host: localhost:4443
-Content-Type: text/turtle
+Content-Type: text/turtle
+If-None-Match: *
It will create an item document with the following triples:
@prefix dh: <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> . @prefix dct: <http://purl.org/dc/terms/> . @@ -165,10 +199,12 @@ Content-Type: text/turtle
A PATCH applies a SPARQL update scoped to the requested document's graph. The request body must contain a single update operation, in one of two forms: DELETE/INSERT with a WHERE clause (either template can be omitted, giving plain INSERT { } WHERE { } or DELETE { } WHERE { }), or the DELETE WHERE shorthand. Every other form is rejected with 422 Unprocessable Entity, as is a body with more than one operation. Appending a description to the Chai product:
A PATCH applies a SPARQL update scoped to the requested document's graph. The request body must contain a single update operation, in one of two forms: DELETE/INSERT with a WHERE clause (either template can be omitted, giving plain INSERT { } WHERE { } or DELETE { } WHERE { }), or the DELETE WHERE shorthand. Every other form is rejected with 422 Unprocessable Entity, as is a body with more than one operation. Like any write to an existing document, it quotes the document's entity tag (see Conditional writes). Appending a description to the Chai product:
PATCH /products/1/ HTTP/1.1
Host: localhost:4443
+Accept: application/n-triples
Content-Type: application/sparql-update
+If-Match: "1e635b4-ntriples"
PREFIX schema: <https://schema.org/>
@@ -177,11 +213,13 @@ INSERT
<https://localhost:4443/products/1/#this> schema:description "A delicate black tea blend" .
}
WHERE {}
- The update executes against that one graph, so GRAPH patterns are not allowed — this request is rejected with
+
The update executes against that one graph, so it cannot name another: GRAPH is not allowed in a template, in the WHERE pattern (including EXISTS and sub-queries) or in a DELETE WHERE, and neither are WITH, USING and USING NAMED. This request is rejected with
422 Unprocessable Entity:
PATCH /products/1/ HTTP/1.1
Host: localhost:4443
+Accept: application/n-triples
Content-Type: application/sparql-update
+If-Match: "1e635b4-ntriples"
PREFIX schema: <https://schema.org/>
@@ -213,10 +251,10 @@ WHERE {}
A write to a document that already exists must say which state it was written against. Every write reads the document's graph, changes it in memory and writes the whole graph back, so two writers that name no precondition overwrite each other with nothing to show that anything was lost.
Such a write is answered 428 Precondition Required. To make it conditional, send either:
If-Match with the document's current ETag, to modify what you readIf-Match with the document's current ETag, to modify what you read. If-Match: * asserts only that some state exists and does not countIf-None-Match: *, to create a document only if it does not exist yetCreating a document needs no precondition — there is no prior state to have been written against — so a PUT to a URL that does not exist is accepted as it always was.
Creating a document needs no precondition — there is no prior state to have been written against — so a PUT to a URL that does not exist is accepted without one. A PUT that quotes an If-Match to a URL that does not exist is refused 412 Precondition Failed: the state it was written against is gone, and a document deleted in the meantime is not recreated.
If the document changed since you read it, the write is refused 412 Precondition Failed, and that response carries the document's current ETag so you can re-read, reapply your change and retry without an extra request. If-None-Match: * against a document that already exists is likewise refused 412, which is how a client can create-or-append in two steps.
HEAD is answered for any access mode the agent holds, not acl:Read alone, so an agent granted acl:Write or acl:Append can read the validator its writes have to quote without being able to read the document. GET still requires acl:Read.
The command line interface does this for you: every ldh command that writes reads the document's validator first and sends it.
PATCH with a SPARQL update edits it — this is how packages are installed and uninstalled. Editing is restricted to end-user dataspaces: a PATCH on an admin dataspace's settings answers 422 Unprocessable EntityPATCH with a SPARQL update edits it — this is how packages are installed and uninstalled. The update takes the same forms as a document PATCH, and is held to the same conditional write terms: it quotes the settings' ETag in If-Match or is answered 428 Precondition Required, a stale tag is answered 412 Precondition Failed, and a successful update answers 204 No Content with the new ETag. Editing is restricted to end-user dataspaces: a PATCH on an admin dataspace's settings answers 422 Unprocessable EntityA deployment with its own compose file adds the ./packages bind mount at /var/www/linkeddatahub/packages (PACKAGE_ROOT), where copies of imported package stylesheets survive a redeploy.
+Authorizations follow plain WAC, the same in both dataspaces:
+acl:Control grants nothing. The owners' admin role is read, append and write on the administration root and everything below it.acl:default on a container. acl:accessToClass refuses the document
+ classes and the classes above them.lacl:OwnerAgent agent class, granted by a seeded Owner access authorization.foaf:primaryTopic it is, and a group's members only in the group's own document.POST grant in acl/authorizations/public-sparql/.An existing deployment migrates its stores with the two SPARQL updates in the platform's migrations/ directory, which the entrypoint does not
+ run. 6.0-hierarchy.ru.template completes the seeded hierarchy, once per store of each dataspace. 6.0-acl.ru.template rewrites
+ the authorizations, once per admin store. Both are idempotent, and their headers show how to run them. A grant on dh:Container or
+ dh:Item alone has no 6.0 equivalent and is dropped: recreate it with acl:default on the container it was meant for.
The oauth2-login and oauth2-authorize grants move from the administration store to the end-user application's @@ -180,7 +197,7 @@
A write to a document that already exists must be conditional: If-Match with the document's current
- ETag, or If-None-Match: * to create. An unconditional write is answered 428 Precondition Required.
ETag, or If-None-Match: * to create. An unconditional write, or one with If-Match: *, is answered 428 Precondition Required.
A client that wrote unconditionally now reads the tag first, as the ldh CLI does for you. An entity tag
@@ -190,8 +207,11 @@
PATCH already was in 5.10. A POST used to be checked on its body alone and is now checked merged with the document, so
appending to a document that violates a constraint is refused 422 until the violation is repaired. A PUT whose body does
not type the document is checked as the dh:Item the server makes it, so it needs a dct:title.
The access control query fails closed on a typeless resource, so a request for a URL that does not exist is answered 403 rather than
- 404 unless the agent is the owner.
Access to a URL that does not exist matches no authorization, so the request is answered 403 rather than 404, in both
+ dataspaces and for owners too.
Containment is server-managed. DELETE of a container that still has documents is refused 409 Conflict: delete its children
+ first. A POST drops sioc:has_parent/sioc:has_container of the document from its body, a PATCH that
+ changes them is refused 422, and so is any write that states the containment of another resource.
ldh:container leaves the ontology, the ldh:ViewConstructor form and the view query. A view's Create button determines the
container from the view's own solutions.
Re-running the install converges on the same documents instead of duplicating data. PUT replaces each document's description outright, and so does
- ldh admin import ontology --file for the namespace ontology, keeping only the ontology document's own description. (The steps that create rather than replace —
- make-public and each CSV import — are POSTs, so a re-run records another authorization and another import; the documents the imports write are the same.)
POSTs, so a re-run records another import; the documents the imports write are the same.)
ldh admin make-public-
The command extends the admin dataspace's acl/authorizations/public/ document: the built-in Public access authorization gains its access grants, and a second - authorization is added. Authorizations are data, so you can inspect what the command wrote — and delete it to unpublish:
+The command extends the admin dataspace's acl/authorizations/public/ document: the built-in Public access authorization gains its access grants. A second
+ authorization, for queries sent with POST, gets a document of its own, acl/authorizations/public-sparql/. Authorizations are data, so you can
+ inspect what the command wrote — and delete it to unpublish:
@prefix acl: <http://www.w3.org/ns/auth/acl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
-@prefix def: <https://w3id.org/atomgraph/linkeddatahub/default#> .
-@prefix dh: <https://w3id.org/atomgraph/linkeddatahub/document-hierarchy#> .
@prefix nfo: <http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@@ -38,15 +37,17 @@
acl:mode acl:Read ;
acl:agentClass foaf:Agent, # matches unauthenticated requests
acl:AuthenticatedAgent ; # matches authenticated ones
- acl:accessToClass def:Root, dh:Container, dh:Item, nfo:FileDataObject ;
- acl:accessTo <https://localhost:4443/sparql> .
-
-<#sparql-post> a acl:Authorization ; # allows queries over POST
+ acl:accessTo <https://localhost:4443/>, <https://localhost:4443/sparql> ;
+ acl:default <https://localhost:4443/> ; # every document below the root
+ acl:accessToClass nfo:FileDataObject . # file uploads
+ # acl/authorizations/public-sparql/
+<#this> a acl:Authorization ; # allows queries over POST
+ rdfs:label "Public SPARQL queries over POST" ;
acl:mode acl:Append ;
acl:agentClass foaf:Agent, acl:AuthenticatedAgent ;
acl:accessTo <https://localhost:4443/sparql> .
- The first authorization grants acl:Read on every document class — the root, containers, items and file uploads — and on the
- SPARQL endpoint; the second additionally allows POSTed queries.
The first authorization grants acl:Read on the root, on every document below it through acl:default, on file uploads and on the
+ SPARQL endpoint; the second additionally allows POSTed queries. Running the command again writes the same grants.
A file larger than the instance's request limit is refused before it reaches the platform, and the page says so without navigating away.
+A file larger than the instance's upload limit is refused before it reaches the platform, and the page says so without navigating away.
Alternatively, upload through a form: