Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cascade delete #64

Merged
merged 21 commits into from
Sep 3, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0a79dd6
fix(delete): add cascade delete for collections
landonreed Aug 24, 2020
c94a52b
test(e2e): add api user flow test to simulate 3rd party apps
landonreed Aug 24, 2020
3fbc850
refactor(Fixed merge conflict): Merged dev and fixed conflicts
Aug 25, 2020
24acb0b
refactor(ApiUserFlowTest): Api and otp related tests using Auth0 auth…
Aug 27, 2020
3d70aa2
refactor(Merge conflict): Fixed merge conflict with dev branch
Aug 27, 2020
993a7ba
refactor(PR updates): Addressed PR feedback
Aug 28, 2020
1fa79e7
refactor(ApiUserController): Corrected issue introduced fixing merge …
Aug 28, 2020
ab88847
refactor(Auth0): do not check valid user if creating self
landonreed Sep 1, 2020
6ff1fd1
refactor: update tests and simplify mock otp
landonreed Sep 1, 2020
6e6e3a0
refactor: remove unused imports
landonreed Sep 1, 2020
39bfee4
refactor(OtpUserController): handle missing API user on OtpUser#create
landonreed Sep 1, 2020
84608b7
refactor: minor tweaks, add more setup docs
landonreed Sep 1, 2020
942c804
refactor(ApiController): change delete return type
landonreed Sep 1, 2020
4ae3945
refactor(user#delete): address PR comments
landonreed Sep 1, 2020
869713d
refactor(Auth0): fix isCreatingSelf check; handle null throwable in b…
landonreed Sep 2, 2020
e4cdb9a
Merge branch 'dev' into cascade-delete
landonreed Sep 2, 2020
d4ecd7a
refactor(DateTimeUtils): use currentTimeMillis util
landonreed Sep 2, 2020
167af9b
test(trip-history): clean up otpuser after each test
landonreed Sep 2, 2020
32f0340
Merge branch 'dev' into cascade-delete
landonreed Sep 3, 2020
69c562f
test: evaluate authDisabled after config is loaded
landonreed Sep 3, 2020
4f43685
Merge branch 'dev' into cascade-delete
landonreed Sep 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import org.opentripplanner.middleware.controllers.api.AdminUserController;
import org.opentripplanner.middleware.controllers.api.ApiUserController;
import org.opentripplanner.middleware.controllers.api.BugsnagController;
import org.opentripplanner.middleware.controllers.api.OtpUserController;
import org.opentripplanner.middleware.controllers.api.LogController;
import org.opentripplanner.middleware.controllers.api.MonitoredTripController;
import org.opentripplanner.middleware.controllers.api.OtpUserController;
import org.opentripplanner.middleware.controllers.api.TripHistoryController;
import org.opentripplanner.middleware.otp.OtpRequestProcessor;
import org.opentripplanner.middleware.persistence.Persistence;
Expand All @@ -36,7 +36,7 @@ public class OtpMiddlewareMain {
private static final Logger LOG = LoggerFactory.getLogger(OtpMiddlewareMain.class);
private static final String DEFAULT_ENV = "configurations/default/env.yml";
private static JsonNode envConfig;
private static final String API_PREFIX = "/api/";
public static final String API_PREFIX = "/api/";
public static boolean inTestEnvironment = false;

public static void main(String[] args) throws IOException {
Expand Down Expand Up @@ -117,8 +117,8 @@ private static void initializeHttpEndpoints() throws IOException {
}

/**
* Load config files from either program arguments or (if no args specified) from
* default configuration file locations. Config fields are retrieved with getConfigProperty.
* Load config files from either program arguments or (if no args specified) from default configuration file
* locations. Config fields are retrieved with getConfigProperty.
*/
private static void loadConfig(String[] args) throws IOException {
FileInputStream envConfigStream;
Expand Down Expand Up @@ -203,16 +203,23 @@ public static String getConfigPropertyAsText(String name, String defaultValue) {
* value if the config value is not defined (null) or cannot be converted to an int.
*/
public static int getConfigPropertyAsInt(String name, int defaultValue) {

int value = defaultValue;

try {
JsonNode node = getConfigProperty(name);
value = Integer.parseInt(node.asText());
if (node != null) {
value = Integer.parseInt(node.asText());
}
} catch (NumberFormatException | NullPointerException e) {
LOG.warn("Unable to parse {}. Using default: {}", name, defaultValue, e);
}
return value;
}

/**
* Returns true only if an environment variable exists and is set to "true".
*/
public static boolean getBooleanEnvVar(String var) {
String variable = System.getenv(var);
return variable != null && variable.equals("true");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.HaltException;
import spark.Request;
import spark.Response;

Expand Down Expand Up @@ -53,12 +54,17 @@ public static void checkUser(Request req) {
try {
DecodedJWT jwt = verifier.verify(token);
Auth0UserProfile profile = new Auth0UserProfile(jwt);
if (!isValidUser(profile)) {
landonreed marked this conversation as resolved.
Show resolved Hide resolved
logMessageAndHalt(req, HttpStatus.NOT_FOUND_404, "Unknown user.");
}
// The user attribute is used on the server side to check user permissions and does not have all of the
// fields that the raw Auth0 profile string does.
addUserToRequest(req, profile);
} catch (JWTVerificationException e) {
// Invalid signature/claims
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");
Expand Down Expand Up @@ -169,40 +175,30 @@ public static boolean authDisabled() {
}

/**
* Confirm that the user exists
* Confirm that the user exists in at least one of the MongoDB user collections.
*/
private static Auth0UserProfile isValidUser(Request request) {

Auth0UserProfile profile = getUserFromRequest(request);
if (profile == null || (profile.adminUser == null && profile.otpUser == null && profile.apiUser == null)) {
logMessageAndHalt(request, HttpStatus.NOT_FOUND_404, "Unknown user.");
}

return profile;
private static boolean isValidUser(Auth0UserProfile profile) {
return profile != null && (profile.adminUser != null || profile.otpUser != null || profile.apiUser != null);
}

/**
* Confirm that the user's actions are on their items if not admin.
*/
public static void isAuthorized(String userId, Request request) {

Auth0UserProfile profile = isValidUser(request);

Auth0UserProfile profile = getUserFromRequest(request);
// let admin do anything
if (profile.adminUser != null) {
return;
}

// If userId is defined, it must be set to a value associated with the user.
if (userId != null) {
if (profile.otpUser != null && profile.otpUser.id.equals(userId)) {
return;
}

if (profile.apiUser != null && profile.apiUser.id.equals(userId)) {
return;
}
}

logMessageAndHalt(request, HttpStatus.FORBIDDEN_403, "Unauthorized access.");
}

Expand Down
59 changes: 50 additions & 9 deletions src/main/java/org/opentripplanner/middleware/auth/Auth0Users.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,30 @@
import org.opentripplanner.middleware.bugsnag.BugsnagReporter;
import org.opentripplanner.middleware.models.AbstractUser;
import org.opentripplanner.middleware.persistence.TypedPersistence;
import org.opentripplanner.middleware.utils.HttpUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request;

import java.net.URI;
import java.net.http.HttpResponse;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import static com.mongodb.client.model.Filters.eq;
import static org.opentripplanner.middleware.OtpMiddlewareMain.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 {
private static final String AUTH0_DOMAIN = getConfigPropertyAsText("AUTH0_DOMAIN");
public static final String AUTH0_DOMAIN = getConfigPropertyAsText("AUTH0_DOMAIN");
landonreed marked this conversation as resolved.
Show resolved Hide resolved
// 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");
Expand All @@ -44,15 +50,19 @@ 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. Defaults to a random UUID password and connection type of
* {@link #DEFAULT_CONNECTION_TYPE}.
*/
public static User createAuth0UserForEmail(String email) throws Auth0Exception {
return createAuth0UserForEmail(email, UUID.randomUUID().toString());
}

public static User createAuth0UserForEmail(String email, String password) throws Auth0Exception {
// Create user object and assign properties.
User user = new User();
user.setEmail(email);
// TODO set name? phone? other Auth0 properties?
user.setPassword(UUID.randomUUID().toString());
user.setPassword(password);
user.setConnection(DEFAULT_CONNECTION_TYPE);
return getManagementAPI()
.users()
Expand All @@ -79,8 +89,8 @@ public static TokenCache getCachedToken() {
}

/**
* Gets an Auth0 API access token for authenticating requests to the Auth0 Management API. This will either create
* a new token using the oauth token endpoint or grab a cached token that it has already created (if it has not
* Gets an Auth0 API access token for authenticating requests to the Auth0 Management API. This will either create a
* new token using the oauth token endpoint or grab a cached token that it has already created (if it has not
landonreed marked this conversation as resolved.
Show resolved Hide resolved
* expired). More information on setting this up is here: https://auth0.com/docs/api/management/v2/get-access-tokens-for-production
*/
public static String getApiToken() {
Expand Down Expand Up @@ -126,9 +136,9 @@ public static User getUserByEmail(String email, boolean createIfNotExists) {
}

/**
* Method to trigger an Auth0 job to resend a verification email. Returns an Auth0 {@link Job} which can be
* used to monitor the progress of the job (using job ID). Typically the verification email goes out pretty quickly
* so there shouldn't be too much of a need to monitor the result.
* Method to trigger an Auth0 job to resend a verification email. Returns an Auth0 {@link Job} which can be used to
* monitor the progress of the job (using job ID). Typically the verification email goes out pretty quickly so there
* shouldn't be too much of a need to monitor the result.
*/
public static Job resendVerificationEmail(String userId) {
try {
Expand Down Expand Up @@ -176,13 +186,15 @@ public static <U extends AbstractUser> User createNewAuth0User(U user, Request r
// Ensure no user with email exists in MongoDB.
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!");
}
// 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);
}
LOG.info("Created new Auth0 user ({}) for user {}", auth0UserProfile.getId(), user.id);
return auth0UserProfile;
}

Expand Down Expand Up @@ -224,4 +236,33 @@ private static String getAuth0Url() {
? "http://locahost:8089"
: "https://" + AUTH0_DOMAIN;
}

/**
* Get an 0Auth token by using the Auth0 'Call Your API Using Resource Owner Password Flow' approach. Auth0 setup
br648 marked this conversation as resolved.
Show resolved Hide resolved
* 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 to the
* caller. In all other cases, null is returned.
*/
public static String get0AuthToken(String username, String password) {
br648 marked this conversation as resolved.
Show resolved Hide resolved
String body = String.format(
"grant_type=password&username=%s&password=%s&audience=%s&scope=read:sample&client_id=%s&client_secret=%s",
username,
password,
"https://otp-middleware", // must match an API identifier
AUTH0_API_CLIENT, // Auth0 application client ID
AUTH0_API_SECRET // Auth0 application client secret
);

HttpResponse<String> response = 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
);

return (response == null || response.statusCode() != HttpStatus.OK_200)
? null
: getSingleNodeValueFromJSON("access_token", response.body());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,6 @@ U preUpdateHook(U user, U preExistingUser, Request req) {
*/
@Override
boolean preDeleteHook(U user, Request req) {
try {
deleteAuth0User(user.auth0UserId);
landonreed marked this conversation as resolved.
Show resolved Hide resolved
} catch (Auth0Exception e) {
// FIXME: Add Bugsnag error report.
logMessageAndHalt(req, 500, "Error deleting user.", e);
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.beerboy.ss.ApiEndpoint;
import com.beerboy.ss.SparkSwagger;
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;
Expand Down Expand Up @@ -216,7 +217,7 @@ private T getOne(Request req, Response res) {
/**
* HTTP endpoint to delete one entity specified by ID.
*/
private String deleteOne(Request req, Response res) {
private T deleteOne(Request req, Response res) {
landonreed marked this conversation as resolved.
Show resolved Hide resolved
long startTime = System.currentTimeMillis();
landonreed marked this conversation as resolved.
Show resolved Hide resolved
String id = getIdFromRequest(req);
Auth0UserProfile requestingUser = Auth0Connection.getUserFromRequest(req);
Expand All @@ -230,12 +231,17 @@ private String deleteOne(Request req, Response res) {
if (!preDeleteHook(object, req)) {
logMessageAndHalt(req, 500, "Unknown error occurred during delete attempt.");
}
boolean success = persistence.removeById(id);
int code = success ? HttpStatus.OK_200 : HttpStatus.INTERNAL_SERVER_ERROR_500;
String message = success
? String.format("Successfully deleted %s.", classToLowercase)
: String.format("Failed to delete %s", classToLowercase);
logMessageAndHalt(req, code, message, null);
boolean success = object.delete();
if (success) {
return object;
} else {
logMessageAndHalt(
req,
HttpStatus.INTERNAL_SERVER_ERROR_500,
String.format("Unknown error encountered. Failed to delete %s", classToLowercase),
null
);
}
} catch (HaltException e) {
throw e;
} catch (Exception e) {
Expand Down Expand Up @@ -335,6 +341,8 @@ private T createOrUpdate(Request req, Response res) {
return persistence.getById(object.id);
} catch (HaltException e) {
throw e;
} catch (JsonProcessingException e) {
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);
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*/
public class ApiUserController extends AbstractUserController<ApiUser> {
private static final Logger LOG = LoggerFactory.getLogger(ApiUserController.class);
private static final String DEFAULT_USAGE_PLAN_ID = 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";
private static final int API_KEY_LIMIT_PER_USER = 2;
private static final String API_KEY_ID_PARAM = "/:apiKeyId";
Expand Down Expand Up @@ -103,13 +103,11 @@ private ApiUser createApiKeyForApiUser(Request req, Response res) {
if (apiKey == null || apiKey.keyId == null) {
logMessageAndHalt(req,
HttpStatus.INTERNAL_SERVER_ERROR_500,
String.format("Unable to get AWS API key for user id (%s) and usage plan id (%s)", targetUser.id, usagePlanId),
String.format("Unable to create AWS API key for user id (%s) and usage plan id (%s)", targetUser.id, usagePlanId),
null
);
return null;
}
// Add new API key to user and persist
targetUser.apiKeys.add(apiKey);
Persistence.apiUsers.replace(targetUser.id, targetUser);
return Persistence.apiUsers.getById(targetUser.id);
}
Expand Down Expand Up @@ -158,20 +156,18 @@ protected ApiUser getUserProfile(Auth0UserProfile profile) {
*/
@Override
ApiUser preCreateHook(ApiUser user, Request req) {
ApiKey apiKey = ApiGatewayUtils.createApiKey(user.id, DEFAULT_USAGE_PLAN_ID);
if (apiKey == null) {
boolean success = user.createApiKey(DEFAULT_USAGE_PLAN_ID, false);
if (!success) {
logMessageAndHalt(req,
HttpStatus.INTERNAL_SERVER_ERROR_500,
String.format("Unable to get AWS api key for user %s", user),
null);
}
// store api key id including the actual api key (value)
user.apiKeys.add(apiKey);
// Call AbstractUserController#preCreateHook and delete api key in case something goes wrong.
try {
return super.preCreateHook(user, req);
} catch (HaltException e) {
deleteApiKey(apiKey);
user.delete();
throw e;
}
}
Expand All @@ -182,10 +178,7 @@ ApiUser preCreateHook(ApiUser user, Request req) {
*/
@Override
boolean preDeleteHook(ApiUser user, Request req) {
// TODO: Create method for deleting user's API keys?
for (ApiKey apiKey : user.apiKeys) {
deleteApiKey(apiKey);
}
// Note: API keys deleted in ApiUser#delete
return true;
}

Expand Down
Loading