Skip to content

Commit

Permalink
Add attribute to allow anonymous GET requests through Syn (#9)
Browse files Browse the repository at this point in the history
* Add attribute to allow anonymous GET requests through Syn

* Additional tests

* Also check HEAD

Clean up allow anonymous checks
  • Loading branch information
whikloj authored and dannylamb committed May 3, 2017
1 parent bb6a59d commit 0924304
Show file tree
Hide file tree
Showing 10 changed files with 676 additions and 67 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
/build
*.ipr
*.iws
/bin/
3 changes: 3 additions & 0 deletions conf/syn-settings.example.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ my secret key
<!-- A site with a key stored in a file -->
<site url='http://test2.com' algorithm='HS256' encoding='base64' path='/somewhere/on/filesystem.key'/>

<!-- A site that allows all GET requests -->
<site url='http://test3.com' algorithm='HS256' encoding='plain' anonymous='true'/>

<!--
This is how you specify a default site, which will be chosen if no
other site matches the JWT url claim
Expand Down
63 changes: 40 additions & 23 deletions src/main/java/ca/islandora/syn/settings/SettingsParser.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package ca.islandora.syn.settings;

import com.auth0.jwt.algorithms.Algorithm;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.digester.Digester;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
Expand All @@ -15,13 +15,16 @@
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.io.File;
import java.io.Reader;
import java.io.StringReader;
import java.io.FileReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.stream.Collectors;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.digester.Digester;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.xml.sax.SAXException;

import com.auth0.jwt.algorithms.Algorithm;

public final class SettingsParser {
private static Digester digester = null;
Expand Down Expand Up @@ -250,23 +253,37 @@ public static Map<String, Algorithm> getSiteAlgorithms(final InputStream setting
}

public static Map<String, Token> getSiteStaticTokens(final InputStream settings) {
final Map<String, Token> tokens = new HashMap<>();
final Config sites = getSites(settings);
if (sites == null) {
return tokens;
return new HashMap<String, Token>();
}

for (Token token : sites.getTokens()) {
if (token.getToken().isEmpty()) {
log.error("Static token is empty ignoring.");
} else {
tokens.put(token.getToken(), token);
}
}
final Map<String, Token> tokens = sites.getTokens().stream().filter(x -> !x.getToken().isEmpty())
.collect(Collectors.toMap(Token::getToken, t -> t));

return tokens;
}

/**
* Build a list of site urls that allow anonymous GET requests.
*
* @param settings the path to the syn-settings file
* @return list of site urls.
*/
public static Map<String, Boolean> getSiteAllowAnonymous(final InputStream settings) {
final Config sites = getSites(settings);
if (sites == null) {
return new HashMap<String, Boolean>();
}

final Map<String, Boolean> anonymousAllowed = sites.getSites().stream().filter(s -> !s.getDefault())
.collect(Collectors.toMap(Site::getUrl, Site::getAnonymous));
sites.getSites().stream().filter(Site::getDefault).findFirst()
.ifPresent(s -> anonymousAllowed.put("default", s.getAnonymous()));

return anonymousAllowed;
}

static Config getSitesObject(final InputStream settings)
throws IOException, SAXException {
return (Config) getDigester().parse(settings);
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/ca/islandora/syn/settings/Site.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class Site {
private String path = null;
private String encoding = null;
private boolean defaultItem = false;
private boolean allowAnonymous = false;

public String getUrl() {
return this.url;
Expand Down Expand Up @@ -49,4 +50,22 @@ public boolean getDefault() {
public void setDefault(final boolean defaultItem) {
this.defaultItem = defaultItem;
}

/**
* Allow GET requests without a token that match this site.
*
* @return whether to allow the request
*/
public boolean getAnonymous() {
return this.allowAnonymous;
}

/**
* Set allow GET requests with a token that match this site.
*
* @param allowAnonGet boolean whether to allow these requests.
*/
public void setAnonymous(final boolean allowAnonGet) {
this.allowAnonymous = allowAnonGet;
}
}
63 changes: 57 additions & 6 deletions src/main/java/ca/islandora/syn/valve/SynValve.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package ca.islandora.syn.valve;

import ca.islandora.syn.settings.SettingsParser;
import ca.islandora.syn.settings.Token;
import ca.islandora.syn.token.Verifier;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import com.auth0.jwt.algorithms.Algorithm;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
Expand All @@ -20,13 +19,21 @@
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;

import com.auth0.jwt.algorithms.Algorithm;

import ca.islandora.syn.settings.SettingsParser;
import ca.islandora.syn.settings.Token;
import ca.islandora.syn.token.Verifier;

public class SynValve extends ValveBase {

private String pathname = "conf/syn-settings.xml";
private static final Log log = LogFactory.getLog(SynValve.class);
private Map<String, Algorithm> algorithmMap = null;
private Map<String, Token> staticTokenMap = null;

private Map<String, Boolean> anonymousGetMap = null;

@Override
public void invoke(final Request request, final Response response)
throws IOException, ServletException {
Expand All @@ -36,7 +43,7 @@ public void invoke(final Request request, final Response response)

if ((constraints == null
&& !request.getContext().getPreemptiveAuthentication())
|| !hasAuthConstraint(constraints)) {
|| !hasAuthConstraint(constraints)) {
this.getNext().invoke(request, response);
} else {
handleAuthentication(request, response);
Expand Down Expand Up @@ -105,6 +112,15 @@ private boolean doAuthentication(final Request request) {
}
}

private void setAnonymousRoles(final Request request) {
final List<String> roles = new ArrayList<String>();
roles.add("anonymous");
roles.add("islandora");
final String name = "anonymous";
final GenericPrincipal principal = new GenericPrincipal(name, null, roles);
request.setUserPrincipal(principal);
}

private void setUserRolesFromStaticToken(final Request request, final Token token) {
final List<String> roles = token.getRoles();
roles.add("islandora");
Expand All @@ -124,13 +140,47 @@ private void setUserRolesFromToken(final Request request, final Verifier verifie

private void handleAuthentication(final Request request, final Response response)
throws IOException, ServletException {
if (doAuthentication(request)) {
if ((request.getMethod().equalsIgnoreCase("GET") ||
request.getMethod().equals("HEAD")) &&
allowGetRequests(request.getHost().toString())) {
// Skip authentication
setAnonymousRoles(request);
this.getNext().invoke(request, response);
} else if (doAuthentication(request)) {
this.getNext().invoke(request, response);
} else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token authentication failed.");
}
}

/**
* Do the logic of allowing GET/HEAD requests.
*
* @param requestURI the site being requested
* @return whether to allow GET requests without authentication.
*/
private boolean allowGetRequests(final String requestURI) {
// If there is a matching site URI, return its value
if (anonymousGetMap.containsKey(requestURI)) {
log.debug(
String.format(
"Using site anonymous ({}) for GET/HEAD requests, site {}",
anonymousGetMap.get(requestURI),
requestURI));
return anonymousGetMap.get(requestURI);
// Else if there is a default, return its value.
} else if (anonymousGetMap.containsKey("default")) {
log.debug(
String.format(
"Using default anonymous ({}) for GET/HEAD requests, host {}",
anonymousGetMap.get("default"),
requestURI));
return anonymousGetMap.get("default");
}
// Else disallow anonymous.
return false;
}

public String getPathname() {
return pathname;
}
Expand All @@ -155,6 +205,7 @@ public synchronized void startInternal() throws LifecycleException {
try {
this.algorithmMap = SettingsParser.getSiteAlgorithms(new FileInputStream(file));
this.staticTokenMap = SettingsParser.getSiteStaticTokens(new FileInputStream(file));
this.anonymousGetMap = SettingsParser.getSiteAllowAnonymous(new FileInputStream(file));
} catch (Exception e) {
throw new LifecycleException("Error parsing XML Configuration", e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package ca.islandora.syn.settings;

import com.auth0.jwt.algorithms.Algorithm;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.assertEquals;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
import static org.junit.Assert.assertEquals;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import com.auth0.jwt.algorithms.Algorithm;

public class SettingsParserAlgorithmsTest {
@Rule
Expand Down Expand Up @@ -179,6 +182,21 @@ public void testSiteNoUrlDefault() throws Exception {
assertEquals(1, algorithms.size());
}

@Test
public void testSiteNoUrl() throws Exception {
final String testXml = String.join("\n"
, "<config version='1'>"
, " <site algorithm='HS256' encoding='plain'>"
, " test data"
, " </site>"
, "</config>"
);

final InputStream stream = new ByteArrayInputStream(testXml.getBytes());
final Map<String,Algorithm> algorithms = SettingsParser.getSiteAlgorithms(stream);
assertEquals(0, algorithms.size());
}

private void testOneSiteRsaInlineKey(final String algorithm) throws Exception {
final String testXml = String.join("\n"
, "<config version='1'>"
Expand Down Expand Up @@ -256,4 +274,58 @@ public void testOneSiteAllRsaInvalidEncoding() throws Exception {
final Map<String,Algorithm> algorithms = SettingsParser.getSiteAlgorithms(stream);
assertEquals(0, algorithms.size());
}


@Test
public void testMultipleDefaults() throws Exception {
final File keyFile = temporaryFolder.newFile();
final String path = keyFile.getAbsolutePath();

final String pemPublicKey = String.join("\n"
, "-----BEGIN PUBLIC KEY-----"
, "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDEVO4MNlZG+iGYhoJd/cBpfMd9"
, "YnKsntF+zhQs8lCbBabgY8kNoXVIEeOm4WPJ+W53gLDAIg6BNrZqxk9z1TLD6Dmz"
, "t176OLYkNoTI9LNf6z4wuBenrlQ/H5UnYl6h5QoOdVpNAgEjkDcdTSOE1lqFLIle"
, "KOT4nEF7MBGyOSP3KQIDAQAB"
, "-----END PUBLIC KEY-----"
);

Files.write(Paths.get(path), pemPublicKey.getBytes());

final String testXml = String.join("\n"
, "<config version=\"1\">"
, " <site algorithm=\"RS384\" path=\"" + path + "\" encoding=\"PEM\" default=\"true\"/>"
, " <site algorithm=\"HS256\" path=\"" + path + "\" encoding=\"plain\" default=\"true\"/>"
, "</config>"
);
final InputStream stream = new ByteArrayInputStream(testXml.getBytes());
final Map<String,Algorithm> algorithms = SettingsParser.getSiteAlgorithms(stream);
assertEquals(1, algorithms.size());
}

@Test
public void testInvalidAlgorithm() throws Exception {
final File keyFile = temporaryFolder.newFile();
final String path = keyFile.getAbsolutePath();

final String pemPublicKey = String.join("\n"
, "-----BEGIN PUBLIC KEY-----"
, "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDEVO4MNlZG+iGYhoJd/cBpfMd9"
, "YnKsntF+zhQs8lCbBabgY8kNoXVIEeOm4WPJ+W53gLDAIg6BNrZqxk9z1TLD6Dmz"
, "t176OLYkNoTI9LNf6z4wuBenrlQ/H5UnYl6h5QoOdVpNAgEjkDcdTSOE1lqFLIle"
, "KOT4nEF7MBGyOSP3KQIDAQAB"
, "-----END PUBLIC KEY-----"
);

Files.write(Paths.get(path), pemPublicKey.getBytes());

final String testXml = String.join("\n"
, "<config version=\"1\">"
, " <site algorithm=\"RSA384\" path=\"" + path + "\" encoding=\"PEM\" default=\"true\"/>"
, "</config>"
);
final InputStream stream = new ByteArrayInputStream(testXml.getBytes());
final Map<String,Algorithm> algorithms = SettingsParser.getSiteAlgorithms(stream);
assertEquals(0, algorithms.size());
}
}
Loading

0 comments on commit 0924304

Please sign in to comment.