Skip to content
Draft
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
4 changes: 4 additions & 0 deletions acs-fuseki/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
db/
dependency-reduced-pom.xml
target/
tmp/
127 changes: 127 additions & 0 deletions acs-fuseki/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# ACS Fuseki plugin

This is a sketch of a plugin to make Apache Jena Fuseki suitable for use
as an RDF graph service within ACS. The principle feature missing is
robust tripe-level access control; this plugin gives an outline of how
this might be provided.

## Building and running

You will need Java, a JDK and Maven. Other dependencies will be
downloaded by Maven as required. You will need `java` and `mvn` in your
`PATH`, and may need to set `JAVA_HOME` and/or `M2_HOME` in the
environment.

To build, run

mvn -B package

This will download the deps and build a JAR in
`target/acs-fuseki-0.0.1.jar`. This is a build of Fuseki Main, i.e. the
triplestore without the UI. Run with

java -jar target/acs-fuseki-0.0.1.jar --config=config.ttl

This will run a triplestore exposing a SPARQL endpoint at
`http://localhost:3030/ds/sparql`, accepting queries and update
operations. The database will be created in the `db` directory.

## Namespaces

These namespaces are relevant:

prefix shex: <http://www.w3.org/ns/shex#> .
prefix acl: <http://factoryplus.app.amrc.co.uk/rdf/2025-05/ac-24-611/acl#> .

## Features

Access control is only implemented on query; all updates are permitted.
The username to authorise is taken from an HTTP Basic Auth header; the
password is ignored. Obviously this would need integrating properly into
an authentication framework eventually but that's not the point at the
moment.

ACLs live in the dataset, in the default graph. ACLs in other graphs are
ignored; I am assuming for the moment that these will represent
hypotheticals or other forms of information and should not be used for
access control. Generally access to named graphs is not considered
properly yet.

A principal is identified by an IRI and a string username; the username
is linked to the IRI by an `acl:username` property. Permission grants
are also linked, with `acl:readTriple` properties. The permission model
is deny by default with grant permissions only. The range of
`acl:readTriple` is the class `acl:ShexCondition`; an object of this
class represents a condition on triples expressed in the
[ShEx](https://shex.io) expression language.

ShEx is intended for schema validation; it's basically an alternative to
SHACL. It isn't entirely suitable for this purpose but is the only
expression language implementation easily available within Jena. The
primary concept in ShEx is the Shape, which can be seen as a condition a
node must satisfy, both in terms of its internal properties
(IRI/blank/literal, datatype and content if literal) and in terms of its
links to other nodes. Normally the validation process accepts a list of
Shapes and a ShapeMap which determines which nodes must match which
Shapes, and a graph passes validation if all relevant nodes conform.

The Jena ShEx implementation makes it possible to evaluate a particular
node against a particular shape without needing to validate the whole
graph. We are using this to implement access control. An
`acl:ShexCondition` object has three properties: `acl:subject`,
`acl:predicate` and `acl:object`. These are all optional but if present
must be a ShEx shape expression; a `shex:TripleConstraint` or a
`shex:NodeConstraint`. A triple passes the constraint (access will be
granted) if each member of the triple conforms to the specified shape;
if a property is omitted then any value passes.

Currently a very small subset of ShEx is supported, consisting of:

* NodeConstraints with `shex:values` only.
* TripleConstraints with `shex:predicate` and `shex:valueExpr` only.

Cardinalities other than 'exactly one' are not supported, nor are the
logical operations or any conditions on literals.

## Example

An example user account with some permission grants might be:

prefix ex: <http://example.org#> .

ex:user acl:username "user";
acl:readTriple [
a acl:ShexCondition;
acl:subject [
a shex:TripleConstraint;
shex:predicate rdf:type;
shex:valueExpr [
a shex:NodeConstraint;
shex:values (ex:Class);
]]].

This permits the `user` user to read any triples with a subject in the
class `ex:Class`.

## Implementation

The ACL layer is implemented primarily in a DatasetGraph subclass; this
is an object which presents an RDF dataset to the rest of Jena. An
Assembler module is provided which can be used in the config file to
create an FPDatasetGraph; this wraps another dataset and implements
access control.

The DatasetGraph API does not have access to request information to
perform authorisation. For this reason we search for endpoints accessing
our dataset and replace the sparql query operation handler with a
subclass; this deals with extracting the username from the request and
injecting it into the dataset used to perform the query.

The ShEx shapes are stored in the graph; unfortunately, although ShEx is
defined as an RDF data model, the Jena implementation can only parse
shapes from JSON or ShExC files. This means we need to traverse the RDF
and build the ShapeExpression objects by hand; this is why only a subset
of ShEx is currently implemented. This job is performed by an
FPShapeBuilder, which builds an FPShapeEvaluator containing a ShexSchema
and a list of triples representing the ShexConditions. Currently this
step is performed for every request but this could be optimised.
79 changes: 79 additions & 0 deletions acs-fuseki/config.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0

## Fuseki Server configuration file.

@prefix : <#> .
@prefix f: <http://jena.apache.org/fuseki#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> .
@prefix rr: <http://jena.hpl.hp.com/2003/RuleReasoner#> .
@prefix tdb: <http://jena.hpl.hp.com/2008/tdb#> .
@prefix acl: <http://factoryplus.app.amrc.co.uk/rdf/2025-05/ac-24-611/acl#>

[] rdf:type f:Server ;
# Example::
# Server-wide query timeout.
#
# Timeout - server-wide default: milliseconds.
# Format 1: "1000" -- 1 second timeout
# Format 2: "10000,60000" -- 10s timeout to first result,
# then 60s timeout for the rest of query.
#
# See javadoc for ARQ.queryTimeout for details.
# This can also be set on a per dataset basis in the dataset assembler.
#
# ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "30000" ] ;

# Add any custom classes you want to load.
# Must have a "public static void init()" method.
# ja:loadClass "your.code.Class" ;

# End triples.
.

acl:DatasetFPAcl ja:assembler "uk.co.amrc.factoryplus.fuseki.FPAclDatasetAssembler".

:svc rdf:type f:Service;
f:name "ds";
f:endpoint [
f:operation f:query;
f:name "sparql"
];
f:endpoint [
f:operation f:update;
f:name "sparql"
];

f:dataset :ds;
.

:ds a acl:DatasetFPAcl;
ja:dataset :tdb;
.

:tdb rdf:type tdb:DatasetTDB;
tdb:location "db";
.

#:ds rdf:type ja:RDFDataset;
# ja:defaultGraph :graph;
# .

#:inference rdf:type ja:InfModel;
# ja:baseModel :graph;
# ja:reasoner [ja:reasonerClass "openllet.jena.PelletReasonerFactory"];
# .

#:inference rdf:type ja:InfModel;
# ja:baseModel :graph;
# ja:reasoner [
# ja:rulesFrom <file:///fuseki/rules/rules.jena>;
# rr:enableTGCCaching "true";
# ];
# .

#:graph rdf:type tdb:GraphTDB;
# tdb:dataset :tdb;
# .

153 changes: 153 additions & 0 deletions acs-fuseki/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>uk.co.amrc.factoryplus.fuseki</groupId>
<artifactId>acs-fuseki</artifactId>
<version>0.0.1</version>

<packaging>jar</packaging>
<name>acs-fuseki</name>

<url>https://github.com/amrc-factoryplus</url>

<properties>
<!-- Build properties -->
<build.time.xsd>${maven.build.timestamp}</build.time.xsd>
<automatic.module.name>uk.co.amrc.factoryplus.fuseki</automatic.module.name>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>

<!-- Maven Plugins -->
<plugin.clean>3.4.1</plugin.clean>
<plugin.compiler>3.14.0</plugin.compiler>
<plugin.cyclonedx>2.9.1</plugin.cyclonedx>
<plugin.dependency>3.8.1</plugin.dependency>
<plugin.deploy>3.1.4</plugin.deploy>
<plugin.enforcer>3.5.0</plugin.enforcer>
<plugin.gpg>3.2.7</plugin.gpg>
<plugin.install>3.1.4</plugin.install>
<plugin.jacoco>0.8.12</plugin.jacoco>
<plugin.jar>3.4.2</plugin.jar>
<plugin.javadoc>3.11.2</plugin.javadoc>
<plugin.nexus>1.7.0</plugin.nexus>
<plugin.resources>3.3.1</plugin.resources>
<plugin.shade>3.6.0</plugin.shade>
<plugin.site>3.21.0</plugin.site>
<plugin.source>3.3.1</plugin.source>
<plugin.surefire>3.5.2</plugin.surefire>

<!-- Dependency Plugins -->
<dependency.jena>5.3.0</dependency.jena>
<dependency.assert>3.27.3</dependency.assert>
<dependency.log4j2>2.24.3</dependency.log4j2>
<dependency.slf4j>2.0.17</dependency.slf4j>
</properties>

<dependencies>
<dependency>
<groupId>org.apache.jena</groupId>
<artifactId>apache-jena-libs</artifactId>
<version>${dependency.jena}</version>
<type>pom</type>
</dependency>

<dependency>
<groupId>org.apache.jena</groupId>
<artifactId>jena-fuseki-main</artifactId>
<version>${dependency.jena}</version>
</dependency>

<dependency>
<groupId>org.apache.jena</groupId>
<artifactId>jena-cmds</artifactId>
<version>${dependency.jena}</version>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${dependency.log4j2}</version>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>${dependency.log4j2}</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${dependency.slf4j}</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${plugin.compiler}</version>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${plugin.shade}</version>
<configuration>
<shadedArtifactAttached>false</shadedArtifactAttached>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>uk.co.amrc.factoryplus.fuseki.RunFuseki</mainClass>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
<transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />
<transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
<addHeader>false</addHeader>
</transformer>
</transformers>

<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<!-- Don't include signing files. -->
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>META-INF/DEPENDENCIES</exclude>
<exclude>META-INF/MANIFEST.MF</exclude>
<!-- Don't include modules ("Shading will break its strong encapsulation.") -->
<exclude>**/module-info.class</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<!--<phase /><!- - Switch off -->
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
13 changes: 13 additions & 0 deletions acs-fuseki/src/main/java/uk/co/amrc/factoryplus/fuseki/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package uk.co.amrc.factoryplus.fuseki;

/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
}
Loading