Merge branch 'master' into ui/input-field-frontend-validation
This commit is contained in:
commit
c3971b123e
@ -16,7 +16,7 @@ from atst.routes.workspaces import bp as workspace_routes
|
|||||||
from atst.routes.requests import requests_bp
|
from atst.routes.requests import requests_bp
|
||||||
from atst.routes.dev import bp as dev_routes
|
from atst.routes.dev import bp as dev_routes
|
||||||
from atst.routes.errors import make_error_pages
|
from atst.routes.errors import make_error_pages
|
||||||
from atst.domain.authnid.crl.validator import Validator
|
from atst.domain.authnid.crl import Validator
|
||||||
from atst.domain.auth import apply_authentication
|
from atst.domain.auth import apply_authentication
|
||||||
|
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ def make_flask_callbacks(app):
|
|||||||
)
|
)
|
||||||
g.dev = os.getenv("FLASK_ENV", "dev") == "dev"
|
g.dev = os.getenv("FLASK_ENV", "dev") == "dev"
|
||||||
g.matchesPath = lambda href: re.match("^" + href, request.path)
|
g.matchesPath = lambda href: re.match("^" + href, request.path)
|
||||||
g.modalOpen = request.args.get("modal", False)
|
g.modal = request.args.get("modal", None)
|
||||||
g.current_user = {
|
g.current_user = {
|
||||||
"id": "cce17030-4109-4719-b958-ed109dbb87c8",
|
"id": "cce17030-4109-4719-b958-ed109dbb87c8",
|
||||||
"first_name": "Amanda",
|
"first_name": "Amanda",
|
||||||
@ -142,8 +142,6 @@ def make_crl_validator(app):
|
|||||||
for filename in pathlib.Path(app.config["CRL_DIRECTORY"]).glob("*"):
|
for filename in pathlib.Path(app.config["CRL_DIRECTORY"]).glob("*"):
|
||||||
crl_locations.append(filename.absolute())
|
crl_locations.append(filename.absolute())
|
||||||
app.crl_validator = Validator(
|
app.crl_validator = Validator(
|
||||||
roots=[app.config["CA_CHAIN"]], crl_locations=crl_locations
|
roots=[app.config["CA_CHAIN"]], crl_locations=crl_locations, logger=app.logger
|
||||||
)
|
)
|
||||||
for e in app.crl_validator.errors:
|
|
||||||
app.logger.error(e)
|
|
||||||
|
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
from atst.domain.exceptions import UnauthenticatedError, NotFoundError
|
||||||
|
from atst.domain.users import Users
|
||||||
|
from .utils import parse_sdn, email_from_certificate
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationContext():
|
||||||
|
|
||||||
|
def __init__(self, crl_validator, auth_status, sdn, cert):
|
||||||
|
if None in locals().values():
|
||||||
|
raise UnauthenticatedError(
|
||||||
|
"Missing required authentication context components"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.crl_validator = crl_validator
|
||||||
|
self.auth_status = auth_status
|
||||||
|
self.sdn = sdn
|
||||||
|
self.cert = cert.encode()
|
||||||
|
self._parsed_sdn = None
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
if not self.auth_status == "SUCCESS":
|
||||||
|
raise UnauthenticatedError("SSL/TLS client authentication failed")
|
||||||
|
|
||||||
|
elif not self._crl_check():
|
||||||
|
raise UnauthenticatedError("Client certificate failed CRL check")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_user(self):
|
||||||
|
try:
|
||||||
|
return Users.get_by_dod_id(self.parsed_sdn["dod_id"])
|
||||||
|
|
||||||
|
except NotFoundError:
|
||||||
|
email = self._get_user_email()
|
||||||
|
return Users.create(**{"email": email, **self.parsed_sdn})
|
||||||
|
|
||||||
|
def _get_user_email(self):
|
||||||
|
try:
|
||||||
|
return email_from_certificate(self.cert)
|
||||||
|
|
||||||
|
# this just means it is not an email certificate; we might choose to
|
||||||
|
# log in that case
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _crl_check(self):
|
||||||
|
if self.cert:
|
||||||
|
result = self.crl_validator.validate(self.cert)
|
||||||
|
return result
|
||||||
|
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parsed_sdn(self):
|
||||||
|
if not self._parsed_sdn:
|
||||||
|
try:
|
||||||
|
self._parsed_sdn = parse_sdn(self.sdn)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise UnauthenticatedError(str(exc))
|
||||||
|
|
||||||
|
return self._parsed_sdn
|
@ -20,11 +20,11 @@ class Validator:
|
|||||||
re.DOTALL,
|
re.DOTALL,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, crl_locations=[], roots=[], base_store=crypto.X509Store):
|
def __init__(self, crl_locations=[], roots=[], base_store=crypto.X509Store, logger=None):
|
||||||
self.errors = []
|
|
||||||
self.crl_locations = crl_locations
|
self.crl_locations = crl_locations
|
||||||
self.roots = roots
|
self.roots = roots
|
||||||
self.base_store = base_store
|
self.base_store = base_store
|
||||||
|
self.logger = logger
|
||||||
self._reset()
|
self._reset()
|
||||||
|
|
||||||
def _reset(self):
|
def _reset(self):
|
||||||
@ -34,12 +34,16 @@ class Validator:
|
|||||||
self._add_roots(self.roots)
|
self._add_roots(self.roots)
|
||||||
self.store.set_flags(crypto.X509StoreFlags.CRL_CHECK)
|
self.store.set_flags(crypto.X509StoreFlags.CRL_CHECK)
|
||||||
|
|
||||||
|
def log_error(self, message):
|
||||||
|
if self.logger:
|
||||||
|
self.logger.error(message)
|
||||||
|
|
||||||
def _add_crls(self, locations):
|
def _add_crls(self, locations):
|
||||||
for filename in locations:
|
for filename in locations:
|
||||||
try:
|
try:
|
||||||
self._add_crl(filename)
|
self._add_crl(filename)
|
||||||
except crypto.Error as err:
|
except crypto.Error as err:
|
||||||
self.errors.append(
|
self.log_error(
|
||||||
"CRL could not be parsed. Filename: {}, Error: {}, args: {}".format(
|
"CRL could not be parsed. Filename: {}, Error: {}, args: {}".format(
|
||||||
filename, type(err), err.args
|
filename, type(err), err.args
|
||||||
)
|
)
|
||||||
@ -116,7 +120,7 @@ class Validator:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
except crypto.X509StoreContextError as err:
|
except crypto.X509StoreContextError as err:
|
||||||
self.errors.append(
|
self.log_error(
|
||||||
"Certificate revoked or errored. Error: {}. Args: {}".format(
|
"Certificate revoked or errored. Error: {}. Args: {}".format(
|
||||||
type(err), err.args
|
type(err), err.args
|
||||||
)
|
)
|
@ -56,7 +56,6 @@ def refresh_crls(out_dir, logger=None):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
import datetime
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
import cryptography.x509 as x509
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
|
|
||||||
# TODO: our sample SDN does not have an email address
|
|
||||||
def parse_sdn(sdn):
|
def parse_sdn(sdn):
|
||||||
try:
|
try:
|
||||||
parts = sdn.split(",")
|
parts = sdn.split(",")
|
||||||
@ -9,5 +11,21 @@ def parse_sdn(sdn):
|
|||||||
cn = cn_string.split("=")[-1]
|
cn = cn_string.split("=")[-1]
|
||||||
info = cn.split(".")
|
info = cn.split(".")
|
||||||
return {"last_name": info[0], "first_name": info[1], "dod_id": info[-1]}
|
return {"last_name": info[0], "first_name": info[1], "dod_id": info[-1]}
|
||||||
|
|
||||||
except (IndexError, AttributeError):
|
except (IndexError, AttributeError):
|
||||||
raise ValueError("'{}' is not a valid SDN".format(sdn))
|
raise ValueError("'{}' is not a valid SDN".format(sdn))
|
||||||
|
|
||||||
|
|
||||||
|
def email_from_certificate(cert_file):
|
||||||
|
cert = x509.load_pem_x509_certificate(cert_file, default_backend())
|
||||||
|
try:
|
||||||
|
ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||||
|
email = ext.value.get_values_for_type(x509.RFC822Name)
|
||||||
|
if email:
|
||||||
|
return email[0]
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError("No email available for certificate with serial {}".format(cert.serial_number))
|
||||||
|
|
||||||
|
except x509.extensions.ExtensionNotFound:
|
||||||
|
raise ValueError("No subjectAltName available for certificate with serial {}".format(cert.serial_number))
|
||||||
|
@ -4,8 +4,8 @@ import pendulum
|
|||||||
|
|
||||||
from atst.domain.requests import Requests
|
from atst.domain.requests import Requests
|
||||||
from atst.domain.users import Users
|
from atst.domain.users import Users
|
||||||
from atst.domain.authnid.utils import parse_sdn
|
from atst.domain.authnid import AuthenticationContext
|
||||||
from atst.domain.exceptions import UnauthenticatedError
|
|
||||||
|
|
||||||
bp = Blueprint("atst", __name__)
|
bp = Blueprint("atst", __name__)
|
||||||
|
|
||||||
@ -30,28 +30,29 @@ def catch_all(path):
|
|||||||
return render_template("{}.html".format(path))
|
return render_template("{}.html".format(path))
|
||||||
|
|
||||||
|
|
||||||
# TODO: this should be partly consolidated into a domain function that takes
|
def _make_authentication_context():
|
||||||
# all the necessary UWSGI environment values as args and either returns a user
|
return AuthenticationContext(
|
||||||
# or raises the UnauthenticatedError
|
crl_validator=app.crl_validator,
|
||||||
|
auth_status=request.environ.get("HTTP_X_SSL_CLIENT_VERIFY"),
|
||||||
|
sdn=request.environ.get("HTTP_X_SSL_CLIENT_S_DN"),
|
||||||
|
cert=request.environ.get("HTTP_X_SSL_CLIENT_CERT")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/login-redirect')
|
@bp.route('/login-redirect')
|
||||||
def login_redirect():
|
def login_redirect():
|
||||||
if request.environ.get('HTTP_X_SSL_CLIENT_VERIFY') == 'SUCCESS' and _is_valid_certificate(request):
|
auth_context = _make_authentication_context()
|
||||||
sdn = request.environ.get('HTTP_X_SSL_CLIENT_S_DN')
|
auth_context.authenticate()
|
||||||
sdn_parts = parse_sdn(sdn)
|
user = auth_context.get_user()
|
||||||
user = Users.get_or_create_by_dod_id(**sdn_parts)
|
session["user_id"] = user.id
|
||||||
session["user_id"] = user.id
|
|
||||||
|
|
||||||
return redirect(url_for("atst.home"))
|
return redirect(url_for("atst.home"))
|
||||||
else:
|
|
||||||
raise UnauthenticatedError()
|
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_certificate(request):
|
def _is_valid_certificate(request):
|
||||||
cert = request.environ.get('HTTP_X_SSL_CLIENT_CERT')
|
cert = request.environ.get('HTTP_X_SSL_CLIENT_CERT')
|
||||||
if cert:
|
if cert:
|
||||||
result = app.crl_validator.validate(cert.encode())
|
result = app.crl_validator.validate(cert.encode())
|
||||||
if not result:
|
|
||||||
app.logger.info(app.crl_validator.errors[-1])
|
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
@ -33,4 +33,6 @@ def requests_index():
|
|||||||
|
|
||||||
mapped_requests = [map_request(r) for r in requests]
|
mapped_requests = [map_request(r) for r in requests]
|
||||||
|
|
||||||
return render_template("requests.html", requests=mapped_requests)
|
pending_fv = any(Requests.is_pending_financial_verification(r) for r in requests)
|
||||||
|
|
||||||
|
return render_template("requests.html", requests=mapped_requests, pending_financial_verification=pending_fv)
|
||||||
|
@ -11,13 +11,15 @@ class JEDIRequestFlow(object):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
current_step,
|
current_step,
|
||||||
|
current_user=None,
|
||||||
request=None,
|
request=None,
|
||||||
post_data=None,
|
post_data=None,
|
||||||
request_id=None,
|
request_id=None,
|
||||||
current_user=None,
|
|
||||||
existing_request=None,
|
existing_request=None,
|
||||||
):
|
):
|
||||||
self.current_step = current_step
|
self.current_step = current_step
|
||||||
|
|
||||||
|
self.current_user = current_user
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|
||||||
self.post_data = post_data
|
self.post_data = post_data
|
||||||
@ -26,16 +28,13 @@ class JEDIRequestFlow(object):
|
|||||||
self.request_id = request_id
|
self.request_id = request_id
|
||||||
self.form = self._form()
|
self.form = self._form()
|
||||||
|
|
||||||
self.current_user = current_user
|
|
||||||
self.existing_request = existing_request
|
self.existing_request = existing_request
|
||||||
|
|
||||||
def _form(self):
|
def _form(self):
|
||||||
if self.is_post:
|
if self.is_post:
|
||||||
return self.form_class()(self.post_data)
|
return self.form_class()(self.post_data)
|
||||||
elif self.request:
|
|
||||||
return self.form_class()(data=self.current_step_data)
|
|
||||||
else:
|
else:
|
||||||
return self.form_class()()
|
return self.form_class()(data=self.current_step_data)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
return self.form.validate()
|
return self.form.validate()
|
||||||
@ -59,6 +58,16 @@ class JEDIRequestFlow(object):
|
|||||||
def form_class(self):
|
def form_class(self):
|
||||||
return self.current_screen["form"]
|
return self.current_screen["form"]
|
||||||
|
|
||||||
|
# maps user data to fields in OrgForm; this should be moved into the
|
||||||
|
# request initialization process when we have a request schema, or we just
|
||||||
|
# shouldn't record this data on the request
|
||||||
|
def map_user_data(self, user):
|
||||||
|
return {
|
||||||
|
"fname_request": user.first_name,
|
||||||
|
"lname_request": user.last_name,
|
||||||
|
"email_request": user.email
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_step_data(self):
|
def current_step_data(self):
|
||||||
data = {}
|
data = {}
|
||||||
@ -69,8 +78,13 @@ class JEDIRequestFlow(object):
|
|||||||
if self.request:
|
if self.request:
|
||||||
if self.form_section == "review_submit":
|
if self.form_section == "review_submit":
|
||||||
data = self.request.body
|
data = self.request.body
|
||||||
|
if self.form_section == "information_about_you":
|
||||||
|
form_data = self.request.body.get(self.form_section, {})
|
||||||
|
data = { **self.map_user_data(self.request.creator), **form_data }
|
||||||
else:
|
else:
|
||||||
data = self.request.body.get(self.form_section, {})
|
data = self.request.body.get(self.form_section, {})
|
||||||
|
elif self.form_section == "information_about_you":
|
||||||
|
data = self.map_user_data(self.current_user)
|
||||||
|
|
||||||
return defaultdict(lambda: defaultdict(lambda: "Input required"), data)
|
return defaultdict(lambda: defaultdict(lambda: "Input required"), data)
|
||||||
|
|
||||||
|
@ -4,12 +4,13 @@ from . import requests_bp
|
|||||||
from atst.domain.requests import Requests
|
from atst.domain.requests import Requests
|
||||||
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
|
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
|
||||||
from atst.models.permissions import Permissions
|
from atst.models.permissions import Permissions
|
||||||
|
from atst.models.request_status_event import RequestStatus
|
||||||
from atst.domain.exceptions import UnauthorizedError
|
from atst.domain.exceptions import UnauthorizedError
|
||||||
|
|
||||||
|
|
||||||
@requests_bp.route("/requests/new/<int:screen>", methods=["GET"])
|
@requests_bp.route("/requests/new/<int:screen>", methods=["GET"])
|
||||||
def requests_form_new(screen):
|
def requests_form_new(screen):
|
||||||
jedi_flow = JEDIRequestFlow(screen, request=None)
|
jedi_flow = JEDIRequestFlow(screen, request=None, current_user=g.current_user)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"requests/screen-%d.html" % int(screen),
|
"requests/screen-%d.html" % int(screen),
|
||||||
@ -31,7 +32,7 @@ def requests_form_update(screen=1, request_id=None):
|
|||||||
_check_can_view_request(request_id)
|
_check_can_view_request(request_id)
|
||||||
|
|
||||||
request = Requests.get(request_id) if request_id is not None else None
|
request = Requests.get(request_id) if request_id is not None else None
|
||||||
jedi_flow = JEDIRequestFlow(screen, request, request_id=request_id)
|
jedi_flow = JEDIRequestFlow(screen, request=request, request_id=request_id, current_user=g.current_user)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"requests/screen-%d.html" % int(screen),
|
"requests/screen-%d.html" % int(screen),
|
||||||
@ -99,8 +100,8 @@ def requests_submit(request_id=None):
|
|||||||
request = Requests.get(request_id)
|
request = Requests.get(request_id)
|
||||||
Requests.submit(request)
|
Requests.submit(request)
|
||||||
|
|
||||||
if request.status == "approved":
|
if request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION:
|
||||||
return redirect("/requests?modal=True")
|
return redirect("/requests?modal=pendingFinancialVerification")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return redirect("/requests")
|
return redirect("/requests")
|
||||||
|
@ -20,7 +20,15 @@ const app = new Vue({
|
|||||||
return {
|
return {
|
||||||
modals: {
|
modals: {
|
||||||
styleguideModal: false,
|
styleguideModal: false,
|
||||||
|
pendingFinancialVerification: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted: function() {
|
||||||
|
const modalOpen = document.querySelector("#modalOpen");
|
||||||
|
if (modalOpen) {
|
||||||
|
const modal = modalOpen.getAttribute("data-modal");
|
||||||
|
this.modals[modal] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -35,6 +35,11 @@
|
|||||||
|
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
{% block modal %}{% endblock %}
|
{% block modal %}{% endblock %}
|
||||||
|
|
||||||
|
{% if g.modal %}
|
||||||
|
<div data-modal="{{ g.modal }}" id="modalOpen">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% assets "js_all" %}
|
{% assets "js_all" %}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
{% macro Alert(title, message=None, actions=None, level='info') -%}
|
{% macro Alert(title, message=None, actions=None, level='info', fragment=None) -%}
|
||||||
{% set role = 'alertdialog' if actions else 'alert' %}
|
{% set role = 'alertdialog' if actions else 'alert' %}
|
||||||
{% set levels = {
|
{% set levels = {
|
||||||
'warning': {
|
'warning': {
|
||||||
@ -31,6 +31,12 @@
|
|||||||
<div class='alert__message'>{{ message | safe }}</div>
|
<div class='alert__message'>{{ message | safe }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if fragment %}
|
||||||
|
<div class='alert__message'>
|
||||||
|
{% include fragment %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if actions %}
|
{% if actions %}
|
||||||
<div class='alert__actions'>{{ actions | safe }}</div>
|
<div class='alert__actions'>{{ actions | safe }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
12
templates/fragments/pending_financial_verification.html
Normal file
12
templates/fragments/pending_financial_verification.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<p>
|
||||||
|
The next step is to create a Task Order associated with JEDI Cloud.
|
||||||
|
Please contact a Contracting Officer (KO), Contracting Officer
|
||||||
|
Representative (COR), or a Financial Manager to help with this step.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Once the Task Order has been created, you will be asked to provide
|
||||||
|
details about the task order in the Financial Verification step.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i>Learn more</i> about the JEDI Task Order and the Financial Verification process.
|
||||||
|
</p>
|
@ -4,37 +4,18 @@
|
|||||||
{% from "components/modal.html" import Modal %}
|
{% from "components/modal.html" import Modal %}
|
||||||
{% from "components/empty_state.html" import EmptyState %}
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
|
|
||||||
{% block modal %}
|
|
||||||
{% if g.modalOpen %}
|
|
||||||
{% call Modal() %}
|
|
||||||
<h1>Your request is now approved!</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Your next step is to create a <b>Task Order (T.O.)</b> associated with
|
|
||||||
JEDI Cloud. Please consult a <b>Contracting Officer (KO)</b> or
|
|
||||||
<b>Contracting Officer Representative (COR)</b> to help with this step.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Once the Task Order (T.O.) has been created, we will need the following
|
|
||||||
details to create your account. These details will help keep your cloud
|
|
||||||
usage in sync with your budget.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{ Alert("You'll need these details: ",
|
|
||||||
message="<p>Task Order Number</p><p>Contracting Officer: Name, E-mail and Office</p>"
|
|
||||||
) }}
|
|
||||||
|
|
||||||
|
|
||||||
<div class='action-group'>
|
|
||||||
<a href='/requests' class='action-group__action usa-button'>Close</a>
|
|
||||||
</div>
|
|
||||||
{% endcall %}
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
{% call Modal(name='pendingFinancialVerification', dismissable=True) %}
|
||||||
|
<h1>Request submitted!</h1>
|
||||||
|
|
||||||
|
{% include 'fragments/pending_financial_verification.html' %}
|
||||||
|
|
||||||
|
<div class='action-group'>
|
||||||
|
<a v-on:click="closeModal('pendingFinancialVerification')" class='action-group__action usa-button'>Close</a>
|
||||||
|
</div>
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
{% if not requests %}
|
{% if not requests %}
|
||||||
|
|
||||||
{{ EmptyState(
|
{{ EmptyState(
|
||||||
@ -46,9 +27,11 @@
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{{ Alert('Pending Financial Verification',
|
{% if pending_financial_verification %}
|
||||||
message="<p>Your next step is to create a Task Order (T.O.) associated with JEDI Cloud. Please consult a Contracting Officer (KO) or Contracting Officer Representative (COR) to help with this step.</p>"
|
|
||||||
) }}
|
{{ Alert('Pending Financial Verification', fragment="fragments/pending_financial_verification.html") }}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="col col--grow">
|
<div class="col col--grow">
|
||||||
|
|
||||||
|
@ -2,11 +2,19 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
import alembic.config
|
import alembic.config
|
||||||
import alembic.command
|
import alembic.command
|
||||||
|
from logging.config import dictConfig
|
||||||
|
|
||||||
from atst.app import make_app, make_config
|
from atst.app import make_app, make_config
|
||||||
from atst.database import db as _db
|
from atst.database import db as _db
|
||||||
import tests.factories as factories
|
import tests.factories as factories
|
||||||
|
|
||||||
|
dictConfig({
|
||||||
|
'version': 1,
|
||||||
|
'handlers': {'wsgi': {
|
||||||
|
'class': 'logging.NullHandler',
|
||||||
|
}}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def app(request):
|
def app(request):
|
||||||
|
92
tests/domain/authnid/test_authentication_context.py
Normal file
92
tests/domain/authnid/test_authentication_context.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from atst.domain.authnid import AuthenticationContext
|
||||||
|
from atst.domain.exceptions import UnauthenticatedError, NotFoundError
|
||||||
|
from atst.domain.users import Users
|
||||||
|
|
||||||
|
from tests.mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS
|
||||||
|
from tests.factories import UserFactory
|
||||||
|
|
||||||
|
CERT = open("tests/fixtures/{}.crt".format(FIXTURE_EMAIL_ADDRESS)).read()
|
||||||
|
|
||||||
|
|
||||||
|
class MockCRLValidator():
|
||||||
|
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def validate(self, cert):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_authenticate():
|
||||||
|
auth_context = AuthenticationContext(
|
||||||
|
MockCRLValidator(True), "SUCCESS", DOD_SDN, CERT
|
||||||
|
)
|
||||||
|
assert auth_context.authenticate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsuccessful_status():
|
||||||
|
auth_context = AuthenticationContext(
|
||||||
|
MockCRLValidator(True), "FAILURE", DOD_SDN, CERT
|
||||||
|
)
|
||||||
|
with pytest.raises(UnauthenticatedError) as excinfo:
|
||||||
|
assert auth_context.authenticate()
|
||||||
|
|
||||||
|
(message,) = excinfo.value.args
|
||||||
|
assert "client authentication" in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_crl_check_fails():
|
||||||
|
auth_context = AuthenticationContext(
|
||||||
|
MockCRLValidator(False), "SUCCESS", DOD_SDN, CERT
|
||||||
|
)
|
||||||
|
with pytest.raises(UnauthenticatedError) as excinfo:
|
||||||
|
assert auth_context.authenticate()
|
||||||
|
|
||||||
|
(message,) = excinfo.value.args
|
||||||
|
assert "CRL check" in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_sdn():
|
||||||
|
auth_context = AuthenticationContext(
|
||||||
|
MockCRLValidator(True), "SUCCESS", "abc123", CERT
|
||||||
|
)
|
||||||
|
with pytest.raises(UnauthenticatedError) as excinfo:
|
||||||
|
auth_context.get_user()
|
||||||
|
|
||||||
|
(message,) = excinfo.value.args
|
||||||
|
assert "SDN" in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_exists():
|
||||||
|
user = UserFactory.create(**DOD_SDN_INFO)
|
||||||
|
auth_context = AuthenticationContext(
|
||||||
|
MockCRLValidator(True), "SUCCESS", DOD_SDN, CERT
|
||||||
|
)
|
||||||
|
auth_user = auth_context.get_user()
|
||||||
|
|
||||||
|
assert auth_user == user
|
||||||
|
|
||||||
|
|
||||||
|
def test_creates_user():
|
||||||
|
# check user does not exist
|
||||||
|
with pytest.raises(NotFoundError):
|
||||||
|
Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
||||||
|
|
||||||
|
auth_context = AuthenticationContext(
|
||||||
|
MockCRLValidator(True), "SUCCESS", DOD_SDN, CERT
|
||||||
|
)
|
||||||
|
user = auth_context.get_user()
|
||||||
|
assert user.dod_id == DOD_SDN_INFO["dod_id"]
|
||||||
|
assert user.email == FIXTURE_EMAIL_ADDRESS
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_cert_has_no_email():
|
||||||
|
cert = open("ssl/client-certs/atat.mil.crt").read()
|
||||||
|
auth_context = AuthenticationContext(
|
||||||
|
MockCRLValidator(True), "SUCCESS", DOD_SDN, cert
|
||||||
|
)
|
||||||
|
user = auth_context.get_user()
|
||||||
|
|
||||||
|
assert user.email == None
|
@ -4,7 +4,7 @@ import re
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from OpenSSL import crypto, SSL
|
from OpenSSL import crypto, SSL
|
||||||
from atst.domain.authnid.crl.validator import Validator
|
from atst.domain.authnid.crl import Validator
|
||||||
import atst.domain.authnid.crl.util as util
|
import atst.domain.authnid.crl.util as util
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,16 +1,39 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import atst.domain.authnid.utils as utils
|
import atst.domain.authnid.utils as utils
|
||||||
from tests.mocks import DOD_SDN
|
from tests.mocks import DOD_SDN, FIXTURE_EMAIL_ADDRESS
|
||||||
|
|
||||||
|
|
||||||
def test_parse_sdn():
|
def test_parse_sdn():
|
||||||
parsed = utils.parse_sdn(DOD_SDN)
|
parsed = utils.parse_sdn(DOD_SDN)
|
||||||
assert parsed.get('first_name') == 'ART'
|
assert parsed.get("first_name") == "ART"
|
||||||
assert parsed.get('last_name') == 'GARFUNKEL'
|
assert parsed.get("last_name") == "GARFUNKEL"
|
||||||
assert parsed.get('dod_id') == '5892460358'
|
assert parsed.get("dod_id") == "5892460358"
|
||||||
|
|
||||||
|
|
||||||
def test_parse_bad_sdn():
|
def test_parse_bad_sdn():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
utils.parse_sdn('this has nothing to do with anything')
|
utils.parse_sdn("this has nothing to do with anything")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
utils.parse_sdn(None)
|
utils.parse_sdn(None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_email_cert():
|
||||||
|
cert_file = open("tests/fixtures/{}.crt".format(FIXTURE_EMAIL_ADDRESS), "rb").read()
|
||||||
|
email = utils.email_from_certificate(cert_file)
|
||||||
|
assert email == FIXTURE_EMAIL_ADDRESS
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_cert_with_no_email():
|
||||||
|
cert_file = open("tests/fixtures/no-email.crt", "rb").read()
|
||||||
|
with pytest.raises(ValueError) as excinfo:
|
||||||
|
email = utils.email_from_certificate(cert_file)
|
||||||
|
(message,) = excinfo.value.args
|
||||||
|
assert "email" in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_cert_with_no_san():
|
||||||
|
cert_file = open("tests/fixtures/no-san.crt", "rb").read()
|
||||||
|
with pytest.raises(ValueError) as excinfo:
|
||||||
|
email = utils.email_from_certificate(cert_file)
|
||||||
|
(message,) = excinfo.value.args
|
||||||
|
assert "subjectAltName" in message
|
||||||
|
46
tests/fixtures/README.md
vendored
Normal file
46
tests/fixtures/README.md
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Regenerating Fixture Certificates
|
||||||
|
|
||||||
|
You don't need to keep the key file generated by this process.
|
||||||
|
|
||||||
|
1. Certificate with an email as subjectAltName:
|
||||||
|
|
||||||
|
```
|
||||||
|
openssl req -x509 \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-sha256 \
|
||||||
|
-nodes \
|
||||||
|
-days 3650 \
|
||||||
|
-keyout _foo.key \
|
||||||
|
-out artgarfunkel@uso.mil.crt \
|
||||||
|
-subj "/CN=GARFUNKEL.ART.G.5892460358" \
|
||||||
|
-extensions SAN \
|
||||||
|
-config <(cat /etc/ssl/openssl.cnf; echo '[SAN]'; echo 'subjectAltName=email:artgarfunkel@uso.mil')
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Certificate with a DNS name as subjectAltName:
|
||||||
|
|
||||||
|
```
|
||||||
|
openssl req -x509 \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-sha256 \
|
||||||
|
-nodes \
|
||||||
|
-days 3650 \
|
||||||
|
-keyout _foo.key \
|
||||||
|
-out no-email.crt \
|
||||||
|
-subj "/CN=GARFUNKEL.ART.G.5892460358" \
|
||||||
|
-extensions SAN \
|
||||||
|
-config <(cat /etc/ssl/openssl.cnf; echo '[SAN]'; echo 'subjectAltName=DNS:artgarfunkel.com')
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Certificate with no subjectAltName:
|
||||||
|
|
||||||
|
```
|
||||||
|
openssl req -x509 \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-sha256 \
|
||||||
|
-nodes \
|
||||||
|
-days 3650 \
|
||||||
|
-keyout _foo.key \
|
||||||
|
-out no-san.crt \
|
||||||
|
-subj "/CN=GARFUNKEL.ART.G.5892460358"
|
||||||
|
```
|
29
tests/fixtures/artgarfunkel@uso.mil.crt
vendored
Normal file
29
tests/fixtures/artgarfunkel@uso.mil.crt
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIE8DCCAtigAwIBAgIJALTstfJQuulmMA0GCSqGSIb3DQEBCwUAMCUxIzAhBgNV
|
||||||
|
BAMMGkdBUkZVTktFTC5BUlQuRy41ODkyNDYwMzU4MB4XDTE4MDgwODE0MDI0N1oX
|
||||||
|
DTI4MDgwNTE0MDI0N1owJTEjMCEGA1UEAwwaR0FSRlVOS0VMLkFSVC5HLjU4OTI0
|
||||||
|
NjAzNTgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQD1OuSSniuiUP3Q
|
||||||
|
JqVJOS2LE+kmK4Y5TexTCCDhBebarg+dEYipdA4AwZMKSDL/6D+lZJCM1MTsUgaN
|
||||||
|
X/8lRv2obVGnWuEL5Rbcwhlf3yTaohhlPk/qFyQoQaxcLZgwlwUn47i5jKG1cFqA
|
||||||
|
l4TignN3n6cwbpjFfkP9Oepiffu4ThrOsrOWTN56IB7TrHElFIdjVuUWbIuK9CET
|
||||||
|
8UWixUecrLr64AKyDndaVyzGJBwhtyn7AanVYld9la0FSxu8ZcYMikSOvOEqqOMA
|
||||||
|
Nu2NapInrb+g1JEPycXTpGxMiLbFscmAkgmAqkxzeFBW0UHCQsbxG6Ep1Km3QfYw
|
||||||
|
QqvEfNRPuGq2bGtpbMUF9K4DSsI2yErtc8QvKVQ86xEuwoiFxiRtO+WQKJrq8CqU
|
||||||
|
sZxcz6ZAw2pERIYtGCi573rxb8g7skEvlIPIYWqljEwFOIrgoRav0x3dHdfA5Ubh
|
||||||
|
M0fx38icinVmL0Xd7G0JFY2RFQ13/r/zaxmmm546tH9tSjn1bwaO/6OcX9g5kCUH
|
||||||
|
p2cWklug3/bDQyKre9UZBjI7bUMWtL1w6uhdRm5yq4lX+o8G/tbUYVPER75z+AKO
|
||||||
|
p/eizAKCKSHRXDKIJr3zZG54jyd+VzTcjBSNQN/liclEBzlnZqZUgPPUR8kQ0S3E
|
||||||
|
n8jQ/Jk9MS/DUuNvEzBgZS5e3KtpZwIDAQABoyMwITAfBgNVHREEGDAWgRRhcnRn
|
||||||
|
YXJmdW5rZWxAdXNvLm1pbDANBgkqhkiG9w0BAQsFAAOCAgEAQzAA7aweU7ZHDK3l
|
||||||
|
pjcpfXruVOqceGst/avMHZp3ZS9YOkd+K3jnLVBObfBGwZkJjsyqvs0AMVi3mTYY
|
||||||
|
WeEkhTk50G2xA2UydsOQcuH/qOT6duj54a0TCB4/2kMBq6IhCT3xR4rbfxA+5ArD
|
||||||
|
yCConiy1FUX5nofYGNC7VPUgjQb64LtTr1+wO6nTwdpALeOX2GZXoBWVQO2W+2Ul
|
||||||
|
buIGV5TnpjoJGJmuO/A76qwMi5+e6EYAKmomjGCaTKyvbb2WAlCoHzdDd+nQMFYm
|
||||||
|
gBBMVOkiTZ2udIbQMFGdqAZjDEP484rsCVrth4PKAZ9/3LAe6XddLZZbqq5cap2l
|
||||||
|
u6jDinFIeV2aldRh285qwvX7+R3KQK7k5wNDbf8DlaPUhnF+CliYDBKFCoKE60AY
|
||||||
|
mp40YME0NE3XSGuIemJUazxFAJ8zUu8yEP3K/mzAwtRHiy+yQwKyPK4Wl+skXYHs
|
||||||
|
XbouRkWK7jleVKXLiE0Uw0EbWkfAVBM8IgGWp70UivCTlAdokwdKBxsLhsn57mJ5
|
||||||
|
GP+9YTpwVQKWTBp06z0ZHaRI91d9Ke7YUSfDmLZ6VE9txqd9P2X2B2HbXFaYzGJh
|
||||||
|
gWtvqRh94ttaVsGr9iK7ANS9gXvn7Vb1ElyyF2wzP64WJtew7tywFq+Xhbm4/WPr
|
||||||
|
wM+BoGmfKP7uq0GBfu/HengJEGk=
|
||||||
|
-----END CERTIFICATE-----
|
29
tests/fixtures/no-email.crt
vendored
Normal file
29
tests/fixtures/no-email.crt
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIE6TCCAtGgAwIBAgIJAKlkkD2Xt+vWMA0GCSqGSIb3DQEBCwUAMCUxIzAhBgNV
|
||||||
|
BAMMGkdBUkZVTktFTC5BUlQuRy41ODkyNDYwMzU4MB4XDTE4MDgwODE0MjI0MFoX
|
||||||
|
DTI4MDgwNTE0MjI0MFowJTEjMCEGA1UEAwwaR0FSRlVOS0VMLkFSVC5HLjU4OTI0
|
||||||
|
NjAzNTgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDi81dB+2WcfgoD
|
||||||
|
ls0A7q/lefu+rOEDp90o22MO/D4uAkqztI/O/JUzGs3MG6YWEREwanlgS67Cnhki
|
||||||
|
NFHWKh0QUXyqGqYgxmyNMXemawFI5ilpCXhToSoi3aKP8Da6YO1FbhF+X9NpEgpC
|
||||||
|
cNHwKnfzOreQ4s01q8TdKL6X9wQvtX1ILNjPpCRMrfaBkiD7VbAC+Ds8SW9V10MD
|
||||||
|
1jQkZyaPtZgNU9nou9OCwHpiva1HIckNy0E8UAuSGHWmwkK62rTUvZfKHrtWWaWY
|
||||||
|
G/njwSdotAZvH4xdFW+/wJdcpj1IHACtzkctLjub78RmuvPsNHcEy6x77efSJKvb
|
||||||
|
oBGvEzOFYqoXDhvLOpxQfsZNFO1suJlcXynzVx9hmVrUfw7l8Z/yUhuNKhuRQ7fw
|
||||||
|
+9YMuXrYrcTCsZx73eTsQCX7A6QSq5//N9GNSHl5/adZXcmSwFed6OOUrMRs73HY
|
||||||
|
IH35yiyGS4BbulyKUeGHdeiD00Crb2/DSxrH5M2BqFQw993clkhdbr8AT/B/lhh8
|
||||||
|
Bysc3fHxwXGN65k1vfgrMm3aULUHLDH9RWjMra8OF2dZndQfmFSIxVOmDmmVjfME
|
||||||
|
lBg1TXY+JyKdkZrMb8IOpd08F+g10s+OnImldjsoSW0qkxDzUIbDRSvPK5dxukDc
|
||||||
|
ygecXqeKB7Bm2lceAurcARZiDdGvRwIDAQABoxwwGjAYBgNVHREEETAPgg1nYXJm
|
||||||
|
dW5rZWwuY29tMA0GCSqGSIb3DQEBCwUAA4ICAQCdaxkg4ZmmFqGqQ5bkjOucEowI
|
||||||
|
UpFIlgn3ORX/NjeAFpRlXr+kAyrezOfe3DzffFM63GVyqCR3swfwu0DdgpaGI++z
|
||||||
|
wMjXdDKDWfCSdFeFQczt/UyOQg7lkgKAgP6AgWrS9iOUwWY2Ecd+IhLjEAJ8ESgO
|
||||||
|
udi60tx9fDSlmpc3BlXBNkZUPGQW8abv+E2hV9dhNwCLVOxgK655E+9Lv3qRFFG5
|
||||||
|
HczGP8UcKL/0e1CIV8JfiPNG3lI9LJKE0fik7jN1nvPuM9ubKwKuxWgxDH4iP4aw
|
||||||
|
qa76rGYRT4VDcU89bRRX6fVCOK7iFd4db32zsAaFcOnztpMWAyIaTSZ4RuJivpqn
|
||||||
|
rTl0+ZOVHLierhFAH96prWcUBtyaprRCx5y/bIme+KBdEuge+s6+H4fYjMeryenQ
|
||||||
|
6kK8yqqAngDxxD400U1uP5TERu+E/JiP1AaiyPyh5j1bOjzM8/aohwTLK4pSeUHC
|
||||||
|
2AITpHPjXumTYMVLJliJ1/B+ZW8wS7kg1ICL6x9hrt/SbdDqQPZa/pE8NHuzMNSr
|
||||||
|
TaTDjaBEz50awlMYv4b3u+YQbVhGabw+2sYDG6VhiMakyuY2FCIi5Tc/ybBvXta8
|
||||||
|
lh8Xo8hSVlwvPumqLLITl17+KXHNL1KnTgWfXntFL6t/2OQrSbDfVXmThtW+FEmm
|
||||||
|
7ZFG54OsGWYdg8uNNg==
|
||||||
|
-----END CERTIFICATE-----
|
31
tests/fixtures/no-san.crt
vendored
Normal file
31
tests/fixtures/no-san.crt
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
Pdu9qr067Y/6AAogCQANMXWfywkc+TZMlQ==
|
||||||
|
-----END CERTIFICATE-----
|
@ -3,18 +3,11 @@ from tests.factories import RequestFactory, UserFactory
|
|||||||
|
|
||||||
MOCK_USER = UserFactory.build()
|
MOCK_USER = UserFactory.build()
|
||||||
MOCK_REQUEST = RequestFactory.build(
|
MOCK_REQUEST = RequestFactory.build(
|
||||||
creator=MOCK_USER.id,
|
creator=MOCK_USER.id, body={"financial_verification": {"pe_id": "0203752A"}}
|
||||||
body={
|
|
||||||
"financial_verification": {
|
|
||||||
"pe_id": "0203752A",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
DOD_SDN_INFO = {
|
DOD_SDN_INFO = {"first_name": "ART", "last_name": "GARFUNKEL", "dod_id": "5892460358"}
|
||||||
'first_name': 'ART',
|
|
||||||
'last_name': 'GARFUNKEL',
|
|
||||||
'dod_id': '5892460358'
|
|
||||||
}
|
|
||||||
DOD_SDN = f"CN={DOD_SDN_INFO['last_name']}.{DOD_SDN_INFO['first_name']}.G.{DOD_SDN_INFO['dod_id']},OU=OTHER,OU=PKI,OU=DoD,O=U.S. Government,C=US"
|
DOD_SDN = f"CN={DOD_SDN_INFO['last_name']}.{DOD_SDN_INFO['first_name']}.G.{DOD_SDN_INFO['dod_id']},OU=OTHER,OU=PKI,OU=DoD,O=U.S. Government,C=US"
|
||||||
|
|
||||||
MOCK_VALID_PE_ID = "8675309U"
|
MOCK_VALID_PE_ID = "8675309U"
|
||||||
|
|
||||||
|
FIXTURE_EMAIL_ADDRESS = "artgarfunkel@uso.mil"
|
||||||
|
@ -66,3 +66,39 @@ def test_nonexistent_request(client, user_session):
|
|||||||
response = client.get("/requests/new/1/foo", follow_redirects=True)
|
response = client.get("/requests/new/1/foo", follow_redirects=True)
|
||||||
|
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_creator_info_is_autopopulated(monkeypatch, client, user_session):
|
||||||
|
user = UserFactory.create()
|
||||||
|
user_session(user)
|
||||||
|
request = RequestFactory.create(creator=user, body={"information_about_you": {}})
|
||||||
|
|
||||||
|
response = client.get("/requests/new/2/{}".format(request.id))
|
||||||
|
body = response.data.decode()
|
||||||
|
assert 'value="{}"'.format(user.first_name) in body
|
||||||
|
assert 'value="{}"'.format(user.last_name) in body
|
||||||
|
assert 'value="{}"'.format(user.email) in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_creator_info_is_autopopulated_for_new_request(monkeypatch, client, user_session):
|
||||||
|
user = UserFactory.create()
|
||||||
|
user_session(user)
|
||||||
|
|
||||||
|
response = client.get("/requests/new/2")
|
||||||
|
body = response.data.decode()
|
||||||
|
assert 'value="{}"'.format(user.first_name) in body
|
||||||
|
assert 'value="{}"'.format(user.last_name) in body
|
||||||
|
assert 'value="{}"'.format(user.email) in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_creator_info_is_not_autopopulated(monkeypatch, client, user_session):
|
||||||
|
user = UserFactory.create()
|
||||||
|
creator = UserFactory.create()
|
||||||
|
user_session(user)
|
||||||
|
request = RequestFactory.create(creator=creator, body={"information_about_you": {}})
|
||||||
|
|
||||||
|
response = client.get("/requests/new/2/{}".format(request.id))
|
||||||
|
body = response.data.decode()
|
||||||
|
assert not user.first_name in body
|
||||||
|
assert not user.last_name in body
|
||||||
|
assert not user.email in body
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from tests.mocks import MOCK_USER
|
from tests.mocks import MOCK_USER
|
||||||
from tests.factories import RequestFactory
|
from tests.factories import RequestFactory
|
||||||
|
from atst.models.request_status_event import RequestStatus
|
||||||
|
|
||||||
|
|
||||||
def _mock_func(*args, **kwargs):
|
def _mock_func(*args, **kwargs):
|
||||||
@ -27,12 +28,11 @@ def test_submit_autoapproved_reviewed_request(monkeypatch, client, user_session)
|
|||||||
user_session()
|
user_session()
|
||||||
monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func)
|
monkeypatch.setattr("atst.domain.requests.Requests.get", _mock_func)
|
||||||
monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func)
|
monkeypatch.setattr("atst.domain.requests.Requests.submit", _mock_func)
|
||||||
monkeypatch.setattr("atst.models.request.Request.status", "approved")
|
monkeypatch.setattr("atst.models.request.Request.status", RequestStatus.PENDING_FINANCIAL_VERIFICATION)
|
||||||
# this just needs to send a known invalid form value
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/requests/submit/1",
|
"/requests/submit/1",
|
||||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
data="",
|
data="",
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
)
|
)
|
||||||
assert "/requests?modal=True" in response.headers["Location"]
|
assert "/requests?modal=" in response.headers["Location"]
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
import pytest
|
||||||
from flask import session, url_for
|
from flask import session, url_for
|
||||||
from .mocks import DOD_SDN
|
from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS
|
||||||
|
from atst.domain.users import Users
|
||||||
|
from atst.domain.exceptions import NotFoundError
|
||||||
|
from .factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
MOCK_USER = {"id": "438567dd-25fa-4d83-a8cc-8aa8366cb24a"}
|
MOCK_USER = {"id": "438567dd-25fa-4d83-a8cc-8aa8366cb24a"}
|
||||||
@ -10,12 +14,15 @@ def _fetch_user_info(c, t):
|
|||||||
|
|
||||||
|
|
||||||
def test_successful_login_redirect(client, monkeypatch):
|
def test_successful_login_redirect(client, monkeypatch):
|
||||||
monkeypatch.setattr("atst.routes._is_valid_certificate", lambda *args: True)
|
monkeypatch.setattr("atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True)
|
||||||
|
monkeypatch.setattr("atst.domain.authnid.AuthenticationContext.get_user", lambda *args: UserFactory.create())
|
||||||
|
|
||||||
resp = client.get(
|
resp = client.get(
|
||||||
"/login-redirect",
|
"/login-redirect",
|
||||||
environ_base={
|
environ_base={
|
||||||
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS", "HTTP_X_SSL_CLIENT_S_DN": DOD_SDN
|
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
||||||
|
"HTTP_X_SSL_CLIENT_S_DN": "",
|
||||||
|
"HTTP_X_SSL_CLIENT_CERT": "",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -58,8 +65,8 @@ UNPROTECTED_ROUTES = ["/", "/login-dev", "/login-redirect", "/unauthorized"]
|
|||||||
|
|
||||||
|
|
||||||
def test_crl_validation_on_login(client):
|
def test_crl_validation_on_login(client):
|
||||||
good_cert = open("ssl/client-certs/atat.mil.crt", "rb").read()
|
good_cert = open("ssl/client-certs/atat.mil.crt").read()
|
||||||
bad_cert = open("ssl/client-certs/bad-atat.mil.crt", "rb").read()
|
bad_cert = open("ssl/client-certs/bad-atat.mil.crt").read()
|
||||||
|
|
||||||
# bad cert is on the test CRL
|
# bad cert is on the test CRL
|
||||||
resp = client.get(
|
resp = client.get(
|
||||||
@ -67,7 +74,7 @@ def test_crl_validation_on_login(client):
|
|||||||
environ_base={
|
environ_base={
|
||||||
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
||||||
"HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
|
"HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
|
||||||
"HTTP_X_SSL_CLIENT_CERT": bad_cert.decode(),
|
"HTTP_X_SSL_CLIENT_CERT": bad_cert,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 401
|
assert resp.status_code == 401
|
||||||
@ -79,9 +86,55 @@ def test_crl_validation_on_login(client):
|
|||||||
environ_base={
|
environ_base={
|
||||||
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
||||||
"HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
|
"HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
|
||||||
"HTTP_X_SSL_CLIENT_CERT": good_cert.decode(),
|
"HTTP_X_SSL_CLIENT_CERT": good_cert,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
assert "home" in resp.headers["Location"]
|
assert "home" in resp.headers["Location"]
|
||||||
assert session["user_id"]
|
assert session["user_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_creates_new_user_on_login(monkeypatch, client):
|
||||||
|
monkeypatch.setattr("atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True)
|
||||||
|
cert_file = open("tests/fixtures/{}.crt".format(FIXTURE_EMAIL_ADDRESS)).read()
|
||||||
|
|
||||||
|
# ensure user does not exist
|
||||||
|
with pytest.raises(NotFoundError):
|
||||||
|
Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
||||||
|
|
||||||
|
resp = client.get(
|
||||||
|
"/login-redirect",
|
||||||
|
environ_base={
|
||||||
|
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
||||||
|
"HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
|
||||||
|
"HTTP_X_SSL_CLIENT_CERT": cert_file,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
user = Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
||||||
|
assert user.first_name == DOD_SDN_INFO["first_name"]
|
||||||
|
assert user.last_name == DOD_SDN_INFO["last_name"]
|
||||||
|
assert user.email == FIXTURE_EMAIL_ADDRESS
|
||||||
|
|
||||||
|
|
||||||
|
def test_creates_new_user_without_email_on_login(monkeypatch, client):
|
||||||
|
monkeypatch.setattr("atst.routes._is_valid_certificate", lambda *args: True)
|
||||||
|
cert_file = open("ssl/client-certs/atat.mil.crt").read()
|
||||||
|
|
||||||
|
# ensure user does not exist
|
||||||
|
with pytest.raises(NotFoundError):
|
||||||
|
Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
||||||
|
|
||||||
|
resp = client.get(
|
||||||
|
"/login-redirect",
|
||||||
|
environ_base={
|
||||||
|
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
||||||
|
"HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
|
||||||
|
"HTTP_X_SSL_CLIENT_CERT": cert_file,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
user = Users.get_by_dod_id(DOD_SDN_INFO["dod_id"])
|
||||||
|
assert user.first_name == DOD_SDN_INFO["first_name"]
|
||||||
|
assert user.last_name == DOD_SDN_INFO["last_name"]
|
||||||
|
assert user.email == None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user