From e3f5ea9992d5da549f81843f8c8e2df73f512288 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Tue, 22 Sep 2020 15:06:25 -0400 Subject: [PATCH 01/30] refactor(auth): WIP modify auth check for API users instead of just checking the Auth0 token for the requesting user, we need to permit third party API users to authenticate with an API token. This begins to make those changes --- .../middleware/auth/Auth0Connection.java | 18 ++++++------- ...h0UserProfile.java => RequestingUser.java} | 26 ++++++++++++------- .../api/AbstractUserController.java | 24 ++++++++++++----- .../controllers/api/AdminUserController.java | 4 +-- .../controllers/api/ApiController.java | 12 +++++---- .../controllers/api/ApiUserController.java | 10 +++---- .../controllers/api/LogController.java | 4 +-- .../controllers/api/OtpRequestProcessor.java | 5 ++-- .../controllers/api/OtpUserController.java | 4 +-- .../middleware/models/AbstractUser.java | 4 +-- .../middleware/models/AdminUser.java | 4 +-- .../middleware/models/Model.java | 6 ++--- .../middleware/models/MonitoredTrip.java | 6 ++--- .../opentripplanner/middleware/TestUtils.java | 6 ++--- 14 files changed, 76 insertions(+), 57 deletions(-) rename src/main/java/org/opentripplanner/middleware/auth/{Auth0UserProfile.java => RequestingUser.java} (80%) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 71b1c9eba..36b78e96a 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -54,7 +54,7 @@ public static void checkUser(Request req) { if (isAuthDisabled()) { // If in a development or testing environment, assign a mock profile of an admin user to the request // attribute and skip authentication. - addUserToRequest(req, Auth0UserProfile.createTestUser(req)); + addUserToRequest(req, RequestingUser.createTestUser(req)); return; } String token = getTokenFromRequest(req); @@ -65,7 +65,7 @@ public static void checkUser(Request req) { // for downstream controllers to check permissions. try { DecodedJWT jwt = verifier.verify(token); - Auth0UserProfile profile = new Auth0UserProfile(jwt); + RequestingUser profile = new RequestingUser(jwt); if (!isValidUser(profile)) { if (isCreatingSelf(req, profile)) { // If creating self, no user account is required (it does not exist yet!). Note: creating an @@ -94,7 +94,7 @@ public static void checkUser(Request req) { /** * Check for POST requests that are creating an {@link AbstractUser} (a proxy for OTP/API users). */ - private static boolean isCreatingSelf(Request req, Auth0UserProfile profile) { + private static boolean isCreatingSelf(Request req, RequestingUser profile) { String uri = req.uri(); String method = req.requestMethod(); // Check that this is a POST request. @@ -132,7 +132,7 @@ public static void checkUserIsAdmin(Request req, Response res) { // Check auth token in request (and add user object to request). checkUser(req); // Check that user object is present and is admin. - Auth0UserProfile user = Auth0Connection.getUserFromRequest(req); + RequestingUser user = Auth0Connection.getUserFromRequest(req); if (!isUserAdmin(user)) { logMessageAndHalt( req, @@ -145,21 +145,21 @@ public static void checkUserIsAdmin(Request req, Response res) { /** * Check if the incoming user is an admin user */ - public static boolean isUserAdmin(Auth0UserProfile user) { + public static boolean isUserAdmin(RequestingUser user) { return user != null && user.adminUser != null; } /** * Add user profile to Spark Request object */ - public static void addUserToRequest(Request req, Auth0UserProfile user) { + public static void addUserToRequest(Request req, RequestingUser user) { req.attribute("user", user); } /** * Get user profile from Spark Request object */ - public static Auth0UserProfile getUserFromRequest(Request req) { + public static RequestingUser getUserFromRequest(Request req) { return req.attribute("user"); } @@ -236,7 +236,7 @@ public static void setAuthDisabled(boolean authDisabled) { /** * Confirm that the user exists in at least one of the MongoDB user collections. */ - private static boolean isValidUser(Auth0UserProfile profile) { + private static boolean isValidUser(RequestingUser profile) { return profile != null && (profile.adminUser != null || profile.otpUser != null || profile.apiUser != null); } @@ -244,7 +244,7 @@ private static boolean isValidUser(Auth0UserProfile profile) { * Confirm that the user's actions are on their items if not admin. */ public static void isAuthorized(String userId, Request request) { - Auth0UserProfile profile = getUserFromRequest(request); + RequestingUser profile = getUserFromRequest(request); // let admin do anything if (profile.adminUser != null) { return; diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0UserProfile.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java similarity index 80% rename from src/main/java/org/opentripplanner/middleware/auth/Auth0UserProfile.java rename to src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index 0afb55ec2..822ccf403 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0UserProfile.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -16,17 +16,17 @@ * User profile that is attached to an HTTP request. */ @JsonIgnoreProperties(ignoreUnknown = true) -public class Auth0UserProfile { - public final OtpUser otpUser; - public final ApiUser apiUser; - public final AdminUser adminUser; - public final String auth0UserId; +public class RequestingUser { + public OtpUser otpUser; + public ApiUser apiUser; + public AdminUser adminUser; + public String auth0UserId; /** * Constructor is only used for creating a test user. If an Auth0 user id is provided check persistence for matching * user else create default user. */ - private Auth0UserProfile(String auth0UserId) { + private RequestingUser(String auth0UserId) { if (auth0UserId == null) { this.auth0UserId = "user_id:string"; otpUser = new OtpUser(); @@ -44,7 +44,7 @@ private Auth0UserProfile(String auth0UserId) { /** * Create a user profile from the request's JSON web token. Check persistence for stored user */ - public Auth0UserProfile(DecodedJWT jwt) { + public RequestingUser(DecodedJWT jwt) { this.auth0UserId = jwt.getClaim("sub").asString(); Bson withAuth0UserId = eq("auth0UserId", auth0UserId); otpUser = Persistence.otpUsers.getOneFiltered(withAuth0UserId); @@ -52,17 +52,25 @@ public Auth0UserProfile(DecodedJWT jwt) { apiUser = Persistence.apiUsers.getOneFiltered(withAuth0UserId); } + public RequestingUser(String apiKey, boolean isApiUser) { + apiUser = getApiUserForApiKey(apiKey); + } + + private ApiUser getApiUserForApiKey(String apiKey) { + return Persistence.apiUsers.getOneFiltered(); + } + /** * Utility method for creating a test user. If a Auth0 user Id is defined within the Authorization header param * define test user based on this. */ - static Auth0UserProfile createTestUser(Request req) { + static RequestingUser createTestUser(Request req) { String auth0UserId = null; if (isAuthHeaderPresent(req)) { // If the auth header has been provided get the Auth0 user id from it. This is different from normal // operation as the parameter will only contain the Auth0 user id and not "Bearer token". auth0UserId = req.headers("Authorization"); } - return new Auth0UserProfile(auth0UserId); + return new RequestingUser(auth0UserId); } } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java index 30430275a..cc4dfbb9b 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java @@ -5,9 +5,10 @@ import com.beerboy.ss.ApiEndpoint; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.auth.Auth0Users; import org.opentripplanner.middleware.models.AbstractUser; +import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.persistence.TypedPersistence; import org.opentripplanner.middleware.utils.JsonUtils; import org.slf4j.Logger; @@ -69,14 +70,14 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { * Obtains the correct AbstractUser-derived object from the Auth0UserProfile object. * (Used in getUserForRequest.) */ - protected abstract U getUserProfile(Auth0UserProfile profile); + protected abstract U getUserProfile(RequestingUser profile); /** - * HTTP endpoint to get the {@link U} entity, if it exists, from an {@link Auth0UserProfile} attribute + * HTTP endpoint to get the {@link U} entity, if it exists, from an {@link RequestingUser} attribute * available from a {@link Request} (this is the case for '/api/secure/' endpoints). */ private U getUserFromRequest(Request req, Response res) { - Auth0UserProfile profile = Auth0Connection.getUserFromRequest(req); + RequestingUser profile = Auth0Connection.getUserFromRequest(req); U user = getUserProfile(profile); // If the user object is null, it is most likely because it was not created yet, @@ -91,7 +92,7 @@ private U getUserFromRequest(Request req, Response res) { private Job resendVerificationEmail(Request req, Response res) { - Auth0UserProfile profile = Auth0Connection.getUserFromRequest(req); + RequestingUser profile = Auth0Connection.getUserFromRequest(req); return Auth0Users.resendVerificationEmail(profile.auth0UserId); } @@ -101,8 +102,17 @@ private Job resendVerificationEmail(Request req, Response res) { */ @Override U preCreateHook(U user, Request req) { - User auth0UserProfile = createNewAuth0User(user, req, this.persistence); - return updateAuthFieldsForUser(user, auth0UserProfile); + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); + // TODO: If MOD UI is to be an ApiUser, we may want to do an additional check here to determine if this is a + // first-party API user (MOD UI) or third party. + if (requestingUser.apiUser != null && user instanceof OtpUser) { + // Do not create Auth0 account for OtpUsers created on behalf of third party API users. + return user; + } else { + // For any other user account, create Auth0 account + User auth0UserProfile = createNewAuth0User(user, req, this.persistence); + return updateAuthFieldsForUser(user, auth0UserProfile); + } } @Override diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/AdminUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/AdminUserController.java index 36b2c9494..b96d1fb56 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/AdminUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/AdminUserController.java @@ -1,6 +1,6 @@ package org.opentripplanner.middleware.controllers.api; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.AdminUser; import org.opentripplanner.middleware.persistence.Persistence; @@ -18,7 +18,7 @@ public AdminUserController(String apiPrefix) { } @Override - protected AdminUser getUserProfile(Auth0UserProfile profile) { + protected AdminUser getUserProfile(RequestingUser profile) { return profile.adminUser; } } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java index 9b9ee685e..206acd585 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java @@ -8,7 +8,7 @@ import org.bson.conversions.Bson; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.Model; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.persistence.TypedPersistence; @@ -174,7 +174,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { // FIXME Will require further granularity for admin private List getMany(Request req, Response res) { - Auth0UserProfile requestingUser = getUserFromRequest(req); + RequestingUser requestingUser = getUserFromRequest(req); if (isUserAdmin(requestingUser)) { // If the user is admin, the context is presumed to be the admin dashboard, so we deliver all entities for // management or review without restriction. @@ -187,6 +187,8 @@ private List getMany(Request req, Response res) { } else { // For all other cases the assumption is that the request is being made by an Otp user and the requested // entities have a 'userId' parameter. Only entities that match the requesting user id are returned. + // FIXME: This needs to change so that third party API users must pass in an OtpUser id in order to get + // filtered objects. This could be either a param (in path) or query param. return getObjectsFiltered("userId", requestingUser.otpUser.id); } } @@ -206,7 +208,7 @@ private List getObjectsFiltered(String fieldName, String value) { * (must be admin) than is desired. */ protected T getEntityForId(Request req, Response res) { - Auth0UserProfile requestingUser = Auth0Connection.getUserFromRequest(req); + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); String id = getIdFromRequest(req); T object = getObjectForId(req, id); @@ -223,7 +225,7 @@ protected T getEntityForId(Request req, Response res) { private T deleteOne(Request req, Response res) { long startTime = DateTimeUtils.currentTimeMillis(); String id = getIdFromRequest(req); - Auth0UserProfile requestingUser = Auth0Connection.getUserFromRequest(req); + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); try { T object = getObjectForId(req, id); // Check that requesting user can manage entity. @@ -303,7 +305,7 @@ private T createOrUpdate(Request req, Response res) { if (req.params(ID_PARAM) == null && req.requestMethod().equals("PUT")) { logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Must provide id"); } - Auth0UserProfile requestingUser = Auth0Connection.getUserFromRequest(req); + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); final boolean isCreating = req.params(ID_PARAM) == null; // Save or update to database try { diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java index 16caffe68..96197356d 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java @@ -3,7 +3,7 @@ import com.beerboy.ss.ApiEndpoint; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.ApiKey; import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.persistence.Persistence; @@ -90,7 +90,7 @@ private boolean userHasKey(ApiUser user, String apiKeyId) { */ private ApiUser createApiKeyForApiUser(Request req, Response res) { ApiUser targetUser = getApiUser(req); - Auth0UserProfile requestingUser = Auth0Connection.getUserFromRequest(req); + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); String usagePlanId = req.queryParamOrDefault("usagePlanId", DEFAULT_USAGE_PLAN_ID); // If requester is not an admin user, force the usage plan ID to the default and enforce key limit. A non-admin // user should not be able to create an API key for any usage plan. @@ -120,7 +120,7 @@ private ApiUser createApiKeyForApiUser(Request req, Response res) { * Delete an api key from a given user's list of api keys (if present) and from AWS api gateway. */ private ApiUser deleteApiKeyForApiUser(Request req, Response res) { - Auth0UserProfile requestingUser = Auth0Connection.getUserFromRequest(req); + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // Do not permit key deletion unless user is an admin. if (!isUserAdmin(requestingUser)) { logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to delete an API key."); @@ -155,7 +155,7 @@ private ApiUser deleteApiKeyForApiUser(Request req, Response res) { } @Override - protected ApiUser getUserProfile(Auth0UserProfile profile) { + protected ApiUser getUserProfile(RequestingUser profile) { return profile.apiUser; } @@ -199,7 +199,7 @@ boolean preDeleteHook(ApiUser user, Request req) { * Get an Api user from Mongo DB based on the provided user id. Make sure user is admin or managing self. */ private static ApiUser getApiUser(Request req) { - Auth0UserProfile requestingUser = Auth0Connection.getUserFromRequest(req); + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); String userId = HttpUtils.getRequiredParamFromRequest(req, ID_PARAM); ApiUser apiUser = Persistence.apiUsers.getById(userId); if (apiUser == null) { diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java index 0e9c366d1..8ac59d1d7 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java @@ -5,7 +5,7 @@ import com.beerboy.ss.rest.Endpoint; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.ApiKey; import org.opentripplanner.middleware.models.ApiUsageResult; import org.opentripplanner.middleware.utils.ApiGatewayUtils; @@ -80,7 +80,7 @@ public void bind(final SparkSwagger restApi) { private static List getUsageLogs(Request req, Response res) { // Get list of API keys (if present) from request. List apiKeys = getApiKeyIdsFromRequest(req); - Auth0UserProfile requestingUser = Auth0Connection.getUserFromRequest(req); + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // If the user is not an admin, the list of API keys is defaulted to their keys. if (!isUserAdmin(requestingUser)) { if (requestingUser.apiUser == null) { diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index f94121099..d7c747cd9 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -4,7 +4,7 @@ import com.beerboy.ss.rest.Endpoint; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.TripRequest; import org.opentripplanner.middleware.models.TripSummary; import org.opentripplanner.middleware.otp.OtpDispatcher; @@ -25,7 +25,6 @@ import static javax.ws.rs.core.MediaType.APPLICATION_XML; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthHeaderPresent; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; -import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_API_ROOT; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -132,7 +131,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons long tripStorageStartTime = DateTimeUtils.currentTimeMillis(); Auth0Connection.checkUser(request); - Auth0UserProfile profile = Auth0Connection.getUserFromRequest(request); + RequestingUser profile = Auth0Connection.getUserFromRequest(request); final boolean storeTripHistory = profile != null && profile.otpUser != null && profile.otpUser.storeTripHistory; // only save trip details if the user has given consent and a response from OTP is provided diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java index f07538e20..d51f9c697 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java @@ -5,7 +5,7 @@ import com.twilio.rest.verify.v2.service.VerificationCheck; import org.eclipse.jetty.http.HttpStatus; import com.mongodb.client.model.Filters; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.bugsnag.BugsnagReporter; import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.models.OtpUser; @@ -80,7 +80,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { } @Override - protected OtpUser getUserProfile(Auth0UserProfile profile) { + protected OtpUser getUserProfile(RequestingUser profile) { return profile.otpUser; } diff --git a/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java b/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java index 5bf385c5a..2fb818d22 100644 --- a/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java @@ -2,7 +2,7 @@ import com.auth0.exception.Auth0Exception; import org.opentripplanner.middleware.auth.Auth0Connection; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.auth.Permission; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,7 +44,7 @@ public abstract class AbstractUser extends Model { * permissions) or if the requesting user has permission to manage the entity type. */ @Override - public boolean canBeManagedBy(Auth0UserProfile user) { + public boolean canBeManagedBy(RequestingUser user) { // If the user is attempting to update someone else's profile, they must be an admin. boolean isManagingSelf = this.auth0UserId.equals(user.auth0UserId); if (isManagingSelf) { diff --git a/src/main/java/org/opentripplanner/middleware/models/AdminUser.java b/src/main/java/org/opentripplanner/middleware/models/AdminUser.java index b23165dd4..b3b2e27e8 100644 --- a/src/main/java/org/opentripplanner/middleware/models/AdminUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/AdminUser.java @@ -1,6 +1,6 @@ package org.opentripplanner.middleware.models; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.auth.Permission; import org.opentripplanner.middleware.persistence.Persistence; import org.slf4j.Logger; @@ -31,7 +31,7 @@ public AdminUser() { * TODO: Change to application admin? */ @Override - public boolean canBeCreatedBy(Auth0UserProfile user) { + public boolean canBeCreatedBy(RequestingUser user) { return isUserAdmin(user); } diff --git a/src/main/java/org/opentripplanner/middleware/models/Model.java b/src/main/java/org/opentripplanner/middleware/models/Model.java index 7067581ec..2e0783354 100644 --- a/src/main/java/org/opentripplanner/middleware/models/Model.java +++ b/src/main/java/org/opentripplanner/middleware/models/Model.java @@ -1,6 +1,6 @@ package org.opentripplanner.middleware.models; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import java.io.Serializable; import java.util.Date; @@ -27,7 +27,7 @@ public Model () { * This is a basic authorization check for any entity to determine if a user can create the entity. By default any * user can create any entity. This method should be overridden if there are more restrictions needed. */ - public boolean canBeCreatedBy(Auth0UserProfile user) { + public boolean canBeCreatedBy(RequestingUser user) { return true; } @@ -35,7 +35,7 @@ public boolean canBeCreatedBy(Auth0UserProfile user) { * This is a basic authorization check for any entity to determine if a user can manage it. This method * should be overridden in subclasses in order to provide more fine-grained checks. */ - public boolean canBeManagedBy(Auth0UserProfile user) { + public boolean canBeManagedBy(RequestingUser user) { // TODO: Check if user has application administrator permission? return isUserAdmin(user); } diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index 9b5b5f123..4c4533a82 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -1,7 +1,7 @@ package org.opentripplanner.middleware.models; import org.bson.conversions.Bson; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.auth.Permission; import org.opentripplanner.middleware.otp.OtpDispatcherResponse; import org.opentripplanner.middleware.otp.response.Itinerary; @@ -179,7 +179,7 @@ public boolean isActiveOnDate(ZonedDateTime zonedDateTime) { } @Override - public boolean canBeCreatedBy(Auth0UserProfile profile) { + public boolean canBeCreatedBy(RequestingUser profile) { OtpUser otpUser = profile.otpUser; if (userId == null) { if (otpUser == null) { @@ -200,7 +200,7 @@ public boolean canBeCreatedBy(Auth0UserProfile profile) { * Confirm that the requesting user has the required permissions */ @Override - public boolean canBeManagedBy(Auth0UserProfile user) { + public boolean canBeManagedBy(RequestingUser user) { // This should not be possible, but return false on a null userId just in case. if (userId == null) return false; // If the user is attempting to update someone else's monitored trip, they must be admin. diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index 018d41ca0..612520872 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; -import org.opentripplanner.middleware.auth.Auth0UserProfile; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.AbstractUser; import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.otp.OtpDispatcherResponse; @@ -54,7 +54,7 @@ public static T getResourceFileContentsAsJSON (String resourcePathName, Clas } /** - * Send request to provided URL placing the Auth0 user id in the headers so that {@link Auth0UserProfile} can check + * Send request to provided URL placing the Auth0 user id in the headers so that {@link RequestingUser} can check * the database for a matching user. Returns the response. */ public static HttpResponse mockAuthenticatedRequest(String path, AbstractUser requestingUser, HttpUtils.REQUEST_METHOD requestMethod) { @@ -107,7 +107,7 @@ private static HashMap getMockHeaders(AbstractUser requestingUse } /** - * Send request to provided URL placing the Auth0 user id in the headers so that {@link Auth0UserProfile} can check + * Send request to provided URL placing the Auth0 user id in the headers so that {@link RequestingUser} can check * the database for a matching user. Returns the response. */ public static HttpResponse mockAuthenticatedPost(String path, AbstractUser requestingUser, String body) { From 07b78c28a38c75c04ea2246d4a5d5c9506e4d412 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 2 Oct 2020 14:32:47 +0100 Subject: [PATCH 02/30] refactor(Refactor of Api user auth): Various updates to auth Api users by api key instead of Auth0 --- .../middleware/auth/Auth0Connection.java | 59 ++++++++++-- .../middleware/auth/RequestingUser.java | 35 +++++-- .../api/AbstractUserController.java | 3 +- .../controllers/api/ApiController.java | 8 +- .../controllers/api/OtpRequestProcessor.java | 36 +++++-- .../middleware/models/AbstractUser.java | 7 ++ .../middleware/models/ApiUser.java | 40 ++++---- .../middleware/models/MonitoredTrip.java | 29 +++--- .../middleware/models/OtpUser.java | 19 +++- .../middleware/ApiKeyManagementTest.java | 6 +- .../middleware/ApiUserFlowTest.java | 96 ++++++++++++------- .../opentripplanner/middleware/TestUtils.java | 33 +++++-- 12 files changed, 276 insertions(+), 95 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 36b78e96a..3d2c41034 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -14,6 +14,7 @@ import org.opentripplanner.middleware.models.AbstractUser; import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.models.OtpUser; +import org.opentripplanner.middleware.persistence.Persistence; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.HaltException; @@ -33,6 +34,7 @@ * This handles verifying the Auth0 token passed in the auth header (e.g., Authorization: Bearer MY_TOKEN of Spark HTTP * requests. */ +// TODO: Come up with a name that covers Auth0 and apiKey auth... could just remove the '0'?! public class Auth0Connection { private static final Logger LOG = LoggerFactory.getLogger(Auth0Connection.class); private static JWTVerifier verifier; @@ -57,6 +59,20 @@ public static void checkUser(Request req) { addUserToRequest(req, RequestingUser.createTestUser(req)); return; } + + // API user authenticated by API key + String apiKey = getApiKeyFromRequest(req); + if (apiKey != null) { + RequestingUser requestingUser = new RequestingUser(apiKey); + if (!isValidUser(requestingUser)) { + // Otherwise, if no valid user is found, halt the request. + logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "API key auth - Unknown user."); + } + addUserToRequest(req, requestingUser); + return; + } + + // Admin and OTP users authenticated by Bearer token String token = getTokenFromRequest(req); // Handle getting the verifier outside of the below verification try/catch, which is intended to catch issues // with the client request. (getVerifier has its own exception/halt handling). @@ -74,7 +90,7 @@ public static void checkUser(Request req) { LOG.info("New user is creating self. OK to proceed without existing user object for auth0UserId"); } else { // Otherwise, if no valid user is found, halt the request. - logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "Unknown user."); + logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "Auth0 auth - Unknown user."); } } // The user attribute is used on the server side to check user permissions and does not have all of the @@ -124,6 +140,10 @@ public static boolean isAuthHeaderPresent(Request req) { return authHeader != null; } + public static boolean isApiKeyHeaderPresent(Request req) { + final String apiKey = req.headers("x-api-key"); + return apiKey != null; + } /** * Assign user to request and check that the user is an admin. @@ -163,6 +183,24 @@ public static RequestingUser getUserFromRequest(Request req) { return req.attribute("user"); } + + /** + * Extract API key from Spark HTTP request (in Authorization header). + */ + private static String getApiKeyFromRequest(Request req) { + if (!isApiKeyHeaderPresent(req)) { + // x-api-key header not present, fallback onto Auth0 check. + return null; + } + + final String apiKey = req.headers("x-api-key"); + if (apiKey == null) { + logMessageAndHalt(req, 401, "Could not find api key"); + } + + return apiKey; + } + /** * Extract JWT token from Spark HTTP request (in Authorization header). */ @@ -244,19 +282,28 @@ private static boolean isValidUser(RequestingUser profile) { * Confirm that the user's actions are on their items if not admin. */ public static void isAuthorized(String userId, Request request) { - RequestingUser profile = getUserFromRequest(request); + RequestingUser requestingUser = getUserFromRequest(request); // let admin do anything - if (profile.adminUser != null) { + if (requestingUser.adminUser != null) { return; } - // If userId is defined, it must be set to a value associated with the user. + // If userId is defined, it must be set to a value associated with the a user. if (userId != null) { - if (profile.otpUser != null && profile.otpUser.id.equals(userId)) { + if (requestingUser.otpUser != null && requestingUser.otpUser.id.equals(userId)) { + // Otp user requesting their item. return; } - if (profile.apiUser != null && profile.apiUser.id.equals(userId)) { + if (requestingUser.apiUser != null && requestingUser.apiUser.id.equals(userId)) { + // Api user requesting their item. return; } + if (requestingUser.apiUser != null) { + // Api user potentially requesting an item on behave of an Otp user they created. + OtpUser otpUser = Persistence.otpUsers.getById(userId); + if (otpUser != null && requestingUser.apiUser.id.equals(otpUser.applicationId)) { + return; + } + } } logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, "Unauthorized access."); } diff --git a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index 822ccf403..cae8a8a03 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -9,8 +9,11 @@ import org.opentripplanner.middleware.persistence.Persistence; import spark.Request; +import java.util.UUID; + import static com.mongodb.client.model.Filters.eq; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthHeaderPresent; +import static org.opentripplanner.middleware.utils.HttpUtils.getRequiredQueryParamFromRequest; /** * User profile that is attached to an HTTP request. @@ -21,43 +24,56 @@ public class RequestingUser { public ApiUser apiUser; public AdminUser adminUser; public String auth0UserId; + public String apiKey; /** * Constructor is only used for creating a test user. If an Auth0 user id is provided check persistence for matching * user else create default user. */ - private RequestingUser(String auth0UserId) { + private RequestingUser(String auth0UserId, String apiKey) { if (auth0UserId == null) { this.auth0UserId = "user_id:string"; otpUser = new OtpUser(); - apiUser = new ApiUser(); adminUser = new AdminUser(); } else { this.auth0UserId = auth0UserId; Bson withAuth0UserId = eq("auth0UserId", auth0UserId); otpUser = Persistence.otpUsers.getOneFiltered(withAuth0UserId); adminUser = Persistence.adminUsers.getOneFiltered(withAuth0UserId); - apiUser = Persistence.apiUsers.getOneFiltered(withAuth0UserId); + } + + if (apiKey == null) { + this.apiKey = UUID.randomUUID().toString(); + apiUser = new ApiUser(); + } else { + this.apiKey = apiKey; + apiUser = getApiUserForApiKey(apiKey); } } /** - * Create a user profile from the request's JSON web token. Check persistence for stored user + * Create a user profile from the request's JSON web token. Check persistence for stored user. */ public RequestingUser(DecodedJWT jwt) { this.auth0UserId = jwt.getClaim("sub").asString(); Bson withAuth0UserId = eq("auth0UserId", auth0UserId); otpUser = Persistence.otpUsers.getOneFiltered(withAuth0UserId); adminUser = Persistence.adminUsers.getOneFiltered(withAuth0UserId); - apiUser = Persistence.apiUsers.getOneFiltered(withAuth0UserId); } - public RequestingUser(String apiKey, boolean isApiUser) { + /** + * Create an API user profile from the provided apiKey. Check persistence for matching stored user. + */ + public RequestingUser(String apiKey) { + this.apiKey = apiKey; apiUser = getApiUserForApiKey(apiKey); } + /** + * Get an API user matching the provided API key + */ private ApiUser getApiUserForApiKey(String apiKey) { - return Persistence.apiUsers.getOneFiltered(); + return ApiUser.userForApiKeyValue(apiKey); } /** @@ -66,11 +82,14 @@ private ApiUser getApiUserForApiKey(String apiKey) { */ static RequestingUser createTestUser(Request req) { String auth0UserId = null; + if (isAuthHeaderPresent(req)) { // If the auth header has been provided get the Auth0 user id from it. This is different from normal // operation as the parameter will only contain the Auth0 user id and not "Bearer token". auth0UserId = req.headers("Authorization"); } - return new RequestingUser(auth0UserId); + + String apiKey = getRequiredQueryParamFromRequest(req, "apiKey", true); + return new RequestingUser(auth0UserId, apiKey); } } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java index cc4dfbb9b..2b4f98e78 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java @@ -8,6 +8,7 @@ import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.auth.Auth0Users; import org.opentripplanner.middleware.models.AbstractUser; +import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.persistence.TypedPersistence; import org.opentripplanner.middleware.utils.JsonUtils; @@ -105,7 +106,7 @@ U preCreateHook(U user, Request req) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // TODO: If MOD UI is to be an ApiUser, we may want to do an additional check here to determine if this is a // first-party API user (MOD UI) or third party. - if (requestingUser.apiUser != null && user instanceof OtpUser) { + if (requestingUser.apiUser != null && user instanceof OtpUser || user instanceof ApiUser) { // Do not create Auth0 account for OtpUsers created on behalf of third party API users. return user; } else { diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java index 206acd585..83c51950c 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java @@ -30,6 +30,7 @@ import static org.opentripplanner.middleware.auth.Auth0Connection.getUserFromRequest; import static org.opentripplanner.middleware.auth.Auth0Connection.isUserAdmin; import static org.opentripplanner.middleware.utils.HttpUtils.JSON_ONLY; +import static org.opentripplanner.middleware.utils.HttpUtils.getRequiredQueryParamFromRequest; import static org.opentripplanner.middleware.utils.JsonUtils.getPOJOFromRequestBody; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; @@ -184,11 +185,14 @@ private List getMany(Request req, Response res) { // OtpUserController. Therefore, the request should be limited to return just the entity matching the // requesting user. return getObjectsFiltered("_id", requestingUser.otpUser.id); + } else if (requestingUser.apiUser != null) { + // Third party API users must pass in an OtpUser id as a query param in order to get filtered objects. + // Query param is used so existing (and new) endpoints aren't affected. + String otpUserId = getRequiredQueryParamFromRequest(req, "otpUserId", false); + return getObjectsFiltered("userId", otpUserId); } else { // For all other cases the assumption is that the request is being made by an Otp user and the requested // entities have a 'userId' parameter. Only entities that match the requesting user id are returned. - // FIXME: This needs to change so that third party API users must pass in an OtpUser id in order to get - // filtered objects. This could be either a param (in path) or query param. return getObjectsFiltered("userId", requestingUser.otpUser.id); } } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index 69991bdf9..fbe34a496 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -5,6 +5,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.auth.RequestingUser; +import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TripRequest; import org.opentripplanner.middleware.models.TripSummary; import org.opentripplanner.middleware.otp.OtpDispatcher; @@ -23,6 +24,7 @@ import static com.beerboy.ss.descriptor.MethodDescriptor.path; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.APPLICATION_XML; +import static org.opentripplanner.middleware.auth.Auth0Connection.isApiKeyHeaderPresent; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthHeaderPresent; import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_API_ROOT; import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_PLAN_ENDPOINT; @@ -105,9 +107,10 @@ private static String proxy(Request request, spark.Response response) { */ private static void handlePlanTripResponse(Request request, OtpDispatcherResponse otpDispatcherResponse) { - // If the Auth header is present, this indicates that the request was made by a logged in user. This indicates - // that we should store trip history (but we verify this preference before doing so). - if (!isAuthHeaderPresent(request)) { + // If the Auth header is present, this indicates that the request was made by a logged in user. If the Api key + // header is present, this indicates that the request was made by an Api user. If either are present we should + // store trip history (but we verify this preference before doing so). + if (!isAuthHeaderPresent(request) && !isApiKeyHeaderPresent(request)) { LOG.debug("Anonymous user, trip history not stored"); return; } @@ -122,9 +125,30 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons long tripStorageStartTime = DateTimeUtils.currentTimeMillis(); Auth0Connection.checkUser(request); - RequestingUser profile = Auth0Connection.getUserFromRequest(request); + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(request); + if (requestingUser == null) { + return; + } + + OtpUser otpUser; + if (requestingUser.apiUser != null) { + // Api user making a trip request on behalf of an Otp user. In this case, the Otp user id must be provided + // as a query parameter. + String otpUserId = request.queryParams("userId"); + otpUser = Persistence.otpUsers.getById(otpUserId); + if (otpUser != null && !otpUser.canBeManagedBy(requestingUser)) { + logMessageAndHalt(request, + HttpStatus.FORBIDDEN_403, + String.format("Api user: %s not authorized to make trip requests for Otp user: %s", + requestingUser.apiUser.email, + otpUser.email)); + } + } else { + // Otp user making a trip request for self. + otpUser = requestingUser.otpUser; + } - final boolean storeTripHistory = profile != null && profile.otpUser != null && profile.otpUser.storeTripHistory; + final boolean storeTripHistory = otpUser != null && otpUser.storeTripHistory; // only save trip details if the user has given consent and a response from OTP is provided if (!storeTripHistory) { LOG.debug("User does not want trip history stored"); @@ -133,7 +157,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons if (otpResponse == null) { LOG.warn("OTP response is null, cannot save trip history for user!"); } else { - TripRequest tripRequest = new TripRequest(profile.otpUser.id, batchId, request.queryParams("fromPlace"), + TripRequest tripRequest = new TripRequest(otpUser.id, batchId, request.queryParams("fromPlace"), request.queryParams("toPlace"), request.queryString()); // only save trip summary if the trip request was saved boolean tripRequestSaved = Persistence.tripRequests.create(tripRequest); diff --git a/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java b/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java index 2fb818d22..0cfe9a924 100644 --- a/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java @@ -30,6 +30,13 @@ public abstract class AbstractUser extends Model { * value, so the stored user will contain the value from Auth0 (e.g., "auth0|abcd1234"). */ public String auth0UserId = UUID.randomUUID().toString(); + + /** + * Random api key for testing. + */ + public String apiKey = UUID.randomUUID().toString(); + + /** Whether a user is also a Data Tools user */ public boolean isDataToolsUser; /** diff --git a/src/main/java/org/opentripplanner/middleware/models/ApiUser.java b/src/main/java/org/opentripplanner/middleware/models/ApiUser.java index a09cd7649..98781832e 100644 --- a/src/main/java/org/opentripplanner/middleware/models/ApiUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/ApiUser.java @@ -1,5 +1,6 @@ package org.opentripplanner.middleware.models; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.utils.ApiGatewayUtils; import org.opentripplanner.middleware.utils.CreateApiKeyException; @@ -40,17 +41,10 @@ public class ApiUser extends AbstractUser { public String name; /** - * Delete API user details including Auth0 user. + * Delete API user's API keys (from AWS), self (from Mongo). */ @Override public boolean delete() { - return delete(true); - } - - /** - * Delete API user's API keys (from AWS), self (from Mongo). Optionally delete user from Auth0. - */ - public boolean delete(boolean deleteAuth0User) { for (ApiKey apiKey : apiKeys) { if (!ApiGatewayUtils.deleteApiKey(apiKey)) { LOG.error("Could not delete API key for user {}. Aborting delete user.", apiKey.keyId); @@ -58,14 +52,6 @@ public boolean delete(boolean deleteAuth0User) { } } - if (deleteAuth0User) { - boolean auth0UserDeleted = super.delete(); - if (!auth0UserDeleted) { - LOG.warn("Aborting user deletion for {}", this.email); - return false; - } - } - return Persistence.apiUsers.removeById(this.id); } @@ -87,4 +73,26 @@ public void createApiKey(String usagePlanId, boolean persist) throws CreateApiKe public static ApiUser userForApiKey(String apiKeyId) { return Persistence.apiUsers.getOneFiltered(Filters.elemMatch("apiKeys", Filters.eq("keyId", apiKeyId))); } + + /** + * @return the first {@link ApiUser} found with an {@link ApiKey#value} in {@link #apiKeys} that matches the + * provided api key value. + */ + public static ApiUser userForApiKeyValue(String apiKeyValue) { + return Persistence.apiUsers.getOneFiltered(Filters.elemMatch("apiKeys", Filters.eq("value", apiKeyValue))); + } + + /** + * Confirm that the requesting user has the required permissions + */ + @Override + public boolean canBeManagedBy(RequestingUser requestingUser) { + if (requestingUser.apiUser != null && requestingUser.apiUser.id.equals(id)) { + // Otp user was created by this Api user. + return true; + } + // Fallback to Model#userCanManage. + return super.canBeManagedBy(requestingUser); + } + } diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index 4c4533a82..fe9768165 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -179,9 +179,9 @@ public boolean isActiveOnDate(ZonedDateTime zonedDateTime) { } @Override - public boolean canBeCreatedBy(RequestingUser profile) { - OtpUser otpUser = profile.otpUser; + public boolean canBeCreatedBy(RequestingUser requestingUser) { if (userId == null) { + OtpUser otpUser = requestingUser.otpUser; if (otpUser == null) { // The otpUser must exist (and be the requester) if the userId is null. Otherwise, there is nobody to // assign the trip to. @@ -191,35 +191,42 @@ public boolean canBeCreatedBy(RequestingUser profile) { userId = otpUser.id; } else { // If userId was provided, follow authorization provided by canBeManagedBy - return canBeManagedBy(profile); + return canBeManagedBy(requestingUser); } - return super.canBeCreatedBy(profile); + return super.canBeCreatedBy(requestingUser); } /** * Confirm that the requesting user has the required permissions */ @Override - public boolean canBeManagedBy(RequestingUser user) { + public boolean canBeManagedBy(RequestingUser requestingUser) { // This should not be possible, but return false on a null userId just in case. if (userId == null) return false; - // If the user is attempting to update someone else's monitored trip, they must be admin. + // If the user is attempting to update someone else's monitored trip, they must be admin or an API user if the + // OTP user is assigned to that API. boolean belongsToUser = false; // Monitored trip can only be owned by an OtpUser (not an ApiUser or AdminUser). - if (user.otpUser != null) { - belongsToUser = userId.equals(user.otpUser.id); + if (requestingUser.otpUser != null) { + belongsToUser = userId.equals(requestingUser.otpUser.id); } if (belongsToUser) { return true; - } else if (user.adminUser != null) { + } else if (requestingUser.apiUser != null) { + // get the required OTP user to confirm they are associated with the requesting API user. + OtpUser otpUser = Persistence.otpUsers.getById(userId); + if (otpUser != null && requestingUser.apiUser.id.equals(otpUser.applicationId)) { + return true; + } + } else if (requestingUser.adminUser != null) { // If not managing self, user must have manage permission. - for (Permission permission : user.adminUser.permissions) { + for (Permission permission : requestingUser.adminUser.permissions) { if (permission.canManage(this.getClass())) return true; } } // Fallback to Model#userCanManage. - return super.canBeManagedBy(user); + return super.canBeManagedBy(requestingUser); } private Bson tripIdFilter() { diff --git a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java index 777f04204..1b75d5b6a 100644 --- a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java @@ -1,6 +1,7 @@ package org.opentripplanner.middleware.models; import com.fasterxml.jackson.annotation.JsonIgnore; +import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.persistence.Persistence; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,10 +73,26 @@ public boolean delete(boolean deleteAuth0User) { boolean auth0UserDeleted = super.delete(); if (!auth0UserDeleted) { LOG.warn("Aborting user deletion for {}", this.email); - return false; + // FIXME: This fails if an Api user is attempting to delete an Otp user they created. No Auth0 account + // would have been created for this user. +// return false; } } return Persistence.otpUsers.removeById(this.id); } + + /** + * Confirm that the requesting user has the required permissions + */ + @Override + public boolean canBeManagedBy(RequestingUser requestingUser) { + if (requestingUser.apiUser != null && requestingUser.apiUser.id.equals(applicationId)) { + // Otp user was created by this Api user. + return true; + } + // Fallback to Model#userCanManage. + return super.canBeManagedBy(requestingUser); + } + } diff --git a/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java b/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java index 4525c0e62..58f94ece1 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.opentripplanner.middleware.TestUtils.isEndToEnd; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; +import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; import static org.opentripplanner.middleware.auth.Auth0Connection.setAuthDisabled; import static org.opentripplanner.middleware.controllers.api.ApiUserController.DEFAULT_USAGE_PLAN_ID; @@ -39,6 +40,7 @@ public class ApiKeyManagementTest extends OtpMiddlewareTest { private static final Logger LOG = LoggerFactory.getLogger(ApiKeyManagementTest.class); private static ApiUser apiUser; private static AdminUser adminUser; + private static boolean prevAuthState; /** * Create an {@link ApiUser} and an {@link AdminUser} prior to unit tests @@ -48,6 +50,7 @@ public static void setUp() throws IOException, InterruptedException { assumeTrue(isEndToEnd); // TODO: It might be useful to allow this to run without DISABLE_AUTH set to true (in an end-to-end environment // using real tokens from Auth0. + prevAuthState = isAuthDisabled(); setAuthDisabled(true); // Load config before checking if tests should run. OtpMiddlewareTest.setUp(); @@ -63,8 +66,9 @@ public static void tearDown() { assumeTrue(isEndToEnd); // refresh API key(s) apiUser = Persistence.apiUsers.getById(apiUser.id); - apiUser.delete(false); + apiUser.delete(); Persistence.adminUsers.removeById(adminUser.id); + setAuthDisabled(prevAuthState); } /** diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index f858875fd..ff14c7684 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -1,7 +1,5 @@ package org.opentripplanner.middleware; -import com.auth0.exception.Auth0Exception; -import com.auth0.json.mgmt.users.User; import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -31,7 +29,6 @@ import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedPost; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; -import static org.opentripplanner.middleware.auth.Auth0Users.createAuth0UserForEmail; import static org.opentripplanner.middleware.controllers.api.ApiUserController.DEFAULT_USAGE_PLAN_ID; import static org.opentripplanner.middleware.controllers.api.OtpRequestProcessor.OTP_PROXY_ENDPOINT; import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_PLAN_ENDPOINT; @@ -86,24 +83,12 @@ public static void setUp() throws IOException, InterruptedException, CreateApiKe // As a pre-condition, create an API User with API key. apiUser = PersistenceUtil.createApiUser(String.format("test-%s@example.com", UUID.randomUUID().toString())); apiUser.createApiKey(DEFAULT_USAGE_PLAN_ID, true); + Persistence.apiUsers.replace(apiUser.id, apiUser); // Create, but do not persist, an OTP user. otpUser = new OtpUser(); otpUser.email = String.format("test-%s@example.com", UUID.randomUUID().toString()); otpUser.hasConsentedToTerms = true; otpUser.storeTripHistory = true; - // create Auth0 users for apiUser and optUser. - try { - User auth0User = createAuth0UserForEmail(apiUser.email, TestUtils.TEMP_AUTH0_USER_PASSWORD); - // update api user with valid auth0 user ID (so the Auth0 delete works) - apiUser.auth0UserId = auth0User.getId(); - Persistence.apiUsers.replace(apiUser.id, apiUser); - - auth0User = createAuth0UserForEmail(otpUser.email, TestUtils.TEMP_AUTH0_USER_PASSWORD); - // update otp user with valid auth0 user ID (so the Auth0 delete works) - otpUser.auth0UserId = auth0User.getId(); - } catch (Auth0Exception e) { - throw new RuntimeException(e); - } } /** @@ -115,7 +100,7 @@ public static void tearDown() { apiUser = Persistence.apiUsers.getById(apiUser.id); if (apiUser != null) apiUser.delete(); otpUser = Persistence.otpUsers.getById(otpUser.id); - if (otpUser != null) otpUser.delete(); + if (otpUser != null) otpUser.delete(false); } /** @@ -124,48 +109,91 @@ public static void tearDown() { */ @Test public void canSimulateApiUserFlow() { - // create otp user as api user + // create otp user as api user. HttpResponse createUserResponse = mockAuthenticatedPost("api/secure/user", apiUser, JsonUtils.toJson(otpUser) ); assertEquals(HttpStatus.OK_200, createUserResponse.statusCode()); + + // Create a monitored trip for an Otp user as Otp user. This will fail because the user was created by an + // Api user and therefore does not have a Auth0 account. OtpUser otpUserResponse = JsonUtils.getPOJOFromJSON(createUserResponse.body(), OtpUser.class); - // Create a monitored trip for the Otp user (API users are prevented from doing this). MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); monitoredTrip.userId = otpUser.id; - HttpResponse createTripResponse = mockAuthenticatedPost("api/secure/monitoredtrip", + HttpResponse createTripResponseAsOtpUser = mockAuthenticatedPost("api/secure/monitoredtrip", otpUserResponse, JsonUtils.toJson(monitoredTrip) ); - assertEquals(HttpStatus.OK_200, createTripResponse.statusCode()); - MonitoredTrip monitoredTripResponse = JsonUtils.getPOJOFromJSON(createTripResponse.body(), MonitoredTrip.class); + assertEquals(HttpStatus.UNAUTHORIZED_401, createTripResponseAsOtpUser.statusCode()); + + // Create a monitored trip for the Otp user as Api user. An Api user should be able to create a monitored trip + // for an Otp user they created. + HttpResponse createTripResponseAsApiUser = mockAuthenticatedPost("api/secure/monitoredtrip", + apiUser, + JsonUtils.toJson(monitoredTrip) + ); + assertEquals(HttpStatus.OK_200, createTripResponseAsApiUser.statusCode()); + MonitoredTrip monitoredTripResponse = JsonUtils.getPOJOFromJSON(createTripResponseAsApiUser.body(), MonitoredTrip.class); - // Plan trip with OTP proxy. Mock plan response will be returned - String otpQuery = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745"; - HttpResponse planTripResponse = mockAuthenticatedRequest(otpQuery, + // Plan trip with OTP proxy. Mock plan response will be returned. This will work as an Otp user (created by MOD UI + // or an Api user) because the end point has not auth. + String otpQuery = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745&userId=" + otpUserResponse.id; + HttpResponse planTripResponseAsOtUser = mockAuthenticatedRequest(otpQuery, otpUserResponse, HttpUtils.REQUEST_METHOD.GET ); - LOG.info("Plan trip response: {}\n....", planTripResponse.body().substring(0, 300)); - assertEquals(HttpStatus.OK_200, planTripResponse.statusCode()); + LOG.info("Plan trip response: {}\n....", planTripResponseAsOtUser.body().substring(0, 300)); + assertEquals(HttpStatus.OK_200, planTripResponseAsOtUser.statusCode()); - // Get trip for user, before it is deleted - HttpResponse tripRequestResponse = mockAuthenticatedRequest(String.format("api/secure/triprequests?userId=%s", + // Plan trip with OTP proxy. Mock plan response will be returned. This will work as an Api user because the end + // point has not auth. + HttpResponse planTripResponseAsApiUser = mockAuthenticatedRequest(otpQuery, + apiUser, + HttpUtils.REQUEST_METHOD.GET + ); + LOG.info("Plan trip response: {}\n....", planTripResponseAsApiUser.body().substring(0, 300)); + assertEquals(HttpStatus.OK_200, planTripResponseAsApiUser.statusCode()); + + + // Get trip for user as Otp user. This will fail because the user was created by an + // Api user and therefore does not have a Auth0 account. + HttpResponse tripRequestResponseAsOtUser = mockAuthenticatedRequest(String.format("api/secure/triprequests?userId=%s", otpUserResponse.id), otpUserResponse, HttpUtils.REQUEST_METHOD.GET ); - assertEquals(HttpStatus.OK_200, tripRequestResponse.statusCode()); - List tripRequests = JsonUtils.getPOJOFromJSONAsList(tripRequestResponse.body(), TripRequest.class); + assertEquals(HttpStatus.UNAUTHORIZED_401, tripRequestResponseAsOtUser.statusCode()); + + // Get trip for user as an Api user. This should work because an Api user should be able to get a trip for an + // Otp user they created. + HttpResponse tripRequestResponseAsApiUser = mockAuthenticatedRequest(String.format("api/secure/triprequests?userId=%s", + otpUserResponse.id), + apiUser, + HttpUtils.REQUEST_METHOD.GET + ); + assertEquals(HttpStatus.OK_200, tripRequestResponseAsApiUser.statusCode()); + + List tripRequests = JsonUtils.getPOJOFromJSONAsList(tripRequestResponseAsApiUser.body(), TripRequest.class); - // Delete otp user. - HttpResponse deleteUserResponse = mockAuthenticatedRequest( + // Delete otp user as Otp user. This will fail because the user was created by an Api user and therefore does + // not have a Auth0 account. + HttpResponse deleteUserResponseAsOtpUser = mockAuthenticatedRequest( String.format("api/secure/user/%s", otpUserResponse.id), otpUserResponse, HttpUtils.REQUEST_METHOD.DELETE ); - assertEquals(HttpStatus.OK_200, deleteUserResponse.statusCode()); + assertEquals(HttpStatus.UNAUTHORIZED_401, deleteUserResponseAsOtpUser.statusCode()); + + // Delete otp user as Api user. This should work because an Api user should be able to delete an Otp user they + // created. + HttpResponse deleteUserResponseAsApiUser = mockAuthenticatedRequest( + String.format("api/secure/user/%s", otpUserResponse.id), + apiUser, + HttpUtils.REQUEST_METHOD.DELETE + ); + assertEquals(HttpStatus.OK_200, deleteUserResponseAsApiUser.statusCode()); + // Verify user no longer exists. OtpUser deletedOtpUser = Persistence.otpUsers.getById(otpUserResponse.id); diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index cc4852d00..962f67ea8 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -5,8 +5,10 @@ import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.AbstractUser; import org.opentripplanner.middleware.models.ApiUser; +import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.otp.OtpDispatcher; import org.opentripplanner.middleware.otp.OtpDispatcherResponse; +import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.utils.FileUtils; import org.opentripplanner.middleware.utils.HttpUtils; import org.slf4j.Logger; @@ -94,23 +96,36 @@ private static HashMap getMockHeaders(AbstractUser requestingUse // the request when received. if (isAuthDisabled()) { headers.put("Authorization", requestingUser.auth0UserId); - } else { - // Otherwise, get a valid oauth token for the user - String token = null; - try { - token = getAuth0Token(requestingUser.email, TEMP_AUTH0_USER_PASSWORD); - } catch (JsonProcessingException e) { - LOG.error("Cannot obtain Auth0 token for user {}", requestingUser.email, e); - } - headers.put("Authorization", "Bearer " + token); + headers.put("x-api-key", requestingUser.apiKey); + return headers; } + // If requester is an API user, add API key value as x-api-key header to simulate request over API Gateway. if (requestingUser instanceof ApiUser) { ApiUser apiUser = (ApiUser) requestingUser; if (!apiUser.apiKeys.isEmpty()) { headers.put("x-api-key", apiUser.apiKeys.get(0).value); } + return headers; + } + + // If requester is an Otp user which was created by an Api user, return empty header because an Otp user created + // by an Api user can not directly access the middleware. + if (requestingUser instanceof OtpUser) { + OtpUser otpUserFromDB = Persistence.otpUsers.getById(requestingUser.id); + if (otpUserFromDB != null && otpUserFromDB.applicationId != null) { + return headers; + } + } + + // Otherwise, get a valid oauth token for the user + String token = null; + try { + token = getAuth0Token(requestingUser.email, TEMP_AUTH0_USER_PASSWORD); + } catch (JsonProcessingException e) { + LOG.error("Cannot obtain Auth0 token for user {}", requestingUser.email, e); } + headers.put("Authorization", "Bearer " + token); return headers; } From 1f60f1ea7f36fe89f60bcdda3f347df3afcb6ba6 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 2 Oct 2020 17:20:43 +0100 Subject: [PATCH 03/30] refactor(Additional auth check): Auth0 account will only be deleted if application id is empty --- .../opentripplanner/middleware/auth/Auth0Connection.java | 7 ++++--- .../org/opentripplanner/middleware/models/OtpUser.java | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 3d2c41034..681231ae6 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -279,7 +279,8 @@ private static boolean isValidUser(RequestingUser profile) { } /** - * Confirm that the user's actions are on their items if not admin. + * Confirm that the user's actions are on their items if not admin. In the case of an Api user confirm that the + * user's actions, on Otp users, are Otp users they created initially. */ public static void isAuthorized(String userId, Request request) { RequestingUser requestingUser = getUserFromRequest(request); @@ -287,7 +288,7 @@ public static void isAuthorized(String userId, Request request) { if (requestingUser.adminUser != null) { return; } - // If userId is defined, it must be set to a value associated with the a user. + // If userId is defined, it must be set to a value associated with a user. if (userId != null) { if (requestingUser.otpUser != null && requestingUser.otpUser.id.equals(userId)) { // Otp user requesting their item. @@ -298,7 +299,7 @@ public static void isAuthorized(String userId, Request request) { return; } if (requestingUser.apiUser != null) { - // Api user potentially requesting an item on behave of an Otp user they created. + // Api user potentially requesting an item on behalf of an Otp user they created. OtpUser otpUser = Persistence.otpUsers.getById(userId); if (otpUser != null && requestingUser.apiUser.id.equals(otpUser.applicationId)) { return; diff --git a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java index 1b75d5b6a..07c0f5d47 100644 --- a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java @@ -69,13 +69,12 @@ public boolean delete(boolean deleteAuth0User) { } } - if (deleteAuth0User) { + // Only attempt to delete Auth0 user if Otp user is not assigned to third party. + if (deleteAuth0User && applicationId.isEmpty()) { boolean auth0UserDeleted = super.delete(); if (!auth0UserDeleted) { LOG.warn("Aborting user deletion for {}", this.email); - // FIXME: This fails if an Api user is attempting to delete an Otp user they created. No Auth0 account - // would have been created for this user. -// return false; + return false; } } From cd7a6688bb4f2806a755dd415f84820c6676921f Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 9 Oct 2020 09:14:24 +0100 Subject: [PATCH 04/30] refactor(Authenticate Api users with Auth0. New endpoint for Api users to auth with middleware.): Re --- .../middleware/auth/Auth0Connection.java | 33 -------------- .../middleware/auth/Auth0Users.java | 7 +-- .../middleware/auth/RequestingUser.java | 35 +++------------ .../api/AbstractUserController.java | 3 +- .../controllers/api/ApiUserController.java | 30 ++++++++++++- .../controllers/api/OtpRequestProcessor.java | 10 ++--- .../middleware/models/ApiUser.java | 40 +++++++---------- .../middleware/ApiUserFlowTest.java | 45 ++++++++++++++----- .../opentripplanner/middleware/TestUtils.java | 8 ---- 9 files changed, 92 insertions(+), 119 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 681231ae6..c1f0cea7d 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -34,7 +34,6 @@ * This handles verifying the Auth0 token passed in the auth header (e.g., Authorization: Bearer MY_TOKEN of Spark HTTP * requests. */ -// TODO: Come up with a name that covers Auth0 and apiKey auth... could just remove the '0'?! public class Auth0Connection { private static final Logger LOG = LoggerFactory.getLogger(Auth0Connection.class); private static JWTVerifier verifier; @@ -59,19 +58,6 @@ public static void checkUser(Request req) { addUserToRequest(req, RequestingUser.createTestUser(req)); return; } - - // API user authenticated by API key - String apiKey = getApiKeyFromRequest(req); - if (apiKey != null) { - RequestingUser requestingUser = new RequestingUser(apiKey); - if (!isValidUser(requestingUser)) { - // Otherwise, if no valid user is found, halt the request. - logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "API key auth - Unknown user."); - } - addUserToRequest(req, requestingUser); - return; - } - // Admin and OTP users authenticated by Bearer token String token = getTokenFromRequest(req); // Handle getting the verifier outside of the below verification try/catch, which is intended to catch issues @@ -183,24 +169,6 @@ public static RequestingUser getUserFromRequest(Request req) { return req.attribute("user"); } - - /** - * Extract API key from Spark HTTP request (in Authorization header). - */ - private static String getApiKeyFromRequest(Request req) { - if (!isApiKeyHeaderPresent(req)) { - // x-api-key header not present, fallback onto Auth0 check. - return null; - } - - final String apiKey = req.headers("x-api-key"); - if (apiKey == null) { - logMessageAndHalt(req, 401, "Could not find api key"); - } - - return apiKey; - } - /** * Extract JWT token from Spark HTTP request (in Authorization header). */ @@ -308,5 +276,4 @@ public static void isAuthorized(String userId, Request request) { } logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, "Unauthorized access."); } - } diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index 2cc9e6ab6..9107b41af 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -42,6 +42,7 @@ public class Auth0Users { private static final String AUTH0_CLIENT_ID = getConfigPropertyAsText("AUTH0_CLIENT_ID"); private static final String AUTH0_CLIENT_SECRET = getConfigPropertyAsText("AUTH0_CLIENT_SECRET"); private static final String DEFAULT_CONNECTION_TYPE = "Username-Password-Authentication"; + private static final String DEFAULT_AUDIENCE = "https://otp-middleware"; private static final String MANAGEMENT_API_VERSION = "v2"; private static final String SEARCH_API_VERSION = "v3"; public static final String API_PATH = "/api/" + MANAGEMENT_API_VERSION; @@ -53,8 +54,8 @@ public class Auth0Users { private static final AuthAPI authAPI = new AuthAPI(AUTH0_DOMAIN, AUTH0_API_CLIENT, AUTH0_API_SECRET); /** - * Creates a standard user for the provided email address. Defaults to a random UUID password and connection type of - * {@link #DEFAULT_CONNECTION_TYPE}. + * Creates a standard user for the provided email address, password (Defaulted to a random UUID) and connection type + * of {@link #DEFAULT_CONNECTION_TYPE}. */ public static User createAuth0UserForEmail(String email) throws Auth0Exception { return createAuth0UserForEmail(email, UUID.randomUUID().toString()); @@ -253,7 +254,7 @@ public static String getAuth0Token(String username, String password) throws Json "grant_type=password&username=%s&password=%s&audience=%s&scope=&client_id=%s&client_secret=%s", username, password, - "https://otp-middleware", // must match an API identifier + DEFAULT_AUDIENCE, // must match an API identifier AUTH0_CLIENT_ID, // Auth0 application client ID AUTH0_CLIENT_SECRET // Auth0 application client secret ); diff --git a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index cae8a8a03..9ef47dfb0 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -9,11 +9,8 @@ import org.opentripplanner.middleware.persistence.Persistence; import spark.Request; -import java.util.UUID; - import static com.mongodb.client.model.Filters.eq; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthHeaderPresent; -import static org.opentripplanner.middleware.utils.HttpUtils.getRequiredQueryParamFromRequest; /** * User profile that is attached to an HTTP request. @@ -24,31 +21,24 @@ public class RequestingUser { public ApiUser apiUser; public AdminUser adminUser; public String auth0UserId; - public String apiKey; /** * Constructor is only used for creating a test user. If an Auth0 user id is provided check persistence for matching * user else create default user. */ - private RequestingUser(String auth0UserId, String apiKey) { + private RequestingUser(String auth0UserId) { if (auth0UserId == null) { this.auth0UserId = "user_id:string"; otpUser = new OtpUser(); + apiUser = new ApiUser(); adminUser = new AdminUser(); } else { this.auth0UserId = auth0UserId; Bson withAuth0UserId = eq("auth0UserId", auth0UserId); otpUser = Persistence.otpUsers.getOneFiltered(withAuth0UserId); + apiUser = Persistence.apiUsers.getOneFiltered(withAuth0UserId); adminUser = Persistence.adminUsers.getOneFiltered(withAuth0UserId); } - - if (apiKey == null) { - this.apiKey = UUID.randomUUID().toString(); - apiUser = new ApiUser(); - } else { - this.apiKey = apiKey; - apiUser = getApiUserForApiKey(apiKey); - } } /** @@ -58,24 +48,10 @@ public RequestingUser(DecodedJWT jwt) { this.auth0UserId = jwt.getClaim("sub").asString(); Bson withAuth0UserId = eq("auth0UserId", auth0UserId); otpUser = Persistence.otpUsers.getOneFiltered(withAuth0UserId); + apiUser = Persistence.apiUsers.getOneFiltered(withAuth0UserId); adminUser = Persistence.adminUsers.getOneFiltered(withAuth0UserId); } - /** - * Create an API user profile from the provided apiKey. Check persistence for matching stored user. - */ - public RequestingUser(String apiKey) { - this.apiKey = apiKey; - apiUser = getApiUserForApiKey(apiKey); - } - - /** - * Get an API user matching the provided API key - */ - private ApiUser getApiUserForApiKey(String apiKey) { - return ApiUser.userForApiKeyValue(apiKey); - } - /** * Utility method for creating a test user. If a Auth0 user Id is defined within the Authorization header param * define test user based on this. @@ -89,7 +65,6 @@ static RequestingUser createTestUser(Request req) { auth0UserId = req.headers("Authorization"); } - String apiKey = getRequiredQueryParamFromRequest(req, "apiKey", true); - return new RequestingUser(auth0UserId, apiKey); + return new RequestingUser(auth0UserId); } } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java index 2b4f98e78..cc4dfbb9b 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java @@ -8,7 +8,6 @@ import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.auth.Auth0Users; import org.opentripplanner.middleware.models.AbstractUser; -import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.persistence.TypedPersistence; import org.opentripplanner.middleware.utils.JsonUtils; @@ -106,7 +105,7 @@ U preCreateHook(U user, Request req) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // TODO: If MOD UI is to be an ApiUser, we may want to do an additional check here to determine if this is a // first-party API user (MOD UI) or third party. - if (requestingUser.apiUser != null && user instanceof OtpUser || user instanceof ApiUser) { + if (requestingUser.apiUser != null && user instanceof OtpUser) { // Do not create Auth0 account for OtpUsers created on behalf of third party API users. return user; } else { diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java index 96197356d..eb2caef0a 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java @@ -1,8 +1,10 @@ package org.opentripplanner.middleware.controllers.api; import com.beerboy.ss.ApiEndpoint; +import com.fasterxml.jackson.core.JsonProcessingException; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; +import org.opentripplanner.middleware.auth.Auth0Users; import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.ApiKey; import org.opentripplanner.middleware.models.ApiUser; @@ -32,9 +34,12 @@ public class ApiUserController extends AbstractUserController { private static final Logger LOG = LoggerFactory.getLogger(ApiUserController.class); public static final String DEFAULT_USAGE_PLAN_ID = getConfigPropertyAsText("DEFAULT_USAGE_PLAN_ID"); private static final String API_KEY_PATH = "/apikey"; + private static final String AUTHENTICATE_PATH = "/authenticate"; private static final int API_KEY_LIMIT_PER_USER = 2; private static final String API_KEY_ID_PARAM = "/:apiKeyId"; public static final String API_USER_PATH = "secure/application"; + private static final String USERNAME_PARAM = "username"; + private static final String PASSWORD_PARAM = "password"; public ApiUserController(String apiPrefix) { super(apiPrefix, Persistence.apiUsers, API_USER_PATH); @@ -66,10 +71,20 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { .and() .withProduces(JSON_ONLY) .withResponseType(persistence.clazz), - this::deleteApiKeyForApiUser, JsonUtils::toJson + this::deleteApiKeyForApiUser, JsonUtils::toJson) + // Authenticate user with Auth0 + .post(path(ID_PATH + AUTHENTICATE_PATH) + .withDescription("Authenticates ApiUser with Auth0.") + .withPathParam().withName(ID_PARAM).withDescription("The user ID.").and() + .withQueryParam().withName(USERNAME_PARAM).withRequired(true) + .withDescription("Auth0 username (usually email address).").and() + .withQueryParam().withName(PASSWORD_PARAM).withRequired(true) + .withDescription("Auth0 password.").and() + .withProduces(JSON_ONLY) + .withResponseType(String.class), + this::authenticateAuth0User, JsonUtils::toJson ); - // Add the regular CRUD methods after defining the /apikey route. super.buildEndpoint(modifiedEndpoint); } @@ -84,6 +99,17 @@ private boolean userHasKey(ApiUser user, String apiKeyId) { .anyMatch(apiKey -> apiKeyId.equals(apiKey.keyId)); } + /** + * Authenticate user with Auth0 based on username (email) and password. If successful, return the bearer token else + * null. + */ + private String authenticateAuth0User(Request req, Response res) throws JsonProcessingException { + String username = HttpUtils.getRequiredQueryParamFromRequest(req, USERNAME_PARAM, false); + // FIXME: Should this be encrypted?! + String password = HttpUtils.getRequiredQueryParamFromRequest(req, PASSWORD_PARAM, false); + return Auth0Users.getAuth0Token(username, password); + } + /** * Create a new API key and assign it to the provided usage plan. If no usage plan is provided use the default * usage plan instead. diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index fbe34a496..f485620d6 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -24,7 +24,6 @@ import static com.beerboy.ss.descriptor.MethodDescriptor.path; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.APPLICATION_XML; -import static org.opentripplanner.middleware.auth.Auth0Connection.isApiKeyHeaderPresent; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthHeaderPresent; import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_API_ROOT; import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_PLAN_ENDPOINT; @@ -107,10 +106,9 @@ private static String proxy(Request request, spark.Response response) { */ private static void handlePlanTripResponse(Request request, OtpDispatcherResponse otpDispatcherResponse) { - // If the Auth header is present, this indicates that the request was made by a logged in user. If the Api key - // header is present, this indicates that the request was made by an Api user. If either are present we should - // store trip history (but we verify this preference before doing so). - if (!isAuthHeaderPresent(request) && !isApiKeyHeaderPresent(request)) { + // If the Auth header is present, this indicates that the request was made by a logged in user. If present + // we should store trip history (but we verify this preference before doing so). + if (!isAuthHeaderPresent(request)) { LOG.debug("Anonymous user, trip history not stored"); return; } @@ -139,7 +137,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons if (otpUser != null && !otpUser.canBeManagedBy(requestingUser)) { logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, - String.format("Api user: %s not authorized to make trip requests for Otp user: %s", + String.format("User: %s not authorized to make trip requests for user: %s", requestingUser.apiUser.email, otpUser.email)); } diff --git a/src/main/java/org/opentripplanner/middleware/models/ApiUser.java b/src/main/java/org/opentripplanner/middleware/models/ApiUser.java index 98781832e..a09cd7649 100644 --- a/src/main/java/org/opentripplanner/middleware/models/ApiUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/ApiUser.java @@ -1,6 +1,5 @@ package org.opentripplanner.middleware.models; -import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.utils.ApiGatewayUtils; import org.opentripplanner.middleware.utils.CreateApiKeyException; @@ -41,10 +40,17 @@ public class ApiUser extends AbstractUser { public String name; /** - * Delete API user's API keys (from AWS), self (from Mongo). + * Delete API user details including Auth0 user. */ @Override public boolean delete() { + return delete(true); + } + + /** + * Delete API user's API keys (from AWS), self (from Mongo). Optionally delete user from Auth0. + */ + public boolean delete(boolean deleteAuth0User) { for (ApiKey apiKey : apiKeys) { if (!ApiGatewayUtils.deleteApiKey(apiKey)) { LOG.error("Could not delete API key for user {}. Aborting delete user.", apiKey.keyId); @@ -52,6 +58,14 @@ public boolean delete() { } } + if (deleteAuth0User) { + boolean auth0UserDeleted = super.delete(); + if (!auth0UserDeleted) { + LOG.warn("Aborting user deletion for {}", this.email); + return false; + } + } + return Persistence.apiUsers.removeById(this.id); } @@ -73,26 +87,4 @@ public void createApiKey(String usagePlanId, boolean persist) throws CreateApiKe public static ApiUser userForApiKey(String apiKeyId) { return Persistence.apiUsers.getOneFiltered(Filters.elemMatch("apiKeys", Filters.eq("keyId", apiKeyId))); } - - /** - * @return the first {@link ApiUser} found with an {@link ApiKey#value} in {@link #apiKeys} that matches the - * provided api key value. - */ - public static ApiUser userForApiKeyValue(String apiKeyValue) { - return Persistence.apiUsers.getOneFiltered(Filters.elemMatch("apiKeys", Filters.eq("value", apiKeyValue))); - } - - /** - * Confirm that the requesting user has the required permissions - */ - @Override - public boolean canBeManagedBy(RequestingUser requestingUser) { - if (requestingUser.apiUser != null && requestingUser.apiUser.id.equals(id)) { - // Otp user was created by this Api user. - return true; - } - // Fallback to Model#userCanManage. - return super.canBeManagedBy(requestingUser); - } - } diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index ff14c7684..df2992f58 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -1,5 +1,7 @@ package org.opentripplanner.middleware; +import com.auth0.exception.Auth0Exception; +import com.auth0.json.mgmt.users.User; import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -10,7 +12,6 @@ import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TripRequest; import org.opentripplanner.middleware.persistence.Persistence; -import org.opentripplanner.middleware.persistence.PersistenceUtil; import org.opentripplanner.middleware.utils.CreateApiKeyException; import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.JsonUtils; @@ -25,13 +26,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.opentripplanner.middleware.TestUtils.TEMP_AUTH0_USER_PASSWORD; import static org.opentripplanner.middleware.TestUtils.isEndToEnd; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedPost; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; +import static org.opentripplanner.middleware.auth.Auth0Users.createAuth0UserForEmail; import static org.opentripplanner.middleware.controllers.api.ApiUserController.DEFAULT_USAGE_PLAN_ID; import static org.opentripplanner.middleware.controllers.api.OtpRequestProcessor.OTP_PROXY_ENDPOINT; import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_PLAN_ENDPOINT; +import static org.opentripplanner.middleware.persistence.PersistenceUtil.createApiUser; /** * Tests to simulate API user flow. The following config parameters must be set in configurations/default/env.yml for @@ -81,7 +85,7 @@ public static void setUp() throws IOException, InterruptedException, CreateApiKe // Mock the OTP server TODO: Run a live OTP instance? TestUtils.mockOtpServer(); // As a pre-condition, create an API User with API key. - apiUser = PersistenceUtil.createApiUser(String.format("test-%s@example.com", UUID.randomUUID().toString())); + apiUser = createApiUser(String.format("test-%s@example.com", UUID.randomUUID().toString())); apiUser.createApiKey(DEFAULT_USAGE_PLAN_ID, true); Persistence.apiUsers.replace(apiUser.id, apiUser); // Create, but do not persist, an OTP user. @@ -89,6 +93,15 @@ public static void setUp() throws IOException, InterruptedException, CreateApiKe otpUser.email = String.format("test-%s@example.com", UUID.randomUUID().toString()); otpUser.hasConsentedToTerms = true; otpUser.storeTripHistory = true; + try { + // create Auth0 user for apiUser. + User auth0User = createAuth0UserForEmail(apiUser.email, TEMP_AUTH0_USER_PASSWORD); + // update api user with valid auth0 user ID (so the Auth0 delete works) + apiUser.auth0UserId = auth0User.getId(); + Persistence.apiUsers.replace(apiUser.id, apiUser); + } catch (Auth0Exception e) { + throw new RuntimeException(e); + } } /** @@ -109,6 +122,19 @@ public static void tearDown() { */ @Test public void canSimulateApiUserFlow() { + + // obtain bearer token + String endpoint = String.format("api/secure/application/%s/authenticate?username=%s&password=%s", + apiUser.id, + apiUser.email, + TEMP_AUTH0_USER_PASSWORD); + HttpResponse getTokenResponse = mockAuthenticatedPost(endpoint, + apiUser, + "" + ); + System.out.println(getTokenResponse.body()); + assertEquals(HttpStatus.OK_200, getTokenResponse.statusCode()); + // create otp user as api user. HttpResponse createUserResponse = mockAuthenticatedPost("api/secure/user", apiUser, @@ -137,7 +163,7 @@ public void canSimulateApiUserFlow() { MonitoredTrip monitoredTripResponse = JsonUtils.getPOJOFromJSON(createTripResponseAsApiUser.body(), MonitoredTrip.class); // Plan trip with OTP proxy. Mock plan response will be returned. This will work as an Otp user (created by MOD UI - // or an Api user) because the end point has not auth. + // or an Api user) because the end point has no auth. String otpQuery = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745&userId=" + otpUserResponse.id; HttpResponse planTripResponseAsOtUser = mockAuthenticatedRequest(otpQuery, otpUserResponse, @@ -147,7 +173,7 @@ public void canSimulateApiUserFlow() { assertEquals(HttpStatus.OK_200, planTripResponseAsOtUser.statusCode()); // Plan trip with OTP proxy. Mock plan response will be returned. This will work as an Api user because the end - // point has not auth. + // point has no auth. HttpResponse planTripResponseAsApiUser = mockAuthenticatedRequest(otpQuery, apiUser, HttpUtils.REQUEST_METHOD.GET @@ -155,7 +181,6 @@ public void canSimulateApiUserFlow() { LOG.info("Plan trip response: {}\n....", planTripResponseAsApiUser.body().substring(0, 300)); assertEquals(HttpStatus.OK_200, planTripResponseAsApiUser.statusCode()); - // Get trip for user as Otp user. This will fail because the user was created by an // Api user and therefore does not have a Auth0 account. HttpResponse tripRequestResponseAsOtUser = mockAuthenticatedRequest(String.format("api/secure/triprequests?userId=%s", @@ -165,8 +190,8 @@ public void canSimulateApiUserFlow() { ); assertEquals(HttpStatus.UNAUTHORIZED_401, tripRequestResponseAsOtUser.statusCode()); - // Get trip for user as an Api user. This should work because an Api user should be able to get a trip for an - // Otp user they created. + // Get trip for user as an Api user. This will work because an Api user should be able to get a trip on behalf + // of an Otp user they created. HttpResponse tripRequestResponseAsApiUser = mockAuthenticatedRequest(String.format("api/secure/triprequests?userId=%s", otpUserResponse.id), apiUser, @@ -176,7 +201,7 @@ public void canSimulateApiUserFlow() { List tripRequests = JsonUtils.getPOJOFromJSONAsList(tripRequestResponseAsApiUser.body(), TripRequest.class); - // Delete otp user as Otp user. This will fail because the user was created by an Api user and therefore does + // Delete Otp user as Otp user. This will fail because the user was created by an Api user and therefore does // not have a Auth0 account. HttpResponse deleteUserResponseAsOtpUser = mockAuthenticatedRequest( String.format("api/secure/user/%s", otpUserResponse.id), @@ -185,8 +210,7 @@ public void canSimulateApiUserFlow() { ); assertEquals(HttpStatus.UNAUTHORIZED_401, deleteUserResponseAsOtpUser.statusCode()); - // Delete otp user as Api user. This should work because an Api user should be able to delete an Otp user they - // created. + // Delete Otp user as Api user. This will work because an Api user can delete an Otp user they created. HttpResponse deleteUserResponseAsApiUser = mockAuthenticatedRequest( String.format("api/secure/user/%s", otpUserResponse.id), apiUser, @@ -194,7 +218,6 @@ public void canSimulateApiUserFlow() { ); assertEquals(HttpStatus.OK_200, deleteUserResponseAsApiUser.statusCode()); - // Verify user no longer exists. OtpUser deletedOtpUser = Persistence.otpUsers.getById(otpUserResponse.id); assertNull(deletedOtpUser); diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index 962f67ea8..0d330f56d 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -69,13 +69,6 @@ public static T getResourceFileContentsAsJSON(String resourcePathName, Class */ public static HttpResponse mockAuthenticatedRequest(String path, AbstractUser requestingUser, HttpUtils.REQUEST_METHOD requestMethod) { HashMap headers = getMockHeaders(requestingUser); - // If requester is an API user, add API key value as x-api-key header to simulate request over API Gateway. - if (requestingUser instanceof ApiUser) { - ApiUser apiUser = (ApiUser) requestingUser; - if (!apiUser.apiKeys.isEmpty()) { - headers.put("x-api-key", apiUser.apiKeys.get(0).value); - } - } return HttpUtils.httpRequestRawResponse( URI.create("http://localhost:4567/" + path), @@ -106,7 +99,6 @@ private static HashMap getMockHeaders(AbstractUser requestingUse if (!apiUser.apiKeys.isEmpty()) { headers.put("x-api-key", apiUser.apiKeys.get(0).value); } - return headers; } // If requester is an Otp user which was created by an Api user, return empty header because an Otp user created From c469e2c9250eb0d0238b88ac31b7fd3bcdc4d46f Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 9 Oct 2020 11:06:01 +0100 Subject: [PATCH 05/30] refactor(latest-spark-swagger-output.yaml): Updated spark swagger output to include new Api user aut --- .../latest-spark-swagger-output.yaml | 1398 ++++++++--------- 1 file changed, 641 insertions(+), 757 deletions(-) diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml index b69926e4a..7fffdd973 100644 --- a/src/main/resources/latest-spark-swagger-output.yaml +++ b/src/main/resources/latest-spark-swagger-output.yaml @@ -2,7 +2,7 @@ swagger: "2.0" info: description: "OpenTripPlanner Middleware API" - version: "" + version: "Local Build" title: "OTP Middleware" termsOfService: "" contact: @@ -12,502 +12,348 @@ info: license: name: "MIT License" url: "https://opensource.org/licenses/MIT" -host: "localhost:4567" -basePath: "/" +host: null +basePath: "/null" tags: -- name: "api/admin/user" - description: "Interface for querying and managing 'AdminUser' entities." -- name: "api/secure/application" - description: "Interface for querying and managing 'ApiUser' entities." -- name: "api/secure/monitoredtrip" - description: "Interface for querying and managing 'MonitoredTrip' entities." -- name: "api/secure/triprequests" - description: "Interface for retrieving trip requests." -- name: "api/secure/user" - description: "Interface for querying and managing 'OtpUser' entities." -- name: "api/secure/logs" - description: "Interface for retrieving API logs from AWS." -- name: "api/admin/bugsnag/eventsummary" - description: "Interface for reporting and retrieving application errors using Bugsnag." -- name: "otp" - description: "Proxy interface for OTP endpoints. Refer to OTP's\ + - name: "api/secure/monitoredtrip" + description: "Interface for querying and managing 'MonitoredTrip' entities." + - name: "api/secure/triprequests" + description: "Interface for retrieving trip requests." + - name: "api/secure/user" + description: "Interface for querying and managing 'OtpUser' entities." + - name: "otp" + description: "Proxy interface for OTP endpoints. Refer to OTP's\ \ API documentation for OTP's supported API resources." + - name: "pelias" + description: "Proxy interface for Pelias geocoder. Refer to Pelias\ + \ Geocoder Documentation for API resources supported by Pelias Geocoder." schemes: -- "https" + - "https" paths: - /api/admin/user/fromtoken: - get: - tags: - - "api/admin/user" - description: "Retrieves an AdminUser entity using an Auth0 access token passed\ - \ in an Authorization header." - produces: - - "application/json" - parameters: [] - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/AdminUser" - /api/admin/user/verification-email: - get: - tags: - - "api/admin/user" - description: "Triggers a job to resend the Auth0 verification email." - parameters: [] - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/Job" - /api/admin/user: - get: - tags: - - "api/admin/user" - description: "Gets a list of all 'AdminUser' entities." - produces: - - "application/json" - parameters: [] - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/AdminUser[]" - post: - tags: - - "api/admin/user" - description: "Creates a 'AdminUser' entity." - consumes: - - "application/json" - produces: - - "application/json" - parameters: - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/AdminUser" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/AdminUser" - /api/admin/user/{id}: - get: - tags: - - "api/admin/user" - description: "Returns the 'AdminUser' entity with the specified id, or 404 if\ - \ not found." - produces: - - "application/json" - parameters: - - name: "id" - in: "path" - description: "The id of the entity to search." - required: true - type: "string" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/AdminUser" - put: - tags: - - "api/admin/user" - description: "Updates and returns the 'AdminUser' entity with the specified\ - \ id, or 404 if not found." - consumes: - - "application/json" - produces: - - "application/json" - parameters: - - name: "id" - in: "path" - description: "The id of the entity to update." - required: true - type: "string" - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/AdminUser" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/AdminUser" - delete: - tags: - - "api/admin/user" - description: "Deletes the 'AdminUser' entity with the specified id if it exists." - produces: - - "application/json" - parameters: - - name: "id" - in: "path" - description: "The id of the entity to delete." - required: true - type: "string" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/AdminUser" - /api/secure/application/{id}/apikey: - post: - tags: - - "api/secure/application" - description: "Creates API key for ApiUser (with optional AWS API Gateway usage\ - \ plan ID)." - produces: - - "application/json" - parameters: - - name: "id" - in: "path" - description: "The user ID" - required: true - type: "string" - - name: "usagePlanId" - in: "query" - description: "Optional AWS API Gateway usage plan ID." - required: false - type: "string" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/ApiUser" - /api/secure/application/{id}/apikey/{apiKeyId}: - delete: - tags: - - "api/secure/application" - description: "Deletes API key for ApiUser." - produces: - - "application/json" - parameters: - - name: "id" - in: "path" - description: "The user ID." - required: true - type: "string" - - name: "apiKeyId" - in: "path" - description: "The ID of the API key." - required: true - type: "string" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/ApiUser" - /api/secure/application/fromtoken: - get: - tags: - - "api/secure/application" - description: "Retrieves an ApiUser entity using an Auth0 access token passed\ - \ in an Authorization header." - produces: - - "application/json" - parameters: [] - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/ApiUser" - /api/secure/application/verification-email: - get: - tags: - - "api/secure/application" - description: "Triggers a job to resend the Auth0 verification email." - parameters: [] - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/Job" - /api/secure/application: - get: - tags: - - "api/secure/application" - description: "Gets a list of all 'ApiUser' entities." - produces: - - "application/json" - parameters: [] - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/ApiUser[]" - post: - tags: - - "api/secure/application" - description: "Creates a 'ApiUser' entity." - consumes: - - "application/json" - produces: - - "application/json" - parameters: - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/ApiUser" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/ApiUser" - /api/secure/application/{id}: - get: - tags: - - "api/secure/application" - description: "Returns the 'ApiUser' entity with the specified id, or 404 if\ - \ not found." - produces: - - "application/json" - parameters: - - name: "id" - in: "path" - description: "The id of the entity to search." - required: true - type: "string" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/ApiUser" - put: - tags: - - "api/secure/application" - description: "Updates and returns the 'ApiUser' entity with the specified id,\ - \ or 404 if not found." - consumes: - - "application/json" - produces: - - "application/json" - parameters: - - name: "id" - in: "path" - description: "The id of the entity to update." - required: true - type: "string" - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/ApiUser" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/ApiUser" - delete: - tags: - - "api/secure/application" - description: "Deletes the 'ApiUser' entity with the specified id if it exists." - produces: - - "application/json" - parameters: - - name: "id" - in: "path" - description: "The id of the entity to delete." - required: true - type: "string" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/ApiUser" /api/secure/monitoredtrip: get: tags: - - "api/secure/monitoredtrip" + - "api/secure/monitoredtrip" description: "Gets a list of all 'MonitoredTrip' entities." produces: - - "application/json" + - "application/json" parameters: [] responses: "200": description: "successful operation" schema: - $ref: "#/definitions/MonitoredTrip[]" + type: "array" + items: + $ref: "#/definitions/MonitoredTrip" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] post: tags: - - "api/secure/monitoredtrip" + - "api/secure/monitoredtrip" description: "Creates a 'MonitoredTrip' entity." consumes: - - "application/json" + - "application/json" produces: - - "application/json" + - "application/json" parameters: - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/MonitoredTrip" + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/MonitoredTrip" responses: "200": description: "successful operation" schema: $ref: "#/definitions/MonitoredTrip" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] /api/secure/monitoredtrip/{id}: get: tags: - - "api/secure/monitoredtrip" + - "api/secure/monitoredtrip" description: "Returns the 'MonitoredTrip' entity with the specified id, or 404\ \ if not found." produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to search." - required: true - type: "string" + - name: "id" + in: "path" + description: "The id of the entity to search." + required: true + type: "string" responses: "200": description: "successful operation" schema: $ref: "#/definitions/MonitoredTrip" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] put: tags: - - "api/secure/monitoredtrip" + - "api/secure/monitoredtrip" description: "Updates and returns the 'MonitoredTrip' entity with the specified\ \ id, or 404 if not found." consumes: - - "application/json" + - "application/json" produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to update." - required: true - type: "string" - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/MonitoredTrip" + - name: "id" + in: "path" + description: "The id of the entity to update." + required: true + type: "string" + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/MonitoredTrip" responses: "200": description: "successful operation" schema: $ref: "#/definitions/MonitoredTrip" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] delete: tags: - - "api/secure/monitoredtrip" + - "api/secure/monitoredtrip" description: "Deletes the 'MonitoredTrip' entity with the specified id if it\ \ exists." produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to delete." - required: true - type: "string" + - name: "id" + in: "path" + description: "The id of the entity to delete." + required: true + type: "string" responses: "200": description: "successful operation" schema: $ref: "#/definitions/MonitoredTrip" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] /api/secure/triprequests: get: tags: - - "api/secure/triprequests" + - "api/secure/triprequests" description: "Gets a list of all trip requests for a user." produces: - - "application/json" + - "application/json" parameters: - - name: "userId" - in: "query" - description: "The OTP user for which to retrieve trip requests." - required: false - type: "string" - - name: "limit" - in: "query" - description: "If specified, the maximum number of trip requests to return,\ + - name: "userId" + in: "query" + description: "The OTP user for which to retrieve trip requests." + required: false + type: "string" + - name: "limit" + in: "query" + description: "If specified, the maximum number of trip requests to return,\ \ starting from the most recent." - required: false - type: "string" - default: "10" - - name: "fromDate" - in: "query" - description: "If specified, the earliest date (format yyyy-MM-dd) for which\ + required: false + type: "string" + default: "10" + - name: "fromDate" + in: "query" + description: "If specified, the earliest date (format yyyy-MM-dd) for which\ \ trip requests are retrieved." - required: false - type: "string" - default: "The current date" - pattern: "yyyy-MM-dd" - - name: "toDate" - in: "query" - description: "If specified, the latest date (format yyyy-MM-dd) for which\ + required: false + type: "string" + default: "The current date" + pattern: "yyyy-MM-dd" + - name: "toDate" + in: "query" + description: "If specified, the latest date (format yyyy-MM-dd) for which\ \ usage logs are retrieved." - required: false - type: "string" - default: "The current date" - pattern: "yyyy-MM-dd" + required: false + type: "string" + default: "The current date" + pattern: "yyyy-MM-dd" responses: "200": description: "successful operation" schema: $ref: "#/definitions/TripRequest" - /api/secure/user/{id}/verify_sms: + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] + /api/secure/user: get: tags: - - "api/secure/user" - description: "Request an SMS verification to be sent to an OtpUser's phone number." - parameters: - - name: "id" - in: "path" - description: "The id of the OtpUser." - required: true - type: "string" + - "api/secure/user" + description: "Gets a list of all 'OtpUser' entities." + produces: + - "application/json" + parameters: [] responses: "200": description: "successful operation" schema: - $ref: "#/definitions/VerificationResult" - /api/secure/user/{id}/verify_sms/{code}: + type: "array" + items: + $ref: "#/definitions/OtpUser" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] post: tags: - - "api/secure/user" - description: "Verify an OtpUser's phone number with a verification code." + - "api/secure/user" + description: "Creates a 'OtpUser' entity." + consumes: + - "application/json" + produces: + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the OtpUser." - required: true - type: "string" - - name: "code" - in: "path" - description: "The SMS verification code." - required: true - type: "string" + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/OtpUser" responses: "200": description: "successful operation" schema: - $ref: "#/definitions/VerificationResult" + $ref: "#/definitions/OtpUser" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] /api/secure/user/fromtoken: get: tags: - - "api/secure/user" + - "api/secure/user" description: "Retrieves an OtpUser entity using an Auth0 access token passed\ \ in an Authorization header." produces: - - "application/json" + - "application/json" parameters: [] responses: "200": description: "successful operation" schema: $ref: "#/definitions/OtpUser" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] /api/secure/user/verification-email: get: tags: - - "api/secure/user" + - "api/secure/user" description: "Triggers a job to resend the Auth0 verification email." parameters: [] responses: @@ -515,209 +361,274 @@ paths: description: "successful operation" schema: $ref: "#/definitions/Job" - /api/secure/user: - get: - tags: - - "api/secure/user" - description: "Gets a list of all 'OtpUser' entities." - produces: - - "application/json" - parameters: [] - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/OtpUser[]" - post: - tags: - - "api/secure/user" - description: "Creates a 'OtpUser' entity." - consumes: - - "application/json" - produces: - - "application/json" - parameters: - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/OtpUser" - responses: - "200": - description: "successful operation" - schema: - $ref: "#/definitions/OtpUser" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] /api/secure/user/{id}: get: tags: - - "api/secure/user" + - "api/secure/user" description: "Returns the 'OtpUser' entity with the specified id, or 404 if\ \ not found." produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to search." - required: true - type: "string" + - name: "id" + in: "path" + description: "The id of the entity to search." + required: true + type: "string" responses: "200": description: "successful operation" schema: $ref: "#/definitions/OtpUser" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] put: tags: - - "api/secure/user" + - "api/secure/user" description: "Updates and returns the 'OtpUser' entity with the specified id,\ \ or 404 if not found." consumes: - - "application/json" + - "application/json" produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to update." - required: true - type: "string" - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/OtpUser" + - name: "id" + in: "path" + description: "The id of the entity to update." + required: true + type: "string" + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/OtpUser" responses: "200": description: "successful operation" schema: $ref: "#/definitions/OtpUser" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] delete: tags: - - "api/secure/user" + - "api/secure/user" description: "Deletes the 'OtpUser' entity with the specified id if it exists." produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to delete." - required: true - type: "string" + - name: "id" + in: "path" + description: "The id of the entity to delete." + required: true + type: "string" responses: "200": description: "successful operation" schema: $ref: "#/definitions/OtpUser" - /api/secure/logs: + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] + /api/secure/user/{id}/verify_sms: get: tags: - - "api/secure/logs" - description: "Gets a list of all API usage logs." - produces: - - "application/json" + - "api/secure/user" + description: "Request an SMS verification to be sent to an OtpUser's phone number." parameters: - - name: "keyId" - in: "query" - description: "If specified, restricts the search to the specified AWS API\ - \ key ID." - required: false - type: "string" - - name: "startDate" - in: "query" - description: "If specified, the earliest date (format yyyy-MM-dd) for which\ - \ usage logs are retrieved." - required: false - type: "string" - default: "30 days prior to the current date" - pattern: "yyyy-MM-dd" - - name: "endDate" - in: "query" - description: "If specified, the latest date (format yyyy-MM-dd) for which\ - \ usage logs are retrieved." - required: false - type: "string" - default: "The current date" - pattern: "yyyy-MM-dd" + - name: "id" + in: "path" + description: "The id of the OtpUser." + required: true + type: "string" responses: "200": description: "successful operation" schema: - $ref: "#/definitions/ApiUsageResult" - /api/admin/bugsnag/eventsummary: - get: + $ref: "#/definitions/VerificationResult" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] + /api/secure/user/{id}/verify_sms/{code}: + post: tags: - - "api/admin/bugsnag/eventsummary" - description: "Gets a list of all Bugsnag event summaries." - produces: - - "application/json" - parameters: [] + - "api/secure/user" + description: "Verify an OtpUser's phone number with a verification code." + parameters: + - name: "id" + in: "path" + description: "The id of the OtpUser." + required: true + type: "string" + - name: "code" + in: "path" + description: "The SMS verification code." + required: true + type: "string" responses: "200": description: "successful operation" schema: - $ref: "#/definitions/BugsnagEvent" + $ref: "#/definitions/VerificationResult" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + security: + - api_key: [] + - bearer_token: [] /otp/*: get: tags: - - "otp" + - "otp" description: "Forwards any GET request to OTP. Refer to OTP's\ \ API documentation for OTP's supported API resources." produces: - - "application/json" - - "application/xml" + - "application/json" + - "application/xml" parameters: [] responses: "200": description: "successful operation" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" + /pelias/*: + get: + tags: + - "pelias" + description: "Forwards any GET request to Pelias Geocoder. Refer to Pelias\ + \ Geocoder Documentation for API resources supported by Pelias Geocoder." + produces: + - "application/json" + parameters: [] + security: + - api_key: [] + responses: + "200": + description: "successful operation" + "400": + $ref: "#/responses/400" + "401": + $ref: "#/responses/401" + "403": + $ref: "#/responses/403" + "404": + $ref: "#/responses/404" + "500": + $ref: "#/responses/500" + default: + $ref: "#/responses/default" definitions: - AdminUser: - type: "object" - Job: - type: "object" - properties: - status: - type: "string" - type: - type: "string" - createdAt: - type: "string" - format: "date" - id: - type: "string" - AdminUser[]: - type: "object" - ApiKey: + AbstractUser: type: "object" + required: + - "auth0UserId" + - "email" properties: - keyId: - type: "string" - name: + email: type: "string" - value: + description: "Email address for contact. This must be unique in the collection." + auth0UserId: type: "string" - ApiUser: + description: "Auth0 user name." + isDataToolsUser: + type: "boolean" + description: "Determines whether this user has access to OTP Data Tools." + description: "An abstract user." + Currency: type: "object" properties: - apiKeys: - type: "array" - items: - $ref: "#/definitions/ApiKey" - appName: - type: "string" - appPurpose: - type: "string" - appUrl: + symbol: type: "string" - company: + currency: type: "string" - hasConsentedToTerms: - type: "boolean" - name: + defaultFractionDigits: + type: "integer" + format: "int32" + currencyCode: type: "string" - ApiUser[]: - type: "object" - MonitoredTrip[]: - type: "object" EncodedPolyline: type: "object" properties: @@ -728,6 +639,32 @@ definitions: length: type: "integer" format: "int32" + Fare: + type: "object" + properties: + regular: + $ref: "#/definitions/Price" + student: + $ref: "#/definitions/Price" + senior: + $ref: "#/definitions/Price" + tram: + $ref: "#/definitions/Price" + special: + $ref: "#/definitions/Price" + youth: + $ref: "#/definitions/Price" + FareComponent: + type: "object" + properties: + fareId: + type: "string" + price: + $ref: "#/definitions/Price" + routes: + type: "array" + items: + type: "string" FareDetails: type: "object" properties: @@ -755,45 +692,66 @@ definitions: type: "array" items: $ref: "#/definitions/FareComponent" - Step: + FareWrapper: type: "object" properties: - distance: - type: "number" - format: "double" - relativeDirection: - type: "string" - streetName: + fare: + $ref: "#/definitions/Fare" + details: + $ref: "#/definitions/FareDetails" + Itinerary: + type: "object" + properties: + duration: + type: "integer" + format: "int64" + startTime: type: "string" - absoluteDirection: + format: "date" + endTime: type: "string" - stayOn: - type: "boolean" - area: - type: "boolean" - bogusName: + format: "date" + walkTime: + type: "integer" + format: "int64" + transitTime: + type: "integer" + format: "int64" + waitingTime: + type: "integer" + format: "int64" + walkDistance: + type: "number" + format: "double" + walkLimitExceeded: type: "boolean" - lon: + elevationLost: type: "number" format: "double" - lat: + elevationGained: type: "number" format: "double" - Fare: + transfers: + type: "integer" + format: "int32" + fare: + $ref: "#/definitions/FareWrapper" + legs: + type: "array" + items: + $ref: "#/definitions/Leg" + Job: type: "object" properties: - regular: - $ref: "#/definitions/Price" - student: - $ref: "#/definitions/Price" - senior: - $ref: "#/definitions/Price" - tram: - $ref: "#/definitions/Price" - special: - $ref: "#/definitions/Price" - youth: - $ref: "#/definitions/Price" + status: + type: "string" + type: + type: "string" + createdAt: + type: "string" + format: "date" + id: + type: "string" Leg: type: "object" properties: @@ -876,85 +834,87 @@ definitions: type: "array" items: $ref: "#/definitions/LocalizedAlert" - Price: - type: "object" - properties: - currency: - $ref: "#/definitions/Currency" - cents: - type: "integer" - format: "int32" - Currency: + LocalizedAlert: type: "object" properties: - symbol: + alertHeaderText: type: "string" - currency: + alertDescriptionText: type: "string" - defaultFractionDigits: - type: "integer" - format: "int32" - currencyCode: + alertUrl: type: "string" - Itinerary: + effectiveStartDate: + type: "string" + format: "date" + MonitoredTrip: type: "object" properties: - duration: - type: "integer" - format: "int64" - startTime: + userId: type: "string" - format: "date" - endTime: + tripName: type: "string" - format: "date" - walkTime: - type: "integer" - format: "int64" - transitTime: - type: "integer" - format: "int64" - waitingTime: + tripTime: + type: "string" + from: + $ref: "#/definitions/Place" + to: + $ref: "#/definitions/Place" + leadTimeInMinutes: type: "integer" - format: "int64" - walkDistance: - type: "number" - format: "double" - walkLimitExceeded: + format: "int32" + monday: type: "boolean" - elevationLost: - type: "number" - format: "double" - elevationGained: - type: "number" - format: "double" - transfers: + tuesday: + type: "boolean" + wednesday: + type: "boolean" + thursday: + type: "boolean" + friday: + type: "boolean" + saturday: + type: "boolean" + sunday: + type: "boolean" + excludeFederalHolidays: + type: "boolean" + isActive: + type: "boolean" + queryParams: + type: "string" + itinerary: + $ref: "#/definitions/Itinerary" + notifyOnAlert: + type: "boolean" + departureVarianceMinutesThreshold: type: "integer" format: "int32" - fare: - $ref: "#/definitions/FareWrapper" - legs: - type: "array" - items: - $ref: "#/definitions/Leg" - FareComponent: - type: "object" - properties: - fareId: - type: "string" - price: - $ref: "#/definitions/Price" - routes: - type: "array" - items: - type: "string" - FareWrapper: - type: "object" - properties: - fare: - $ref: "#/definitions/Fare" - details: - $ref: "#/definitions/FareDetails" + arrivalVarianceMinutesThreshold: + type: "integer" + format: "int32" + notifyOnItineraryChange: + type: "boolean" + OtpUser: + allOf: + - $ref: "#/definitions/AbstractUser" + - type: "object" + properties: + hasConsentedToTerms: + type: "boolean" + isPhoneNumberVerified: + type: "boolean" + notificationChannel: + type: "string" + phoneNumber: + type: "string" + savedLocations: + type: "array" + items: + $ref: "#/definitions/UserLocation" + storeTripHistory: + type: "boolean" + applicationId: + type: "string" Place: type: "object" properties: @@ -998,66 +958,38 @@ definitions: type: "string" address: type: "string" - LocalizedAlert: + Price: type: "object" properties: - alertHeaderText: - type: "string" - alertDescriptionText: - type: "string" - alertUrl: - type: "string" - effectiveStartDate: - type: "string" - format: "date" - MonitoredTrip: + currency: + $ref: "#/definitions/Currency" + cents: + type: "integer" + format: "int32" + Step: type: "object" properties: - userId: + distance: + type: "number" + format: "double" + relativeDirection: type: "string" - tripName: + streetName: type: "string" - tripTime: + absoluteDirection: type: "string" - from: - $ref: "#/definitions/Place" - to: - $ref: "#/definitions/Place" - leadTimeInMinutes: - type: "integer" - format: "int32" - monday: - type: "boolean" - tuesday: - type: "boolean" - wednesday: - type: "boolean" - thursday: - type: "boolean" - friday: - type: "boolean" - saturday: - type: "boolean" - sunday: - type: "boolean" - excludeFederalHolidays: - type: "boolean" - isActive: + stayOn: type: "boolean" - queryParams: - type: "string" - itinerary: - $ref: "#/definitions/Itinerary" - notifyOnAlert: + area: type: "boolean" - departureVarianceMinutesThreshold: - type: "integer" - format: "int32" - arrivalVarianceMinutesThreshold: - type: "integer" - format: "int32" - notifyOnItineraryChange: + bogusName: type: "boolean" + lon: + type: "number" + format: "double" + lat: + type: "number" + format: "double" TripRequest: type: "object" properties: @@ -1071,15 +1003,6 @@ definitions: type: "string" queryParams: type: "string" - VerificationResult: - type: "object" - properties: - sid: - type: "string" - status: - type: "string" - valid: - type: "boolean" UserLocation: type: "object" properties: @@ -1097,83 +1020,44 @@ definitions: type: "string" type: type: "string" - OtpUser: - type: "object" - properties: - hasConsentedToTerms: - type: "boolean" - isPhoneNumberVerified: - type: "boolean" - notificationChannel: - type: "string" - phoneNumber: - type: "string" - savedLocations: - type: "array" - items: - $ref: "#/definitions/UserLocation" - storeTripHistory: - type: "boolean" - applicationId: - type: "string" - OtpUser[]: - type: "object" - GetUsageResult: - type: "object" - properties: - usagePlanId: - type: "string" - startDate: - type: "string" - endDate: - type: "string" - position: - type: "string" - items: - $ref: "#/definitions/Map" - ApiUsageResult: - type: "object" - properties: - result: - $ref: "#/definitions/GetUsageResult" - apiUsers: - $ref: "#/definitions/Map" - App: - type: "object" - properties: - releaseStage: - type: "string" - BugsnagEvent: + VerificationResult: type: "object" properties: - eventDataId: - type: "string" - projectId: - type: "string" - errorId: - type: "string" - receivedAt: - type: "string" - format: "date" - exceptions: - type: "array" - items: - $ref: "#/definitions/EventException" - severity: + sid: type: "string" - context: + status: type: "string" - unhandled: + valid: type: "boolean" - app: - $ref: "#/definitions/App" - EventException: - type: "object" - properties: - errorClass: - type: "string" - message: - type: "string" externalDocs: description: "" url: "" +securityDefinitions: + api_key: + description: "API key header authentication." + in: "header" + name: "x-api-key" + type: "apiKey" + bearer_token: + description: "Bearer token authentication using Auth0." + in: "header" + name: "Authorization" + type: "apiKey" +responses: + "400": + description: "The request was not formed properly (e.g., some required parameters\ + \ may be missing). See the details of the returned response to determine the\ + \ exact issue." + "401": + description: "The server was not able to authenticate the request. This can happen\ + \ if authentication headers are missing or malformed, or the authentication\ + \ server cannot be reached." + "403": + description: "The requesting user is not allowed to perform the request." + "404": + description: "The requested item was not found." + "500": + description: "An error occurred while performing the request. Contact an API administrator\ + \ for more information." + default: + description: "An unexpected error occurred." From 0bf0e185be91456aba2863ab50b0ee81a23f0c4c Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 9 Oct 2020 15:26:18 +0100 Subject: [PATCH 06/30] refactor(latest-spark-swagger-output.yaml): Correctly updated swagger output --- .../latest-spark-swagger-output.yaml | 1434 +++++++++-------- 1 file changed, 789 insertions(+), 645 deletions(-) diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml index 7fffdd973..a712ca787 100644 --- a/src/main/resources/latest-spark-swagger-output.yaml +++ b/src/main/resources/latest-spark-swagger-output.yaml @@ -2,7 +2,7 @@ swagger: "2.0" info: description: "OpenTripPlanner Middleware API" - version: "Local Build" + version: "" title: "OTP Middleware" termsOfService: "" contact: @@ -12,348 +12,530 @@ info: license: name: "MIT License" url: "https://opensource.org/licenses/MIT" -host: null -basePath: "/null" +host: "localhost:4567" +basePath: "/" tags: - - name: "api/secure/monitoredtrip" - description: "Interface for querying and managing 'MonitoredTrip' entities." - - name: "api/secure/triprequests" - description: "Interface for retrieving trip requests." - - name: "api/secure/user" - description: "Interface for querying and managing 'OtpUser' entities." - - name: "otp" - description: "Proxy interface for OTP endpoints. Refer to OTP's\ +- name: "api/admin/user" + description: "Interface for querying and managing 'AdminUser' entities." +- name: "api/secure/application" + description: "Interface for querying and managing 'ApiUser' entities." +- name: "api/secure/monitoredtrip" + description: "Interface for querying and managing 'MonitoredTrip' entities." +- name: "api/secure/triprequests" + description: "Interface for retrieving trip requests." +- name: "api/secure/user" + description: "Interface for querying and managing 'OtpUser' entities." +- name: "api/secure/logs" + description: "Interface for retrieving API logs from AWS." +- name: "api/admin/bugsnag/eventsummary" + description: "Interface for reporting and retrieving application errors using Bugsnag." +- name: "otp" + description: "Proxy interface for OTP endpoints. Refer to OTP's\ \ API documentation for OTP's supported API resources." - - name: "pelias" - description: "Proxy interface for Pelias geocoder. Refer to Pelias\ - \ Geocoder Documentation for API resources supported by Pelias Geocoder." schemes: - - "https" +- "https" paths: + /api/admin/user/fromtoken: + get: + tags: + - "api/admin/user" + description: "Retrieves an AdminUser entity using an Auth0 access token passed\ + \ in an Authorization header." + produces: + - "application/json" + parameters: [] + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/AdminUser" + /api/admin/user/verification-email: + get: + tags: + - "api/admin/user" + description: "Triggers a job to resend the Auth0 verification email." + parameters: [] + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/Job" + /api/admin/user: + get: + tags: + - "api/admin/user" + description: "Gets a list of all 'AdminUser' entities." + produces: + - "application/json" + parameters: [] + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/AdminUser[]" + post: + tags: + - "api/admin/user" + description: "Creates a 'AdminUser' entity." + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/AdminUser" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/AdminUser" + /api/admin/user/{id}: + get: + tags: + - "api/admin/user" + description: "Returns the 'AdminUser' entity with the specified id, or 404 if\ + \ not found." + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "The id of the entity to search." + required: true + type: "string" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/AdminUser" + put: + tags: + - "api/admin/user" + description: "Updates and returns the 'AdminUser' entity with the specified\ + \ id, or 404 if not found." + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "The id of the entity to update." + required: true + type: "string" + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/AdminUser" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/AdminUser" + delete: + tags: + - "api/admin/user" + description: "Deletes the 'AdminUser' entity with the specified id if it exists." + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "The id of the entity to delete." + required: true + type: "string" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/AdminUser" + /api/secure/application/{id}/apikey: + post: + tags: + - "api/secure/application" + description: "Creates API key for ApiUser (with optional AWS API Gateway usage\ + \ plan ID)." + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "The user ID" + required: true + type: "string" + - name: "usagePlanId" + in: "query" + description: "Optional AWS API Gateway usage plan ID." + required: false + type: "string" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/ApiUser" + /api/secure/application/{id}/apikey/{apiKeyId}: + delete: + tags: + - "api/secure/application" + description: "Deletes API key for ApiUser." + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "The user ID." + required: true + type: "string" + - name: "apiKeyId" + in: "path" + description: "The ID of the API key." + required: true + type: "string" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/ApiUser" + /api/secure/application/{id}/authenticate: + post: + tags: + - "api/secure/application" + description: "Authenticates ApiUser with Auth0." + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "The user ID." + required: true + type: "string" + - name: "username" + in: "query" + description: "Auth0 username (usually email address)." + required: false + type: "string" + - name: "password" + in: "query" + description: "Auth0 password." + required: false + type: "string" + responses: + "200": + description: "successful operation" + schema: + type: "string" + /api/secure/application/fromtoken: + get: + tags: + - "api/secure/application" + description: "Retrieves an ApiUser entity using an Auth0 access token passed\ + \ in an Authorization header." + produces: + - "application/json" + parameters: [] + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/ApiUser" + /api/secure/application/verification-email: + get: + tags: + - "api/secure/application" + description: "Triggers a job to resend the Auth0 verification email." + parameters: [] + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/Job" + /api/secure/application: + get: + tags: + - "api/secure/application" + description: "Gets a list of all 'ApiUser' entities." + produces: + - "application/json" + parameters: [] + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/ApiUser[]" + post: + tags: + - "api/secure/application" + description: "Creates a 'ApiUser' entity." + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/ApiUser" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/ApiUser" + /api/secure/application/{id}: + get: + tags: + - "api/secure/application" + description: "Returns the 'ApiUser' entity with the specified id, or 404 if\ + \ not found." + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "The id of the entity to search." + required: true + type: "string" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/ApiUser" + put: + tags: + - "api/secure/application" + description: "Updates and returns the 'ApiUser' entity with the specified id,\ + \ or 404 if not found." + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "The id of the entity to update." + required: true + type: "string" + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/ApiUser" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/ApiUser" + delete: + tags: + - "api/secure/application" + description: "Deletes the 'ApiUser' entity with the specified id if it exists." + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "The id of the entity to delete." + required: true + type: "string" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/ApiUser" /api/secure/monitoredtrip: get: tags: - - "api/secure/monitoredtrip" + - "api/secure/monitoredtrip" description: "Gets a list of all 'MonitoredTrip' entities." produces: - - "application/json" + - "application/json" parameters: [] responses: "200": description: "successful operation" schema: - type: "array" - items: - $ref: "#/definitions/MonitoredTrip" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] + $ref: "#/definitions/MonitoredTrip[]" post: tags: - - "api/secure/monitoredtrip" + - "api/secure/monitoredtrip" description: "Creates a 'MonitoredTrip' entity." consumes: - - "application/json" + - "application/json" produces: - - "application/json" + - "application/json" parameters: - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/MonitoredTrip" + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/MonitoredTrip" responses: "200": description: "successful operation" schema: $ref: "#/definitions/MonitoredTrip" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] /api/secure/monitoredtrip/{id}: get: tags: - - "api/secure/monitoredtrip" + - "api/secure/monitoredtrip" description: "Returns the 'MonitoredTrip' entity with the specified id, or 404\ \ if not found." produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to search." - required: true - type: "string" + - name: "id" + in: "path" + description: "The id of the entity to search." + required: true + type: "string" responses: "200": description: "successful operation" schema: $ref: "#/definitions/MonitoredTrip" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] put: tags: - - "api/secure/monitoredtrip" + - "api/secure/monitoredtrip" description: "Updates and returns the 'MonitoredTrip' entity with the specified\ \ id, or 404 if not found." consumes: - - "application/json" + - "application/json" produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to update." - required: true - type: "string" - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/MonitoredTrip" + - name: "id" + in: "path" + description: "The id of the entity to update." + required: true + type: "string" + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/MonitoredTrip" responses: "200": description: "successful operation" schema: $ref: "#/definitions/MonitoredTrip" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] delete: tags: - - "api/secure/monitoredtrip" + - "api/secure/monitoredtrip" description: "Deletes the 'MonitoredTrip' entity with the specified id if it\ \ exists." produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to delete." - required: true - type: "string" + - name: "id" + in: "path" + description: "The id of the entity to delete." + required: true + type: "string" responses: "200": description: "successful operation" schema: $ref: "#/definitions/MonitoredTrip" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] /api/secure/triprequests: get: tags: - - "api/secure/triprequests" + - "api/secure/triprequests" description: "Gets a list of all trip requests for a user." produces: - - "application/json" + - "application/json" parameters: - - name: "userId" - in: "query" - description: "The OTP user for which to retrieve trip requests." - required: false - type: "string" - - name: "limit" - in: "query" - description: "If specified, the maximum number of trip requests to return,\ + - name: "userId" + in: "query" + description: "The OTP user for which to retrieve trip requests." + required: false + type: "string" + - name: "limit" + in: "query" + description: "If specified, the maximum number of trip requests to return,\ \ starting from the most recent." - required: false - type: "string" - default: "10" - - name: "fromDate" - in: "query" - description: "If specified, the earliest date (format yyyy-MM-dd) for which\ + required: false + type: "string" + default: "10" + - name: "fromDate" + in: "query" + description: "If specified, the earliest date (format yyyy-MM-dd) for which\ \ trip requests are retrieved." - required: false - type: "string" - default: "The current date" - pattern: "yyyy-MM-dd" - - name: "toDate" - in: "query" - description: "If specified, the latest date (format yyyy-MM-dd) for which\ + required: false + type: "string" + default: "The current date" + pattern: "yyyy-MM-dd" + - name: "toDate" + in: "query" + description: "If specified, the latest date (format yyyy-MM-dd) for which\ \ usage logs are retrieved." - required: false - type: "string" - default: "The current date" - pattern: "yyyy-MM-dd" + required: false + type: "string" + default: "The current date" + pattern: "yyyy-MM-dd" responses: "200": description: "successful operation" schema: $ref: "#/definitions/TripRequest" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] - /api/secure/user: + /api/secure/user/{id}/verify_sms: get: tags: - - "api/secure/user" - description: "Gets a list of all 'OtpUser' entities." - produces: - - "application/json" - parameters: [] + - "api/secure/user" + description: "Request an SMS verification to be sent to an OtpUser's phone number." + parameters: + - name: "id" + in: "path" + description: "The id of the OtpUser." + required: true + type: "string" responses: "200": description: "successful operation" schema: - type: "array" - items: - $ref: "#/definitions/OtpUser" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] + $ref: "#/definitions/VerificationResult" + /api/secure/user/{id}/verify_sms/{code}: post: tags: - - "api/secure/user" - description: "Creates a 'OtpUser' entity." - consumes: - - "application/json" - produces: - - "application/json" + - "api/secure/user" + description: "Verify an OtpUser's phone number with a verification code." parameters: - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/OtpUser" + - name: "id" + in: "path" + description: "The id of the OtpUser." + required: true + type: "string" + - name: "code" + in: "path" + description: "The SMS verification code." + required: true + type: "string" responses: "200": description: "successful operation" schema: - $ref: "#/definitions/OtpUser" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] + $ref: "#/definitions/VerificationResult" /api/secure/user/fromtoken: get: tags: - - "api/secure/user" + - "api/secure/user" description: "Retrieves an OtpUser entity using an Auth0 access token passed\ \ in an Authorization header." produces: - - "application/json" + - "application/json" parameters: [] responses: "200": description: "successful operation" schema: $ref: "#/definitions/OtpUser" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] /api/secure/user/verification-email: get: tags: - - "api/secure/user" + - "api/secure/user" description: "Triggers a job to resend the Auth0 verification email." parameters: [] responses: @@ -361,274 +543,209 @@ paths: description: "successful operation" schema: $ref: "#/definitions/Job" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] + /api/secure/user: + get: + tags: + - "api/secure/user" + description: "Gets a list of all 'OtpUser' entities." + produces: + - "application/json" + parameters: [] + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/OtpUser[]" + post: + tags: + - "api/secure/user" + description: "Creates a 'OtpUser' entity." + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/OtpUser" + responses: + "200": + description: "successful operation" + schema: + $ref: "#/definitions/OtpUser" /api/secure/user/{id}: get: tags: - - "api/secure/user" + - "api/secure/user" description: "Returns the 'OtpUser' entity with the specified id, or 404 if\ \ not found." produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to search." - required: true - type: "string" + - name: "id" + in: "path" + description: "The id of the entity to search." + required: true + type: "string" responses: "200": description: "successful operation" schema: $ref: "#/definitions/OtpUser" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] put: tags: - - "api/secure/user" + - "api/secure/user" description: "Updates and returns the 'OtpUser' entity with the specified id,\ \ or 404 if not found." consumes: - - "application/json" + - "application/json" produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to update." - required: true - type: "string" - - in: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/OtpUser" + - name: "id" + in: "path" + description: "The id of the entity to update." + required: true + type: "string" + - in: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/OtpUser" responses: "200": description: "successful operation" schema: $ref: "#/definitions/OtpUser" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] delete: tags: - - "api/secure/user" + - "api/secure/user" description: "Deletes the 'OtpUser' entity with the specified id if it exists." produces: - - "application/json" + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the entity to delete." - required: true - type: "string" + - name: "id" + in: "path" + description: "The id of the entity to delete." + required: true + type: "string" responses: "200": description: "successful operation" schema: $ref: "#/definitions/OtpUser" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] - /api/secure/user/{id}/verify_sms: + /api/secure/logs: get: tags: - - "api/secure/user" - description: "Request an SMS verification to be sent to an OtpUser's phone number." + - "api/secure/logs" + description: "Gets a list of all API usage logs." + produces: + - "application/json" parameters: - - name: "id" - in: "path" - description: "The id of the OtpUser." - required: true - type: "string" + - name: "keyId" + in: "query" + description: "If specified, restricts the search to the specified AWS API\ + \ key ID." + required: false + type: "string" + - name: "startDate" + in: "query" + description: "If specified, the earliest date (format yyyy-MM-dd) for which\ + \ usage logs are retrieved." + required: false + type: "string" + default: "30 days prior to the current date" + pattern: "yyyy-MM-dd" + - name: "endDate" + in: "query" + description: "If specified, the latest date (format yyyy-MM-dd) for which\ + \ usage logs are retrieved." + required: false + type: "string" + default: "The current date" + pattern: "yyyy-MM-dd" responses: "200": description: "successful operation" schema: - $ref: "#/definitions/VerificationResult" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] - /api/secure/user/{id}/verify_sms/{code}: - post: + $ref: "#/definitions/ApiUsageResult" + /api/admin/bugsnag/eventsummary: + get: tags: - - "api/secure/user" - description: "Verify an OtpUser's phone number with a verification code." - parameters: - - name: "id" - in: "path" - description: "The id of the OtpUser." - required: true - type: "string" - - name: "code" - in: "path" - description: "The SMS verification code." - required: true - type: "string" + - "api/admin/bugsnag/eventsummary" + description: "Gets a list of all Bugsnag event summaries." + produces: + - "application/json" + parameters: [] responses: "200": description: "successful operation" schema: - $ref: "#/definitions/VerificationResult" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - security: - - api_key: [] - - bearer_token: [] + $ref: "#/definitions/BugsnagEvent" /otp/*: get: tags: - - "otp" + - "otp" description: "Forwards any GET request to OTP. Refer to OTP's\ \ API documentation for OTP's supported API resources." produces: - - "application/json" - - "application/xml" - parameters: [] - responses: - "200": - description: "successful operation" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" - /pelias/*: - get: - tags: - - "pelias" - description: "Forwards any GET request to Pelias Geocoder. Refer to Pelias\ - \ Geocoder Documentation for API resources supported by Pelias Geocoder." - produces: - - "application/json" + - "application/json" + - "application/xml" parameters: [] - security: - - api_key: [] responses: "200": description: "successful operation" - "400": - $ref: "#/responses/400" - "401": - $ref: "#/responses/401" - "403": - $ref: "#/responses/403" - "404": - $ref: "#/responses/404" - "500": - $ref: "#/responses/500" - default: - $ref: "#/responses/default" definitions: - AbstractUser: + AdminUser: + type: "object" + Job: type: "object" - required: - - "auth0UserId" - - "email" properties: - email: + status: type: "string" - description: "Email address for contact. This must be unique in the collection." - auth0UserId: + type: type: "string" - description: "Auth0 user name." - isDataToolsUser: - type: "boolean" - description: "Determines whether this user has access to OTP Data Tools." - description: "An abstract user." - Currency: + createdAt: + type: "string" + format: "date" + id: + type: "string" + AdminUser[]: + type: "object" + ApiKey: type: "object" properties: - symbol: + keyId: type: "string" - currency: + name: type: "string" - defaultFractionDigits: - type: "integer" - format: "int32" - currencyCode: + value: + type: "string" + ApiUser: + type: "object" + properties: + apiKeys: + type: "array" + items: + $ref: "#/definitions/ApiKey" + appName: type: "string" + appPurpose: + type: "string" + appUrl: + type: "string" + company: + type: "string" + hasConsentedToTerms: + type: "boolean" + name: + type: "string" + ApiUser[]: + type: "object" + MonitoredTrip[]: + type: "object" EncodedPolyline: type: "object" properties: @@ -639,32 +756,6 @@ definitions: length: type: "integer" format: "int32" - Fare: - type: "object" - properties: - regular: - $ref: "#/definitions/Price" - student: - $ref: "#/definitions/Price" - senior: - $ref: "#/definitions/Price" - tram: - $ref: "#/definitions/Price" - special: - $ref: "#/definitions/Price" - youth: - $ref: "#/definitions/Price" - FareComponent: - type: "object" - properties: - fareId: - type: "string" - price: - $ref: "#/definitions/Price" - routes: - type: "array" - items: - type: "string" FareDetails: type: "object" properties: @@ -692,66 +783,45 @@ definitions: type: "array" items: $ref: "#/definitions/FareComponent" - FareWrapper: - type: "object" - properties: - fare: - $ref: "#/definitions/Fare" - details: - $ref: "#/definitions/FareDetails" - Itinerary: + Step: type: "object" properties: - duration: - type: "integer" - format: "int64" - startTime: - type: "string" - format: "date" - endTime: - type: "string" - format: "date" - walkTime: - type: "integer" - format: "int64" - transitTime: - type: "integer" - format: "int64" - waitingTime: - type: "integer" - format: "int64" - walkDistance: - type: "number" - format: "double" - walkLimitExceeded: - type: "boolean" - elevationLost: - type: "number" - format: "double" - elevationGained: + distance: type: "number" format: "double" - transfers: - type: "integer" - format: "int32" - fare: - $ref: "#/definitions/FareWrapper" - legs: - type: "array" - items: - $ref: "#/definitions/Leg" - Job: - type: "object" - properties: - status: - type: "string" - type: + relativeDirection: type: "string" - createdAt: + streetName: type: "string" - format: "date" - id: + absoluteDirection: type: "string" + stayOn: + type: "boolean" + area: + type: "boolean" + bogusName: + type: "boolean" + lon: + type: "number" + format: "double" + lat: + type: "number" + format: "double" + Fare: + type: "object" + properties: + regular: + $ref: "#/definitions/Price" + student: + $ref: "#/definitions/Price" + senior: + $ref: "#/definitions/Price" + tram: + $ref: "#/definitions/Price" + special: + $ref: "#/definitions/Price" + youth: + $ref: "#/definitions/Price" Leg: type: "object" properties: @@ -834,87 +904,85 @@ definitions: type: "array" items: $ref: "#/definitions/LocalizedAlert" - LocalizedAlert: + Price: type: "object" properties: - alertHeaderText: - type: "string" - alertDescriptionText: + currency: + $ref: "#/definitions/Currency" + cents: + type: "integer" + format: "int32" + Currency: + type: "object" + properties: + symbol: type: "string" - alertUrl: + currency: type: "string" - effectiveStartDate: + defaultFractionDigits: + type: "integer" + format: "int32" + currencyCode: type: "string" - format: "date" - MonitoredTrip: + Itinerary: type: "object" properties: - userId: - type: "string" - tripName: + duration: + type: "integer" + format: "int64" + startTime: type: "string" - tripTime: + format: "date" + endTime: type: "string" - from: - $ref: "#/definitions/Place" - to: - $ref: "#/definitions/Place" - leadTimeInMinutes: + format: "date" + walkTime: type: "integer" - format: "int32" - monday: - type: "boolean" - tuesday: - type: "boolean" - wednesday: - type: "boolean" - thursday: - type: "boolean" - friday: - type: "boolean" - saturday: - type: "boolean" - sunday: - type: "boolean" - excludeFederalHolidays: - type: "boolean" - isActive: - type: "boolean" - queryParams: - type: "string" - itinerary: - $ref: "#/definitions/Itinerary" - notifyOnAlert: - type: "boolean" - departureVarianceMinutesThreshold: + format: "int64" + transitTime: type: "integer" - format: "int32" - arrivalVarianceMinutesThreshold: + format: "int64" + waitingTime: type: "integer" - format: "int32" - notifyOnItineraryChange: + format: "int64" + walkDistance: + type: "number" + format: "double" + walkLimitExceeded: type: "boolean" - OtpUser: - allOf: - - $ref: "#/definitions/AbstractUser" - - type: "object" - properties: - hasConsentedToTerms: - type: "boolean" - isPhoneNumberVerified: - type: "boolean" - notificationChannel: - type: "string" - phoneNumber: - type: "string" - savedLocations: - type: "array" - items: - $ref: "#/definitions/UserLocation" - storeTripHistory: - type: "boolean" - applicationId: - type: "string" + elevationLost: + type: "number" + format: "double" + elevationGained: + type: "number" + format: "double" + transfers: + type: "integer" + format: "int32" + fare: + $ref: "#/definitions/FareWrapper" + legs: + type: "array" + items: + $ref: "#/definitions/Leg" + FareComponent: + type: "object" + properties: + fareId: + type: "string" + price: + $ref: "#/definitions/Price" + routes: + type: "array" + items: + type: "string" + FareWrapper: + type: "object" + properties: + fare: + $ref: "#/definitions/Fare" + details: + $ref: "#/definitions/FareDetails" Place: type: "object" properties: @@ -958,38 +1026,66 @@ definitions: type: "string" address: type: "string" - Price: + LocalizedAlert: type: "object" properties: - currency: - $ref: "#/definitions/Currency" - cents: - type: "integer" - format: "int32" - Step: + alertHeaderText: + type: "string" + alertDescriptionText: + type: "string" + alertUrl: + type: "string" + effectiveStartDate: + type: "string" + format: "date" + MonitoredTrip: type: "object" properties: - distance: - type: "number" - format: "double" - relativeDirection: + userId: type: "string" - streetName: + tripName: type: "string" - absoluteDirection: + tripTime: type: "string" - stayOn: + from: + $ref: "#/definitions/Place" + to: + $ref: "#/definitions/Place" + leadTimeInMinutes: + type: "integer" + format: "int32" + monday: type: "boolean" - area: + tuesday: type: "boolean" - bogusName: + wednesday: + type: "boolean" + thursday: + type: "boolean" + friday: + type: "boolean" + saturday: + type: "boolean" + sunday: + type: "boolean" + excludeFederalHolidays: + type: "boolean" + isActive: + type: "boolean" + queryParams: + type: "string" + itinerary: + $ref: "#/definitions/Itinerary" + notifyOnAlert: + type: "boolean" + departureVarianceMinutesThreshold: + type: "integer" + format: "int32" + arrivalVarianceMinutesThreshold: + type: "integer" + format: "int32" + notifyOnItineraryChange: type: "boolean" - lon: - type: "number" - format: "double" - lat: - type: "number" - format: "double" TripRequest: type: "object" properties: @@ -1003,6 +1099,15 @@ definitions: type: "string" queryParams: type: "string" + VerificationResult: + type: "object" + properties: + sid: + type: "string" + status: + type: "string" + valid: + type: "boolean" UserLocation: type: "object" properties: @@ -1020,44 +1125,83 @@ definitions: type: "string" type: type: "string" - VerificationResult: + OtpUser: type: "object" properties: - sid: + hasConsentedToTerms: + type: "boolean" + isPhoneNumberVerified: + type: "boolean" + notificationChannel: type: "string" - status: + phoneNumber: type: "string" - valid: + savedLocations: + type: "array" + items: + $ref: "#/definitions/UserLocation" + storeTripHistory: + type: "boolean" + applicationId: + type: "string" + OtpUser[]: + type: "object" + GetUsageResult: + type: "object" + properties: + usagePlanId: + type: "string" + startDate: + type: "string" + endDate: + type: "string" + position: + type: "string" + items: + $ref: "#/definitions/Map" + ApiUsageResult: + type: "object" + properties: + result: + $ref: "#/definitions/GetUsageResult" + apiUsers: + $ref: "#/definitions/Map" + App: + type: "object" + properties: + releaseStage: + type: "string" + BugsnagEvent: + type: "object" + properties: + eventDataId: + type: "string" + projectId: + type: "string" + errorId: + type: "string" + receivedAt: + type: "string" + format: "date" + exceptions: + type: "array" + items: + $ref: "#/definitions/EventException" + severity: + type: "string" + context: + type: "string" + unhandled: type: "boolean" + app: + $ref: "#/definitions/App" + EventException: + type: "object" + properties: + errorClass: + type: "string" + message: + type: "string" externalDocs: description: "" url: "" -securityDefinitions: - api_key: - description: "API key header authentication." - in: "header" - name: "x-api-key" - type: "apiKey" - bearer_token: - description: "Bearer token authentication using Auth0." - in: "header" - name: "Authorization" - type: "apiKey" -responses: - "400": - description: "The request was not formed properly (e.g., some required parameters\ - \ may be missing). See the details of the returned response to determine the\ - \ exact issue." - "401": - description: "The server was not able to authenticate the request. This can happen\ - \ if authentication headers are missing or malformed, or the authentication\ - \ server cannot be reached." - "403": - description: "The requesting user is not allowed to perform the request." - "404": - description: "The requested item was not found." - "500": - description: "An error occurred while performing the request. Contact an API administrator\ - \ for more information." - default: - description: "An unexpected error occurred." From ca3e5ce56ad2432721cd3539e901457f260315f9 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Tue, 13 Oct 2020 16:12:22 +0100 Subject: [PATCH 07/30] refactor(Addressed PR feedback): Addressed PR feedback --- .../middleware/auth/Auth0Connection.java | 40 ++++++-------- .../middleware/auth/Auth0Users.java | 47 +++++++++------- .../middleware/auth/RequestingUser.java | 7 +++ .../api/AbstractUserController.java | 17 +++--- .../controllers/api/ApiController.java | 55 ++++++++++--------- .../controllers/api/ApiUserController.java | 44 +++++++-------- .../controllers/api/OtpRequestProcessor.java | 15 ++--- .../controllers/api/OtpUserController.java | 29 +++++----- .../middleware/models/MonitoredTrip.java | 2 +- .../middleware/models/OtpUser.java | 2 +- .../middleware/ApiUserFlowTest.java | 47 ++++++++-------- .../opentripplanner/middleware/TestUtils.java | 14 +---- .../TripHistoryPersistenceTest.java | 4 +- 13 files changed, 158 insertions(+), 165 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index c1f0cea7d..7c5c9be7e 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -15,6 +15,8 @@ import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.persistence.Persistence; +import org.opentripplanner.middleware.utils.ConfigUtils; +import org.opentripplanner.middleware.utils.JsonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.HaltException; @@ -25,10 +27,6 @@ import static org.opentripplanner.middleware.controllers.api.ApiUserController.API_USER_PATH; import static org.opentripplanner.middleware.controllers.api.OtpUserController.OTP_USER_PATH; -import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; -import static org.opentripplanner.middleware.utils.ConfigUtils.hasConfigProperty; -import static org.opentripplanner.middleware.utils.JsonUtils.getPOJOFromRequestBody; -import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** * This handles verifying the Auth0 token passed in the auth header (e.g., Authorization: Bearer MY_TOKEN of Spark HTTP @@ -76,7 +74,7 @@ public static void checkUser(Request req) { LOG.info("New user is creating self. OK to proceed without existing user object for auth0UserId"); } else { // Otherwise, if no valid user is found, halt the request. - logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "Auth0 auth - Unknown user."); + JsonUtils.logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "Auth0 auth - Unknown user."); } } // The user attribute is used on the server side to check user permissions and does not have all of the @@ -84,12 +82,12 @@ public static void checkUser(Request req) { addUserToRequest(req, profile); } catch (JWTVerificationException e) { // Invalid signature/claims - logMessageAndHalt(req, 401, "Login failed to verify with our authorization provider.", e); + JsonUtils.logMessageAndHalt(req, 401, "Login failed to verify with our authorization provider.", e); } catch (HaltException e) { throw e; } catch (Exception e) { LOG.warn("Login failed to verify with our authorization provider.", e); - logMessageAndHalt(req, 401, "Could not verify user's token"); + JsonUtils.logMessageAndHalt(req, 401, "Could not verify user's token"); } } @@ -111,7 +109,7 @@ private static boolean isCreatingSelf(Request req, RequestingUser profile) { try { // Next, get the user object from the request body, verifying that the Auth0UserId matches between // requester and the new user object. - AbstractUser user = getPOJOFromRequestBody(req, userClass); + AbstractUser user = JsonUtils.getPOJOFromRequestBody(req, userClass); return profile.auth0UserId.equals(user.auth0UserId); } catch (JsonProcessingException e) { LOG.warn("Could not parse user object from request.", e); @@ -126,11 +124,6 @@ public static boolean isAuthHeaderPresent(Request req) { return authHeader != null; } - public static boolean isApiKeyHeaderPresent(Request req) { - final String apiKey = req.headers("x-api-key"); - return apiKey != null; - } - /** * Assign user to request and check that the user is an admin. */ @@ -140,7 +133,7 @@ public static void checkUserIsAdmin(Request req, Response res) { // Check that user object is present and is admin. RequestingUser user = Auth0Connection.getUserFromRequest(req); if (!isUserAdmin(user)) { - logMessageAndHalt( + JsonUtils.logMessageAndHalt( req, HttpStatus.UNAUTHORIZED_401, "User is not authorized to perform administrative action" @@ -174,19 +167,19 @@ public static RequestingUser getUserFromRequest(Request req) { */ private static String getTokenFromRequest(Request req) { if (!isAuthHeaderPresent(req)) { - logMessageAndHalt(req, 401, "Authorization header is missing."); + JsonUtils.logMessageAndHalt(req, 401, "Authorization header is missing."); } // Check that auth header is present and formatted correctly (Authorization: Bearer [token]). final String authHeader = req.headers("Authorization"); String[] parts = authHeader.split(" "); if (parts.length != 2 || !"bearer".equals(parts[0].toLowerCase())) { - logMessageAndHalt(req, 401, String.format("Authorization header is malformed: %s", authHeader)); + JsonUtils.logMessageAndHalt(req, 401, String.format("Authorization header is malformed: %s", authHeader)); } // Retrieve token from auth header. String token = parts[1]; if (token == null) { - logMessageAndHalt(req, 401, "Could not find authorization token"); + JsonUtils.logMessageAndHalt(req, 401, "Could not find authorization token"); } return token; } @@ -198,7 +191,7 @@ private static String getTokenFromRequest(Request req) { private static JWTVerifier getVerifier(Request req, String token) { if (verifier == null) { try { - final String domain = "https://" + getConfigPropertyAsText("AUTH0_DOMAIN") + "/"; + final String domain = "https://" + ConfigUtils.getConfigPropertyAsText("AUTH0_DOMAIN") + "/"; JwkProvider provider = new UrlJwkProvider(domain); // Decode the token. DecodedJWT jwt = JWT.decode(token); @@ -215,14 +208,15 @@ private static JWTVerifier getVerifier(Request req, String token) { .build(); } catch (IllegalStateException | NullPointerException | JwkException e) { LOG.error("Auth0 verifier configured incorrectly."); - logMessageAndHalt(req, 500, "Server authentication configured incorrectly.", e); + JsonUtils.logMessageAndHalt(req, 500, "Server authentication configured incorrectly.", e); } } return verifier; } public static boolean getDefaultAuthDisabled() { - return hasConfigProperty("DISABLE_AUTH") && "true".equals(getConfigPropertyAsText("DISABLE_AUTH")); + return ConfigUtils.hasConfigProperty("DISABLE_AUTH") && + "true".equals(ConfigUtils.getConfigPropertyAsText("DISABLE_AUTH")); } /** @@ -262,11 +256,11 @@ public static void isAuthorized(String userId, Request request) { // Otp user requesting their item. return; } - if (requestingUser.apiUser != null && requestingUser.apiUser.id.equals(userId)) { + if (requestingUser.isThirdPartyUser() && requestingUser.apiUser.id.equals(userId)) { // Api user requesting their item. return; } - if (requestingUser.apiUser != null) { + if (requestingUser.isThirdPartyUser()) { // Api user potentially requesting an item on behalf of an Otp user they created. OtpUser otpUser = Persistence.otpUsers.getById(userId); if (otpUser != null && requestingUser.apiUser.id.equals(otpUser.applicationId)) { @@ -274,6 +268,6 @@ public static void isAuthorized(String userId, Request request) { } } } - logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, "Unauthorized access."); + JsonUtils.logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, "Unauthorized access."); } } diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index 9107b41af..006643b5f 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -3,16 +3,18 @@ import com.auth0.client.auth.AuthAPI; import com.auth0.client.mgmt.ManagementAPI; import com.auth0.exception.Auth0Exception; +import com.auth0.json.auth.TokenHolder; import com.auth0.json.mgmt.jobs.Job; import com.auth0.json.mgmt.users.User; import com.auth0.net.AuthRequest; -import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.commons.validator.routines.EmailValidator; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.bugsnag.BugsnagReporter; import org.opentripplanner.middleware.models.AbstractUser; import org.opentripplanner.middleware.persistence.TypedPersistence; +import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.HttpUtils; +import org.opentripplanner.middleware.utils.JsonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.Request; @@ -25,26 +27,21 @@ import java.util.UUID; import static com.mongodb.client.model.Filters.eq; -import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; -import static org.opentripplanner.middleware.utils.HttpUtils.httpRequestRawResponse; -import static org.opentripplanner.middleware.utils.JsonUtils.getSingleNodeValueFromJSON; -import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** * This class contains methods for querying Auth0 users using the Auth0 User Management API. Auth0 docs describing the * searchable fields and query syntax are here: https://auth0.com/docs/api/management/v2/user-search */ public class Auth0Users { - public static final String AUTH0_DOMAIN = getConfigPropertyAsText("AUTH0_DOMAIN"); + public static final String AUTH0_DOMAIN = ConfigUtils.getConfigPropertyAsText("AUTH0_DOMAIN"); // This client/secret pair is for making requests for an API access token used with the Management API. - private static final String AUTH0_API_CLIENT = getConfigPropertyAsText("AUTH0_API_CLIENT"); - private static final String AUTH0_API_SECRET = getConfigPropertyAsText("AUTH0_API_SECRET"); - private static final String AUTH0_CLIENT_ID = getConfigPropertyAsText("AUTH0_CLIENT_ID"); - private static final String AUTH0_CLIENT_SECRET = getConfigPropertyAsText("AUTH0_CLIENT_SECRET"); + private static final String AUTH0_API_CLIENT = ConfigUtils.getConfigPropertyAsText("AUTH0_API_CLIENT"); + private static final String AUTH0_API_SECRET = ConfigUtils.getConfigPropertyAsText("AUTH0_API_SECRET"); + private static final String AUTH0_CLIENT_ID = ConfigUtils.getConfigPropertyAsText("AUTH0_CLIENT_ID"); + private static final String AUTH0_CLIENT_SECRET = ConfigUtils.getConfigPropertyAsText("AUTH0_CLIENT_SECRET"); private static final String DEFAULT_CONNECTION_TYPE = "Username-Password-Authentication"; private static final String DEFAULT_AUDIENCE = "https://otp-middleware"; private static final String MANAGEMENT_API_VERSION = "v2"; - private static final String SEARCH_API_VERSION = "v3"; public static final String API_PATH = "/api/" + MANAGEMENT_API_VERSION; /** * Cached API token so that we do not have to request a new one each time a Management API request is made. @@ -192,12 +189,12 @@ public static User createNewAuth0User(U user, Request r U userWithEmail = userStore.getOneFiltered(eq("email", user.email)); if (userWithEmail != null) { // TODO: Does this need to change to allow multiple applications to create otpuser's with the same email? - logMessageAndHalt(req, 400, "User with email already exists in database!"); + JsonUtils.logMessageAndHalt(req, 400, "User with email already exists in database!"); } // Check for pre-existing user in Auth0 and create if not exists. User auth0UserProfile = getUserByEmail(user.email, true); if (auth0UserProfile == null) { - logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Error creating user for email " + user.email); + JsonUtils.logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Error creating user for email " + user.email); } LOG.info("Created new Auth0 user ({}) for user {}", auth0UserProfile.getId(), user.id); return auth0UserProfile; @@ -208,7 +205,7 @@ public static User createNewAuth0User(U user, Request r */ public static void validateUser(U user, Request req) { if (!isValidEmail(user.email)) { - logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Email address is invalid."); + JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Email address is invalid."); } } @@ -220,11 +217,11 @@ public static void validateExistingUser(U user, U preEx // Verify that email address for user has not changed. // TODO: should we permit changing email addresses? This would require making an update to Auth0. if (!preExistingUser.email.equals(user.email)) { - logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Cannot change user email address!"); + JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Cannot change user email address!"); } // Verify that Auth0 ID for user has not changed. if (!preExistingUser.auth0UserId.equals(user.auth0UserId)) { - logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Cannot change Auth0 ID!"); + JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Cannot change Auth0 ID!"); } } @@ -245,10 +242,10 @@ private static String getAuth0Url() { /** * Get an Auth0 oauth token for use in mocking user requests by using the Auth0 'Call Your API Using Resource Owner * Password Flow' approach. Auth0 setup can be reviewed here: https://auth0.com/docs/flows/call-your-api-using-resource-owner-password-flow. - * If the user is successfully validated by Auth0 a bearer access token is returned, which is extracted and returned + * If the user is successfully validated by Auth0 a complete token is returned, which is extracted and returned * to the caller. In all other cases, null is returned. */ - public static String getAuth0Token(String username, String password) throws JsonProcessingException { + public static TokenHolder getCompleteAuth0Token(String username, String password) { if (Auth0Connection.isAuthDisabled()) return null; String body = String.format( "grant_type=password&username=%s&password=%s&audience=%s&scope=&client_id=%s&client_secret=%s", @@ -258,8 +255,7 @@ public static String getAuth0Token(String username, String password) throws Json AUTH0_CLIENT_ID, // Auth0 application client ID AUTH0_CLIENT_SECRET // Auth0 application client secret ); - - HttpResponse response = httpRequestRawResponse( + HttpResponse response = HttpUtils.httpRequestRawResponse( URI.create(String.format("https://%s/oauth/token", AUTH0_DOMAIN)), 1000, HttpUtils.REQUEST_METHOD.POST, @@ -270,6 +266,15 @@ public static String getAuth0Token(String username, String password) throws Json LOG.error("Cannot obtain Auth0 token for user {}. response: {} - {}", username, response.statusCode(), response.body()); return null; } - return getSingleNodeValueFromJSON("access_token", response.body()); + return JsonUtils.getPOJOFromJSON(response.body(), TokenHolder.class); + } + + /** + * Extract from a complete Auth0 token just the access token. If the token is not available, return null instead. + */ + public static String getAuth0Token(String username, String password) { + TokenHolder token = getCompleteAuth0Token(username, password); + return (token == null) ? null : token.getAccessToken(); } + } diff --git a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index 9ef47dfb0..a28d43815 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -67,4 +67,11 @@ static RequestingUser createTestUser(Request req) { return new RequestingUser(auth0UserId); } + + /** + * Determine if requesting user is a third party user. + */ + public boolean isThirdPartyUser() { + return apiUser != null; + } } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java index cc4dfbb9b..d4a909131 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java @@ -17,11 +17,7 @@ import spark.Response; import static com.beerboy.ss.descriptor.MethodDescriptor.path; -import static org.opentripplanner.middleware.auth.Auth0Users.createNewAuth0User; -import static org.opentripplanner.middleware.auth.Auth0Users.updateAuthFieldsForUser; -import static org.opentripplanner.middleware.auth.Auth0Users.validateExistingUser; import static org.opentripplanner.middleware.utils.HttpUtils.JSON_ONLY; -import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** * Implementation of the {@link ApiController} abstract class for managing users. This controller connects with Auth0 @@ -85,7 +81,10 @@ private U getUserFromRequest(Request req, Response res) { // but have not completed the account setup form yet. // For those users, the user profile would be 404 not found (as opposed to 403 forbidden). if (user == null) { - logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, String.format(NO_USER_WITH_AUTH0_ID_MESSAGE, profile.auth0UserId), null); + JsonUtils.logMessageAndHalt(req, + HttpStatus.NOT_FOUND_404, + String.format(NO_USER_WITH_AUTH0_ID_MESSAGE, profile.auth0UserId), + null); } return user; } @@ -105,19 +104,19 @@ U preCreateHook(U user, Request req) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // TODO: If MOD UI is to be an ApiUser, we may want to do an additional check here to determine if this is a // first-party API user (MOD UI) or third party. - if (requestingUser.apiUser != null && user instanceof OtpUser) { + if (requestingUser.isThirdPartyUser() && user instanceof OtpUser) { // Do not create Auth0 account for OtpUsers created on behalf of third party API users. return user; } else { // For any other user account, create Auth0 account - User auth0UserProfile = createNewAuth0User(user, req, this.persistence); - return updateAuthFieldsForUser(user, auth0UserProfile); + User auth0UserProfile = Auth0Users.createNewAuth0User(user, req, this.persistence); + return Auth0Users.updateAuthFieldsForUser(user, auth0UserProfile); } } @Override U preUpdateHook(U user, U preExistingUser, Request req) { - validateExistingUser(user, preExistingUser, req, this.persistence); + Auth0Users.validateExistingUser(user, preExistingUser, req, this.persistence); return user; } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java index e46060f61..ad5a36e31 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java @@ -26,12 +26,7 @@ import static com.beerboy.ss.descriptor.EndpointDescriptor.endpointPath; import static com.beerboy.ss.descriptor.MethodDescriptor.path; -import static org.opentripplanner.middleware.auth.Auth0Connection.getUserFromRequest; -import static org.opentripplanner.middleware.auth.Auth0Connection.isUserAdmin; import static org.opentripplanner.middleware.utils.HttpUtils.JSON_ONLY; -import static org.opentripplanner.middleware.utils.HttpUtils.getQueryParamFromRequest; -import static org.opentripplanner.middleware.utils.JsonUtils.getPOJOFromRequestBody; -import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** * Generic API controller abstract class. This class provides CRUD methods using {@link spark.Spark} HTTP request @@ -189,10 +184,10 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { // FIXME Maybe better if the user check (and filtering) was done in a pre hook? // FIXME Will require further granularity for admin private ResponseList getMany(Request req, Response res) { - int limit = getQueryParamFromRequest(req, LIMIT_PARAM, 0, DEFAULT_LIMIT, 100); - int offset = getQueryParamFromRequest(req, OFFSET_PARAM, 0, DEFAULT_OFFSET); - RequestingUser requestingUser = getUserFromRequest(req); - if (isUserAdmin(requestingUser)) { + int limit = HttpUtils.getQueryParamFromRequest(req, LIMIT_PARAM, 0, DEFAULT_LIMIT, 100); + int offset = HttpUtils.getQueryParamFromRequest(req, OFFSET_PARAM, 0, DEFAULT_OFFSET); + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); + if (Auth0Connection.isUserAdmin(requestingUser)) { // If the user is admin, the context is presumed to be the admin dashboard, so we deliver all entities for // management or review without restriction. return persistence.getResponseList(offset, limit); @@ -201,11 +196,11 @@ private ResponseList getMany(Request req, Response res) { // OtpUserController. Therefore, the request should be limited to return just the entity matching the // requesting user. return persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit); - } else if (requestingUser.apiUser != null) { + } else if (requestingUser.isThirdPartyUser()) { // Third party API users must pass in an OtpUser id as a query param in order to get filtered objects. // Query param is used so existing (and new) endpoints aren't affected. - String otpUserId = getQueryParamFromRequest(req, "otpUserId", false); - return persistence.getResponseList(Filters.eq("userId", otpUserId), offset, limit); + String userId = HttpUtils.getQueryParamFromRequest(req, "userId", false); + return persistence.getResponseList(Filters.eq("userId", userId), offset, limit); } else { // For all other cases the assumption is that the request is being made by an Otp user and the requested // entities have a 'userId' parameter. Only entities that match the requesting user id are returned. @@ -225,7 +220,7 @@ protected T getEntityForId(Request req, Response res) { T object = getObjectForId(req, id); if (!object.canBeManagedBy(requestingUser)) { - logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to get %s.", className)); + JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to get %s.", className)); } return object; @@ -242,17 +237,17 @@ private T deleteOne(Request req, Response res) { T object = getObjectForId(req, id); // Check that requesting user can manage entity. if (!object.canBeManagedBy(requestingUser)) { - logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to delete %s.", className)); + JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to delete %s.", className)); } // Run pre-delete hook. If return value is false, abort. if (!preDeleteHook(object, req)) { - logMessageAndHalt(req, 500, "Unknown error occurred during delete attempt."); + JsonUtils.logMessageAndHalt(req, 500, "Unknown error occurred during delete attempt."); } boolean success = object.delete(); if (success) { return object; } else { - logMessageAndHalt( + JsonUtils.logMessageAndHalt( req, HttpStatus.INTERNAL_SERVER_ERROR_500, String.format("Unknown error encountered. Failed to delete %s", className), @@ -262,7 +257,7 @@ private T deleteOne(Request req, Response res) { } catch (HaltException e) { throw e; } catch (Exception e) { - logMessageAndHalt( + JsonUtils.logMessageAndHalt( req, HttpStatus.INTERNAL_SERVER_ERROR_500, String.format("Error deleting %s", className), @@ -280,7 +275,7 @@ private T deleteOne(Request req, Response res) { private T getObjectForId(Request req, String id) { T object = persistence.getById(id); if (object == null) { - logMessageAndHalt( + JsonUtils.logMessageAndHalt( req, HttpStatus.NOT_FOUND_404, String.format("No %s with id=%s found.", className, id), @@ -315,18 +310,20 @@ private T createOrUpdate(Request req, Response res) { // Check if an update or create operation depending on presence of id param // This needs to be final because it is used in a lambda operation below. if (req.params(ID_PARAM) == null && req.requestMethod().equals("PUT")) { - logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Must provide id"); + JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Must provide id"); } RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); final boolean isCreating = req.params(ID_PARAM) == null; // Save or update to database try { // Validate fields by deserializing into POJO. - T object = getPOJOFromRequestBody(req, clazz); + T object = JsonUtils.getPOJOFromRequestBody(req, clazz); if (isCreating) { // Verify that the requesting user can create object. if (!object.canBeCreatedBy(requestingUser)) { - logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to create %s.", className)); + JsonUtils.logMessageAndHalt(req, + HttpStatus.FORBIDDEN_403, + String.format("Requesting user not authorized to create %s.", className)); } // Run pre-create hook and use updated object (with potentially modified values) in create operation. T updatedObject = preCreateHook(object, req); @@ -335,12 +332,14 @@ private T createOrUpdate(Request req, Response res) { String id = getIdFromRequest(req); T preExistingObject = getObjectForId(req, id); if (preExistingObject == null) { - logMessageAndHalt(req, 400, "Object to update does not exist!"); + JsonUtils.logMessageAndHalt(req, 400, "Object to update does not exist!"); return null; } // Check that requesting user can manage entity. if (!preExistingObject.canBeManagedBy(requestingUser)) { - logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to update %s.", className)); + JsonUtils.logMessageAndHalt(req, + HttpStatus.FORBIDDEN_403, + String.format("Requesting user not authorized to update %s.", className)); } // Update last updated value. object.lastUpdated = new Date(); @@ -348,7 +347,7 @@ private T createOrUpdate(Request req, Response res) { object.dateCreated = preExistingObject.dateCreated; // Validate that ID in JSON body matches ID param. TODO add test if (!id.equals(object.id)) { - logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "ID in JSON body must match ID param."); + JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "ID in JSON body must match ID param."); } // Get updated object from pre-update hook method. T updatedObject = preUpdateHook(object, preExistingObject, req); @@ -359,9 +358,13 @@ private T createOrUpdate(Request req, Response res) { } catch (HaltException e) { throw e; } catch (JsonProcessingException e) { - logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Error parsing JSON for " + clazz.getSimpleName(), e); + JsonUtils.logMessageAndHalt(req, + HttpStatus.BAD_REQUEST_400, + "Error parsing JSON for " + clazz.getSimpleName(), e); } catch (Exception e) { - logMessageAndHalt(req, 500, "An error was encountered while trying to save to the database", e); + JsonUtils.logMessageAndHalt(req, + 500, + "An error was encountered while trying to save to the database", e); } finally { String operation = isCreating ? "Create" : "Update"; LOG.info("{} {} operation took {} msec", operation, className, DateTimeUtils.currentTimeMillis() - startTime); diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java index 4eff47d8a..9a1bbfb5b 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java @@ -1,7 +1,7 @@ package org.opentripplanner.middleware.controllers.api; +import com.auth0.json.auth.TokenHolder; import com.beerboy.ss.ApiEndpoint; -import com.fasterxml.jackson.core.JsonProcessingException; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.auth.Auth0Users; @@ -10,6 +10,7 @@ import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.utils.ApiGatewayUtils; +import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.CreateApiKeyException; import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.JsonUtils; @@ -20,11 +21,7 @@ import spark.Response; import static com.beerboy.ss.descriptor.MethodDescriptor.path; -import static org.opentripplanner.middleware.auth.Auth0Connection.isUserAdmin; -import static org.opentripplanner.middleware.utils.ApiGatewayUtils.deleteApiKey; -import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; import static org.opentripplanner.middleware.utils.HttpUtils.JSON_ONLY; -import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** * Implementation of the {@link AbstractUserController} for {@link ApiUser}. This controller also contains methods for @@ -32,7 +29,7 @@ */ public class ApiUserController extends AbstractUserController { private static final Logger LOG = LoggerFactory.getLogger(ApiUserController.class); - public static final String DEFAULT_USAGE_PLAN_ID = getConfigPropertyAsText("DEFAULT_USAGE_PLAN_ID"); + public static final String DEFAULT_USAGE_PLAN_ID = ConfigUtils.getConfigPropertyAsText("DEFAULT_USAGE_PLAN_ID"); private static final String API_KEY_PATH = "/apikey"; private static final String AUTHENTICATE_PATH = "/authenticate"; private static final int API_KEY_LIMIT_PER_USER = 2; @@ -81,7 +78,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { .withQueryParam().withName(PASSWORD_PARAM).withRequired(true) .withDescription("Auth0 password.").and() .withProduces(JSON_ONLY) - .withResponseType(String.class), + .withResponseType(TokenHolder.class), this::authenticateAuth0User, JsonUtils::toJson ); @@ -100,14 +97,14 @@ private boolean userHasKey(ApiUser user, String apiKeyId) { } /** - * Authenticate user with Auth0 based on username (email) and password. If successful, return the bearer token else - * null. + * Authenticate user with Auth0 based on username (email) and password. If successful, return the complete Auth0 + * token else null. */ - private String authenticateAuth0User(Request req, Response res) throws JsonProcessingException { + private TokenHolder authenticateAuth0User(Request req, Response res) { String username = HttpUtils.getQueryParamFromRequest(req, USERNAME_PARAM, false); // FIXME: Should this be encrypted?! String password = HttpUtils.getQueryParamFromRequest(req, PASSWORD_PARAM, false); - return Auth0Users.getAuth0Token(username, password); + return Auth0Users.getCompleteAuth0Token(username, password); } /** @@ -120,10 +117,10 @@ private ApiUser createApiKeyForApiUser(Request req, Response res) { String usagePlanId = req.queryParamOrDefault("usagePlanId", DEFAULT_USAGE_PLAN_ID); // If requester is not an admin user, force the usage plan ID to the default and enforce key limit. A non-admin // user should not be able to create an API key for any usage plan. - if (!isUserAdmin(requestingUser)) { + if (!Auth0Connection.isUserAdmin(requestingUser)) { usagePlanId = DEFAULT_USAGE_PLAN_ID; if (targetUser.apiKeys.size() >= API_KEY_LIMIT_PER_USER) { - logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "User has reached API key limit."); + JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "User has reached API key limit."); } } // FIXME Should an Api user be limited to one api key per usage plan (and perhaps stage)? @@ -133,7 +130,7 @@ private ApiUser createApiKeyForApiUser(Request req, Response res) { targetUser.apiKeys.add(apiKey); Persistence.apiUsers.replace(targetUser.id, targetUser); } catch (CreateApiKeyException e) { - logMessageAndHalt(req, + JsonUtils.logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Error creating API key", e @@ -148,26 +145,26 @@ private ApiUser createApiKeyForApiUser(Request req, Response res) { private ApiUser deleteApiKeyForApiUser(Request req, Response res) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // Do not permit key deletion unless user is an admin. - if (!isUserAdmin(requestingUser)) { - logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to delete an API key."); + if (!Auth0Connection.isUserAdmin(requestingUser)) { + JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to delete an API key."); } ApiUser targetUser = getApiUser(req); String apiKeyId = HttpUtils.getRequiredParamFromRequest(req, "apiKeyId"); if (apiKeyId == null) { - logMessageAndHalt(req, + JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "An api key id is required", null); } if (!userHasKey(targetUser, apiKeyId)) { - logMessageAndHalt(req, + JsonUtils.logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, String.format("User id (%s) does not have expected api key id (%s)", targetUser.id, apiKeyId), null); } // Delete API key from AWS. - boolean success = deleteApiKey(new ApiKey(apiKeyId)); + boolean success = ApiGatewayUtils.deleteApiKey(new ApiKey(apiKeyId)); if (success) { // Delete api key from user and persist targetUser.apiKeys.removeIf(apiKey -> apiKeyId.equals(apiKey.keyId)); @@ -175,7 +172,7 @@ private ApiUser deleteApiKeyForApiUser(Request req, Response res) { return Persistence.apiUsers.getById(targetUser.id); } else { // Throw halt if API key deletion failed. - logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Unknown error deleting API key."); + JsonUtils.logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Unknown error deleting API key."); return null; } } @@ -194,7 +191,7 @@ ApiUser preCreateHook(ApiUser user, Request req) { try { user.createApiKey(DEFAULT_USAGE_PLAN_ID, false); } catch (CreateApiKeyException e) { - logMessageAndHalt( + JsonUtils.logMessageAndHalt( req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Error creating API key", @@ -229,7 +226,7 @@ private static ApiUser getApiUser(Request req) { String userId = HttpUtils.getRequiredParamFromRequest(req, ID_PARAM); ApiUser apiUser = Persistence.apiUsers.getById(userId); if (apiUser == null) { - logMessageAndHalt( + JsonUtils.logMessageAndHalt( req, HttpStatus.NOT_FOUND_404, String.format("No Api user matching the given user id (%s)", userId), @@ -238,9 +235,8 @@ private static ApiUser getApiUser(Request req) { } if (!apiUser.canBeManagedBy(requestingUser)) { - logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to perform this operation."); + JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to perform this operation."); } - return apiUser; } } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index f485620d6..c25a528ac 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -14,6 +14,7 @@ import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.utils.DateTimeUtils; import org.opentripplanner.middleware.utils.HttpUtils; +import org.opentripplanner.middleware.utils.JsonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.Request; @@ -27,7 +28,6 @@ import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthHeaderPresent; import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_API_ROOT; import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_PLAN_ENDPOINT; -import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** * Responsible for getting a response from OTP based on the parameters provided by the requester. If the target service @@ -78,7 +78,7 @@ public void bind(final SparkSwagger restApi) { */ private static String proxy(Request request, spark.Response response) { if (OTP_API_ROOT == null) { - logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No OTP Server provided, check config."); + JsonUtils.logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No OTP Server provided, check config."); return null; } // Get request path intended for OTP API by removing the proxy endpoint (/otp). @@ -87,7 +87,7 @@ private static String proxy(Request request, spark.Response response) { // attempt to get response from OTP server based on requester's query parameters OtpDispatcherResponse otpDispatcherResponse = OtpDispatcher.sendOtpRequest(request.queryString(), otpRequestPath); if (otpDispatcherResponse == null || otpDispatcherResponse.responseBody == null) { - logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No response from OTP server."); + JsonUtils.logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No response from OTP server."); return null; } @@ -128,14 +128,15 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons return; } + // Determine if the Otp request is being made by an actual Otp user or by a third party on behalf of an Otp user. OtpUser otpUser; - if (requestingUser.apiUser != null) { + if (requestingUser.isThirdPartyUser()) { // Api user making a trip request on behalf of an Otp user. In this case, the Otp user id must be provided // as a query parameter. - String otpUserId = request.queryParams("userId"); - otpUser = Persistence.otpUsers.getById(otpUserId); + String userId = request.queryParams("userId"); + otpUser = Persistence.otpUsers.getById(userId); if (otpUser != null && !otpUser.canBeManagedBy(requestingUser)) { - logMessageAndHalt(request, + JsonUtils.logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, String.format("User: %s not authorized to make trip requests for user: %s", requestingUser.apiUser.email, diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java index d51f9c697..213753426 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java @@ -38,20 +38,23 @@ public OtpUserController(String apiPrefix) { @Override OtpUser preCreateHook(OtpUser user, Request req) { - // Check API key and assign user to appropriate third-party application. Note: this is only relevant for - // instances of otp-middleware running behind API Gateway. String apiKey = req.headers("x-api-key"); - ApiUser apiUser = Persistence.apiUsers.getOneFiltered(Filters.eq("apiKeys.value", apiKey)); - if (apiUser != null) { - // If API user found, assign to new OTP user. - user.applicationId = apiUser.id; - } else { - // If API user not found, report to Bugsnag for further investigation. - BugsnagReporter.reportErrorToBugsnag( - "OTP user created with API key that is not linked to any API user", - apiKey, - new IllegalArgumentException("API key not linked to API user.") - ); + // If an api key is present an API user is attempting to create an OTP user. + if (apiKey != null) { + // Check API key and assign user to appropriate third-party application. Note: this is only relevant for + // instances of otp-middleware running behind API Gateway. + ApiUser apiUser = Persistence.apiUsers.getOneFiltered(Filters.eq("apiKeys.value", apiKey)); + if (apiUser != null) { + // If API user found, assign to new OTP user. + user.applicationId = apiUser.id; + } else { + // If API user not found, report to Bugsnag for further investigation. + BugsnagReporter.reportErrorToBugsnag( + "OTP user created with API key that is not linked to any API user", + apiKey, + new IllegalArgumentException("API key not linked to API user.") + ); + } } return super.preCreateHook(user, req); } diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index 74644d2e1..6a32aa81e 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -212,7 +212,7 @@ public boolean canBeManagedBy(RequestingUser requestingUser) { if (belongsToUser) { return true; - } else if (requestingUser.apiUser != null) { + } else if (requestingUser.isThirdPartyUser()) { // get the required OTP user to confirm they are associated with the requesting API user. OtpUser otpUser = Persistence.otpUsers.getById(userId); if (otpUser != null && requestingUser.apiUser.id.equals(otpUser.applicationId)) { diff --git a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java index 07c0f5d47..f71d7f895 100644 --- a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java @@ -86,7 +86,7 @@ public boolean delete(boolean deleteAuth0User) { */ @Override public boolean canBeManagedBy(RequestingUser requestingUser) { - if (requestingUser.apiUser != null && requestingUser.apiUser.id.equals(applicationId)) { + if (requestingUser.isThirdPartyUser() && requestingUser.apiUser.id.equals(applicationId)) { // Otp user was created by this Api user. return true; } diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index 91b5c306c..76e70b831 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -2,9 +2,6 @@ import com.auth0.exception.Auth0Exception; import com.auth0.json.mgmt.users.User; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -16,6 +13,7 @@ import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TripRequest; import org.opentripplanner.middleware.persistence.Persistence; +import org.opentripplanner.middleware.persistence.PersistenceUtil; import org.opentripplanner.middleware.utils.CreateApiKeyException; import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.JsonUtils; @@ -25,7 +23,6 @@ import java.io.IOException; import java.net.http.HttpResponse; import java.util.LinkedHashMap; -import java.util.List; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -40,7 +37,6 @@ import static org.opentripplanner.middleware.controllers.api.ApiUserController.DEFAULT_USAGE_PLAN_ID; import static org.opentripplanner.middleware.controllers.api.OtpRequestProcessor.OTP_PROXY_ENDPOINT; import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_PLAN_ENDPOINT; -import static org.opentripplanner.middleware.persistence.PersistenceUtil.createApiUser; /** * Tests to simulate API user flow. The following config parameters must be set in configurations/default/env.yml for @@ -90,9 +86,8 @@ public static void setUp() throws IOException, InterruptedException, CreateApiKe // Mock the OTP server TODO: Run a live OTP instance? TestUtils.mockOtpServer(); // As a pre-condition, create an API User with API key. - apiUser = createApiUser(String.format("test-%s@example.com", UUID.randomUUID().toString())); + apiUser = PersistenceUtil.createApiUser(String.format("test-%s@example.com", UUID.randomUUID().toString())); apiUser.createApiKey(DEFAULT_USAGE_PLAN_ID, true); - Persistence.apiUsers.replace(apiUser.id, apiUser); // Create, but do not persist, an OTP user. otpUser = new OtpUser(); otpUser.email = String.format("test-%s@example.com", UUID.randomUUID().toString()); @@ -128,7 +123,7 @@ public static void tearDown() { @Test public void canSimulateApiUserFlow() { - // obtain bearer token + // obtain Auth0 token for Api user. String endpoint = String.format("api/secure/application/%s/authenticate?username=%s&password=%s", apiUser.id, apiUser.email, @@ -137,18 +132,19 @@ public void canSimulateApiUserFlow() { apiUser, "" ); - System.out.println(getTokenResponse.body()); + LOG.info(getTokenResponse.body()); assertEquals(HttpStatus.OK_200, getTokenResponse.statusCode()); - // create otp user as api user. + // create an Otp user authenticating as an Api user. HttpResponse createUserResponse = mockAuthenticatedPost("api/secure/user", apiUser, JsonUtils.toJson(otpUser) ); + assertEquals(HttpStatus.OK_200, createUserResponse.statusCode()); - // Create a monitored trip for an Otp user as Otp user. This will fail because the user was created by an - // Api user and therefore does not have a Auth0 account. + // Attempt to create a monitored trip for an Otp user authenticating as an Otp user. This will fail because the + // user was created by an Api user and therefore does not have a Auth0 account. OtpUser otpUserResponse = JsonUtils.getPOJOFromJSON(createUserResponse.body(), OtpUser.class); MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); monitoredTrip.userId = otpUser.id; @@ -158,8 +154,8 @@ public void canSimulateApiUserFlow() { ); assertEquals(HttpStatus.UNAUTHORIZED_401, createTripResponseAsOtpUser.statusCode()); - // Create a monitored trip for the Otp user as Api user. An Api user should be able to create a monitored trip - // for an Otp user they created. + // Create a monitored trip for an Otp user authenticating as an Api user. An Api user can create a monitored + // trip for an Otp user they created. HttpResponse createTripResponseAsApiUser = mockAuthenticatedPost("api/secure/monitoredtrip", apiUser, JsonUtils.toJson(monitoredTrip) @@ -167,8 +163,8 @@ public void canSimulateApiUserFlow() { assertEquals(HttpStatus.OK_200, createTripResponseAsApiUser.statusCode()); MonitoredTrip monitoredTripResponse = JsonUtils.getPOJOFromJSON(createTripResponseAsApiUser.body(), MonitoredTrip.class); - // Plan trip with OTP proxy. Mock plan response will be returned. This will work as an Otp user (created by MOD UI - // or an Api user) because the end point has no auth. + // Plan trip with OTP proxy authenticating as an OTP user. Mock plan response will be returned. This will work + // as an Otp user (created by MOD UI or an Api user) because the end point has no auth. String otpQuery = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745&userId=" + otpUserResponse.id; HttpResponse planTripResponseAsOtUser = mockAuthenticatedRequest(otpQuery, otpUserResponse, @@ -177,8 +173,8 @@ public void canSimulateApiUserFlow() { LOG.info("Plan trip response: {}\n....", planTripResponseAsOtUser.body().substring(0, 300)); assertEquals(HttpStatus.OK_200, planTripResponseAsOtUser.statusCode()); - // Plan trip with OTP proxy. Mock plan response will be returned. This will work as an Api user because the end - // point has no auth. + // Plan trip with OTP proxy authenticating as an Api user. Mock plan response will be returned. This will work + // as an Api user because the end point has no auth. HttpResponse planTripResponseAsApiUser = mockAuthenticatedRequest(otpQuery, apiUser, HttpUtils.REQUEST_METHOD.GET @@ -186,8 +182,8 @@ public void canSimulateApiUserFlow() { LOG.info("Plan trip response: {}\n....", planTripResponseAsApiUser.body().substring(0, 300)); assertEquals(HttpStatus.OK_200, planTripResponseAsApiUser.statusCode()); - // Get trip for user as Otp user. This will fail because the user was created by an - // Api user and therefore does not have a Auth0 account. + // Get trip request history for user authenticating as an Otp user. This will fail because the user was created + // by an Api user and therefore does not have a Auth0 account. HttpResponse tripRequestResponseAsOtUser = mockAuthenticatedRequest(String.format("api/secure/triprequests?userId=%s", otpUserResponse.id), otpUserResponse, @@ -196,8 +192,8 @@ public void canSimulateApiUserFlow() { assertEquals(HttpStatus.UNAUTHORIZED_401, tripRequestResponseAsOtUser.statusCode()); - // Get trip for user as an Api user. This will work because an Api user should be able to get a trip on behalf - // of an Otp user they created. + // Get trip request history for user authenticating as an Api user. This will work because an Api user is able + // to get a trip on behalf of an Otp user they created. HttpResponse tripRequestResponseAsApiUser = mockAuthenticatedRequest(String.format("api/secure/triprequests?userId=%s", otpUserResponse.id), apiUser, @@ -207,8 +203,8 @@ public void canSimulateApiUserFlow() { ResponseList tripRequests = JsonUtils.getPOJOFromJSON(tripRequestResponseAsApiUser.body(), ResponseList.class); - // Delete Otp user as Otp user. This will fail because the user was created by an Api user and therefore does - // not have a Auth0 account. + // Delete Otp user authenticating as an Otp user. This will fail because the user was created by an Api user and + // therefore does not have a Auth0 account. HttpResponse deleteUserResponseAsOtpUser = mockAuthenticatedRequest( String.format("api/secure/user/%s", otpUserResponse.id), otpUserResponse, @@ -216,7 +212,8 @@ public void canSimulateApiUserFlow() { ); assertEquals(HttpStatus.UNAUTHORIZED_401, deleteUserResponseAsOtpUser.statusCode()); - // Delete Otp user as Api user. This will work because an Api user can delete an Otp user they created. + // Delete Otp user authenticating as an Api user. This will work because an Api user can delete an Otp user they + // created. HttpResponse deleteUserResponseAsApiUser = mockAuthenticatedRequest( String.format("api/secure/user/%s", otpUserResponse.id), apiUser, diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index 0d330f56d..b8686acf1 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -1,7 +1,7 @@ package org.opentripplanner.middleware; -import com.fasterxml.jackson.core.JsonProcessingException; import org.eclipse.jetty.http.HttpStatus; +import org.opentripplanner.middleware.auth.Auth0Users; import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.AbstractUser; import org.opentripplanner.middleware.models.ApiUser; @@ -11,8 +11,6 @@ import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.utils.FileUtils; import org.opentripplanner.middleware.utils.HttpUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import spark.Request; import spark.Response; import spark.Service; @@ -25,13 +23,11 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; -import static org.opentripplanner.middleware.auth.Auth0Users.getAuth0Token; import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_PLAN_ENDPOINT; import static spark.Service.ignite; public class TestUtils { - private static final Logger LOG = LoggerFactory.getLogger(TestUtils.class); /** * Whether the end-to-end environment variable is enabled. */ @@ -111,13 +107,7 @@ private static HashMap getMockHeaders(AbstractUser requestingUse } // Otherwise, get a valid oauth token for the user - String token = null; - try { - token = getAuth0Token(requestingUser.email, TEMP_AUTH0_USER_PASSWORD); - } catch (JsonProcessingException e) { - LOG.error("Cannot obtain Auth0 token for user {}", requestingUser.email, e); - } - headers.put("Authorization", "Bearer " + token); + headers.put("Authorization", "Bearer " + Auth0Users.getAuth0Token(requestingUser.email, TEMP_AUTH0_USER_PASSWORD)); return headers; } diff --git a/src/test/java/org/opentripplanner/middleware/persistence/TripHistoryPersistenceTest.java b/src/test/java/org/opentripplanner/middleware/persistence/TripHistoryPersistenceTest.java index d8dbf6c04..edc587adc 100644 --- a/src/test/java/org/opentripplanner/middleware/persistence/TripHistoryPersistenceTest.java +++ b/src/test/java/org/opentripplanner/middleware/persistence/TripHistoryPersistenceTest.java @@ -92,8 +92,6 @@ public void canDeleteTripSummary() { @Test public void canGetFilteredTripRequestsWithFromAndToDate() { - OtpUser user = createUser(TEST_EMAIL); - List tripRequests = createTripRequests(LIMIT, user.id); LocalDateTime fromStartOfDay = DateTimeUtils.nowAsLocalDate().atTime(LocalTime.MIN); LocalDateTime toEndOfDay = DateTimeUtils.nowAsLocalDate().atTime(LocalTime.MAX); Date fromDate = Date.from(fromStartOfDay @@ -102,7 +100,7 @@ public void canGetFilteredTripRequestsWithFromAndToDate() { Date toDate = Date.from(toEndOfDay .atZone(DateTimeUtils.getSystemZoneId()) .toInstant()); - Bson filter = filterByUserAndDateRange(user.id, fromDate, toDate); + Bson filter = filterByUserAndDateRange(otpUser.id, fromDate, toDate); ResponseList result = Persistence.tripRequests.getResponseList(filter, 0, LIMIT); assertEquals(result.data.size(), tripRequests.size()); } From b7758a81184a5196cfd64b70fb099733dbd6663b Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Tue, 13 Oct 2020 18:02:47 +0100 Subject: [PATCH 08/30] refactor(Updated spark swagger output): Updated spark swagger output after updating the return type --- .../resources/latest-spark-swagger-output.yaml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml index 2bc606d08..dc78f1f68 100644 --- a/src/main/resources/latest-spark-swagger-output.yaml +++ b/src/main/resources/latest-spark-swagger-output.yaml @@ -239,7 +239,7 @@ paths: "200": description: "successful operation" schema: - type: "string" + $ref: "#/definitions/TokenHolder" /api/secure/application/fromtoken: get: tags: @@ -826,6 +826,20 @@ definitions: type: "boolean" name: type: "string" + TokenHolder: + type: "object" + properties: + accessToken: + type: "string" + idToken: + type: "string" + refreshToken: + type: "string" + tokenType: + type: "string" + expiresIn: + type: "integer" + format: "int64" EncodedPolyline: type: "object" properties: From 777e2187ecf34a4e11112ff5939cc1067b6064c3 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Thu, 15 Oct 2020 11:27:49 +0100 Subject: [PATCH 09/30] refactor(Addressed PR feedback and included update for issue #81): Address issue with otp user obtai --- .../middleware/auth/Auth0Connection.java | 5 +- .../controllers/api/ApiController.java | 55 ++++++- .../middleware/ApiUserFlowTest.java | 18 ++- .../middleware/GetMonitoredTripsTest.java | 153 ++++++++++++++++++ .../opentripplanner/middleware/TestUtils.java | 1 + .../persistence/PersistenceUtil.java | 2 + 6 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 7c5c9be7e..916b0e676 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -142,10 +142,11 @@ public static void checkUserIsAdmin(Request req, Response res) { } /** - * Check if the incoming user is an admin user + * Check if the incoming user is an admin user. To be classed as an admin user, the user must not be any other user + * type. */ public static boolean isUserAdmin(RequestingUser user) { - return user != null && user.adminUser != null; + return user != null && user.adminUser != null && user.apiUser == null && user.otpUser == null; } /** diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java index ad5a36e31..d112fc801 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java @@ -51,6 +51,7 @@ public abstract class ApiController implements Endpoint { public static final int DEFAULT_LIMIT = 10; public static final int DEFAULT_OFFSET = 0; public static final String OFFSET_PARAM = "offset"; + public static final String USER_ID_PARAM = "userId"; public static final ParameterDescriptor LIMIT = ParameterDescriptor.newBuilder() .withName(LIMIT_PARAM) @@ -186,6 +187,12 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { private ResponseList getMany(Request req, Response res) { int limit = HttpUtils.getQueryParamFromRequest(req, LIMIT_PARAM, 0, DEFAULT_LIMIT, 100); int offset = HttpUtils.getQueryParamFromRequest(req, OFFSET_PARAM, 0, DEFAULT_OFFSET); + String userId = HttpUtils.getQueryParamFromRequest(req, USER_ID_PARAM, true); + // Filter the response based on the user id, if provided. + if (userId != null) { + return persistence.getResponseList(Filters.eq(USER_ID_PARAM, userId), offset, limit); + } + // If the user id is not provided filter response based on requesting user. RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); if (Auth0Connection.isUserAdmin(requestingUser)) { // If the user is admin, the context is presumed to be the admin dashboard, so we deliver all entities for @@ -197,16 +204,54 @@ private ResponseList getMany(Request req, Response res) { // requesting user. return persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit); } else if (requestingUser.isThirdPartyUser()) { - // Third party API users must pass in an OtpUser id as a query param in order to get filtered objects. - // Query param is used so existing (and new) endpoints aren't affected. - String userId = HttpUtils.getQueryParamFromRequest(req, "userId", false); - return persistence.getResponseList(Filters.eq("userId", userId), offset, limit); + // A user id must be provided if the request is being made by a third party user. + JsonUtils.logMessageAndHalt(req, + HttpStatus.BAD_REQUEST_400, + String.format("The parameter name (%s) must be provided.", USER_ID_PARAM)); + return null; } else { // For all other cases the assumption is that the request is being made by an Otp user and the requested // entities have a 'userId' parameter. Only entities that match the requesting user id are returned. - return persistence.getResponseList(Filters.eq("userId", requestingUser.otpUser.id), offset, limit); + return persistence.getResponseList(Filters.eq(USER_ID_PARAM, requestingUser.otpUser.id), offset, limit); } } +// private ResponseList getMany(Request req, Response res) { +// int limit = HttpUtils.getQueryParamFromRequest(req, LIMIT_PARAM, 0, DEFAULT_LIMIT, 100); +// int offset = HttpUtils.getQueryParamFromRequest(req, OFFSET_PARAM, 0, DEFAULT_OFFSET); +// String userId = HttpUtils.getQueryParamFromRequest(req, USER_ID_PARAM, true); +// // Filter the response based on the user id, if provided. +// if (userId != null) { +// // Define the correct field name depending on the entities being requested. If one of the user classes is +// // being requested, this will limit the response to a single entity. +// String fieldName = (persistence.clazz == OtpUser.class || +// persistence.clazz == ApiUser.class || +// persistence.clazz == AdminUser.class) +// ? "_id" : "userId"; +// return persistence.getResponseList(Filters.eq(fieldName, userId), offset, limit); +// } +// // If the user id is not provided filter response based on requesting user. +// RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); +// if (Auth0Connection.isUserAdmin(requestingUser)) { +// // If the user is admin, the context is presumed to be the admin dashboard, so we deliver all entities for +// // management or review without restriction. +// return persistence.getResponseList(offset, limit); +// } else if (persistence.clazz == OtpUser.class) { +// // If the required entity is of type 'OtpUser' the assumption is that a call is being made via the +// // OtpUserController. Therefore, the request should be limited to return just the entity matching the +// // requesting user. +// return persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit); +// } else if (requestingUser.isThirdPartyUser()) { +// // A user id must be provided if the request is being made by a third party user. +// JsonUtils.logMessageAndHalt(req, +// HttpStatus.BAD_REQUEST_400, +// String.format("The parameter name (%s) must be provided.", USER_ID_PARAM)); +// return null; +// } else { +// // For all other cases the assumption is that the request is being made by an Otp user and the requested +// // entities have a 'userId' parameter. Only entities that match the requesting user id are returned. +// return persistence.getResponseList(Filters.eq("userId", requestingUser.otpUser.id), offset, limit); +// } +// } /** * HTTP endpoint to get one entity specified by ID. This will return an object based on the checks carried out in diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index 76e70b831..a8d4d3c2b 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -76,7 +76,7 @@ private static boolean testsShouldRun() { } /** - * Create an {@link ApiUser} and an {@link AdminUser} prior to unit tests + * Create an {@link ApiUser} and an {@link OtpUser} prior to unit tests */ @BeforeAll public static void setUp() throws IOException, InterruptedException, CreateApiKeyException { @@ -163,6 +163,22 @@ public void canSimulateApiUserFlow() { assertEquals(HttpStatus.OK_200, createTripResponseAsApiUser.statusCode()); MonitoredTrip monitoredTripResponse = JsonUtils.getPOJOFromJSON(createTripResponseAsApiUser.body(), MonitoredTrip.class); + // Request all monitored trip for an Otp user authenticating as an Api user. + HttpResponse getAllMonitoredTripsForOtpUser = mockAuthenticatedRequest(String.format("api/secure/monitoredtrip?userId=%s", + otpUserResponse.id), + apiUser, + HttpUtils.REQUEST_METHOD.GET + ); + assertEquals(HttpStatus.OK_200, getAllMonitoredTripsForOtpUser.statusCode()); + + // Request all monitored trip for an Otp user authenticating as an Api user. Without defining the user id. + getAllMonitoredTripsForOtpUser = mockAuthenticatedRequest("api/secure/monitoredtrip", + apiUser, + HttpUtils.REQUEST_METHOD.GET + ); + assertEquals(HttpStatus.BAD_REQUEST_400, getAllMonitoredTripsForOtpUser.statusCode()); + + // Plan trip with OTP proxy authenticating as an OTP user. Mock plan response will be returned. This will work // as an Otp user (created by MOD UI or an Api user) because the end point has no auth. String otpQuery = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745&userId=" + otpUserResponse.id; diff --git a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java new file mode 100644 index 000000000..d4151724d --- /dev/null +++ b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java @@ -0,0 +1,153 @@ +package org.opentripplanner.middleware; + +import com.auth0.exception.Auth0Exception; +import com.auth0.json.mgmt.users.User; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.opentripplanner.middleware.auth.Auth0Users; +import org.opentripplanner.middleware.controllers.response.ResponseList; +import org.opentripplanner.middleware.models.AdminUser; +import org.opentripplanner.middleware.models.MonitoredTrip; +import org.opentripplanner.middleware.models.OtpUser; +import org.opentripplanner.middleware.persistence.Persistence; +import org.opentripplanner.middleware.persistence.PersistenceUtil; +import org.opentripplanner.middleware.utils.HttpUtils; +import org.opentripplanner.middleware.utils.JsonUtils; + +import java.io.IOException; +import java.net.http.HttpResponse; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.opentripplanner.middleware.TestUtils.TEMP_AUTH0_USER_PASSWORD; +import static org.opentripplanner.middleware.TestUtils.isEndToEnd; +import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedPost; +import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; +import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; + +/** + * Tests to simulate getting trips as an Otp user with enhanced admin credentials. The following config parameters must + * be set in configurations/default/env.yml for these end-to-end tests to run: + * + * AUTH0_DOMAIN set to a valid Auth0 domain. + * + * AUTH0_API_CLIENT set to a valid Auth0 application client id. + * + * AUTH0_API_SECRET set to a valid Auth0 application client secret. + * + * OTP_API_ROOT set to a live OTP instance (e.g. http://otp-server.example.com/otp). + * + * OTP_PLAN_ENDPOINT set to a live OTP plan endpoint (e.g. /routers/default/plan). + * + * The following environment variable must be set for these tests to run: - RUN_E2E=true. + * + * Auth0 must be correctly configured as described here: https://auth0.com/docs/flows/call-your-api-using-resource-owner-password-flow + */ +public class GetMonitoredTripsTest { + private static AdminUser adminUser; + private static OtpUser otpUser1; + private static OtpUser otpUser2; + + /** + * Whether tests for this class should run. End to End must be enabled and Auth must NOT be disabled. This should be + * evaluated after the middleware application starts up (to ensure default disableAuth value has been applied from + * config). + */ + private static boolean testsShouldRun() { + return isEndToEnd && !isAuthDisabled(); + } + + /** + * Create Otp and Admin user accounts. Create Auth0 account for just the Otp users. If + * an Auth0 account is created for the admin user it will fail because the email address already exists. + */ + @BeforeAll + public static void setUp() throws IOException, InterruptedException { + // Load config before checking if tests should run. + OtpMiddlewareTest.setUp(); + assumeTrue(testsShouldRun()); + // Mock the OTP server TODO: Run a live OTP instance? + TestUtils.mockOtpServer(); + String email = String.format("test-%s@example.com", UUID.randomUUID().toString()); + otpUser1 = PersistenceUtil.createUser(String.format("test-%s@example.com", UUID.randomUUID().toString())); + otpUser2 = PersistenceUtil.createUser(email); + adminUser = PersistenceUtil.createAdminUser(email); + try { + User auth0User = Auth0Users.createAuth0UserForEmail(otpUser1.email, TEMP_AUTH0_USER_PASSWORD); + otpUser1.auth0UserId = auth0User.getId(); + Persistence.otpUsers.replace(otpUser1.id, otpUser1); + auth0User = Auth0Users.createAuth0UserForEmail(otpUser2.email, TEMP_AUTH0_USER_PASSWORD); + otpUser2.auth0UserId = auth0User.getId(); + Persistence.otpUsers.replace(otpUser2.id, otpUser2); + // Uncommenting will fail set-up because the email address already exists with Auth0. +// auth0User = createAuth0UserForEmail(otpUser.email, TEMP_AUTH0_USER_PASSWORD); + adminUser.auth0UserId = auth0User.getId(); + Persistence.adminUsers.replace(adminUser.id, adminUser); + } catch (Auth0Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Delete the users if they were not already deleted during the test script. + */ + @AfterAll + public static void tearDown() { + assumeTrue(testsShouldRun()); + otpUser1 = Persistence.otpUsers.getById(otpUser1.id); + if (otpUser1 != null) otpUser1.delete(false); + otpUser2 = Persistence.otpUsers.getById(otpUser2.id); + if (otpUser2 != null) otpUser2.delete(false); + adminUser = Persistence.adminUsers.getById(adminUser.id); + if (adminUser != null) adminUser.delete(); + } + + /** + * Create trips for two different Otp users and attempt to get both trips with Otp user that has 'enhanced' admin + * credentials. + */ + @Test + public void canGetOwnMonitoredTrips() { + + // Create trip as Otp user 1. + MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); + monitoredTrip.userId = otpUser1.id; + HttpResponse response = mockAuthenticatedPost("api/secure/monitoredtrip", + otpUser1, + JsonUtils.toJson(monitoredTrip) + ); + assertEquals(HttpStatus.OK_200, response.statusCode()); + + // Create trip as Otp user 2. + monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); + monitoredTrip.userId = otpUser2.id; + response = mockAuthenticatedPost("api/secure/monitoredtrip", + otpUser2, + JsonUtils.toJson(monitoredTrip) + ); + assertEquals(HttpStatus.OK_200, response.statusCode()); + + // Get trips for Otp user 2. + response = mockAuthenticatedRequest("api/secure/monitoredtrip", + otpUser2, + HttpUtils.REQUEST_METHOD.GET + ); + ResponseList tripRequests = JsonUtils.getPOJOFromJSON(response.body(), ResponseList.class); + + // Although Otp user 2 has 'enhanced' admin credentials a single trip will be returned. + assertEquals(1, tripRequests.data.size()); + + // Get trips for Otp user 2 defining user id. + response = mockAuthenticatedRequest(String.format("api/secure/monitoredtrip?userId=%s", otpUser2.id), + otpUser2, + HttpUtils.REQUEST_METHOD.GET + ); + tripRequests = JsonUtils.getPOJOFromJSON(response.body(), ResponseList.class); + + // Just the trip for Otp user 2 will be returned. + assertEquals(1, tripRequests.data.size()); + } +} diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index b8686acf1..3da444f28 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -137,6 +137,7 @@ public static void mockOtpServer() { } Service http = ignite().port(8080); http.get("/otp" + OTP_PLAN_ENDPOINT, TestUtils::mockOtpPlanResponse); + mockOtpServerSetUpIsDone = true; } /** diff --git a/src/test/java/org/opentripplanner/middleware/persistence/PersistenceUtil.java b/src/test/java/org/opentripplanner/middleware/persistence/PersistenceUtil.java index 4936ca95c..1f88f14c7 100644 --- a/src/test/java/org/opentripplanner/middleware/persistence/PersistenceUtil.java +++ b/src/test/java/org/opentripplanner/middleware/persistence/PersistenceUtil.java @@ -36,6 +36,8 @@ public static OtpUser createUser(String email, String phoneNumber) { user.email = email; user.phoneNumber = phoneNumber; user.notificationChannel = "email"; + user.hasConsentedToTerms = true; + user.storeTripHistory = true; Persistence.otpUsers.create(user); return user; } From 768597714a1fc64c1df3a77974c42796d24775b9 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 16 Oct 2020 10:22:52 +0100 Subject: [PATCH 10/30] refactor(Addressed PR feedback): Addressed PR feedback --- .../middleware/auth/Auth0Connection.java | 23 ++---- .../middleware/auth/Auth0Users.java | 2 +- .../middleware/auth/RequestingUser.java | 21 +++-- .../api/AbstractUserController.java | 13 ++- .../controllers/api/ApiController.java | 81 ++++++------------- .../controllers/api/ApiUserController.java | 20 +++-- .../controllers/api/LogController.java | 31 +++---- .../controllers/api/OtpRequestProcessor.java | 27 +++---- .../middleware/models/AdminUser.java | 4 +- .../middleware/models/Model.java | 4 +- .../middleware/models/MonitoredTrip.java | 14 ++-- .../middleware/models/OtpUser.java | 2 +- .../latest-spark-swagger-output.yaml | 20 +++++ .../opentripplanner/middleware/TestUtils.java | 2 +- 14 files changed, 114 insertions(+), 150 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 916b0e676..5649e5804 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -11,6 +11,8 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.core.JsonProcessingException; import org.eclipse.jetty.http.HttpStatus; +import org.opentripplanner.middleware.controllers.api.ApiUserController; +import org.opentripplanner.middleware.controllers.api.OtpUserController; import org.opentripplanner.middleware.models.AbstractUser; import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.models.OtpUser; @@ -25,9 +27,6 @@ import java.security.interfaces.RSAPublicKey; -import static org.opentripplanner.middleware.controllers.api.ApiUserController.API_USER_PATH; -import static org.opentripplanner.middleware.controllers.api.OtpUserController.OTP_USER_PATH; - /** * This handles verifying the Auth0 token passed in the auth header (e.g., Authorization: Bearer MY_TOKEN of Spark HTTP * requests. @@ -101,8 +100,8 @@ private static boolean isCreatingSelf(Request req, RequestingUser profile) { if (method.equalsIgnoreCase("POST")) { // Next, check that an OtpUser or ApiUser is being created (an admin must rely on another admin to create // them). - boolean creatingOtpUser = uri.endsWith(OTP_USER_PATH); - boolean creatingApiUser = uri.endsWith(API_USER_PATH); + boolean creatingOtpUser = uri.endsWith(OtpUserController.OTP_USER_PATH); + boolean creatingApiUser = uri.endsWith(ApiUserController.API_USER_PATH); if (creatingApiUser || creatingOtpUser) { // Get the correct user class depending on request path. Class userClass = creatingApiUser ? ApiUser.class : OtpUser.class; @@ -132,7 +131,7 @@ public static void checkUserIsAdmin(Request req, Response res) { checkUser(req); // Check that user object is present and is admin. RequestingUser user = Auth0Connection.getUserFromRequest(req); - if (!isUserAdmin(user)) { + if (!user.isAdmin()) { JsonUtils.logMessageAndHalt( req, HttpStatus.UNAUTHORIZED_401, @@ -141,14 +140,6 @@ public static void checkUserIsAdmin(Request req, Response res) { } } - /** - * Check if the incoming user is an admin user. To be classed as an admin user, the user must not be any other user - * type. - */ - public static boolean isUserAdmin(RequestingUser user) { - return user != null && user.adminUser != null && user.apiUser == null && user.otpUser == null; - } - /** * Add user profile to Spark Request object */ @@ -257,11 +248,11 @@ public static void isAuthorized(String userId, Request request) { // Otp user requesting their item. return; } - if (requestingUser.isThirdPartyUser() && requestingUser.apiUser.id.equals(userId)) { + if (requestingUser.isThirdParty() && requestingUser.apiUser.id.equals(userId)) { // Api user requesting their item. return; } - if (requestingUser.isThirdPartyUser()) { + if (requestingUser.isThirdParty()) { // Api user potentially requesting an item on behalf of an Otp user they created. OtpUser otpUser = Persistence.otpUsers.getById(userId); if (otpUser != null && requestingUser.apiUser.id.equals(otpUser.applicationId)) { diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index 006643b5f..19f20df1d 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -272,7 +272,7 @@ public static TokenHolder getCompleteAuth0Token(String username, String password /** * Extract from a complete Auth0 token just the access token. If the token is not available, return null instead. */ - public static String getAuth0Token(String username, String password) { + public static String getAuth0AccessToken(String username, String password) { TokenHolder token = getCompleteAuth0Token(username, password); return (token == null) ? null : token.getAccessToken(); } diff --git a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index a28d43815..5c1f6f6e0 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -2,6 +2,7 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.mongodb.client.model.Filters; import org.bson.conversions.Bson; import org.opentripplanner.middleware.models.AdminUser; import org.opentripplanner.middleware.models.ApiUser; @@ -9,9 +10,6 @@ import org.opentripplanner.middleware.persistence.Persistence; import spark.Request; -import static com.mongodb.client.model.Filters.eq; -import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthHeaderPresent; - /** * User profile that is attached to an HTTP request. */ @@ -34,7 +32,7 @@ private RequestingUser(String auth0UserId) { adminUser = new AdminUser(); } else { this.auth0UserId = auth0UserId; - Bson withAuth0UserId = eq("auth0UserId", auth0UserId); + Bson withAuth0UserId = Filters.eq("auth0UserId", auth0UserId); otpUser = Persistence.otpUsers.getOneFiltered(withAuth0UserId); apiUser = Persistence.apiUsers.getOneFiltered(withAuth0UserId); adminUser = Persistence.adminUsers.getOneFiltered(withAuth0UserId); @@ -46,7 +44,7 @@ private RequestingUser(String auth0UserId) { */ public RequestingUser(DecodedJWT jwt) { this.auth0UserId = jwt.getClaim("sub").asString(); - Bson withAuth0UserId = eq("auth0UserId", auth0UserId); + Bson withAuth0UserId = Filters.eq("auth0UserId", auth0UserId); otpUser = Persistence.otpUsers.getOneFiltered(withAuth0UserId); apiUser = Persistence.apiUsers.getOneFiltered(withAuth0UserId); adminUser = Persistence.adminUsers.getOneFiltered(withAuth0UserId); @@ -59,7 +57,7 @@ public RequestingUser(DecodedJWT jwt) { static RequestingUser createTestUser(Request req) { String auth0UserId = null; - if (isAuthHeaderPresent(req)) { + if (Auth0Connection.isAuthHeaderPresent(req)) { // If the auth header has been provided get the Auth0 user id from it. This is different from normal // operation as the parameter will only contain the Auth0 user id and not "Bearer token". auth0UserId = req.headers("Authorization"); @@ -71,7 +69,16 @@ static RequestingUser createTestUser(Request req) { /** * Determine if requesting user is a third party user. */ - public boolean isThirdPartyUser() { + public boolean isThirdParty() { return apiUser != null; } + + /** + * Check if the incoming user is an admin user. To be classed as an admin user, the user must not be any other user + * type. + */ + public boolean isAdmin() { + return adminUser != null && apiUser == null && otpUser == null; + } + } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java index d4a909131..0fd1de141 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java @@ -3,6 +3,7 @@ import com.auth0.json.mgmt.jobs.Job; import com.auth0.json.mgmt.users.User; import com.beerboy.ss.ApiEndpoint; +import com.beerboy.ss.descriptor.MethodDescriptor; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.auth.RequestingUser; @@ -10,15 +11,13 @@ import org.opentripplanner.middleware.models.AbstractUser; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.persistence.TypedPersistence; +import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.JsonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.Request; import spark.Response; -import static com.beerboy.ss.descriptor.MethodDescriptor.path; -import static org.opentripplanner.middleware.utils.HttpUtils.JSON_ONLY; - /** * Implementation of the {@link ApiController} abstract class for managing users. This controller connects with Auth0 * services using the hooks provided by {@link ApiController}. @@ -45,14 +44,14 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { // by spark as 'GET user with id "fromtoken"', which we don't want). ApiEndpoint modifiedEndpoint = baseEndpoint // Get user from token. - .get(path(ROOT_ROUTE + TOKEN_PATH) + .get(MethodDescriptor.path(ROOT_ROUTE + TOKEN_PATH) .withDescription("Retrieves an " + persistence.clazz.getSimpleName() + " entity using an Auth0 access token passed in an Authorization header.") - .withProduces(JSON_ONLY) + .withProduces(HttpUtils.JSON_ONLY) .withResponseType(persistence.clazz), this::getUserFromRequest, JsonUtils::toJson ) // Resend verification email - .get(path(ROOT_ROUTE + VERIFICATION_EMAIL_PATH) + .get(MethodDescriptor.path(ROOT_ROUTE + VERIFICATION_EMAIL_PATH) .withDescription("Triggers a job to resend the Auth0 verification email.") .withResponseType(Job.class), this::resendVerificationEmail, JsonUtils::toJson @@ -104,7 +103,7 @@ U preCreateHook(U user, Request req) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // TODO: If MOD UI is to be an ApiUser, we may want to do an additional check here to determine if this is a // first-party API user (MOD UI) or third party. - if (requestingUser.isThirdPartyUser() && user instanceof OtpUser) { + if (requestingUser.isThirdParty() && user instanceof OtpUser) { // Do not create Auth0 account for OtpUsers created on behalf of third party API users. return user; } else { diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java index 5342c5b62..68261e476 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java @@ -2,6 +2,8 @@ import com.beerboy.ss.ApiEndpoint; import com.beerboy.ss.SparkSwagger; +import com.beerboy.ss.descriptor.EndpointDescriptor; +import com.beerboy.ss.descriptor.MethodDescriptor; import com.beerboy.ss.descriptor.ParameterDescriptor; import com.beerboy.ss.rest.Endpoint; import com.fasterxml.jackson.core.JsonProcessingException; @@ -22,12 +24,6 @@ import spark.Request; import spark.Response; -import java.util.Date; - -import static com.beerboy.ss.descriptor.EndpointDescriptor.endpointPath; -import static com.beerboy.ss.descriptor.MethodDescriptor.path; -import static org.opentripplanner.middleware.utils.HttpUtils.JSON_ONLY; - /** * Generic API controller abstract class. This class provides CRUD methods using {@link spark.Spark} HTTP request * methods. This will identify the MongoDB collection on which to operate based on the provided {@link Model} class. @@ -61,6 +57,10 @@ public abstract class ApiController implements Endpoint { .withName(OFFSET_PARAM) .withDefaultValue(String.valueOf(DEFAULT_OFFSET)) .withDescription("If specified, the number of records to skip/offset.").build(); + public static final ParameterDescriptor USER_ID = ParameterDescriptor.newBuilder() + .withName(USER_ID_PARAM) + .withRequired(false) + .withDescription("If specified, the required user id.").build(); /** * @param apiPrefix string prefix to use in determining the resource location @@ -90,7 +90,8 @@ public ApiController(String apiPrefix, TypedPersistence persistence, String r @Override public void bind(final SparkSwagger restApi) { ApiEndpoint apiEndpoint = restApi.endpoint( - endpointPath(ROOT_ROUTE).withDescription("Interface for querying and managing '" + className + "' entities."), + EndpointDescriptor.endpointPath(ROOT_ROUTE) + .withDescription("Interface for querying and managing '" + className + "' entities."), HttpUtils.NO_FILTER ); buildEndpoint(apiEndpoint); @@ -122,45 +123,46 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { baseEndpoint // Get multiple entities. .get( - path(ROOT_ROUTE) + MethodDescriptor.path(ROOT_ROUTE) .withDescription("Gets a paginated list of all '" + className + "' entities.") .withQueryParam(LIMIT) .withQueryParam(OFFSET) - .withProduces(JSON_ONLY) + .withQueryParam(USER_ID) + .withProduces(HttpUtils.JSON_ONLY) .withResponseType(ResponseList.class), this::getMany, JsonUtils::toJson ) // Get one entity. .get( - path(ROOT_ROUTE + ID_PATH) + MethodDescriptor.path(ROOT_ROUTE + ID_PATH) .withDescription("Returns the '" + className + "' entity with the specified id, or 404 if not found.") .withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The id of the entity to search.").and() // .withResponses(...) // FIXME: not implemented (requires source change). - .withProduces(JSON_ONLY) + .withProduces(HttpUtils.JSON_ONLY) .withResponseType(clazz), this::getEntityForId, JsonUtils::toJson ) // Create entity request .post( - path("") + MethodDescriptor.path("") .withDescription("Creates a '" + className + "' entity.") - .withConsumes(JSON_ONLY) + .withConsumes(HttpUtils.JSON_ONLY) .withRequestType(clazz) - .withProduces(JSON_ONLY) + .withProduces(HttpUtils.JSON_ONLY) .withResponseType(clazz), this::createOrUpdate, JsonUtils::toJson ) // Update entity request .put( - path(ID_PATH) + MethodDescriptor.path(ID_PATH) .withDescription("Updates and returns the '" + className + "' entity with the specified id, or 404 if not found.") .withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The id of the entity to update.").and() - .withConsumes(JSON_ONLY) + .withConsumes(HttpUtils.JSON_ONLY) .withRequestType(clazz) - .withProduces(JSON_ONLY) + .withProduces(HttpUtils.JSON_ONLY) // FIXME: `withResponses` is supposed to document the expected HTTP responses (200, 403, 404)... // but that doesn't appear to be implemented in spark-swagger. // .withResponses(...) @@ -170,10 +172,10 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { // Delete entity request .delete( - path(ID_PATH) + MethodDescriptor.path(ID_PATH) .withDescription("Deletes the '" + className + "' entity with the specified id if it exists.") .withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The id of the entity to delete.").and() - .withProduces(JSON_ONLY) + .withProduces(HttpUtils.JSON_ONLY) .withResponseType(clazz), this::deleteOne, JsonUtils::toJson ); @@ -194,7 +196,7 @@ private ResponseList getMany(Request req, Response res) { } // If the user id is not provided filter response based on requesting user. RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); - if (Auth0Connection.isUserAdmin(requestingUser)) { + if (requestingUser.isAdmin()) { // If the user is admin, the context is presumed to be the admin dashboard, so we deliver all entities for // management or review without restriction. return persistence.getResponseList(offset, limit); @@ -203,7 +205,7 @@ private ResponseList getMany(Request req, Response res) { // OtpUserController. Therefore, the request should be limited to return just the entity matching the // requesting user. return persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit); - } else if (requestingUser.isThirdPartyUser()) { + } else if (requestingUser.isThirdParty()) { // A user id must be provided if the request is being made by a third party user. JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, @@ -215,43 +217,6 @@ private ResponseList getMany(Request req, Response res) { return persistence.getResponseList(Filters.eq(USER_ID_PARAM, requestingUser.otpUser.id), offset, limit); } } -// private ResponseList getMany(Request req, Response res) { -// int limit = HttpUtils.getQueryParamFromRequest(req, LIMIT_PARAM, 0, DEFAULT_LIMIT, 100); -// int offset = HttpUtils.getQueryParamFromRequest(req, OFFSET_PARAM, 0, DEFAULT_OFFSET); -// String userId = HttpUtils.getQueryParamFromRequest(req, USER_ID_PARAM, true); -// // Filter the response based on the user id, if provided. -// if (userId != null) { -// // Define the correct field name depending on the entities being requested. If one of the user classes is -// // being requested, this will limit the response to a single entity. -// String fieldName = (persistence.clazz == OtpUser.class || -// persistence.clazz == ApiUser.class || -// persistence.clazz == AdminUser.class) -// ? "_id" : "userId"; -// return persistence.getResponseList(Filters.eq(fieldName, userId), offset, limit); -// } -// // If the user id is not provided filter response based on requesting user. -// RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); -// if (Auth0Connection.isUserAdmin(requestingUser)) { -// // If the user is admin, the context is presumed to be the admin dashboard, so we deliver all entities for -// // management or review without restriction. -// return persistence.getResponseList(offset, limit); -// } else if (persistence.clazz == OtpUser.class) { -// // If the required entity is of type 'OtpUser' the assumption is that a call is being made via the -// // OtpUserController. Therefore, the request should be limited to return just the entity matching the -// // requesting user. -// return persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit); -// } else if (requestingUser.isThirdPartyUser()) { -// // A user id must be provided if the request is being made by a third party user. -// JsonUtils.logMessageAndHalt(req, -// HttpStatus.BAD_REQUEST_400, -// String.format("The parameter name (%s) must be provided.", USER_ID_PARAM)); -// return null; -// } else { -// // For all other cases the assumption is that the request is being made by an Otp user and the requested -// // entities have a 'userId' parameter. Only entities that match the requesting user id are returned. -// return persistence.getResponseList(Filters.eq("userId", requestingUser.otpUser.id), offset, limit); -// } -// } /** * HTTP endpoint to get one entity specified by ID. This will return an object based on the checks carried out in diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java index 9a1bbfb5b..bf97875c9 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java @@ -2,6 +2,7 @@ import com.auth0.json.auth.TokenHolder; import com.beerboy.ss.ApiEndpoint; +import com.beerboy.ss.descriptor.MethodDescriptor; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.auth.Auth0Users; @@ -20,9 +21,6 @@ import spark.Request; import spark.Response; -import static com.beerboy.ss.descriptor.MethodDescriptor.path; -import static org.opentripplanner.middleware.utils.HttpUtils.JSON_ONLY; - /** * Implementation of the {@link AbstractUserController} for {@link ApiUser}. This controller also contains methods for * managing an {@link ApiUser}'s API keys. @@ -49,35 +47,35 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { LOG.info("Registering path {}.", ROOT_ROUTE + ID_PATH + API_KEY_PATH); ApiEndpoint modifiedEndpoint = baseEndpoint // Create API key - .post(path(ID_PATH + API_KEY_PATH) + .post(MethodDescriptor.path(ID_PATH + API_KEY_PATH) .withDescription("Creates API key for ApiUser (with optional AWS API Gateway usage plan ID).") .withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The user ID") .and() .withQueryParam().withName("usagePlanId").withDescription("Optional AWS API Gateway usage plan ID.") .and() - .withProduces(JSON_ONLY) + .withProduces(HttpUtils.JSON_ONLY) .withResponseType(persistence.clazz), this::createApiKeyForApiUser, JsonUtils::toJson ) // Delete API key - .delete(path(ID_PATH + API_KEY_PATH + API_KEY_ID_PARAM) + .delete(MethodDescriptor.path(ID_PATH + API_KEY_PATH + API_KEY_ID_PARAM) .withDescription("Deletes API key for ApiUser.") .withPathParam().withName(ID_PARAM).withDescription("The user ID.") .and() .withPathParam().withName("apiKeyId").withDescription("The ID of the API key.") .and() - .withProduces(JSON_ONLY) + .withProduces(HttpUtils.JSON_ONLY) .withResponseType(persistence.clazz), this::deleteApiKeyForApiUser, JsonUtils::toJson) // Authenticate user with Auth0 - .post(path(ID_PATH + AUTHENTICATE_PATH) + .post(MethodDescriptor.path(ID_PATH + AUTHENTICATE_PATH) .withDescription("Authenticates ApiUser with Auth0.") .withPathParam().withName(ID_PARAM).withDescription("The user ID.").and() .withQueryParam().withName(USERNAME_PARAM).withRequired(true) .withDescription("Auth0 username (usually email address).").and() .withQueryParam().withName(PASSWORD_PARAM).withRequired(true) .withDescription("Auth0 password.").and() - .withProduces(JSON_ONLY) + .withProduces(HttpUtils.JSON_ONLY) .withResponseType(TokenHolder.class), this::authenticateAuth0User, JsonUtils::toJson ); @@ -117,7 +115,7 @@ private ApiUser createApiKeyForApiUser(Request req, Response res) { String usagePlanId = req.queryParamOrDefault("usagePlanId", DEFAULT_USAGE_PLAN_ID); // If requester is not an admin user, force the usage plan ID to the default and enforce key limit. A non-admin // user should not be able to create an API key for any usage plan. - if (!Auth0Connection.isUserAdmin(requestingUser)) { + if (!requestingUser.isAdmin()) { usagePlanId = DEFAULT_USAGE_PLAN_ID; if (targetUser.apiKeys.size() >= API_KEY_LIMIT_PER_USER) { JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "User has reached API key limit."); @@ -145,7 +143,7 @@ private ApiUser createApiKeyForApiUser(Request req, Response res) { private ApiUser deleteApiKeyForApiUser(Request req, Response res) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // Do not permit key deletion unless user is an admin. - if (!Auth0Connection.isUserAdmin(requestingUser)) { + if (!requestingUser.isAdmin()) { JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to delete an API key."); } ApiUser targetUser = getApiUser(req); diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java index 05a1fe926..5f2586b34 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java @@ -2,6 +2,8 @@ import com.amazonaws.services.apigateway.model.GetUsageResult; import com.beerboy.ss.SparkSwagger; +import com.beerboy.ss.descriptor.EndpointDescriptor; +import com.beerboy.ss.descriptor.MethodDescriptor; import com.beerboy.ss.rest.Endpoint; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; @@ -21,13 +23,6 @@ import java.util.List; import java.util.stream.Collectors; -import static com.beerboy.ss.descriptor.EndpointDescriptor.endpointPath; -import static com.beerboy.ss.descriptor.MethodDescriptor.path; -import static org.opentripplanner.middleware.auth.Auth0Connection.isUserAdmin; -import static org.opentripplanner.middleware.utils.DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN; -import static org.opentripplanner.middleware.utils.HttpUtils.JSON_ONLY; -import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; - /** * Sets up HTTP endpoints for getting logging and request summary information from AWS Cloudwatch and API Gateway. */ @@ -45,30 +40,30 @@ public LogController(String apiPrefix) { @Override public void bind(final SparkSwagger restApi) { restApi.endpoint( - endpointPath(ROOT_ROUTE).withDescription("Interface for retrieving API logs from AWS."), + EndpointDescriptor.endpointPath(ROOT_ROUTE).withDescription("Interface for retrieving API logs from AWS."), HttpUtils.NO_FILTER - ).get(path(ROOT_ROUTE) + ).get(MethodDescriptor.path(ROOT_ROUTE) .withDescription("Gets a list of all API usage logs.") .withQueryParam() .withName("keyId") .withDescription("If specified, restricts the search to the specified AWS API key ID.").and() .withQueryParam() .withName("startDate") - .withPattern(DEFAULT_DATE_FORMAT_PATTERN) + .withPattern(DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN) .withDefaultValue("30 days prior to the current date") .withDescription(String.format( "If specified, the earliest date (format %s) for which usage logs are retrieved.", - DEFAULT_DATE_FORMAT_PATTERN + DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN )).and() .withQueryParam() .withName("endDate") - .withPattern(DEFAULT_DATE_FORMAT_PATTERN) + .withPattern(DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN) .withDefaultValue("The current date") .withDescription(String.format( "If specified, the latest date (format %s) for which usage logs are retrieved.", - DEFAULT_DATE_FORMAT_PATTERN + DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN )).and() - .withProduces(JSON_ONLY) + .withProduces(HttpUtils.JSON_ONLY) // Note: unlike what the name suggests, withResponseAsCollection does not generate an array // as the return type for this method. (It does generate the type for that class nonetheless.) .withResponseAsCollection(ApiUsageResult.class), @@ -84,9 +79,9 @@ private static List getUsageLogs(Request req, Response res) { List apiKeys = getApiKeyIdsFromRequest(req); RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // If the user is not an admin, the list of API keys is defaulted to their keys. - if (!isUserAdmin(requestingUser)) { + if (!requestingUser.isAdmin()) { if (requestingUser.apiUser == null) { - logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Action is not permitted for user."); + JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Action is not permitted for user."); return null; } apiKeys = requestingUser.apiUser.apiKeys; @@ -98,7 +93,7 @@ private static List getUsageLogs(Request req, Response res) { LocalDateTime now = DateTimeUtils.nowAsLocalDateTime(); // TODO: Future work might modify this so that we accept multiple API key IDs for a single request (depends on // how third party developer accounts are structured). - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT_PATTERN); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN); String startDate = req.queryParamOrDefault("startDate", formatter.format(now.minusDays(30))); String endDate = req.queryParamOrDefault("endDate", formatter.format(now)); try { @@ -114,7 +109,7 @@ private static List getUsageLogs(Request req, Response res) { .collect(Collectors.toList()); } catch (Exception e) { // Catch any issues with bad request parameters (e.g., invalid API keyId or bad date format). - logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Error requesting usage results", e); + JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Error requesting usage results", e); } return null; diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index 29d785e34..7ad2ceebe 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -1,6 +1,8 @@ package org.opentripplanner.middleware.controllers.api; import com.beerboy.ss.SparkSwagger; +import com.beerboy.ss.descriptor.EndpointDescriptor; +import com.beerboy.ss.descriptor.MethodDescriptor; import com.beerboy.ss.rest.Endpoint; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; @@ -19,16 +21,9 @@ import org.slf4j.LoggerFactory; import spark.Request; +import javax.ws.rs.core.MediaType; import java.util.List; -import static com.beerboy.ss.descriptor.EndpointDescriptor.endpointPath; -import static com.beerboy.ss.descriptor.MethodDescriptor.path; -import static javax.ws.rs.core.MediaType.APPLICATION_JSON; -import static javax.ws.rs.core.MediaType.APPLICATION_XML; -import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthHeaderPresent; -import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_API_ROOT; -import static org.opentripplanner.middleware.otp.OtpDispatcher.OTP_PLAN_ENDPOINT; - /** * Responsible for getting a response from OTP based on the parameters provided by the requester. If the target service * is of interest the response is intercepted and processed. In all cases, the response from OTP (content and HTTP @@ -60,12 +55,12 @@ public class OtpRequestProcessor implements Endpoint { @Override public void bind(final SparkSwagger restApi) { restApi.endpoint( - endpointPath(OTP_PROXY_ENDPOINT).withDescription("Proxy interface for OTP endpoints. " + OTP_DOC_LINK), + EndpointDescriptor.endpointPath(OTP_PROXY_ENDPOINT).withDescription("Proxy interface for OTP endpoints. " + OTP_DOC_LINK), HttpUtils.NO_FILTER ).get( - path("/*") + MethodDescriptor.path("/*") .withDescription("Forwards any GET request to OTP. " + OTP_DOC_LINK) - .withProduces(List.of(APPLICATION_JSON, APPLICATION_XML)), + .withProduces(List.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)), OtpRequestProcessor::proxy ); } @@ -77,7 +72,7 @@ public void bind(final SparkSwagger restApi) { * status) is passed back to the requester. */ private static String proxy(Request request, spark.Response response) { - if (OTP_API_ROOT == null) { + if (OtpDispatcher.OTP_API_ROOT == null) { JsonUtils.logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No OTP Server provided, check config."); return null; } @@ -92,10 +87,10 @@ private static String proxy(Request request, spark.Response response) { } // If the request path ends with the plan endpoint (e.g., '/plan' or '/default/plan'), process response. - if (otpRequestPath.endsWith(OTP_PLAN_ENDPOINT)) handlePlanTripResponse(request, otpDispatcherResponse); + if (otpRequestPath.endsWith(OtpDispatcher.OTP_PLAN_ENDPOINT)) handlePlanTripResponse(request, otpDispatcherResponse); // provide response to requester as received from OTP server - response.type(APPLICATION_JSON); + response.type(MediaType.APPLICATION_JSON); response.status(otpDispatcherResponse.statusCode); return otpDispatcherResponse.responseBody; } @@ -108,7 +103,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons // If the Auth header is present, this indicates that the request was made by a logged in user. If present // we should store trip history (but we verify this preference before doing so). - if (!isAuthHeaderPresent(request)) { + if (!Auth0Connection.isAuthHeaderPresent(request)) { LOG.debug("Anonymous user, trip history not stored"); return; } @@ -130,7 +125,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons // Determine if the Otp request is being made by an actual Otp user or by a third party on behalf of an Otp user. OtpUser otpUser; - if (requestingUser.isThirdPartyUser()) { + if (requestingUser.isThirdParty()) { // Api user making a trip request on behalf of an Otp user. In this case, the Otp user id must be provided // as a query parameter. String userId = request.queryParams("userId"); diff --git a/src/main/java/org/opentripplanner/middleware/models/AdminUser.java b/src/main/java/org/opentripplanner/middleware/models/AdminUser.java index b3b2e27e8..f3d92692e 100644 --- a/src/main/java/org/opentripplanner/middleware/models/AdminUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/AdminUser.java @@ -6,8 +6,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.opentripplanner.middleware.auth.Auth0Connection.isUserAdmin; - /** * Represents an administrative user of the OTP Admin Dashboard (otp-admin-ui). */ @@ -32,7 +30,7 @@ public AdminUser() { */ @Override public boolean canBeCreatedBy(RequestingUser user) { - return isUserAdmin(user); + return user.isAdmin(); } @Override diff --git a/src/main/java/org/opentripplanner/middleware/models/Model.java b/src/main/java/org/opentripplanner/middleware/models/Model.java index 577779bb9..38fb70447 100644 --- a/src/main/java/org/opentripplanner/middleware/models/Model.java +++ b/src/main/java/org/opentripplanner/middleware/models/Model.java @@ -8,8 +8,6 @@ import java.util.Objects; import java.util.UUID; -import static org.opentripplanner.middleware.auth.Auth0Connection.isUserAdmin; - public class Model implements Serializable { private static final long serialVersionUID = 1L; @@ -38,7 +36,7 @@ public boolean canBeCreatedBy(RequestingUser user) { */ public boolean canBeManagedBy(RequestingUser user) { // TODO: Check if user has application administrator permission? - return isUserAdmin(user); + return user.isAdmin(); } @Override diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index e2cddfc9f..682975f1c 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -1,6 +1,7 @@ package org.opentripplanner.middleware.models; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.mongodb.client.model.Filters; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import com.mongodb.client.FindIterable; @@ -16,15 +17,12 @@ import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.time.DayOfWeek; import java.time.ZonedDateTime; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import static com.mongodb.client.model.Filters.eq; -import static java.nio.charset.StandardCharsets.UTF_8; - /** * A monitored trip represents a trip a user would like to receive notification on if affected by a delay and/or route * change. @@ -245,13 +243,13 @@ public boolean canBeManagedBy(RequestingUser requestingUser) { if (belongsToUser) { return true; - } else if (requestingUser.isThirdPartyUser()) { + } else if (requestingUser.isThirdParty()) { // get the required OTP user to confirm they are associated with the requesting API user. OtpUser otpUser = Persistence.otpUsers.getById(userId); if (otpUser != null && requestingUser.apiUser.id.equals(otpUser.applicationId)) { return true; } - } else if (requestingUser.adminUser != null) { + } else if (requestingUser.isAdmin()) { // If not managing self, user must have manage permission. for (Permission permission : requestingUser.adminUser.permissions) { if (permission.canManage(this.getClass())) return true; @@ -262,7 +260,7 @@ public boolean canBeManagedBy(RequestingUser requestingUser) { } private Bson tripIdFilter() { - return eq("monitoredTripId", this.id); + return Filters.eq("monitoredTripId", this.id); } /** @@ -322,7 +320,7 @@ public boolean delete() { public Map parseQueryParams() throws URISyntaxException { return URLEncodedUtils.parse( new URI(String.format("http://example.com/plan?%s", queryParams)), - UTF_8 + StandardCharsets.UTF_8 ).stream().collect(Collectors.toMap(NameValuePair::getName, NameValuePair::getValue)); } diff --git a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java index f71d7f895..4c35bc809 100644 --- a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java @@ -86,7 +86,7 @@ public boolean delete(boolean deleteAuth0User) { */ @Override public boolean canBeManagedBy(RequestingUser requestingUser) { - if (requestingUser.isThirdPartyUser() && requestingUser.apiUser.id.equals(applicationId)) { + if (requestingUser.isThirdParty() && requestingUser.apiUser.id.equals(applicationId)) { // Otp user was created by this Api user. return true; } diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml index dc78f1f68..80d22865d 100644 --- a/src/main/resources/latest-spark-swagger-output.yaml +++ b/src/main/resources/latest-spark-swagger-output.yaml @@ -80,6 +80,11 @@ paths: required: false type: "string" default: "0" + - name: "userId" + in: "query" + description: "If specified, the required user id." + required: false + type: "string" responses: "200": description: "successful operation" @@ -285,6 +290,11 @@ paths: required: false type: "string" default: "0" + - name: "userId" + in: "query" + description: "If specified, the required user id." + required: false + type: "string" responses: "200": description: "successful operation" @@ -390,6 +400,11 @@ paths: required: false type: "string" default: "0" + - name: "userId" + in: "query" + description: "If specified, the required user id." + required: false + type: "string" responses: "200": description: "successful operation" @@ -604,6 +619,11 @@ paths: required: false type: "string" default: "0" + - name: "userId" + in: "query" + description: "If specified, the required user id." + required: false + type: "string" responses: "200": description: "successful operation" diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index c53d6db36..e00d92037 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -124,7 +124,7 @@ private static HashMap getMockHeaders(AbstractUser requestingUse } // Otherwise, get a valid oauth token for the user - headers.put("Authorization", "Bearer " + Auth0Users.getAuth0Token(requestingUser.email, TEMP_AUTH0_USER_PASSWORD)); + headers.put("Authorization", "Bearer " + Auth0Users.getAuth0AccessToken(requestingUser.email, TEMP_AUTH0_USER_PASSWORD)); return headers; } From d94f0e57ed711bb28038671023e92e29929eb0a2 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 16 Oct 2020 16:20:53 +0100 Subject: [PATCH 11/30] refactor(Addressed PR feedback): Minor chanages to GetMonitoredTripsTest and the definition of admin --- .../middleware/auth/RequestingUser.java | 5 ++--- .../middleware/GetMonitoredTripsTest.java | 12 +++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index 5c1f6f6e0..d19a350f7 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -74,11 +74,10 @@ public boolean isThirdParty() { } /** - * Check if the incoming user is an admin user. To be classed as an admin user, the user must not be any other user - * type. + * Check if the incoming user is an admin user. */ public boolean isAdmin() { - return adminUser != null && apiUser == null && otpUser == null; + return adminUser != null; } } diff --git a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java index fa0599875..998f0c463 100644 --- a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java +++ b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java @@ -77,14 +77,15 @@ public static void setUp() throws IOException, InterruptedException { otpUser2 = PersistenceUtil.createUser(email); adminUser = PersistenceUtil.createAdminUser(email); try { + // Should use Auth0User.createNewAuth0User but this generates a random password preventing the mock headers + // from being able to use TEMP_AUTH0_USER_PASSWORD. User auth0User = Auth0Users.createAuth0UserForEmail(otpUser1.email, TEMP_AUTH0_USER_PASSWORD); otpUser1.auth0UserId = auth0User.getId(); Persistence.otpUsers.replace(otpUser1.id, otpUser1); - auth0User = Auth0Users.createAuth0UserForEmail(otpUser2.email, TEMP_AUTH0_USER_PASSWORD); + auth0User = Auth0Users.createAuth0UserForEmail(email, TEMP_AUTH0_USER_PASSWORD); otpUser2.auth0UserId = auth0User.getId(); Persistence.otpUsers.replace(otpUser2.id, otpUser2); - // Uncommenting will fail set-up because the email address already exists with Auth0. -// auth0User = createAuth0UserForEmail(otpUser.email, TEMP_AUTH0_USER_PASSWORD); + // Use the same Auth0 user id as otpUser2 as the email address is the same. adminUser.auth0UserId = auth0User.getId(); Persistence.adminUsers.replace(adminUser.id, adminUser); } catch (Auth0Exception e) { @@ -138,8 +139,9 @@ public void canGetOwnMonitoredTrips() throws URISyntaxException { ); ResponseList tripRequests = JsonUtils.getPOJOFromJSON(response.body(), ResponseList.class); - // Although Otp user 2 has 'enhanced' admin credentials a single trip will be returned. - assertEquals(1, tripRequests.data.size()); + // Otp user 2 has 'enhanced' admin credentials both trips will be returned. The expectation here is that the UI + // will always provide the user id to prevent this (as with the next test). + assertEquals(2, tripRequests.data.size()); // Get trips for Otp user 2 defining user id. response = mockAuthenticatedRequest(String.format("api/secure/monitoredtrip?userId=%s", otpUser2.id), From 7e54d2870fa41ce23dd698ad0bb8fa32579c48f0 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 16 Oct 2020 16:57:22 +0100 Subject: [PATCH 12/30] refactor(Created issue with API key): An API key was added to the abstract user class for testing. T --- .../opentripplanner/middleware/models/AbstractUser.java | 6 ------ .../java/org/opentripplanner/middleware/TestUtils.java | 7 ++++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java b/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java index 0cfe9a924..7164e3491 100644 --- a/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/AbstractUser.java @@ -31,12 +31,6 @@ public abstract class AbstractUser extends Model { */ public String auth0UserId = UUID.randomUUID().toString(); - /** - * Random api key for testing. - */ - public String apiKey = UUID.randomUUID().toString(); - - /** Whether a user is also a Data Tools user */ public boolean isDataToolsUser; /** diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index e00d92037..9425ae109 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -41,6 +41,11 @@ public class TestUtils { */ static final String TEMP_AUTH0_USER_PASSWORD = UUID.randomUUID().toString(); + /** + * x-api-key used when auth is disabled. + */ + static final String TEMP_X_API_KEY = UUID.randomUUID().toString(); + /** * Whether the end-to-end environment variable is enabled. */ @@ -102,7 +107,7 @@ private static HashMap getMockHeaders(AbstractUser requestingUse // the request when received. if (isAuthDisabled()) { headers.put("Authorization", requestingUser.auth0UserId); - headers.put("x-api-key", requestingUser.apiKey); + headers.put("x-api-key", TEMP_X_API_KEY); return headers; } From 6e2df1501aeda0c8debd24b3e320f4ac565910a9 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Mon, 19 Oct 2020 19:05:04 +0100 Subject: [PATCH 13/30] refactor(Addressed PR feedback): Addressed PR feedback --- .../middleware/OtpMiddlewareMain.java | 6 +- .../middleware/auth/Auth0Connection.java | 8 +- .../middleware/auth/Auth0Users.java | 3 +- .../middleware/auth/RequestingUser.java | 14 +++- .../api/AbstractUserController.java | 2 +- .../controllers/api/ApiController.java | 2 +- .../controllers/api/OtpRequestProcessor.java | 20 +++-- .../middleware/models/MonitoredTrip.java | 6 +- .../middleware/models/OtpUser.java | 2 +- .../latest-spark-swagger-output.yaml | 8 +- .../middleware/ApiKeyManagementTest.java | 4 +- .../middleware/ApiUserFlowTest.java | 79 +++++++++++++------ .../middleware/GetMonitoredTripsTest.java | 21 +++-- .../opentripplanner/middleware/TestUtils.java | 35 ++++---- 14 files changed, 130 insertions(+), 80 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java b/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java index 7c19889e6..e0a26fae5 100644 --- a/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java +++ b/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java @@ -131,9 +131,11 @@ private static void initializeHttpEndpoints() throws IOException, InterruptedExc return "OK"; }); - // Security checks for admin and /secure/ endpoints. + // Security checks for admin and /secure/ endpoints. Excluding /authenticate so that API users can obtain a + // bearer token to authenticate against all other /secure/ endpoints. spark.before(API_PREFIX + "/secure/*", ((request, response) -> { - if (!request.requestMethod().equals("OPTIONS")) Auth0Connection.checkUser(request); + if (!request.requestMethod().equals("OPTIONS") && !request.pathInfo().endsWith("/authenticate")) + Auth0Connection.checkUser(request); })); spark.before(API_PREFIX + "admin/*", ((request, response) -> { if (!request.requestMethod().equals("OPTIONS")) { diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 5649e5804..d4197a161 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -244,18 +244,18 @@ public static void isAuthorized(String userId, Request request) { } // If userId is defined, it must be set to a value associated with a user. if (userId != null) { - if (requestingUser.otpUser != null && requestingUser.otpUser.id.equals(userId)) { + if (requestingUser.isFirstPartyUser() && requestingUser.otpUser.id.equals(userId)) { // Otp user requesting their item. return; } - if (requestingUser.isThirdParty() && requestingUser.apiUser.id.equals(userId)) { + if (requestingUser.isThirdPartyUser() && requestingUser.apiUser.id.equals(userId)) { // Api user requesting their item. return; } - if (requestingUser.isThirdParty()) { + if (requestingUser.isThirdPartyUser()) { // Api user potentially requesting an item on behalf of an Otp user they created. OtpUser otpUser = Persistence.otpUsers.getById(userId); - if (otpUser != null && requestingUser.apiUser.id.equals(otpUser.applicationId)) { + if (otpUser != null && otpUser.canBeManagedBy(requestingUser)) { return; } } diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index 19f20df1d..e84914955 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -242,8 +242,7 @@ private static String getAuth0Url() { /** * Get an Auth0 oauth token for use in mocking user requests by using the Auth0 'Call Your API Using Resource Owner * Password Flow' approach. Auth0 setup can be reviewed here: https://auth0.com/docs/flows/call-your-api-using-resource-owner-password-flow. - * If the user is successfully validated by Auth0 a complete token is returned, which is extracted and returned - * to the caller. In all other cases, null is returned. + * If the user is successfully validated by Auth0 a complete token is returned. In all other cases, null is returned. */ public static TokenHolder getCompleteAuth0Token(String username, String password) { if (Auth0Connection.isAuthDisabled()) return null; diff --git a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index d19a350f7..25c033a4d 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -67,12 +67,22 @@ static RequestingUser createTestUser(Request req) { } /** - * Determine if requesting user is a third party user. + * Determine if requesting user is a third party user. A third party user is classed as a user coming via the AWS + * API gateway. */ - public boolean isThirdParty() { + public boolean isThirdPartyUser() { + // TODO: Look to enhance api user check. Perhaps define specific field to indicate this? return apiUser != null; } + /** + * Determine if requesting user is a first party user. A first party user is a user coming directly from MOD UI. + */ + public boolean isFirstPartyUser() { + // TODO: Look to enhance otp user check. Perhaps define specific field to indicate this? + return otpUser != null; + } + /** * Check if the incoming user is an admin user. */ diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java index 0fd1de141..3275a1ad1 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java @@ -103,7 +103,7 @@ U preCreateHook(U user, Request req) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // TODO: If MOD UI is to be an ApiUser, we may want to do an additional check here to determine if this is a // first-party API user (MOD UI) or third party. - if (requestingUser.isThirdParty() && user instanceof OtpUser) { + if (requestingUser.isThirdPartyUser() && user instanceof OtpUser) { // Do not create Auth0 account for OtpUsers created on behalf of third party API users. return user; } else { diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java index 68261e476..7c43add66 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java @@ -205,7 +205,7 @@ private ResponseList getMany(Request req, Response res) { // OtpUserController. Therefore, the request should be limited to return just the entity matching the // requesting user. return persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit); - } else if (requestingUser.isThirdParty()) { + } else if (requestingUser.isThirdPartyUser()) { // A user id must be provided if the request is being made by a third party user. JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index 7ad2ceebe..efd18386f 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -3,6 +3,7 @@ import com.beerboy.ss.SparkSwagger; import com.beerboy.ss.descriptor.EndpointDescriptor; import com.beerboy.ss.descriptor.MethodDescriptor; +import com.beerboy.ss.descriptor.ParameterDescriptor; import com.beerboy.ss.rest.Endpoint; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; @@ -31,6 +32,7 @@ */ public class OtpRequestProcessor implements Endpoint { private static final Logger LOG = LoggerFactory.getLogger(OtpRequestProcessor.class); + private static final String USER_ID_PARAM = "userId"; /** * Endpoint for the OTP Middleware's OTP proxy @@ -54,12 +56,17 @@ public class OtpRequestProcessor implements Endpoint { */ @Override public void bind(final SparkSwagger restApi) { + ParameterDescriptor USER_ID = ParameterDescriptor.newBuilder() + .withName(USER_ID_PARAM) + .withRequired(false) + .withDescription("If a third party user is making a trip request on behalf of an OTP user, the OTP user id must be specified.").build(); restApi.endpoint( EndpointDescriptor.endpointPath(OTP_PROXY_ENDPOINT).withDescription("Proxy interface for OTP endpoints. " + OTP_DOC_LINK), HttpUtils.NO_FILTER ).get( MethodDescriptor.path("/*") .withDescription("Forwards any GET request to OTP. " + OTP_DOC_LINK) + .withQueryParam(USER_ID) .withProduces(List.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)), OtpRequestProcessor::proxy ); @@ -124,12 +131,14 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons } // Determine if the Otp request is being made by an actual Otp user or by a third party on behalf of an Otp user. - OtpUser otpUser; - if (requestingUser.isThirdParty()) { + OtpUser otpUser = null; + if (requestingUser.isFirstPartyUser()) { + // Otp user making a trip request for self. + otpUser = requestingUser.otpUser; + } else if (requestingUser.isThirdPartyUser()) { // Api user making a trip request on behalf of an Otp user. In this case, the Otp user id must be provided // as a query parameter. - String userId = request.queryParams("userId"); - otpUser = Persistence.otpUsers.getById(userId); + otpUser = Persistence.otpUsers.getById(request.queryParams(USER_ID_PARAM)); if (otpUser != null && !otpUser.canBeManagedBy(requestingUser)) { JsonUtils.logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, @@ -137,9 +146,6 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons requestingUser.apiUser.email, otpUser.email)); } - } else { - // Otp user making a trip request for self. - otpUser = requestingUser.otpUser; } final boolean storeTripHistory = otpUser != null && otpUser.storeTripHistory; diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index 682975f1c..5653dbaba 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -237,16 +237,16 @@ public boolean canBeManagedBy(RequestingUser requestingUser) { // OTP user is assigned to that API. boolean belongsToUser = false; // Monitored trip can only be owned by an OtpUser (not an ApiUser or AdminUser). - if (requestingUser.otpUser != null) { + if (requestingUser.isFirstPartyUser()) { belongsToUser = userId.equals(requestingUser.otpUser.id); } if (belongsToUser) { return true; - } else if (requestingUser.isThirdParty()) { + } else if (requestingUser.isThirdPartyUser()) { // get the required OTP user to confirm they are associated with the requesting API user. OtpUser otpUser = Persistence.otpUsers.getById(userId); - if (otpUser != null && requestingUser.apiUser.id.equals(otpUser.applicationId)) { + if (otpUser != null && otpUser.canBeManagedBy(requestingUser)) { return true; } } else if (requestingUser.isAdmin()) { diff --git a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java index 4c35bc809..f71d7f895 100644 --- a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java @@ -86,7 +86,7 @@ public boolean delete(boolean deleteAuth0User) { */ @Override public boolean canBeManagedBy(RequestingUser requestingUser) { - if (requestingUser.isThirdParty() && requestingUser.apiUser.id.equals(applicationId)) { + if (requestingUser.isThirdPartyUser() && requestingUser.apiUser.id.equals(applicationId)) { // Otp user was created by this Api user. return true; } diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml index 80d22865d..3d68c7fe0 100644 --- a/src/main/resources/latest-spark-swagger-output.yaml +++ b/src/main/resources/latest-spark-swagger-output.yaml @@ -778,7 +778,13 @@ paths: produces: - "application/json" - "application/xml" - parameters: [] + parameters: + - name: "userId" + in: "query" + description: "If a third party user is making a trip request on behalf of\ + \ an OTP user, the OTP user id must be specified." + required: false + type: "string" responses: "200": description: "successful operation" diff --git a/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java b/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java index 58f94ece1..cabdfe5c5 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java @@ -161,7 +161,7 @@ private boolean ensureApiKeyExists() { */ private HttpResponse createApiKeyRequest(String targetUserId, AbstractUser requestingUser) { String path = String.format("api/secure/application/%s/apikey", targetUserId); - return mockAuthenticatedRequest(path, requestingUser, HttpUtils.REQUEST_METHOD.POST); + return mockAuthenticatedRequest(path, "", requestingUser, HttpUtils.REQUEST_METHOD.POST, true); } /** @@ -169,6 +169,6 @@ private HttpResponse createApiKeyRequest(String targetUserId, AbstractUs */ private static HttpResponse deleteApiKeyRequest(String targetUserId, String apiKeyId, AbstractUser requestingUser) { String path = String.format("api/secure/application/%s/apikey/%s", targetUserId, apiKeyId); - return mockAuthenticatedRequest(path, requestingUser, HttpUtils.REQUEST_METHOD.DELETE); + return mockAuthenticatedRequest(path, "", requestingUser, HttpUtils.REQUEST_METHOD.DELETE, true); } } diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index f6941cea6..9ed945d05 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -1,6 +1,7 @@ package org.opentripplanner.middleware; import com.auth0.exception.Auth0Exception; +import com.auth0.json.auth.TokenHolder; import com.auth0.json.mgmt.users.User; import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.AfterAll; @@ -22,6 +23,7 @@ import java.io.IOException; import java.net.URISyntaxException; import java.net.http.HttpResponse; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.UUID; @@ -29,8 +31,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.opentripplanner.middleware.TestUtils.TEMP_AUTH0_USER_PASSWORD; +import static org.opentripplanner.middleware.TestUtils.authenticatedRequest; import static org.opentripplanner.middleware.TestUtils.isEndToEnd; -import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedPost; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; import static org.opentripplanner.middleware.auth.Auth0Users.createAuth0UserForEmail; @@ -128,17 +130,27 @@ public void canSimulateApiUserFlow() throws URISyntaxException { apiUser.id, apiUser.email, TEMP_AUTH0_USER_PASSWORD); - HttpResponse getTokenResponse = mockAuthenticatedPost(endpoint, + HttpResponse getTokenResponse = mockAuthenticatedRequest(endpoint, + "", apiUser, - "" + HttpUtils.REQUEST_METHOD.POST, + false ); LOG.info(getTokenResponse.body()); assertEquals(HttpStatus.OK_200, getTokenResponse.statusCode()); + TokenHolder tokenHolder = JsonUtils.getPOJOFromJSON(getTokenResponse.body(), TokenHolder.class); + + // Define the header values to be used in requests from this point forward. + HashMap headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + tokenHolder.getAccessToken()); + headers.put("x-api-key", apiUser.apiKeys.get(0).value); + // create an Otp user authenticating as an Api user. - HttpResponse createUserResponse = mockAuthenticatedPost("api/secure/user", - apiUser, - JsonUtils.toJson(otpUser) + HttpResponse createUserResponse = authenticatedRequest("api/secure/user", + JsonUtils.toJson(otpUser), + headers, + HttpUtils.REQUEST_METHOD.POST ); assertEquals(HttpStatus.OK_200, createUserResponse.statusCode()); @@ -150,32 +162,37 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // Create a monitored trip for the Otp user (API users are prevented from doing this). MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); monitoredTrip.userId = otpUser.id; - HttpResponse createTripResponseAsOtpUser = mockAuthenticatedPost("api/secure/monitoredtrip", + HttpResponse createTripResponseAsOtpUser = mockAuthenticatedRequest("api/secure/monitoredtrip", + JsonUtils.toJson(monitoredTrip), otpUserResponse, - JsonUtils.toJson(monitoredTrip) + HttpUtils.REQUEST_METHOD.POST, + true ); assertEquals(HttpStatus.UNAUTHORIZED_401, createTripResponseAsOtpUser.statusCode()); // Create a monitored trip for an Otp user authenticating as an Api user. An Api user can create a monitored // trip for an Otp user they created. - HttpResponse createTripResponseAsApiUser = mockAuthenticatedPost("api/secure/monitoredtrip", - apiUser, - JsonUtils.toJson(monitoredTrip) + HttpResponse createTripResponseAsApiUser = authenticatedRequest("api/secure/monitoredtrip", + JsonUtils.toJson(monitoredTrip), + headers, + HttpUtils.REQUEST_METHOD.POST ); assertEquals(HttpStatus.OK_200, createTripResponseAsApiUser.statusCode()); MonitoredTrip monitoredTripResponse = JsonUtils.getPOJOFromJSON(createTripResponseAsApiUser.body(), MonitoredTrip.class); // Request all monitored trip for an Otp user authenticating as an Api user. - HttpResponse getAllMonitoredTripsForOtpUser = mockAuthenticatedRequest(String.format("api/secure/monitoredtrip?userId=%s", + HttpResponse getAllMonitoredTripsForOtpUser = authenticatedRequest(String.format("api/secure/monitoredtrip?userId=%s", otpUserResponse.id), - apiUser, + "", + headers, HttpUtils.REQUEST_METHOD.GET ); assertEquals(HttpStatus.OK_200, getAllMonitoredTripsForOtpUser.statusCode()); // Request all monitored trip for an Otp user authenticating as an Api user. Without defining the user id. - getAllMonitoredTripsForOtpUser = mockAuthenticatedRequest("api/secure/monitoredtrip", - apiUser, + getAllMonitoredTripsForOtpUser = authenticatedRequest("api/secure/monitoredtrip", + "", + headers, HttpUtils.REQUEST_METHOD.GET ); assertEquals(HttpStatus.BAD_REQUEST_400, getAllMonitoredTripsForOtpUser.statusCode()); @@ -185,16 +202,19 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // as an Otp user (created by MOD UI or an Api user) because the end point has no auth. String otpQuery = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745&userId=" + otpUserResponse.id; HttpResponse planTripResponseAsOtUser = mockAuthenticatedRequest(otpQuery, + "", otpUserResponse, - HttpUtils.REQUEST_METHOD.GET + HttpUtils.REQUEST_METHOD.GET, + true ); LOG.info("Plan trip response: {}\n....", planTripResponseAsOtUser.body().substring(0, 300)); assertEquals(HttpStatus.OK_200, planTripResponseAsOtUser.statusCode()); // Plan trip with OTP proxy authenticating as an Api user. Mock plan response will be returned. This will work // as an Api user because the end point has no auth. - HttpResponse planTripResponseAsApiUser = mockAuthenticatedRequest(otpQuery, - apiUser, + HttpResponse planTripResponseAsApiUser = authenticatedRequest(otpQuery, + "", + headers, HttpUtils.REQUEST_METHOD.GET ); LOG.info("Plan trip response: {}\n....", planTripResponseAsApiUser.body().substring(0, 300)); @@ -204,17 +224,20 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // by an Api user and therefore does not have a Auth0 account. HttpResponse tripRequestResponseAsOtUser = mockAuthenticatedRequest(String.format("api/secure/triprequests?userId=%s", otpUserResponse.id), + "", otpUserResponse, - HttpUtils.REQUEST_METHOD.GET + HttpUtils.REQUEST_METHOD.GET, + true ); assertEquals(HttpStatus.UNAUTHORIZED_401, tripRequestResponseAsOtUser.statusCode()); // Get trip request history for user authenticating as an Api user. This will work because an Api user is able // to get a trip on behalf of an Otp user they created. - HttpResponse tripRequestResponseAsApiUser = mockAuthenticatedRequest(String.format("api/secure/triprequests?userId=%s", + HttpResponse tripRequestResponseAsApiUser = authenticatedRequest(String.format("api/secure/triprequests?userId=%s", otpUserResponse.id), - apiUser, + "", + headers, HttpUtils.REQUEST_METHOD.GET ); assertEquals(HttpStatus.OK_200, tripRequestResponseAsApiUser.statusCode()); @@ -225,16 +248,19 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // therefore does not have a Auth0 account. HttpResponse deleteUserResponseAsOtpUser = mockAuthenticatedRequest( String.format("api/secure/user/%s", otpUserResponse.id), + "", otpUserResponse, - HttpUtils.REQUEST_METHOD.DELETE + HttpUtils.REQUEST_METHOD.DELETE, + true ); assertEquals(HttpStatus.UNAUTHORIZED_401, deleteUserResponseAsOtpUser.statusCode()); // Delete Otp user authenticating as an Api user. This will work because an Api user can delete an Otp user they // created. - HttpResponse deleteUserResponseAsApiUser = mockAuthenticatedRequest( + HttpResponse deleteUserResponseAsApiUser = authenticatedRequest( String.format("api/secure/user/%s", otpUserResponse.id), - apiUser, + "", + headers, HttpUtils.REQUEST_METHOD.DELETE ); assertEquals(HttpStatus.OK_200, deleteUserResponseAsApiUser.statusCode()); @@ -253,9 +279,10 @@ public void canSimulateApiUserFlow() throws URISyntaxException { assertNull(tripRequest); // Delete API user (this would happen through the OTP Admin portal). - HttpResponse deleteApiUserResponse = mockAuthenticatedRequest( + HttpResponse deleteApiUserResponse = authenticatedRequest( String.format("api/secure/application/%s", apiUser.id), - apiUser, + "", + headers, HttpUtils.REQUEST_METHOD.DELETE ); assertEquals(HttpStatus.OK_200, deleteApiUserResponse.statusCode()); diff --git a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java index 998f0c463..2ffabac48 100644 --- a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java +++ b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java @@ -25,7 +25,6 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.opentripplanner.middleware.TestUtils.TEMP_AUTH0_USER_PASSWORD; import static org.opentripplanner.middleware.TestUtils.isEndToEnd; -import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedPost; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; @@ -117,25 +116,31 @@ public void canGetOwnMonitoredTrips() throws URISyntaxException { // Create trip as Otp user 1. MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); monitoredTrip.userId = otpUser1.id; - HttpResponse response = mockAuthenticatedPost("api/secure/monitoredtrip", + HttpResponse response = mockAuthenticatedRequest("api/secure/monitoredtrip", + JsonUtils.toJson(monitoredTrip), otpUser1, - JsonUtils.toJson(monitoredTrip) + HttpUtils.REQUEST_METHOD.POST, + true ); assertEquals(HttpStatus.OK_200, response.statusCode()); // Create trip as Otp user 2. monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); monitoredTrip.userId = otpUser2.id; - response = mockAuthenticatedPost("api/secure/monitoredtrip", + response = mockAuthenticatedRequest("api/secure/monitoredtrip", + JsonUtils.toJson(monitoredTrip), otpUser2, - JsonUtils.toJson(monitoredTrip) + HttpUtils.REQUEST_METHOD.POST, + true ); assertEquals(HttpStatus.OK_200, response.statusCode()); // Get trips for Otp user 2. response = mockAuthenticatedRequest("api/secure/monitoredtrip", + "", otpUser2, - HttpUtils.REQUEST_METHOD.GET + HttpUtils.REQUEST_METHOD.GET, + true ); ResponseList tripRequests = JsonUtils.getPOJOFromJSON(response.body(), ResponseList.class); @@ -145,8 +150,10 @@ public void canGetOwnMonitoredTrips() throws URISyntaxException { // Get trips for Otp user 2 defining user id. response = mockAuthenticatedRequest(String.format("api/secure/monitoredtrip?userId=%s", otpUser2.id), + "", otpUser2, - HttpUtils.REQUEST_METHOD.GET + HttpUtils.REQUEST_METHOD.GET, + true ); tripRequests = JsonUtils.getPOJOFromJSON(response.body(), ResponseList.class); diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index 9425ae109..bac938d5b 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -81,22 +81,6 @@ public static T getResourceFileContentsAsJSON(String resourcePathName, Class ); } - /** - * Send request to provided URL placing the Auth0 user id in the headers so that {@link RequestingUser} can check - * the database for a matching user. Returns the response. - */ - public static HttpResponse mockAuthenticatedRequest(String path, AbstractUser requestingUser, HttpUtils.REQUEST_METHOD requestMethod) { - HashMap headers = getMockHeaders(requestingUser); - - return HttpUtils.httpRequestRawResponse( - URI.create("http://localhost:4567/" + path), - 1000, - requestMethod, - headers, - "" - ); - } - /** * Construct http header values based on user type and status of DISABLE_AUTH config parameter. If authorization is * disabled, use Auth0 user ID to authenticate else attempt to get a valid 0auth token from Auth0 and use this. @@ -133,22 +117,31 @@ private static HashMap getMockHeaders(AbstractUser requestingUse return headers; } + /** * Send request to provided URL placing the Auth0 user id in the headers so that {@link RequestingUser} can check - * the database for a matching user. Returns the response. + * the database for a matching user. */ - public static HttpResponse mockAuthenticatedPost(String path, AbstractUser requestingUser, String body) { - HashMap headers = getMockHeaders(requestingUser); - + static HttpResponse authenticatedRequest(String path, String body, HashMap headers, + HttpUtils.REQUEST_METHOD requestMethod) { return HttpUtils.httpRequestRawResponse( URI.create("http://localhost:4567/" + path), 1000, - HttpUtils.REQUEST_METHOD.POST, + requestMethod, headers, body ); } + /** + * Construct http headers according to caller request and then make an authenticated call. + */ + static HttpResponse mockAuthenticatedRequest(String path, String body, AbstractUser requestingUser, + HttpUtils.REQUEST_METHOD requestMethod, boolean mockHeaders) { + HashMap headers = (mockHeaders) ? getMockHeaders(requestingUser) : null; + return authenticatedRequest(path, body, headers, requestMethod); + } + /** * Configure a mock OTP server for providing mock OTP responses. Note: this expects the config value * OTP_API_ROOT=http://localhost:8080/otp From efad45fb1ccd6236e23456d6720eda3a8a543793 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 23 Oct 2020 13:03:55 +0100 Subject: [PATCH 14/30] refactor(Addressed PR feedback): Addressed PR feedback --- .../middleware/OtpMiddlewareMain.java | 3 +- .../middleware/auth/Auth0Connection.java | 4 +-- .../middleware/auth/Auth0Users.java | 23 ++++++------ .../middleware/auth/RequestingUser.java | 8 ----- .../controllers/api/ApiUserController.java | 16 +++++++-- .../controllers/api/OtpRequestProcessor.java | 5 +-- .../middleware/models/MonitoredTrip.java | 2 +- .../middleware/models/OtpUser.java | 2 +- .../latest-spark-swagger-output.yaml | 11 ++---- .../middleware/ApiUserFlowTest.java | 35 ++++++------------- .../opentripplanner/middleware/TestUtils.java | 23 ++++++++++++ 11 files changed, 70 insertions(+), 62 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java b/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java index e0a26fae5..36d2e4e1d 100644 --- a/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java +++ b/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java @@ -134,8 +134,9 @@ private static void initializeHttpEndpoints() throws IOException, InterruptedExc // Security checks for admin and /secure/ endpoints. Excluding /authenticate so that API users can obtain a // bearer token to authenticate against all other /secure/ endpoints. spark.before(API_PREFIX + "/secure/*", ((request, response) -> { - if (!request.requestMethod().equals("OPTIONS") && !request.pathInfo().endsWith("/authenticate")) + if (!request.requestMethod().equals("OPTIONS") && !request.pathInfo().endsWith("/authenticate")) { Auth0Connection.checkUser(request); + } })); spark.before(API_PREFIX + "admin/*", ((request, response) -> { if (!request.requestMethod().equals("OPTIONS")) { diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index d4197a161..07bd61c2c 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -73,7 +73,7 @@ public static void checkUser(Request req) { LOG.info("New user is creating self. OK to proceed without existing user object for auth0UserId"); } else { // Otherwise, if no valid user is found, halt the request. - JsonUtils.logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "Auth0 auth - Unknown user."); + JsonUtils.logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "User is unknown to Auth0 tenant."); } } // The user attribute is used on the server side to check user permissions and does not have all of the @@ -244,7 +244,7 @@ public static void isAuthorized(String userId, Request request) { } // If userId is defined, it must be set to a value associated with a user. if (userId != null) { - if (requestingUser.isFirstPartyUser() && requestingUser.otpUser.id.equals(userId)) { + if (requestingUser.otpUser != null && requestingUser.otpUser.id.equals(userId)) { // Otp user requesting their item. return; } diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index e84914955..675019a62 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -240,11 +240,11 @@ private static String getAuth0Url() { } /** - * Get an Auth0 oauth token for use in mocking user requests by using the Auth0 'Call Your API Using Resource Owner - * Password Flow' approach. Auth0 setup can be reviewed here: https://auth0.com/docs/flows/call-your-api-using-resource-owner-password-flow. - * If the user is successfully validated by Auth0 a complete token is returned. In all other cases, null is returned. + * Get an Auth0 oauth token response for use in mocking user requests by using the Auth0 'Call Your API Using Resource + * Owner Password Flow' approach. Auth0 setup can be reviewed here: https://auth0.com/docs/flows/call-your-api-using-resource-owner-password-flow. + * If token response is returned to calling methods for evaluation. */ - public static TokenHolder getCompleteAuth0Token(String username, String password) { + public static HttpResponse getCompleteAuth0TokenResponse(String username, String password) { if (Auth0Connection.isAuthDisabled()) return null; String body = String.format( "grant_type=password&username=%s&password=%s&audience=%s&scope=&client_id=%s&client_secret=%s", @@ -254,26 +254,25 @@ public static TokenHolder getCompleteAuth0Token(String username, String password AUTH0_CLIENT_ID, // Auth0 application client ID AUTH0_CLIENT_SECRET // Auth0 application client secret ); - HttpResponse response = HttpUtils.httpRequestRawResponse( + return HttpUtils.httpRequestRawResponse( URI.create(String.format("https://%s/oauth/token", AUTH0_DOMAIN)), 1000, HttpUtils.REQUEST_METHOD.POST, Collections.singletonMap("content-type", "application/x-www-form-urlencoded"), body ); - if (response == null || response.statusCode() != HttpStatus.OK_200) { - LOG.error("Cannot obtain Auth0 token for user {}. response: {} - {}", username, response.statusCode(), response.body()); - return null; - } - return JsonUtils.getPOJOFromJSON(response.body(), TokenHolder.class); } /** * Extract from a complete Auth0 token just the access token. If the token is not available, return null instead. */ public static String getAuth0AccessToken(String username, String password) { - TokenHolder token = getCompleteAuth0Token(username, password); + HttpResponse response = getCompleteAuth0TokenResponse(username, password); + if (response == null || response.statusCode() != HttpStatus.OK_200) { + LOG.error("Cannot obtain Auth0 token for user {}. response: {} - {}", username, response.statusCode(), response.body()); + return null; + } + TokenHolder token = JsonUtils.getPOJOFromJSON(response.body(), TokenHolder.class); return (token == null) ? null : token.getAccessToken(); } - } diff --git a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index 25c033a4d..928fb10dc 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -75,14 +75,6 @@ public boolean isThirdPartyUser() { return apiUser != null; } - /** - * Determine if requesting user is a first party user. A first party user is a user coming directly from MOD UI. - */ - public boolean isFirstPartyUser() { - // TODO: Look to enhance otp user check. Perhaps define specific field to indicate this? - return otpUser != null; - } - /** * Check if the incoming user is an admin user. */ diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java index bf97875c9..ae74cadcc 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java @@ -21,6 +21,8 @@ import spark.Request; import spark.Response; +import java.net.http.HttpResponse; + /** * Implementation of the {@link AbstractUserController} for {@link ApiUser}. This controller also contains methods for * managing an {@link ApiUser}'s API keys. @@ -68,7 +70,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { .withResponseType(persistence.clazz), this::deleteApiKeyForApiUser, JsonUtils::toJson) // Authenticate user with Auth0 - .post(MethodDescriptor.path(ID_PATH + AUTHENTICATE_PATH) + .post(MethodDescriptor.path(AUTHENTICATE_PATH) .withDescription("Authenticates ApiUser with Auth0.") .withPathParam().withName(ID_PARAM).withDescription("The user ID.").and() .withQueryParam().withName(USERNAME_PARAM).withRequired(true) @@ -96,13 +98,21 @@ private boolean userHasKey(ApiUser user, String apiKeyId) { /** * Authenticate user with Auth0 based on username (email) and password. If successful, return the complete Auth0 - * token else null. + * token else log message and halt. */ private TokenHolder authenticateAuth0User(Request req, Response res) { String username = HttpUtils.getQueryParamFromRequest(req, USERNAME_PARAM, false); // FIXME: Should this be encrypted?! String password = HttpUtils.getQueryParamFromRequest(req, PASSWORD_PARAM, false); - return Auth0Users.getCompleteAuth0Token(username, password); + HttpResponse auth0TokenResponse = Auth0Users.getCompleteAuth0TokenResponse(username, password); + if (auth0TokenResponse == null || auth0TokenResponse.statusCode() != HttpStatus.OK_200) { + JsonUtils.logMessageAndHalt(req, + auth0TokenResponse.statusCode(), + String.format("Cannot obtain Auth0 token for user %s", username), + null + ); + } + return JsonUtils.getPOJOFromJSON(auth0TokenResponse.body(), TokenHolder.class); } /** diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index efd18386f..cd7a02fcd 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -59,7 +59,8 @@ public void bind(final SparkSwagger restApi) { ParameterDescriptor USER_ID = ParameterDescriptor.newBuilder() .withName(USER_ID_PARAM) .withRequired(false) - .withDescription("If a third party user is making a trip request on behalf of an OTP user, the OTP user id must be specified.").build(); + .withDescription("If a third-party application is making a trip plan request on behalf of an end user (OtpUser), the user id must be specified.") + .build(); restApi.endpoint( EndpointDescriptor.endpointPath(OTP_PROXY_ENDPOINT).withDescription("Proxy interface for OTP endpoints. " + OTP_DOC_LINK), HttpUtils.NO_FILTER @@ -132,7 +133,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons // Determine if the Otp request is being made by an actual Otp user or by a third party on behalf of an Otp user. OtpUser otpUser = null; - if (requestingUser.isFirstPartyUser()) { + if (requestingUser.otpUser != null) { // Otp user making a trip request for self. otpUser = requestingUser.otpUser; } else if (requestingUser.isThirdPartyUser()) { diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index 5653dbaba..d9ca1b01f 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -237,7 +237,7 @@ public boolean canBeManagedBy(RequestingUser requestingUser) { // OTP user is assigned to that API. boolean belongsToUser = false; // Monitored trip can only be owned by an OtpUser (not an ApiUser or AdminUser). - if (requestingUser.isFirstPartyUser()) { + if (requestingUser.otpUser != null) { belongsToUser = userId.equals(requestingUser.otpUser.id); } diff --git a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java index f71d7f895..b533d2698 100644 --- a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java @@ -86,7 +86,7 @@ public boolean delete(boolean deleteAuth0User) { */ @Override public boolean canBeManagedBy(RequestingUser requestingUser) { - if (requestingUser.isThirdPartyUser() && requestingUser.apiUser.id.equals(applicationId)) { + if (requestingUser.apiUser.id.equals(applicationId)) { // Otp user was created by this Api user. return true; } diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml index 3d68c7fe0..b67834d36 100644 --- a/src/main/resources/latest-spark-swagger-output.yaml +++ b/src/main/resources/latest-spark-swagger-output.yaml @@ -217,7 +217,7 @@ paths: description: "successful operation" schema: $ref: "#/definitions/ApiUser" - /api/secure/application/{id}/authenticate: + /api/secure/application/authenticate: post: tags: - "api/secure/application" @@ -225,11 +225,6 @@ paths: produces: - "application/json" parameters: - - name: "id" - in: "path" - description: "The user ID." - required: true - type: "string" - name: "username" in: "query" description: "Auth0 username (usually email address)." @@ -781,8 +776,8 @@ paths: parameters: - name: "userId" in: "query" - description: "If a third party user is making a trip request on behalf of\ - \ an OTP user, the OTP user id must be specified." + description: "If a third-party application is making a trip plan request on\ + \ behalf of an end user (OtpUser), the user id must be specified." required: false type: "string" responses: diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index 9ed945d05..b5eef3cdf 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -31,8 +31,10 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.opentripplanner.middleware.TestUtils.TEMP_AUTH0_USER_PASSWORD; +import static org.opentripplanner.middleware.TestUtils.authenticatedGet; import static org.opentripplanner.middleware.TestUtils.authenticatedRequest; import static org.opentripplanner.middleware.TestUtils.isEndToEnd; +import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedGet; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; import static org.opentripplanner.middleware.auth.Auth0Users.createAuth0UserForEmail; @@ -126,8 +128,7 @@ public static void tearDown() { public void canSimulateApiUserFlow() throws URISyntaxException { // obtain Auth0 token for Api user. - String endpoint = String.format("api/secure/application/%s/authenticate?username=%s&password=%s", - apiUser.id, + String endpoint = String.format("api/secure/application/authenticate?username=%s&password=%s", apiUser.email, TEMP_AUTH0_USER_PASSWORD); HttpResponse getTokenResponse = mockAuthenticatedRequest(endpoint, @@ -181,11 +182,9 @@ public void canSimulateApiUserFlow() throws URISyntaxException { MonitoredTrip monitoredTripResponse = JsonUtils.getPOJOFromJSON(createTripResponseAsApiUser.body(), MonitoredTrip.class); // Request all monitored trip for an Otp user authenticating as an Api user. - HttpResponse getAllMonitoredTripsForOtpUser = authenticatedRequest(String.format("api/secure/monitoredtrip?userId=%s", + HttpResponse getAllMonitoredTripsForOtpUser = authenticatedGet(String.format("api/secure/monitoredtrip?userId=%s", otpUserResponse.id), - "", - headers, - HttpUtils.REQUEST_METHOD.GET + headers ); assertEquals(HttpStatus.OK_200, getAllMonitoredTripsForOtpUser.statusCode()); @@ -201,10 +200,8 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // Plan trip with OTP proxy authenticating as an OTP user. Mock plan response will be returned. This will work // as an Otp user (created by MOD UI or an Api user) because the end point has no auth. String otpQuery = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745&userId=" + otpUserResponse.id; - HttpResponse planTripResponseAsOtUser = mockAuthenticatedRequest(otpQuery, - "", + HttpResponse planTripResponseAsOtUser = mockAuthenticatedGet(otpQuery, otpUserResponse, - HttpUtils.REQUEST_METHOD.GET, true ); LOG.info("Plan trip response: {}\n....", planTripResponseAsOtUser.body().substring(0, 300)); @@ -212,21 +209,15 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // Plan trip with OTP proxy authenticating as an Api user. Mock plan response will be returned. This will work // as an Api user because the end point has no auth. - HttpResponse planTripResponseAsApiUser = authenticatedRequest(otpQuery, - "", - headers, - HttpUtils.REQUEST_METHOD.GET - ); + HttpResponse planTripResponseAsApiUser = authenticatedGet(otpQuery,headers); LOG.info("Plan trip response: {}\n....", planTripResponseAsApiUser.body().substring(0, 300)); assertEquals(HttpStatus.OK_200, planTripResponseAsApiUser.statusCode()); // Get trip request history for user authenticating as an Otp user. This will fail because the user was created // by an Api user and therefore does not have a Auth0 account. - HttpResponse tripRequestResponseAsOtUser = mockAuthenticatedRequest(String.format("api/secure/triprequests?userId=%s", + HttpResponse tripRequestResponseAsOtUser = mockAuthenticatedGet(String.format("api/secure/triprequests?userId=%s", otpUserResponse.id), - "", otpUserResponse, - HttpUtils.REQUEST_METHOD.GET, true ); @@ -234,11 +225,9 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // Get trip request history for user authenticating as an Api user. This will work because an Api user is able // to get a trip on behalf of an Otp user they created. - HttpResponse tripRequestResponseAsApiUser = authenticatedRequest(String.format("api/secure/triprequests?userId=%s", + HttpResponse tripRequestResponseAsApiUser = authenticatedGet(String.format("api/secure/triprequests?userId=%s", otpUserResponse.id), - "", - headers, - HttpUtils.REQUEST_METHOD.GET + headers ); assertEquals(HttpStatus.OK_200, tripRequestResponseAsApiUser.statusCode()); @@ -246,11 +235,9 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // Delete Otp user authenticating as an Otp user. This will fail because the user was created by an Api user and // therefore does not have a Auth0 account. - HttpResponse deleteUserResponseAsOtpUser = mockAuthenticatedRequest( + HttpResponse deleteUserResponseAsOtpUser = mockAuthenticatedGet( String.format("api/secure/user/%s", otpUserResponse.id), - "", otpUserResponse, - HttpUtils.REQUEST_METHOD.DELETE, true ); assertEquals(HttpStatus.UNAUTHORIZED_401, deleteUserResponseAsOtpUser.statusCode()); diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index bac938d5b..32013f7a6 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -133,6 +133,20 @@ static HttpResponse authenticatedRequest(String path, String body, HashM ); } + /** + * Send 'get' request to provided URL placing the Auth0 user id in the headers so that {@link RequestingUser} can + * check the database for a matching user. + */ + static HttpResponse authenticatedGet(String path, HashMap headers) { + return HttpUtils.httpRequestRawResponse( + URI.create("http://localhost:4567/" + path), + 1000, + HttpUtils.REQUEST_METHOD.GET, + headers, + "" + ); + } + /** * Construct http headers according to caller request and then make an authenticated call. */ @@ -142,6 +156,15 @@ static HttpResponse mockAuthenticatedRequest(String path, String body, A return authenticatedRequest(path, body, headers, requestMethod); } + /** + * Construct http headers according to caller request and then make an authenticated 'get' call. + */ + static HttpResponse mockAuthenticatedGet(String path, AbstractUser requestingUser, boolean mockHeaders) { + HashMap headers = (mockHeaders) ? getMockHeaders(requestingUser) : null; + return authenticatedRequest(path, "", headers, HttpUtils.REQUEST_METHOD.GET); + } + + /** * Configure a mock OTP server for providing mock OTP responses. Note: this expects the config value * OTP_API_ROOT=http://localhost:8080/otp From d989ff934a0dac657b39eed6d0714d829956e2c4 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 23 Oct 2020 15:02:02 +0100 Subject: [PATCH 15/30] refactor(Auth0Users): Replaced Auth0 vars --- .../java/org/opentripplanner/middleware/auth/Auth0Users.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index 675019a62..46eadccfe 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -251,8 +251,8 @@ public static HttpResponse getCompleteAuth0TokenResponse(String username username, password, DEFAULT_AUDIENCE, // must match an API identifier - AUTH0_CLIENT_ID, // Auth0 application client ID - AUTH0_CLIENT_SECRET // Auth0 application client secret + AUTH0_API_CLIENT, // Auth0 application client ID + AUTH0_API_SECRET // Auth0 application client secret ); return HttpUtils.httpRequestRawResponse( URI.create(String.format("https://%s/oauth/token", AUTH0_DOMAIN)), From befa0c16eaf7be3eddc37335634062c37d2c599c Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 23 Oct 2020 16:08:49 +0100 Subject: [PATCH 16/30] refactor(Auth0Users): Added end to end check for auth0 vars --- .../opentripplanner/middleware/auth/Auth0Users.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index 46eadccfe..df6b796e7 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -27,6 +27,7 @@ import java.util.UUID; import static com.mongodb.client.model.Filters.eq; +import static org.opentripplanner.middleware.utils.ConfigUtils.getBooleanEnvVar; /** * This class contains methods for querying Auth0 users using the Auth0 User Management API. Auth0 docs describing the @@ -43,6 +44,12 @@ public class Auth0Users { private static final String DEFAULT_AUDIENCE = "https://otp-middleware"; private static final String MANAGEMENT_API_VERSION = "v2"; public static final String API_PATH = "/api/" + MANAGEMENT_API_VERSION; + + /** + * Whether the end-to-end environment variable is enabled. + */ + private static final boolean isEndToEnd = getBooleanEnvVar("RUN_E2E"); + /** * Cached API token so that we do not have to request a new one each time a Management API request is made. */ @@ -251,8 +258,8 @@ public static HttpResponse getCompleteAuth0TokenResponse(String username username, password, DEFAULT_AUDIENCE, // must match an API identifier - AUTH0_API_CLIENT, // Auth0 application client ID - AUTH0_API_SECRET // Auth0 application client secret + (isEndToEnd) ? AUTH0_CLIENT_ID : AUTH0_API_CLIENT, // Auth0 application client ID + (isEndToEnd) ? AUTH0_CLIENT_SECRET : AUTH0_API_SECRET // Auth0 application client secret ); return HttpUtils.httpRequestRawResponse( URI.create(String.format("https://%s/oauth/token", AUTH0_DOMAIN)), From ce30456d52beeeadb6bdf2c87335a6d463832e95 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Thu, 29 Oct 2020 16:25:36 +0000 Subject: [PATCH 17/30] refactor(Addressed PR feedback): Addressed PR feedback --- .../middleware/auth/Auth0Connection.java | 20 ++++---- .../middleware/auth/Auth0Users.java | 11 +++-- .../middleware/auth/RequestingUser.java | 7 ++- .../api/AbstractUserController.java | 4 +- .../controllers/api/ApiController.java | 40 +++++++++------- .../controllers/api/ApiUserController.java | 48 +++++++++---------- .../controllers/api/LogController.java | 6 ++- .../controllers/api/OtpRequestProcessor.java | 9 ++-- .../controllers/api/OtpUserController.java | 30 ++++++------ .../middleware/models/ApiUser.java | 28 +++++++++++ .../middleware/models/MonitoredTrip.java | 2 +- .../middleware/models/OtpUser.java | 10 ++-- .../middleware/ApiUserFlowTest.java | 32 +++++++------ 13 files changed, 148 insertions(+), 99 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 07bd61c2c..9b9539aaf 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -27,6 +27,8 @@ import java.security.interfaces.RSAPublicKey; +import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; + /** * This handles verifying the Auth0 token passed in the auth header (e.g., Authorization: Bearer MY_TOKEN of Spark HTTP * requests. @@ -73,7 +75,7 @@ public static void checkUser(Request req) { LOG.info("New user is creating self. OK to proceed without existing user object for auth0UserId"); } else { // Otherwise, if no valid user is found, halt the request. - JsonUtils.logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "User is unknown to Auth0 tenant."); + logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "User is unknown to Auth0 tenant."); } } // The user attribute is used on the server side to check user permissions and does not have all of the @@ -81,12 +83,12 @@ public static void checkUser(Request req) { addUserToRequest(req, profile); } catch (JWTVerificationException e) { // Invalid signature/claims - JsonUtils.logMessageAndHalt(req, 401, "Login failed to verify with our authorization provider.", e); + logMessageAndHalt(req, 401, "Login failed to verify with our authorization provider.", e); } catch (HaltException e) { throw e; } catch (Exception e) { LOG.warn("Login failed to verify with our authorization provider.", e); - JsonUtils.logMessageAndHalt(req, 401, "Could not verify user's token"); + logMessageAndHalt(req, 401, "Could not verify user's token"); } } @@ -132,7 +134,7 @@ public static void checkUserIsAdmin(Request req, Response res) { // Check that user object is present and is admin. RequestingUser user = Auth0Connection.getUserFromRequest(req); if (!user.isAdmin()) { - JsonUtils.logMessageAndHalt( + logMessageAndHalt( req, HttpStatus.UNAUTHORIZED_401, "User is not authorized to perform administrative action" @@ -159,19 +161,19 @@ public static RequestingUser getUserFromRequest(Request req) { */ private static String getTokenFromRequest(Request req) { if (!isAuthHeaderPresent(req)) { - JsonUtils.logMessageAndHalt(req, 401, "Authorization header is missing."); + logMessageAndHalt(req, 401, "Authorization header is missing."); } // Check that auth header is present and formatted correctly (Authorization: Bearer [token]). final String authHeader = req.headers("Authorization"); String[] parts = authHeader.split(" "); if (parts.length != 2 || !"bearer".equals(parts[0].toLowerCase())) { - JsonUtils.logMessageAndHalt(req, 401, String.format("Authorization header is malformed: %s", authHeader)); + logMessageAndHalt(req, 401, String.format("Authorization header is malformed: %s", authHeader)); } // Retrieve token from auth header. String token = parts[1]; if (token == null) { - JsonUtils.logMessageAndHalt(req, 401, "Could not find authorization token"); + logMessageAndHalt(req, 401, "Could not find authorization token"); } return token; } @@ -200,7 +202,7 @@ private static JWTVerifier getVerifier(Request req, String token) { .build(); } catch (IllegalStateException | NullPointerException | JwkException e) { LOG.error("Auth0 verifier configured incorrectly."); - JsonUtils.logMessageAndHalt(req, 500, "Server authentication configured incorrectly.", e); + logMessageAndHalt(req, 500, "Server authentication configured incorrectly.", e); } } return verifier; @@ -260,6 +262,6 @@ public static void isAuthorized(String userId, Request request) { } } } - JsonUtils.logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, "Unauthorized access."); + logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, "Unauthorized access."); } } diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index df6b796e7..f4bba760d 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -28,6 +28,7 @@ import static com.mongodb.client.model.Filters.eq; import static org.opentripplanner.middleware.utils.ConfigUtils.getBooleanEnvVar; +import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** * This class contains methods for querying Auth0 users using the Auth0 User Management API. Auth0 docs describing the @@ -196,12 +197,12 @@ public static User createNewAuth0User(U user, Request r U userWithEmail = userStore.getOneFiltered(eq("email", user.email)); if (userWithEmail != null) { // TODO: Does this need to change to allow multiple applications to create otpuser's with the same email? - JsonUtils.logMessageAndHalt(req, 400, "User with email already exists in database!"); + logMessageAndHalt(req, 400, "User with email already exists in database!"); } // Check for pre-existing user in Auth0 and create if not exists. User auth0UserProfile = getUserByEmail(user.email, true); if (auth0UserProfile == null) { - JsonUtils.logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Error creating user for email " + user.email); + logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Error creating user for email " + user.email); } LOG.info("Created new Auth0 user ({}) for user {}", auth0UserProfile.getId(), user.id); return auth0UserProfile; @@ -212,7 +213,7 @@ public static User createNewAuth0User(U user, Request r */ public static void validateUser(U user, Request req) { if (!isValidEmail(user.email)) { - JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Email address is invalid."); + logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Email address is invalid."); } } @@ -224,11 +225,11 @@ public static void validateExistingUser(U user, U preEx // Verify that email address for user has not changed. // TODO: should we permit changing email addresses? This would require making an update to Auth0. if (!preExistingUser.email.equals(user.email)) { - JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Cannot change user email address!"); + logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Cannot change user email address!"); } // Verify that Auth0 ID for user has not changed. if (!preExistingUser.auth0UserId.equals(user.auth0UserId)) { - JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Cannot change Auth0 ID!"); + logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Cannot change Auth0 ID!"); } } diff --git a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index 928fb10dc..8fa126a2f 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -67,8 +67,11 @@ static RequestingUser createTestUser(Request req) { } /** - * Determine if requesting user is a third party user. A third party user is classed as a user coming via the AWS - * API gateway. + * Determine if the requesting user is a third party API user. A third party API user is classed as a user that has + * signed up for access to the otp-middleware API. These users are expected to make requests on behalf of the + * OtpUsers they sign up, via a server application that authenticates via otp-middleware's authenticate + * endpoint (/api/secure/application/authenticate). OtpUsers created for third party API users enjoy a more limited + * range of activities (e.g., they cannot receive email/SMS notifications from otp-middleware). */ public boolean isThirdPartyUser() { // TODO: Look to enhance api user check. Perhaps define specific field to indicate this? diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java index 3275a1ad1..fbb6fba11 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java @@ -18,6 +18,8 @@ import spark.Request; import spark.Response; +import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; + /** * Implementation of the {@link ApiController} abstract class for managing users. This controller connects with Auth0 * services using the hooks provided by {@link ApiController}. @@ -80,7 +82,7 @@ private U getUserFromRequest(Request req, Response res) { // but have not completed the account setup form yet. // For those users, the user profile would be 404 not found (as opposed to 403 forbidden). if (user == null) { - JsonUtils.logMessageAndHalt(req, + logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, String.format(NO_USER_WITH_AUTH0_ID_MESSAGE, profile.auth0UserId), null); diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java index 7c43add66..3f1727323 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java @@ -14,6 +14,7 @@ import org.opentripplanner.middleware.controllers.response.ResponseList; import org.opentripplanner.middleware.models.Model; import org.opentripplanner.middleware.models.OtpUser; +import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.persistence.TypedPersistence; import org.opentripplanner.middleware.utils.DateTimeUtils; import org.opentripplanner.middleware.utils.HttpUtils; @@ -24,6 +25,8 @@ import spark.Request; import spark.Response; +import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; + /** * Generic API controller abstract class. This class provides CRUD methods using {@link spark.Spark} HTTP request * methods. This will identify the MongoDB collection on which to operate based on the provided {@link Model} class. @@ -191,11 +194,14 @@ private ResponseList getMany(Request req, Response res) { int offset = HttpUtils.getQueryParamFromRequest(req, OFFSET_PARAM, 0, DEFAULT_OFFSET); String userId = HttpUtils.getQueryParamFromRequest(req, USER_ID_PARAM, true); // Filter the response based on the user id, if provided. - if (userId != null) { - return persistence.getResponseList(Filters.eq(USER_ID_PARAM, userId), offset, limit); - } // If the user id is not provided filter response based on requesting user. RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); + if (userId != null) { + OtpUser otpUser = Persistence.otpUsers.getById(userId); + return (otpUser != null && otpUser.canBeManagedBy(requestingUser)) ? + persistence.getResponseList(Filters.eq(USER_ID_PARAM, userId), offset, limit) : + null; + } if (requestingUser.isAdmin()) { // If the user is admin, the context is presumed to be the admin dashboard, so we deliver all entities for // management or review without restriction. @@ -207,7 +213,7 @@ private ResponseList getMany(Request req, Response res) { return persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit); } else if (requestingUser.isThirdPartyUser()) { // A user id must be provided if the request is being made by a third party user. - JsonUtils.logMessageAndHalt(req, + logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, String.format("The parameter name (%s) must be provided.", USER_ID_PARAM)); return null; @@ -230,7 +236,7 @@ protected T getEntityForId(Request req, Response res) { T object = getObjectForId(req, id); if (!object.canBeManagedBy(requestingUser)) { - JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to get %s.", className)); + logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to get %s.", className)); } return object; @@ -247,17 +253,17 @@ private T deleteOne(Request req, Response res) { T object = getObjectForId(req, id); // Check that requesting user can manage entity. if (!object.canBeManagedBy(requestingUser)) { - JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to delete %s.", className)); + logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to delete %s.", className)); } // Run pre-delete hook. If return value is false, abort. if (!preDeleteHook(object, req)) { - JsonUtils.logMessageAndHalt(req, 500, "Unknown error occurred during delete attempt."); + logMessageAndHalt(req, 500, "Unknown error occurred during delete attempt."); } boolean success = object.delete(); if (success) { return object; } else { - JsonUtils.logMessageAndHalt( + logMessageAndHalt( req, HttpStatus.INTERNAL_SERVER_ERROR_500, String.format("Unknown error encountered. Failed to delete %s", className), @@ -267,7 +273,7 @@ private T deleteOne(Request req, Response res) { } catch (HaltException e) { throw e; } catch (Exception e) { - JsonUtils.logMessageAndHalt( + logMessageAndHalt( req, HttpStatus.INTERNAL_SERVER_ERROR_500, String.format("Error deleting %s", className), @@ -285,7 +291,7 @@ private T deleteOne(Request req, Response res) { private T getObjectForId(Request req, String id) { T object = persistence.getById(id); if (object == null) { - JsonUtils.logMessageAndHalt( + logMessageAndHalt( req, HttpStatus.NOT_FOUND_404, String.format("No %s with id=%s found.", className, id), @@ -320,7 +326,7 @@ private T createOrUpdate(Request req, Response res) { // Check if an update or create operation depending on presence of id param // This needs to be final because it is used in a lambda operation below. if (req.params(ID_PARAM) == null && req.requestMethod().equals("PUT")) { - JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Must provide id"); + logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Must provide id"); } RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); final boolean isCreating = req.params(ID_PARAM) == null; @@ -331,7 +337,7 @@ private T createOrUpdate(Request req, Response res) { if (isCreating) { // Verify that the requesting user can create object. if (!object.canBeCreatedBy(requestingUser)) { - JsonUtils.logMessageAndHalt(req, + logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to create %s.", className)); } @@ -342,12 +348,12 @@ private T createOrUpdate(Request req, Response res) { String id = getIdFromRequest(req); T preExistingObject = getObjectForId(req, id); if (preExistingObject == null) { - JsonUtils.logMessageAndHalt(req, 400, "Object to update does not exist!"); + logMessageAndHalt(req, 400, "Object to update does not exist!"); return null; } // Check that requesting user can manage entity. if (!preExistingObject.canBeManagedBy(requestingUser)) { - JsonUtils.logMessageAndHalt(req, + logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to update %s.", className)); } @@ -357,7 +363,7 @@ private T createOrUpdate(Request req, Response res) { object.dateCreated = preExistingObject.dateCreated; // Validate that ID in JSON body matches ID param. TODO add test if (!id.equals(object.id)) { - JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "ID in JSON body must match ID param."); + logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "ID in JSON body must match ID param."); } // Get updated object from pre-update hook method. T updatedObject = preUpdateHook(object, preExistingObject, req); @@ -368,11 +374,11 @@ private T createOrUpdate(Request req, Response res) { } catch (HaltException e) { throw e; } catch (JsonProcessingException e) { - JsonUtils.logMessageAndHalt(req, + logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Error parsing JSON for " + clazz.getSimpleName(), e); } catch (Exception e) { - JsonUtils.logMessageAndHalt(req, + logMessageAndHalt(req, 500, "An error was encountered while trying to save to the database", e); } finally { diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java index ae74cadcc..6807b8c52 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java @@ -23,6 +23,8 @@ import java.net.http.HttpResponse; +import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; + /** * Implementation of the {@link AbstractUserController} for {@link ApiUser}. This controller also contains methods for * managing an {@link ApiUser}'s API keys. @@ -87,29 +89,27 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { } /** - * Shorthand method to determine if an API user exists and has an API key. - */ - private boolean userHasKey(ApiUser user, String apiKeyId) { - return user != null && - user.apiKeys - .stream() - .anyMatch(apiKey -> apiKeyId.equals(apiKey.keyId)); - } - - /** - * Authenticate user with Auth0 based on username (email) and password. If successful, return the complete Auth0 + * Authenticate user with Auth0 based on username (email) and password. If successful, confirm that the provided API + * key is return the complete Auth0 * token else log message and halt. */ private TokenHolder authenticateAuth0User(Request req, Response res) { String username = HttpUtils.getQueryParamFromRequest(req, USERNAME_PARAM, false); // FIXME: Should this be encrypted?! String password = HttpUtils.getQueryParamFromRequest(req, PASSWORD_PARAM, false); + String apiKeyFromHeader = req.headers("x-api-key"); + ApiUser apiUser = ApiUser.userForApiKeyValue(apiKeyFromHeader); + if (apiKeyFromHeader == null || apiUser == null || !apiUser.email.equals(username)) { + logMessageAndHalt(req, + HttpStatus.BAD_REQUEST_400, + "An API key must be provided and match Api user." + ); + } HttpResponse auth0TokenResponse = Auth0Users.getCompleteAuth0TokenResponse(username, password); if (auth0TokenResponse == null || auth0TokenResponse.statusCode() != HttpStatus.OK_200) { - JsonUtils.logMessageAndHalt(req, + logMessageAndHalt(req, auth0TokenResponse.statusCode(), - String.format("Cannot obtain Auth0 token for user %s", username), - null + String.format("Cannot obtain Auth0 token for user %s", username) ); } return JsonUtils.getPOJOFromJSON(auth0TokenResponse.body(), TokenHolder.class); @@ -128,7 +128,7 @@ private ApiUser createApiKeyForApiUser(Request req, Response res) { if (!requestingUser.isAdmin()) { usagePlanId = DEFAULT_USAGE_PLAN_ID; if (targetUser.apiKeys.size() >= API_KEY_LIMIT_PER_USER) { - JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "User has reached API key limit."); + logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "User has reached API key limit."); } } // FIXME Should an Api user be limited to one api key per usage plan (and perhaps stage)? @@ -138,7 +138,7 @@ private ApiUser createApiKeyForApiUser(Request req, Response res) { targetUser.apiKeys.add(apiKey); Persistence.apiUsers.replace(targetUser.id, targetUser); } catch (CreateApiKeyException e) { - JsonUtils.logMessageAndHalt(req, + logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Error creating API key", e @@ -154,18 +154,18 @@ private ApiUser deleteApiKeyForApiUser(Request req, Response res) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); // Do not permit key deletion unless user is an admin. if (!requestingUser.isAdmin()) { - JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to delete an API key."); + logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to delete an API key."); } ApiUser targetUser = getApiUser(req); String apiKeyId = HttpUtils.getRequiredParamFromRequest(req, "apiKeyId"); if (apiKeyId == null) { - JsonUtils.logMessageAndHalt(req, + logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "An api key id is required", null); } - if (!userHasKey(targetUser, apiKeyId)) { - JsonUtils.logMessageAndHalt(req, + if (targetUser != null && !targetUser.hasKey(apiKeyId)) { + logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, String.format("User id (%s) does not have expected api key id (%s)", targetUser.id, apiKeyId), null); @@ -180,7 +180,7 @@ private ApiUser deleteApiKeyForApiUser(Request req, Response res) { return Persistence.apiUsers.getById(targetUser.id); } else { // Throw halt if API key deletion failed. - JsonUtils.logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Unknown error deleting API key."); + logMessageAndHalt(req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Unknown error deleting API key."); return null; } } @@ -199,7 +199,7 @@ ApiUser preCreateHook(ApiUser user, Request req) { try { user.createApiKey(DEFAULT_USAGE_PLAN_ID, false); } catch (CreateApiKeyException e) { - JsonUtils.logMessageAndHalt( + logMessageAndHalt( req, HttpStatus.INTERNAL_SERVER_ERROR_500, "Error creating API key", @@ -234,7 +234,7 @@ private static ApiUser getApiUser(Request req) { String userId = HttpUtils.getRequiredParamFromRequest(req, ID_PARAM); ApiUser apiUser = Persistence.apiUsers.getById(userId); if (apiUser == null) { - JsonUtils.logMessageAndHalt( + logMessageAndHalt( req, HttpStatus.NOT_FOUND_404, String.format("No Api user matching the given user id (%s)", userId), @@ -243,7 +243,7 @@ private static ApiUser getApiUser(Request req) { } if (!apiUser.canBeManagedBy(requestingUser)) { - JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to perform this operation."); + logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to perform this operation."); } return apiUser; } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java index 5f2586b34..f776783ec 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java @@ -23,6 +23,8 @@ import java.util.List; import java.util.stream.Collectors; +import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; + /** * Sets up HTTP endpoints for getting logging and request summary information from AWS Cloudwatch and API Gateway. */ @@ -81,7 +83,7 @@ private static List getUsageLogs(Request req, Response res) { // If the user is not an admin, the list of API keys is defaulted to their keys. if (!requestingUser.isAdmin()) { if (requestingUser.apiUser == null) { - JsonUtils.logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Action is not permitted for user."); + logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Action is not permitted for user."); return null; } apiKeys = requestingUser.apiUser.apiKeys; @@ -109,7 +111,7 @@ private static List getUsageLogs(Request req, Response res) { .collect(Collectors.toList()); } catch (Exception e) { // Catch any issues with bad request parameters (e.g., invalid API keyId or bad date format). - JsonUtils.logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Error requesting usage results", e); + logMessageAndHalt(req, HttpStatus.BAD_REQUEST_400, "Error requesting usage results", e); } return null; diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index cd7a02fcd..d1036f311 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -25,6 +25,8 @@ import javax.ws.rs.core.MediaType; import java.util.List; +import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; + /** * Responsible for getting a response from OTP based on the parameters provided by the requester. If the target service * is of interest the response is intercepted and processed. In all cases, the response from OTP (content and HTTP @@ -81,7 +83,7 @@ public void bind(final SparkSwagger restApi) { */ private static String proxy(Request request, spark.Response response) { if (OtpDispatcher.OTP_API_ROOT == null) { - JsonUtils.logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No OTP Server provided, check config."); + logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No OTP Server provided, check config."); return null; } // Get request path intended for OTP API by removing the proxy endpoint (/otp). @@ -90,7 +92,7 @@ private static String proxy(Request request, spark.Response response) { // attempt to get response from OTP server based on requester's query parameters OtpDispatcherResponse otpDispatcherResponse = OtpDispatcher.sendOtpRequest(request.queryString(), otpRequestPath); if (otpDispatcherResponse == null || otpDispatcherResponse.responseBody == null) { - JsonUtils.logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No response from OTP server."); + logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No response from OTP server."); return null; } @@ -127,6 +129,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons Auth0Connection.checkUser(request); RequestingUser requestingUser = Auth0Connection.getUserFromRequest(request); + // A requesting user (Otp or third party user) is required to proceed. if (requestingUser == null) { return; } @@ -141,7 +144,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons // as a query parameter. otpUser = Persistence.otpUsers.getById(request.queryParams(USER_ID_PARAM)); if (otpUser != null && !otpUser.canBeManagedBy(requestingUser)) { - JsonUtils.logMessageAndHalt(request, + logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, String.format("User: %s not authorized to make trip requests for user: %s", requestingUser.apiUser.email, diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java index 213753426..992608a67 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java @@ -4,10 +4,8 @@ import com.twilio.rest.verify.v2.service.Verification; import com.twilio.rest.verify.v2.service.VerificationCheck; import org.eclipse.jetty.http.HttpStatus; -import com.mongodb.client.model.Filters; +import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.auth.RequestingUser; -import org.opentripplanner.middleware.bugsnag.BugsnagReporter; -import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.utils.JsonUtils; @@ -38,22 +36,22 @@ public OtpUserController(String apiPrefix) { @Override OtpUser preCreateHook(OtpUser user, Request req) { - String apiKey = req.headers("x-api-key"); - // If an api key is present an API user is attempting to create an OTP user. - if (apiKey != null) { + RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); + String apiKeyFromHeader = req.headers("x-api-key"); + // If third party and an api key is present user is attempting to create an OTP user. + if (requestingUser.apiUser != null && apiKeyFromHeader != null) { // Check API key and assign user to appropriate third-party application. Note: this is only relevant for // instances of otp-middleware running behind API Gateway. - ApiUser apiUser = Persistence.apiUsers.getOneFiltered(Filters.eq("apiKeys.value", apiKey)); - if (apiUser != null) { - // If API user found, assign to new OTP user. - user.applicationId = apiUser.id; + if (requestingUser.apiUser.hasToken(apiKeyFromHeader)) { + // If third party and using their own api key, assign to new OTP user. + user.applicationId = requestingUser.apiUser.id; } else { - // If API user not found, report to Bugsnag for further investigation. - BugsnagReporter.reportErrorToBugsnag( - "OTP user created with API key that is not linked to any API user", - apiKey, - new IllegalArgumentException("API key not linked to API user.") - ); + // If API user not found, log message and halt. + logMessageAndHalt( + req, + HttpStatus.FORBIDDEN_403, + "Attempting to create OTP user with API key that is not linked to calling third party", + new IllegalArgumentException("API key not linked to API user.")); } } return super.preCreateHook(user, req); diff --git a/src/main/java/org/opentripplanner/middleware/models/ApiUser.java b/src/main/java/org/opentripplanner/middleware/models/ApiUser.java index a09cd7649..6de568d8a 100644 --- a/src/main/java/org/opentripplanner/middleware/models/ApiUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/ApiUser.java @@ -87,4 +87,32 @@ public void createApiKey(String usagePlanId, boolean persist) throws CreateApiKe public static ApiUser userForApiKey(String apiKeyId) { return Persistence.apiUsers.getOneFiltered(Filters.elemMatch("apiKeys", Filters.eq("keyId", apiKeyId))); } + + /** + * @return the first {@link ApiUser} found with an {@link ApiKey#value} in {@link #apiKeys} that matches the + * provided apiKeyValue. + */ + public static ApiUser userForApiKeyValue(String apiKeyValue) { + return Persistence.apiUsers.getOneFiltered(Filters.elemMatch("apiKeys", Filters.eq("value", apiKeyValue))); + } + + /** + * Shorthand method to determine if an API user has an API key. + */ + public boolean hasKey(String apiKeyId) { + return apiKeys + .stream() + .anyMatch(apiKey -> apiKeyId.equals(apiKey.keyId)); + } + + /** + * Shorthand method to determine if an API user has an API token (value). + */ + public boolean hasToken(String apiKeyValue) { + return apiKeys + .stream() + .anyMatch(apiKey -> apiKeyValue.equals(apiKey.value)); + } + + } diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index d9ca1b01f..0bae4b17a 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -243,7 +243,7 @@ public boolean canBeManagedBy(RequestingUser requestingUser) { if (belongsToUser) { return true; - } else if (requestingUser.isThirdPartyUser()) { + } else if (requestingUser.apiUser != null) { // get the required OTP user to confirm they are associated with the requesting API user. OtpUser otpUser = Persistence.otpUsers.getById(userId); if (otpUser != null && otpUser.canBeManagedBy(requestingUser)) { diff --git a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java index b533d2698..c8627d13c 100644 --- a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java @@ -1,6 +1,7 @@ package org.opentripplanner.middleware.models; import com.fasterxml.jackson.annotation.JsonIgnore; +import org.opentripplanner.middleware.auth.Auth0Users; import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.persistence.Persistence; import org.slf4j.Logger; @@ -44,6 +45,7 @@ public class OtpUser extends AbstractUser { public boolean storeTripHistory; @JsonIgnore + /** If this user was created by an Api user, this parameter will match the Api user's id */ public String applicationId; @Override @@ -69,8 +71,8 @@ public boolean delete(boolean deleteAuth0User) { } } - // Only attempt to delete Auth0 user if Otp user is not assigned to third party. - if (deleteAuth0User && applicationId.isEmpty()) { + // Only attempt to delete Auth0 user if they exist within Auth0 tenant. + if (deleteAuth0User && Auth0Users.getUserByEmail(email, false) != null) { boolean auth0UserDeleted = super.delete(); if (!auth0UserDeleted) { LOG.warn("Aborting user deletion for {}", this.email); @@ -86,8 +88,8 @@ public boolean delete(boolean deleteAuth0User) { */ @Override public boolean canBeManagedBy(RequestingUser requestingUser) { - if (requestingUser.apiUser.id.equals(applicationId)) { - // Otp user was created by this Api user. + if (requestingUser.apiUser != null && requestingUser.apiUser.id.equals(applicationId)) { + // Otp user was created by this Api user (first or third party). return true; } // Fallback to Model#userCanManage. diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index b5eef3cdf..3cf9135bb 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -127,25 +127,25 @@ public static void tearDown() { @Test public void canSimulateApiUserFlow() throws URISyntaxException { + // Define the header values to be used in requests from this point forward. + HashMap headers = new HashMap<>(); + headers.put("x-api-key", apiUser.apiKeys.get(0).value); + // obtain Auth0 token for Api user. String endpoint = String.format("api/secure/application/authenticate?username=%s&password=%s", apiUser.email, TEMP_AUTH0_USER_PASSWORD); - HttpResponse getTokenResponse = mockAuthenticatedRequest(endpoint, + HttpResponse getTokenResponse = authenticatedRequest(endpoint, "", - apiUser, - HttpUtils.REQUEST_METHOD.POST, - false + headers, + HttpUtils.REQUEST_METHOD.POST ); LOG.info(getTokenResponse.body()); assertEquals(HttpStatus.OK_200, getTokenResponse.statusCode()); TokenHolder tokenHolder = JsonUtils.getPOJOFromJSON(getTokenResponse.body(), TokenHolder.class); - // Define the header values to be used in requests from this point forward. - HashMap headers = new HashMap<>(); + // Define the bearer value to be used in requests from this point forward. headers.put("Authorization", "Bearer " + tokenHolder.getAccessToken()); - headers.put("x-api-key", apiUser.apiKeys.get(0).value); - // create an Otp user authenticating as an Api user. HttpResponse createUserResponse = authenticatedRequest("api/secure/user", @@ -156,8 +156,8 @@ public void canSimulateApiUserFlow() throws URISyntaxException { assertEquals(HttpStatus.OK_200, createUserResponse.statusCode()); - // Attempt to create a monitored trip for an Otp user authenticating as an Otp user. This will fail because the - // user was created by an Api user and therefore does not have a Auth0 account. + // Attempt to create a monitored trip for an Otp user using mock authentication. This will fail because the user + // was created by an Api user and therefore does not have a Auth0 account. OtpUser otpUserResponse = JsonUtils.getPOJOFromJSON(createUserResponse.body(), OtpUser.class); // Create a monitored trip for the Otp user (API users are prevented from doing this). @@ -181,14 +181,16 @@ public void canSimulateApiUserFlow() throws URISyntaxException { assertEquals(HttpStatus.OK_200, createTripResponseAsApiUser.statusCode()); MonitoredTrip monitoredTripResponse = JsonUtils.getPOJOFromJSON(createTripResponseAsApiUser.body(), MonitoredTrip.class); - // Request all monitored trip for an Otp user authenticating as an Api user. + // Request all monitored trips for an Otp user authenticating as an Api user. This will work and return all trips + // matching the user id provided. HttpResponse getAllMonitoredTripsForOtpUser = authenticatedGet(String.format("api/secure/monitoredtrip?userId=%s", otpUserResponse.id), headers ); assertEquals(HttpStatus.OK_200, getAllMonitoredTripsForOtpUser.statusCode()); - // Request all monitored trip for an Otp user authenticating as an Api user. Without defining the user id. + // Request all monitored trips for an Otp user authenticating as an Api user. Without defining the user id. This + // will fail because an Api user must provide a user id. getAllMonitoredTripsForOtpUser = authenticatedRequest("api/secure/monitoredtrip", "", headers, @@ -200,12 +202,12 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // Plan trip with OTP proxy authenticating as an OTP user. Mock plan response will be returned. This will work // as an Otp user (created by MOD UI or an Api user) because the end point has no auth. String otpQuery = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745&userId=" + otpUserResponse.id; - HttpResponse planTripResponseAsOtUser = mockAuthenticatedGet(otpQuery, + HttpResponse planTripResponseAsOtpUser = mockAuthenticatedGet(otpQuery, otpUserResponse, true ); - LOG.info("Plan trip response: {}\n....", planTripResponseAsOtUser.body().substring(0, 300)); - assertEquals(HttpStatus.OK_200, planTripResponseAsOtUser.statusCode()); + LOG.info("Plan trip response: {}\n....", planTripResponseAsOtpUser.body().substring(0, 300)); + assertEquals(HttpStatus.OK_200, planTripResponseAsOtpUser.statusCode()); // Plan trip with OTP proxy authenticating as an Api user. Mock plan response will be returned. This will work // as an Api user because the end point has no auth. From 790e3fe1d9c559156e4df9c11f6d9f4bf6e6156f Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Thu, 29 Oct 2020 16:47:23 +0000 Subject: [PATCH 18/30] refactor(OtpUserControllerTest): Fixed issue with mock auth request --- .../org/opentripplanner/middleware/TestUtils.java | 2 +- .../controllers/api/OtpUserControllerTest.java | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index 32013f7a6..8cb0cd4fb 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -159,7 +159,7 @@ static HttpResponse mockAuthenticatedRequest(String path, String body, A /** * Construct http headers according to caller request and then make an authenticated 'get' call. */ - static HttpResponse mockAuthenticatedGet(String path, AbstractUser requestingUser, boolean mockHeaders) { + public static HttpResponse mockAuthenticatedGet(String path, AbstractUser requestingUser, boolean mockHeaders) { HashMap headers = (mockHeaders) ? getMockHeaders(requestingUser) : null; return authenticatedRequest(path, "", headers, HttpUtils.REQUEST_METHOD.GET); } diff --git a/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java b/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java index fed99fa47..88471dfe8 100644 --- a/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java +++ b/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java @@ -9,7 +9,6 @@ import org.opentripplanner.middleware.OtpMiddlewareTest; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.persistence.Persistence; -import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.JsonUtils; import java.io.IOException; @@ -19,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; +import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedGet; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; import static org.opentripplanner.middleware.auth.Auth0Connection.setAuthDisabled; @@ -65,22 +64,22 @@ public static void tearDown() { public void invalidNumbersShouldProduceBadRequest(String badNumber, int statusCode) { // 1. Request verification SMS. // The invalid number should fail the call. - HttpResponse response = mockAuthenticatedRequest( + HttpResponse response = mockAuthenticatedGet( String.format("api/secure/user/%s/verify_sms/%s", otpUser.id, badNumber ), otpUser, - HttpUtils.REQUEST_METHOD.GET + true ); assertEquals(statusCode, response.statusCode()); // 2. Fetch the newly-created user. // The phone number should not be updated. - HttpResponse otpUserWithPhoneRequest = mockAuthenticatedRequest( + HttpResponse otpUserWithPhoneRequest = mockAuthenticatedGet( String.format("api/secure/user/%s", otpUser.id), otpUser, - HttpUtils.REQUEST_METHOD.GET + true ); assertEquals(HttpStatus.OK_200, otpUserWithPhoneRequest.statusCode()); From ae7bc47fa2dd9eecae6340e0b563f30e3dfd194f Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Fri, 30 Oct 2020 08:47:06 +0000 Subject: [PATCH 19/30] refactor(Comment update and explict use of auth path): Comment update and explict use of auth path f --- .../org/opentripplanner/middleware/OtpMiddlewareMain.java | 4 +++- .../org/opentripplanner/middleware/auth/Auth0Connection.java | 2 +- .../middleware/controllers/api/ApiUserController.java | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java b/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java index 36d2e4e1d..4452de055 100644 --- a/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java +++ b/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java @@ -30,6 +30,8 @@ import java.util.concurrent.TimeUnit; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.opentripplanner.middleware.controllers.api.ApiUserController.API_USER_PATH; +import static org.opentripplanner.middleware.controllers.api.ApiUserController.AUTHENTICATE_PATH; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -134,7 +136,7 @@ private static void initializeHttpEndpoints() throws IOException, InterruptedExc // Security checks for admin and /secure/ endpoints. Excluding /authenticate so that API users can obtain a // bearer token to authenticate against all other /secure/ endpoints. spark.before(API_PREFIX + "/secure/*", ((request, response) -> { - if (!request.requestMethod().equals("OPTIONS") && !request.pathInfo().endsWith("/authenticate")) { + if (!request.requestMethod().equals("OPTIONS") && !request.pathInfo().endsWith(API_USER_PATH + AUTHENTICATE_PATH)) { Auth0Connection.checkUser(request); } })); diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 9b9539aaf..98527a6f3 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -75,7 +75,7 @@ public static void checkUser(Request req) { LOG.info("New user is creating self. OK to proceed without existing user object for auth0UserId"); } else { // Otherwise, if no valid user is found, halt the request. - logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "User is unknown to Auth0 tenant."); + logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "No user found in database associated with the provided auth token."); } } // The user attribute is used on the server side to check user permissions and does not have all of the diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java index 6807b8c52..fa6092f52 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java @@ -33,7 +33,7 @@ public class ApiUserController extends AbstractUserController { private static final Logger LOG = LoggerFactory.getLogger(ApiUserController.class); public static final String DEFAULT_USAGE_PLAN_ID = ConfigUtils.getConfigPropertyAsText("DEFAULT_USAGE_PLAN_ID"); private static final String API_KEY_PATH = "/apikey"; - private static final String AUTHENTICATE_PATH = "/authenticate"; + public static final String AUTHENTICATE_PATH = "/authenticate"; private static final int API_KEY_LIMIT_PER_USER = 2; private static final String API_KEY_ID_PARAM = "/:apiKeyId"; public static final String API_USER_PATH = "secure/application"; From 3954147afdd85092e5615103e8cbd0a3cca9c9e8 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Mon, 2 Nov 2020 17:47:51 +0000 Subject: [PATCH 20/30] refactor(Addressed PR feedback): Addressed PR feedback --- .../middleware/OtpMiddlewareMain.java | 4 +- .../middleware/auth/Auth0Connection.java | 9 +++-- .../middleware/auth/Auth0Users.java | 12 +++--- .../middleware/auth/RequestingUser.java | 2 +- .../api/AbstractUserController.java | 6 +-- .../controllers/api/ApiController.java | 38 ++++++++++--------- .../controllers/api/ApiUserController.java | 15 ++++---- .../controllers/api/LogController.java | 15 ++++---- .../controllers/api/OtpRequestProcessor.java | 8 ++-- .../controllers/api/OtpUserController.java | 8 ++-- .../docs/PublicApiDocGenerator.java | 3 +- .../middleware/tripMonitor/Main.java | 4 +- .../tripMonitor/jobs/CheckMonitoredTrip.java | 1 - .../middleware/utils/DateTimeUtils.java | 4 +- .../middleware/ApiUserFlowTest.java | 11 +++++- 15 files changed, 77 insertions(+), 63 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java b/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java index 4452de055..2640e25d2 100644 --- a/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java +++ b/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java @@ -16,7 +16,6 @@ import org.opentripplanner.middleware.docs.PublicApiDocGenerator; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.tripMonitor.jobs.MonitorAllTripsJob; -import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.Scheduler; import org.slf4j.Logger; @@ -32,6 +31,7 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static org.opentripplanner.middleware.controllers.api.ApiUserController.API_USER_PATH; import static org.opentripplanner.middleware.controllers.api.ApiUserController.AUTHENTICATE_PATH; +import static org.opentripplanner.middleware.utils.ConfigUtils.loadConfig; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -45,7 +45,7 @@ public class OtpMiddlewareMain { public static void main(String[] args) throws IOException, InterruptedException { // Load configuration. - ConfigUtils.loadConfig(args); + loadConfig(args); // Connect to MongoDB. Persistence.initialize(); diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 98527a6f3..900c385bd 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -17,7 +17,6 @@ import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.persistence.Persistence; -import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.JsonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,6 +26,8 @@ import java.security.interfaces.RSAPublicKey; +import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; +import static org.opentripplanner.middleware.utils.ConfigUtils.hasConfigProperty; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -185,7 +186,7 @@ private static String getTokenFromRequest(Request req) { private static JWTVerifier getVerifier(Request req, String token) { if (verifier == null) { try { - final String domain = "https://" + ConfigUtils.getConfigPropertyAsText("AUTH0_DOMAIN") + "/"; + final String domain = "https://" + getConfigPropertyAsText("AUTH0_DOMAIN") + "/"; JwkProvider provider = new UrlJwkProvider(domain); // Decode the token. DecodedJWT jwt = JWT.decode(token); @@ -209,8 +210,8 @@ private static JWTVerifier getVerifier(Request req, String token) { } public static boolean getDefaultAuthDisabled() { - return ConfigUtils.hasConfigProperty("DISABLE_AUTH") && - "true".equals(ConfigUtils.getConfigPropertyAsText("DISABLE_AUTH")); + return hasConfigProperty("DISABLE_AUTH") && + "true".equals(getConfigPropertyAsText("DISABLE_AUTH")); } /** diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index f4bba760d..e25ebeb1c 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -12,7 +12,6 @@ import org.opentripplanner.middleware.bugsnag.BugsnagReporter; import org.opentripplanner.middleware.models.AbstractUser; import org.opentripplanner.middleware.persistence.TypedPersistence; -import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.JsonUtils; import org.slf4j.Logger; @@ -28,6 +27,7 @@ import static com.mongodb.client.model.Filters.eq; import static org.opentripplanner.middleware.utils.ConfigUtils.getBooleanEnvVar; +import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -35,12 +35,12 @@ * searchable fields and query syntax are here: https://auth0.com/docs/api/management/v2/user-search */ public class Auth0Users { - public static final String AUTH0_DOMAIN = ConfigUtils.getConfigPropertyAsText("AUTH0_DOMAIN"); + public static final String AUTH0_DOMAIN = getConfigPropertyAsText("AUTH0_DOMAIN"); // This client/secret pair is for making requests for an API access token used with the Management API. - private static final String AUTH0_API_CLIENT = ConfigUtils.getConfigPropertyAsText("AUTH0_API_CLIENT"); - private static final String AUTH0_API_SECRET = ConfigUtils.getConfigPropertyAsText("AUTH0_API_SECRET"); - private static final String AUTH0_CLIENT_ID = ConfigUtils.getConfigPropertyAsText("AUTH0_CLIENT_ID"); - private static final String AUTH0_CLIENT_SECRET = ConfigUtils.getConfigPropertyAsText("AUTH0_CLIENT_SECRET"); + private static final String AUTH0_API_CLIENT = getConfigPropertyAsText("AUTH0_API_CLIENT"); + private static final String AUTH0_API_SECRET = getConfigPropertyAsText("AUTH0_API_SECRET"); + private static final String AUTH0_CLIENT_ID = getConfigPropertyAsText("AUTH0_CLIENT_ID"); + private static final String AUTH0_CLIENT_SECRET = getConfigPropertyAsText("AUTH0_CLIENT_SECRET"); private static final String DEFAULT_CONNECTION_TYPE = "Username-Password-Authentication"; private static final String DEFAULT_AUDIENCE = "https://otp-middleware"; private static final String MANAGEMENT_API_VERSION = "v2"; diff --git a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index 8fa126a2f..1bbcb678c 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -67,7 +67,7 @@ static RequestingUser createTestUser(Request req) { } /** - * Determine if the requesting user is a third party API user. A third party API user is classed as a user that has + * Determine if the requesting user is a third party API user. A third party API user is classified as a user that has * signed up for access to the otp-middleware API. These users are expected to make requests on behalf of the * OtpUsers they sign up, via a server application that authenticates via otp-middleware's authenticate * endpoint (/api/secure/application/authenticate). OtpUsers created for third party API users enjoy a more limited diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java index fbb6fba11..7ae61a4b6 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/AbstractUserController.java @@ -3,7 +3,6 @@ import com.auth0.json.mgmt.jobs.Job; import com.auth0.json.mgmt.users.User; import com.beerboy.ss.ApiEndpoint; -import com.beerboy.ss.descriptor.MethodDescriptor; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.auth.RequestingUser; @@ -18,6 +17,7 @@ import spark.Request; import spark.Response; +import static com.beerboy.ss.descriptor.MethodDescriptor.path; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -46,14 +46,14 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { // by spark as 'GET user with id "fromtoken"', which we don't want). ApiEndpoint modifiedEndpoint = baseEndpoint // Get user from token. - .get(MethodDescriptor.path(ROOT_ROUTE + TOKEN_PATH) + .get(path(ROOT_ROUTE + TOKEN_PATH) .withDescription("Retrieves an " + persistence.clazz.getSimpleName() + " entity using an Auth0 access token passed in an Authorization header.") .withProduces(HttpUtils.JSON_ONLY) .withResponseType(persistence.clazz), this::getUserFromRequest, JsonUtils::toJson ) // Resend verification email - .get(MethodDescriptor.path(ROOT_ROUTE + VERIFICATION_EMAIL_PATH) + .get(path(ROOT_ROUTE + VERIFICATION_EMAIL_PATH) .withDescription("Triggers a job to resend the Auth0 verification email.") .withResponseType(Job.class), this::resendVerificationEmail, JsonUtils::toJson diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java index 3f1727323..b35af2470 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java @@ -3,7 +3,6 @@ import com.beerboy.ss.ApiEndpoint; import com.beerboy.ss.SparkSwagger; import com.beerboy.ss.descriptor.EndpointDescriptor; -import com.beerboy.ss.descriptor.MethodDescriptor; import com.beerboy.ss.descriptor.ParameterDescriptor; import com.beerboy.ss.rest.Endpoint; import com.fasterxml.jackson.core.JsonProcessingException; @@ -25,6 +24,8 @@ import spark.Request; import spark.Response; +import static com.beerboy.ss.descriptor.MethodDescriptor.path; +import static org.opentripplanner.middleware.utils.HttpUtils.getRequiredParamFromRequest; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -125,8 +126,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { baseEndpoint // Get multiple entities. - .get( - MethodDescriptor.path(ROOT_ROUTE) + .get(path(ROOT_ROUTE) .withDescription("Gets a paginated list of all '" + className + "' entities.") .withQueryParam(LIMIT) .withQueryParam(OFFSET) @@ -137,8 +137,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { ) // Get one entity. - .get( - MethodDescriptor.path(ROOT_ROUTE + ID_PATH) + .get(path(ROOT_ROUTE + ID_PATH) .withDescription("Returns the '" + className + "' entity with the specified id, or 404 if not found.") .withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The id of the entity to search.").and() // .withResponses(...) // FIXME: not implemented (requires source change). @@ -148,8 +147,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { ) // Create entity request - .post( - MethodDescriptor.path("") + .post(path("") .withDescription("Creates a '" + className + "' entity.") .withConsumes(HttpUtils.JSON_ONLY) .withRequestType(clazz) @@ -159,8 +157,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { ) // Update entity request - .put( - MethodDescriptor.path(ID_PATH) + .put(path(ID_PATH) .withDescription("Updates and returns the '" + className + "' entity with the specified id, or 404 if not found.") .withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The id of the entity to update.").and() .withConsumes(HttpUtils.JSON_ONLY) @@ -174,8 +171,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { ) // Delete entity request - .delete( - MethodDescriptor.path(ID_PATH) + .delete(path(ID_PATH) .withDescription("Deletes the '" + className + "' entity with the specified id if it exists.") .withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The id of the entity to delete.").and() .withProduces(HttpUtils.JSON_ONLY) @@ -198,9 +194,12 @@ private ResponseList getMany(Request req, Response res) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); if (userId != null) { OtpUser otpUser = Persistence.otpUsers.getById(userId); - return (otpUser != null && otpUser.canBeManagedBy(requestingUser)) ? - persistence.getResponseList(Filters.eq(USER_ID_PARAM, userId), offset, limit) : - null; + if(otpUser != null && otpUser.canBeManagedBy(requestingUser)) { + return persistence.getResponseList(Filters.eq(USER_ID_PARAM, userId), offset, limit); + } else { + res.status(HttpStatus.FORBIDDEN_403); + return null; + } } if (requestingUser.isAdmin()) { // If the user is admin, the context is presumed to be the admin dashboard, so we deliver all entities for @@ -208,9 +207,12 @@ private ResponseList getMany(Request req, Response res) { return persistence.getResponseList(offset, limit); } else if (persistence.clazz == OtpUser.class) { // If the required entity is of type 'OtpUser' the assumption is that a call is being made via the - // OtpUserController. Therefore, the request should be limited to return just the entity matching the - // requesting user. - return persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit); + // OtpUserController. If the request is being made by an Api user the response will be limited to the Otp users + // created by this Api user. If not, the assumption is that an Otp user is making the request and the response + // will be limited to just the entity matching this Otp user. + return (requestingUser.apiUser != null) + ? persistence.getResponseList(Filters.eq("applicationId", requestingUser.apiUser.id), offset, limit) + : persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit); } else if (requestingUser.isThirdPartyUser()) { // A user id must be provided if the request is being made by a third party user. logMessageAndHalt(req, @@ -392,6 +394,6 @@ private T createOrUpdate(Request req, Response res) { * Get entity ID from request. */ private String getIdFromRequest(Request req) { - return HttpUtils.getRequiredParamFromRequest(req, "id"); + return getRequiredParamFromRequest(req, "id"); } } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java index fa6092f52..c95cce017 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java @@ -2,7 +2,6 @@ import com.auth0.json.auth.TokenHolder; import com.beerboy.ss.ApiEndpoint; -import com.beerboy.ss.descriptor.MethodDescriptor; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.auth.Auth0Users; @@ -11,7 +10,6 @@ import org.opentripplanner.middleware.models.ApiUser; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.utils.ApiGatewayUtils; -import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.CreateApiKeyException; import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.JsonUtils; @@ -23,6 +21,8 @@ import java.net.http.HttpResponse; +import static com.beerboy.ss.descriptor.MethodDescriptor.path; +import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -31,7 +31,7 @@ */ public class ApiUserController extends AbstractUserController { private static final Logger LOG = LoggerFactory.getLogger(ApiUserController.class); - public static final String DEFAULT_USAGE_PLAN_ID = ConfigUtils.getConfigPropertyAsText("DEFAULT_USAGE_PLAN_ID"); + public static final String DEFAULT_USAGE_PLAN_ID = getConfigPropertyAsText("DEFAULT_USAGE_PLAN_ID"); private static final String API_KEY_PATH = "/apikey"; public static final String AUTHENTICATE_PATH = "/authenticate"; private static final int API_KEY_LIMIT_PER_USER = 2; @@ -51,7 +51,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { LOG.info("Registering path {}.", ROOT_ROUTE + ID_PATH + API_KEY_PATH); ApiEndpoint modifiedEndpoint = baseEndpoint // Create API key - .post(MethodDescriptor.path(ID_PATH + API_KEY_PATH) + .post(path(ID_PATH + API_KEY_PATH) .withDescription("Creates API key for ApiUser (with optional AWS API Gateway usage plan ID).") .withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The user ID") .and() @@ -62,7 +62,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { this::createApiKeyForApiUser, JsonUtils::toJson ) // Delete API key - .delete(MethodDescriptor.path(ID_PATH + API_KEY_PATH + API_KEY_ID_PARAM) + .delete(path(ID_PATH + API_KEY_PATH + API_KEY_ID_PARAM) .withDescription("Deletes API key for ApiUser.") .withPathParam().withName(ID_PARAM).withDescription("The user ID.") .and() @@ -72,7 +72,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { .withResponseType(persistence.clazz), this::deleteApiKeyForApiUser, JsonUtils::toJson) // Authenticate user with Auth0 - .post(MethodDescriptor.path(AUTHENTICATE_PATH) + .post(path(AUTHENTICATE_PATH) .withDescription("Authenticates ApiUser with Auth0.") .withPathParam().withName(ID_PARAM).withDescription("The user ID.").and() .withQueryParam().withName(USERNAME_PARAM).withRequired(true) @@ -89,8 +89,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { } /** - * Authenticate user with Auth0 based on username (email) and password. If successful, confirm that the provided API - * key is return the complete Auth0 + * Authenticate user with Auth0 based on username (email) and password. If successful, return the complete Auth0 * token else log message and halt. */ private TokenHolder authenticateAuth0User(Request req, Response res) { diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java index f776783ec..80523371d 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/LogController.java @@ -3,7 +3,6 @@ import com.amazonaws.services.apigateway.model.GetUsageResult; import com.beerboy.ss.SparkSwagger; import com.beerboy.ss.descriptor.EndpointDescriptor; -import com.beerboy.ss.descriptor.MethodDescriptor; import com.beerboy.ss.rest.Endpoint; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; @@ -23,6 +22,8 @@ import java.util.List; import java.util.stream.Collectors; +import static com.beerboy.ss.descriptor.MethodDescriptor.path; +import static org.opentripplanner.middleware.utils.DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -44,26 +45,26 @@ public void bind(final SparkSwagger restApi) { restApi.endpoint( EndpointDescriptor.endpointPath(ROOT_ROUTE).withDescription("Interface for retrieving API logs from AWS."), HttpUtils.NO_FILTER - ).get(MethodDescriptor.path(ROOT_ROUTE) + ).get(path(ROOT_ROUTE) .withDescription("Gets a list of all API usage logs.") .withQueryParam() .withName("keyId") .withDescription("If specified, restricts the search to the specified AWS API key ID.").and() .withQueryParam() .withName("startDate") - .withPattern(DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN) + .withPattern(DEFAULT_DATE_FORMAT_PATTERN) .withDefaultValue("30 days prior to the current date") .withDescription(String.format( "If specified, the earliest date (format %s) for which usage logs are retrieved.", - DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN + DEFAULT_DATE_FORMAT_PATTERN )).and() .withQueryParam() .withName("endDate") - .withPattern(DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN) + .withPattern(DEFAULT_DATE_FORMAT_PATTERN) .withDefaultValue("The current date") .withDescription(String.format( "If specified, the latest date (format %s) for which usage logs are retrieved.", - DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN + DEFAULT_DATE_FORMAT_PATTERN )).and() .withProduces(HttpUtils.JSON_ONLY) // Note: unlike what the name suggests, withResponseAsCollection does not generate an array @@ -95,7 +96,7 @@ private static List getUsageLogs(Request req, Response res) { LocalDateTime now = DateTimeUtils.nowAsLocalDateTime(); // TODO: Future work might modify this so that we accept multiple API key IDs for a single request (depends on // how third party developer accounts are structured). - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateTimeUtils.DEFAULT_DATE_FORMAT_PATTERN); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT_PATTERN); String startDate = req.queryParamOrDefault("startDate", formatter.format(now.minusDays(30))); String endDate = req.queryParamOrDefault("endDate", formatter.format(now)); try { diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index d1036f311..11e88c39e 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -2,7 +2,6 @@ import com.beerboy.ss.SparkSwagger; import com.beerboy.ss.descriptor.EndpointDescriptor; -import com.beerboy.ss.descriptor.MethodDescriptor; import com.beerboy.ss.descriptor.ParameterDescriptor; import com.beerboy.ss.rest.Endpoint; import org.eclipse.jetty.http.HttpStatus; @@ -17,7 +16,6 @@ import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.utils.DateTimeUtils; import org.opentripplanner.middleware.utils.HttpUtils; -import org.opentripplanner.middleware.utils.JsonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.Request; @@ -25,6 +23,7 @@ import javax.ws.rs.core.MediaType; import java.util.List; +import static com.beerboy.ss.descriptor.MethodDescriptor.path; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -66,8 +65,7 @@ public void bind(final SparkSwagger restApi) { restApi.endpoint( EndpointDescriptor.endpointPath(OTP_PROXY_ENDPOINT).withDescription("Proxy interface for OTP endpoints. " + OTP_DOC_LINK), HttpUtils.NO_FILTER - ).get( - MethodDescriptor.path("/*") + ).get(path("/*") .withDescription("Forwards any GET request to OTP. " + OTP_DOC_LINK) .withQueryParam(USER_ID) .withProduces(List.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)), @@ -130,7 +128,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons Auth0Connection.checkUser(request); RequestingUser requestingUser = Auth0Connection.getUserFromRequest(request); // A requesting user (Otp or third party user) is required to proceed. - if (requestingUser == null) { + if (requestingUser.otpUser == null && requestingUser.apiUser == null) { return; } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java index 97a097f77..753ed174a 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java @@ -44,12 +44,12 @@ public OtpUserController(String apiPrefix) { @Override OtpUser preCreateHook(OtpUser user, Request req) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); - String apiKeyFromHeader = req.headers("x-api-key"); - // If third party and an api key is present user is attempting to create an OTP user. - if (requestingUser.apiUser != null && apiKeyFromHeader != null) { + // If an Api user is present it is assumed an attempt is being made to create an OTP user. + if (requestingUser.apiUser != null) { + String apiKeyFromHeader = req.headers("x-api-key"); // Check API key and assign user to appropriate third-party application. Note: this is only relevant for // instances of otp-middleware running behind API Gateway. - if (requestingUser.apiUser.hasToken(apiKeyFromHeader)) { + if (apiKeyFromHeader != null && requestingUser.apiUser.hasToken(apiKeyFromHeader)) { // If third party and using their own api key, assign to new OTP user. user.applicationId = requestingUser.apiUser.id; } else { diff --git a/src/main/java/org/opentripplanner/middleware/docs/PublicApiDocGenerator.java b/src/main/java/org/opentripplanner/middleware/docs/PublicApiDocGenerator.java index 8762f8e7f..0f2d48fb1 100644 --- a/src/main/java/org/opentripplanner/middleware/docs/PublicApiDocGenerator.java +++ b/src/main/java/org/opentripplanner/middleware/docs/PublicApiDocGenerator.java @@ -30,6 +30,7 @@ import java.util.function.Predicate; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; +import static org.opentripplanner.middleware.utils.ConfigUtils.getVersionFromJar; /** * Class that generates an enhanced public-facing documentation, in OpenAPI 2.0 (Swagger) format, @@ -109,7 +110,7 @@ public Path generatePublicApiDocs() throws IOException { // Overwrite top-level parameters. swaggerRoot.put("host", AWS_API_SERVER); swaggerRoot.put("basePath", "/" + AWS_API_STAGE); - ((ObjectNode) swaggerRoot.get("info")).put("version", ConfigUtils.getVersionFromJar()); + ((ObjectNode) swaggerRoot.get("info")).put("version", getVersionFromJar()); // Generate output file. diff --git a/src/main/java/org/opentripplanner/middleware/tripMonitor/Main.java b/src/main/java/org/opentripplanner/middleware/tripMonitor/Main.java index 8772f768f..d2700a2e7 100644 --- a/src/main/java/org/opentripplanner/middleware/tripMonitor/Main.java +++ b/src/main/java/org/opentripplanner/middleware/tripMonitor/Main.java @@ -8,10 +8,12 @@ import java.io.IOException; import java.util.concurrent.TimeUnit; +import static org.opentripplanner.middleware.utils.ConfigUtils.loadConfig; + public class Main { public static void main(String[] args) throws IOException { // Load configuration. - ConfigUtils.loadConfig(args); + loadConfig(args); // Connect to MongoDB. Persistence.initialize(); diff --git a/src/main/java/org/opentripplanner/middleware/tripMonitor/jobs/CheckMonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/tripMonitor/jobs/CheckMonitoredTrip.java index 7c878adc0..ac1f9344e 100644 --- a/src/main/java/org/opentripplanner/middleware/tripMonitor/jobs/CheckMonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/tripMonitor/jobs/CheckMonitoredTrip.java @@ -14,7 +14,6 @@ import org.opentripplanner.middleware.otp.response.LocalizedAlert; import org.opentripplanner.middleware.otp.response.OtpResponse; import org.opentripplanner.middleware.persistence.Persistence; -import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.DateTimeUtils; import org.opentripplanner.middleware.utils.NotificationUtils; import org.slf4j.Logger; diff --git a/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java b/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java index 7593ed936..470e4c2e9 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java @@ -15,6 +15,8 @@ import java.time.temporal.ChronoField; import java.util.Date; +import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; + /** * Date and time specific utils. All timing in this application should be obtained by using this method in order to * ensure that the correct system clock is used. During testing, the internal clock is often set to a fixed instant to @@ -143,7 +145,7 @@ public static ZoneId getSystemZoneId() { * timezone identifier of the first agency that it finds. */ public static ZoneId getOtpZoneId() { - String otpTzId = ConfigUtils.getConfigPropertyAsText("OTP_TIMEZONE"); + String otpTzId = getConfigPropertyAsText("OTP_TIMEZONE"); if (otpTzId == null) { throw new RuntimeException("OTP_TIMEZONE is not defined in config!"); } diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index 3cf9135bb..23e01ffc2 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -25,6 +25,7 @@ import java.net.http.HttpResponse; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -156,6 +157,14 @@ public void canSimulateApiUserFlow() throws URISyntaxException { assertEquals(HttpStatus.OK_200, createUserResponse.statusCode()); + // Request all Otp users created by an Api user. This will work and return all Otp users. + HttpResponse getAllOtpUsersCreatedByApiUser = authenticatedGet("api/secure/user", + headers + ); + assertEquals(HttpStatus.OK_200, getAllOtpUsersCreatedByApiUser.statusCode()); + ResponseList otpUsers = JsonUtils.getPOJOFromJSON(getAllOtpUsersCreatedByApiUser.body(), ResponseList.class); + assertEquals(1, otpUsers.total); + // Attempt to create a monitored trip for an Otp user using mock authentication. This will fail because the user // was created by an Api user and therefore does not have a Auth0 account. OtpUser otpUserResponse = JsonUtils.getPOJOFromJSON(createUserResponse.body(), OtpUser.class); @@ -189,7 +198,7 @@ public void canSimulateApiUserFlow() throws URISyntaxException { ); assertEquals(HttpStatus.OK_200, getAllMonitoredTripsForOtpUser.statusCode()); - // Request all monitored trips for an Otp user authenticating as an Api user. Without defining the user id. This + // Request all monitored trips for an Otp user authenticating as an Api user, without defining the user id. This // will fail because an Api user must provide a user id. getAllMonitoredTripsForOtpUser = authenticatedRequest("api/secure/monitoredtrip", "", From 86581bb3d89c0fb6948a27d7dc6aecf5b6d5681a Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Tue, 3 Nov 2020 10:47:24 +0000 Subject: [PATCH 21/30] refactor(Addressed PR feedback): Addressed PR feedback --- .../middleware/controllers/api/ApiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java index b35af2470..ff5610ab8 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java @@ -194,7 +194,7 @@ private ResponseList getMany(Request req, Response res) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); if (userId != null) { OtpUser otpUser = Persistence.otpUsers.getById(userId); - if(otpUser != null && otpUser.canBeManagedBy(requestingUser)) { + if (otpUser != null && otpUser.canBeManagedBy(requestingUser)) { return persistence.getResponseList(Filters.eq(USER_ID_PARAM, userId), offset, limit); } else { res.status(HttpStatus.FORBIDDEN_403); From feef7f54c205f48f1ee9dd3de77bc42961947ad5 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Thu, 5 Nov 2020 14:29:16 +0000 Subject: [PATCH 22/30] refactor(Addressed edge case with matching otp and api users when processing OTP plan response): Add --- .../controllers/api/OtpRequestProcessor.java | 10 +++---- .../middleware/ApiUserFlowTest.java | 29 ++++++++++++++----- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index 11e88c39e..da6fb73bd 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -131,16 +131,16 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons if (requestingUser.otpUser == null && requestingUser.apiUser == null) { return; } - - // Determine if the Otp request is being made by an actual Otp user or by a third party on behalf of an Otp user. + // If a user id is provided, the assumption is that an Api user is making a plan request on behalf of an Otp user. + String userId = request.queryParams(USER_ID_PARAM); OtpUser otpUser = null; - if (requestingUser.otpUser != null) { + if (requestingUser.otpUser != null && userId == null) { // Otp user making a trip request for self. otpUser = requestingUser.otpUser; - } else if (requestingUser.isThirdPartyUser()) { + } else if (requestingUser.apiUser != null) { // Api user making a trip request on behalf of an Otp user. In this case, the Otp user id must be provided // as a query parameter. - otpUser = Persistence.otpUsers.getById(request.queryParams(USER_ID_PARAM)); + otpUser = Persistence.otpUsers.getById(userId); if (otpUser != null && !otpUser.canBeManagedBy(requestingUser)) { logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index 23e01ffc2..99c1ca827 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -25,7 +25,6 @@ import java.net.http.HttpResponse; import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.List; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -70,6 +69,7 @@ public class ApiUserFlowTest { private static final Logger LOG = LoggerFactory.getLogger(ApiUserFlowTest.class); private static ApiUser apiUser; private static OtpUser otpUser; + private static OtpUser otpUserMatchingApiUser; /** * Whether tests for this class should run. End to End must be enabled and Auth must NOT be disabled. This should be @@ -104,6 +104,11 @@ public static void setUp() throws IOException, InterruptedException, CreateApiKe // update api user with valid auth0 user ID (so the Auth0 delete works) apiUser.auth0UserId = auth0User.getId(); Persistence.apiUsers.replace(apiUser.id, apiUser); + // create Otp user matching Api user to aid with testing edge cases. + otpUserMatchingApiUser = new OtpUser(); + otpUserMatchingApiUser.email = apiUser.email; + otpUserMatchingApiUser.auth0UserId = apiUser.auth0UserId; + Persistence.otpUsers.create(otpUserMatchingApiUser); } catch (Auth0Exception e) { throw new RuntimeException(e); } @@ -119,6 +124,8 @@ public static void tearDown() { if (apiUser != null) apiUser.delete(); otpUser = Persistence.otpUsers.getById(otpUser.id); if (otpUser != null) otpUser.delete(false); + otpUserMatchingApiUser = Persistence.otpUsers.getById(otpUserMatchingApiUser.id); + if (otpUserMatchingApiUser != null) otpUserMatchingApiUser.delete(false); } /** @@ -209,19 +216,27 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // Plan trip with OTP proxy authenticating as an OTP user. Mock plan response will be returned. This will work - // as an Otp user (created by MOD UI or an Api user) because the end point has no auth. - String otpQuery = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745&userId=" + otpUserResponse.id; - HttpResponse planTripResponseAsOtpUser = mockAuthenticatedGet(otpQuery, + // as an Otp user (created by MOD UI or an Api user) because the end point has no auth. A lack of auth also means + // the plan is not saved. + String otpQueryForOtpUserRequest = OTP_PROXY_ENDPOINT + + OTP_PLAN_ENDPOINT + + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745"; + HttpResponse planTripResponseAsOtpUser = mockAuthenticatedGet(otpQueryForOtpUserRequest, otpUserResponse, true ); - LOG.info("Plan trip response: {}\n....", planTripResponseAsOtpUser.body().substring(0, 300)); + LOG.info("OTP user: Plan trip response: {}\n....", planTripResponseAsOtpUser.body().substring(0, 300)); assertEquals(HttpStatus.OK_200, planTripResponseAsOtpUser.statusCode()); + + // Plan trip with OTP proxy authenticating as an Api user. Mock plan response will be returned. This will work // as an Api user because the end point has no auth. - HttpResponse planTripResponseAsApiUser = authenticatedGet(otpQuery,headers); - LOG.info("Plan trip response: {}\n....", planTripResponseAsApiUser.body().substring(0, 300)); + String otpQueryForApiUserRequest = OTP_PROXY_ENDPOINT + + OTP_PLAN_ENDPOINT + + String.format("?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745&userId=%s",otpUserResponse.id); + HttpResponse planTripResponseAsApiUser = authenticatedGet(otpQueryForApiUserRequest, headers); + LOG.info("API user (on behalf of an Otp user): Plan trip response: {}\n....", planTripResponseAsApiUser.body().substring(0, 300)); assertEquals(HttpStatus.OK_200, planTripResponseAsApiUser.statusCode()); // Get trip request history for user authenticating as an Otp user. This will fail because the user was created From 6b504c2339e279d7bee3e3ce1160bfa67b2ce576 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Thu, 5 Nov 2020 16:03:01 +0000 Subject: [PATCH 23/30] refactor(OtpRequestProcessor): Added additional OTP user check and TODO --- .../middleware/controllers/api/OtpRequestProcessor.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index da6fb73bd..4b57ebc7f 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -131,6 +131,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons if (requestingUser.otpUser == null && requestingUser.apiUser == null) { return; } + // TODO: Consider moving/replicating these checks (or a subset of) to before sending the request to OTP. // If a user id is provided, the assumption is that an Api user is making a plan request on behalf of an Otp user. String userId = request.queryParams(USER_ID_PARAM); OtpUser otpUser = null; @@ -141,7 +142,9 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons // Api user making a trip request on behalf of an Otp user. In this case, the Otp user id must be provided // as a query parameter. otpUser = Persistence.otpUsers.getById(userId); - if (otpUser != null && !otpUser.canBeManagedBy(requestingUser)) { + if (otpUser == null) { + logMessageAndHalt(request, HttpStatus.NOT_FOUND_404, "The specified user id was not found."); + } else if (!otpUser.canBeManagedBy(requestingUser)) { logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, String.format("User: %s not authorized to make trip requests for user: %s", From 7729deb1f620ff20be079c3fe31058372dcc3f35 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Thu, 5 Nov 2020 17:19:53 +0000 Subject: [PATCH 24/30] refactor(OtpRequestProcessor): Minor update so ApiUser can still make OTP requests without specifyin --- .../middleware/controllers/api/OtpRequestProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index 4b57ebc7f..cef678c35 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -142,7 +142,7 @@ private static void handlePlanTripResponse(Request request, OtpDispatcherRespons // Api user making a trip request on behalf of an Otp user. In this case, the Otp user id must be provided // as a query parameter. otpUser = Persistence.otpUsers.getById(userId); - if (otpUser == null) { + if (otpUser == null && userId != null) { logMessageAndHalt(request, HttpStatus.NOT_FOUND_404, "The specified user id was not found."); } else if (!otpUser.canBeManagedBy(requestingUser)) { logMessageAndHalt(request, From bb80a154e1d0eb0f39eb8d64f3cdca784fbf623a Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 6 Nov 2020 12:04:39 -0500 Subject: [PATCH 25/30] refactor(TestUtils): rename request methods, remove mockHeaders arg --- .../middleware/ApiKeyManagementTest.java | 5 +- .../middleware/ApiUserFlowTest.java | 117 ++++++++---------- .../middleware/GetMonitoredTripsTest.java | 48 +++---- .../opentripplanner/middleware/TestUtils.java | 55 +++++--- .../api/OtpUserControllerTest.java | 6 +- 5 files changed, 124 insertions(+), 107 deletions(-) diff --git a/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java b/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java index cabdfe5c5..bdcb64500 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.opentripplanner.middleware.TestUtils.isEndToEnd; +import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedDelete; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; import static org.opentripplanner.middleware.auth.Auth0Connection.setAuthDisabled; @@ -161,7 +162,7 @@ private boolean ensureApiKeyExists() { */ private HttpResponse createApiKeyRequest(String targetUserId, AbstractUser requestingUser) { String path = String.format("api/secure/application/%s/apikey", targetUserId); - return mockAuthenticatedRequest(path, "", requestingUser, HttpUtils.REQUEST_METHOD.POST, true); + return mockAuthenticatedRequest(path, "", requestingUser, HttpUtils.REQUEST_METHOD.POST); } /** @@ -169,6 +170,6 @@ private HttpResponse createApiKeyRequest(String targetUserId, AbstractUs */ private static HttpResponse deleteApiKeyRequest(String targetUserId, String apiKeyId, AbstractUser requestingUser) { String path = String.format("api/secure/application/%s/apikey/%s", targetUserId, apiKeyId); - return mockAuthenticatedRequest(path, "", requestingUser, HttpUtils.REQUEST_METHOD.DELETE, true); + return mockAuthenticatedDelete(path, requestingUser); } } diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index 99c1ca827..b524f3ee3 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -24,15 +24,15 @@ import java.net.URISyntaxException; import java.net.http.HttpResponse; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.opentripplanner.middleware.TestUtils.TEMP_AUTH0_USER_PASSWORD; -import static org.opentripplanner.middleware.TestUtils.authenticatedGet; -import static org.opentripplanner.middleware.TestUtils.authenticatedRequest; +import static org.opentripplanner.middleware.TestUtils.makeDeleteRequest; +import static org.opentripplanner.middleware.TestUtils.makeGetRequest; +import static org.opentripplanner.middleware.TestUtils.makeRequest; import static org.opentripplanner.middleware.TestUtils.isEndToEnd; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedGet; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; @@ -70,6 +70,8 @@ public class ApiUserFlowTest { private static ApiUser apiUser; private static OtpUser otpUser; private static OtpUser otpUserMatchingApiUser; + private static final String OTP_USER_PATH = "api/secure/user"; + private static final String MONITORED_TRIP_PATH = "api/secure/monitoredtrip"; /** * Whether tests for this class should run. End to End must be enabled and Auth must NOT be disabled. This should be @@ -130,46 +132,49 @@ public static void tearDown() { /** * Tests to confirm that an otp user, related monitored trip and plan can be created and deleted leaving no orphaned - * records. This also includes Auth0 users if auth is enabled. + * records. This also includes Auth0 users if auth is enabled. The basic script for this test is as follows: + * 1. Start with existing API User that has a valid API key. + * 2. Use the API key to make a request to the authenticate endpoint to get Auth0 token for further requests. + * 3. Create OTP user. + * 4. Make subsequent requests on behalf of OTP users. + * 5. Delete user and verify that their associated objects are also deleted. */ @Test public void canSimulateApiUserFlow() throws URISyntaxException { // Define the header values to be used in requests from this point forward. - HashMap headers = new HashMap<>(); - headers.put("x-api-key", apiUser.apiKeys.get(0).value); - + HashMap apiUserHeaders = new HashMap<>(); + apiUserHeaders.put("x-api-key", apiUser.apiKeys.get(0).value); // obtain Auth0 token for Api user. - String endpoint = String.format("api/secure/application/authenticate?username=%s&password=%s", + String authenticateEndpoint = String.format("api/secure/application/authenticate?username=%s&password=%s", apiUser.email, TEMP_AUTH0_USER_PASSWORD); - HttpResponse getTokenResponse = authenticatedRequest(endpoint, + HttpResponse getTokenResponse = makeRequest(authenticateEndpoint, "", - headers, + apiUserHeaders, HttpUtils.REQUEST_METHOD.POST ); - LOG.info(getTokenResponse.body()); + // Note: do not log the Auth0 token (could be a security risk). + LOG.info("Token response status: {}", getTokenResponse.statusCode()); assertEquals(HttpStatus.OK_200, getTokenResponse.statusCode()); TokenHolder tokenHolder = JsonUtils.getPOJOFromJSON(getTokenResponse.body(), TokenHolder.class); // Define the bearer value to be used in requests from this point forward. - headers.put("Authorization", "Bearer " + tokenHolder.getAccessToken()); + apiUserHeaders.put("Authorization", "Bearer " + tokenHolder.getAccessToken()); // create an Otp user authenticating as an Api user. - HttpResponse createUserResponse = authenticatedRequest("api/secure/user", + HttpResponse createUserResponse = makeRequest(OTP_USER_PATH, JsonUtils.toJson(otpUser), - headers, + apiUserHeaders, HttpUtils.REQUEST_METHOD.POST ); assertEquals(HttpStatus.OK_200, createUserResponse.statusCode()); // Request all Otp users created by an Api user. This will work and return all Otp users. - HttpResponse getAllOtpUsersCreatedByApiUser = authenticatedGet("api/secure/user", - headers - ); + HttpResponse getAllOtpUsersCreatedByApiUser = makeGetRequest(OTP_USER_PATH, apiUserHeaders); assertEquals(HttpStatus.OK_200, getAllOtpUsersCreatedByApiUser.statusCode()); - ResponseList otpUsers = JsonUtils.getPOJOFromJSON(getAllOtpUsersCreatedByApiUser.body(), ResponseList.class); + ResponseList otpUsers = JsonUtils.getPOJOFromJSON(getAllOtpUsersCreatedByApiUser.body(), ResponseList.class); assertEquals(1, otpUsers.total); // Attempt to create a monitored trip for an Otp user using mock authentication. This will fail because the user @@ -179,37 +184,41 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // Create a monitored trip for the Otp user (API users are prevented from doing this). MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); monitoredTrip.userId = otpUser.id; - HttpResponse createTripResponseAsOtpUser = mockAuthenticatedRequest("api/secure/monitoredtrip", + HttpResponse createTripResponseAsOtpUser = mockAuthenticatedRequest( + MONITORED_TRIP_PATH, JsonUtils.toJson(monitoredTrip), otpUserResponse, - HttpUtils.REQUEST_METHOD.POST, - true + HttpUtils.REQUEST_METHOD.POST ); assertEquals(HttpStatus.UNAUTHORIZED_401, createTripResponseAsOtpUser.statusCode()); // Create a monitored trip for an Otp user authenticating as an Api user. An Api user can create a monitored // trip for an Otp user they created. - HttpResponse createTripResponseAsApiUser = authenticatedRequest("api/secure/monitoredtrip", + HttpResponse createTripResponseAsApiUser = makeRequest( + MONITORED_TRIP_PATH, JsonUtils.toJson(monitoredTrip), - headers, + apiUserHeaders, HttpUtils.REQUEST_METHOD.POST ); assertEquals(HttpStatus.OK_200, createTripResponseAsApiUser.statusCode()); - MonitoredTrip monitoredTripResponse = JsonUtils.getPOJOFromJSON(createTripResponseAsApiUser.body(), MonitoredTrip.class); + MonitoredTrip monitoredTripResponse = JsonUtils.getPOJOFromJSON( + createTripResponseAsApiUser.body(), + MonitoredTrip.class + ); // Request all monitored trips for an Otp user authenticating as an Api user. This will work and return all trips // matching the user id provided. - HttpResponse getAllMonitoredTripsForOtpUser = authenticatedGet(String.format("api/secure/monitoredtrip?userId=%s", - otpUserResponse.id), - headers + HttpResponse getAllMonitoredTripsForOtpUser = makeGetRequest( + String.format("api/secure/monitoredtrip?userId=%s", otpUserResponse.id), + apiUserHeaders ); assertEquals(HttpStatus.OK_200, getAllMonitoredTripsForOtpUser.statusCode()); // Request all monitored trips for an Otp user authenticating as an Api user, without defining the user id. This // will fail because an Api user must provide a user id. - getAllMonitoredTripsForOtpUser = authenticatedRequest("api/secure/monitoredtrip", + getAllMonitoredTripsForOtpUser = makeRequest(MONITORED_TRIP_PATH, "", - headers, + apiUserHeaders, HttpUtils.REQUEST_METHOD.GET ); assertEquals(HttpStatus.BAD_REQUEST_400, getAllMonitoredTripsForOtpUser.statusCode()); @@ -221,10 +230,7 @@ public void canSimulateApiUserFlow() throws URISyntaxException { String otpQueryForOtpUserRequest = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + "?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745"; - HttpResponse planTripResponseAsOtpUser = mockAuthenticatedGet(otpQueryForOtpUserRequest, - otpUserResponse, - true - ); + HttpResponse planTripResponseAsOtpUser = mockAuthenticatedGet(otpQueryForOtpUserRequest, otpUserResponse); LOG.info("OTP user: Plan trip response: {}\n....", planTripResponseAsOtpUser.body().substring(0, 300)); assertEquals(HttpStatus.OK_200, planTripResponseAsOtpUser.statusCode()); @@ -235,47 +241,36 @@ public void canSimulateApiUserFlow() throws URISyntaxException { String otpQueryForApiUserRequest = OTP_PROXY_ENDPOINT + OTP_PLAN_ENDPOINT + String.format("?fromPlace=28.45119,-81.36818&toPlace=28.54834,-81.37745&userId=%s",otpUserResponse.id); - HttpResponse planTripResponseAsApiUser = authenticatedGet(otpQueryForApiUserRequest, headers); + HttpResponse planTripResponseAsApiUser = makeGetRequest(otpQueryForApiUserRequest, apiUserHeaders); LOG.info("API user (on behalf of an Otp user): Plan trip response: {}\n....", planTripResponseAsApiUser.body().substring(0, 300)); assertEquals(HttpStatus.OK_200, planTripResponseAsApiUser.statusCode()); // Get trip request history for user authenticating as an Otp user. This will fail because the user was created // by an Api user and therefore does not have a Auth0 account. - HttpResponse tripRequestResponseAsOtUser = mockAuthenticatedGet(String.format("api/secure/triprequests?userId=%s", - otpUserResponse.id), - otpUserResponse, - true - ); + String tripRequestsPath = String.format("api/secure/triprequests?userId=%s", otpUserResponse.id); + HttpResponse tripRequestResponseAsOtUser = mockAuthenticatedGet(tripRequestsPath, otpUserResponse); assertEquals(HttpStatus.UNAUTHORIZED_401, tripRequestResponseAsOtUser.statusCode()); // Get trip request history for user authenticating as an Api user. This will work because an Api user is able // to get a trip on behalf of an Otp user they created. - HttpResponse tripRequestResponseAsApiUser = authenticatedGet(String.format("api/secure/triprequests?userId=%s", - otpUserResponse.id), - headers - ); + HttpResponse tripRequestResponseAsApiUser = makeGetRequest(tripRequestsPath, apiUserHeaders); assertEquals(HttpStatus.OK_200, tripRequestResponseAsApiUser.statusCode()); - ResponseList tripRequests = JsonUtils.getPOJOFromJSON(tripRequestResponseAsApiUser.body(), ResponseList.class); + ResponseList tripRequests = JsonUtils.getPOJOFromJSON( + tripRequestResponseAsApiUser.body(), + ResponseList.class + ); // Delete Otp user authenticating as an Otp user. This will fail because the user was created by an Api user and // therefore does not have a Auth0 account. - HttpResponse deleteUserResponseAsOtpUser = mockAuthenticatedGet( - String.format("api/secure/user/%s", otpUserResponse.id), - otpUserResponse, - true - ); + String otpUserPath = String.format("api/secure/user/%s", otpUserResponse.id); + HttpResponse deleteUserResponseAsOtpUser = mockAuthenticatedGet(otpUserPath, otpUserResponse); assertEquals(HttpStatus.UNAUTHORIZED_401, deleteUserResponseAsOtpUser.statusCode()); // Delete Otp user authenticating as an Api user. This will work because an Api user can delete an Otp user they // created. - HttpResponse deleteUserResponseAsApiUser = authenticatedRequest( - String.format("api/secure/user/%s", otpUserResponse.id), - "", - headers, - HttpUtils.REQUEST_METHOD.DELETE - ); + HttpResponse deleteUserResponseAsApiUser = makeDeleteRequest(otpUserPath, apiUserHeaders); assertEquals(HttpStatus.OK_200, deleteUserResponseAsApiUser.statusCode()); // Verify user no longer exists. @@ -287,16 +282,14 @@ public void canSimulateApiUserFlow() throws URISyntaxException { assertNull(deletedTrip); // Verify trip request no longer exists. - LinkedHashMap trip = (LinkedHashMap) tripRequests.data.get(0); - TripRequest tripRequest = Persistence.tripRequests.getById(trip.get("id").toString()); - assertNull(tripRequest); + TripRequest tripRequestFromResponse = tripRequests.data.get(0); + TripRequest tripRequestFromDb = Persistence.tripRequests.getById(tripRequestFromResponse.id); + assertNull(tripRequestFromDb); // Delete API user (this would happen through the OTP Admin portal). - HttpResponse deleteApiUserResponse = authenticatedRequest( + HttpResponse deleteApiUserResponse = makeDeleteRequest( String.format("api/secure/application/%s", apiUser.id), - "", - headers, - HttpUtils.REQUEST_METHOD.DELETE + apiUserHeaders ); assertEquals(HttpStatus.OK_200, deleteApiUserResponse.statusCode()); diff --git a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java index 2ffabac48..d42d99c27 100644 --- a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java +++ b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java @@ -11,6 +11,7 @@ import org.opentripplanner.middleware.models.AdminUser; import org.opentripplanner.middleware.models.MonitoredTrip; import org.opentripplanner.middleware.models.OtpUser; +import org.opentripplanner.middleware.models.TripRequest; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.persistence.PersistenceUtil; import org.opentripplanner.middleware.utils.HttpUtils; @@ -25,6 +26,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.opentripplanner.middleware.TestUtils.TEMP_AUTH0_USER_PASSWORD; import static org.opentripplanner.middleware.TestUtils.isEndToEnd; +import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedGet; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; @@ -50,6 +52,7 @@ public class GetMonitoredTripsTest { private static AdminUser adminUser; private static OtpUser otpUser1; private static OtpUser otpUser2; + private static final String MONITORED_TRIP_PATH = "api/secure/monitoredtrip"; /** * Whether tests for this class should run. End to End must be enabled and Auth must NOT be disabled. This should be @@ -116,48 +119,47 @@ public void canGetOwnMonitoredTrips() throws URISyntaxException { // Create trip as Otp user 1. MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); monitoredTrip.userId = otpUser1.id; - HttpResponse response = mockAuthenticatedRequest("api/secure/monitoredtrip", + HttpResponse createTripResponse = mockAuthenticatedRequest(MONITORED_TRIP_PATH, JsonUtils.toJson(monitoredTrip), otpUser1, - HttpUtils.REQUEST_METHOD.POST, - true + HttpUtils.REQUEST_METHOD.POST ); - assertEquals(HttpStatus.OK_200, response.statusCode()); + assertEquals(HttpStatus.OK_200, createTripResponse.statusCode()); // Create trip as Otp user 2. monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); monitoredTrip.userId = otpUser2.id; - response = mockAuthenticatedRequest("api/secure/monitoredtrip", + HttpResponse createTripResponse2 = mockAuthenticatedRequest(MONITORED_TRIP_PATH, JsonUtils.toJson(monitoredTrip), otpUser2, - HttpUtils.REQUEST_METHOD.POST, - true + HttpUtils.REQUEST_METHOD.POST ); - assertEquals(HttpStatus.OK_200, response.statusCode()); + assertEquals(HttpStatus.OK_200, createTripResponse2.statusCode()); + + // Get trips for Otp user 1. + HttpResponse getTripsResponse1 = mockAuthenticatedGet(MONITORED_TRIP_PATH, otpUser2); + ResponseList trips = JsonUtils.getPOJOFromJSON(getTripsResponse1.body(), ResponseList.class); + + // Expect only 1 trip for Otp user 1. + assertEquals(1, trips.data.size()); // Get trips for Otp user 2. - response = mockAuthenticatedRequest("api/secure/monitoredtrip", - "", - otpUser2, - HttpUtils.REQUEST_METHOD.GET, - true - ); - ResponseList tripRequests = JsonUtils.getPOJOFromJSON(response.body(), ResponseList.class); + HttpResponse getTripsCombined = mockAuthenticatedGet(MONITORED_TRIP_PATH, otpUser2); + ResponseList tripsCombined = JsonUtils.getPOJOFromJSON(getTripsCombined.body(), ResponseList.class); // Otp user 2 has 'enhanced' admin credentials both trips will be returned. The expectation here is that the UI // will always provide the user id to prevent this (as with the next test). - assertEquals(2, tripRequests.data.size()); + // TODO: Determine if a separate admin endpoint should be maintained for getting all/combined trips. + assertEquals(2, tripsCombined.data.size()); // Get trips for Otp user 2 defining user id. - response = mockAuthenticatedRequest(String.format("api/secure/monitoredtrip?userId=%s", otpUser2.id), - "", - otpUser2, - HttpUtils.REQUEST_METHOD.GET, - true + HttpResponse getTripsFiltered = mockAuthenticatedGet( + String.format("api/secure/monitoredtrip?userId=%s", otpUser2.id), + otpUser2 ); - tripRequests = JsonUtils.getPOJOFromJSON(response.body(), ResponseList.class); + ResponseList tripsFiltered = JsonUtils.getPOJOFromJSON(getTripsFiltered.body(), ResponseList.class); // Just the trip for Otp user 2 will be returned. - assertEquals(1, tripRequests.data.size()); + assertEquals(1, tripsFiltered.data.size()); } } diff --git a/src/test/java/org/opentripplanner/middleware/TestUtils.java b/src/test/java/org/opentripplanner/middleware/TestUtils.java index 8cb0cd4fb..c3d85ff9c 100644 --- a/src/test/java/org/opentripplanner/middleware/TestUtils.java +++ b/src/test/java/org/opentripplanner/middleware/TestUtils.java @@ -36,6 +36,11 @@ public class TestUtils { private static final Logger LOG = LoggerFactory.getLogger(TestUtils.class); private static final ObjectMapper mapper = new ObjectMapper(); + /** + * Base URL for application running during testing. + */ + private static final String BASE_URL = "http://localhost:4567/"; + /** * Password used to create and validate temporary Auth0 users */ @@ -119,13 +124,12 @@ private static HashMap getMockHeaders(AbstractUser requestingUse /** - * Send request to provided URL placing the Auth0 user id in the headers so that {@link RequestingUser} can check - * the database for a matching user. + * Send request to provided URL. */ - static HttpResponse authenticatedRequest(String path, String body, HashMap headers, - HttpUtils.REQUEST_METHOD requestMethod) { + static HttpResponse makeRequest(String path, String body, HashMap headers, + HttpUtils.REQUEST_METHOD requestMethod) { return HttpUtils.httpRequestRawResponse( - URI.create("http://localhost:4567/" + path), + URI.create(BASE_URL + path), 1000, requestMethod, headers, @@ -134,12 +138,11 @@ static HttpResponse authenticatedRequest(String path, String body, HashM } /** - * Send 'get' request to provided URL placing the Auth0 user id in the headers so that {@link RequestingUser} can - * check the database for a matching user. + * Send 'get' request to provided URL. */ - static HttpResponse authenticatedGet(String path, HashMap headers) { + static HttpResponse makeGetRequest(String path, HashMap headers) { return HttpUtils.httpRequestRawResponse( - URI.create("http://localhost:4567/" + path), + URI.create(BASE_URL + path), 1000, HttpUtils.REQUEST_METHOD.GET, headers, @@ -148,20 +151,40 @@ static HttpResponse authenticatedGet(String path, HashMap makeDeleteRequest(String path, HashMap headers) { + return HttpUtils.httpRequestRawResponse( + URI.create(BASE_URL + path), + 1000, + HttpUtils.REQUEST_METHOD.DELETE, + headers, + "" + ); + } + + /** + * Construct http headers according to caller request and then make an authenticated call by placing the Auth0 user + * id in the headers so that {@link RequestingUser} can check the database for a matching user. */ static HttpResponse mockAuthenticatedRequest(String path, String body, AbstractUser requestingUser, - HttpUtils.REQUEST_METHOD requestMethod, boolean mockHeaders) { - HashMap headers = (mockHeaders) ? getMockHeaders(requestingUser) : null; - return authenticatedRequest(path, body, headers, requestMethod); + HttpUtils.REQUEST_METHOD requestMethod) { + return makeRequest(path, body, getMockHeaders(requestingUser), requestMethod); } /** * Construct http headers according to caller request and then make an authenticated 'get' call. */ - public static HttpResponse mockAuthenticatedGet(String path, AbstractUser requestingUser, boolean mockHeaders) { - HashMap headers = (mockHeaders) ? getMockHeaders(requestingUser) : null; - return authenticatedRequest(path, "", headers, HttpUtils.REQUEST_METHOD.GET); + public static HttpResponse mockAuthenticatedGet(String path, AbstractUser requestingUser) { + return makeGetRequest(path, getMockHeaders(requestingUser)); + } + + /** + * Construct http headers according to caller request and then make an authenticated call by placing the Auth0 user + * id in the headers so that {@link RequestingUser} can check the database for a matching user. + */ + static HttpResponse mockAuthenticatedDelete(String path, AbstractUser requestingUser) { + return makeDeleteRequest(path, getMockHeaders(requestingUser)); } diff --git a/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java b/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java index 88471dfe8..8f6744311 100644 --- a/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java +++ b/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java @@ -69,8 +69,7 @@ public void invalidNumbersShouldProduceBadRequest(String badNumber, int statusCo otpUser.id, badNumber ), - otpUser, - true + otpUser ); assertEquals(statusCode, response.statusCode()); @@ -78,8 +77,7 @@ public void invalidNumbersShouldProduceBadRequest(String badNumber, int statusCo // The phone number should not be updated. HttpResponse otpUserWithPhoneRequest = mockAuthenticatedGet( String.format("api/secure/user/%s", otpUser.id), - otpUser, - true + otpUser ); assertEquals(HttpStatus.OK_200, otpUserWithPhoneRequest.statusCode()); From 1b57450cc432d4a7e6b71de07ecc505f474ecca2 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 6 Nov 2020 14:35:48 -0500 Subject: [PATCH 26/30] refactor: fix tests and add restoreDefaultAuthDisabled --- .../middleware/auth/Auth0Connection.java | 11 +- .../middleware/models/MonitoredTrip.java | 12 +- .../middleware/ApiKeyManagementTest.java | 6 +- .../middleware/GetMonitoredTripsTest.java | 106 +++++++++--------- .../api/OtpUserControllerTest.java | 10 +- 5 files changed, 69 insertions(+), 76 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 900c385bd..60e4a5b62 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -222,12 +222,21 @@ public static boolean isAuthDisabled() { } /** - * Override the current {@link #authDisabled} value. + * Override the current {@link #authDisabled} value. This is used principally for setting up test environments that + * require auth to be disabled. */ public static void setAuthDisabled(boolean authDisabled) { Auth0Connection.authDisabled = authDisabled; } + /** + * Restore default {@link #authDisabled} value. This is used principally for tearing down test environments that + * require auth to be disabled. + */ + public static void restoreDefaultAuthDisabled() { + setAuthDisabled(getDefaultAuthDisabled()); + } + /** * Confirm that the user exists in at least one of the MongoDB user collections. */ diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index 0bae4b17a..3b19f9a5c 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -1,6 +1,8 @@ package org.opentripplanner.middleware.models; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.mongodb.client.model.Filters; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; @@ -324,16 +326,6 @@ public Map parseQueryParams() throws URISyntaxException { ).stream().collect(Collectors.toMap(NameValuePair::getName, NameValuePair::getValue)); } - /** - * Check if the trip is planned with the target time being an arriveBy or departAt query. - * - * @return true, if the trip's target time is for an arriveBy query - */ - public boolean isArriveBy() throws URISyntaxException { - // if arriveBy is not included in query params, OTP will default to false, so initialize to false - return parseQueryParams().getOrDefault("arriveBy", "false").equals("true"); - } - /** * Returns the target hour of the day that the trip is either departing at or arriving by */ diff --git a/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java b/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java index bdcb64500..37c1b9a71 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiKeyManagementTest.java @@ -24,7 +24,9 @@ import static org.opentripplanner.middleware.TestUtils.isEndToEnd; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedDelete; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; +import static org.opentripplanner.middleware.auth.Auth0Connection.getDefaultAuthDisabled; import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; +import static org.opentripplanner.middleware.auth.Auth0Connection.restoreDefaultAuthDisabled; import static org.opentripplanner.middleware.auth.Auth0Connection.setAuthDisabled; import static org.opentripplanner.middleware.controllers.api.ApiUserController.DEFAULT_USAGE_PLAN_ID; @@ -41,7 +43,6 @@ public class ApiKeyManagementTest extends OtpMiddlewareTest { private static final Logger LOG = LoggerFactory.getLogger(ApiKeyManagementTest.class); private static ApiUser apiUser; private static AdminUser adminUser; - private static boolean prevAuthState; /** * Create an {@link ApiUser} and an {@link AdminUser} prior to unit tests @@ -51,7 +52,6 @@ public static void setUp() throws IOException, InterruptedException { assumeTrue(isEndToEnd); // TODO: It might be useful to allow this to run without DISABLE_AUTH set to true (in an end-to-end environment // using real tokens from Auth0. - prevAuthState = isAuthDisabled(); setAuthDisabled(true); // Load config before checking if tests should run. OtpMiddlewareTest.setUp(); @@ -69,7 +69,7 @@ public static void tearDown() { apiUser = Persistence.apiUsers.getById(apiUser.id); apiUser.delete(); Persistence.adminUsers.removeById(adminUser.id); - setAuthDisabled(prevAuthState); + restoreDefaultAuthDisabled(); } /** diff --git a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java index d42d99c27..539566577 100644 --- a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java +++ b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java @@ -16,6 +16,8 @@ import org.opentripplanner.middleware.persistence.PersistenceUtil; import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.JsonUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URISyntaxException; @@ -28,7 +30,8 @@ import static org.opentripplanner.middleware.TestUtils.isEndToEnd; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedGet; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedRequest; -import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; +import static org.opentripplanner.middleware.auth.Auth0Connection.restoreDefaultAuthDisabled; +import static org.opentripplanner.middleware.auth.Auth0Connection.setAuthDisabled; /** * Tests to simulate getting trips as an Otp user with enhanced admin credentials. The following config parameters must @@ -49,19 +52,11 @@ * Auth0 must be correctly configured as described here: https://auth0.com/docs/flows/call-your-api-using-resource-owner-password-flow */ public class GetMonitoredTripsTest { - private static AdminUser adminUser; - private static OtpUser otpUser1; - private static OtpUser otpUser2; + private static AdminUser multiAdminUser; + private static OtpUser soloOtpUser; + private static OtpUser multiOtpUser; private static final String MONITORED_TRIP_PATH = "api/secure/monitoredtrip"; - - /** - * Whether tests for this class should run. End to End must be enabled and Auth must NOT be disabled. This should be - * evaluated after the middleware application starts up (to ensure default disableAuth value has been applied from - * config). - */ - private static boolean testsShouldRun() { - return isEndToEnd && !isAuthDisabled(); - } + private static final Logger LOG = LoggerFactory.getLogger(GetMonitoredTripsTest.class); /** * Create Otp and Admin user accounts. Create Auth0 account for just the Otp users. If @@ -71,25 +66,26 @@ private static boolean testsShouldRun() { public static void setUp() throws IOException, InterruptedException { // Load config before checking if tests should run. OtpMiddlewareTest.setUp(); - assumeTrue(testsShouldRun()); + assumeTrue(isEndToEnd); + setAuthDisabled(false); // Mock the OTP server TODO: Run a live OTP instance? TestUtils.mockOtpServer(); - String email = String.format("test-%s@example.com", UUID.randomUUID().toString()); - otpUser1 = PersistenceUtil.createUser(String.format("test-%s@example.com", UUID.randomUUID().toString())); - otpUser2 = PersistenceUtil.createUser(email); - adminUser = PersistenceUtil.createAdminUser(email); + String multiUserEmail = String.format("test-%s@example.com", UUID.randomUUID().toString()); + soloOtpUser = PersistenceUtil.createUser(String.format("test-%s@example.com", UUID.randomUUID().toString())); + multiOtpUser = PersistenceUtil.createUser(multiUserEmail); + multiAdminUser = PersistenceUtil.createAdminUser(multiUserEmail); try { // Should use Auth0User.createNewAuth0User but this generates a random password preventing the mock headers // from being able to use TEMP_AUTH0_USER_PASSWORD. - User auth0User = Auth0Users.createAuth0UserForEmail(otpUser1.email, TEMP_AUTH0_USER_PASSWORD); - otpUser1.auth0UserId = auth0User.getId(); - Persistence.otpUsers.replace(otpUser1.id, otpUser1); - auth0User = Auth0Users.createAuth0UserForEmail(email, TEMP_AUTH0_USER_PASSWORD); - otpUser2.auth0UserId = auth0User.getId(); - Persistence.otpUsers.replace(otpUser2.id, otpUser2); + User auth0User = Auth0Users.createAuth0UserForEmail(soloOtpUser.email, TEMP_AUTH0_USER_PASSWORD); + soloOtpUser.auth0UserId = auth0User.getId(); + Persistence.otpUsers.replace(soloOtpUser.id, soloOtpUser); + auth0User = Auth0Users.createAuth0UserForEmail(multiUserEmail, TEMP_AUTH0_USER_PASSWORD); + multiOtpUser.auth0UserId = auth0User.getId(); + Persistence.otpUsers.replace(multiOtpUser.id, multiOtpUser); // Use the same Auth0 user id as otpUser2 as the email address is the same. - adminUser.auth0UserId = auth0User.getId(); - Persistence.adminUsers.replace(adminUser.id, adminUser); + multiAdminUser.auth0UserId = auth0User.getId(); + Persistence.adminUsers.replace(multiAdminUser.id, multiAdminUser); } catch (Auth0Exception e) { throw new RuntimeException(e); } @@ -100,13 +96,14 @@ public static void setUp() throws IOException, InterruptedException { */ @AfterAll public static void tearDown() { - assumeTrue(testsShouldRun()); - otpUser1 = Persistence.otpUsers.getById(otpUser1.id); - if (otpUser1 != null) otpUser1.delete(false); - otpUser2 = Persistence.otpUsers.getById(otpUser2.id); - if (otpUser2 != null) otpUser2.delete(false); - adminUser = Persistence.adminUsers.getById(adminUser.id); - if (adminUser != null) adminUser.delete(); + assumeTrue(isEndToEnd); + restoreDefaultAuthDisabled(); + soloOtpUser = Persistence.otpUsers.getById(soloOtpUser.id); + if (soloOtpUser != null) soloOtpUser.delete(false); + multiOtpUser = Persistence.otpUsers.getById(multiOtpUser.id); + if (multiOtpUser != null) multiOtpUser.delete(false); + multiAdminUser = Persistence.adminUsers.getById(multiAdminUser.id); + if (multiAdminUser != null) multiAdminUser.delete(); } /** @@ -118,47 +115,46 @@ public void canGetOwnMonitoredTrips() throws URISyntaxException { // Create trip as Otp user 1. MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); - monitoredTrip.userId = otpUser1.id; - HttpResponse createTripResponse = mockAuthenticatedRequest(MONITORED_TRIP_PATH, + monitoredTrip.userId = soloOtpUser.id; + HttpResponse createTrip1Response = mockAuthenticatedRequest(MONITORED_TRIP_PATH, JsonUtils.toJson(monitoredTrip), - otpUser1, + soloOtpUser, HttpUtils.REQUEST_METHOD.POST ); - assertEquals(HttpStatus.OK_200, createTripResponse.statusCode()); + assertEquals(HttpStatus.OK_200, createTrip1Response.statusCode()); // Create trip as Otp user 2. monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); - monitoredTrip.userId = otpUser2.id; + monitoredTrip.userId = multiOtpUser.id; HttpResponse createTripResponse2 = mockAuthenticatedRequest(MONITORED_TRIP_PATH, JsonUtils.toJson(monitoredTrip), - otpUser2, + multiOtpUser, HttpUtils.REQUEST_METHOD.POST ); assertEquals(HttpStatus.OK_200, createTripResponse2.statusCode()); - // Get trips for Otp user 1. - HttpResponse getTripsResponse1 = mockAuthenticatedGet(MONITORED_TRIP_PATH, otpUser2); - ResponseList trips = JsonUtils.getPOJOFromJSON(getTripsResponse1.body(), ResponseList.class); + // Get trips for solo Otp user. + HttpResponse soloTripsResponse = mockAuthenticatedGet(MONITORED_TRIP_PATH, soloOtpUser); + ResponseList soloTrips = JsonUtils.getPOJOFromJSON(soloTripsResponse.body(), ResponseList.class); - // Expect only 1 trip for Otp user 1. - assertEquals(1, trips.data.size()); + // Expect only 1 trip for solo Otp user. + assertEquals(1, soloTrips.data.size()); - // Get trips for Otp user 2. - HttpResponse getTripsCombined = mockAuthenticatedGet(MONITORED_TRIP_PATH, otpUser2); - ResponseList tripsCombined = JsonUtils.getPOJOFromJSON(getTripsCombined.body(), ResponseList.class); + // Get trips for multi Otp user/admin user. + HttpResponse multiTripsResponse = mockAuthenticatedGet(MONITORED_TRIP_PATH, multiOtpUser); + ResponseList multiTrips = JsonUtils.getPOJOFromJSON(multiTripsResponse.body(), ResponseList.class); - // Otp user 2 has 'enhanced' admin credentials both trips will be returned. The expectation here is that the UI + // Multi Otp user has 'enhanced' admin credentials both trips will be returned. The expectation here is that the UI // will always provide the user id to prevent this (as with the next test). // TODO: Determine if a separate admin endpoint should be maintained for getting all/combined trips. - assertEquals(2, tripsCombined.data.size()); + assertEquals(2, multiTrips.data.size()); - // Get trips for Otp user 2 defining user id. - HttpResponse getTripsFiltered = mockAuthenticatedGet( - String.format("api/secure/monitoredtrip?userId=%s", otpUser2.id), - otpUser2 + // Get trips for only the multi Otp user by specifying Otp user id. + HttpResponse tripsFilteredResponse = mockAuthenticatedGet( + String.format("api/secure/monitoredtrip?userId=%s", multiOtpUser.id), + multiOtpUser ); - ResponseList tripsFiltered = JsonUtils.getPOJOFromJSON(getTripsFiltered.body(), ResponseList.class); - + ResponseList tripsFiltered = JsonUtils.getPOJOFromJSON(tripsFilteredResponse.body(), ResponseList.class); // Just the trip for Otp user 2 will be returned. assertEquals(1, tripsFiltered.data.size()); } diff --git a/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java b/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java index 8f6744311..662f69465 100644 --- a/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java +++ b/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java @@ -19,23 +19,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.middleware.TestUtils.mockAuthenticatedGet; -import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthDisabled; +import static org.opentripplanner.middleware.auth.Auth0Connection.restoreDefaultAuthDisabled; import static org.opentripplanner.middleware.auth.Auth0Connection.setAuthDisabled; public class OtpUserControllerTest { private static final String INITIAL_PHONE_NUMBER = "+15555550222"; // Fake US 555 number. private static OtpUser otpUser; - private static boolean originalIsAuthDisabled; @BeforeAll public static void setUp() throws IOException, InterruptedException { // Load config. OtpMiddlewareTest.setUp(); - - // Save original isAuthDisabled state to restore it on tear down. - originalIsAuthDisabled = isAuthDisabled(); + // Ensure auth is disabled. setAuthDisabled(true); - // Create a persisted OTP user. otpUser = new OtpUser(); otpUser.email = String.format("test-%s@example.com", UUID.randomUUID().toString()); @@ -52,7 +48,7 @@ public static void tearDown() { if (otpUser != null) otpUser.delete(); // Restore original isAuthDisabled state. - setAuthDisabled(originalIsAuthDisabled); + restoreDefaultAuthDisabled(); } /** From 6edd37d71e62436104907b6a7967561556876c5c Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 6 Nov 2020 15:14:16 -0500 Subject: [PATCH 27/30] test(JsonUtils): fix responseList parser method --- .../middleware/utils/JsonUtils.java | 20 +++++++++++-------- .../middleware/ApiUserFlowTest.java | 11 +++++----- .../middleware/GetMonitoredTripsTest.java | 10 ++++++---- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java b/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java index 488ae65e7..88f4b188c 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java @@ -1,11 +1,14 @@ package org.opentripplanner.middleware.utils; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.type.CollectionType; import org.opentripplanner.middleware.bugsnag.BugsnagReporter; +import org.opentripplanner.middleware.controllers.response.ResponseList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.HaltException; @@ -69,12 +72,20 @@ public static T getPOJOFromJSON(String json, Class clazz) { return null; } + /** + * Utility method to parse a string representing a {@link ResponseList} correctly into its parameterized type. + */ + public static ResponseList getResponseListFromJSON(String json, Class contentClass) throws JsonProcessingException { + JavaType type = mapper.getTypeFactory().constructParametricType(ResponseList.class, contentClass); + return mapper.readValue(json, type); + } + /** * Utility method to parse generic objects from JSON String and return as list */ public static List getPOJOFromJSONAsList(String json, Class clazz) { try { - JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, clazz); + CollectionType type = mapper.getTypeFactory().constructCollectionType(List.class, clazz); return mapper.readValue(json, type); } catch (JsonProcessingException e) { BugsnagReporter.reportErrorToBugsnag( @@ -147,11 +158,4 @@ public static ObjectNode getObjectNode(String message, int code, Exception e) { .put("code", code) .put("detail", detail); } - - /** - * Get a single node value from JSON if present, else return null - */ - public static String getSingleNodeValueFromJSON(String nodeName, String json) throws JsonProcessingException { - return mapper.readTree(json).get(nodeName).textValue(); - } } diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index b524f3ee3..60c4e3613 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -3,6 +3,8 @@ import com.auth0.exception.Auth0Exception; import com.auth0.json.auth.TokenHolder; import com.auth0.json.mgmt.users.User; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -140,7 +142,7 @@ public static void tearDown() { * 5. Delete user and verify that their associated objects are also deleted. */ @Test - public void canSimulateApiUserFlow() throws URISyntaxException { + public void canSimulateApiUserFlow() throws URISyntaxException, JsonProcessingException { // Define the header values to be used in requests from this point forward. HashMap apiUserHeaders = new HashMap<>(); @@ -174,7 +176,7 @@ public void canSimulateApiUserFlow() throws URISyntaxException { // Request all Otp users created by an Api user. This will work and return all Otp users. HttpResponse getAllOtpUsersCreatedByApiUser = makeGetRequest(OTP_USER_PATH, apiUserHeaders); assertEquals(HttpStatus.OK_200, getAllOtpUsersCreatedByApiUser.statusCode()); - ResponseList otpUsers = JsonUtils.getPOJOFromJSON(getAllOtpUsersCreatedByApiUser.body(), ResponseList.class); + ResponseList otpUsers = JsonUtils.getResponseListFromJSON(getAllOtpUsersCreatedByApiUser.body(), OtpUser.class); assertEquals(1, otpUsers.total); // Attempt to create a monitored trip for an Otp user using mock authentication. This will fail because the user @@ -257,10 +259,7 @@ public void canSimulateApiUserFlow() throws URISyntaxException { HttpResponse tripRequestResponseAsApiUser = makeGetRequest(tripRequestsPath, apiUserHeaders); assertEquals(HttpStatus.OK_200, tripRequestResponseAsApiUser.statusCode()); - ResponseList tripRequests = JsonUtils.getPOJOFromJSON( - tripRequestResponseAsApiUser.body(), - ResponseList.class - ); + ResponseList tripRequests = JsonUtils.getResponseListFromJSON(tripRequestResponseAsApiUser.body(), TripRequest.class); // Delete Otp user authenticating as an Otp user. This will fail because the user was created by an Api user and // therefore does not have a Auth0 account. diff --git a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java index 539566577..65329aae7 100644 --- a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java +++ b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java @@ -2,6 +2,8 @@ import com.auth0.exception.Auth0Exception; import com.auth0.json.mgmt.users.User; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -111,7 +113,7 @@ public static void tearDown() { * credentials. */ @Test - public void canGetOwnMonitoredTrips() throws URISyntaxException { + public void canGetOwnMonitoredTrips() throws URISyntaxException, JsonProcessingException { // Create trip as Otp user 1. MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); @@ -135,14 +137,14 @@ public void canGetOwnMonitoredTrips() throws URISyntaxException { // Get trips for solo Otp user. HttpResponse soloTripsResponse = mockAuthenticatedGet(MONITORED_TRIP_PATH, soloOtpUser); - ResponseList soloTrips = JsonUtils.getPOJOFromJSON(soloTripsResponse.body(), ResponseList.class); + ResponseList soloTrips = JsonUtils.getResponseListFromJSON(soloTripsResponse.body(), MonitoredTrip.class); // Expect only 1 trip for solo Otp user. assertEquals(1, soloTrips.data.size()); // Get trips for multi Otp user/admin user. HttpResponse multiTripsResponse = mockAuthenticatedGet(MONITORED_TRIP_PATH, multiOtpUser); - ResponseList multiTrips = JsonUtils.getPOJOFromJSON(multiTripsResponse.body(), ResponseList.class); + ResponseList multiTrips = JsonUtils.getResponseListFromJSON(multiTripsResponse.body(), MonitoredTrip.class); // Multi Otp user has 'enhanced' admin credentials both trips will be returned. The expectation here is that the UI // will always provide the user id to prevent this (as with the next test). @@ -154,7 +156,7 @@ public void canGetOwnMonitoredTrips() throws URISyntaxException { String.format("api/secure/monitoredtrip?userId=%s", multiOtpUser.id), multiOtpUser ); - ResponseList tripsFiltered = JsonUtils.getPOJOFromJSON(tripsFilteredResponse.body(), ResponseList.class); + ResponseList tripsFiltered = JsonUtils.getResponseListFromJSON(tripsFilteredResponse.body(), MonitoredTrip.class); // Just the trip for Otp user 2 will be returned. assertEquals(1, tripsFiltered.data.size()); } From b16534a3e928c61288546f5043dc01e29706e619 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Mon, 9 Nov 2020 18:03:53 +0000 Subject: [PATCH 28/30] refactor: Addressed PR feedback --- .../middleware/auth/Auth0Connection.java | 21 ++++- .../middleware/auth/Auth0Users.java | 4 +- .../middleware/auth/RequestingUser.java | 7 ++ .../controllers/api/ApiController.java | 16 ++-- .../controllers/api/ApiUserController.java | 4 +- .../controllers/api/OtpRequestProcessor.java | 81 ++++++++----------- .../controllers/api/OtpUserController.java | 15 +--- .../middleware/models/ApiUser.java | 8 +- .../middleware/models/MonitoredTrip.java | 4 +- .../middleware/models/OtpUser.java | 2 +- 10 files changed, 82 insertions(+), 80 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 60e4a5b62..83764b975 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -143,6 +143,25 @@ public static void checkUserIsAdmin(Request req, Response res) { } } + /** + * Check that an {@link ApiUser} is linked to an Api key. + */ + //FIXME: Move this check into existing auth checks so it would be carried out automatically prior to any + // business logic. Consider edge cases where a user can be both an API user and OTP user. + public static void linkApiKeyToApiUser(Request req) { + RequestingUser requestingUser = getUserFromRequest(req); + String apiKeyValueFromHeader = req.headers("x-api-key"); + if (requestingUser.apiUser == null || + apiKeyValueFromHeader == null || + !requestingUser.apiUser.hasApiKeyValue(apiKeyValueFromHeader)) { + // If API user not found, log message and halt. + logMessageAndHalt( + req, + HttpStatus.FORBIDDEN_403, + "API key not linked to an API user."); + } + } + /** * Add user profile to Spark Request object */ @@ -267,7 +286,7 @@ public static void isAuthorized(String userId, Request request) { if (requestingUser.isThirdPartyUser()) { // Api user potentially requesting an item on behalf of an Otp user they created. OtpUser otpUser = Persistence.otpUsers.getById(userId); - if (otpUser != null && otpUser.canBeManagedBy(requestingUser)) { + if (requestingUser.canManageEntity(otpUser)) { return; } } diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index e25ebeb1c..163f76ce5 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -259,8 +259,8 @@ public static HttpResponse getCompleteAuth0TokenResponse(String username username, password, DEFAULT_AUDIENCE, // must match an API identifier - (isEndToEnd) ? AUTH0_CLIENT_ID : AUTH0_API_CLIENT, // Auth0 application client ID - (isEndToEnd) ? AUTH0_CLIENT_SECRET : AUTH0_API_SECRET // Auth0 application client secret + AUTH0_API_CLIENT, // Auth0 application client ID + AUTH0_API_SECRET // Auth0 application client secret ); return HttpUtils.httpRequestRawResponse( URI.create(String.format("https://%s/oauth/token", AUTH0_DOMAIN)), diff --git a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java index 1bbcb678c..5a8a209d7 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java +++ b/src/main/java/org/opentripplanner/middleware/auth/RequestingUser.java @@ -6,6 +6,7 @@ import org.bson.conversions.Bson; import org.opentripplanner.middleware.models.AdminUser; import org.opentripplanner.middleware.models.ApiUser; +import org.opentripplanner.middleware.models.Model; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.persistence.Persistence; import spark.Request; @@ -85,4 +86,10 @@ public boolean isAdmin() { return adminUser != null; } + /** + * Check if this requesting user can manage the specified entity. + */ + public boolean canManageEntity(Model model) { + return model != null && model.canBeManagedBy(this); + } } diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java index ff5610ab8..225d63cd2 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiController.java @@ -7,6 +7,7 @@ import com.beerboy.ss.rest.Endpoint; import com.fasterxml.jackson.core.JsonProcessingException; import com.mongodb.client.model.Filters; +import org.bson.conversions.Bson; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.auth.RequestingUser; @@ -194,7 +195,7 @@ private ResponseList getMany(Request req, Response res) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); if (userId != null) { OtpUser otpUser = Persistence.otpUsers.getById(userId); - if (otpUser != null && otpUser.canBeManagedBy(requestingUser)) { + if (requestingUser.canManageEntity(otpUser)) { return persistence.getResponseList(Filters.eq(USER_ID_PARAM, userId), offset, limit); } else { res.status(HttpStatus.FORBIDDEN_403); @@ -210,9 +211,10 @@ private ResponseList getMany(Request req, Response res) { // OtpUserController. If the request is being made by an Api user the response will be limited to the Otp users // created by this Api user. If not, the assumption is that an Otp user is making the request and the response // will be limited to just the entity matching this Otp user. - return (requestingUser.apiUser != null) - ? persistence.getResponseList(Filters.eq("applicationId", requestingUser.apiUser.id), offset, limit) - : persistence.getResponseList(Filters.eq("_id", requestingUser.otpUser.id), offset, limit); + Bson filter = (requestingUser.apiUser != null) + ? Filters.eq("applicationId", requestingUser.apiUser.id) + : Filters.eq("_id", requestingUser.otpUser.id); + return persistence.getResponseList(filter, offset, limit); } else if (requestingUser.isThirdPartyUser()) { // A user id must be provided if the request is being made by a third party user. logMessageAndHalt(req, @@ -237,7 +239,7 @@ protected T getEntityForId(Request req, Response res) { String id = getIdFromRequest(req); T object = getObjectForId(req, id); - if (!object.canBeManagedBy(requestingUser)) { + if (!requestingUser.canManageEntity(object)) { logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to get %s.", className)); } @@ -254,7 +256,7 @@ private T deleteOne(Request req, Response res) { try { T object = getObjectForId(req, id); // Check that requesting user can manage entity. - if (!object.canBeManagedBy(requestingUser)) { + if (!requestingUser.canManageEntity(object)) { logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to delete %s.", className)); } // Run pre-delete hook. If return value is false, abort. @@ -354,7 +356,7 @@ private T createOrUpdate(Request req, Response res) { return null; } // Check that requesting user can manage entity. - if (!preExistingObject.canBeManagedBy(requestingUser)) { + if (!requestingUser.canManageEntity(preExistingObject)) { logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, String.format("Requesting user not authorized to update %s.", className)); diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java index c95cce017..85a329055 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java @@ -163,7 +163,7 @@ private ApiUser deleteApiKeyForApiUser(Request req, Response res) { "An api key id is required", null); } - if (targetUser != null && !targetUser.hasKey(apiKeyId)) { + if (targetUser != null && !targetUser.hasApiKeyId(apiKeyId)) { logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, String.format("User id (%s) does not have expected api key id (%s)", targetUser.id, apiKeyId), @@ -241,7 +241,7 @@ private static ApiUser getApiUser(Request req) { ); } - if (!apiUser.canBeManagedBy(requestingUser)) { + if (!requestingUser.canManageEntity(apiUser)) { logMessageAndHalt(req, HttpStatus.FORBIDDEN_403, "Must be an admin to perform this operation."); } return apiUser; diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index cef678c35..04484e4cd 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -5,7 +5,6 @@ import com.beerboy.ss.descriptor.ParameterDescriptor; import com.beerboy.ss.rest.Endpoint; import org.eclipse.jetty.http.HttpStatus; -import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TripRequest; @@ -24,6 +23,10 @@ import java.util.List; import static com.beerboy.ss.descriptor.MethodDescriptor.path; +import static org.opentripplanner.middleware.auth.Auth0Connection.checkUser; +import static org.opentripplanner.middleware.auth.Auth0Connection.getUserFromRequest; +import static org.opentripplanner.middleware.auth.Auth0Connection.isAuthHeaderPresent; +import static org.opentripplanner.middleware.controllers.api.ApiController.USER_ID_PARAM; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; /** @@ -33,7 +36,6 @@ */ public class OtpRequestProcessor implements Endpoint { private static final Logger LOG = LoggerFactory.getLogger(OtpRequestProcessor.class); - private static final String USER_ID_PARAM = "userId"; /** * Endpoint for the OTP Middleware's OTP proxy @@ -84,19 +86,43 @@ private static String proxy(Request request, spark.Response response) { logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No OTP Server provided, check config."); return null; } + OtpUser otpUser = null; + // If the Auth header is present, this indicates that the request was made by a logged in user. + if (isAuthHeaderPresent(request)) { + checkUser(request); + RequestingUser requestingUser = getUserFromRequest(request); + // If a user id is provided, the assumption is that an API user is making a plan request on behalf of an Otp user. + String userId = request.queryParams(USER_ID_PARAM); + if (requestingUser.otpUser != null && userId == null) { + // Otp user making a trip request for self. + otpUser = requestingUser.otpUser; + } else if (requestingUser.apiUser != null) { + // Api user making a trip request on behalf of an Otp user. In this case, the Otp user id must be provided + // as a query parameter. + otpUser = Persistence.otpUsers.getById(userId); + if (otpUser == null && userId != null) { + logMessageAndHalt(request, HttpStatus.NOT_FOUND_404, "The specified user id was not found."); + } else if (!requestingUser.canManageEntity(otpUser)) { + logMessageAndHalt(request, + HttpStatus.FORBIDDEN_403, + String.format("User: %s not authorized to make trip requests for user: %s", + requestingUser.apiUser.email, + otpUser.email)); + } + } + } // Get request path intended for OTP API by removing the proxy endpoint (/otp). String otpRequestPath = request.uri().replaceFirst(OTP_PROXY_ENDPOINT, ""); - // attempt to get response from OTP server based on requester's query parameters OtpDispatcherResponse otpDispatcherResponse = OtpDispatcher.sendOtpRequest(request.queryString(), otpRequestPath); if (otpDispatcherResponse == null || otpDispatcherResponse.responseBody == null) { logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No response from OTP server."); return null; } - // If the request path ends with the plan endpoint (e.g., '/plan' or '/default/plan'), process response. - if (otpRequestPath.endsWith(OtpDispatcher.OTP_PLAN_ENDPOINT)) handlePlanTripResponse(request, otpDispatcherResponse); - + if (otpRequestPath.endsWith(OtpDispatcher.OTP_PLAN_ENDPOINT) && otpUser != null) { + handlePlanTripResponse(request, otpDispatcherResponse, otpUser); + } // provide response to requester as received from OTP server response.type(MediaType.APPLICATION_JSON); response.status(otpDispatcherResponse.statusCode); @@ -107,55 +133,16 @@ private static String proxy(Request request, spark.Response response) { * Process plan response from OTP. Store the response if consent is given. Handle the process and all exceptions * seamlessly so as not to affect the response provided to the requester. */ - private static void handlePlanTripResponse(Request request, OtpDispatcherResponse otpDispatcherResponse) { - - // If the Auth header is present, this indicates that the request was made by a logged in user. If present - // we should store trip history (but we verify this preference before doing so). - if (!Auth0Connection.isAuthHeaderPresent(request)) { - LOG.debug("Anonymous user, trip history not stored"); - return; - } + private static void handlePlanTripResponse(Request request, OtpDispatcherResponse otpDispatcherResponse, OtpUser otpUser) { String batchId = request.queryParams("batchId"); if (batchId == null) { //TODO place holder for now batchId = "-1"; } - - // Dispatch request to OTP and store request/response summary if user elected to store trip history. long tripStorageStartTime = DateTimeUtils.currentTimeMillis(); - - Auth0Connection.checkUser(request); - RequestingUser requestingUser = Auth0Connection.getUserFromRequest(request); - // A requesting user (Otp or third party user) is required to proceed. - if (requestingUser.otpUser == null && requestingUser.apiUser == null) { - return; - } - // TODO: Consider moving/replicating these checks (or a subset of) to before sending the request to OTP. - // If a user id is provided, the assumption is that an Api user is making a plan request on behalf of an Otp user. - String userId = request.queryParams(USER_ID_PARAM); - OtpUser otpUser = null; - if (requestingUser.otpUser != null && userId == null) { - // Otp user making a trip request for self. - otpUser = requestingUser.otpUser; - } else if (requestingUser.apiUser != null) { - // Api user making a trip request on behalf of an Otp user. In this case, the Otp user id must be provided - // as a query parameter. - otpUser = Persistence.otpUsers.getById(userId); - if (otpUser == null && userId != null) { - logMessageAndHalt(request, HttpStatus.NOT_FOUND_404, "The specified user id was not found."); - } else if (!otpUser.canBeManagedBy(requestingUser)) { - logMessageAndHalt(request, - HttpStatus.FORBIDDEN_403, - String.format("User: %s not authorized to make trip requests for user: %s", - requestingUser.apiUser.email, - otpUser.email)); - } - } - - final boolean storeTripHistory = otpUser != null && otpUser.storeTripHistory; // only save trip details if the user has given consent and a response from OTP is provided - if (!storeTripHistory) { + if (!otpUser.storeTripHistory) { LOG.debug("User does not want trip history stored"); } else { OtpResponse otpResponse = otpDispatcherResponse.getResponse(); diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java index 753ed174a..d60e37e25 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java @@ -44,22 +44,11 @@ public OtpUserController(String apiPrefix) { @Override OtpUser preCreateHook(OtpUser user, Request req) { RequestingUser requestingUser = Auth0Connection.getUserFromRequest(req); - // If an Api user is present it is assumed an attempt is being made to create an OTP user. if (requestingUser.apiUser != null) { - String apiKeyFromHeader = req.headers("x-api-key"); // Check API key and assign user to appropriate third-party application. Note: this is only relevant for // instances of otp-middleware running behind API Gateway. - if (apiKeyFromHeader != null && requestingUser.apiUser.hasToken(apiKeyFromHeader)) { - // If third party and using their own api key, assign to new OTP user. - user.applicationId = requestingUser.apiUser.id; - } else { - // If API user not found, log message and halt. - logMessageAndHalt( - req, - HttpStatus.FORBIDDEN_403, - "Attempting to create OTP user with API key that is not linked to calling third party", - new IllegalArgumentException("API key not linked to API user.")); - } + Auth0Connection.linkApiKeyToApiUser(req); + user.applicationId = requestingUser.apiUser.id; } return super.preCreateHook(user, req); } diff --git a/src/main/java/org/opentripplanner/middleware/models/ApiUser.java b/src/main/java/org/opentripplanner/middleware/models/ApiUser.java index 6de568d8a..108346851 100644 --- a/src/main/java/org/opentripplanner/middleware/models/ApiUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/ApiUser.java @@ -97,18 +97,18 @@ public static ApiUser userForApiKeyValue(String apiKeyValue) { } /** - * Shorthand method to determine if an API user has an API key. + * Shorthand method to determine if an API user has an API key id. */ - public boolean hasKey(String apiKeyId) { + public boolean hasApiKeyId(String apiKeyId) { return apiKeys .stream() .anyMatch(apiKey -> apiKeyId.equals(apiKey.keyId)); } /** - * Shorthand method to determine if an API user has an API token (value). + * Shorthand method to determine if an API user has an API key value. */ - public boolean hasToken(String apiKeyValue) { + public boolean hasApiKeyValue(String apiKeyValue) { return apiKeys .stream() .anyMatch(apiKey -> apiKeyValue.equals(apiKey.value)); diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index 3b19f9a5c..05007da59 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -1,8 +1,6 @@ package org.opentripplanner.middleware.models; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; import com.mongodb.client.model.Filters; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; @@ -248,7 +246,7 @@ public boolean canBeManagedBy(RequestingUser requestingUser) { } else if (requestingUser.apiUser != null) { // get the required OTP user to confirm they are associated with the requesting API user. OtpUser otpUser = Persistence.otpUsers.getById(userId); - if (otpUser != null && otpUser.canBeManagedBy(requestingUser)) { + if (requestingUser.canManageEntity(otpUser)) { return true; } } else if (requestingUser.isAdmin()) { diff --git a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java index e5c5b4ab9..189845d84 100644 --- a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java @@ -44,7 +44,7 @@ public class OtpUser extends AbstractUser { public boolean storeTripHistory; @JsonIgnore - /** If this user was created by an Api user, this parameter will match the Api user's id */ + /** If this user was created by an {@link ApiUser}, this parameter will match the {@link ApiUser}'s id */ public String applicationId; @Override From 31aea841a3b87ef77af62e0af40247eb501c32e3 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Tue, 10 Nov 2020 10:04:55 +0000 Subject: [PATCH 29/30] refactor: Addressed PR feedback --- .../middleware/auth/Auth0Connection.java | 5 +++-- .../opentripplanner/middleware/auth/Auth0Users.java | 8 -------- .../controllers/api/ApiUserController.java | 1 - .../controllers/api/OtpRequestProcessor.java | 12 ++++++++++-- .../controllers/api/OtpUserController.java | 2 +- .../opentripplanner/middleware/ApiUserFlowTest.java | 1 - 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java index 83764b975..a46d56f6a 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Connection.java @@ -144,11 +144,12 @@ public static void checkUserIsAdmin(Request req, Response res) { } /** - * Check that an {@link ApiUser} is linked to an Api key. + * Check that the API key used in the incoming request is associated with the matching {@link ApiUser} (which is + * determined from the Authorization header). */ //FIXME: Move this check into existing auth checks so it would be carried out automatically prior to any // business logic. Consider edge cases where a user can be both an API user and OTP user. - public static void linkApiKeyToApiUser(Request req) { + public static void ensureApiUserHasApiKey(Request req) { RequestingUser requestingUser = getUserFromRequest(req); String apiKeyValueFromHeader = req.headers("x-api-key"); if (requestingUser.apiUser == null || diff --git a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java index 163f76ce5..939ed36ac 100644 --- a/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java +++ b/src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java @@ -26,7 +26,6 @@ import java.util.UUID; import static com.mongodb.client.model.Filters.eq; -import static org.opentripplanner.middleware.utils.ConfigUtils.getBooleanEnvVar; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; @@ -39,18 +38,11 @@ public class Auth0Users { // This client/secret pair is for making requests for an API access token used with the Management API. private static final String AUTH0_API_CLIENT = getConfigPropertyAsText("AUTH0_API_CLIENT"); private static final String AUTH0_API_SECRET = getConfigPropertyAsText("AUTH0_API_SECRET"); - private static final String AUTH0_CLIENT_ID = getConfigPropertyAsText("AUTH0_CLIENT_ID"); - private static final String AUTH0_CLIENT_SECRET = getConfigPropertyAsText("AUTH0_CLIENT_SECRET"); private static final String DEFAULT_CONNECTION_TYPE = "Username-Password-Authentication"; private static final String DEFAULT_AUDIENCE = "https://otp-middleware"; private static final String MANAGEMENT_API_VERSION = "v2"; public static final String API_PATH = "/api/" + MANAGEMENT_API_VERSION; - /** - * Whether the end-to-end environment variable is enabled. - */ - private static final boolean isEndToEnd = getBooleanEnvVar("RUN_E2E"); - /** * Cached API token so that we do not have to request a new one each time a Management API request is made. */ diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java index 85a329055..ed5effcd2 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/ApiUserController.java @@ -74,7 +74,6 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) { // Authenticate user with Auth0 .post(path(AUTHENTICATE_PATH) .withDescription("Authenticates ApiUser with Auth0.") - .withPathParam().withName(ID_PARAM).withDescription("The user ID.").and() .withQueryParam().withName(USERNAME_PARAM).withRequired(true) .withDescription("Auth0 username (usually email address).").and() .withQueryParam().withName(PASSWORD_PARAM).withRequired(true) diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java index 04484e4cd..53e1b3aac 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpRequestProcessor.java @@ -5,6 +5,7 @@ import com.beerboy.ss.descriptor.ParameterDescriptor; import com.beerboy.ss.rest.Endpoint; import org.eclipse.jetty.http.HttpStatus; +import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TripRequest; @@ -86,17 +87,19 @@ private static String proxy(Request request, spark.Response response) { logMessageAndHalt(request, HttpStatus.INTERNAL_SERVER_ERROR_500, "No OTP Server provided, check config."); return null; } + // If a user id is provided, the assumption is that an API user is making a plan request on behalf of an Otp user. + String userId = request.queryParams(USER_ID_PARAM); + String apiKeyValueFromHeader = request.headers("x-api-key"); OtpUser otpUser = null; // If the Auth header is present, this indicates that the request was made by a logged in user. if (isAuthHeaderPresent(request)) { checkUser(request); RequestingUser requestingUser = getUserFromRequest(request); - // If a user id is provided, the assumption is that an API user is making a plan request on behalf of an Otp user. - String userId = request.queryParams(USER_ID_PARAM); if (requestingUser.otpUser != null && userId == null) { // Otp user making a trip request for self. otpUser = requestingUser.otpUser; } else if (requestingUser.apiUser != null) { + Auth0Connection.ensureApiUserHasApiKey(request); // Api user making a trip request on behalf of an Otp user. In this case, the Otp user id must be provided // as a query parameter. otpUser = Persistence.otpUsers.getById(userId); @@ -110,6 +113,11 @@ private static String proxy(Request request, spark.Response response) { otpUser.email)); } } + } else if (userId != null && apiKeyValueFromHeader == null) { + // User id has been provided, but no means to authorize the requesting user. + logMessageAndHalt(request, + HttpStatus.UNAUTHORIZED_401, + "Unauthorized trip request, authorization required."); } // Get request path intended for OTP API by removing the proxy endpoint (/otp). String otpRequestPath = request.uri().replaceFirst(OTP_PROXY_ENDPOINT, ""); diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java index d60e37e25..916dfe23a 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java @@ -47,7 +47,7 @@ OtpUser preCreateHook(OtpUser user, Request req) { if (requestingUser.apiUser != null) { // Check API key and assign user to appropriate third-party application. Note: this is only relevant for // instances of otp-middleware running behind API Gateway. - Auth0Connection.linkApiKeyToApiUser(req); + Auth0Connection.ensureApiUserHasApiKey(req); user.applicationId = requestingUser.apiUser.id; } return super.preCreateHook(user, req); diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index 60c4e3613..f24c7cee0 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -4,7 +4,6 @@ import com.auth0.json.auth.TokenHolder; import com.auth0.json.mgmt.users.User; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; From 9072f1e963f179329df8e1b04314b60e9f2c5389 Mon Sep 17 00:00:00 2001 From: Rob Beer Date: Tue, 10 Nov 2020 10:38:27 +0000 Subject: [PATCH 30/30] refactor(Fixed failed unit tests): Fixed failed unit tests as a result of merge --- .../java/org/opentripplanner/middleware/ApiUserFlowTest.java | 1 + .../org/opentripplanner/middleware/GetMonitoredTripsTest.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java index f24c7cee0..83b82f339 100644 --- a/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java +++ b/src/test/java/org/opentripplanner/middleware/ApiUserFlowTest.java @@ -184,6 +184,7 @@ public void canSimulateApiUserFlow() throws URISyntaxException, JsonProcessingEx // Create a monitored trip for the Otp user (API users are prevented from doing this). MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); + monitoredTrip.updateAllDaysOfWeek(true); monitoredTrip.userId = otpUser.id; HttpResponse createTripResponseAsOtpUser = mockAuthenticatedRequest( MONITORED_TRIP_PATH, diff --git a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java index 65329aae7..009240059 100644 --- a/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java +++ b/src/test/java/org/opentripplanner/middleware/GetMonitoredTripsTest.java @@ -117,6 +117,7 @@ public void canGetOwnMonitoredTrips() throws URISyntaxException, JsonProcessingE // Create trip as Otp user 1. MonitoredTrip monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); + monitoredTrip.updateAllDaysOfWeek(true); monitoredTrip.userId = soloOtpUser.id; HttpResponse createTrip1Response = mockAuthenticatedRequest(MONITORED_TRIP_PATH, JsonUtils.toJson(monitoredTrip), @@ -127,6 +128,7 @@ public void canGetOwnMonitoredTrips() throws URISyntaxException, JsonProcessingE // Create trip as Otp user 2. monitoredTrip = new MonitoredTrip(TestUtils.sendSamplePlanRequest()); + monitoredTrip.updateAllDaysOfWeek(true); monitoredTrip.userId = multiOtpUser.id; HttpResponse createTripResponse2 = mockAuthenticatedRequest(MONITORED_TRIP_PATH, JsonUtils.toJson(monitoredTrip),