Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ Multiple dataspaces can share the same backend SPARQL service.
The application runs as a multi-container setup:
- **nginx**: Reverse proxy and SSL termination (wildcard subdomain routing for dataspaces)
- **linkeddatahub**: Main Java application (Tomcat)
- **fuseki-admin/fuseki-end-user**: Separate SPARQL stores
- **egress**: Squid forward proxy for the stores' outbound requests (SPARQL `SERVICE`, `LOAD`): public destinations only, so a query cannot reach the other store, Varnish or the platform
- **varnish-frontend/varnish-admin/varnish-end-user**: Caching layers
- **fuseki**: One SPARQL server holding a TDB2 dataset per dataspace role (`config/fuseki/config.ttl`), named after the dataspace origin (deployment host dropped, role appended: `end-user`, `admin`, `northwind-traders.demo.end-user`, …), each under `fuseki/<dataset>/`; bound to apps in `config/system.trig`
- **egress**: Squid forward proxy for the store's and platform's outbound requests (SPARQL `SERVICE`, `LOAD`): public destinations only, so a query cannot reach another dataset, Varnish or the platform
- **varnish-frontend/varnish-admin/varnish-end-user**: Caching layers (admin and end-user caches both front the single `fuseki`)

### Data Flow
1. Requests come through nginx proxy
Expand Down
18 changes: 9 additions & 9 deletions bin/sitemap/generate-sitemap.sh
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# This script directly queries the fuseki-admin and fuseki-end-user endpoints exposed on localhost, as the entrypoint does
# inside the stack. You can expose them in docker-compose.override.yml like this:
# fuseki-admin:
# This script directly queries the single fuseki endpoint exposed on localhost, as the entrypoint does inside the
# stack. You can expose it in docker-compose.override.yml like this:
# fuseki:
# ports:
# - 3030:3030
# fuseki-end-user:
# ports:
# - 3031:3030
#
# One dataspace per run, because a sitemap may only list the documents of the origin serving it. Pass that origin:
# One dataspace per run, because a sitemap may only list the documents of the origin serving it. Pass that origin
# (its host, minus the deployment host `.localhost`, is the dataset name; the root origin's datasets are end-user/admin):
# ./generate-sitemap.sh https://northwind-traders.demo.localhost:4443

origin="${1:-https://localhost:4443}"
admin_origin=$(echo "$origin" | sed 's|://|://admin.|')

admin_endpoint="http://localhost:3030/ds/"
end_user_endpoint="http://localhost:3031/ds/"
# the dataspace's datasets in the single fuseki: <host minus .localhost>.<role>, or plain end-user/admin for root
prefix=$(echo "$origin" | sed -E 's|^https?://||; s|:[0-9]+$||; s|\.?localhost$||')
admin_endpoint="http://localhost:3030/${prefix:+$prefix.}admin/"
end_user_endpoint="http://localhost:3030/${prefix:+$prefix.}end-user/"

admin_base="${admin_origin}/" envsubst '$admin_base' < ../../platform/sitemap/public-rules.rq > public-rules.rq

Expand Down
57 changes: 48 additions & 9 deletions config/fuseki/config.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,51 @@ PREFIX tdb2: <http://jena.apache.org/2016/tdb#>

[] a fuseki:Server .

<#service> a fuseki:Service ;
fuseki:name "ds" ;
fuseki:endpoint [ fuseki:operation fuseki:query; ] ;
fuseki:endpoint [ fuseki:operation fuseki:update;] ;
fuseki:endpoint [ fuseki:operation fuseki:gsp-rw; ] ;
fuseki:dataset <#dataset> .

<#dataset> a tdb2:DatasetTDB2 ;
tdb2:location "/fuseki/databases/DB2" .
# One dataset per dataspace role, named after the dataspace's origin with the deployment host dropped and the role
# appended: https://northwind-traders.demo.localhost -> northwind-traders.demo.end-user, and its admin origin ->
# northwind-traders.demo.admin. The root dataspace's prefix is empty, hence plain end-user and admin. Each dataset
# is stored in its own folder under ./fuseki/<name>/DB2.

# Queries are cancelled after 60 s, the Varnish backends' first_byte_timeout, past which no proxied client is still
# waiting for the answer. The timeout sits on the query endpoint: Fuseki honours ja:context there and on the
# dataset, but silently ignores it on the fuseki:Service.

<#end-user> a fuseki:Service ;
fuseki:name "end-user" ;
fuseki:endpoint [ fuseki:operation fuseki:query ; ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "60000" ] ] ;
fuseki:endpoint [ fuseki:operation fuseki:update ] ;
fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ] ;
fuseki:dataset <#end-user-dataset> .

<#end-user-dataset> a tdb2:DatasetTDB2 ;
tdb2:location "/fuseki/databases/end-user/DB2" .

<#admin> a fuseki:Service ;
fuseki:name "admin" ;
fuseki:endpoint [ fuseki:operation fuseki:query ; ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "60000" ] ] ;
fuseki:endpoint [ fuseki:operation fuseki:update ] ;
fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ] ;
fuseki:dataset <#admin-dataset> .

<#admin-dataset> a tdb2:DatasetTDB2 ;
tdb2:location "/fuseki/databases/admin/DB2" .

<#northwind-traders.demo.end-user> a fuseki:Service ;
fuseki:name "northwind-traders.demo.end-user" ;
fuseki:endpoint [ fuseki:operation fuseki:query ; ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "60000" ] ] ;
fuseki:endpoint [ fuseki:operation fuseki:update ] ;
fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ] ;
fuseki:dataset <#northwind-traders.demo.end-user-dataset> .

<#northwind-traders.demo.end-user-dataset> a tdb2:DatasetTDB2 ;
tdb2:location "/fuseki/databases/northwind-traders.demo.end-user/DB2" .

<#northwind-traders.demo.admin> a fuseki:Service ;
fuseki:name "northwind-traders.demo.admin" ;
fuseki:endpoint [ fuseki:operation fuseki:query ; ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "60000" ] ] ;
fuseki:endpoint [ fuseki:operation fuseki:update ] ;
fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ] ;
fuseki:dataset <#northwind-traders.demo.admin-dataset> .

<#northwind-traders.demo.admin-dataset> a tdb2:DatasetTDB2 ;
tdb2:location "/fuseki/databases/northwind-traders.demo.admin/DB2" .
44 changes: 34 additions & 10 deletions config/system.trig
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
<urn:linkeddatahub:services/admin> a sd:Service ;
dct:title "LinkedDataHub admin service" ;
sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ;
sd:endpoint <http://fuseki-admin:3030/ds/> ;
a:graphStore <http://fuseki-admin:3030/ds/> ;
a:quadStore <http://fuseki-admin:3030/ds/> .
sd:endpoint <http://fuseki:3030/admin/> ;
a:graphStore <http://fuseki:3030/admin/> ;
a:quadStore <http://fuseki:3030/admin/> .
}

# root end-user - type + service binding
Expand Down Expand Up @@ -59,23 +59,47 @@
<urn:linkeddatahub:services/end-user> a sd:Service ;
dct:title "LinkedDataHub service" ;
sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ;
sd:endpoint <http://fuseki-end-user:3030/ds/> ;
a:graphStore <http://fuseki-end-user:3030/ds/> ;
a:quadStore <http://fuseki-end-user:3030/ds/> .
sd:endpoint <http://fuseki:3030/end-user/> ;
a:graphStore <http://fuseki:3030/end-user/> ;
a:quadStore <http://fuseki:3030/end-user/> .
}

# northwind-traders admin - type + service binding (reuses root admin service)
# northwind-traders admin - type + service binding

<urn:linkeddatahub:apps/northwind-traders/admin>
{
<urn:linkeddatahub:apps/northwind-traders/admin> a lapp:AdminApplication ;
ldt:service <urn:linkeddatahub:services/admin> .
ldt:service <urn:linkeddatahub:services/northwind-traders/admin> .
}

# northwind-traders admin - service description

<urn:linkeddatahub:services/northwind-traders/admin>
{
<urn:linkeddatahub:services/northwind-traders/admin> a sd:Service ;
dct:title "Northwind Traders admin service" ;
sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ;
sd:endpoint <http://fuseki:3030/northwind-traders.demo.admin/> ;
a:graphStore <http://fuseki:3030/northwind-traders.demo.admin/> ;
a:quadStore <http://fuseki:3030/northwind-traders.demo.admin/> .
}

# northwind-traders end-user - type + service binding (reuses root end-user service)
# northwind-traders end-user - type + service binding

<urn:linkeddatahub:apps/northwind-traders/end-user>
{
<urn:linkeddatahub:apps/northwind-traders/end-user> a lapp:EndUserApplication ;
ldt:service <urn:linkeddatahub:services/end-user> .
ldt:service <urn:linkeddatahub:services/northwind-traders/end-user> .
}

# northwind-traders end-user - service description

<urn:linkeddatahub:services/northwind-traders/end-user>
{
<urn:linkeddatahub:services/northwind-traders/end-user> a sd:Service ;
dct:title "Northwind Traders service" ;
sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ;
sd:endpoint <http://fuseki:3030/northwind-traders.demo.end-user/> ;
a:graphStore <http://fuseki:3030/northwind-traders.demo.end-user/> ;
a:quadStore <http://fuseki:3030/northwind-traders.demo.end-user/> .
}
7 changes: 2 additions & 5 deletions docker-compose.debug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ services:
- CATALINA_OPTS=-XX:+UseContainerSupport -XX:MaxRAMPercentage=75 --add-exports java.base/sun.security.tools.keytool=ALL-UNNAMED -XX:+UseLinuxPosixThreadCPUClocks -agentpath:/var/linkeddatahub/profiler/lib/deployed/jdk16/linux-amd64/libprofilerinterface.so=/var/linkeddatahub/profiler/lib,5140 # heap will use up to 75% of container's RAM
volumes:
- ./platform/profiler:/var/linkeddatahub/profiler
fuseki-admin:
fuseki:
ports:
- 3030:3030
fuseki-end-user:
ports:
- 3031:3030
- 3030:3030
35 changes: 9 additions & 26 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ services:
build: .
mem_limit: 2048m
depends_on:
- fuseki-admin
- fuseki-end-user
- fuseki
- sef-compiler
- egress
environment:
Expand Down Expand Up @@ -120,41 +119,25 @@ services:
restart: on-failure
expose:
- 8080 # internal only - the platform is the sole client
fuseki-admin:
fuseki: # one server; every dataspace role is its own dataset in config/fuseki/config.ttl, stored under ./fuseki/<dataset>
image: atomgraph/fuseki:6.1.0
user: root # otherwise fuseki user does not have permissions to the mounted folder which is owner by root
mem_limit: 1536m # leave headroom above heap for TDB mmap/native memory
mem_limit: 4608m # the former fuseki-admin (1536m) and fuseki-end-user (3072m) combined
restart: on-failure
depends_on:
- egress
environment:
- JAVA_OPTIONS=-Xmx768m -Xms768m
- JAVA_OPTIONS=-Xmx2304m -Xms2304m # the former heaps combined: 768m admin + 1536m end-user
# every outbound request (SPARQL SERVICE, LOAD) goes through egress, which only lets it reach public addresses.
# JAVA_TOOL_OPTIONS rather than JAVA_OPTIONS, which an override setting the heap would replace. The empty
# nonProxyHosts is load-bearing: by default the JVM bypasses the proxy for localhost
# nonProxyHosts is load-bearing: by default the JVM bypasses the proxy for localhost, and
# SERVICE <http://localhost:3030/other-dataset/> would read a sibling dataset in this same server
- JAVA_TOOL_OPTIONS=-Dhttp.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttp.proxyPort=3128 -Dhttps.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttps.proxyPort=3128 -Dhttp.nonProxyHosts=
expose:
- 3030
volumes:
- ./config/fuseki/config.ttl:/fuseki/config.ttl:ro
- ./fuseki/admin:/fuseki/databases
command: [ "--config", "/fuseki/config.ttl" ]
fuseki-end-user:
image: atomgraph/fuseki:6.1.0
user: root # otherwise the fuseki user does not have permissions to the mounted folder which is owner by root
mem_limit: 3072m # leave headroom above heap for TDB mmap/native memory
restart: on-failure
depends_on:
- egress
environment:
- JAVA_OPTIONS=-Xmx1536m -Xms1536m
# see fuseki-admin: outbound requests only reach public addresses, and not localhost either
- JAVA_TOOL_OPTIONS=-Dhttp.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttp.proxyPort=3128 -Dhttps.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttps.proxyPort=3128 -Dhttp.nonProxyHosts=
expose:
- 3030
volumes:
- ./config/fuseki/config.ttl:/fuseki/config.ttl:ro
- ./fuseki/end-user:/fuseki/databases
- ./fuseki:/fuseki/databases
command: [ "--config", "/fuseki/config.ttl" ]
egress: # forward proxy for the triplestores' outbound requests: refuses loopback, private and link-local destinations
image: ubuntu/squid:6.6-24.04_beta
Expand Down Expand Up @@ -497,7 +480,7 @@ configs:
import xkey;

backend default {
.host = "${VARNISH_ADMIN_BACKEND_HOST:-fuseki-admin}";
.host = "${VARNISH_ADMIN_BACKEND_HOST:-fuseki}";
.port = "${VARNISH_ADMIN_BACKEND_PORT:-3030}";
.first_byte_timeout = 60s;
}
Expand Down Expand Up @@ -580,7 +563,7 @@ configs:
import std;

backend default {
.host = "${VARNISH_END_USER_BACKEND_HOST:-fuseki-end-user}";
.host = "${VARNISH_END_USER_BACKEND_HOST:-fuseki}";
.port = "${VARNISH_END_USER_BACKEND_PORT:-3030}";
.first_byte_timeout = 60s;
}
Expand Down
24 changes: 12 additions & 12 deletions http-tests/config/system.trig
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
<urn:linkeddatahub:services/admin> a sd:Service ;
dct:title "LinkedDataHub admin service" ;
sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ;
sd:endpoint <http://fuseki-admin:3030/ds/> ;
a:graphStore <http://fuseki-admin:3030/ds/> ;
a:quadStore <http://fuseki-admin:3030/ds/> .
sd:endpoint <http://fuseki:3030/admin/> ;
a:graphStore <http://fuseki:3030/admin/> ;
a:quadStore <http://fuseki:3030/admin/> .
}

# root end-user - type + service binding
Expand All @@ -43,9 +43,9 @@
<urn:linkeddatahub:services/end-user> a sd:Service ;
dct:title "LinkedDataHub service" ;
sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ;
sd:endpoint <http://fuseki-end-user:3030/ds/> ;
a:graphStore <http://fuseki-end-user:3030/ds/> ;
a:quadStore <http://fuseki-end-user:3030/ds/> .
sd:endpoint <http://fuseki:3030/end-user/> ;
a:graphStore <http://fuseki:3030/end-user/> ;
a:quadStore <http://fuseki:3030/end-user/> .
}

# test admin - type + service binding
Expand All @@ -63,9 +63,9 @@
<urn:linkeddatahub:services/test/admin> a sd:Service ;
dct:title "Test admin service" ;
sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ;
sd:endpoint <http://fuseki-admin:3030/ds/> ;
a:graphStore <http://fuseki-admin:3030/ds/> ;
a:quadStore <http://fuseki-admin:3030/ds/> .
sd:endpoint <http://fuseki:3030/admin/> ;
a:graphStore <http://fuseki:3030/admin/> ;
a:quadStore <http://fuseki:3030/admin/> .
}

# test end-user - type + service binding
Expand All @@ -83,7 +83,7 @@
<urn:linkeddatahub:services/test/end-user> a sd:Service ;
dct:title "Test service" ;
sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ;
sd:endpoint <http://fuseki-end-user:3030/ds/> ;
a:graphStore <http://fuseki-end-user:3030/ds/> ;
a:quadStore <http://fuseki-end-user:3030/ds/> .
sd:endpoint <http://fuseki:3030/end-user/> ;
a:graphStore <http://fuseki:3030/end-user/> ;
a:quadStore <http://fuseki:3030/end-user/> .
}
9 changes: 2 additions & 7 deletions http-tests/docker-compose.http-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,8 @@ services:
- ./http-tests/ssl/server:/var/linkeddatahub/ssl/server
- ./http-tests/ssl/owner:/var/linkeddatahub/ssl/owner
- ./http-tests/ssl/secretary:/var/linkeddatahub/ssl/secretary
fuseki-admin:
fuseki:
ports:
- 3030:3030
#volumes:
# -./http-tests/data/admin:/fuseki/databases
fuseki-end-user:
ports:
- 3031:3030
#volumes:
# - ./http-tests/data/end-user:/fuseki/databases
# - ./http-tests/data:/fuseki/databases
2 changes: 1 addition & 1 deletion http-tests/document-hierarchy/PATCH-service-internal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ item=$(ldh create item \
--title "SERVICE PATCH target" \
--container "$END_USER_BASE_URL")

endpoint="http://fuseki-admin:3030/ds/"
endpoint="http://fuseki:3030/admin/"

update=$(cat <<EOF
INSERT
Expand Down
2 changes: 1 addition & 1 deletion http-tests/imports/import-rdf-service-internal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ echo "DEBUG: ${imported}: ${document}"
# check that nothing from the admin store was imported

if grep -q '<urn:test:leak>' <<< "$document"; then
echo "SERVICE <http://fuseki-admin:3030/ds/> in an import mapping returned data"
echo "SERVICE <http://fuseki:3030/admin/> in an import mapping returned data"
exit 1
fi
2 changes: 1 addition & 1 deletion http-tests/imports/rdf-service-internal.rq
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ WHERE

OPTIONAL
{
SERVICE SILENT <http://fuseki-admin:3030/ds/>
SERVICE SILENT <http://fuseki:3030/admin/>
{
SELECT ?g
{
Expand Down
4 changes: 2 additions & 2 deletions http-tests/proxy/GET-proxied-internal-403.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ ldh admin add agent \
"${ADMIN_BASE_URL}acl/groups/readers/"

# LNK-009: Test that internal Docker services are blocked via SSRF protection
# Attempt to access the internal fuseki-admin SPARQL endpoint via the proxy
# Attempt to access the internal fuseki admin dataset endpoint via the proxy
# This should be blocked and return 400 Bad Request

http_status=$(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=http://fuseki-admin:3030/ds" \
--data-urlencode "uri=http://fuseki:3030/admin" \
"$END_USER_BASE_URL" || true)

# Verify that access was rejected (400)
Expand Down
4 changes: 2 additions & 2 deletions http-tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ export -f purge_cache
export HTTP_TEST_ROOT="$PWD"
export TEST_RESULTS_DIR="${TEST_RESULTS_DIR:-$HTTP_TEST_ROOT/out}"
mkdir -p "$TEST_RESULTS_DIR"
export END_USER_ENDPOINT_URL="http://localhost:3031/ds/"
export ADMIN_ENDPOINT_URL="http://localhost:3030/ds/"
export END_USER_ENDPOINT_URL="http://localhost:3030/end-user/"
export ADMIN_ENDPOINT_URL="http://localhost:3030/admin/"
export END_USER_BASE_URL="https://localhost:4443/"
export ADMIN_BASE_URL="https://admin.localhost:4443/"
export END_USER_VARNISH_SERVICE="varnish-end-user"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE"
# SERVICE SILENT turns a refused call into a single empty solution, so the results are checked for data rather than
# the response for a status code: a target that answered binds ?g

for endpoint in "http://fuseki-admin:3030/ds/" "http://varnish-admin/ds/" "http://localhost:3030/ds/"
for endpoint in "http://fuseki:3030/admin/" "http://varnish-admin/admin/" "http://localhost:3030/admin/"
do
results=$(curl -k -f -s -G \
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
Expand Down
Loading
Loading