blob: 5d823fad799821605ac40bcff1cc886eb682e64e [file] [log] [blame]
/*
* 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.main.access;
import static org.apache.jena.fuseki.main.access.AccessTestLib.assertSeen;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.jena.atlas.iterator.Iter;
import org.apache.jena.atlas.lib.SetUtils;
import org.apache.jena.atlas.lib.StrUtils;
import org.apache.jena.atlas.logging.LogCtl;
import org.apache.jena.atlas.web.HttpException;
import org.apache.jena.fuseki.Fuseki;
import org.apache.jena.fuseki.main.FusekiLib;
import org.apache.jena.fuseki.main.FusekiServer;
import org.apache.jena.graph.Node;
import org.apache.jena.query.Dataset;
import org.apache.jena.query.QuerySolution;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.rdfconnection.RDFConnection;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.sparql.core.Quad;
import org.apache.jena.sparql.sse.SSE;
import org.apache.jena.sys.JenaSystem;
import org.apache.jena.system.Txn;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
/**
* Tests on the assembler for data access control.
* <ul>
* <li>assem-security.ttl - two services "/database" and "/plain" each with their own dataset.
* <li>assem-security-shared.ttl - two services "/database" and "/plain" with a shared dataset.
* </ul>
*
* @see TestSecurityFilterFuseki TestSecurityFilterFuseki for other HTTP tests.
*/
public abstract class AbstractTestFusekiSecurityAssembler {
static { JenaSystem.init(); }
static final String DIR = "testing/Access/";
private final String assemblerFile;
private static AtomicReference<String> user = new AtomicReference<>();
private boolean sharedDatabase;
private static FusekiServer server;
private FusekiServer getServer() {
if ( server == null )
server = setup(assemblerFile, false);
return server;
}
@AfterClass public static void afterClass() {
server.stop();
server = null;
user.set(null);
}
@Before
public void before() {
user.set(null);
}
private String getURL() {
FusekiServer server = getServer();
return server.datasetURL("/database");
}
private static FusekiServer setup(String assembler, boolean sharedDatabase) {
// This will have a warning because authentication is not set (no password
// file, no security handler) and that's what we want - no authentication -
// because we use "user.get()"in the tests.
//
// Altering the logging level is simply to avoid the Fuseki.configLog message
// in "build()" without turning warnings off everywhere.
// -- Start log manipulation.
String level = LogCtl.getLevel(Fuseki.configLog.getName());
LogCtl.disable(Fuseki.configLog);
// In case Fuseki.configLog is active - make sure the test log shows the build()
// message is expected.
Fuseki.configLog.warn(" (Expect one warning here)");
FusekiServer server = FusekiServer.create()
.port(0)
.parseConfigFile(assembler)
.build();
// Special way to get the servlet remote user (the authorized principle).
FusekiLib.modifyForAccessCtl(server.getDataAccessPointRegistry(), (a)->user.get());
server.start();
LogCtl.setLevel(Fuseki.configLog, level);
// -- End log manipulation.
if ( sharedDatabase ) {
String data = StrUtils.strjoinNL
("PREFIX : <http://example/>"
,"INSERT DATA {"
," :s0 :p :o ."
," GRAPH <http://host/graphname1> {:s1 :p :o}"
," GRAPH <http://host/graphname3> {:s3 :p :o}"
," GRAPH <http://host/graphname9> {:s9 :p :o}"
,"}"
);
String plainUrl = server.datasetURL("/plain");
try(RDFConnection conn = RDFConnection.connect(plainUrl)) {
conn.update(data);
}
} else {
DatasetGraph dsg = server.getDataAccessPointRegistry().get("/database").getDataService().getDataset();
Txn.executeWrite(dsg, ()->{
dsg.add(SSE.parseQuad("(<http://host/graphname1> :s1 :p :o)"));
dsg.add(SSE.parseQuad("(<http://host/graphname3> :s3 :p :o)"));
dsg.add(SSE.parseQuad("(<http://host/graphname9> :s9 :p :o)"));
});
}
return server;
}
protected AbstractTestFusekiSecurityAssembler(String assemberFile, boolean sharedDatabase) {
this.assemblerFile = assemberFile;
this.sharedDatabase = sharedDatabase;
}
private static Node s1 = SSE.parseNode(":s1");
private static Node s2 = SSE.parseNode(":s2");
private static Node s3 = SSE.parseNode(":s3");
private static Node s9 = SSE.parseNode(":s9");
// The access controlled dataset.
// { SecurityRegistry
// user1 -> dft:false / [http://host/graphname2, http://host/graphname1, http://host/graphname3]
// user2 -> dft:false / [http://host/graphname9]
// userZ -> dft:false / [http://host/graphnameZ]
// user3 -> dft:false / [http://host/graphname4, http://host/graphname3, http://host/graphname5]
// }
@Test public void query_user1() {
user.set("user1");
try(RDFConnection conn = RDFConnection.connect(getURL())) {
Set<Node> visible = query(conn, "SELECT * { GRAPH ?g { ?s ?p ?o }}");
assertSeen(visible, s1, s3);
}
}
@Test public void query_userX() {
user.set("userX"); // No such user in the registry
try(RDFConnection conn = RDFConnection.connect(getURL())) {
Set<Node> visible = query(conn, "SELECT * { GRAPH ?g { ?s ?p ?o }}");
assertSeen(visible);
}
}
@Test public void query_no_user() {
user.set(null); // No user.
try(RDFConnection conn = RDFConnection.connect(getURL())) {
Set<Node> visible = query(conn, "SELECT * { GRAPH ?g { ?s ?p ?o }}");
assertSeen(visible);
}
}
@Test public void query_user2() {
user.set("user2");
try(RDFConnection conn = RDFConnection.connect(getURL())) {
Set<Node> visible = query(conn, "SELECT * { GRAPH ?g { ?s ?p ?o }}");
assertSeen(visible, s9);
}
}
@Test public void query_userZ() {
user.set("userZ"); // No graphs with data.
try(RDFConnection conn = RDFConnection.connect(getURL())) {
Set<Node> visible = query(conn, "SELECT * { GRAPH ?g { ?s ?p ?o }}");
assertSeen(visible);
}
}
// GSP. "http://host/graphname1"
@Test public void gsp_dft_user1() {
user.set("user1");
try(RDFConnection conn = RDFConnection.connect(getURL())) {
Set<Node> visible = gsp(conn, null);
assertSeen(visible);
}
}
@Test public void gsp_ng_user1() {
user.set("user1");
try(RDFConnection conn = RDFConnection.connect(getURL())) {
Set<Node> visible = gsp(conn, "http://host/graphname1");
assertSeen(visible, s1);
}
}
@Test public void gsp_dft_user2() {
user.set("user2");
try(RDFConnection conn = RDFConnection.connect(getURL())) {
gsp404(conn, null);
}
}
@Test public void gsp_ng_user2() {
user.set("user2");
try(RDFConnection conn = RDFConnection.connect(getURL())) {
gsp404(conn, "http://host/graphname1");
}
}
@Test public void gsp_dft_userX() {
user.set("userX");
try(RDFConnection conn = RDFConnection.connect(getURL())) {
gsp404(conn, null);
}
}
@Test public void gsp_ng_userX() {
user.set("userX");
try(RDFConnection conn = RDFConnection.connect(getURL())) {
gsp404(conn, "http://host/graphname1");
}
}
@Test public void gsp_dft_user_null() {
user.set(null);
try(RDFConnection conn = RDFConnection.connect(getURL())) {
gsp404(conn, null);
}
}
@Test public void gsp_ng_user_null() {
try(RDFConnection conn = RDFConnection.connect(getURL())) {
gsp404(conn, "http://host/graphname1");
}
}
// // Quads
// user.set("user1");
// try(RDFConnection conn = RDFConnection.connect(getURL())) {
// Set<Node> visible = dataset(conn);
// assertSeen(visible, s1, s3);
// }
// user.set("user2");
// try(RDFConnection conn = RDFConnection.connect(getURL())) {
// Set<Node> visible = dataset(conn);
// assertSeen(visible, s9);
// }
// user.set("userX");
// try(RDFConnection conn = RDFConnection.connect(getURL())) {
// Set<Node> visible = dataset(conn);
// assertSeen(visible);
// }
// user.set(null);
// try(RDFConnection conn = RDFConnection.connect(getURL())) {
// Set<Node> visible = dataset(conn);
// assertSeen(visible);
// }
private Set<Node> gsp(RDFConnection conn, String graphName) {
Set<Node> results = new HashSet<>();
Model model = graphName == null ? conn.fetch() : conn.fetch(graphName);
// Extract subjects.
Set<Node> seen =
SetUtils.toSet(
Iter.asStream(model.listSubjects())
.map(Resource::asNode)
);
return seen;
}
private void gsp404(RDFConnection conn, String graphName) {
gspHttp(conn, 404, graphName);
}
private void gspHttp(RDFConnection conn, int statusCode, String graphName) {
try {
gsp(conn, graphName);
if ( statusCode < 200 && statusCode > 299 )
fail("Should have responded with "+statusCode);
} catch (HttpException ex) {
assertEquals(statusCode, ex.getStatusCode());
}
}
private Set<Node> dataset(RDFConnection conn) {
Dataset ds = conn.fetchDataset();
Set<Node> seen =
SetUtils.toSet(
Iter.asStream(ds.asDatasetGraph().find())
.map(Quad::getSubject)
);
return seen;
}
private Set<Node> query(RDFConnection conn, String queryString) {
Set<Node> results = new HashSet<>();
conn.queryResultSet(queryString, rs->{
List<QuerySolution> list = Iter.toList(rs);
list.stream()
.map(qs->qs.get("s"))
.filter(Objects::nonNull)
.map(RDFNode::asNode)
.forEach(n->results.add(n));
});
return results;
}
}