diff --git a/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/AccessCtl_SPARQL_QueryDataset.java b/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/AccessCtl_SPARQL_QueryDataset.java index 0ca6f9c6549..67387766e7d 100644 --- a/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/AccessCtl_SPARQL_QueryDataset.java +++ b/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/AccessCtl_SPARQL_QueryDataset.java @@ -26,15 +26,19 @@ import java.util.function.Function; import org.apache.jena.atlas.lib.Pair; +import org.apache.jena.atlas.logging.Log; import org.apache.jena.fuseki.servlets.*; import org.apache.jena.query.Query; 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 requestUser; public AccessCtl_SPARQL_QueryDataset(Function requestUser) { @@ -54,17 +58,20 @@ protected Collection customParams() { */ @Override protected Pair decideDataset(HttpAction action, Query query, String queryStringLog) { + Log.debug(this, "decideDataset"); DatasetGraph dsg = action.getActiveDSG(); if ( ! DataAccessCtl.isAccessControlled(dsg) ) 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); } private DatasetGraph dynamicDataset(HttpAction action, Query query, DatasetGraph dsg0, DatasetDescription dsDesc0, SecurityContext sCxt) { + Log.debug(this, "dynamicDataset"); if ( dsDesc0 == null ) return dsg0; if ( ! ALLOW_FROM ) @@ -104,8 +111,27 @@ private List mask(List graphURIs, SecurityContext sCxt) { .collect(toList()); } + // Note: This is called before both decideDataset() and createQueryExecution() + @Override + protected void execute(String queryString, HttpAction action) { + Log.debug(this, "execute"); + action.getContext().set(symSecQuery, queryString); + super.execute(queryString, action); + } + + private SecurityContext getSecurityContext(HttpAction action, DatasetGraph dsg, Function requestUser) { + SecurityContext sCxt = DataAccessLib.getSecurityContext(action, dsg, requestUser); + if ( sCxt instanceof SecurityContextDynamic ) { + Log.debug(this, "Dynamic mode for user " + requestUser.toString()); + String queryString = action.getContext().getAsString(symSecQuery); + return ((SecurityContextDynamic) sCxt).instantiateFromQuery(queryString); + } + return sCxt; + } + @Override protected QueryExecution createQueryExecution(HttpAction action, Query query, DatasetGraph target) { + Log.debug(this, "createQueryExecution"); if ( ! ALLOW_FROM ) { if ( target instanceof DynamicDatasetGraph ) // Protocol query/FROM should have been caught by decideDataset @@ -120,7 +146,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; diff --git a/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/AssemblerSecurityRegistry.java b/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/AssemblerSecurityRegistry.java index 92bdf05da4c..c6321acf188 100644 --- a/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/AssemblerSecurityRegistry.java +++ b/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/AssemblerSecurityRegistry.java @@ -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 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; diff --git a/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/SecurityContext.java b/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/SecurityContext.java index 8800a33f452..63cd09be3bb 100644 --- a/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/SecurityContext.java +++ b/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/SecurityContext.java @@ -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 names = Iter.toList(dsg.listGraphNodes()); //return new SecurityContextAllowNamedGraphs(dsg); diff --git a/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/SecurityContextDynamic.java b/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/SecurityContextDynamic.java new file mode 100644 index 00000000000..915df40e560 --- /dev/null +++ b/jena-fuseki2/jena-fuseki-access/src/main/java/org/apache/jena/fuseki/access/SecurityContextDynamic.java @@ -0,0 +1,91 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.Quad; + +/** 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 ||..| + * + */ +public class SecurityContextDynamic implements SecurityContext { + + public static final Node dynamicAccess = NodeFactory.createURI("urn:jena:accessGraphsDynamic"); + + private static final Pattern GRAPH_PRAGMA_DELIMITER_PATTERN = Pattern.compile("\\|"); + 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 SecurityContextDynamic() {} + + @Override + public Collection visibleGraphs() { + return Collections.emptyList(); + } + + @Override + public boolean visableDefaultGraph() { return false; } + + @Override + public Predicate predicateQuad() { return q -> false; } + + @Override + public void filterTDB(DatasetGraph dsg, QueryExecution qExec) { + Predicate pred = tuple->false; + qExec.getContext().set(GraphFilter.getContextKey(dsg), pred); + } + + public SecurityContext instantiateFromQuery(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); + return new SecurityContextView(graphs); + } + } + return new SecurityContextAllowNone(); + } +}