Skip to content

Commit 3031914

Browse files
committed
Release version 5.6.1
2 parents 6d831a0 + 97e4f61 commit 3031914

31 files changed

Lines changed: 664 additions & 172 deletions

File tree

.github/dependabot.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
version: 2
2+
updates:
3+
# Maven dependencies (pom.xml)
4+
- package-ecosystem: "maven"
5+
directory: "/"
6+
schedule:
7+
interval: "weekly"
8+
open-pull-requests-limit: 10
9+
groups:
10+
# collapse routine minor/patch bumps into a single PR to cut noise; majors stay separate
11+
minor-and-patch:
12+
update-types:
13+
- "minor"
14+
- "patch"
15+
16+
# Base image in the Dockerfile
17+
- package-ecosystem: "docker"
18+
directory: "/"
19+
schedule:
20+
interval: "weekly"
21+
22+
# GitHub Actions used by the workflows
23+
- package-ecosystem: "github-actions"
24+
directory: "/"
25+
schedule:
26+
interval: "weekly"

AGENTS.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# LinkedDataHub — Agent Guide
2+
3+
This document describes how an autonomous agent (or any HTTP/LLM client) drives a **running LinkedDataHub (LDH) instance's HTTP API**. It is the API-usage counterpart to `CLAUDE.md` (which is for contributing to the codebase).
4+
5+
LinkedDataHub is a data-driven Knowledge Graph platform. Everything — documents, applications, access control, the UI — is RDF, managed over a small, uniform HTTP API and standard protocols. There is no bespoke REST surface to learn: you work with RDF documents and SPARQL.
6+
7+
## Data model
8+
9+
- The content is a **hierarchy of documents** (containers and items). A container holds child documents; items are leaves.
10+
- **Every document URL is a named graph.** Reading a document returns the RDF in that graph; writing changes it. This is the [SPARQL 1.1 Graph Store Protocol](https://www.w3.org/TR/sparql11-http-rdf-update/).
11+
- Identifiers are opaque URLs. Do not parse structure out of them; follow links (hypermedia) instead.
12+
13+
## Authentication
14+
15+
- **WebID-TLS** (client certificate) is the primary mechanism for programmatic agents. Every request carries the cert; the certificate's WebID is the agent identity. With `curl`: `-E cert.pem:password` (`-k` in dev with self-signed certs).
16+
- **OAuth2 (Google)** and **OpenID Connect (ORCID)** are available for human logins.
17+
- **Delegation**: an authorized secretary agent can act for a principal via the `On-Behalf-Of: <principal-WebID>` request header.
18+
- Authorization is WebID-based ACLs (`acl:Read`/`Append`/`Write`/`Control`), enforced per document. A response's `Link` headers advertise the modes the current agent holds on that resource.
19+
20+
## Reading data
21+
22+
`GET` a document URL with content negotiation:
23+
24+
- `Accept: text/turtle` · `application/rdf+xml` · `application/ld+json` · `application/n-triples` (any RDF serialization Jena supports) → the document's RDF.
25+
- `Accept: text/html` → the application shell (Saxon-JS then renders client-side). Request RDF, not HTML, when you want data.
26+
27+
## Writing data (the discipline)
28+
29+
Writes go through the **document URLs**, never through the SPARQL endpoint (which is read-only):
30+
31+
| Intent | Method | Body | Notes |
32+
|--------|--------|------|-------|
33+
| Create a child in a container | `POST` container URL | RDF (e.g. `Content-Type: text/turtle`) | Server mints the child URL and returns it in `Location` |
34+
| Create or replace a document at a known URL | `PUT` document URL | RDF | Replaces the whole named graph |
35+
| Update a document in place | `PATCH` document URL | `Content-Type: application/sparql-update` | A SPARQL Update (`INSERT`/`DELETE`) applied to that named graph |
36+
| Delete a document | `DELETE` document URL || Removes the named graph |
37+
38+
Relative URIs in a request body resolve against the target URL. See `bin/post.sh`, `bin/put.sh`, `bin/patch.sh`, `bin/delete.sh` for exact, working invocations.
39+
40+
## Querying (read-only)
41+
42+
The dataspace exposes a **read-only SPARQL 1.1 Query** endpoint (advertised via the Service Description `sd:endpoint`; conventionally `/sparql`). `GET`/`POST` a `SELECT`/`CONSTRUCT`/`DESCRIBE`/`ASK`; results are content-negotiated. The endpoint does **not** accept SPARQL Update — mutate via `PATCH` on document URLs (above).
43+
44+
Write portable, standard SPARQL: use explicit `GRAPH` patterns, no engine-specific extensions.
45+
46+
## Content & document model
47+
48+
- Documents carry ordered **content blocks**. Only `ldh:Object` (an embedded RDF resource view) and `ldh:XHTML` (rich text) are permitted as block values; anything else must be wrapped in an `ldh:Object`.
49+
- **Views** (`ldh:View`) are SPARQL-driven blocks (`SELECT`/`CONSTRUCT`/`DESCRIBE`) rendered as lists, tables, grids, charts, maps, or a graph.
50+
- Forms and validation are ontology-driven (SPIN constructors + SHACL shapes), so instance data is shaped by the app's ontology rather than hardcoded schemas.
51+
52+
## Dataspaces
53+
54+
A single instance hosts multiple **dataspaces**, each a subdomain (origin). Each dataspace pairs an end-user app (`<subdomain>`) with an admin app at the **`admin.` prefix** (`admin.<subdomain>`) — never an `/admin` path. Admin apps manage ontologies, ACLs, and app settings.
55+
56+
## Tooling
57+
58+
- **CLI**: the `bin/` scripts wrap every operation above (`get.sh`, `post.sh`, `put.sh`, `patch.sh`, `delete.sh`, `create-container.sh`, `create-item.sh`, `add-view.sh`, `add-select.sh`, `add-construct.sh`, `add-result-set-chart.sh`, `add-file.sh`, `webid-keygen.sh`). They are the authoritative reference for request shapes.
59+
- **Programmatic / MCP**: [Web-Algebra](https://github.com/AtomGraph/Web-Algebra) is the recommended path for agent-composed workflows — a JSON DSL and MCP server whose operations (create container/item, add view/chart, generate portal, …) compose multi-step LDH writes atomically under WebID auth.
60+
61+
## Standards
62+
63+
WebID-TLS · SPARQL 1.1 Query & Update · Graph Store Protocol · Linked Data Templates · SHACL · SPIN · RDF (Turtle/RDF-XML/JSON-LD/N-Triples). LDH composes existing W3C/IETF standards; it does not define new wire protocols.

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
## [Unreleased]
2+
### Security
3+
- SSRF: `URLValidator` now blocks wildcard/any-local (0.0.0.0, ::) addresses in addition to link-local and private ranges, and checks every address the host resolves to (narrowing the DNS-rebinding window). Loopback stays reachable for same-origin document/WebID dereferencing; the backend triplestore is site-local (already blocked). `ALLOW_INTERNAL_URLS` remains the development escape hatch (LNK-003/LNK-009)
4+
- XXE: added `SecureXML` hardened parser factories — `XSLTMasterUpdater` parses with DTDs and external entities disabled, and the external responses parsed by `ldh:send-request` use secure processing (entity-expansion capped) with external entities disabled (LNK-005 residual)
5+
- Upgraded `java-jwt` 3.19.4 → 4.5.2 on the OAuth2/OIDC verification path
6+
- Documented the pinned-truststore invariant behind the disabled hostname verification on internal HTTP clients
7+
8+
### Added
9+
- Unit tests for `AuthorizationFilter`: the HTTP-method → ACL access-mode contract (`GET`/`HEAD`→Read, `POST`→Append, `PUT`/`DELETE`/`PATCH`→Write), mode lookup, and the owner Read/Write/Append grant
10+
- Loopback/wildcard `URLValidator` tests; JWKS-based `JWTVerifier` tests (valid, wrong issuer, wrong audience, expired, missing `kid`, bad signature)
11+
- `AGENTS.md`: an agent-facing guide to driving a running instance's HTTP API — data model, WebID auth, read/write discipline (writes via `POST`/`PUT`/`PATCH` on document URLs; read-only SPARQL), content model, dataspaces, tooling
12+
- Dependabot config (`.github/dependabot.yml`) for Maven, the Docker base image, and GitHub Actions updates; routine Maven minor/patch bumps grouped into one PR
13+
14+
### Changed
15+
- Cache TTLs configurable: the WebID model cache and the JWKS cache now read their expiration (seconds) from `WEBID_CACHE_EXPIRATION` / `JWKS_CACHE_EXPIRATION` (default 86400 = 1 day), via `CATALINA_OPTS` system properties like the `CLIENT_*` timeouts. Lowering `WEBID_CACHE_EXPIRATION` bounds how long a revoked WebID stays authenticated
16+
117
## [5.6.0] - 2026-07-08
218
### Added
319
- `OntologyRepository` (renamed from `OntologyModelGetter`): a bounded, evicting ontology cache that serves bundled vocabularies without querying SPARQL; per-app creation is thread-safe and each ontology is materialized once under a lock (`owl:imports` closure flattened manually, then RDFS-inferred and materialized). Seeded ad-hoc in `Namespace`

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ ENV CLIENT_CONNECTION_TIME_TO_LIVE=300000
117117

118118
ENV CLIENT_VALIDATE_AFTER_INACTIVITY=10000
119119

120+
ENV WEBID_CACHE_EXPIRATION=86400
121+
122+
ENV JWKS_CACHE_EXPIRATION=86400
123+
120124
ENV IMPORT_KEEPALIVE=
121125

122126
ENV MAX_IMPORT_THREADS=10

platform/entrypoint.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,14 @@ if [ -n "$CLIENT_VALIDATE_AFTER_INACTIVITY" ]; then
10921092
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.atomgraph.linkeddatahub.validateAfterInactivity=$CLIENT_VALIDATE_AFTER_INACTIVITY"
10931093
fi
10941094

1095+
if [ -n "$WEBID_CACHE_EXPIRATION" ]; then
1096+
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.atomgraph.linkeddatahub.webIDCacheExpiration=$WEBID_CACHE_EXPIRATION"
1097+
fi
1098+
1099+
if [ -n "$JWKS_CACHE_EXPIRATION" ]; then
1100+
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.atomgraph.linkeddatahub.jwksCacheExpiration=$JWKS_CACHE_EXPIRATION"
1101+
fi
1102+
10951103
if [ -n "$MAX_CONTENT_LENGTH" ]; then
10961104
MAX_CONTENT_LENGTH_PARAM="--stringparam ldhc:maxContentLength '$MAX_CONTENT_LENGTH' "
10971105
fi

pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<groupId>com.atomgraph</groupId>
55
<artifactId>linkeddatahub</artifactId>
6-
<version>5.6.0</version>
6+
<version>5.6.1</version>
77
<packaging>${packaging.type}</packaging>
88

99
<name>AtomGraph LinkedDataHub</name>
@@ -46,7 +46,7 @@
4646
<url>https://github.com/AtomGraph/LinkedDataHub</url>
4747
<connection>scm:git:git://github.com/AtomGraph/LinkedDataHub.git</connection>
4848
<developerConnection>scm:git:git@github.com:AtomGraph/LinkedDataHub.git</developerConnection>
49-
<tag>linkeddatahub-5.6.0</tag>
49+
<tag>linkeddatahub-5.6.1</tag>
5050
</scm>
5151

5252
<repositories>
@@ -169,19 +169,19 @@
169169
<dependency>
170170
<groupId>${project.groupId}</groupId>
171171
<artifactId>client</artifactId>
172-
<version>5.0.2</version>
172+
<version>5.0.3</version>
173173
<classifier>classes</classifier>
174174
</dependency>
175175
<dependency>
176176
<groupId>${project.groupId}</groupId>
177177
<artifactId>client</artifactId>
178-
<version>5.0.2</version>
178+
<version>5.0.3</version>
179179
<type>war</type>
180180
</dependency>
181181
<dependency>
182182
<groupId>com.auth0</groupId>
183183
<artifactId>java-jwt</artifactId>
184-
<version>3.19.4</version>
184+
<version>4.5.2</version>
185185
</dependency>
186186
<dependency>
187187
<groupId>net.jodah</groupId>

src/main/java/com/atomgraph/linkeddatahub/Application.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,9 @@ public class Application extends ResourceConfig
288288
private final KeyStore keyStore, trustStore;
289289
private final URI secretaryWebIDURI;
290290
private final List<Locale> supportedLanguages;
291-
private final ExpiringMap<URI, Model> webIDmodelCache = ExpiringMap.builder().expiration(1, TimeUnit.DAYS).build(); // TO-DO: config for the expiration period?
291+
private final ExpiringMap<URI, Model> webIDmodelCache = ExpiringMap.builder().expiration(Long.parseLong(System.getProperty("com.atomgraph.linkeddatahub.webIDCacheExpiration", "86400")), TimeUnit.SECONDS).build(); // TTL (seconds) configurable via WEBID_CACHE_EXPIRATION; a lower value bounds how long a revoked WebID stays cached
292292
private final ExpiringMap<String, Model> oidcModelCache = ExpiringMap.builder().variableExpiration().build();
293-
private final ExpiringMap<String, jakarta.json.JsonObject> jwksCache = ExpiringMap.builder().expiration(1, TimeUnit.DAYS).build(); // Cache JWKS responses
293+
private final ExpiringMap<String, jakarta.json.JsonObject> jwksCache = ExpiringMap.builder().expiration(Long.parseLong(System.getProperty("com.atomgraph.linkeddatahub.jwksCacheExpiration", "86400")), TimeUnit.SECONDS).build(); // Cache JWKS responses; TTL (seconds) configurable via JWKS_CACHE_EXPIRATION
294294
private final Map<URI, XsltExecutable> xsltExecutableCache = new ConcurrentHashMap<>();
295295
private final MessageDigest messageDigest;
296296
private final boolean enableWebIDSignUp;
@@ -1519,6 +1519,7 @@ public static Client getClient(KeyStore keyStore, String keyStorePassword, KeySt
15191519
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
15201520

15211521
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create().
1522+
// hostname verification is safely disabled because ctx trusts only the pinned truststore; revisit if that truststore ever widens to public CAs
15221523
register("https", new SSLConnectionSocketFactory(ctx, NoopHostnameVerifier.INSTANCE)).
15231524
register("http", new PlainConnectionSocketFactory()).
15241525
build();
@@ -1626,6 +1627,7 @@ public static Client getNoCertClient(KeyStore trustStore, Integer maxConnPerRout
16261627
ctx.init(null, tmf.getTrustManagers(), null);
16271628

16281629
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create().
1630+
// hostname verification is safely disabled because ctx trusts only the pinned truststore; revisit if that truststore ever widens to public CAs
16291631
register("https", new SSLConnectionSocketFactory(ctx, NoopHostnameVerifier.INSTANCE)).
16301632
register("http", new PlainConnectionSocketFactory()).
16311633
build();

src/main/java/com/atomgraph/linkeddatahub/server/filter/request/auth/IDTokenFilterBase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public SecurityContext authenticate(ContainerRequestContext request)
195195
else
196196
{
197197
if (log.isDebugEnabled()) log.debug("ID token for subject '{}' has expired at {}, refresh token not found", jwt.getSubject(), jwt.getExpiresAt());
198-
throw new TokenExpiredException("ID token for subject '%s' has expired at %s".formatted(jwt.getSubject(), jwt.getExpiresAt()));
198+
throw new TokenExpiredException("ID token for subject '%s' has expired at %s".formatted(jwt.getSubject(), jwt.getExpiresAt()), jwt.getExpiresAt().toInstant());
199199
}
200200
}
201201
if (!verify(jwt)) return null;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Copyright 2026 Martynas Jusevičius <martynas@atomgraph.com>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.atomgraph.linkeddatahub.server.util;
18+
19+
import javax.xml.XMLConstants;
20+
import javax.xml.parsers.DocumentBuilderFactory;
21+
import javax.xml.parsers.ParserConfigurationException;
22+
import javax.xml.parsers.SAXParserFactory;
23+
import org.xml.sax.SAXException;
24+
import org.xml.sax.XMLReader;
25+
26+
/**
27+
* Factory helpers for XML parsers hardened against XXE and entity-expansion (billion laughs) attacks.
28+
*
29+
* @author Martynas Jusevičius {@literal <martynas@atomgraph.com>}
30+
* @see <a href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html">OWASP XXE Prevention</a>
31+
*/
32+
public final class SecureXML
33+
{
34+
35+
private SecureXML()
36+
{
37+
}
38+
39+
/**
40+
* Returns a namespace-aware {@link DocumentBuilderFactory} with DTDs and external entities disabled.
41+
* Suitable for parsing trusted internal XML (e.g. stylesheets) that never carries a DOCTYPE.
42+
*
43+
* @return hardened document builder factory
44+
* @throws ParserConfigurationException if a feature cannot be set
45+
*/
46+
public static DocumentBuilderFactory newDocumentBuilderFactory() throws ParserConfigurationException
47+
{
48+
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
49+
factory.setNamespaceAware(true);
50+
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
51+
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
52+
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
53+
factory.setXIncludeAware(false);
54+
factory.setExpandEntityReferences(false);
55+
return factory;
56+
}
57+
58+
/**
59+
* Returns an {@link XMLReader} hardened for parsing untrusted external content.
60+
* Secure processing caps entity expansion (billion laughs) and external entities are disabled,
61+
* while a benign internal DOCTYPE (e.g. XHTML) is still tolerated.
62+
*
63+
* @return hardened XML reader
64+
* @throws ParserConfigurationException if a feature cannot be set
65+
* @throws SAXException if the reader cannot be created
66+
*/
67+
public static XMLReader newXMLReader() throws ParserConfigurationException, SAXException
68+
{
69+
SAXParserFactory factory = SAXParserFactory.newInstance();
70+
factory.setNamespaceAware(true);
71+
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
72+
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
73+
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
74+
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
75+
return factory.newSAXParser().getXMLReader();
76+
}
77+
78+
}

0 commit comments

Comments
 (0)