Skip to content

Commit 744dfcb

Browse files
namedgraphclaude
andcommitted
Enforce ContentMode block-type restriction via SPIN constraint
Add ldh:InvalidContentBlockType matching rdf:_N values whose explicit type is neither ldh:Object nor ldh:XHTML, attached to def:Root, dh:Item, dh:Container. Match rdf:_N by URI shape (REGEX) since RDFS inference isn't enabled in the validation pipeline. The constraint requires `?block a [] .` so it only fires on blocks with an explicit non-Object/XHTML type — untyped/unknown blocks pass. DocumentHierarchyGraphStoreImpl.patch() validates only triples of changed resources, so neighbours' type triples aren't visible; a strict "must be typed Object/XHTML" check would false-positive on every PATCH of an existing Root/Container/Item. Cover with positive and negative HTTP tests for both PUT and PATCH: - PUT-content-blocks.sh accepts well-typed Object+XHTML blocks - PUT-invalid-content-block-422.sh rejects sp:Construct in rdf:_N - PATCH-invalid-content-block-422.sh mirrors the PUT negative on PATCH - PATCH.sh unchanged in intent (untyped insertion remains 204) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent be7c4eb commit 744dfcb

6 files changed

Lines changed: 173 additions & 6 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
5+
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
6+
purge_cache "$END_USER_VARNISH_SERVICE"
7+
purge_cache "$ADMIN_VARNISH_SERVICE"
8+
purge_cache "$FRONTEND_VARNISH_SERVICE"
9+
10+
# add agent to the writers group
11+
12+
add-agent-to-group.sh \
13+
-f "$OWNER_CERT_FILE" \
14+
-p "$OWNER_CERT_PWD" \
15+
--agent "$AGENT_URI" \
16+
"${ADMIN_BASE_URL}acl/groups/writers/"
17+
18+
# PATCH the root with an rdf:_N pointing at a block explicitly typed as sp:Construct
19+
# (neither ldh:Object nor ldh:XHTML). Expected: rejected by ldh:InvalidContentBlockType with 422.
20+
# Use rdf:_99 to avoid colliding with existing rdf:_1..rdf:_8 in the test dataset.
21+
22+
update=$(cat <<EOF
23+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
24+
PREFIX sp: <http://spinrdf.org/sp#>
25+
PREFIX dct: <http://purl.org/dc/terms/>
26+
27+
INSERT
28+
{
29+
<${END_USER_BASE_URL}> rdf:_99 <${END_USER_BASE_URL}#bad-block> .
30+
<${END_USER_BASE_URL}#bad-block> a sp:Construct ;
31+
dct:title "Not a valid content block" ;
32+
sp:text "CONSTRUCT WHERE {}"
33+
}
34+
WHERE
35+
{}
36+
EOF
37+
)
38+
39+
curl -k -w "%{http_code}\n" -o /dev/null -s \
40+
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
41+
-X PATCH \
42+
-H "Content-Type: application/sparql-update" \
43+
"$END_USER_BASE_URL" \
44+
--data-binary "$update" \
45+
| grep -q "$STATUS_UNPROCESSABLE_ENTITY"

http-tests/document-hierarchy/PATCH.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ curl -k -f -s -G \
4343
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
4444
-H "Accept: application/n-triples" \
4545
"$END_USER_BASE_URL" \
46-
| grep "<${END_USER_BASE_URL}#whateverest>" > /dev/null
46+
| grep "<${END_USER_BASE_URL}#whateverest>" > /dev/null
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
5+
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
6+
purge_cache "$END_USER_VARNISH_SERVICE"
7+
purge_cache "$ADMIN_VARNISH_SERVICE"
8+
purge_cache "$FRONTEND_VARNISH_SERVICE"
9+
10+
# add agent to the writers group
11+
12+
add-agent-to-group.sh \
13+
-f "$OWNER_CERT_FILE" \
14+
-p "$OWNER_CERT_PWD" \
15+
--agent "$AGENT_URI" \
16+
"${ADMIN_BASE_URL}acl/groups/writers/"
17+
18+
# create an item with random slug
19+
20+
slug=$(uuidgen | tr '[:upper:]' '[:lower:]')
21+
22+
item=$(create-item.sh \
23+
-f "$AGENT_CERT_FILE" \
24+
-p "$AGENT_CERT_PWD" \
25+
-b "$END_USER_BASE_URL" \
26+
--title "Test item" \
27+
--slug "$slug" \
28+
--container "$END_USER_BASE_URL")
29+
30+
# PUT a body where rdf:_1 points at an ldh:Object and rdf:_2 points at an ldh:XHTML.
31+
# Both are the only types ldh:InvalidContentBlockType accepts. Expected: 200 OK.
32+
33+
curl -k -w "%{http_code}\n" -o /dev/null -f -s \
34+
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
35+
-X PUT \
36+
-H "Accept: application/n-triples" \
37+
-H "Content-Type: application/n-triples" \
38+
--data-binary @- \
39+
"$item" <<EOF \
40+
| grep -q "$STATUS_OK"
41+
<${item}> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://www.w3.org/ns/ldt/document-hierarchy#Item> .
42+
<${item}> <http://purl.org/dc/terms/title> "Test item" .
43+
<${item}> <http://rdfs.org/sioc/ns#has_container> <${END_USER_BASE_URL}> .
44+
<${item}> <http://www.w3.org/1999/02/22-rdf-syntax-ns#_1> <${item}#obj> .
45+
<${item}> <http://www.w3.org/1999/02/22-rdf-syntax-ns#_2> <${item}#xhtml> .
46+
<${item}#obj> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/atomgraph/linkeddatahub#Object> .
47+
<${item}#obj> <http://www.w3.org/1999/02/22-rdf-syntax-ns#value> <${END_USER_BASE_URL}> .
48+
<${item}#xhtml> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/atomgraph/linkeddatahub#XHTML> .
49+
<${item}#xhtml> <http://www.w3.org/1999/02/22-rdf-syntax-ns#value> "<div xmlns=\"http://www.w3.org/1999/xhtml\"/>"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral> .
50+
EOF
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
5+
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
6+
purge_cache "$END_USER_VARNISH_SERVICE"
7+
purge_cache "$ADMIN_VARNISH_SERVICE"
8+
purge_cache "$FRONTEND_VARNISH_SERVICE"
9+
10+
# add agent to the writers group
11+
12+
add-agent-to-group.sh \
13+
-f "$OWNER_CERT_FILE" \
14+
-p "$OWNER_CERT_PWD" \
15+
--agent "$AGENT_URI" \
16+
"${ADMIN_BASE_URL}acl/groups/writers/"
17+
18+
# create an item with random slug
19+
20+
slug=$(uuidgen | tr '[:upper:]' '[:lower:]')
21+
22+
item=$(create-item.sh \
23+
-f "$AGENT_CERT_FILE" \
24+
-p "$AGENT_CERT_PWD" \
25+
-b "$END_USER_BASE_URL" \
26+
--title "Test item" \
27+
--slug "$slug" \
28+
--container "$END_USER_BASE_URL")
29+
30+
# PUT a body where rdf:_1 points at a block typed as something other than ldh:Object/ldh:XHTML
31+
# (here: sp:Construct, a SPARQL query — must be wrapped in ldh:Object to be a valid block).
32+
# Expected: rejected by ldh:InvalidContentBlockType with 422.
33+
34+
curl -k -w "%{http_code}\n" -o /dev/null -s \
35+
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
36+
-X PUT \
37+
-H "Accept: application/n-triples" \
38+
-H "Content-Type: application/n-triples" \
39+
--data-binary @- \
40+
"$item" <<EOF \
41+
| grep -q "$STATUS_UNPROCESSABLE_ENTITY"
42+
<${item}> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://www.w3.org/ns/ldt/document-hierarchy#Item> .
43+
<${item}> <http://purl.org/dc/terms/title> "Test item" .
44+
<${item}> <http://rdfs.org/sioc/ns#has_container> <${END_USER_BASE_URL}> .
45+
<${item}> <http://www.w3.org/1999/02/22-rdf-syntax-ns#_1> <${item}#bad-block> .
46+
<${item}#bad-block> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://spinrdf.org/sp#Construct> .
47+
<${item}#bad-block> <http://purl.org/dc/terms/title> "Not a valid content block" .
48+
<${item}#bad-block> <http://spinrdf.org/sp#text> "CONSTRUCT WHERE {}" .
49+
EOF

src/main/resources/com/atomgraph/linkeddatahub/def.ttl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
:Root a owl:Class ;
2525
rdfs:subClassOf sioc:Space, sioc:Container ;
2626
spin:constructor ldh:TitleConstructor, ldh:DescriptionConstructor, ldh:PrimaryTopicConstructor, ldh:BlockConstructor ;
27-
spin:constraint :MissingTitle ; # roots do not have parents, therefore no ldh:MissingParent
27+
spin:constraint :MissingTitle, ldh:InvalidContentBlockType ; # roots do not have parents, therefore no ldh:MissingParent
2828
rdfs:label "Root" ;
2929
rdfs:isDefinedBy : .
3030

@@ -36,9 +36,9 @@
3636

3737
### EXTERNAL ASSERTIONS
3838

39-
dh:Container spin:constraint :MissingTitle .
39+
dh:Container spin:constraint :MissingTitle, ldh:InvalidContentBlockType .
4040

41-
dh:Item spin:constraint :MissingTitle .
41+
dh:Item spin:constraint :MissingTitle, ldh:InvalidContentBlockType .
4242

4343
# http://spinrdf.org/sp
4444

src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,8 @@ WHERE {
364364
:ConstructPropertyCardinality a sp:Construct ;
365365
sp:text """PREFIX spin: <http://spinrdf.org/spin#>
366366
367-
CONSTRUCT
368-
{
367+
CONSTRUCT
368+
{
369369
_:c0 a spin:ConstraintViolation .
370370
_:c0 spin:violationRoot ?this .
371371
_:c0 spin:violationPath ?arg1 .
@@ -380,6 +380,29 @@ WHERE
380380
rdfs:label "Construct property cardinality" ;
381381
rdfs:isDefinedBy : .
382382

383+
# match rdf:_N by URI shape (RDFS inference is not enabled in the validation pipeline,
384+
# so ?p a rdfs:ContainerMembershipProperty would never match)
385+
:InvalidContentBlockType a sp:Construct ;
386+
sp:text """PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
387+
PREFIX spin: <http://spinrdf.org/spin#>
388+
PREFIX ldh: <https://w3id.org/atomgraph/linkeddatahub#>
389+
390+
CONSTRUCT {
391+
_:cv a spin:ConstraintViolation .
392+
_:cv spin:violationRoot ?this .
393+
_:cv spin:violationPath ?p .
394+
_:cv spin:violationValue ?block .
395+
}
396+
WHERE {
397+
?this ?p ?block .
398+
FILTER (REGEX(STR(?p), \"^http://www\\\\.w3\\\\.org/1999/02/22-rdf-syntax-ns#_[0-9]+$\"))
399+
?block a [] . # only fire when the block is typed in this model; PATCH validation only sees triples of changed resources
400+
FILTER NOT EXISTS { ?block a ldh:Object }
401+
FILTER NOT EXISTS { ?block a ldh:XHTML }
402+
}""" ;
403+
rdfs:label "Invalid content block type" ;
404+
rdfs:isDefinedBy : .
405+
383406
# SPIN TEMPLATES
384407

385408
:MissingPropertyValue a spin:Template ;

0 commit comments

Comments
 (0)