From 926929482a131d8a0682a1d2916d509013640ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 17 Feb 2026 00:04:31 +0100 Subject: [PATCH 01/10] Removed system endpoint resources from default RDF datasets As a result, during the ACL check resources that match `acl:accessTo` authorization(s) are no longer required to have an explicit `rdf:type` --- platform/datasets/admin.trig | 86 +-------------- platform/datasets/end-user.trig | 96 +---------------- platform/entrypoint.sh | 2 +- platform/namespace-ontology.trig.template | 100 ++++++++++++------ .../filter/request/AuthorizationFilter.java | 71 +++++++------ .../com/atomgraph/linkeddatahub/ldh.ttl | 2 +- 6 files changed, 113 insertions(+), 244 deletions(-) diff --git a/platform/datasets/admin.trig b/platform/datasets/admin.trig index 07eac47c0..861864b0c 100644 --- a/platform/datasets/admin.trig +++ b/platform/datasets/admin.trig @@ -1,11 +1,6 @@ @prefix def: . @prefix ldh: . -@prefix ac: . @prefix rdf: . -@prefix xsd: . -@prefix dh: . -@prefix sd: . -@prefix sp: . @prefix sioc: . @prefix foaf: . @prefix dct: . @@ -23,40 +18,6 @@ } -# ENDPOINTS - - -{ - - a foaf:Document ; - dct:title "SPARQL endpoint" . - -} - - -{ - - a foaf:Document ; - dct:title "Namespace endpoint" . - -} - - -{ - - a foaf:Document ; - dct:title "Add data endpoint" . - -} - - -{ - - a foaf:Document ; - dct:title "Generate data endpoint" . - -} - ### ADMIN-SPECIFIC @prefix lacl: . @@ -70,7 +31,7 @@ { - a adm:SignUp ; + a foaf:Document ; dct:title "Sign up" ; rdf:_1 . @@ -288,44 +249,6 @@ WHERE } -# access endpoint - - -{ - - a dh:Item ; - sioc:has_container ; - dct:title "Access description access" ; - foaf:primaryTopic . - - a acl:Authorization ; - rdfs:label "Access description access" ; - rdfs:comment "Allows non-authenticated access" ; - acl:accessToClass ldh:Access ; - acl:mode acl:Read ; - acl:agentClass foaf:Agent, acl:AuthenticatedAgent . - -} - -# access request endpoint - - -{ - - a dh:Item ; - sioc:has_container ; - dct:title "Access request access" ; - foaf:primaryTopic . - - a acl:Authorization ; - rdfs:label "Access request access" ; - rdfs:comment "Allows non-authenticated access" ; - acl:accessToClass ldh:AccessRequest ; - acl:mode acl:Append ; - acl:agentClass foaf:Agent, acl:AuthenticatedAgent . - -} - # sign up @@ -339,8 +262,7 @@ WHERE a acl:Authorization ; rdfs:label "Signup access" ; rdfs:comment "Required to enable public signup" ; - acl:accessTo ; # TO-DO: only allow access by the secretary agent? - acl:accessToClass adm:SignUp ; + acl:accessTo , ; # TO-DO: only allow access by the secretary agent? acl:mode acl:Read, acl:Append ; acl:agentClass foaf:Agent . @@ -359,7 +281,7 @@ WHERE a acl:Authorization ; rdfs:label "OAuth2 login access" ; rdfs:comment "Required to enable public OAuth2 login" ; - acl:accessToClass ldh:OAuthLogin ; + acl:accessToClass , ; acl:mode acl:Read ; acl:agentClass foaf:Agent . @@ -378,7 +300,7 @@ WHERE a acl:Authorization ; rdfs:label "OAuth2 authorization" ; rdfs:comment "Required to enable public OAuth2 login" ; - acl:accessToClass ldh:OAuthAuthorize ; + acl:accessTo , ; acl:mode acl:Read ; acl:agentClass foaf:Agent . diff --git a/platform/datasets/end-user.trig b/platform/datasets/end-user.trig index 65c624610..351174081 100644 --- a/platform/datasets/end-user.trig +++ b/platform/datasets/end-user.trig @@ -1,11 +1,6 @@ @prefix def: . @prefix ldh: . -@prefix ac: . @prefix rdf: . -@prefix xsd: . -@prefix dh: . -@prefix sd: . -@prefix sp: . @prefix sioc: . @prefix foaf: . @prefix dct: . @@ -23,97 +18,10 @@ } -# ENDPOINTS - - -{ - - a foaf:Document ; - dct:title "SPARQL endpoint" . - -} - - -{ - - a foaf:Document ; - dct:title "Namespace endpoint" . - -} - - -{ - - a foaf:Document ; - dct:title "Add data endpoint" . - -} - - -{ - - a foaf:Document ; - dct:title "Generate data endpoint" . - -} - ### END-USER-SPECIFIC - -{ - - a ldh:Access ; - dct:title "Access endpoint" . - -} - - -{ - - a ldh:AccessRequest ; - dct:title "Access request endpoint" . - -} - - -{ - - a ldh:OAuthLogin ; - dct:title "OAuth 2.0 login" . - -} - - -{ - - a ldh:OAuthAuthorize ; - dct:title "Google OAuth2.0 authorization" . - -} - - -{ - - a ldh:OAuthLogin ; - dct:title "ORCID OAuth2.0 login" . - -} - - -{ - - a ldh:OAuthAuthorize ; - dct:title "ORCID OAuth2.0 authorization" . - -} - - -{ - - a foaf:Document ; - dct:title "Settings endpoint" . - -} +@prefix dh: . +@prefix sd: . { diff --git a/platform/entrypoint.sh b/platform/entrypoint.sh index 1fbe571cf..b91be4b64 100755 --- a/platform/entrypoint.sh +++ b/platform/entrypoint.sh @@ -698,7 +698,7 @@ for app in "${apps[@]}"; do namespace_ontology_dataset_path="/var/linkeddatahub/datasets/${app_folder}/namespace-ontology.trig" mkdir -p "$(dirname "$namespace_ontology_dataset_path")" - export end_user_origin admin_origin + export end_user_origin envsubst < namespace-ontology.trig.template > "$namespace_ontology_dataset_path" trig --base="${admin_origin}/" --output=nq "$namespace_ontology_dataset_path" > "/var/linkeddatahub/based-datasets/${app_folder}/namespace-ontology.nq" diff --git a/platform/namespace-ontology.trig.template b/platform/namespace-ontology.trig.template index a3531ccb8..b9042d5a6 100644 --- a/platform/namespace-ontology.trig.template +++ b/platform/namespace-ontology.trig.template @@ -20,10 +20,10 @@ # namespace ontology -<${admin_origin}/ontologies/namespace/> + { - <${admin_origin}/ontologies/namespace/> a dh:Item ; - sioc:has_container <${admin_origin}/ontologies/> ; + a dh:Item ; + sioc:has_container ; dct:title "Namespace" ; foaf:primaryTopic <${end_user_origin}/ns#> . @@ -37,15 +37,15 @@ # public namespace authorization -<${admin_origin}/acl/authorizations/public-namespace/> + { - <${admin_origin}/acl/authorizations/public-namespace/> a dh:Item ; - sioc:has_container <${admin_origin}/acl/authorizations/> ; + a dh:Item ; + sioc:has_container ; dct:title "Public namespace access" ; - foaf:primaryTopic <${admin_origin}/acl/authorizations/public-namespace/#this> . + foaf:primaryTopic . - <${admin_origin}/acl/authorizations/public-namespace/#this> a acl:Authorization ; + a acl:Authorization ; rdfs:label "Public namespace access" ; rdfs:comment "Allows non-authenticated access" ; acl:accessTo <${end_user_origin}/ns> ; # end-user ontologies are public @@ -56,15 +56,15 @@ # SPARQL endpoint authorization -<${admin_origin}/acl/authorizations/sparql-endpoint/> + { - <${admin_origin}/acl/authorizations/sparql-endpoint/> a dh:Item ; - sioc:has_container <${admin_origin}/acl/authorizations/> ; + a dh:Item ; + sioc:has_container ; dct:title "SPARQL endpoint access" ; - foaf:primaryTopic <${admin_origin}/acl/authorizations/sparql-endpoint/#this> . + foaf:primaryTopic . - <${admin_origin}/acl/authorizations/sparql-endpoint/#this> a acl:Authorization ; + a acl:Authorization ; rdfs:label "SPARQL endpoint access" ; rdfs:comment "Allows only authenticated access" ; acl:accessTo <${end_user_origin}/sparql> ; @@ -75,60 +75,98 @@ # write/append authorization -<${admin_origin}/acl/authorizations/write-append/> + { - <${admin_origin}/acl/authorizations/write-append/> a dh:Item ; - sioc:has_container <${admin_origin}/acl/authorizations/> ; + a dh:Item ; + sioc:has_container ; dct:title "Write/append access" ; - foaf:primaryTopic <${admin_origin}/acl/authorizations/write-append/#this> . + foaf:primaryTopic . - <${admin_origin}/acl/authorizations/write-append/#this> a acl:Authorization ; + a acl:Authorization ; rdfs:label "Write/append access" ; rdfs:comment "Allows write access to all documents and containers" ; acl:accessToClass dh:Item, dh:Container, def:Root ; acl:accessTo <${end_user_origin}/sparql>, <${end_user_origin}/importer>, <${end_user_origin}/add>, <${end_user_origin}/generate>, <${end_user_origin}/ns> ; acl:mode acl:Write, acl:Append ; - acl:agentGroup <${admin_origin}/acl/groups/owners/#this>, <${admin_origin}/acl/groups/writers/#this> . + acl:agentGroup , . } # full access authorization -<${admin_origin}/acl/authorizations/full-control/> + { - <${admin_origin}/acl/authorizations/full-control/> a dh:Item ; - sioc:has_container <${admin_origin}/acl/authorizations/> ; + a dh:Item ; + sioc:has_container ; dct:title "Full control" ; - foaf:primaryTopic <${admin_origin}/acl/authorizations/full-control/#this> . + foaf:primaryTopic . - <${admin_origin}/acl/authorizations/full-control/#this> a acl:Authorization ; + a acl:Authorization ; rdfs:label "Full control" ; rdfs:comment "Allows full read/write access to all application resources" ; acl:accessToClass dh:Item, dh:Container, def:Root ; acl:accessTo <${end_user_origin}/sparql>, <${end_user_origin}/importer>, <${end_user_origin}/add>, <${end_user_origin}/generate>, <${end_user_origin}/ns>, <${end_user_origin}/settings> ; acl:mode acl:Read, acl:Append, acl:Write, acl:Control ; - acl:agentGroup <${admin_origin}/acl/groups/owners/#this> . + acl:agentGroup . } # read access -<${admin_origin}/acl/authorizations/read/> + { - <${admin_origin}/acl/authorizations/read/> a dh:Item ; - sioc:has_container <${admin_origin}/acl/authorizations/> ; + a dh:Item ; + sioc:has_container ; dct:title "Read access" ; - foaf:primaryTopic <${admin_origin}/acl/authorizations/read/#this> . + foaf:primaryTopic . - <${admin_origin}/acl/authorizations/read/#this> a acl:Authorization ; + a acl:Authorization ; rdfs:label "Read access" ; rdfs:comment "Allows read access to all resources" ; acl:accessToClass dh:Item, dh:Container, def:Root, ; acl:accessTo <${end_user_origin}/sparql> ; acl:mode acl:Read ; - acl:agentGroup <${admin_origin}/acl/groups/owners/#this>, <${admin_origin}/acl/groups/writers/#this>, <${admin_origin}/acl/groups/readers/#this> . + acl:agentGroup , , . + +} + +# access endpoint + + +{ + + a dh:Item ; + sioc:has_container ; + dct:title "Access description access" ; + foaf:primaryTopic . + + a acl:Authorization ; + rdfs:label "Access description access" ; + rdfs:comment "Allows non-authenticated access" ; + acl:accessTo <${end_user_origin}/access> ; + acl:mode acl:Read ; + acl:agentClass foaf:Agent, acl:AuthenticatedAgent . + +} + +# access request endpoint + + +{ + + a dh:Item ; + sioc:has_container ; + dct:title "Access request access" ; + foaf:primaryTopic . + + a acl:Authorization ; + rdfs:label "Access request access" ; + rdfs:comment "Allows non-authenticated access" ; + acl:accessTo <${end_user_origin}/access/request> ; + acl:mode acl:Append ; + acl:agentClass foaf:Agent, acl:AuthenticatedAgent . } diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java index 2ddbda545..f13102931 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java @@ -157,8 +157,6 @@ public void filter(ContainerRequestContext request) throws IOException public Model authorize(ContainerRequestContext request, Resource agent, Resource accessMode) { Resource accessTo = ResourceFactory.createResource(request.getUriInfo().getAbsolutePath().toString()); - QuerySolutionMap thisQsm = new QuerySolutionMap(); - thisQsm.add(SPIN.THIS_VAR_NAME, accessTo); Model authorizations = ModelFactory.createDefaultModel(); // the agent is the owner of the requested document - automatically grant acl:Read/acl:Append/acl:Write access. @@ -169,46 +167,49 @@ public Model authorize(ContainerRequestContext request, Resource agent, Resource createOwnerAuthorization(authorizations, accessTo, agent); } - ResultSetRewindable docTypesResult = loadResultSet(getApplication().get().getService(), getDocumentTypeQuery(), thisQsm); - try + // special case for PUT requests to non-existing document: allow if the agent has acl:Write acess to the *parent* URI + if (request.getMethod().equals(HttpMethod.PUT) && accessMode.equals(ACL.Write)) { - if (!docTypesResult.hasNext()) // if the document resource has no types, we assume the document does not exist - { - // special case for PUT requests to non-existing document: allow if the agent has acl:Write acess to the *parent* URI - if (request.getMethod().equals(HttpMethod.PUT) && accessMode.equals(ACL.Write)) - { - URI parentURI = URI.create(accessTo.getURI()).resolve(".."); - log.debug("Requested document <{}> not found, falling back to parent URI <{}>", accessTo, parentURI); - accessTo = ResourceFactory.createResource(parentURI.toString()); + URI parentURI = URI.create(accessTo.getURI()).resolve(".."); + Resource parent = ResourceFactory.createResource(parentURI.toString()); + log.debug("Requested document <{}> not found, falling back to parent URI <{}>", parent, parentURI); - thisQsm = new QuerySolutionMap(); - thisQsm.add(SPIN.THIS_VAR_NAME, accessTo); - - docTypesResult.close(); - docTypesResult = loadResultSet(getApplication().get().getService(), getDocumentTypeQuery(), thisQsm); + QuerySolutionMap parentQsm = new QuerySolutionMap(); + parentQsm.add(SPIN.THIS_VAR_NAME, parent); + ResultSetRewindable docTypesResult = loadResultSet(getApplication().get().getService(), getDocumentTypeQuery(), parentQsm); + try + { + Set parentTypes = new HashSet<>(); + docTypesResult.forEachRemaining(qs -> parentTypes.add(qs.getResource("Type"))); - Set parentTypes = new HashSet<>(); - docTypesResult.forEachRemaining(qs -> parentTypes.add(qs.getResource("Type"))); + // only root and containers allow child documents. This needs to be checked before checking ownership + if (Collections.disjoint(parentTypes, Set.of(Default.Root, DH.Container))) return null; - // only root and containers allow child documents. This needs to be checked before checking ownership - if (Collections.disjoint(parentTypes, Set.of(Default.Root, DH.Container))) return null; - docTypesResult.reset(); // rewind result set to the beginning - it's used again later on - - // the agent is the owner of the requested document - automatically grant acl:Read/acl:Append/acl:Write access - if (agent != null && isOwner(accessTo, agent)) - { - log.debug("Agent <{}> is the owner of <{}>, granting acl:Read/acl:Append/acl:Write access", agent, accessTo); - createOwnerAuthorization(authorizations, accessTo, agent); - } + // the agent is the owner of the requested document - automatically grant acl:Read/acl:Append/acl:Write access + if (agent != null && isOwner(parent, agent)) + { + log.debug("Agent <{}> is the owner of <{}>, granting acl:Read/acl:Append/acl:Write access", agent, parent); + createOwnerAuthorization(authorizations, parent, agent); } - // access to non-existing documents is denied if the request method is not PUT *and* the agent has no Write access - else return null; } - + finally + { + docTypesResult.close(); + } + } + + QuerySolutionMap thisQsm = new QuerySolutionMap(); + thisQsm.add(SPIN.THIS_VAR_NAME, accessTo); + ResultSetRewindable docTypesResult = loadResultSet(getApplication().get().getService(), getDocumentTypeQuery(), thisQsm); + try + { ParameterizedSparqlString pss = getApplication().get().canAs(EndUserApplication.class) ? getACLQuery() : getOwnerACLQuery(); - Query query = new SetResultSetValues().apply(pss.asQuery(), docTypesResult); - pss = new ParameterizedSparqlString(query.toString()); // make sure VALUES are now part of the query string - assert pss.toString().contains("VALUES"); + if (docTypesResult.hasNext()) + { + Query query = new SetResultSetValues().apply(pss.asQuery(), docTypesResult); + pss = new ParameterizedSparqlString(query.toString()); // make sure VALUES are now part of the query string + assert pss.toString().contains("VALUES"); + } // note we're not setting the $mode value on the ACL queries as we want to provide the AuthorizationContext with all of the agent's authorizations authorizations.add(loadModel(getAdminService(), pss, new AuthorizationParams(getAdminBase(), accessTo, agent).get())); diff --git a/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl b/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl index caa46a07f..511871d81 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl @@ -657,7 +657,7 @@ sp:Query spin:constructor [ PREFIX xsd: PREFIX sp: PREFIX sd: - PREFIX : + PREFIX : CONSTRUCT { $this rdfs:label [ a xsd:string ] ; From 2236da15977b3689aadb2ccd2a3074f2e2445ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 17 Feb 2026 00:33:29 +0100 Subject: [PATCH 02/10] Prefix fix --- bin/admin/ontologies/{add-construct.sh => add-constructor.sh} | 0 platform/datasets/admin.trig | 1 + 2 files changed, 1 insertion(+) rename bin/admin/ontologies/{add-construct.sh => add-constructor.sh} (100%) diff --git a/bin/admin/ontologies/add-construct.sh b/bin/admin/ontologies/add-constructor.sh similarity index 100% rename from bin/admin/ontologies/add-construct.sh rename to bin/admin/ontologies/add-constructor.sh diff --git a/platform/datasets/admin.trig b/platform/datasets/admin.trig index 861864b0c..112061d34 100644 --- a/platform/datasets/admin.trig +++ b/platform/datasets/admin.trig @@ -22,6 +22,7 @@ @prefix lacl: . @prefix adm: . +@prefix dh: . @prefix rdfs: . @prefix owl: . @prefix acl: . From 3530bea04f7e1662f496418db93d60544557cdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 17 Feb 2026 00:38:17 +0100 Subject: [PATCH 03/10] Another prefix fix --- platform/datasets/admin.trig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/datasets/admin.trig b/platform/datasets/admin.trig index 112061d34..9f3448235 100644 --- a/platform/datasets/admin.trig +++ b/platform/datasets/admin.trig @@ -27,7 +27,7 @@ @prefix owl: . @prefix acl: . @prefix cert: . -@prefix spin: . +@prefix sp: . { From 0fc008f8fd8cf761842eebc976285322127f4756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 17 Feb 2026 01:02:59 +0100 Subject: [PATCH 04/10] Test fixes --- .../{DELETE-non-existing-403.sh => DELETE-404.sh} | 4 ++-- .../{GET-non-existing-403.sh => GET-404.sh} | 4 ++-- .../{POST-non-existing-403.sh => POST-404.sh} | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename http-tests/document-hierarchy/{DELETE-non-existing-403.sh => DELETE-404.sh} (89%) rename http-tests/document-hierarchy/{GET-non-existing-403.sh => GET-404.sh} (85%) rename http-tests/document-hierarchy/{POST-non-existing-403.sh => POST-404.sh} (89%) diff --git a/http-tests/document-hierarchy/DELETE-non-existing-403.sh b/http-tests/document-hierarchy/DELETE-404.sh similarity index 89% rename from http-tests/document-hierarchy/DELETE-non-existing-403.sh rename to http-tests/document-hierarchy/DELETE-404.sh index 25d67b5a9..bbf35a421 100755 --- a/http-tests/document-hierarchy/DELETE-non-existing-403.sh +++ b/http-tests/document-hierarchy/DELETE-404.sh @@ -15,11 +15,11 @@ add-agent-to-group.sh \ --agent "$AGENT_URI" \ "${ADMIN_BASE_URL}acl/groups/writers/" -# check that access to non-existing graph is forbidden +# check that non-existing document is not found curl -k -w "%{http_code}\n" -o /dev/null -s -G \ -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ -X DELETE \ -H "Accept: application/n-triples" \ "${END_USER_BASE_URL}non-existing/" \ -| grep -q "$STATUS_FORBIDDEN" \ No newline at end of file +| grep -q "$STATUS_NOT_FOUND" \ No newline at end of file diff --git a/http-tests/document-hierarchy/GET-non-existing-403.sh b/http-tests/document-hierarchy/GET-404.sh similarity index 85% rename from http-tests/document-hierarchy/GET-non-existing-403.sh rename to http-tests/document-hierarchy/GET-404.sh index 07e1b3d61..de39593de 100755 --- a/http-tests/document-hierarchy/GET-non-existing-403.sh +++ b/http-tests/document-hierarchy/GET-404.sh @@ -15,10 +15,10 @@ add-agent-to-group.sh \ --agent "$AGENT_URI" \ "${ADMIN_BASE_URL}acl/groups/writers/" -# check that access to graph with parent is allowed, but the graph is not found +# check that non-existing document is not found curl -k -w "%{http_code}\n" -o /dev/null -s -G \ -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ -H "Accept: application/n-triples" \ "${END_USER_BASE_URL}non-existing/" \ -| grep -q "$STATUS_FORBIDDEN" \ No newline at end of file +| grep -q "$STATUS_NOT_FOUND" \ No newline at end of file diff --git a/http-tests/document-hierarchy/POST-non-existing-403.sh b/http-tests/document-hierarchy/POST-404.sh similarity index 89% rename from http-tests/document-hierarchy/POST-non-existing-403.sh rename to http-tests/document-hierarchy/POST-404.sh index c5d17178d..dfb8b0bdf 100755 --- a/http-tests/document-hierarchy/POST-non-existing-403.sh +++ b/http-tests/document-hierarchy/POST-404.sh @@ -15,7 +15,7 @@ add-agent-to-group.sh \ --agent "$AGENT_URI" \ "${ADMIN_BASE_URL}acl/groups/writers/" -# check that access to non-existing graph is forbidden +# check that non-existing document is not found ( curl -k -w "%{http_code}\n" -o /dev/null -s \ @@ -27,4 +27,4 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ . EOF ) \ -| grep -q "$STATUS_FORBIDDEN" \ No newline at end of file +| grep -q "$STATUS_NOT_FOUND" \ No newline at end of file From f6e0f7283e54af850e046790360e04b29e76304b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 17 Feb 2026 18:01:05 +0100 Subject: [PATCH 05/10] Test fix --- .../{GET-proxied-403.sh => GET-proxied-404.sh} | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) rename http-tests/proxy/{GET-proxied-403.sh => GET-proxied-404.sh} (61%) diff --git a/http-tests/proxy/GET-proxied-403.sh b/http-tests/proxy/GET-proxied-404.sh similarity index 61% rename from http-tests/proxy/GET-proxied-403.sh rename to http-tests/proxy/GET-proxied-404.sh index 04eadb652..ef695402f 100755 --- a/http-tests/proxy/GET-proxied-403.sh +++ b/http-tests/proxy/GET-proxied-404.sh @@ -17,20 +17,14 @@ add-agent-to-group.sh \ # Test that status codes are correctly proxied through # Generate a random UUID for a non-existing resource -random_uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen) -non_existing_uri="${END_USER_BASE_URL}${random_uuid}/" +uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen) +non_existing_uri="${END_USER_BASE_URL}${uuid}/" # Attempt to proxy a non-existing document on the END_USER_BASE_URL -# This should return 403 Forbidden (not found resources return 403 in LinkedDataHub) -http_status=$(curl -k -s -o /dev/null -w "%{http_code}" \ +curl -k -s -o /dev/null -w "%{http_code}" \ -G \ -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ -H 'Accept: application/n-triples' \ --data-urlencode "uri=${non_existing_uri}" \ - "$END_USER_BASE_URL" || true) - -# Verify that the proxied status code matches the backend status code (403) -if [ "$http_status" != "403" ]; then - echo "Expected HTTP 403 Forbidden for non-existing proxied document, got: $http_status" - exit 1 -fi + "$END_USER_BASE_URL" \ +| grep -q "$STATUS_NOT_FOUND" From 7661a2e31198e48accedaa2a545309d5b38cac89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 17 Feb 2026 21:28:16 +0100 Subject: [PATCH 06/10] URI resolution fix in `AuthorizationFilter` Test fixes --- ...PATCH-non-existing-403.sh => PATCH-404.sh} | 2 +- .../document-hierarchy/PATCH-empty-item.sh | 2 +- http-tests/document-hierarchy/POST-404.sh | 2 +- .../document-hierarchy/PUT-no-slash-308.sh | 24 ++++++++++++++++--- .../filter/request/AuthorizationFilter.java | 6 +++-- 5 files changed, 28 insertions(+), 8 deletions(-) rename http-tests/document-hierarchy/{PATCH-non-existing-403.sh => PATCH-404.sh} (97%) diff --git a/http-tests/document-hierarchy/PATCH-non-existing-403.sh b/http-tests/document-hierarchy/PATCH-404.sh similarity index 97% rename from http-tests/document-hierarchy/PATCH-non-existing-403.sh rename to http-tests/document-hierarchy/PATCH-404.sh index 6bc4ecfc5..c8055110b 100755 --- a/http-tests/document-hierarchy/PATCH-non-existing-403.sh +++ b/http-tests/document-hierarchy/PATCH-404.sh @@ -37,4 +37,4 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ "${END_USER_BASE_URL}non-existing/" \ --data-binary "$update" ) \ -| grep -q "$STATUS_FORBIDDEN" \ No newline at end of file +| grep -q "$STATUS_NOT_FOUND" \ No newline at end of file diff --git a/http-tests/document-hierarchy/PATCH-empty-item.sh b/http-tests/document-hierarchy/PATCH-empty-item.sh index 26737efee..5f45ac0a0 100755 --- a/http-tests/document-hierarchy/PATCH-empty-item.sh +++ b/http-tests/document-hierarchy/PATCH-empty-item.sh @@ -55,4 +55,4 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ -H "Accept: application/n-triples" \ "$item" \ -| grep -q "$STATUS_FORBIDDEN" +| grep -q "$STATUS_NOT_FOUND" diff --git a/http-tests/document-hierarchy/POST-404.sh b/http-tests/document-hierarchy/POST-404.sh index dfb8b0bdf..2cf0fccf9 100755 --- a/http-tests/document-hierarchy/POST-404.sh +++ b/http-tests/document-hierarchy/POST-404.sh @@ -27,4 +27,4 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ . EOF ) \ -| grep -q "$STATUS_NOT_FOUND" \ No newline at end of file +| grep -q "$STATUS_NOT_FOUND" diff --git a/http-tests/document-hierarchy/PUT-no-slash-308.sh b/http-tests/document-hierarchy/PUT-no-slash-308.sh index f4507ede9..9ce59fd6c 100755 --- a/http-tests/document-hierarchy/PUT-no-slash-308.sh +++ b/http-tests/document-hierarchy/PUT-no-slash-308.sh @@ -7,6 +7,24 @@ purge_cache "$END_USER_VARNISH_SERVICE" purge_cache "$ADMIN_VARNISH_SERVICE" purge_cache "$FRONTEND_VARNISH_SERVICE" +# add agent to the writers group + +add-agent-to-group.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + --agent "$AGENT_URI" \ + "${ADMIN_BASE_URL}acl/groups/writers/" + +# create test container + +container=$(create-container.sh \ + -f "$AGENT_CERT_FILE" \ + -p "$AGENT_CERT_PWD" \ + -b "$END_USER_BASE_URL" \ + --title "Test Container" \ + --slug "test-container" \ + --parent "$END_USER_BASE_URL") + # add an explicit read/write authorization for the parent since the child document will inherit it create-authorization.sh \ @@ -15,14 +33,14 @@ create-authorization.sh \ -p "$OWNER_CERT_PWD" \ --label "Write base" \ --agent "$AGENT_URI" \ - --to "$END_USER_BASE_URL" \ + --to "$container" \ --read \ --write -invalid_item="${END_USER_BASE_URL}no-slash" - # check URI without trailing slash gets redirected +invalid_item="${container}no-slash" + ( curl -k -w "%{http_code}\n" -o /dev/null -s \ -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java index f13102931..5063ccc86 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java @@ -43,7 +43,7 @@ import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.container.PreMatching; import jakarta.ws.rs.core.Response; -import java.net.URI; +import org.apache.jena.irix.IRIx; import java.util.HashSet; import java.util.Set; import org.apache.jena.query.ParameterizedSparqlString; @@ -170,7 +170,9 @@ public Model authorize(ContainerRequestContext request, Resource agent, Resource // special case for PUT requests to non-existing document: allow if the agent has acl:Write acess to the *parent* URI if (request.getMethod().equals(HttpMethod.PUT) && accessMode.equals(ACL.Write)) { - URI parentURI = URI.create(accessTo.getURI()).resolve(".."); + // Use Jena's IRIx for RFC 3986-compliant resolution - java.net.URI.resolve("..") is non-compliant + // (RFC 3986 section 5.2.4 step 2D requires ".." to be removed, but java.net.URI leaves it literal) + IRIx parentURI = IRIx.create(accessTo.getURI()).resolve(".."); Resource parent = ResourceFactory.createResource(parentURI.toString()); log.debug("Requested document <{}> not found, falling back to parent URI <{}>", parent, parentURI); From feb105167a2ddf56f51948c605f8932c4fd30f7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 17 Feb 2026 21:54:42 +0100 Subject: [PATCH 07/10] Test fix --- .../document-hierarchy/PUT-no-slash-308.sh | 14 +--- .../filter/request/AuthorizationFilter.java | 66 ++++++++++--------- 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/http-tests/document-hierarchy/PUT-no-slash-308.sh b/http-tests/document-hierarchy/PUT-no-slash-308.sh index 9ce59fd6c..e0b6ae1ce 100755 --- a/http-tests/document-hierarchy/PUT-no-slash-308.sh +++ b/http-tests/document-hierarchy/PUT-no-slash-308.sh @@ -15,16 +15,6 @@ add-agent-to-group.sh \ --agent "$AGENT_URI" \ "${ADMIN_BASE_URL}acl/groups/writers/" -# create test container - -container=$(create-container.sh \ - -f "$AGENT_CERT_FILE" \ - -p "$AGENT_CERT_PWD" \ - -b "$END_USER_BASE_URL" \ - --title "Test Container" \ - --slug "test-container" \ - --parent "$END_USER_BASE_URL") - # add an explicit read/write authorization for the parent since the child document will inherit it create-authorization.sh \ @@ -33,13 +23,13 @@ create-authorization.sh \ -p "$OWNER_CERT_PWD" \ --label "Write base" \ --agent "$AGENT_URI" \ - --to "$container" \ + --to "$END_USER_BASE_URL" \ --read \ --write # check URI without trailing slash gets redirected -invalid_item="${container}no-slash" +invalid_item="${END_USER_BASE_URL}no-slash" ( curl -k -w "%{http_code}\n" -o /dev/null -s \ diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java index 5063ccc86..c2a3968e6 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java @@ -167,49 +167,51 @@ public Model authorize(ContainerRequestContext request, Resource agent, Resource createOwnerAuthorization(authorizations, accessTo, agent); } - // special case for PUT requests to non-existing document: allow if the agent has acl:Write acess to the *parent* URI - if (request.getMethod().equals(HttpMethod.PUT) && accessMode.equals(ACL.Write)) + QuerySolutionMap thisQsm = new QuerySolutionMap(); + thisQsm.add(SPIN.THIS_VAR_NAME, accessTo); + ResultSetRewindable docTypesResult = loadResultSet(getApplication().get().getService(), getDocumentTypeQuery(), thisQsm); + try { - // Use Jena's IRIx for RFC 3986-compliant resolution - java.net.URI.resolve("..") is non-compliant - // (RFC 3986 section 5.2.4 step 2D requires ".." to be removed, but java.net.URI leaves it literal) - IRIx parentURI = IRIx.create(accessTo.getURI()).resolve(".."); - Resource parent = ResourceFactory.createResource(parentURI.toString()); - log.debug("Requested document <{}> not found, falling back to parent URI <{}>", parent, parentURI); - - QuerySolutionMap parentQsm = new QuerySolutionMap(); - parentQsm.add(SPIN.THIS_VAR_NAME, parent); - ResultSetRewindable docTypesResult = loadResultSet(getApplication().get().getService(), getDocumentTypeQuery(), parentQsm); - try + // special case for PUT requests: if the document does not exist, check acl:Write access on the *parent* URI instead + if (!docTypesResult.hasNext() && request.getMethod().equals(HttpMethod.PUT) && accessMode.equals(ACL.Write)) { - Set parentTypes = new HashSet<>(); - docTypesResult.forEachRemaining(qs -> parentTypes.add(qs.getResource("Type"))); + // Use Jena's IRIx for RFC 3986-compliant resolution - java.net.URI.resolve("..") is non-compliant + // (RFC 3986 section 5.2.4 step 2D requires ".." to be removed, but java.net.URI leaves it literal) + IRIx parentURI = IRIx.create(accessTo.getURI()).resolve(".."); + Resource parent = ResourceFactory.createResource(parentURI.toString()); + log.debug("Requested document <{}> not found, falling back to parent URI <{}>", parent, parentURI); - // only root and containers allow child documents. This needs to be checked before checking ownership - if (Collections.disjoint(parentTypes, Set.of(Default.Root, DH.Container))) return null; + QuerySolutionMap parentQsm = new QuerySolutionMap(); + parentQsm.add(SPIN.THIS_VAR_NAME, parent); + ResultSetRewindable parentTypesResult = loadResultSet(getApplication().get().getService(), getDocumentTypeQuery(), parentQsm); + try + { + Set parentTypes = new HashSet<>(); + parentTypesResult.forEachRemaining(qs -> parentTypes.add(qs.getResource("Type"))); + + // only root and containers allow child documents. This needs to be checked before checking ownership + if (Collections.disjoint(parentTypes, Set.of(Default.Root, DH.Container))) return null; - // the agent is the owner of the requested document - automatically grant acl:Read/acl:Append/acl:Write access - if (agent != null && isOwner(parent, agent)) + // the agent is the owner of the requested document - automatically grant acl:Read/acl:Append/acl:Write access + if (agent != null && isOwner(parent, agent)) + { + log.debug("Agent <{}> is the owner of <{}>, granting acl:Read/acl:Append/acl:Write access", agent, parent); + createOwnerAuthorization(authorizations, parent, agent); + } + + accessTo = parent; // redirect ACL query to parent URI since the document does not exist yet + } + finally { - log.debug("Agent <{}> is the owner of <{}>, granting acl:Read/acl:Append/acl:Write access", agent, parent); - createOwnerAuthorization(authorizations, parent, agent); + parentTypesResult.close(); } } - finally - { - docTypesResult.close(); - } - } - - QuerySolutionMap thisQsm = new QuerySolutionMap(); - thisQsm.add(SPIN.THIS_VAR_NAME, accessTo); - ResultSetRewindable docTypesResult = loadResultSet(getApplication().get().getService(), getDocumentTypeQuery(), thisQsm); - try - { + ParameterizedSparqlString pss = getApplication().get().canAs(EndUserApplication.class) ? getACLQuery() : getOwnerACLQuery(); if (docTypesResult.hasNext()) { Query query = new SetResultSetValues().apply(pss.asQuery(), docTypesResult); - pss = new ParameterizedSparqlString(query.toString()); // make sure VALUES are now part of the query string + pss = new ParameterizedSparqlString(query.toString()); // make sure type VALUES are now part of the query string assert pss.toString().contains("VALUES"); } From 2a357900bc8d5c51e0609df205be926c4950a3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 17 Feb 2026 22:47:46 +0100 Subject: [PATCH 08/10] Test fixes --- http-tests/document-hierarchy/DELETE-404.sh | 11 +++++++- .../DELETE-no-parent-403.sh | 25 ------------------- .../PATCH-empty-container.sh | 6 ++--- .../document-hierarchy/PATCH-empty-item.sh | 4 +-- .../PUT-double-slash-uri-400.sh | 13 +++++++++- 5 files changed, 27 insertions(+), 32 deletions(-) delete mode 100755 http-tests/document-hierarchy/DELETE-no-parent-403.sh diff --git a/http-tests/document-hierarchy/DELETE-404.sh b/http-tests/document-hierarchy/DELETE-404.sh index bbf35a421..e5b99b11a 100755 --- a/http-tests/document-hierarchy/DELETE-404.sh +++ b/http-tests/document-hierarchy/DELETE-404.sh @@ -22,4 +22,13 @@ curl -k -w "%{http_code}\n" -o /dev/null -s -G \ -X DELETE \ -H "Accept: application/n-triples" \ "${END_USER_BASE_URL}non-existing/" \ -| grep -q "$STATUS_NOT_FOUND" \ No newline at end of file +| grep -q "$STATUS_NOT_FOUND" + +# check that document without parent is not found + +curl -k -w "%{http_code}\n" -o /dev/null -s -G \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -X DELETE \ + -H "Accept: application/n-triples" \ + "${END_USER_BASE_URL}parent/non-existing/" \ +| grep -q "$STATUS_NOT_FOUND" diff --git a/http-tests/document-hierarchy/DELETE-no-parent-403.sh b/http-tests/document-hierarchy/DELETE-no-parent-403.sh deleted file mode 100755 index cf10ffd5b..000000000 --- a/http-tests/document-hierarchy/DELETE-no-parent-403.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" -initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" -purge_cache "$END_USER_VARNISH_SERVICE" -purge_cache "$ADMIN_VARNISH_SERVICE" -purge_cache "$FRONTEND_VARNISH_SERVICE" - -# add agent to the writers - -add-agent-to-group.sh \ - -f "$OWNER_CERT_FILE" \ - -p "$OWNER_CERT_PWD" \ - --agent "$AGENT_URI" \ - "${ADMIN_BASE_URL}acl/groups/writers/" - -# check that graph without parent is forbidden - -curl -k -w "%{http_code}\n" -o /dev/null -s -G \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -X DELETE \ - -H "Accept: application/n-triples" \ - "${END_USER_BASE_URL}parent/non-existing/" \ -| grep -q "$STATUS_FORBIDDEN" \ No newline at end of file diff --git a/http-tests/document-hierarchy/PATCH-empty-container.sh b/http-tests/document-hierarchy/PATCH-empty-container.sh index 5b95eb841..f61f40dad 100755 --- a/http-tests/document-hierarchy/PATCH-empty-container.sh +++ b/http-tests/document-hierarchy/PATCH-empty-container.sh @@ -32,11 +32,11 @@ container=$(create-container.sh \ update=$(cat < ?p ?o + ?s ?p ?o } WHERE { - <${container}> ?p ?o + ?s ?p ?o } EOF ) @@ -55,4 +55,4 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ -H "Accept: application/n-triples" \ "$container" \ -| grep -q "$STATUS_FORBIDDEN" +| grep -q "$STATUS_NOT_FOUND" diff --git a/http-tests/document-hierarchy/PATCH-empty-item.sh b/http-tests/document-hierarchy/PATCH-empty-item.sh index 5f45ac0a0..6f4d02978 100755 --- a/http-tests/document-hierarchy/PATCH-empty-item.sh +++ b/http-tests/document-hierarchy/PATCH-empty-item.sh @@ -32,11 +32,11 @@ item=$(create-item.sh \ update=$(cat < ?p ?o + ?s ?p ?o } WHERE { - <${item}> ?p ?o + ?s ?p ?o } EOF ) diff --git a/http-tests/document-hierarchy/PUT-double-slash-uri-400.sh b/http-tests/document-hierarchy/PUT-double-slash-uri-400.sh index 09585116f..23ffd6883 100755 --- a/http-tests/document-hierarchy/PUT-double-slash-uri-400.sh +++ b/http-tests/document-hierarchy/PUT-double-slash-uri-400.sh @@ -15,9 +15,20 @@ add-agent-to-group.sh \ --agent "$AGENT_URI" \ "${ADMIN_BASE_URL}acl/groups/writers/" +# create a container - IRIx resolves ".." on "new-item//" to "new-item/" (one segment per slash), +# so the parent container must exist for authorization to pass and reach the // validation in put() + +container=$(create-container.sh \ + -f "$AGENT_CERT_FILE" \ + -p "$AGENT_CERT_PWD" \ + -b "$END_USER_BASE_URL" \ + --title "Test Container" \ + --slug "new-item" \ + --parent "$END_USER_BASE_URL") + # creating new document fails because URIs with double slashes are not allowed -item="${END_USER_BASE_URL}new-item//" +item="${container}/" ( curl -k -w "%{http_code}\n" -o /dev/null -s \ From 444fc0fd858a5ab1b12e092f214a995ca999f1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 17 Feb 2026 22:52:30 +0100 Subject: [PATCH 09/10] Test fix --- http-tests/document-hierarchy/DELETE.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-tests/document-hierarchy/DELETE.sh b/http-tests/document-hierarchy/DELETE.sh index 5f784f27c..8ffd924f4 100755 --- a/http-tests/document-hierarchy/DELETE.sh +++ b/http-tests/document-hierarchy/DELETE.sh @@ -45,4 +45,4 @@ curl -k -w "%{http_code}\n" -o /dev/null -s -G \ -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ -H "Accept: application/n-triples" \ "$container" \ -| grep -q "$STATUS_FORBIDDEN" \ No newline at end of file +| grep -q "$STATUS_NOT_FOUND" From b9a6aea3bcc06c1b79a82107189bf0482e6ae558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 17 Feb 2026 23:03:38 +0100 Subject: [PATCH 10/10] Test fix --- http-tests/admin/packages/install-uninstall-package-ontology.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-tests/admin/packages/install-uninstall-package-ontology.sh b/http-tests/admin/packages/install-uninstall-package-ontology.sh index b96db04fd..9f5fa2330 100755 --- a/http-tests/admin/packages/install-uninstall-package-ontology.sh +++ b/http-tests/admin/packages/install-uninstall-package-ontology.sh @@ -58,4 +58,4 @@ fi curl -k -w "%{http_code}\n" -o /dev/null -s \ -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ "${ADMIN_BASE_URL}ontologies/${package_ontology_hash}/" \ -| grep -q "$STATUS_FORBIDDEN" +| grep -q "$STATUS_NOT_FOUND"