diff --git a/auth-proxy/conf/etc/httpd/conf.d/01-auth-proxy.conf b/auth-proxy/conf/etc/httpd/conf.d/01-auth-proxy.conf
index 9001287..8908a70 100644
--- a/auth-proxy/conf/etc/httpd/conf.d/01-auth-proxy.conf
+++ b/auth-proxy/conf/etc/httpd/conf.d/01-auth-proxy.conf
@@ -1,14 +1,5 @@
-ServerName localhost
-
-# Force all traffic to HTTPS
-
- RewriteEngine On
- RewriteCond %{HTTPS} off
- RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
-
-
+# Auth Proxy
- DocumentRoot /var/www/html
# Makes $HOSTNAME available as SSI envvar
PassEnv HOSTNAME
@@ -30,15 +21,15 @@ ServerName localhost
OIDCAuthNHeader ${REMOTE_USER_HEADER}
# Upstream API Location
-
+
AuthType oauth20
AuthName "OpenID Connect (HMDA Ops)"
Require valid-user
LogMessage "REMOTE_USER: %{REMOTE_USER}" hook=check_authz
- ProxyPass ${UPSTREAM_API_URI}
- ProxyPassReverse ${UPSTREAM_API_URI}
+ ProxyPass ${FILING_API_UPSTREAM_URI}
+ ProxyPassReverse ${FILING_API_UPSTREAM_URI}
# Top-level path, providing default settings for CORS and OIDC
diff --git a/auth-proxy/conf/etc/httpd/conf.d/02-institution-search.conf b/auth-proxy/conf/etc/httpd/conf.d/02-institution-search.conf
new file mode 100644
index 0000000..50a0b0e
--- /dev/null
+++ b/auth-proxy/conf/etc/httpd/conf.d/02-institution-search.conf
@@ -0,0 +1,35 @@
+# Institution Search Reverse Proxy
+
+
+ # Enable HTTPS with default Apache cert
+ SSLEngine On
+ SSLCertificateFile /etc/pki/tls/certs/localhost.crt
+ SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
+
+ # Route all traffic CORS and ProxyPass
+
+
+ # CORS Preflight
+ # SEE: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
+
+ LogMessage "CORS Preflight - Origin: %{req:Origin}; Headers: %{req:Access-Control-Request-Headers}; Methods: %{req:Access-Control-Request-Method}"
+
+ Header always set Access-Control-Allow-Origin "*"
+ Header always set Access-Control-Allow-Methods: "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD"
+ Header always set Access-Control-Allow-Headers: "Authorization, Cache-Control, Accept, Content-Type"
+ Header always set Access-Control-Max-Age "600"
+
+ RewriteEngine On
+ RewriteRule ^(.*)$ $1 [R=204,L]
+
+
+ LogMessage "CORS Request - Origin: %{req:Origin}"
+
+ Header always set Access-Control-Allow-Origin "*"
+
+
+ ProxyPass ${PUBLIC_API_UPSTREAM_URI}
+ ProxyPassReverse ${PUBLIC_API_UPSTREAM_URI}
+
+
+
diff --git a/auth-proxy/conf/etc/httpd/conf/httpd.conf b/auth-proxy/conf/etc/httpd/conf/httpd.conf
index a7768c7..9dc86f1 100644
--- a/auth-proxy/conf/etc/httpd/conf/httpd.conf
+++ b/auth-proxy/conf/etc/httpd/conf/httpd.conf
@@ -22,8 +22,10 @@ Group apache
# Bind Apache to specific IP addresses and/or ports
-Listen 8080
-Listen 8443
+# Auth Proxy
+Listen 8443
+# Institution Search
+Listen 9443
# Deny access to the entirety of your server's filesystem.
diff --git a/institution-search/.gitignore b/institution-search/.gitignore
deleted file mode 100644
index 5258dc5..0000000
--- a/institution-search/.gitignore
+++ /dev/null
@@ -1,95 +0,0 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-env/
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*,cover
-.hypothesis/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# pyenv
-.python-version
-
-# celery beat schedule file
-celerybeat-schedule
-
-# dotenv
-.env
-
-# virtualenv
-.venv/
-venv/
-ENV/
-
-# Spyder project settings
-.spyderproject
-
-# Rope project settings
-.ropeproject
-
-# SSL directory
-ssl.crt
-ssl.key
-
diff --git a/institution-search/Dockerfile b/institution-search/Dockerfile
deleted file mode 100644
index 386fa00..0000000
--- a/institution-search/Dockerfile
+++ /dev/null
@@ -1,5 +0,0 @@
-FROM python:3.5-onbuild
-
-MAINTAINER Hans Keeler
-
-ENTRYPOINT ["./docker-entrypoint.sh"]
diff --git a/institution-search/app.py b/institution-search/app.py
deleted file mode 100644
index 6b1da48..0000000
--- a/institution-search/app.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from collections import defaultdict
-from flask import abort, Flask, jsonify, request
-from flask_cors import CORS
-from pprint import pprint
-from werkzeug.exceptions import BadRequest
-import yaml
-
-institutions = None
-
-with open("data/institutions.yaml", 'r') as f:
- institutions = yaml.safe_load(f)['institutions']
-
-app = Flask(__name__)
-CORS(app)
-
-inst_by_domain = {}
-
-for inst in institutions:
- for domain in inst['domains']:
- try:
- inst_by_domain[domain].append(inst)
- except KeyError:
- inst_by_domain[domain] = [inst]
-
-
-@app.route('/', methods=['GET'])
-def home():
- resp = {'message': 'Welcome to the CFPB\'s Institutions API'}
-
- return jsonify(resp)
-
-
-@app.route('/institutions')
-def get_institutions():
- domain = request.args['domain']
- results = inst_by_domain.get(domain, [])
-
- return jsonify(results=results)
-
-
-def gen_error_json(message, code):
- """
- Builds standard JSON error message
- """
- resp = {'message': message, 'statusCode': code}
-
- return jsonify(resp), code
-
-# Register all Flask error handlers
-@app.errorhandler(404)
-def not_found_error(error):
- return gen_error_json('Resource not found', 404)
-
-@app.errorhandler(Exception)
-def default_error(error):
- app.logger.exception('Internal server error: {}'.format(error))
- return gen_error_json('Internal server error', 500)
-
-if __name__ == '__main__':
- app.run(host='0.0.0.0', debug=True)
diff --git a/institution-search/conf/gunicorn.py b/institution-search/conf/gunicorn.py
deleted file mode 100644
index 91787b8..0000000
--- a/institution-search/conf/gunicorn.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import multiprocessing
-
-# Number of workers based on Gunicorn docs:
-# http://gunicorn-docs.readthedocs.org/en/latest/design.html#how-many-workers
-workers = multiprocessing.cpu_count() * 2 + 1
-
-# Logging
-loglevel = "info"
-# "-" = stderr
-accesslog = "-"
-errorlog = "-"
-
-# Accept X-Forwarded-For from reverse proxy:
-# http://gunicorn-docs.readthedocs.org/en/latest/settings.html#forwarded-allow-ips
-# http://gunicorn-docs.readthedocs.org/en/latest/deploy.html
-forwarded_allow_ips = "*"
-
-# SSL
-# Key and cert are generated at server startup. See docker-entrypoint.sh for details.
-certfile = 'ssl.crt'
-keyfile = 'ssl.key'
diff --git a/institution-search/data/institutions.yaml b/institution-search/data/institutions.yaml
deleted file mode 100644
index 4a36508..0000000
--- a/institution-search/data/institutions.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-institutions:
-
- - id: '0'
- name: 'Bank 0'
- domains:
- - 'bank0.com'
- externalIds:
- - name: 'RSSD ID'
- value: '1234567'
- - name: 'EIN'
- value: '12-3456789'
- - name: 'FDIC Cert No'
- value: '12345'
-
- - id: '1'
- name: 'Bank 1'
- domains:
- - 'bank1.com'
- - 'bankone.com'
- externalIds:
- - name: 'RSSD ID'
- value: '1111111'
- - name: 'EIN'
- value: '11-1111111'
-
- - id: '2'
- name: 'Bank 2'
- domains:
- - 'bank2.com'
- externalIds:
- - name: 'RSSD ID'
- value: '2222222'
- - name: 'EIN'
- value: '22-2222222'
-
- - id: '3'
- name: 'Bank 2 Too'
- domains:
- - 'bank2.com'
- externalIds:
- - name: 'RSSD ID'
- value: '3333333'
- - name: 'EIN'
- value: '33-3333333'
- - name: 'NCUA Charter'
- value: '333333'
-
diff --git a/institution-search/docker-entrypoint.sh b/institution-search/docker-entrypoint.sh
deleted file mode 100755
index d1c1006..0000000
--- a/institution-search/docker-entrypoint.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-
-# Generate SSL key and cert
-openssl req \
- -new \
- -newkey rsa:4096 \
- -days 365 \
- -nodes \
- -x509 \
- -subj "/C=US/ST=DC/L=Washington/O=CFPB/CN=localhost" \
- -keyout ssl.key \
- -out ssl.crt && \
-gunicorn -c conf/gunicorn.py -b 0.0.0.0:5000 app:app
diff --git a/institution-search/requirements.txt b/institution-search/requirements.txt
deleted file mode 100644
index 29d9aca..0000000
--- a/institution-search/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-flask==0.11.1
-flask-cors==3.0.2
-gunicorn==19.6.0
-PyYAML==3.12
diff --git a/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/HmdaValidInstitutionsFormAction.java b/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/HmdaValidInstitutionsFormAction.java
index b8587b2..c4efb53 100644
--- a/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/HmdaValidInstitutionsFormAction.java
+++ b/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/HmdaValidInstitutionsFormAction.java
@@ -22,7 +22,6 @@ public class HmdaValidInstitutionsFormAction implements FormAction, FormActionFa
public static final String FIELD_INSTITUTIONS = "user.attributes.institutions";
public static final String MISSING_INSTITUTION_MESSAGE = "missingInstitutionMessage";
public static final String INVALID_INSTITUTION_MESSAGE = "invalidInstitutionMessage";
- public static final String UNKNOWN_INSTITUTION_MESSAGE = "unknownInstitutionMessage";
public static final String UNKNOWN_EMAIL_DOMAIN_MESSAGE = "unknownEmailDomainMessage";
public static final String INSTITUTION_ERROR_MESSAGE = "institutionErrorMessage";
@@ -37,14 +36,12 @@ public void buildPage(FormContext context, LoginFormsProvider form) {
@Override
public void validate(ValidationContext context) {
- logger.info("Validating Institutions...");
-
String domain = null;
MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
List errors = new ArrayList<>();
- logger.info("Form Data: " + formData);
+ logger.info("Form data: " + formData);
String instFieldVal = formData.getFirst(FIELD_INSTITUTIONS);
if (Validation.isBlank(instFieldVal)) {
@@ -55,14 +52,15 @@ public void validate(ValidationContext context) {
return;
}
+ Set userInstIds = new HashSet<>(Arrays.asList(instFieldVal.split(",")));
+
try {
- Set userInstIds = new HashSet<>(Arrays.asList(instFieldVal.split(",")));
String email = formData.getFirst(RegistrationPage.FIELD_EMAIL);
domain = email.split("@")[1];
- logger.info("Email Domain: " + domain);
+ List domainInsts = institutionService.findInstitutionsByDomain(domain);
- Set domainInsts = institutionService.findInstitutionsByDomain(domain);
+ logger.info(domainInsts.size() + " institution(s) found for domain \"" + domain + "\"");
if (domainInsts.isEmpty()) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, UNKNOWN_EMAIL_DOMAIN_MESSAGE, domain));
@@ -74,25 +72,23 @@ public void validate(ValidationContext context) {
// Get a set of institutionId(s) for a given domain
Set domainInstIds = new HashSet<>(domainInsts.size());
- for (Institution domainInstId : domainInsts) {
- domainInstIds.add(domainInstId.getId());
+ for (Institution domainInst : domainInsts) {
+ domainInstIds.add(domainInst.getId());
}
// Add error for every institution submitted not associated with domain
if (!domainInsts.containsAll(userInstIds)) {
+ Set invalidInstIds = new HashSet<>(userInstIds);
+ invalidInstIds.removeAll(domainInstIds);
- // Remove all matched institutions
- userInstIds.removeAll(domainInstIds);
-
- for (String userInstId : userInstIds) {
- if (!domainInsts.contains(userInstId)) {
- errors.add(new FormMessage(FIELD_INSTITUTIONS, INVALID_INSTITUTION_MESSAGE, userInstId, domain));
- }
+ for (String invalidInstId : invalidInstIds) {
+ logger.warn("User submitted invalid institution ID " + invalidInstId + " for domain \"" + domain + "\"");
+ errors.add(new FormMessage(FIELD_INSTITUTIONS, INVALID_INSTITUTION_MESSAGE, invalidInstId, domain));
}
}
} catch (Exception e) {
- logger.error("Error occurred while validating institution(s) against \"" + domain + "\" domain", e);
+ logger.error("Error occurred while validating institution(s) " + userInstIds + " against \"" + domain + "\" domain", e);
errors.add(new FormMessage(FIELD_INSTITUTIONS, INSTITUTION_ERROR_MESSAGE, domain));
}
@@ -133,9 +129,9 @@ public void init(Config.Scope scope) {
String uri = scope.get("institutionSearchUri");
Boolean validateSsl = scope.getBoolean("institutionSearchValidateSsl", true);
- logger.info("Initializing institution search: uri="+uri+", validateSsl="+validateSsl);
+ logger.info("Initializing institution search: uri=" + uri + ", validateSsl=" + validateSsl);
- institutionService = new InstitutionSearchHmdaApiImpl(uri, validateSsl);
+ institutionService = new InstitutionServiceHmdaApiImpl(uri, validateSsl);
}
@Override
diff --git a/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionSearchResults.java b/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionSearchResults.java
index d56dba3..f310ed1 100644
--- a/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionSearchResults.java
+++ b/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionSearchResults.java
@@ -5,23 +5,23 @@
public class InstitutionSearchResults {
- private List results;
+ private List institutions;
public InstitutionSearchResults() {
}
- public List getResults() {
- return results;
+ public List getInstitutions() {
+ return institutions;
}
- public void setResults(List results) {
- this.results = results;
+ public void setInstitutions(List institutions) {
+ this.institutions = institutions;
}
@Override
public String toString() {
return "InstitutionSearchResults{" +
- "results=" + results +
+ "institutions=" + institutions +
'}';
}
}
diff --git a/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionService.java b/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionService.java
index defeccf..99b2fa9 100644
--- a/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionService.java
+++ b/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionService.java
@@ -1,12 +1,9 @@
package gov.cfpb.keycloak.authenticator.hmda;
-import java.util.Set;
+import java.util.List;
-/**
- * Created by keelerh on 1/26/17.
- */
public interface InstitutionService {
- public Set findInstitutionsByDomain(String domain);
+ public List findInstitutionsByDomain(String domain) throws InstitutionServiceException;
}
diff --git a/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionServiceException.java b/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionServiceException.java
new file mode 100644
index 0000000..4f571e8
--- /dev/null
+++ b/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionServiceException.java
@@ -0,0 +1,16 @@
+package gov.cfpb.keycloak.authenticator.hmda;
+
+/**
+ * Created by keelerh on 2/8/17.
+ */
+public class InstitutionServiceException extends Exception {
+
+ public InstitutionServiceException(String message) {
+ super(message);
+ }
+
+ public InstitutionServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionSearchHmdaApiImpl.java b/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionServiceHmdaApiImpl.java
similarity index 60%
rename from keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionSearchHmdaApiImpl.java
rename to keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionServiceHmdaApiImpl.java
index 8fca85d..deced7f 100644
--- a/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionSearchHmdaApiImpl.java
+++ b/keycloak/providers/authenticator/hmda/src/main/java/gov/cfpb/keycloak/authenticator/hmda/InstitutionServiceHmdaApiImpl.java
@@ -6,22 +6,23 @@
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.ArrayList;
+import java.util.List;
-public class InstitutionSearchHmdaApiImpl implements InstitutionService {
+public class InstitutionServiceHmdaApiImpl implements InstitutionService {
- private static final Logger logger = Logger.getLogger(InstitutionSearchHmdaApiImpl.class);
+ private static final Logger logger = Logger.getLogger(InstitutionServiceHmdaApiImpl.class);
private WebTarget apiClient;
- public InstitutionSearchHmdaApiImpl(String apiUri, Boolean validateSsl) {
+ public InstitutionServiceHmdaApiImpl(String apiUri, Boolean validateSsl) {
this.apiClient = buildClient(apiUri, validateSsl);
}
@@ -62,11 +63,28 @@ private WebTarget buildClient(String apiUri, Boolean validateSsl) {
}
@Override
- public Set findInstitutionsByDomain(String domain) {
- WebTarget target = apiClient.queryParam("domain", domain);
- InstitutionSearchResults results = target.request(MediaType.APPLICATION_JSON_TYPE).get(InstitutionSearchResults.class);
+ public List findInstitutionsByDomain(String domain) throws InstitutionServiceException {
+ try {
+ Response response = apiClient.queryParam("domain", domain).request(MediaType.APPLICATION_JSON_TYPE).get();
+ Response.Status status = Response.Status.fromStatusCode(response.getStatus());
+
+ switch (status) {
+ case OK:
+ return response.readEntity(InstitutionSearchResults.class).getInstitutions();
+
+ case NOT_FOUND:
+ return new ArrayList<>();
+
+ default:
+ // TODO: Include details from error JSON message, if available.
+ logger.error("Unexpected response from institution search API while querying by domain=\""+domain+"\". " +
+ "HTTP Status: " + status.getStatusCode() + " - " + status.getReasonPhrase());
+ throw new InstitutionServiceException("Error occurred while searching for institutions with domain '" + domain + "'");
+ }
- return new HashSet<>(results.getResults());
+ } catch (Exception e) {
+ throw new InstitutionServiceException("Error occurred while searching for institutions with domain \"" + domain + "\"", e);
+ }
}
}
diff --git a/keycloak/themes/hmda/login/messages/messages_en.properties b/keycloak/themes/hmda/login/messages/messages_en.properties
index 3fccbb4..f097ae5 100644
--- a/keycloak/themes/hmda/login/messages/messages_en.properties
+++ b/keycloak/themes/hmda/login/messages/messages_en.properties
@@ -1,6 +1,5 @@
missingInstitutionMessage=• Please specify the Institution(s) for which you will be filing HMDA data.
invalidInstitutionMessage=• Institution {0} is not associated with {1} email domain.
-unknownInstitutionMessage=• No Institution found with an identifier of {0}.
unknownEmailDomainMessage=• The email domain {0} is not in the known list of HMDA filer domains.
institutionErrorMessage=• An error occurred while validating the institution(s) against the {0} email domain.
missingFirstNameMessage=• Please specify first name.
diff --git a/keycloak/themes/hmda/login/register.ftl b/keycloak/themes/hmda/login/register.ftl
index 7ce0edc..b74a441 100644
--- a/keycloak/themes/hmda/login/register.ftl
+++ b/keycloak/themes/hmda/login/register.ftl
@@ -56,6 +56,14 @@