Merge branch 'master' into ui/tooltips
This commit is contained in:
commit
143c58addc
19
.travis.yml
19
.travis.yml
@ -1,14 +1,17 @@
|
|||||||
sudo: required
|
sudo: required
|
||||||
language: python
|
language: minimal
|
||||||
python: "3.6"
|
|
||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
git:
|
git:
|
||||||
submodules: false
|
submodules: false
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- TESTER_IMAGE_NAME=atst-tester
|
|
||||||
- PROD_IMAGE_NAME=atst-prod
|
- PROD_IMAGE_NAME=atst-prod
|
||||||
|
- TESTER_IMAGE1_NAME=atst-tester-nocrls
|
||||||
|
- TESTER_IMAGE2_NAME=atst-tester
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- crl
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
# Use sed to replace the SSH URL with the public URL
|
# Use sed to replace the SSH URL with the public URL
|
||||||
@ -17,16 +20,22 @@ before_install:
|
|||||||
- git submodule update --init --recursive
|
- git submodule update --init --recursive
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
|
- rm -rf ./crl/*
|
||||||
- docker run -d --name postgres96 postgres:9.6-alpine
|
- docker run -d --name postgres96 postgres:9.6-alpine
|
||||||
- docker run -d --name redis redis:4.0.10-alpine
|
- docker run -d --name redis redis:4.0.10-alpine
|
||||||
- docker run --link postgres96:postgres96 --link redis:redis waisbrot/wait
|
- docker run --link postgres96:postgres96 --link redis:redis waisbrot/wait
|
||||||
- export postgres_ip="$(docker inspect -f "{{ .NetworkSettings.IPAddress }}" postgres96)"
|
- export postgres_ip="$(docker inspect -f "{{ .NetworkSettings.IPAddress }}" postgres96)"
|
||||||
- export redis_ip="$(docker inspect -f "{{ .NetworkSettings.IPAddress }}" redis)"
|
- export redis_ip="$(docker inspect -f "{{ .NetworkSettings.IPAddress }}" redis)"
|
||||||
- docker login -u $ATAT_DOCKER_REGISTRY_USERNAME -p $ATAT_DOCKER_REGISTRY_PASSWORD $ATAT_DOCKER_REGISTRY_URL
|
- docker login -u $ATAT_DOCKER_REGISTRY_USERNAME -p $ATAT_DOCKER_REGISTRY_PASSWORD $ATAT_DOCKER_REGISTRY_URL
|
||||||
- docker build --tag "${TESTER_IMAGE_NAME}" --add-host "postgreshost:${postgres_ip}" --add-host "redishost:${redis_ip}" . -f deploy/docker/tester/Dockerfile
|
- docker build --tag "${TESTER_IMAGE1_NAME}" --add-host "postgreshost:${postgres_ip}" --add-host "redishost:${redis_ip}" . -f deploy/docker/tester/Dockerfile
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- docker run --add-host "postgreshost:${postgres_ip}" --add-host "redishost:${redis_ip}" "${TESTER_IMAGE_NAME}"
|
- docker run -d --entrypoint='/bin/sh' -t --name current-atst-tester "${TESTER_IMAGE1_NAME}"
|
||||||
|
- docker container exec -t current-atst-tester script/sync-crls
|
||||||
|
- docker commit current-atst-tester "${TESTER_IMAGE2_NAME}"
|
||||||
|
- docker cp current-atst-tester:/opt/atat/atst/crl/. ./crl/
|
||||||
|
- docker container stop current-atst-tester
|
||||||
|
- docker run --add-host "postgreshost:${postgres_ip}" --add-host "redishost:${redis_ip}" "${TESTER_IMAGE2_NAME}"
|
||||||
|
|
||||||
before_deploy:
|
before_deploy:
|
||||||
- docker build --tag "${PROD_IMAGE_NAME}" . -f deploy/docker/prod/Dockerfile
|
- docker build --tag "${PROD_IMAGE_NAME}" . -f deploy/docker/prod/Dockerfile
|
||||||
|
@ -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))
|
||||||
|
12
atst/domain/date.py
Normal file
12
atst/domain/date.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import pendulum
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date(data):
|
||||||
|
date_formats = ["YYYY-MM-DD", "MM/DD/YYYY"]
|
||||||
|
for _format in date_formats:
|
||||||
|
try:
|
||||||
|
return pendulum.from_format(data, _format).date()
|
||||||
|
except (ValueError, pendulum.parsing.exceptions.ParserError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise ValueError("Unable to parse string {}".format(data))
|
@ -150,3 +150,7 @@ class Requests(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def is_pending_financial_verification(cls, request):
|
def is_pending_financial_verification(cls, request):
|
||||||
return request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
|
return request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_pending_ccpo_approval(cls, request):
|
||||||
|
return request.status == RequestStatus.PENDING_CCPO_APPROVAL
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
from wtforms.fields.html5 import DateField
|
from wtforms.fields.html5 import DateField
|
||||||
from wtforms.fields import Field
|
from wtforms.fields import Field
|
||||||
from wtforms.widgets import TextArea
|
from wtforms.widgets import TextArea
|
||||||
import pendulum
|
|
||||||
|
from atst.domain.date import parse_date
|
||||||
|
|
||||||
|
|
||||||
class DateField(DateField):
|
class DateField(DateField):
|
||||||
def _value(self):
|
def _value(self):
|
||||||
if self.data:
|
if self.data:
|
||||||
date_formats = ["YYYY-MM-DD", "MM/DD/YYYY"]
|
return parse_date(self.data)
|
||||||
for _format in date_formats:
|
|
||||||
try:
|
|
||||||
return pendulum.from_format(self.data, _format).date()
|
|
||||||
except (ValueError, pendulum.parsing.exceptions.ParserError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise ValueError("Unable to parse string {}".format(self.data))
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from wtforms.fields.html5 import EmailField
|
from wtforms.fields.html5 import EmailField
|
||||||
from wtforms.fields import StringField, SelectField
|
from wtforms.fields import StringField, SelectField
|
||||||
from wtforms.form import Form
|
|
||||||
from wtforms.validators import Required, Email
|
from wtforms.validators import Required, Email
|
||||||
|
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
@ -41,7 +40,7 @@ def suggest_pe_id(pe_id):
|
|||||||
|
|
||||||
def validate_pe_id(field, existing_request):
|
def validate_pe_id(field, existing_request):
|
||||||
try:
|
try:
|
||||||
pe_number = PENumbers.get(field.data)
|
PENumbers.get(field.data)
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
suggestion = suggest_pe_id(field.data)
|
suggestion = suggest_pe_id(field.data)
|
||||||
error_str = (
|
error_str = (
|
||||||
|
@ -12,9 +12,11 @@ class OrgForm(ValidatedForm):
|
|||||||
|
|
||||||
lname_request = StringField("Last Name", validators=[Required(), Alphabet()])
|
lname_request = StringField("Last Name", validators=[Required(), Alphabet()])
|
||||||
|
|
||||||
email_request = EmailField("Email Address", validators=[Required(), Email()])
|
email_request = EmailField("E-mail Address", validators=[Required(), Email()])
|
||||||
|
|
||||||
phone_number = TelField("Phone Number", validators=[Required(), PhoneNumber()])
|
phone_number = TelField("Phone Number",
|
||||||
|
description='Enter a 10-digit phone number',
|
||||||
|
validators=[Required(), PhoneNumber()])
|
||||||
|
|
||||||
service_branch = StringField(
|
service_branch = StringField(
|
||||||
"Service Branch or Agency",
|
"Service Branch or Agency",
|
||||||
|
@ -2,7 +2,7 @@ from wtforms.fields import StringField
|
|||||||
from wtforms.fields.html5 import EmailField
|
from wtforms.fields.html5 import EmailField
|
||||||
from wtforms.validators import Required, Email, Length
|
from wtforms.validators import Required, Email, Length
|
||||||
from .forms import ValidatedForm
|
from .forms import ValidatedForm
|
||||||
from .validators import IsNumber, Alphabet
|
from .validators import IsNumber
|
||||||
|
|
||||||
|
|
||||||
class POCForm(ValidatedForm):
|
class POCForm(ValidatedForm):
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
from wtforms.fields.html5 import IntegerField
|
from wtforms.fields.html5 import IntegerField
|
||||||
from wtforms.fields import RadioField, StringField, TextAreaField, SelectField
|
from wtforms.fields import RadioField, TextAreaField, SelectField
|
||||||
from wtforms.validators import NumberRange, InputRequired
|
|
||||||
from .fields import DateField
|
from .fields import DateField
|
||||||
from .forms import ValidatedForm
|
from .forms import ValidatedForm
|
||||||
from .validators import DateRange
|
|
||||||
import pendulum
|
|
||||||
|
|
||||||
|
|
||||||
class RequestForm(ValidatedForm):
|
class RequestForm(ValidatedForm):
|
||||||
|
@ -2,18 +2,19 @@ import re
|
|||||||
from wtforms.validators import ValidationError
|
from wtforms.validators import ValidationError
|
||||||
import pendulum
|
import pendulum
|
||||||
|
|
||||||
|
from atst.domain.date import parse_date
|
||||||
|
|
||||||
|
|
||||||
def DateRange(lower_bound=None, upper_bound=None, message=None):
|
def DateRange(lower_bound=None, upper_bound=None, message=None):
|
||||||
def _date_range(form, field):
|
def _date_range(form, field):
|
||||||
now = pendulum.now().date()
|
now = pendulum.now().date()
|
||||||
|
date = parse_date(field.data)
|
||||||
|
|
||||||
if lower_bound is not None:
|
if lower_bound is not None:
|
||||||
date = pendulum.parse(field.data).date()
|
|
||||||
if (now - lower_bound) > date:
|
if (now - lower_bound) > date:
|
||||||
raise ValidationError(message)
|
raise ValidationError(message)
|
||||||
|
|
||||||
if upper_bound is not None:
|
if upper_bound is not None:
|
||||||
date = pendulum.parse(field.data).date()
|
|
||||||
if (now + upper_bound) < date:
|
if (now + upper_bound) < date:
|
||||||
raise ValidationError(message)
|
raise ValidationError(message)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -9,8 +9,10 @@ def map_request(request):
|
|||||||
time_created = pendulum.instance(request.time_created)
|
time_created = pendulum.instance(request.time_created)
|
||||||
is_new = time_created.add(days=1) > pendulum.now()
|
is_new = time_created.add(days=1) > pendulum.now()
|
||||||
app_count = request.body.get("details_of_use", {}).get("num_software_systems", 0)
|
app_count = request.body.get("details_of_use", {}).get("num_software_systems", 0)
|
||||||
update_url = url_for('requests.requests_form_update', screen=1, request_id=request.id)
|
update_url = url_for(
|
||||||
verify_url = url_for('requests.financial_verification', request_id=request.id)
|
"requests.requests_form_update", screen=1, request_id=request.id
|
||||||
|
)
|
||||||
|
verify_url = url_for("requests.financial_verification", request_id=request.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"order_id": request.id,
|
"order_id": request.id,
|
||||||
@ -19,7 +21,9 @@ def map_request(request):
|
|||||||
"app_count": app_count,
|
"app_count": app_count,
|
||||||
"date": time_created.format("M/DD/YYYY"),
|
"date": time_created.format("M/DD/YYYY"),
|
||||||
"full_name": request.creator.full_name,
|
"full_name": request.creator.full_name,
|
||||||
"edit_link": verify_url if Requests.is_pending_financial_verification(request) else update_url
|
"edit_link": verify_url if Requests.is_pending_financial_verification(
|
||||||
|
request
|
||||||
|
) else update_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -33,4 +37,12 @@ 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)
|
||||||
|
pending_ccpo = any(Requests.is_pending_ccpo_approval(r) for r in requests)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"requests.html",
|
||||||
|
requests=mapped_requests,
|
||||||
|
pending_financial_verification=pending_fv,
|
||||||
|
pending_ccpo_approval=pending_ccpo,
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
|
elif 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),
|
||||||
@ -41,6 +42,7 @@ def requests_form_update(screen=1, request_id=None):
|
|||||||
current=screen,
|
current=screen,
|
||||||
next_screen=screen + 1,
|
next_screen=screen + 1,
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
|
jedi_request=jedi_flow.request,
|
||||||
can_submit=jedi_flow.can_submit,
|
can_submit=jedi_flow.can_submit,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -99,11 +101,11 @@ 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?modal=pendingCCPOApproval")
|
||||||
|
|
||||||
|
|
||||||
# TODO: generalize this, along with other authorizations, into a policy-pattern
|
# TODO: generalize this, along with other authorizations, into a policy-pattern
|
||||||
|
43
deploy/kubernetes/atst-debugger.yml
Normal file
43
deploy/kubernetes/atst-debugger.yml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: atst-debugger
|
||||||
|
namespace: atat
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
fsGroup: 101
|
||||||
|
containers:
|
||||||
|
- name: atst-debugger
|
||||||
|
image: registry.atat.codes:443/atst-prod:a1916b1
|
||||||
|
args: ["/bin/bash", "-c", "while true; do date; sleep 45; done"]
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: atst-envvars
|
||||||
|
volumeMounts:
|
||||||
|
- name: atst-config
|
||||||
|
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
||||||
|
subPath: atst-overrides.ini
|
||||||
|
- name: uwsgi-config
|
||||||
|
mountPath: "/opt/atat/atst/uwsgi-config.ini"
|
||||||
|
subPath: uwsgi-config.ini
|
||||||
|
- name: uwsgi-socket-dir
|
||||||
|
mountPath: "/var/run/uwsgi"
|
||||||
|
volumes:
|
||||||
|
- name: atst-config
|
||||||
|
secret:
|
||||||
|
secretName: atst-config-ini
|
||||||
|
items:
|
||||||
|
- key: atst-overrides.ini
|
||||||
|
path: atst-overrides.ini
|
||||||
|
mode: 0644
|
||||||
|
- name: uwsgi-config
|
||||||
|
configMap:
|
||||||
|
name: atst-config
|
||||||
|
items:
|
||||||
|
- key: uwsgi-config
|
||||||
|
path: uwsgi-config.ini
|
||||||
|
mode: 0644
|
||||||
|
- name: uwsgi-socket-dir
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
restartPolicy: Never
|
@ -55,9 +55,9 @@ data:
|
|||||||
ssl_stapling_verify on;
|
ssl_stapling_verify on;
|
||||||
resolver 8.8.8.8 8.8.4.4;
|
resolver 8.8.8.8 8.8.4.4;
|
||||||
# Request and validate client certificate
|
# Request and validate client certificate
|
||||||
#ssl_verify_client on;
|
ssl_verify_client on;
|
||||||
#ssl_verify_depth 10;
|
ssl_verify_depth 10;
|
||||||
#ssl_client_certificate /etc/nginx/ssl/ca/client-ca.pem;
|
ssl_client_certificate /etc/ssl/client-ca-bundle.pem;
|
||||||
# Guard against HTTPS -> HTTP downgrade
|
# Guard against HTTPS -> HTTP downgrade
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always";
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always";
|
||||||
location / {
|
location / {
|
||||||
|
@ -24,7 +24,10 @@ spec:
|
|||||||
fsGroup: 101
|
fsGroup: 101
|
||||||
containers:
|
containers:
|
||||||
- name: atst
|
- name: atst
|
||||||
image: registry.atat.codes:443/atst-prod:e9b6f76
|
image: registry.atat.codes:443/atst-prod:a1916b1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "2500Mi"
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: atst-envvars
|
name: atst-envvars
|
||||||
@ -32,6 +35,9 @@ spec:
|
|||||||
- name: atst-config
|
- name: atst-config
|
||||||
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
||||||
subPath: atst-overrides.ini
|
subPath: atst-overrides.ini
|
||||||
|
- name: nginx-client-ca-bundle
|
||||||
|
mountPath: "/opt/atat/atst/ssl/server-certs/ca-chain.pem"
|
||||||
|
subPath: client-ca-bundle.pem
|
||||||
- name: uwsgi-config
|
- name: uwsgi-config
|
||||||
mountPath: "/opt/atat/atst/uwsgi-config.ini"
|
mountPath: "/opt/atat/atst/uwsgi-config.ini"
|
||||||
subPath: uwsgi-config.ini
|
subPath: uwsgi-config.ini
|
||||||
@ -47,6 +53,9 @@ spec:
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: nginx-auth-tls
|
- name: nginx-auth-tls
|
||||||
mountPath: "/etc/ssl/private"
|
mountPath: "/etc/ssl/private"
|
||||||
|
- name: nginx-client-ca-bundle
|
||||||
|
mountPath: "/etc/ssl/client-ca-bundle.pem"
|
||||||
|
subPath: client-ca-bundle.pem
|
||||||
- name: nginx-config
|
- name: nginx-config
|
||||||
mountPath: "/etc/nginx/conf.d/atst.conf"
|
mountPath: "/etc/nginx/conf.d/atst.conf"
|
||||||
subPath: atst.conf
|
subPath: atst.conf
|
||||||
@ -78,6 +87,13 @@ spec:
|
|||||||
- key: tls.key
|
- key: tls.key
|
||||||
path: auth.atat.key
|
path: auth.atat.key
|
||||||
mode: 0640
|
mode: 0640
|
||||||
|
- name: nginx-client-ca-bundle
|
||||||
|
secret:
|
||||||
|
secretName: nginx-client-ca-bundle
|
||||||
|
items:
|
||||||
|
- key: client-ca-bundle.pem
|
||||||
|
path: client-ca-bundle.pem
|
||||||
|
mode: 0666
|
||||||
- name: nginx-config
|
- name: nginx-config
|
||||||
configMap:
|
configMap:
|
||||||
name: atst-nginx
|
name: atst-nginx
|
||||||
|
4
deploy/kubernetes/set_clientca_secret.sh
Executable file
4
deploy/kubernetes/set_clientca_secret.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
kubectl -n atat delete secret atst-config-ini
|
||||||
|
kubectl -n atat create secret generic nginx-client-ca-bundle --from-file="${1}"
|
@ -14,32 +14,54 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: () => 'anything'
|
default: () => 'anything'
|
||||||
},
|
},
|
||||||
value: {
|
initialValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: () => ''
|
default: () => ''
|
||||||
}
|
},
|
||||||
|
initialErrors: Array
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
showError: false,
|
showError: (this.initialErrors && this.initialErrors.length) || false,
|
||||||
showValid: false,
|
showValid: false,
|
||||||
mask: inputValidations[this.validation].mask,
|
mask: inputValidations[this.validation].mask,
|
||||||
renderedValue: this.value
|
pipe: inputValidations[this.validation].pipe || undefined,
|
||||||
|
keepCharPositions: inputValidations[this.validation].keepCharPositions || false,
|
||||||
|
validationError: inputValidations[this.validation].validationError || '',
|
||||||
|
value: this.initialValue,
|
||||||
|
modified: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed:{
|
||||||
|
rawValue: function () {
|
||||||
|
return this._rawValue(this.value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
const value = this.$refs.input.value
|
if (this.value) {
|
||||||
if (value) {
|
this._checkIfValid({ value: this.value, invalidate: true })
|
||||||
this._checkIfValid({ value, invalidate: true })
|
|
||||||
this.renderedValue = conformToMask(value, this.mask).conformedValue
|
if (this.mask && this.validation !== 'email') {
|
||||||
|
const mask = typeof this.mask.mask !== 'function'
|
||||||
|
? this.mask
|
||||||
|
: mask.mask(this.value).filter((val) => val !== '[]')
|
||||||
|
|
||||||
|
this.value = conformToMask(this.value, mask).conformedValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
// When user types a character
|
// When user types a character
|
||||||
onInput: function (value) {
|
onInput: function (e) {
|
||||||
|
// When we use the native textarea element, we receive an event object
|
||||||
|
// When we use the masked-input component, we receive the value directly
|
||||||
|
const value = typeof e === 'object' ? e.target.value : e
|
||||||
|
this.value = value
|
||||||
|
this.modified = true
|
||||||
this._checkIfValid({ value })
|
this._checkIfValid({ value })
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -52,7 +74,11 @@ export default {
|
|||||||
//
|
//
|
||||||
_checkIfValid: function ({ value, invalidate = false}) {
|
_checkIfValid: function ({ value, invalidate = false}) {
|
||||||
// Validate the value
|
// Validate the value
|
||||||
const valid = this._validate(value)
|
let valid = this._validate(value)
|
||||||
|
|
||||||
|
if (!this.modified && this.initialErrors && this.initialErrors.length) {
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
// Show error messages or not
|
// Show error messages or not
|
||||||
if (valid) {
|
if (valid) {
|
||||||
@ -70,13 +96,14 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
_validate: function (value) {
|
_rawValue: function (value) {
|
||||||
// Strip out all the mask characters
|
return inputValidations[this.validation].unmask.reduce((currentValue, character) => {
|
||||||
let rawValue = inputValidations[this.validation].unmask.reduce((currentValue, character) => {
|
|
||||||
return currentValue.split(character).join('')
|
return currentValue.split(character).join('')
|
||||||
}, value)
|
}, value)
|
||||||
|
},
|
||||||
|
|
||||||
return inputValidations[this.validation].match.test(rawValue)
|
_validate: function (value) {
|
||||||
|
return inputValidations[this.validation].match.test(this._rawValue(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,16 @@ const app = new Vue({
|
|||||||
return {
|
return {
|
||||||
modals: {
|
modals: {
|
||||||
styleguideModal: false,
|
styleguideModal: false,
|
||||||
|
pendingFinancialVerification: false,
|
||||||
|
pendingCCPOApproval: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted: function() {
|
||||||
|
const modalOpen = document.querySelector("#modalOpen");
|
||||||
|
if (modalOpen) {
|
||||||
|
const modal = modalOpen.getAttribute("data-modal");
|
||||||
|
this.modals[modal] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,20 +1,56 @@
|
|||||||
import createNumberMask from 'text-mask-addons/dist/createNumberMask'
|
import createNumberMask from 'text-mask-addons/dist/createNumberMask'
|
||||||
import emailMask from 'text-mask-addons/dist/emailMask'
|
import emailMask from 'text-mask-addons/dist/emailMask'
|
||||||
|
import createAutoCorrectedDatePipe from 'text-mask-addons/dist/createAutoCorrectedDatePipe'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
anything: {
|
anything: {
|
||||||
mask: false,
|
mask: false,
|
||||||
match: /^(?!\s*$).+/,
|
match: /^(?!\s*$).+/,
|
||||||
unmask: [],
|
unmask: [],
|
||||||
|
validationError: 'Please enter a response.'
|
||||||
|
},
|
||||||
|
integer: {
|
||||||
|
mask: createNumberMask({ prefix: '', allowDecimal: false }),
|
||||||
|
match: /^[1-9]\d*$/,
|
||||||
|
unmask: [','],
|
||||||
|
validationError: 'Please enter a number.'
|
||||||
},
|
},
|
||||||
dollars: {
|
dollars: {
|
||||||
mask: createNumberMask({ prefix: '$', allowDecimal: true }),
|
mask: createNumberMask({ prefix: '$', allowDecimal: true }),
|
||||||
match: /^-?\d+\.?\d*$/,
|
match: /^-?\d+\.?\d*$/,
|
||||||
unmask: ['$',',']
|
unmask: ['$',','],
|
||||||
|
validationError: 'Please enter a dollar amount.'
|
||||||
|
},
|
||||||
|
gigabytes: {
|
||||||
|
mask: createNumberMask({ prefix: '', suffix:'GB', allowDecimal: false }),
|
||||||
|
match: /^[1-9]\d*$/,
|
||||||
|
unmask: [',','GB'],
|
||||||
|
validationError: 'Please enter an amount of data in gigabytes.'
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
mask: emailMask,
|
mask: emailMask,
|
||||||
match: /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/,
|
match: /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/,
|
||||||
unmask: [],
|
unmask: [],
|
||||||
|
validationError: 'Please enter a valid e-mail address.'
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
mask: [/\d/,/\d/,'/',/\d/,/\d/,'/',/\d/,/\d/,/\d/,/\d/],
|
||||||
|
match: /(0[1-9]|1[012])[- \/.](0[1-9]|[12][0-9]|3[01])[- \/.](19|20)\d\d/,
|
||||||
|
unmask: [],
|
||||||
|
pipe: createAutoCorrectedDatePipe('mm/dd/yyyy HH:MM'),
|
||||||
|
keepCharPositions: true,
|
||||||
|
validationError: 'Please enter a valid date in the form MM/DD/YYYY.'
|
||||||
|
},
|
||||||
|
usPhone: {
|
||||||
|
mask: ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/],
|
||||||
|
match: /^\d{10}$/,
|
||||||
|
unmask: ['(',')','-',' '],
|
||||||
|
validationError: 'Please enter a 10-digit phone number.'
|
||||||
|
},
|
||||||
|
dodId: {
|
||||||
|
mask: createNumberMask({ prefix: '', allowDecimal: false, includeThousandsSeparator: false }),
|
||||||
|
match: /^\d{10}$/,
|
||||||
|
unmask: [],
|
||||||
|
validationError: 'Please enter a 10-digit D.O.D. ID number.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ APP_USER="atst"
|
|||||||
APP_UID="8010"
|
APP_UID="8010"
|
||||||
|
|
||||||
# Add additional packages required by app dependencies
|
# Add additional packages required by app dependencies
|
||||||
ADDITIONAL_PACKAGES="postgresql-libs python3 uwsgi uwsgi-python3"
|
ADDITIONAL_PACKAGES="postgresql-libs python3 rsync uwsgi uwsgi-python3"
|
||||||
|
|
||||||
# Run the shared alpine setup script
|
# Run the shared alpine setup script
|
||||||
source ./script/include/run_alpine_setup
|
source ./script/include/run_alpine_setup
|
||||||
|
@ -10,7 +10,7 @@ export FLASK_ENV=test
|
|||||||
RESET_DB="true"
|
RESET_DB="true"
|
||||||
|
|
||||||
# Define all relevant python files and directories for this app
|
# Define all relevant python files and directories for this app
|
||||||
PYTHON_FILES="./app.py ./atst ./config"
|
PYTHON_FILES="./app.py ./atst/** ./config"
|
||||||
|
|
||||||
# Enable Python testing
|
# Enable Python testing
|
||||||
RUN_PYTHON_TESTS="true"
|
RUN_PYTHON_TESTS="true"
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
@import 'elements/labels';
|
@import 'elements/labels';
|
||||||
@import 'elements/diff';
|
@import 'elements/diff';
|
||||||
@import 'elements/tooltip';
|
@import 'elements/tooltip';
|
||||||
|
@import 'elements/kpi';
|
||||||
|
|
||||||
@import 'components/topbar';
|
@import 'components/topbar';
|
||||||
@import 'components/global_layout';
|
@import 'components/global_layout';
|
||||||
|
@ -79,7 +79,7 @@
|
|||||||
margin-left: $gap;
|
margin-left: $gap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-input__title {
|
.usa-input__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -90,7 +90,7 @@
|
|||||||
margin-left: $gap/2;
|
margin-left: $gap/2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-input__help {
|
.usa-input__help {
|
||||||
@include h4;
|
@include h4;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
25
styles/elements/_kpi.scss
Normal file
25
styles/elements/_kpi.scss
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.kpi {
|
||||||
|
|
||||||
|
margin-bottom: $gap;
|
||||||
|
|
||||||
|
.kpi__item {
|
||||||
|
@include panel-base;
|
||||||
|
text-align: center;
|
||||||
|
margin: $gap;
|
||||||
|
padding: $gap * 2;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: -$gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: -$gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi__item__value {
|
||||||
|
@include h1;
|
||||||
|
padding-bottom: $gap / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -29,6 +29,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin panel-row {
|
||||||
|
@include grid-row;
|
||||||
|
|
||||||
|
.col {
|
||||||
|
margin: 0 $site-margins-mobile * 2;
|
||||||
|
|
||||||
|
@include media($medium-screen) {
|
||||||
|
margin: 0 $site-margins * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@mixin panel-actions {
|
@mixin panel-actions {
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,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 %}
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
{{ field.label | striptags}}
|
{{ field.label | striptags}}
|
||||||
{% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}
|
{% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if field.description %}
|
{% if field.description %}
|
||||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,24 +1,65 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% from "components/tooltip.html" import Tooltip %}
|
{% from "components/tooltip.html" import Tooltip %}
|
||||||
|
|
||||||
{% macro TextInput(field, tooltip='', placeholder='') -%}
|
{% macro TextInput(field, tooltip='', placeholder='', validation='anything', paragraph=False) -%}
|
||||||
<div class='usa-input {% if field.errors %}usa-input--error{% endif %}'>
|
<textinput
|
||||||
<label for={{field.name}}>
|
name='{{ field.name }}'
|
||||||
<div class="usa-input__title">{{ field.label | striptags }} {% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}</div>
|
validation='{{ validation }}'
|
||||||
|
{% if field.data %}initial-value='{{ field.data }}'{% endif %}
|
||||||
|
{% if field.errors %}v-bind:initial-errors='{{ field.errors }}'{% endif %}
|
||||||
|
inline-template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-bind:class="['usa-input usa-input--validation--' + validation, { 'usa-input--error': showError, 'usa-input--success': showValid }]">
|
||||||
|
|
||||||
|
<label for={{field.name}}>
|
||||||
|
<div class="usa-input__title">{{ field.label | striptags }} {% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}</div>
|
||||||
|
|
||||||
|
{% if field.description %}
|
||||||
|
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span v-show='showError'>{{ Icon('alert',classes="icon-validation")) }}</span>
|
||||||
|
<span v-show='showValid'>{{ Icon('ok',classes="icon-validation")) }}</span>
|
||||||
|
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{% if paragraph %}
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
v-on:input='onInput'
|
||||||
|
v-on:change='onChange'
|
||||||
|
v-bind:value='value'
|
||||||
|
id='{{ field.name }}'
|
||||||
|
ref='input'
|
||||||
|
placeholder='{{ placeholder }}'>
|
||||||
|
</textarea>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<masked-input
|
||||||
|
v-on:input='onInput'
|
||||||
|
v-on:change='onChange'
|
||||||
|
v-bind:value='value'
|
||||||
|
v-bind:mask='mask'
|
||||||
|
v-bind:pipe='pipe'
|
||||||
|
v-bind:keep-char-positions='keepCharPositions'
|
||||||
|
v-bind:aria-invalid='showError'
|
||||||
|
id='{{ field.name }}'
|
||||||
|
type='text'
|
||||||
|
ref='input'
|
||||||
|
placeholder='{{ placeholder }}'>
|
||||||
|
</masked-input>
|
||||||
|
|
||||||
{% if field.description %}
|
|
||||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if field.errors %} {{ Icon('alert',classes="icon-validation") }} {% endif %}
|
<input type='hidden' v-bind:value='rawValue' name='{{ field.name }}' />
|
||||||
</label>
|
|
||||||
|
|
||||||
{{ field(placeholder=placeholder) | safe }}
|
<template v-if='showError'>
|
||||||
|
<span v-if='initialErrors' v-for='error in initialErrors' class='usa-input__message' v-html='error'></span>
|
||||||
|
<span v-if='!initialErrors' class='usa-input__message' v-html='validationError'></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
{% if field.errors %}
|
</div>
|
||||||
{% for error in field.errors %}
|
</textinput>
|
||||||
<span class='usa-input__message'>{{ error }}</span>
|
{%- endmacro %}
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{%- endmacro %}
|
|
||||||
|
11
templates/fragments/pending_ccpo_approval_alert.html
Normal file
11
templates/fragments/pending_ccpo_approval_alert.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<p>
|
||||||
|
We will review and respond to your request in 72 hours. You’ll be notified via email or phone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
While your request is being reviewed, your 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>
|
||||||
|
Learn more about the JEDI Task Order and the Financial Verification process.
|
||||||
|
</p>
|
35
templates/fragments/pending_ccpo_approval_modal.html
Normal file
35
templates/fragments/pending_ccpo_approval_modal.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<h1>
|
||||||
|
Request submitted. Approval pending.
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
We will review and respond to your request in 72 hours. You’ll be notified via email or phone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Your request is being reviewed because:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Your request includes over $1 million for cloud resources
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
We may need more information about your request
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Next Steps
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
While your request is being reviewed, your 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>
|
||||||
|
Learn more about the JEDI Task Order and the Financial Verification process.
|
||||||
|
</p>
|
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,27 @@
|
|||||||
{% 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 %}
|
||||||
|
|
||||||
|
{% call Modal(name='pendingCCPOApproval', dismissable=True) %}
|
||||||
|
|
||||||
|
{% include 'fragments/pending_ccpo_approval_modal.html' %}
|
||||||
|
|
||||||
|
<div class='action-group'>
|
||||||
|
<a v-on:click="closeModal('pendingCCPOApproval')" class='action-group__action usa-button'>Close</a>
|
||||||
|
</div>
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
{% if not requests %}
|
{% if not requests %}
|
||||||
|
|
||||||
{{ EmptyState(
|
{{ EmptyState(
|
||||||
@ -46,9 +36,32 @@
|
|||||||
|
|
||||||
{% 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 %}
|
||||||
|
|
||||||
|
{% if pending_ccpo_approval %}
|
||||||
|
|
||||||
|
{{ Alert('Request submitted. Approval pending.', fragment="fragments/pending_ccpo_approval_alert.html") }}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row kpi">
|
||||||
|
<div class="kpi__item col col--grow">
|
||||||
|
<div class="kpi__item__value">3</div>
|
||||||
|
<div class="kpi__item__description">Pending Requests</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi__item col col--grow">
|
||||||
|
<div class="kpi__item__value">2,456</div>
|
||||||
|
<div class="kpi__item__description">Completed Requests This Year</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi__item col col--grow">
|
||||||
|
<div class="kpi__item__value">234</div>
|
||||||
|
<div class="kpi__item__description">Denied Requests</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col col--grow">
|
<div class="col col--grow">
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="progress-menu progress-menu--four">
|
<div class="progress-menu progress-menu--four">
|
||||||
<ul>
|
<ul>
|
||||||
{% for s in screens %}
|
{% for s in screens %}
|
||||||
{% if loop.index < current %}
|
{% if jedi_request and s.section in jedi_request.body %}
|
||||||
{% set step_indicator = 'complete' %}
|
{% set step_indicator = 'complete' %}
|
||||||
{% elif loop.index == current %}
|
{% elif loop.index == current %}
|
||||||
{% set step_indicator = 'active' %}
|
{% set step_indicator = 'active' %}
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
) }}
|
) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<p>We’d like to know a little about how you plan to use JEDI Cloud services to process your request. Please answer the following questions to the best of your ability. Note that the CCPO does not directly help with migrating systems to JEDI Cloud. These questions are for learning about your cloud readiness and financial usage of the JEDI Cloud; your estimates will not be used for any department level reporting.</p>
|
<p>We’d like to know a little about how you plan to use JEDI Cloud services to process your request. Please answer the following questions to the best of your ability. Note that the CCPO does not directly help with migrating systems to JEDI Cloud. These questions are for learning about your cloud readiness and financial usage of the JEDI Cloud; your estimates will not be used for any department level reporting.</p>
|
||||||
<p><em>All fields are required, unless specified optional.</em></p>
|
<p><em>All fields are required, unless specified optional.</em></p>
|
||||||
|
|
||||||
@ -26,7 +25,7 @@
|
|||||||
{{ TextInput(f.jedi_usage,placeholder="Briefly describe how you are expecting to use the JEDI Cloud. \n e.g. We are migrating XYZ application to the cloud so that...",tooltip="Your answer will help us provide tangible examples to DoD leadership how and why commercial cloud resources are accelerating the Department\\'s missions.") }}
|
{{ TextInput(f.jedi_usage,placeholder="Briefly describe how you are expecting to use the JEDI Cloud. \n e.g. We are migrating XYZ application to the cloud so that...",tooltip="Your answer will help us provide tangible examples to DoD leadership how and why commercial cloud resources are accelerating the Department\\'s missions.") }}
|
||||||
|
|
||||||
<h2>Cloud Readiness</h2>
|
<h2>Cloud Readiness</h2>
|
||||||
{{ TextInput(f.num_software_systems,placeholder="Number of systems",tooltip="A software system can be any code that you plan to host on cloud infrastructure. For example, it could be a custom-developed web application, or a large ERP system.") }}
|
{{ TextInput(f.num_software_systems,validation="integer",tooltip="A software system can be any code that you plan to host on cloud infrastructure. For example, it could be a custom-developed web application, or a large ERP system.") }}
|
||||||
{{ OptionsInput(f.jedi_migration, tooltip="Cloud migration is the process of moving data, applications or other business elements from an organization\\'s onsite computers/data centers to the cloud, or moving them from one cloud environment to another.") }}
|
{{ OptionsInput(f.jedi_migration, tooltip="Cloud migration is the process of moving data, applications or other business elements from an organization\\'s onsite computers/data centers to the cloud, or moving them from one cloud environment to another.") }}
|
||||||
{{ OptionsInput(f.rationalization_software_systems, tooltip="Rationalization is the DoD process to determine whether the application should move to the cloud.") }}
|
{{ OptionsInput(f.rationalization_software_systems, tooltip="Rationalization is the DoD process to determine whether the application should move to the cloud.") }}
|
||||||
{{ OptionsInput(f.technical_support_team) }}
|
{{ OptionsInput(f.technical_support_team) }}
|
||||||
@ -37,13 +36,12 @@
|
|||||||
{{ OptionsInput(f.cloud_native, tooltip="Cloud native is architecting and designing your application to use all the benefits of the commercial cloud. Specifically, designing applications so that they are decoupled from a physical resource.") }}
|
{{ OptionsInput(f.cloud_native, tooltip="Cloud native is architecting and designing your application to use all the benefits of the commercial cloud. Specifically, designing applications so that they are decoupled from a physical resource.") }}
|
||||||
|
|
||||||
<h2>Financial Usage</h2>
|
<h2>Financial Usage</h2>
|
||||||
{{ TextInput(f.estimated_monthly_spend, tooltip="Refer to financial verification step help docs") }}
|
{{ TextInput(f.estimated_monthly_spend, tooltip="Refer to financial verification step help docs", validation="dollars") }}
|
||||||
<span>So this means you are spending approximately <b class="label">$1,000,023</b> annually.</span>
|
<span>So this means you are spending approximately <b class="label">$1,000,023</b> annually.</span>
|
||||||
{{ TextInput(f.dollar_value) }}
|
{{ TextInput(f.dollar_value,validation="dollars") }}
|
||||||
{{ TextInput(f.number_user_sessions) }}
|
{{ TextInput(f.number_user_sessions,validation="integer") }}
|
||||||
{{ TextInput(f.average_daily_traffic, tooltip="Requests are the client-to-server network traffic that is being transferred to your systems") }}
|
{{ TextInput(f.average_daily_traffic, tooltip="Requests are the client-to-server network traffic that is being transferred to your systems",validation="integer") }}
|
||||||
{{ TextInput(f.average_daily_traffic_gb, tooltip="GB uploaded is the gigabyte amount of data traffic that is being transferred to your systems") }}
|
{{ TextInput(f.average_daily_traffic_gb, tooltip="GB uploaded is the gigabyte amount of data traffic that is being transferred to your systems",validation="gigabytes") }}
|
||||||
{{ TextInput(f.start_date) }}
|
{{ TextInput(f.start_date, validation="date", placeholder="MM / DD / YYYY") }}
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -19,16 +19,15 @@
|
|||||||
|
|
||||||
<p>Please tell us more about you.</p>
|
<p>Please tell us more about you.</p>
|
||||||
|
|
||||||
{{ TextInput(f.fname_request,placeholder='First Name') }}
|
{{ TextInput(f.fname_request, placeholder='First Name') }}
|
||||||
{{ TextInput(f.lname_request,placeholder='Last Name') }}
|
{{ TextInput(f.lname_request, placeholder='Last Name') }}
|
||||||
{{ TextInput(f.email_request,placeholder='jane@mail.mil') }}
|
{{ TextInput(f.email_request, placeholder='jane@mail.mil', validation='email') }}
|
||||||
{{ TextInput(f.phone_number,placeholder='(123) 456-7890') }}
|
{{ TextInput(f.phone_number, placeholder='e.g. (123) 456-7890', validation='usPhone') }}
|
||||||
|
|
||||||
<p>We want to collect the following information from you for security auditing and determining priviledged user access.</p>
|
<p>We want to collect the following information from you for security auditing and determining priviledged user access.</p>
|
||||||
|
|
||||||
{{ TextInput(f.service_branch,placeholder='e.g. US Air Force, US Army, US Navy, Marine Corps, Defense Media Agency') }}
|
{{ TextInput(f.service_branch,placeholder='e.g. US Air Force, US Army, US Navy, Marine Corps, Defense Media Agency') }}
|
||||||
{{ OptionsInput(f.citizenship) }}
|
{{ OptionsInput(f.citizenship) }}
|
||||||
{{ OptionsInput(f.designation) }}
|
{{ OptionsInput(f.designation) }}
|
||||||
{{ TextInput(f.date_latest_training,tooltip="When was the last time you completed the IA training? <br> Information Assurance (IA) training is an important step in cyber awareness.") }}
|
{{ TextInput(f.date_latest_training,tooltip="When was the last time you completed the IA training? <br> Information Assurance (IA) training is an important step in cyber awareness.",placeholder="MM / DD / YYYY", validation="date") }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
{{ TextInput(f.fname_poc,placeholder='First Name') }}
|
{{ TextInput(f.fname_poc,placeholder='First Name') }}
|
||||||
{{ TextInput(f.lname_poc,placeholder='Last Name') }}
|
{{ TextInput(f.lname_poc,placeholder='Last Name') }}
|
||||||
{{ TextInput(f.email_poc,placeholder='jane@mail.mil') }}
|
{{ TextInput(f.email_poc,placeholder='jane@mail.mil', validation='email') }}
|
||||||
{{ TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC') }}
|
{{ TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC', validation='dodId') }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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
|
||||||
|
21
tests/domain/test_date.py
Normal file
21
tests/domain/test_date.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import pytest
|
||||||
|
import pendulum
|
||||||
|
|
||||||
|
from atst.domain.date import parse_date
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_with_slashes():
|
||||||
|
date_str = "1/2/2020"
|
||||||
|
assert parse_date(date_str) == pendulum.date(2020, 1, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_with_dashes():
|
||||||
|
date_str = "2020-1-2"
|
||||||
|
assert parse_date(date_str) == pendulum.date(2020, 1, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_date():
|
||||||
|
date_str = "This is not a valid data"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
parse_date(date_str)
|
||||||
|
|
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-----
|
||||||
|
MIIFVTCCAz2gAwIBAgIJAJRtzRX0VhJJMA0GCSqGSIb3DQEBCwUAMCUxIzAhBgNV
|
||||||
|
BAMTGkdBUkZVTktFTC5BUlQuRy41ODkyNDYwMzU4MB4XDTE4MDgwODE0MjQ1NFoX
|
||||||
|
DTI4MDgwNTE0MjQ1NFowJTEjMCEGA1UEAxMaR0FSRlVOS0VMLkFSVC5HLjU4OTI0
|
||||||
|
NjAzNTgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCak8upRyMLNUKK
|
||||||
|
vP6Ly50KGXDAktTBOFHDDsTRIIBeEiRImcuQ3nrqgHPKxlYdPG1k88VSDnrCDZry
|
||||||
|
DTm58NGCrtB6tJPqlZag8vpNffk9pEPOBKvUN9v5xqGgSN3sIdv0aMtMRJUXS61R
|
||||||
|
gzKJ76D+QM/7sKFhtPmETcfkBN99On7Zxw33TcwIlpv8t8tPB6F/r8jw07oWFBza
|
||||||
|
Z1Ui2+mFs6rZlxFOP8qRo82iencrMuW3/Tvqjl0N/AHPkdT7PbqAyg1aDkHYIBvc
|
||||||
|
euk/23Rgp1BQCX/Dia412/mMW0l6wYrw3pMBQ0j9LPSKTWx6rf7xa5TTweqcoKhB
|
||||||
|
zaeOV90wQk7gd+13u12ZqtPDI2Lgzi9PiIIDyDOGe4yX+O4YGTOV1pX2RyYCx9Hi
|
||||||
|
D6Pz9LoABz7TYq7A+LjKx5T5Q4XXiyUiQHTHQ5dC8v1rcUdZBB47eyAE0ZtVcCVI
|
||||||
|
tcG6eJgbM907AAabwca5sy0ogfYABMSUz6YWA1SMeDclwtRBlSWMFa2OCDJl7wBU
|
||||||
|
5Iyj/5a4MJ834IJh++gxpeijTktU1RyCDRUgXlAQNdqFxPmgwPbTo4KPDOw/YUnt
|
||||||
|
PSZfO2jiqhXgSRxlG5+2CAMiUVo2kelJxemDkJ30Yk3ebjx6qyEYizE0Mmh3xFYf
|
||||||
|
cOr7h1dxhjvAUtu3/ekNZWdz4WUcMQIDAQABo4GHMIGEMB0GA1UdDgQWBBRGIuCr
|
||||||
|
zBlH956853iOtEt/RF1wkzBVBgNVHSMETjBMgBRGIuCrzBlH956853iOtEt/RF1w
|
||||||
|
k6EppCcwJTEjMCEGA1UEAxMaR0FSRlVOS0VMLkFSVC5HLjU4OTI0NjAzNTiCCQCU
|
||||||
|
bc0V9FYSSTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAHVurBIQJS
|
||||||
|
makSWkuIFYuhKI5GDU9R1xeFe56zDVKE6Xoqki6CxUlHcIY/QN2nJ02GVN12GMAi
|
||||||
|
p4jewaiomr0LIlmzk63jn380okRjOFNoieIyQiXL0rH2oV4DESbWLuLoFnWFHGI7
|
||||||
|
8VsyURDe00H58t3MsEEOzrbjSV7KeyifjIND6yrDuzoLY2FquTOq3Q41XRJxIOuk
|
||||||
|
0p0Cd9E07YzAb9kzODO5ZPvXfkAIqZIrAYb9bjcMs6gb8CbzA/STdSEPp2NjgAsc
|
||||||
|
fjI0VtUPyTX2fKE9nrHeSNsT7WFPslbzvXVtlmUvlyDgnHglKjsgSLTgFaAERUSz
|
||||||
|
WkJG0+lysAPga/qpD22C3OB/igT/S+KJjw8oubX6iAAxIDM1Oa+YStft5IXX2KSm
|
||||||
|
5FT2HIMtXBG9pkgmJ9O+xrDrJwSz+sezXYuV88T4fDYdXAUqgBudmml/h+OGEB4C
|
||||||
|
k3Mc0ibe5Np4SyDg9qWDa+u6GojQCkTA0ygxcXR0M/t204MXqV7g4zCt624BB+nH
|
||||||
|
TYLeq49SQsl2XmPLsjwWIToly1F6tizP0gWYFamGD2bqZNDIEl/5a/CLwpOlSWc8
|
||||||
|
K6tfqAlNnM56/vMXDeo/na7XLRHPkLisUZCxBYVuSFu77gZsawVxcZlO3Hwn1L7a
|
||||||
|
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,49 @@ 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 "initial-value='{}'".format(user.first_name) in body
|
||||||
|
assert "initial-value='{}'".format(user.last_name) in body
|
||||||
|
assert "initial-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 "initial-value='{}'".format(user.first_name) in body
|
||||||
|
assert "initial-value='{}'".format(user.last_name) in body
|
||||||
|
assert "initial-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
|
||||||
|
|
||||||
|
def test_can_review_data(user_session, client):
|
||||||
|
creator = UserFactory.create()
|
||||||
|
user_session(creator)
|
||||||
|
request = RequestFactory.create(creator=creator)
|
||||||
|
response = client.get("/requests/new/4/{}".format(request.id))
|
||||||
|
body = response.data.decode()
|
||||||
|
# assert a sampling of the request data is on the review page
|
||||||
|
assert request.body["primary_poc"]["fname_poc"] in body
|
||||||
|
assert request.body["information_about_you"]["email_request"] 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):
|
||||||
@ -20,19 +21,18 @@ def test_submit_reviewed_request(monkeypatch, client, user_session):
|
|||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
)
|
)
|
||||||
assert "/requests" in response.headers["Location"]
|
assert "/requests" in response.headers["Location"]
|
||||||
assert "modal" not in response.headers["Location"]
|
assert "modal=pendingCCPOApproval" in response.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
def test_submit_autoapproved_reviewed_request(monkeypatch, client, user_session):
|
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=pendingFinancialVerification" 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
|
||||||
|
@ -1,46 +1,67 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from tests.mocks import MOCK_USER
|
from urllib.parse import urlencode
|
||||||
|
from .factories import UserFactory, RequestFactory
|
||||||
|
|
||||||
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
|
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
|
||||||
|
from atst.models.request_status_event import RequestStatus
|
||||||
|
from atst.domain.requests import Requests
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def screens(app):
|
def screens(app):
|
||||||
return JEDIRequestFlow(3).screens
|
return JEDIRequestFlow(3).screens
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip()
|
def test_stepthrough_request_form(user_session, screens, client):
|
||||||
def test_stepthrough_request_form(monkeypatch, screens, client):
|
user = UserFactory.create()
|
||||||
monkeypatch.setattr(
|
user_session(user)
|
||||||
"atst.handlers.request_new.RequestNew.get_current_user", lambda s: MOCK_USER
|
mock_request = RequestFactory.stub()
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
"atst.handlers.request_new.RequestNew.check_xsrf_cookie", lambda s: True
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
"atst.handlers.request_new.JEDIRequestFlow.validate", lambda s: True
|
|
||||||
)
|
|
||||||
|
|
||||||
def take_a_step(inc, req=None):
|
def post_form(url, redirects=False, data=""):
|
||||||
|
return client.post(
|
||||||
|
url,
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
data=data,
|
||||||
|
follow_redirects=redirects,
|
||||||
|
)
|
||||||
|
|
||||||
|
def take_a_step(inc, req=None, data=None):
|
||||||
req_url = "/requests/new/{}".format(inc)
|
req_url = "/requests/new/{}".format(inc)
|
||||||
if req:
|
if req:
|
||||||
req_url += "/" + req
|
req_url += "/" + req
|
||||||
response = client.post(
|
# we do it twice, with and without redirect, in order to get the
|
||||||
req_url,
|
# destination url
|
||||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
prelim_resp = post_form(req_url, data=data)
|
||||||
data="meaning=42",
|
response = post_form(req_url, True, data=data)
|
||||||
)
|
return (prelim_resp.headers.get("Location"), response)
|
||||||
return response
|
|
||||||
|
|
||||||
# GET the initial form
|
# GET the initial form
|
||||||
response = client.get("/requests/new")
|
response = client.get("/requests/new/1")
|
||||||
assert screens[0]["title"] in response.data.decode()
|
assert screens[0]["title"] in response.data.decode()
|
||||||
|
|
||||||
# POST to each of the form pages up until review and submit
|
# POST to each of the form pages up until review and submit
|
||||||
req_id = None
|
req_id = None
|
||||||
for i in range(1, len(screens)):
|
for i in range(1, len(screens)):
|
||||||
resp = take_a_step(i, req=req_id)
|
# get appropriate form data to POST for this section
|
||||||
__import__('ipdb').set_trace()
|
section = screens[i - 1]["section"]
|
||||||
req_id = resp.effective_url.split("/")[-1]
|
post_data = urlencode(mock_request.body[section])
|
||||||
|
|
||||||
|
effective_url, resp = take_a_step(i, req=req_id, data=post_data)
|
||||||
|
req_id = effective_url.split("/")[-1]
|
||||||
screen_title = screens[i]["title"].replace("&", "&")
|
screen_title = screens[i]["title"].replace("&", "&")
|
||||||
|
|
||||||
assert "/requests/new/{}/{}".format(i + 1, req_id) in resp.effective_url
|
assert "/requests/new/{}/{}".format(i + 1, req_id) in effective_url
|
||||||
assert screen_title in resp.data.decode()
|
assert screen_title in resp.data.decode()
|
||||||
|
|
||||||
|
# at this point, the real request we made and the mock_request bodies
|
||||||
|
# should be equivalent
|
||||||
|
assert Requests.get(req_id).body == mock_request.body
|
||||||
|
|
||||||
|
# finish the review and submit step
|
||||||
|
client.post(
|
||||||
|
"/requests/submit/{}".format(req_id),
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
)
|
||||||
|
|
||||||
|
finished_request = Requests.get(req_id)
|
||||||
|
assert finished_request.status == RequestStatus.PENDING_CCPO_APPROVAL
|
||||||
|
Loading…
x
Reference in New Issue
Block a user