Skip to content

Commit

Permalink
jena-fuseki-access - Dynamic Graph selection at query time
Browse files Browse the repository at this point in the history
- "#pragma acl.graphs" preamble can be used to specify one or more
  graphs to restriction query exectution to
- The functionality is only enabled if an access:entry for a specific user
  contains just <urn:jena:accessGraphsDynamic>
- Only applies to SPARQL query, not GSP (since the latter can already be
  filtered dynamically)
- Requires context propagation patch (see apache#1291), since query
  is stored in request-specific context
  • Loading branch information
vtermanis committed Jul 14, 2022
1 parent 733aa9b commit 7f8f46a
Show file tree
Hide file tree
Showing 13 changed files with 356 additions and 8 deletions.
9 changes: 9 additions & 0 deletions jena-fuseki2/jena-fuseki-access/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
</parent>

<packaging>jar</packaging>


<properties>
<automatic.module.name>org.apache.jena.fuseki.access</automatic.module.name>
Expand All @@ -47,6 +48,13 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>pl.pragmatists</groupId>
<artifactId>JUnitParams</artifactId>
<version>1.1.1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
Expand Down Expand Up @@ -80,6 +88,7 @@
</execution>
</executions>
</plugin>


<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@
import org.apache.jena.query.QueryExecution;
import org.apache.jena.sparql.core.*;
import org.apache.jena.sparql.core.DynamicDatasets.DynamicDatasetGraph;
import org.apache.jena.sparql.util.Symbol;

/** A Query {@link ActionService} that inserts a security filter on each query. */
final
public class AccessCtl_SPARQL_QueryDataset extends SPARQL_QueryDataset {
// For storing original query in execution context. (Used for SecurityContextDynamic support.)
private static final Symbol symSecQuery = Symbol.create(VocabSecurity.getURI() + "secCxtQuery");
private final Function<HttpAction, String> requestUser;

public AccessCtl_SPARQL_QueryDataset(Function<HttpAction, String> requestUser) {
Expand All @@ -59,7 +62,7 @@ protected Pair<DatasetGraph, Query> decideDataset(HttpAction action, Query query
return super.decideDataset(action, query, queryStringLog);

DatasetDescription dsDesc0 = SPARQLProtocol.getDatasetDescription(action, query);
SecurityContext sCxt = DataAccessLib.getSecurityContext(action, dsg, requestUser);
SecurityContext sCxt = getSecurityContext(action, dsg, requestUser);
DatasetGraph dsg2 = dynamicDataset(action, query, dsg, dsDesc0, sCxt);
return Pair.create(dsg2, query);
}
Expand Down Expand Up @@ -104,6 +107,23 @@ private List<String> mask(List<String> graphURIs, SecurityContext sCxt) {
.collect(toList());
}

// TODO: This is called before both decideDataset() and createQueryExecution(). Calculating the query-specific set
// of visible graphs twice seems wrong.
@Override
protected void execute(String queryString, HttpAction action) {
action.getContext().set(symSecQuery, queryString);
super.execute(queryString, action);
}

private SecurityContext getSecurityContext(HttpAction action, DatasetGraph dsg, Function<HttpAction, String> requestUser) {
SecurityContext sCxt = DataAccessLib.getSecurityContext(action, dsg, requestUser);
if ( sCxt instanceof SecurityContextDynamic ) {
String queryString = action.getContext().getAsString(symSecQuery);
return SecurityContextDynamic.forQuery(queryString);
}
return sCxt;
}

@Override
protected QueryExecution createQueryExecution(HttpAction action, Query query, DatasetGraph target) {
if ( ! ALLOW_FROM ) {
Expand All @@ -120,7 +140,7 @@ protected QueryExecution createQueryExecution(HttpAction action, Query query, Da
if ( ! DataAccessCtl.isAccessControlled(dsg) )
return super.createQueryExecution(action, query, target);

SecurityContext sCxt = DataAccessLib.getSecurityContext(action, dsg, requestUser);
SecurityContext sCxt = getSecurityContext(action, dsg, requestUser);
// A QueryExecution for controlled access
QueryExecution qExec = sCxt.createQueryExecution(action, query, target);
return qExec;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,12 @@ public AuthorizationService open(Assembler a, Resource root, Mode mode) {
});

map.keySet().forEach(u->{
SecurityContext sCxt = new SecurityContextView(map.get(u));
registry.put(u, sCxt);
Collection<Node> visibleGraphs = map.get(u);
if ( visibleGraphs.size() == 1 && visibleGraphs.contains(SecurityContextDynamic.dynamicAccess)) {
registry.put(u, SecurityContext.DYNAMIC);
} else {
registry.put(u, new SecurityContextView(visibleGraphs));
}
});

return registry;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
public interface SecurityContext {
public static final SecurityContext NONE = new SecurityContextAllowNone();
public static final SecurityContext ALL = new SecurityContextAllowAll();
public static final SecurityContext DYNAMIC = new SecurityContextDynamic();
public static SecurityContext ALL_NG(DatasetGraph dsg) {
Collection<Node> names = Iter.toList(dsg.listGraphNodes());
//return new SecurityContextAllowNamedGraphs(dsg);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.jena.fuseki.access;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;

/** A {@link SecurityContext} that provides no access itself but indicates that the set of visible graphs should be
* constructed from a set embedded in the query. Use {@link SecurityContextDynamic#instantiateFromQuery} to create the
* query-specific set of graphs to filter.
* This context is not meant to be used directly by end users: The initial query has a list of visible graphs prepended
* before execution based on use-case specific criteria, with the following format:
*
* #pragma acl.graphs <graph1>|<graph2>|..|<graphN>
*
*/
public class SecurityContextDynamic extends SecurityContextAllowNone {

public static final Node dynamicAccess = NodeFactory.createURI("urn:jena:accessGraphsDynamic");

public static final String GRAPH_PRAGMA_DELIMITER = "|";
private static final Pattern GRAPH_PRAGMA_DELIMITER_PATTERN = Pattern.compile("\\" + GRAPH_PRAGMA_DELIMITER);
private static final String GRAPH_PRAGMA_PATTERN_GROUP = "graphs";
private static final Pattern GRAPH_PRAGMA_PATTERN = Pattern.compile(
"^" +
// Leading whitespace
"\\s*" +
// For simplicity, expect pragma on first non-whitespace line
"#pragma acl\\.graphs\\h+(?<" + GRAPH_PRAGMA_PATTERN_GROUP + ">\\S*)\\h*\\v" +
// Anything else
".*",
Pattern.DOTALL
);

public static SecurityContext forQuery(String queryString) {
if ( queryString != null ) {
Matcher m = GRAPH_PRAGMA_PATTERN.matcher(queryString);
if ( m.matches() ) {
String[] graphs = GRAPH_PRAGMA_DELIMITER_PATTERN
.splitAsStream(m.group(GRAPH_PRAGMA_PATTERN_GROUP))
.map(String::trim)
.toArray(String[]::new);
if ( graphs.length > 0 ) {
return new SecurityContextView(graphs);
}
}
}
/* Note: SecurityContext.ALL is not supported since according to AssemblerSecurityRegistry support for
* urn:jena:accessAllGraphs (*) and urn:jena:accessAllNamedGraphs (**) is unfinished.
*/
return SecurityContext.NONE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
// These integration test are in jena-fuseki-main:org.apache.jena.fuseki.main.access
TestSecurityFilterLocal.class
, TestSecurityRegistry.class
, TestSecurityContextDynamic.class
})

public class TS_Access {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.jena.fuseki.access;

import java.util.Arrays;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

// Note: JUnit 4 does not support parameterized tests
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;

import org.junit.Test;
import org.junit.runner.RunWith;

/** Test parsing of assemblers with security aspects */
@RunWith(JUnitParamsRunner.class)
public class TestSecurityContextDynamic {

private static final String ACL_PRAGMA = "#pragma acl.graphs";
private static final String ONE_GRAPH_PRAGMA = ACL_PRAGMA + " some:graph";

@Test
@Parameters({
// empty
"",
// other comments before pragma
"# Comment\n" + ONE_GRAPH_PRAGMA,
// pragma after grammar start
"PREFIX ex: http://example.org/\n" + ONE_GRAPH_PRAGMA,
// without space delimiter
ACL_PRAGMA + "some:graph",
// with empty graph list
ACL_PRAGMA + " ",
})
public void forQuery_returns_allow_none_context_when_pragma_not_matched(String preamble) {
final String query = "\nSELECT 1 {}";

SecurityContext sCxt = SecurityContextDynamic.forQuery(preamble + query);

assertEquals(SecurityContext.NONE, sCxt);
}

@Test
@Parameters(method="graphValues")
public void forQuery_returns_expected_context_when_pragma_matched(String[] graphs) {
final String query = "\nSELECT 1 {}";

String graphString = String.join(SecurityContextDynamic.GRAPH_PRAGMA_DELIMITER, graphs);

SecurityContext sCxt = SecurityContextDynamic.forQuery(ACL_PRAGMA + " " + graphString + query);

assertFalse(sCxt.visableDefaultGraph());
assertEquals(graphs.length, sCxt.visibleGraphs().size());
assertTrue(sCxt.visibleGraphNames().containsAll(Arrays.asList(graphs)));
}
private Object[] graphValues() {
return new Object[]{
new Object[]{new String[]{"graph:one"}},
new Object[]{new String[]{"graph:one", "graph:two"}},
new Object[]{new String[]{"graph:one", "graph:two", "graph:three"}},
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
package org.apache.jena.fuseki.access;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;

import org.apache.jena.graph.Node;
Expand Down Expand Up @@ -69,6 +71,15 @@ public class TestSecurityRegistry {
assertEquals(SecurityContext.allGraphs, x);
}

{
SecurityContext sCxt = authService.get("user4");
assertTrue(sCxt instanceof SecurityContextDynamic);
// In dynamic mode, the security context forbids everything
assertFalse(sCxt.visableDefaultGraph());
assertEquals(0, sCxt.visibleGraphs().size());

}

{
SecurityContext sCxt = authService.get("*");
assertEquals(1, sCxt.visibleGraphs().size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ PREFIX ja: <http://jena.hpl.hp.com/2005/11/Assembler#>
PREFIX access: <http://jena.apache.org/access#>

<#securityRegistry> rdf:type access:SecurityRegistry ;
## user1, all named graphs
## user1, all named graphs
access:entry ("user1" "*") ;
## user2, all graphs
access:entry ("user2" "**") ;
## user3, all graphs, as named + dft.
access:entry ("user3" "*" <urn:x-arq:DefaultGraph>) ;
## user4, dynamic mode marker
access:entry ("user4" <urn:jena:accessGraphsDynamic>) ;
## any user
access:entry ("*" <http://host/graphname1> ) ;
.
Loading

0 comments on commit 7f8f46a

Please sign in to comment.