You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2018/11/17 17:20:44 UTC
[18/34] jena git commit: JENA-1623: Service authorization by HTTP
authentication.
JENA-1623: Service authorization by HTTP authentication.
Project: http://git-wip-us.apache.org/repos/asf/jena/repo
Commit: http://git-wip-us.apache.org/repos/asf/jena/commit/83858ba1
Tree: http://git-wip-us.apache.org/repos/asf/jena/tree/83858ba1
Diff: http://git-wip-us.apache.org/repos/asf/jena/diff/83858ba1
Branch: refs/heads/master
Commit: 83858ba1145674ed14331e4c73863f72de75e99f
Parents: 34b89a6
Author: Andy Seaborne <an...@apache.org>
Authored: Fri Nov 2 17:21:19 2018 +0000
Committer: Andy Seaborne <an...@apache.org>
Committed: Tue Nov 13 15:39:14 2018 +0000
----------------------------------------------------------------------
.../jena/fuseki/access/TestPasswordAccess.java | 101 +++++++++++++++++--
.../fuseki/access/TestSecurityFilterFuseki.java | 19 ++++
.../jena/fuseki/build/FusekiBuildLib.java | 28 +++++
.../apache/jena/fuseki/build/FusekiConfig.java | 56 ++++++++--
.../apache/jena/fuseki/server/DataService.java | 7 ++
.../jena/fuseki/servlets/ActionService.java | 10 ++
6 files changed, 206 insertions(+), 15 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/jena/blob/83858ba1/jena-fuseki2/jena-fuseki-access/src/test/java/org/apache/jena/fuseki/access/TestPasswordAccess.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-access/src/test/java/org/apache/jena/fuseki/access/TestPasswordAccess.java b/jena-fuseki2/jena-fuseki-access/src/test/java/org/apache/jena/fuseki/access/TestPasswordAccess.java
index d5506f8..d81de8c 100644
--- a/jena-fuseki2/jena-fuseki-access/src/test/java/org/apache/jena/fuseki/access/TestPasswordAccess.java
+++ b/jena-fuseki2/jena-fuseki-access/src/test/java/org/apache/jena/fuseki/access/TestPasswordAccess.java
@@ -20,18 +20,25 @@ package org.apache.jena.fuseki.access;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+
+import java.util.Arrays;
import org.apache.http.client.HttpClient;
import org.apache.jena.atlas.logging.LogCtl;
import org.apache.jena.atlas.web.HttpException;
import org.apache.jena.atlas.web.TypedInputStream;
import org.apache.jena.atlas.web.WebLib;
+import org.apache.jena.fuseki.build.FusekiBuilder;
import org.apache.jena.fuseki.jetty.JettyLib;
import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.server.DataService;
import org.apache.jena.query.DatasetFactory;
import org.apache.jena.riot.web.HttpCaptureResponse;
import org.apache.jena.riot.web.HttpOp;
import org.apache.jena.riot.web.HttpOp.CaptureInput;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
import org.apache.jena.web.HttpSC;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.UserStore;
@@ -40,13 +47,19 @@ import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
-/** Tests for password access to a server. */
+/** Tests for password access to a server and to services. */
public class TestPasswordAccess {
private static FusekiServer fusekiServer = null;
private static int port = WebLib.choosePort();
private static String serverURL = "http://localhost:"+port+"/";
- private static AuthSetup authSetup = new AuthSetup("localhost", port, "user1", "pw1", "TripleStore");
+ private static AuthSetup authSetup1 = new AuthSetup("localhost", port, "user1", "pw1", "TripleStore");
+ private static AuthSetup authSetup2 = new AuthSetup("localhost", port, "user2", "pw2", "TripleStore");
+ // Not in the user store.
+ private static AuthSetup authSetupX = new AuthSetup("localhost", port, "userX", "pwX", "TripleStore");
+
+ // All users must log in to access any resource. Disables some "open" tests.
+ private static boolean ConstraintServer = true;
@BeforeClass
public static void beforeClass() {
@@ -54,18 +67,37 @@ public class TestPasswordAccess {
// To watch the HTTP headers
LogCtl.enable("org.apache.http.headers");
- UserStore userStore = JettyLib.makeUserStore(authSetup.user, authSetup.password);
- ConstraintSecurityHandler sh = JettyLib.makeSecurityHandler(authSetup.realm, userStore);
+ // Two authorized users.
+ UserStore userStore = new UserStore();
+ JettyLib.addUser(userStore, authSetup1.user, authSetup1.password);
+ JettyLib.addUser(userStore, authSetup2.user, authSetup2.password);
+ try { userStore.start(); }
+ catch (Exception ex) { throw new RuntimeException("UserStore", ex); }
+
+ ConstraintSecurityHandler sh = JettyLib.makeSecurityHandler(authSetup1.realm, userStore);
// Secure these areas.
JettyLib.addPathConstraint(sh, "/ds");
+ // Allow auth control even through there isn't anything there
JettyLib.addPathConstraint(sh, "/nowhere");
+
+ JettyLib.addPathConstraint(sh, "/ctl");
+ // Not controlled: JettyLib.addPathConstraint(sh, "/open");
+
+ // Server wide (breaks "open" tests)
+ if ( ConstraintServer )
+ JettyLib.addPathConstraint(sh, "/*");
+
+ DataService dSrv = new DataService(DatasetGraphFactory.createTxnMem());
+ FusekiBuilder.populateStdServices(dSrv, false);
+ dSrv.setAllowedUsers(Arrays.asList("user1"));
fusekiServer =
FusekiServer.create()
.port(port)
.add("/ds", DatasetFactory.createTxnMem())
.add("/open", DatasetFactory.createTxnMem())
+ .add("/ctl", dSrv)
.securityHandler(sh)
//.staticFileBase(".")
.build();
@@ -87,9 +119,13 @@ public class TestPasswordAccess {
HttpOp.setDefaultHttpClient(hc);
}
+ // Server authentication.
+
@Test public void access_server() {
+ assumeFalse(ConstraintServer);
try( TypedInputStream in = HttpOp.execHttpGet(serverURL) ) {
assertNotNull(in);
+ fail("Didn't expect to succeed");
} catch (HttpException ex) {
// 404 is OK - no static file area.
if ( ex.getResponseCode() != HttpSC.NOT_FOUND_404 )
@@ -98,15 +134,32 @@ public class TestPasswordAccess {
}
@Test public void access_open() {
+ assumeFalse(ConstraintServer);
try( TypedInputStream in = HttpOp.execHttpGet(serverURL+"open") ) {
assertNotNull(in);
}
}
+ @Test public void access_open_user1() {
+ // OK.
+ LibSec.withAuth(serverURL+"open", authSetup1, (conn)->{
+ conn.queryAsk("ASK{}");
+ });
+ }
+
+ @Test public void access_open_userX() {
+ assumeFalse(ConstraintServer);
+ // OK.
+ LibSec.withAuth(serverURL+"open", authSetupX, (conn)->{
+ conn.queryAsk("ASK{}");
+ });
+ }
+
+
// Should fail.
@Test public void access_deny_ds() {
try( TypedInputStream in = HttpOp.execHttpGet(serverURL+"ds") ) {
- assertNotNull(in);
+ fail("Didn't expect to succeed");
} catch (HttpException ex) {
if ( ex.getResponseCode() != HttpSC.UNAUTHORIZED_401 )
throw ex;
@@ -116,7 +169,7 @@ public class TestPasswordAccess {
// Should be 401, not be 404.
@Test public void access_deny_nowhere() {
try( TypedInputStream in = HttpOp.execHttpGet(serverURL+"nowhere") ) {
- assertNotNull(in);
+ fail("Didn't expect to succeed");
} catch (HttpException ex) {
if ( ex.getResponseCode() != HttpSC.UNAUTHORIZED_401 )
throw ex;
@@ -124,7 +177,7 @@ public class TestPasswordAccess {
}
@Test public void access_allow_nowhere() {
- HttpClient hc = LibSec.httpClient(authSetup);
+ HttpClient hc = LibSec.httpClient(authSetup1);
HttpCaptureResponse<TypedInputStream> handler = new CaptureInput();
try( TypedInputStream in = HttpOp.execHttpGet(serverURL+"nowhere", null, hc, null) ) {
// null for 404.
@@ -136,10 +189,42 @@ public class TestPasswordAccess {
}
@Test public void access_allow_ds() {
- HttpClient hc = LibSec.httpClient(authSetup);
+ HttpClient hc = LibSec.httpClient(authSetup1);
HttpCaptureResponse<TypedInputStream> handler = new CaptureInput();
try( TypedInputStream in = HttpOp.execHttpGet(serverURL+"ds", null, hc, null) ) {
assertNotNull(in);
}
}
+
+ // Service level : ctl.
+
+ @Test public void access_service_ctl_user1() {
+ // user1 -- allowed.
+ HttpClient hc = LibSec.httpClient(authSetup1);
+ try( TypedInputStream in = HttpOp.execHttpGet(serverURL+"ctl", null, hc, null) ) {
+ assertNotNull(in);
+ }
+ }
+
+ @Test public void access_service_ctl_user2() {
+ // user2 -- can login, not allowed.
+ HttpClient hc = LibSec.httpClient(authSetup2);
+ try( TypedInputStream in = HttpOp.execHttpGet(serverURL+"ctl", null, hc, null) ) {
+ fail("Didn't expect to succeed");
+ } catch (HttpException ex) {
+ if ( ex.getResponseCode() != HttpSC.FORBIDDEN_403)
+ throw ex;
+ }
+ }
+
+ @Test public void access_service_ctl_userX() {
+ // userX -- can't login, not allowed.
+ HttpClient hc = LibSec.httpClient(authSetupX);
+ try( TypedInputStream in = HttpOp.execHttpGet(serverURL+"ctl", null, hc, null) ) {
+ fail("Didn't expect to succeed");
+ } catch (HttpException ex) {
+ if ( ex.getResponseCode() != HttpSC.UNAUTHORIZED_401)
+ throw ex;
+ }
+ }
}
http://git-wip-us.apache.org/repos/asf/jena/blob/83858ba1/jena-fuseki2/jena-fuseki-access/src/test/java/org/apache/jena/fuseki/access/TestSecurityFilterFuseki.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-access/src/test/java/org/apache/jena/fuseki/access/TestSecurityFilterFuseki.java b/jena-fuseki2/jena-fuseki-access/src/test/java/org/apache/jena/fuseki/access/TestSecurityFilterFuseki.java
index 4ec6bc4..91c2afb 100644
--- a/jena-fuseki2/jena-fuseki-access/src/test/java/org/apache/jena/fuseki/access/TestSecurityFilterFuseki.java
+++ b/jena-fuseki2/jena-fuseki-access/src/test/java/org/apache/jena/fuseki/access/TestSecurityFilterFuseki.java
@@ -203,6 +203,25 @@ public class TestSecurityFilterFuseki {
query401("user0", "not-the-password", queryAll);
}
+ // More queries. DevSecureNG.java // FROM, "GRAPH ?g" and non-graph (also TestSecurityFilterLocal)
+
+// query(connx, "SELECT * FROM <http://host/graphname1> { GRAPH ?g {} }");
+// query(connx, "SELECT * FROM NAMED <http://host/graphname1> { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } }");
+// query(connx, "SELECT * FROM NAMED <http://host/graphname1> { GRAPH <http://host/graphname1> { ?s ?p ?o } }");
+// query(connx, "SELECT * { GRAPH ?g {} }");
+// query(connx, "SELECT * { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } }");
+// query(connx, "SELECT * FROM <http://host/graphname1> { ?s ?p ?o }");
+// query(connx, "SELECT * FROM <http://host/graphname1> { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } }");
+// query(connx, "SELECT * FROM NAMED <http://host/graphname1> { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } }");
+// query(connx, "SELECT * { GRAPH <http://host/graphname1> { ?s ?p ?o }}");
+//
+// query(connx, "SELECT * FROM <http://host/graphname4> { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } }");
+// query(connx, "SELECT * { GRAPH <http://host/graphname4> { ?s ?p ?o }}");
+//
+// query(connx, "SELECT * { GRAPH <"+Quad.unionGraph.getURI()+"> { ?s ?p ?o } }");
+// query(connx, "SELECT * FROM <"+Quad.unionGraph.getURI()+"> { ?s ?p ?o }");
+
+
private Set<Node> gsp(String user, String password, String graphName) {
Set<Node> results = new HashSet<>();
try (RDFConnection conn = RDFConnectionFactory.connectPW(baseUrl, user, password)) {
http://git-wip-us.apache.org/repos/asf/jena/blob/83858ba1/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiBuildLib.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiBuildLib.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiBuildLib.java
index a964c9d..07f5be6 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiBuildLib.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiBuildLib.java
@@ -18,12 +18,18 @@
package org.apache.jena.fuseki.build;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
import org.apache.jena.fuseki.FusekiConfigException;
import org.apache.jena.query.* ;
import org.apache.jena.rdf.model.Literal ;
import org.apache.jena.rdf.model.Model ;
+import org.apache.jena.rdf.model.RDFList;
import org.apache.jena.rdf.model.RDFNode ;
import org.apache.jena.rdf.model.Resource ;
+import org.apache.jena.shared.JenaException;
import org.apache.jena.shared.PrefixMapping ;
import org.apache.jena.vocabulary.RDFS ;
@@ -94,6 +100,28 @@ public class FusekiBuildLib {
return x ;
}
+ /**
+ * Get all object of a subject/property, whether repeated triples or RDF Lists or a
+ * mixture. If the subject/property isn't present, return null, so a caller can tell
+ * the difference between "not present" and an empty list value.
+ */
+ public static Collection<RDFNode> getAll(Resource svc, String property) {
+ ResultSet rs = FusekiBuildLib.query("SELECT * { ?svc " + property + " ?x}", svc.getModel(), "svc", svc) ;
+ if ( ! rs.hasNext() )
+ return null;
+ List<RDFNode> results = new ArrayList<>();
+ rs.forEachRemaining(qs->{
+ RDFNode n = qs.get("x");
+ try {
+ RDFList list = n.as(RDFList.class);
+ results.addAll(list.asJavaList());
+ } catch (JenaException x) {
+ results.add(n);
+ }
+ });
+ return results ;
+ }
+
// Node presentation
public static String nodeLabel(RDFNode n) {
if ( n == null )
http://git-wip-us.apache.org/repos/asf/jena/blob/83858ba1/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiConfig.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiConfig.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiConfig.java
index 7bf5ea0..25d4f70 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiConfig.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/build/FusekiConfig.java
@@ -19,6 +19,7 @@
package org.apache.jena.fuseki.build ;
import static java.lang.String.format;
+import static java.util.stream.Collectors.toList;
import static org.apache.jena.fuseki.server.FusekiVocab.*;
import static org.apache.jena.riot.RDFLanguages.filenameToLang;
import static org.apache.jena.riot.RDFParserRegistry.isRegistered;
@@ -31,6 +32,7 @@ import java.nio.file.Files ;
import java.nio.file.Path ;
import java.nio.file.Paths ;
import java.util.ArrayList ;
+import java.util.Collection;
import java.util.Collections ;
import java.util.List ;
@@ -41,12 +43,23 @@ import org.apache.jena.atlas.lib.StrUtils ;
import org.apache.jena.datatypes.xsd.XSDDatatype;
import org.apache.jena.fuseki.Fuseki ;
import org.apache.jena.fuseki.FusekiConfigException ;
-import org.apache.jena.fuseki.server.* ;
+import org.apache.jena.fuseki.server.DataAccessPoint;
+import org.apache.jena.fuseki.server.DataService;
+import org.apache.jena.fuseki.server.DataServiceStatus;
+import org.apache.jena.fuseki.server.FusekiVocab;
+import org.apache.jena.fuseki.server.Operation;
+import org.apache.jena.graph.Node;
import org.apache.jena.query.Dataset ;
import org.apache.jena.query.QuerySolution ;
import org.apache.jena.query.ReadWrite ;
import org.apache.jena.query.ResultSet ;
-import org.apache.jena.rdf.model.* ;
+import org.apache.jena.rdf.model.Literal;
+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.rdf.model.Statement;
+import org.apache.jena.rdf.model.StmtIterator;
+import org.apache.jena.rdf.model.impl.Util;
import org.apache.jena.riot.Lang;
import org.apache.jena.sparql.core.assembler.AssemblerUtils ;
import org.apache.jena.sparql.util.FmtUtils;
@@ -208,19 +221,49 @@ public class FusekiConfig {
if ( ! n.isLiteral() )
throw new FusekiConfigException("Not a literal for access point name: "+FmtUtils.stringForRDFNode(n));
Literal object = n.asLiteral() ;
-
+
if ( object.getDatatype() != null && ! object.getDatatype().equals(XSDDatatype.XSDstring) )
Fuseki.configLog.error(format("Service name '%s' is not a string", FmtUtils.stringForRDFNode(object)));
+
String name = object.getLexicalForm() ;
name = DataAccessPoint.canonical(name) ;
-
- DataService dataService = buildDataServiceCustom(svc, dsDescMap) ;
+ DataService dataService = buildDataService(svc, dsDescMap) ;
+ Collection<String> allowedUsers = getAllowedUsers(svc);
+ dataService.setAllowedUsers(allowedUsers);
DataAccessPoint dataAccess = new DataAccessPoint(name, dataService) ;
return dataAccess ;
}
+
+ /** Get the allowed users on some resources.
+ *
+ * @param resource
+ * @return Collection<String>
+ */
+ public static Collection<String> getAllowedUsers(Resource resource) {
+ Collection<RDFNode> allowedUsers = FusekiBuildLib.getAll(resource, "fu:"+pAllowedUsers.getLocalName());
+ Collection<String> userNames = null;
+ if ( allowedUsers != null ) {
+ // Check all values are simple strings
+ List<String> bad = allowedUsers.stream()
+ .map(RDFNode::asNode)
+ .filter(rn -> ! Util.isSimpleString(rn))
+ .map(rn->rn.toString())
+ .collect(toList());
+ if ( ! bad.isEmpty() ) {
+ //Fuseki.configLog.error(format("User names must be a simple string: bad = %s", bad));
+ throw new FusekiConfigException(format("User names should be a simple string: bad = %s", bad));
+ }
+ // RDFNodes/literals to strings.
+ userNames = allowedUsers.stream()
+ .map(RDFNode::asNode)
+ .map(Node::getLiteralLexicalForm)
+ .collect(toList());
+ }
+ return userNames;
+ }
/** Build a DatasetRef starting at Resource svc, having the services as described by the descriptions. */
- private static DataService buildDataServiceCustom(Resource svc, DatasetDescriptionRegistry dsDescMap) {
+ private static DataService buildDataService(Resource svc, DatasetDescriptionRegistry dsDescMap) {
Resource datasetDesc = ((Resource)FusekiBuildLib.getOne(svc, "fu:dataset")) ;
Dataset ds = getDataset(datasetDesc, dsDescMap);
@@ -258,7 +301,6 @@ public class FusekiConfig {
// sDesc.maximumTimeoutOverride = (int)(svc.getProperty(FusekiVocab.pMaximumTimeoutOverride).getObject().asLiteral().getFloat() * 1000) ;
// }
// }
-
return dataService ;
}
http://git-wip-us.apache.org/repos/asf/jena/blob/83858ba1/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataService.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataService.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataService.java
index af760c1..63f323e 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataService.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/server/DataService.java
@@ -38,6 +38,7 @@ public class DataService {
private ListMultimap<Operation, Endpoint> operations = ArrayListMultimap.create();
private Map<String, Endpoint> endpoints = new HashMap<>();
+ private Collection<String> allowedUsers = null;
/**
* Record which {@link DataAccessPoint DataAccessPoints} this {@code DataService} is
@@ -212,5 +213,11 @@ public class DataService {
} else
dataset.close();
}
+
+ public void setAllowedUsers(Collection<String> allowedUsers) { this.allowedUsers= allowedUsers; }
+
+ /** Returning null implies no access control */
+ public Collection<String> getAllowedUsers() { return allowedUsers; }
+
}
http://git-wip-us.apache.org/repos/asf/jena/blob/83858ba1/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionService.java
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionService.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionService.java
index 443c212..4da1e42 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionService.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/ActionService.java
@@ -57,11 +57,21 @@ public abstract class ActionService extends ActionBase {
String datasetUri = mapRequestToDataset(action);
if ( datasetUri != null ) {
dataAccessPoint = action.getDataAccessPointRegistry().get(datasetUri);
+
if ( dataAccessPoint == null ) {
ServletOps.errorNotFound("No dataset for URI: " + datasetUri);
return;
}
dSrv = dataAccessPoint.getDataService();
+
+ if ( dSrv.getAllowedUsers() != null ) {
+ String user = action.request.getRemoteUser();
+ if ( user == null )
+ ServletOps.errorForbidden();
+ if ( ! dSrv.getAllowedUsers().contains(user) ) {
+ ServletOps.errorForbidden();
+ }
+ }
if ( !dSrv.isAcceptingRequests() ) {
ServletOps.error(HttpSC.SERVICE_UNAVAILABLE_503, "Dataset not currently active");
return;