Skip to content

Commit

Permalink
Allow requests from all client origins to obtain access cookie (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark A. Matney, Jr authored Jun 7, 2023
1 parent 273b820 commit 7adb57d
Show file tree
Hide file tree
Showing 14 changed files with 27 additions and 242 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<freelib.utils.version>3.3.0</freelib.utils.version>
<cidr.ip.version>1.0.1</cidr.ip.version>
<commons.codec.version>1.15</commons.codec.version>
<vertx.version>4.3.8</vertx.version>
<vertx.version>4.4.2</vertx.version>

<!-- Build plugin versions -->
<clean.plugin.version>3.1.0</clean.plugin.version>
Expand Down
8 changes: 1 addition & 7 deletions src/main/java/edu/ucla/library/iiif/auth/CookieJsonKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
* <pre>
* {
* "clientIpAddress": "127.0.0.1",
* "campusNetwork": false,
* "degradedAllowed": true
* "campusNetwork": false
* }
* </pre>
*/
Expand Down Expand Up @@ -51,11 +50,6 @@ public final class CookieJsonKeys {
*/
public static final String CAMPUS_NETWORK = "campusNetwork";

/**
* The JSON key for whether degraded content is available at the origin for which the cookie applies.
*/
public static final String DEGRADED_ALLOWED = "degradedAllowed";

/**
* Private constructor for utility class.
*/
Expand Down
5 changes: 0 additions & 5 deletions src/main/java/edu/ucla/library/iiif/auth/TemplateKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ public final class TemplateKeys {
*/
public static final String CLIENT_IP_ADDRESS = "clientIpAddress";

/**
* The degraded allowed key.
*/
public static final String DEGRADED_ALLOWED = "degradedAllowed";

/**
* The window close delay key.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@
import edu.ucla.library.iiif.auth.Param;
import edu.ucla.library.iiif.auth.TemplateKeys;
import edu.ucla.library.iiif.auth.services.AccessCookieService;
import edu.ucla.library.iiif.auth.services.DatabaseService;
import edu.ucla.library.iiif.auth.utils.MediaType;

import info.freelibrary.util.HTTP;
import info.freelibrary.util.Logger;
import info.freelibrary.util.LoggerFactory;

import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.Cookie;
Expand Down Expand Up @@ -56,11 +54,6 @@ public class AccessCookieHandler implements Handler<RoutingContext> {
*/
private final JsonObject myConfig;

/**
* The service proxy for accessing the database.
*/
private final DatabaseService myDatabaseServiceProxy;

/**
* The template engine for rendering the response.
*/
Expand Down Expand Up @@ -94,7 +87,6 @@ public class AccessCookieHandler implements Handler<RoutingContext> {
*/
public AccessCookieHandler(final Vertx aVertx, final JsonObject aConfig) {
myConfig = aConfig;
myDatabaseServiceProxy = DatabaseService.createProxy(aVertx);
myHtmlTemplateEngine = HandlebarsTemplateEngine.create(aVertx);
myCampusNetworkSubnets = new Cidr4Trie<>();
myAccessCookieService = AccessCookieService.createProxy(aVertx);
Expand Down Expand Up @@ -136,32 +128,26 @@ public void handle(final RoutingContext aContext) {

isOnCampusNetwork = isOnNetwork(clientIpAddress, myCampusNetworkSubnets);

myDatabaseServiceProxy.getDegradedAllowed(origin.toString()).compose(isDegradedAllowed -> {
final Future<String> cookieGeneration = myAccessCookieService.generateCookie(clientIpAddress.getAddress(),
isOnCampusNetwork, isDegradedAllowed);

return cookieGeneration.compose(cookieValue -> {
final Cookie cookie =
Cookie.cookie(CookieNames.HAUTH, cookieValue).setSameSite(CookieSameSite.NONE).setSecure(true);
myAccessCookieService.generateCookie(clientIpAddress.getAddress(), isOnCampusNetwork).compose(cookieValue -> {
final Cookie cookie =
Cookie.cookie(CookieNames.HAUTH, cookieValue).setSameSite(CookieSameSite.NONE).setSecure(true);

// Along with the origin, pass all the cookie data to the HTML template
final JsonObject templateData = new JsonObject().put(TemplateKeys.ORIGIN, origin)
.put(TemplateKeys.VERSION, myConfig.getString(Config.HAUTH_VERSION))
.put(TemplateKeys.CLIENT_IP_ADDRESS, clientIpAddress)
.put(TemplateKeys.CAMPUS_NETWORK, isOnCampusNetwork)
.put(TemplateKeys.DEGRADED_ALLOWED, isDegradedAllowed);
// Along with the origin, pass all the cookie data to the HTML template
final JsonObject templateData = new JsonObject().put(TemplateKeys.ORIGIN, origin)
.put(TemplateKeys.VERSION, myConfig.getString(Config.HAUTH_VERSION))
.put(TemplateKeys.CLIENT_IP_ADDRESS, clientIpAddress)
.put(TemplateKeys.CAMPUS_NETWORK, isOnCampusNetwork);

myWindowCloseDelay.ifPresent(delay -> {
if (delay >= 0) {
templateData.put(TemplateKeys.WINDOW_CLOSE_DELAY, delay);
}
});
myCookieDomain.ifPresent(cookie::setDomain);
myWindowCloseDelay.ifPresent(delay -> {
if (delay >= 0) {
templateData.put(TemplateKeys.WINDOW_CLOSE_DELAY, delay);
}
});
myCookieDomain.ifPresent(cookie::setDomain);

response.addCookie(cookie);
response.addCookie(cookie);

return myHtmlTemplateEngine.render(templateData, "templates/cookie.hbs");
});
return myHtmlTemplateEngine.render(templateData, "templates/cookie.hbs");
}).onSuccess(renderedHtmlTemplate -> {
response.setStatusCode(HTTP.OK).end(renderedHtmlTemplate);
}).onFailure(error -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,10 @@ static AccessCookieService createProxy(final Vertx aVertx) {
*
* @param aClientIpAddress The IP address of the client
* @param aIsOnCampusNetwork If the client is on a campus network subnet
* @param aIsDegradedAllowed If the origin allows degraded access to content
* @return A Future that resolves to a value that can be used to create a cookie with
* {@link Cookie#cookie(String, String)}
*/
Future<String> generateCookie(String aClientIpAddress, boolean aIsOnCampusNetwork, boolean aIsDegradedAllowed);
Future<String> generateCookie(String aClientIpAddress, boolean aIsOnCampusNetwork);

/**
* Decrypts an access cookie value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,9 @@ public Future<Void> close() {
}

@Override
public Future<String> generateCookie(final String aClientIpAddress, final boolean aIsOnCampusNetwork,
final boolean aIsDegradedAllowed) {
public Future<String> generateCookie(final String aClientIpAddress, final boolean aIsOnCampusNetwork) {
final JsonObject cookieData = new JsonObject().put(CookieJsonKeys.CLIENT_IP_ADDRESS, aClientIpAddress)
.put(CookieJsonKeys.CAMPUS_NETWORK, aIsOnCampusNetwork)
.put(CookieJsonKeys.DEGRADED_ALLOWED, aIsDegradedAllowed);
.put(CookieJsonKeys.CAMPUS_NETWORK, aIsOnCampusNetwork);
final byte[] encryptedCookieData;
final JsonObject unencodedCookie;
final String cookie;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,4 @@ static DatabaseService createProxy(final Vertx aVertx) {
* @return A Future that resolves once the items have been set
*/
Future<Void> setItems(JsonArray aItems);

/**
* Gets the "degraded allowed" for content hosted at the given origin.
*
* @param aOrigin The origin
* @return A Future that resolves to the degraded allowed once it's been fetched
*/
Future<Boolean> getDegradedAllowed(String aOrigin);

/**
* Sets the given "degraded allowed" for content hosted at the given origin.
*
* @param aOrigin The origin
* @param aDegradedAllowed The degraded allowed to set for the origin
* @return A Future that resolves once the degraded allowed has been set
*/
Future<Void> setDegradedAllowed(String aOrigin, boolean aDegradedAllowed);
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,6 @@ public class DatabaseServiceImpl implements DatabaseService {
private static final String UPSERT_ACCESS_MODE = String.join(SPACE, "INSERT INTO items VALUES ($1, $2)",
"ON CONFLICT (uid) DO", "UPDATE SET access_mode = EXCLUDED.access_mode");

/**
* The PreparedQuery template for selecting an origin's "degraded allowed".
*/
private static final String SELECT_DEGRADED_ALLOWED = "SELECT degraded_allowed FROM origins WHERE url = $1";

/**
* The PreparedQuery template for upserting an origin's "degraded allowed".
*/
private static final String UPSERT_DEGRADED_ALLOWED = String.join(SPACE, "INSERT INTO origins VALUES ($1, $2)",
"ON CONFLICT (url) DO", "UPDATE SET degraded_allowed = EXCLUDED.degraded_allowed");

/**
* The database's default hostname.
*/
Expand Down Expand Up @@ -163,29 +152,6 @@ public Future<Void> setItems(final JsonArray aItems) {
}).compose(result -> Future.succeededFuture());
}

@Override
public Future<Boolean> getDegradedAllowed(final String aOrigin) {
return myDbConnectionPool.withConnection(connection -> {
return connection.preparedQuery(SELECT_DEGRADED_ALLOWED).execute(Tuple.of(aOrigin));
}).recover(error -> {
return Future.failedFuture(new ServiceException(INTERNAL_ERROR, error.getMessage()));
}).compose(select -> {
if (hasSingleRow(select)) {
return Future.succeededFuture(select.iterator().next().getBoolean("degraded_allowed"));
}
return Future.failedFuture(new ServiceException(NOT_FOUND_ERROR, aOrigin));
});
}

@Override
public Future<Void> setDegradedAllowed(final String aOrigin, final boolean aDegradedAllowed) {
return myDbConnectionPool.withConnection(connection -> {
return connection.preparedQuery(UPSERT_DEGRADED_ALLOWED).execute(Tuple.of(aOrigin, aDegradedAllowed));
}).recover(error -> {
return Future.failedFuture(new ServiceException(INTERNAL_ERROR, error.getMessage()));
}).compose(result -> Future.succeededFuture());
}

/**
* Gets the options for the database connection pool.
*
Expand Down
8 changes: 1 addition & 7 deletions src/main/resources/templates/cookie.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@
hosted at: <em id="origin">{{origin}}</em>
<p>
<p>
{{#if degradedAllowed}}
Degraded versions of the content are accessible
{{else}}
The content is not accessible
{{/if}}

to users outside of the Campus Network.
Degraded versions of the content are accessible to users outside of the Campus Network.
</p>
<p>
Your current IP address: <em id="client-ip-address">{{clientIpAddress}}</em>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,7 @@ public void setUp(final Vertx aVertx, final VertxTestContext aContext) {
final DatabaseService db = DatabaseService.create(aVertx, config);
@SuppressWarnings("rawtypes")
final List<Future> dbOps = List.of(db.setAccessMode(TEST_ID_OPEN_ACCESS, 0),
db.setAccessMode(TEST_ID_TIERED_ACCESS, 1), db.setAccessMode(TEST_ID_ALL_OR_NOTHING_ACCESS, 2),
db.setDegradedAllowed(TEST_ORIGIN, true));
db.setAccessMode(TEST_ID_TIERED_ACCESS, 1), db.setAccessMode(TEST_ID_ALL_OR_NOTHING_ACCESS, 2));

myConfig = config;
myWebClient = WebClient.create(aVertx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import io.netty.handler.codec.http.cookie.DefaultCookie;

import org.jsoup.Jsoup;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

Expand Down Expand Up @@ -83,26 +82,4 @@ public void testGetCookie(final boolean aReverseProxyDeployment, final Vertx aVe
});
}).onFailure(aContext::failNow);
}

/**
* Tests that a client can't obtain an access cookie for an unknown origin.
*
* @param aVertx A Vert.x instance
* @param aContext A test context
*/
@Test
public void testGetCookieUnknownOrigin(final Vertx aVertx, final VertxTestContext aContext) {
final String requestURI = StringUtils.format(GET_COOKIE_PATH,
URLEncoder.encode("https://iiif.unknown.library.ucla.edu", StandardCharsets.UTF_8));
final HttpRequest<?> getCookie = myWebClient.get(myPort, Constants.INADDR_ANY, requestURI);

getCookie.send().onSuccess(response -> {
aContext.verify(() -> {
assertEquals(HTTP.BAD_REQUEST, response.statusCode());
assertEquals(MediaType.TEXT_HTML.toString(), response.headers().get(HttpHeaders.CONTENT_TYPE));

aContext.completeNow();
});
}).onFailure(aContext::failNow);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,7 @@ public final void tearDown(final Vertx aVertx, final VertxTestContext aContext)
public final void testValidateGeneratedCookie(final Vertx aVertx, final VertxTestContext aContext) {
final String clientIpAddress = LOCALHOST;
final boolean isCampusNetwork = true;
final boolean isDegradedAllowed = false;
final Future<String> generateCookie =
myServiceProxy.generateCookie(clientIpAddress, isCampusNetwork, isDegradedAllowed);
final Future<String> generateCookie = myServiceProxy.generateCookie(clientIpAddress, isCampusNetwork);

generateCookie.compose(cookie -> {
// The result is base64-encoded JSON with three keys
Expand All @@ -157,8 +155,7 @@ public final void testValidateGeneratedCookie(final Vertx aVertx, final VertxTes
return myServiceProxy.decryptCookie(cookie, clientIpAddress);
}).onSuccess(decryptedCookie -> {
final JsonObject expected = new JsonObject().put(CookieJsonKeys.CLIENT_IP_ADDRESS, clientIpAddress)
.put(CookieJsonKeys.CAMPUS_NETWORK, isCampusNetwork)
.put(CookieJsonKeys.DEGRADED_ALLOWED, isDegradedAllowed);
.put(CookieJsonKeys.CAMPUS_NETWORK, isCampusNetwork);

completeIfExpectedElseFail(decryptedCookie, expected, aContext);
}).onFailure(aContext::failNow);
Expand All @@ -175,9 +172,7 @@ public final void testValidateGeneratedCookie(final Vertx aVertx, final VertxTes
public final void testInvalidateTamperedCookie(final Vertx aVertx, final VertxTestContext aContext) {
final String clientIpAddress = LOCALHOST;
final boolean isCampusNetwork = false;
final boolean isDegradedAllowed = false;
final Future<String> generateCookie =
myServiceProxy.generateCookie(clientIpAddress, isCampusNetwork, isDegradedAllowed);
final Future<String> generateCookie = myServiceProxy.generateCookie(clientIpAddress, isCampusNetwork);

generateCookie.compose(cookie -> {
final JsonObject decodedCookie = new JsonObject(new String(Base64.getDecoder().decode(cookie.getBytes())));
Expand All @@ -203,9 +198,7 @@ public final void testInvalidateTamperedCookie(final Vertx aVertx, final VertxTe
if (decryptedCookie.containsKey(CookieJsonKeys.CLIENT_IP_ADDRESS) &&
decryptedCookie.getString(CookieJsonKeys.CLIENT_IP_ADDRESS).equals(clientIpAddress) &&
decryptedCookie.containsKey(CookieJsonKeys.CAMPUS_NETWORK) &&
decryptedCookie.getBoolean(CookieJsonKeys.CAMPUS_NETWORK) == isCampusNetwork &&
decryptedCookie.containsKey(CookieJsonKeys.DEGRADED_ALLOWED) &&
decryptedCookie.getBoolean(CookieJsonKeys.DEGRADED_ALLOWED) == isDegradedAllowed) {
decryptedCookie.getBoolean(CookieJsonKeys.CAMPUS_NETWORK) == isCampusNetwork) {
aContext.failNow(StringUtils.format(MessageCodes.AUTH_009, decryptedCookie));
} else {
aContext.completeNow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,64 +194,6 @@ final void testSetItemsInvalidItem(final VertxTestContext aContext) {
});
}

/**
* Tests reading an origin whose "degraded allowed" has not been set.
*
* @param aContext A test context
*/
@Test
final void testGetDegradedAllowedUnset(final VertxTestContext aContext) {
final String url = "https://library.ucla.edu";
final String expected = NULL;

myServiceProxy.getDegradedAllowed(url).onFailure(details -> {
final ServiceException error = (ServiceException) details;

aContext.verify(() -> {
assertEquals(Error.NOT_FOUND.ordinal(), error.failureCode());
assertEquals(url, error.getMessage());

aContext.completeNow();
});
}).onSuccess(result -> {
// The following will always fail
completeIfExpectedElseFail(result, expected, aContext);
});
}

/**
* Tests reading an origin whose "degraded allowed" has been set once.
*
* @param aContext A test context
*/
@Test
final void testGetDegradedAllowedSetOnce(final VertxTestContext aContext) {
final String url = "https://iiif.library.ucla.edu";
final boolean expected = true;
final Future<Void> setOnce = myServiceProxy.setDegradedAllowed(url, expected);

setOnce.compose(put -> myServiceProxy.getDegradedAllowed(url)).onSuccess(result -> {
completeIfExpectedElseFail(result, expected, aContext);
}).onFailure(aContext::failNow);
}

/**
* Tests reading an origin whose "degraded allowed" has been set more than once.
*
* @param aContext A test context
*/
@Test
final void testGetDegradedAllowedSetTwice(final VertxTestContext aContext) {
final String url = "https://iiif.sinaimanuscripts.library.ucla.edu";
final boolean expected = true;
final Future<Void> setTwice = myServiceProxy.setDegradedAllowed(url, false)
.compose(put -> myServiceProxy.setDegradedAllowed(url, expected));

setTwice.compose(put -> myServiceProxy.getDegradedAllowed(url)).onSuccess(result -> {
completeIfExpectedElseFail(result, expected, aContext);
}).onFailure(aContext::failNow);
}

protected Logger getLogger() {
return LOGGER;
}
Expand Down
Loading

0 comments on commit 7adb57d

Please sign in to comment.