Merge pull request #129 from dod-ccpo/authnid

Authnid
This commit is contained in:
dandds 2018-08-06 15:31:17 -04:00 committed by GitHub
commit 8344672348
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2903 additions and 272 deletions

4
.gitignore vendored
View File

@ -30,3 +30,7 @@ static/assets/*
log/* log/*
config/dev.ini config/dev.ini
# CRLs
/crl
/crl-tmp

View File

@ -18,6 +18,8 @@ flask-sqlalchemy = "*"
flask-assets = "*" flask-assets = "*"
flask-session = "*" flask-session = "*"
flask-wtf = "*" flask-wtf = "*"
pyopenssl = "*"
requests = "*"
[dev-packages] [dev-packages]
bandit = "*" bandit = "*"

130
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "0738d50fa0153e356ddd9ce23bcc781914ed0fe860044457a9db9fc0e1cff46b" "sha256": "647d98b5384d1942bbe6bfe7930b1cd249886da2f47645802cd6f93369f44538"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -24,6 +24,64 @@
"index": "pypi", "index": "pypi",
"version": "==1.0.0" "version": "==1.0.0"
}, },
"asn1crypto": {
"hashes": [
"sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
"sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"
],
"version": "==0.24.0"
},
"certifi": {
"hashes": [
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
],
"version": "==2018.4.16"
},
"cffi": {
"hashes": [
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
],
"version": "==1.11.5"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": { "click": {
"hashes": [ "hashes": [
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
@ -31,6 +89,30 @@
], ],
"version": "==6.7" "version": "==6.7"
}, },
"cryptography": {
"hashes": [
"sha256:21af753934f2f6d1a10fe8f4c0a64315af209ef6adeaee63ca349797d747d687",
"sha256:27bb401a20a838d6d0ea380f08c6ead3ccd8c9d8a0232dc9adcc0e4994576a66",
"sha256:29720c4253263cff9aea64585adbbe85013ba647f6e98367efff9db2d7193ded",
"sha256:2a35b7570d8f247889784010aac8b384fd2e4a47b33e15c4a60b45a7c1944120",
"sha256:42c531a6a354407f42ee07fda5c2c0dc822cf6d52744949c182f2b295fbd4183",
"sha256:5eb86f03f9c4f0ac2336ac5431271072ddf7ecc76b338e26366732cfac58aa19",
"sha256:67f7f57eae8dede577f3f7775957f5bec93edd6bdb6ce597bb5b28e1bdf3d4fb",
"sha256:6ec84edcbc966ae460560a51a90046503ff0b5b66157a9efc61515c68059f6c8",
"sha256:7ba834564daef87557e7fcd35c3c3183a4147b0b3a57314e53317360b9b201b3",
"sha256:7d7f084cbe1fdb82be5a0545062b59b1ad3637bc5a48612ac2eb428ff31b31ea",
"sha256:82409f5150e529d699e5c33fa8fd85e965104db03bc564f5f4b6a9199e591f7c",
"sha256:87d092a7c2a44e5f7414ab02fb4145723ebba411425e1a99773531dd4c0e9b8d",
"sha256:8c56ef989342e42b9fcaba7c74b446f0cc9bed546dd00034fa7ad66fc00307ef",
"sha256:9449f5d4d7c516a6118fa9210c4a00f34384cb1d2028672100ee0c6cce49d7f6",
"sha256:bc2301170986ad82d9349a91eb8884e0e191209c45f5541b16aa7c0cfb135978",
"sha256:c132bab45d4bd0fff1d3fe294d92b0a6eb8404e93337b3127bdec9f21de117e6",
"sha256:c3d945b7b577f07a477700f618f46cbc287af3a9222cd73035c6ef527ef2c363",
"sha256:cee18beb4c807b5c0b178f4fa2fae03cef9d51821a358c6890f8b23465b7e5d2",
"sha256:d01dfc5c2b3495184f683574e03c70022674ca9a7be88589c5aba130d835ea90"
],
"version": "==2.3"
},
"flask": { "flask": {
"hashes": [ "hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
@ -70,6 +152,13 @@
"index": "pypi", "index": "pypi",
"version": "==0.14.2" "version": "==0.14.2"
}, },
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.7"
},
"itsdangerous": { "itsdangerous": {
"hashes": [ "hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
@ -150,6 +239,20 @@
"index": "pypi", "index": "pypi",
"version": "==2.7.5" "version": "==2.7.5"
}, },
"pycparser": {
"hashes": [
"sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226"
],
"version": "==2.18"
},
"pyopenssl": {
"hashes": [
"sha256:26ff56a6b5ecaf3a2a59f132681e2a80afcc76b4f902f612f518f92c2a1bf854",
"sha256:6488f1423b00f73b7ad5167885312bb0ce410d3312eb212393795b53c8caa580"
],
"index": "pypi",
"version": "==18.0.0"
},
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
"sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
@ -168,7 +271,7 @@
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622", "sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5" "sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
], ],
"markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7'", "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'",
"version": "==2018.5" "version": "==2018.5"
}, },
"redis": { "redis": {
@ -179,6 +282,14 @@
"index": "pypi", "index": "pypi",
"version": "==2.10.6" "version": "==2.10.6"
}, },
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
],
"index": "pypi",
"version": "==2.19.1"
},
"six": { "six": {
"hashes": [ "hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
@ -214,6 +325,14 @@
"index": "pypi", "index": "pypi",
"version": "==1.1" "version": "==1.1"
}, },
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
],
"markers": "python_version >= '2.6' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version < '4'",
"version": "==1.23"
},
"webassets": { "webassets": {
"hashes": [ "hashes": [
"sha256:e7d9c8887343123fd5b32309b33167428cb1318cdda97ece12d0907fd69d38db" "sha256:e7d9c8887343123fd5b32309b33167428cb1318cdda97ece12d0907fd69d38db"
@ -350,6 +469,7 @@
"sha256:0e9a1227a3a0f3297a485715e72ee6eb77081b17b629367042b586e38c03c867", "sha256:0e9a1227a3a0f3297a485715e72ee6eb77081b17b629367042b586e38c03c867",
"sha256:b4840807a94a3bad0217d6ed3f9b65a1cc6e1db1c99e1184673056ae2c0a4c4d" "sha256:b4840807a94a3bad0217d6ed3f9b65a1cc6e1db1c99e1184673056ae2c0a4c4d"
], ],
"markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*'",
"version": "==0.8.17" "version": "==0.8.17"
}, },
"flask": { "flask": {
@ -402,7 +522,7 @@
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
], ],
"markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*'", "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
"version": "==4.3.4" "version": "==4.3.4"
}, },
"itsdangerous": { "itsdangerous": {
@ -520,7 +640,7 @@
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
], ],
"markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*'", "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
"version": "==0.7.1" "version": "==0.7.1"
}, },
"prompt-toolkit": { "prompt-toolkit": {
@ -543,7 +663,7 @@
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
], ],
"markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*'", "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
"version": "==1.5.4" "version": "==1.5.4"
}, },
"pygments": { "pygments": {

View File

@ -0,0 +1,38 @@
"""update_user_from_authnid
Revision ID: 1f57f784ed5b
Revises: 55ba973d08b9
Create Date: 2018-07-30 16:53:05.945005
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1f57f784ed5b'
down_revision = '55ba973d08b9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('dod_id', sa.String(), nullable=True))
op.add_column('users', sa.Column('email', sa.String(), nullable=True))
op.add_column('users', sa.Column('first_name', sa.String(), nullable=True))
op.add_column('users', sa.Column('last_name', sa.String(), nullable=True))
op.create_unique_constraint('users_dod_id_unique', 'users', ['dod_id'])
op.create_unique_constraint('users_email_unqiue', 'users', ['email'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('users_dod_id_unique', 'users', type_='unique')
op.drop_constraint('users_email_unqiue', 'users', type_='unique')
op.drop_column('users', 'last_name')
op.drop_column('users', 'first_name')
op.drop_column('users', 'email')
op.drop_column('users', 'dod_id')
# ### end Alembic commands ###

View File

@ -1,5 +1,6 @@
import os import os
import re import re
import pathlib
from configparser import ConfigParser from configparser import ConfigParser
from flask import Flask, request, g from flask import Flask, request, g
from flask_session import Session from flask_session import Session
@ -12,6 +13,9 @@ from atst.assets import environment as assets_environment
from atst.routes import bp from atst.routes import bp
from atst.routes.workspaces import bp as workspace_routes 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.domain.authnid.crl.validator import Validator
from atst.domain.auth import apply_authentication
ENV = os.getenv("FLASK_ENV", "dev") ENV = os.getenv("FLASK_ENV", "dev")
@ -32,6 +36,7 @@ def make_app(config):
app.config.update({"SESSION_REDIS": redis}) app.config.update({"SESSION_REDIS": redis})
make_flask_callbacks(app) make_flask_callbacks(app)
make_crl_validator(app)
db.init_app(app) db.init_app(app)
Session(app) Session(app)
@ -40,6 +45,10 @@ def make_app(config):
app.register_blueprint(bp) app.register_blueprint(bp)
app.register_blueprint(workspace_routes) app.register_blueprint(workspace_routes)
app.register_blueprint(requests_bp) app.register_blueprint(requests_bp)
if ENV != "production":
app.register_blueprint(dev_routes)
apply_authentication(app)
return app return app
@ -77,6 +86,7 @@ def map_config(config):
"SQLALCHEMY_DATABASE_URI": config["default"]["DATABASE_URI"], "SQLALCHEMY_DATABASE_URI": config["default"]["DATABASE_URI"],
"SQLALCHEMY_TRACK_MODIFICATIONS": False, "SQLALCHEMY_TRACK_MODIFICATIONS": False,
**config["default"], **config["default"],
"PERMANENT_SESSION_LIFETIME": int(config["default"]["PERMANENT_SESSION_LIFETIME"]),
} }
@ -120,3 +130,14 @@ def make_config():
def make_redis(config): def make_redis(config):
return redis.Redis.from_url(config['REDIS_URI']) return redis.Redis.from_url(config['REDIS_URI'])
def make_crl_validator(app):
crl_locations = []
for filename in pathlib.Path(app.config["CRL_DIRECTORY"]).glob("*"):
crl_locations.append(filename.absolute())
app.crl_validator = Validator(
roots=[app.config["CA_CHAIN"]], crl_locations=crl_locations
)
for e in app.crl_validator.errors:
app.logger.error(e)

32
atst/domain/auth.py Normal file
View File

@ -0,0 +1,32 @@
from flask import g, redirect, url_for, session, request
from atst.domain.users import Users
UNPROTECTED_ROUTES = ["atst.root", "dev.login_dev", "atst.login_redirect", "atst.unauthorized", "static"]
def apply_authentication(app):
@app.before_request
# pylint: disable=unused-variable
def enforce_login():
if not _unprotected_route(request):
user = get_current_user()
if user:
g.current_user = user
else:
return redirect(url_for("atst.root"))
def get_current_user():
user_id = session.get("user_id")
if user_id:
return Users.get(user_id)
else:
return False
def _unprotected_route(request):
if request.endpoint in UNPROTECTED_ROUTES:
return True

View File

View File

@ -0,0 +1,72 @@
import requests
import re
import os
from html.parser import HTMLParser
_DISA_CRLS = "https://iasecontent.disa.mil/pki-pke/data/crls/dod_crldps.htm"
def fetch_disa():
response = requests.get(_DISA_CRLS)
return response.text
class DISAParser(HTMLParser):
crl_list = []
_CRL_MATCH = re.compile("DOD(ROOT|EMAIL|ID)?CA")
def handle_starttag(self, tag, attrs):
if tag == "a":
href = [pair[1] for pair in attrs if pair[0] == "href"].pop()
if re.search(self._CRL_MATCH, href):
self.crl_list.append(href)
def crl_list_from_disa_html(html):
parser = DISAParser()
parser.reset()
parser.feed(html)
return parser.crl_list
def write_crl(out_dir, crl_location):
name = re.split("/", crl_location)[-1]
crl = os.path.join(out_dir, name)
with requests.get(crl_location, stream=True) as r:
with open(crl, "wb") as crl_file:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
crl_file.write(chunk)
def refresh_crls(out_dir, logger=None):
disa_html = fetch_disa()
crl_list = crl_list_from_disa_html(disa_html)
for crl_location in crl_list:
if logger:
logger.info("updating CRL from {}".format(crl_location))
try:
write_crl(out_dir, crl_location)
except requests.exceptions.ChunkedEncodingError:
if logger:
logger.error(
"Error downloading {}, continuing anyway".format(crl_location)
)
if __name__ == "__main__":
import sys
import datetime
import logging
logging.basicConfig(
level=logging.INFO, format="[%(asctime)s]:%(levelname)s: %(message)s"
)
logger = logging.getLogger()
logger.info("Updating CRLs")
try:
refresh_crls(sys.argv[1], logger=logger)
except Exception as err:
logger.exception("Fatal error encountered, stopping")
sys.exit(1)
logger.info("Finished updating CRLs")

View File

@ -0,0 +1,124 @@
import sys
import os
import re
import hashlib
from OpenSSL import crypto, SSL
def sha256_checksum(filename, block_size=65536):
sha256 = hashlib.sha256()
with open(filename, "rb") as f:
for block in iter(lambda: f.read(block_size), b""):
sha256.update(block)
return sha256.hexdigest()
class Validator:
_PEM_RE = re.compile(
b"-----BEGIN CERTIFICATE-----\r?.+?\r?-----END CERTIFICATE-----\r?\n?",
re.DOTALL,
)
def __init__(self, crl_locations=[], roots=[], base_store=crypto.X509Store):
self.errors = []
self.crl_locations = crl_locations
self.roots = roots
self.base_store = base_store
self._reset()
def _reset(self):
self.cache = {}
self.store = self.base_store()
self._add_crls(self.crl_locations)
self._add_roots(self.roots)
self.store.set_flags(crypto.X509StoreFlags.CRL_CHECK)
def _add_crls(self, locations):
for filename in locations:
try:
self._add_crl(filename)
except crypto.Error as err:
self.errors.append(
"CRL could not be parsed. Filename: {}, Error: {}, args: {}".format(
filename, type(err), err.args
)
)
# This caches the CRL issuer with the CRL filepath and a checksum, in addition to adding the CRL to the store.
def _add_crl(self, filename):
with open(filename, "rb") as crl_file:
crl = crypto.load_crl(crypto.FILETYPE_ASN1, crl_file.read())
self.cache[crl.get_issuer().der()] = (filename, sha256_checksum(filename))
self._add_carefully("add_crl", crl)
def _parse_roots(self, root_str):
return [match.group(0) for match in self._PEM_RE.finditer(root_str)]
def _add_roots(self, roots):
for filename in roots:
with open(filename, "rb") as f:
for raw_ca in self._parse_roots(f.read()):
ca = crypto.load_certificate(crypto.FILETYPE_PEM, raw_ca)
self._add_carefully("add_cert", ca)
# in testing, it seems that openssl is maintaining a local cache of certs
# in a hash table and throws errors if you try to add redundant certs or
# CRLs. For now, we catch and ignore that error with great specificity.
def _add_carefully(self, method_name, obj):
try:
getattr(self.store, method_name)(obj)
except crypto.Error as error:
if self._is_preloaded_error(error):
pass
else:
raise error
PRELOADED_CRL = (
[
(
"x509 certificate routines",
"X509_STORE_add_crl",
"cert already in hash table",
)
],
)
PRELOADED_CERT = (
[
(
"x509 certificate routines",
"X509_STORE_add_cert",
"cert already in hash table",
)
],
)
def _is_preloaded_error(self, error):
return error.args == self.PRELOADED_CRL or error.args == self.PRELOADED_CERT
# Checks that the CRL currently in-memory is up-to-date via the checksum.
def refresh_cache(self, cert):
der = cert.get_issuer().der()
if der in self.cache:
filename, checksum = self.cache[der]
if sha256_checksum(filename) != checksum:
self._reset()
def validate(self, cert):
parsed = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.refresh_cache(parsed)
context = crypto.X509StoreContext(self.store, parsed)
try:
context.verify_certificate()
return True
except crypto.X509StoreContextError as err:
self.errors.append(
"Certificate revoked or errored. Error: {}. Args: {}".format(
type(err), err.args
)
)
return False

View File

@ -0,0 +1,13 @@
import re
# TODO: our sample SDN does not have an email address
def parse_sdn(sdn):
try:
parts = sdn.split(",")
cn_string = [piece for piece in parts if re.match("^CN=", piece)][0]
cn = cn_string.split("=")[-1]
info = cn.split(".")
return {"last_name": info[0], "first_name": info[1], "dod_id": info[-1]}
except (IndexError, AttributeError):
raise ValueError("'{}' is not a valid SDN".format(sdn))

View File

@ -1,20 +1,21 @@
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models import Role from atst.models import Role
from .exceptions import NotFoundError from .exceptions import NotFoundError
class Roles(object): class Roles(object):
def __init__(self, db_session):
self.db_session = db_session
def get(self, role_name): @classmethod
def get(cls, role_name):
try: try:
role = self.db_session.query(Role).filter_by(name=role_name).one() role = db.session.query(Role).filter_by(name=role_name).one()
except NoResultFound: except NoResultFound:
raise NotFoundError("role") raise NotFoundError("role")
return role return role
def get_all(self): @classmethod
return self.db_session.query(Role).all() def get_all(cls):
return db.session.query(Role).all()

View File

@ -1,17 +1,17 @@
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models.task_order import TaskOrder from atst.models.task_order import TaskOrder
from .exceptions import NotFoundError from .exceptions import NotFoundError
class TaskOrders(object): class TaskOrders(object):
def __init__(self, db_session):
self.db_session = db_session
@classmethod
def get(self, order_number): def get(self, order_number):
try: try:
task_order = ( task_order = (
self.db_session.query(TaskOrder).filter_by(number=order_number).one() db.session.query(TaskOrder).filter_by(number=order_number).one()
) )
except NoResultFound: except NoResultFound:
raise NotFoundError("task_order") raise NotFoundError("task_order")

View File

@ -1,6 +1,7 @@
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from atst.database import db
from atst.models import User from atst.models import User
from .roles import Roles from .roles import Roles
@ -8,47 +9,57 @@ from .exceptions import NotFoundError, AlreadyExistsError
class Users(object): class Users(object):
def __init__(self, db_session):
self.db_session = db_session
self.roles_repo = Roles(db_session)
def get(self, user_id): @classmethod
def get(cls, user_id):
try: try:
user = self.db_session.query(User).filter_by(id=user_id).one() user = db.session.query(User).filter_by(id=user_id).one()
except NoResultFound: except NoResultFound:
raise NotFoundError("user") raise NotFoundError("user")
return user return user
def create(self, user_id, atat_role_name): @classmethod
atat_role = self.roles_repo.get(atat_role_name) def get_by_dod_id(cls, dod_id):
try:
user = db.session.query(User).filter_by(dod_id=dod_id).one()
except NoResultFound:
raise NotFoundError("user")
return user
@classmethod
def create(cls, atat_role_name="developer", **kwargs):
atat_role = Roles.get(atat_role_name)
try: try:
user = User(id=user_id, atat_role=atat_role) user = User(atat_role=atat_role, **kwargs)
self.db_session.add(user) db.session.add(user)
self.db_session.commit() db.session.commit()
except IntegrityError: except IntegrityError:
raise AlreadyExistsError("user") raise AlreadyExistsError("user")
return user return user
def get_or_create(self, user_id, *args, **kwargs): @classmethod
def get_or_create_by_dod_id(cls, dod_id, **kwargs):
try: try:
user = self.get(user_id) user = Users.get_by_dod_id(dod_id)
except NotFoundError: except NotFoundError:
user = self.create(user_id, *args, **kwargs) user = Users.create(dod_id=dod_id, **kwargs)
self.db_session.add(user) db.session.add(user)
self.db_session.commit() db.session.commit()
return user return user
def update(self, user_id, atat_role_name): @classmethod
def update(cls, user_id, atat_role_name):
user = self.get(user_id) user = Users.get(user_id)
atat_role = self.roles_repo.get(atat_role_name) atat_role = Roles.get(atat_role_name)
user.atat_role = atat_role user.atat_role = atat_role
self.db_session.add(user) db.session.add(user)
self.db_session.commit() db.session.commit()
return user return user

View File

@ -1,22 +1,21 @@
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models.workspace_role import WorkspaceRole from atst.models.workspace_role import WorkspaceRole
from atst.models.workspace_user import WorkspaceUser from atst.models.workspace_user import WorkspaceUser
from atst.models.user import User from atst.models.user import User
from .roles import Roles from .roles import Roles
from .users import Users from .users import Users
from .exceptions import NotFoundError from .exceptions import NotFoundError
class WorkspaceUsers(object): class WorkspaceUsers(object):
def __init__(self, db_session):
self.db_session = db_session
self.roles_repo = Roles(db_session)
self.users_repo = Users(db_session)
def get(self, workspace_id, user_id): @classmethod
def get(cls, workspace_id, user_id):
try: try:
user = self.users_repo.get(user_id) user = Users.get(user_id)
except NoResultFound: except NoResultFound:
raise NotFoundError("user") raise NotFoundError("user")
@ -31,24 +30,25 @@ class WorkspaceUsers(object):
return WorkspaceUser(user, workspace_role) return WorkspaceUser(user, workspace_role)
def add_many(self, workspace_id, workspace_user_dicts): @classmethod
def add_many(cls, workspace_id, workspace_user_dicts):
workspace_users = [] workspace_users = []
for user_dict in workspace_user_dicts: for user_dict in workspace_user_dicts:
try: try:
user = self.users_repo.get(user_dict["id"]) user = Users.get(user_dict["id"])
except NoResultFound: except NoResultFound:
default_role = self.roles_repo.get("developer") default_role = Roles.get("developer")
user = User(id=user_dict["id"], atat_role=default_role) user = User(id=user_dict["id"], atat_role=default_role)
try: try:
role = self.roles_repo.get(user_dict["workspace_role"]) role = Roles.get(user_dict["workspace_role"])
except NoResultFound: except NoResultFound:
raise NotFoundError("role") raise NotFoundError("role")
try: try:
existing_workspace_role = ( existing_workspace_role = (
self.db_session.query(WorkspaceRole) db.session.query(WorkspaceRole)
.filter( .filter(
WorkspaceRole.user == user, WorkspaceRole.user == user,
WorkspaceRole.workspace_id == workspace_id, WorkspaceRole.workspace_id == workspace_id,
@ -66,8 +66,8 @@ class WorkspaceUsers(object):
workspace_user = WorkspaceUser(user, new_workspace_role) workspace_user = WorkspaceUser(user, new_workspace_role)
workspace_users.append(workspace_user) workspace_users.append(workspace_user)
self.db_session.add(user) db.session.add(user)
self.db_session.commit() db.session.commit()
return workspace_users return workspace_users

View File

@ -16,6 +16,11 @@ class User(Base):
atat_role = relationship("Role") atat_role = relationship("Role")
workspace_roles = relationship("WorkspaceRole", backref="user") workspace_roles = relationship("WorkspaceRole", backref="user")
email = Column(String, unique=True)
dod_id = Column(String, unique=True)
first_name = Column(String)
last_name = Column(String)
@property @property
def atat_permissions(self): def atat_permissions(self):
return self.atat_role.permissions return self.atat_role.permissions

View File

@ -1,7 +1,10 @@
from flask import Blueprint, render_template, g from flask import Blueprint, render_template, g, redirect, session, url_for, request
from flask import current_app as app
import pendulum import pendulum
from atst.domain.requests import Requests from atst.domain.requests import Requests
from atst.domain.users import Users
from atst.domain.authnid.utils import parse_sdn
bp = Blueprint("atst", __name__) bp = Blueprint("atst", __name__)
@ -24,3 +27,35 @@ def styleguide():
@bp.route('/<path:path>') @bp.route('/<path:path>')
def catch_all(path): def catch_all(path):
return render_template("{}.html".format(path)) return render_template("{}.html".format(path))
@bp.route('/login-redirect')
def login_redirect():
if request.environ.get('HTTP_X_SSL_CLIENT_VERIFY') == 'SUCCESS' and _is_valid_certificate(request):
sdn = request.environ.get('HTTP_X_SSL_CLIENT_S_DN')
sdn_parts = parse_sdn(sdn)
user = Users.get_or_create_by_dod_id(**sdn_parts)
session["user_id"] = user.id
return redirect(url_for("atst.home"))
else:
return redirect(url_for("atst.unauthorized"))
@bp.route("/unauthorized")
def unauthorized():
template = render_template('unauthorized.html')
response = app.make_response(template)
response.status_code = 401
return response
def _is_valid_certificate(request):
cert = request.environ.get('HTTP_X_SSL_CLIENT_CERT')
if cert:
result = app.crl_validator.validate(cert.encode())
if not result:
app.logger.info(app.crl_validator.errors[-1])
return result
else:
return False

58
atst/routes/dev.py Normal file
View File

@ -0,0 +1,58 @@
from flask import Blueprint, request, session, redirect, url_for
from atst.domain.users import Users
bp = Blueprint("dev", __name__)
_DEV_USERS = {
"sam": {
"dod_id": "1234567890",
"first_name": "Sam",
"last_name": "Seeceepio",
"atat_role": "ccpo",
},
"amanda": {
"dod_id": "2345678901",
"first_name": "Amanda",
"last_name": "Adamson",
"atat_role": "default",
},
"brandon": {
"dod_id": "3456789012",
"first_name": "Brandon",
"last_name": "Buchannan",
"atat_role": "default",
},
"christina": {
"dod_id": "4567890123",
"first_name": "Christina",
"last_name": "Collins",
"atat_role": "default",
},
"dominick": {
"dod_id": "5678901234",
"first_name": "Dominick",
"last_name": "Domingo",
"atat_role": "default",
},
"erica": {
"dod_id": "6789012345",
"first_name": "Erica",
"last_name": "Eichner",
"atat_role": "default",
},
}
@bp.route("/login-dev")
def login_dev():
role = request.args.get("username", "amanda")
user_data = _DEV_USERS[role]
basic_data = {k:v for k,v in user_data.items() if k not in ["dod_id", "atat_role"]}
user = _set_user_permissions(user_data["dod_id"], user_data["atat_role"], basic_data)
session["user_id"] = user.id
return redirect(url_for("atst.home"))
def _set_user_permissions(dod_id, role, user_data):
return Users.get_or_create_by_dod_id(dod_id, atat_role_name=role, **user_data)

View File

@ -24,11 +24,11 @@ def requests_index():
requests = [] requests = []
if ( if (
"review_and_approve_jedi_workspace_request" "review_and_approve_jedi_workspace_request"
in g.current_user["atat_permissions"] in g.current_user.atat_permissions
): ):
requests = Requests.get_many() requests = Requests.get_many()
else: else:
requests = Requests.get_many(creator_id=g.current_user["id"]) requests = Requests.get_many(creator_id=g.current_user.id)
mapped_requests = [map_request(g.current_user, r) for r in requests] mapped_requests = [map_request(g.current_user, r) for r in requests]

View File

@ -124,5 +124,5 @@ class JEDIRequestFlow(object):
if self.request_id: if self.request_id:
Requests.update(self.request_id, request_data) Requests.update(self.request_id, request_data)
else: else:
request = Requests.create(self.current_user["id"], request_data) request = Requests.create(self.current_user.id, request_data)
self.request_id = request.id self.request_id = request.id

View File

@ -5,7 +5,6 @@ from atst.domain.requests import Requests
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
@requests_bp.route("/requests/new", defaults={"screen": 1})
@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)

View File

@ -6,10 +6,9 @@ AUTHNID_BASE_URL= https://localhost:8001
COOKIE_SECRET = some-secret-please-replace COOKIE_SECRET = some-secret-please-replace
SECRET = change_me_into_something_secret SECRET = change_me_into_something_secret
SECRET_KEY = change_me_into_something_secret SECRET_KEY = change_me_into_something_secret
CAC_URL = https://localhost:8001 CAC_URL = http://localhost:8000/login-redirect
PE_NUMBER_CSV_URL = http://c95e1ebb198426ee57b8-174bb05a294821bedbf46b6384fe9b1f.r31.cf5.rackcdn.com/penumbers.csv PE_NUMBER_CSV_URL = http://c95e1ebb198426ee57b8-174bb05a294821bedbf46b6384fe9b1f.r31.cf5.rackcdn.com/penumbers.csv
REDIS_URI = redis://localhost:6379 REDIS_URI = redis://localhost:6379
SESSION_TTL_SECONDS = 600
PGAPPNAME = atst PGAPPNAME = atst
PGHOST = localhost PGHOST = localhost
PGPORT = 5432 PGPORT = 5432
@ -17,6 +16,8 @@ PGUSER = postgres
PGPASSWORD = postgres PGPASSWORD = postgres
PGDATABASE = atat PGDATABASE = atat
SESSION_TYPE = redis SESSION_TYPE = redis
SESSION_COOKIE_DOMAIN= "atat.codes" SESSION_COOKIE_NAME=atat
SESSION_COOKIE_SECURE = True
SESSION_USE_SIGNER = True SESSION_USE_SIGNER = True
PERMANENT_SESSION_LIFETIME = 600
CRL_DIRECTORY = crl
CA_CHAIN = ssl/server-certs/ca-chain.pem

View File

@ -2,3 +2,4 @@
PGHOST = postgreshost PGHOST = postgreshost
PGDATABASE = atat_test PGDATABASE = atat_test
REDIS_URI = redis://redishost:6379 REDIS_URI = redis://redishost:6379
CRL_DIRECTORY = tests/fixtures/crl

2
config/prod.ini Normal file
View File

@ -0,0 +1,2 @@
[default]
SESSION_COOKIE_SECURE=True

View File

@ -1,2 +1,3 @@
[default] [default]
PGDATABASE = atat_test PGDATABASE = atat_test
CRL_DIRECTORY = tests/fixtures/crl

16
script/sync-crls Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
# script/sync-crls: update the DOD CRLs and place them where authnid expects them
set -e
cd "$(dirname "$0")/.."
mkdir -p crl-tmp
pipenv run python ./atst/domain/authnid/crl/util.py crl-tmp
mkdir -p crl
rsync -rq crl-tmp/. crl/.
rm -rf crl-tmp
if [[ $FLASK_ENV != "production" ]]; then
# place our test CRL there
cp ssl/client-certs/client-ca.der.crl crl/
fi

25
script/sync-dod-certs Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
# script/sync-dod-certs: update the CA bundle with DOD intermediate and root CAs
CAS_FILE_NAME="Certificates_PKCS7_v5.3_DoD"
CA_CHAIN="ssl/server-certs/ca-chain.pem"
echo "Resetting CA bundle..."
rm ssl/server-certs/ca-chain.pem &> /dev/null || true
touch $CA_CHAIN
if [[ $FLASK_ENV != "production" ]]; then
# only for testing and development
echo "Copy in testing client CA..."
cat ssl/client-certs/client-ca.crt >> $CA_CHAIN
fi
# dod intermediate certs
echo "Adding DoD root certs"
rm -rf tmp || true
mkdir tmp
curl --silent -o tmp/dod-cas.zip "https://iasecontent.disa.mil/pki-pke/$CAS_FILE_NAME.zip"
unzip tmp/dod-cas.zip -d tmp/ &> /dev/null
openssl pkcs7 -in "tmp/$CAS_FILE_NAME/$CAS_FILE_NAME.pem.p7b" -print_certs >> $CA_CHAIN
rm -rf tmp

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEJDCCAwygAwIBAgIJAK4JGo3BBGhVMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
BAYTAlVTMRUwEwYDVQQIEwxQZW5uc3lsdmFuaWExFTATBgNVBAcTDFBoaWxhZGVs
cGhpYTEMMAoGA1UEChMDRG9EMQwwCgYDVQQLEwNERFMxEDAOBgNVBAMTB0FUQVQg
Q0EwHhcNMTgwNjAxMTk0NjIyWhcNMzgwNTI3MTk0NjIyWjBpMQswCQYDVQQGEwJV
UzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRUwEwYDVQQHEwxQaGlsYWRlbHBoaWEx
DDAKBgNVBAoTA0RvRDEMMAoGA1UECxMDRERTMRAwDgYDVQQDEwdBVEFUIENBMIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzYU7UbstArnnVliaC/TB6Vir
kVWMnAEYMUZA1BKP8DZaNEKbzFH2+mMw7O0BY7Ph9x0hEZ1kXLr6U93xcKyUWNPo
13i5EwUUCSh2MdPfS8ZZt8DUIIKC7XzFnKyKSKQmr0Mt9dC44rryPKTBvmI60rQ8
VZkFEgvs8FCP0M4Ar6/gtJ24ZLEtilu5dQBSlru4nPGXg07r2C2JgEZWshtMBtbH
LkOM2gtp/pkYCCG0zqeU+0s3H8IqDq0uYkONOfVeCumbg1/AtjgrZu7aOVPKyibk
aI6sTTooXE5aSZkfkx0z6+fKM2nPSe30HgiBODtb7G+44ln08d0isjpQ67OvGQID
AQABo4HOMIHLMB0GA1UdDgQWBBSl7CUAWPbx8XqotKKKAufPh0wn4DCBmwYDVR0j
BIGTMIGQgBSl7CUAWPbx8XqotKKKAufPh0wn4KFtpGswaTELMAkGA1UEBhMCVVMx
FTATBgNVBAgTDFBlbm5zeWx2YW5pYTEVMBMGA1UEBxMMUGhpbGFkZWxwaGlhMQww
CgYDVQQKEwNEb0QxDDAKBgNVBAsTA0REUzEQMA4GA1UEAxMHQVRBVCBDQYIJAK4J
Go3BBGhVMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABguwdFk42YP
8U6Du5HQ6Is1jfc1KEOowdh0d2MCH8q0KNktqiu6kWzjH1gRjRwc07bAkAWqXPB6
6gkRGYe/FRgi2Rn+Uo5UC5ahI4cXkE8OitCIEP3Br9fUw+vj/3Iiov0QZ6Hv81Kl
ZTZhLiZbjAg5maL/vufnUp+n15qzm67APh3/2hcgO93UlE9o9vXohWy1lHs8u12o
hPLxghSmGc9eKalEWEs61OrohpOtCHUEd1isq76WhaiXSwSUrBxgy89Z517A7ffC
BjzLo5AVo6a9ou+ONVeZk8qw6YR6X9J7axy8YuTWt+Z82WFvOF0ubkqjm72d001M
7R9zCOQ3O+g=
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAzYU7UbstArnnVliaC/TB6VirkVWMnAEYMUZA1BKP8DZaNEKb
zFH2+mMw7O0BY7Ph9x0hEZ1kXLr6U93xcKyUWNPo13i5EwUUCSh2MdPfS8ZZt8DU
IIKC7XzFnKyKSKQmr0Mt9dC44rryPKTBvmI60rQ8VZkFEgvs8FCP0M4Ar6/gtJ24
ZLEtilu5dQBSlru4nPGXg07r2C2JgEZWshtMBtbHLkOM2gtp/pkYCCG0zqeU+0s3
H8IqDq0uYkONOfVeCumbg1/AtjgrZu7aOVPKyibkaI6sTTooXE5aSZkfkx0z6+fK
M2nPSe30HgiBODtb7G+44ln08d0isjpQ67OvGQIDAQABAoIBAHR4EInc3UEyQVu5
knM8Hbgzu+b86FZweFlUSuDkNBYZdz0ukkRUHvb+x3c9SRBLnL8CDv+AhqPWgo6M
tIr6Aofkb4vMqnWQ5y3ZdEIApAa5PZbY/F4AGFql3wdO8H8CJ7ojBCTOSDiVYTnk
1Lcjy9okshyAP1Ne1sPJo/bdB56HtXs+wqok1NntIQwiXjjD9xUuc1EZk0J4M97L
vBUjUGNX942UjtRiey5zwhRp3bTPasTduHcA01NaIbOVYlRFwc2W+cflz0l6ml2p
14TNEEvIMMMCNKnlPrpGI23n0psAvE4nbuxZQGVYAFvXrWn+Gyvz0Yag2EoMUCEs
ziLED9ECgYEA6IByu+xqIuIAhj/PwIIxV4+lkuV4TXIlfAFLR4JuokOVfbRsmu2e
9EfeOUD9LfQ4KsG5mu4Abpja0k/VKRKRGRjV6Oe2C6VK942HFP6Kpn0hgIuomZkD
eVv8naDezZjAvVace38zjRWB2GXTpapwBAgf/YflPPsDZ8bi/weqZCMCgYEA4kqx
Ka489Rr7+cSXpMeS5lLufhlaE5OVQc5HVFREDAI5vXU8BM2sLiHTC/BHjis2JvLm
aRJ0UsxUoIUURl2KjTbx3zns4HDVkzBrSpoDXWxBjAo0oEg7JVc+6+qEqbDHHS1L
/UJ6mlUegsE42MkFWG3YJQuHxyLZqPXIwNAyhZMCgYEA5cxnGnSt5rJoAEi7xzMn
H7s71Hf3stw6TlldFV3GiZyw+aDFo09vR1RtQTuJwczbYu88yvOn+6gax7neHo1a
WmrgqiWzGcmS0iDRPZ/kXG/bGBlxV/cTpvSTNx0UejMbdUhQvANaaXyzbLYgPWK6
+lEphUW2/tG+aOj73UOvVu8CgYA5L8sJz4CUKJeZDTeNauoSzs56i4mZ/OfxU2Hv
S8ROjJlu6ZubUya6Gc4t7DEJGp56xVO5JfLDoeOZFUiEZ8tF2KbTVN4p8hnnMotK
tRU4nM0LyOB3yQk5bIz4LbIM+CG5m+LiQ9Sb//rP7GijUFnLeSbwZbOQfZwn+MUd
BQBfhQKBgQDmuX8tJdPkjE133IhQhZHbHHt6AEQA3aXkFdvPvbYD9VbGTZ8wnpFO
VJrDDWnIKAgO2FerIX9oq+H9a5fggYtTMeAX1cOA6b9SnLmFjt0utxrQKxf7p5I+
n+EsmcAWfb+KRQwoB0L/mE9Ool14AeJ15kHyNIrCrMPv0J4zoC0Jdg==
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1 @@
F4D74F1607DD3C83

View File

@ -0,0 +1,30 @@
Right now, we have two client certificates:
- atat.mil.crt: beautiful, good, works great
- bad-atat.mil.crt: banned, very bad, is on the CRL
I more or less used [this article](https://access.redhat.com/documentation/en-us/red_hat_update_infrastructure/2.1/html/administration_guide/chap-red_hat_update_infrastructure-administration_guide-certification_revocation_list_crl) to generate the CRL. Note that I departed from it slightly and used a variation on the openssl config recommended by the ca man page (`man ca`).
I added the new crl:
```
openssl crl -inform pem -in ssl/client-certs/client-ca.crl -outform der -out crl/simon.crl
```
Running the scripts verifies that the good one is good and the bad one is bad.
We can also verify with OpenSSL. First concatenate the CA Bundle and the CRL:
```
cat ssl/server-certs/ca-chain.pem ssl/client-certs/client-ca.crl > /tmp/test.pem
```
Verify the certs:
```
openssl verify -verbose -CAfile /tmp/test.pem -crl_check ssl/client-certs/bad-atat.mil.crt
> error 23 at 0 depth lookup:certificate revoked
openssl verify -verbose -CAfile /tmp/test.pem -crl_check ssl/client-certs/atat.mil.crt
> atat.mil.crt: OK
```

View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDqTCCApECCQCoSzDcVuoXYzANBgkqhkiG9w0BAQsFADCBjTELMAkGA1UEBhMC
VVMxFTATBgNVBAgTDFBlbm5zeWx2YW5pYTEVMBMGA1UEBxMMUGhpbGFkZWxwaGlh
MRAwDgYDVQQKEwdGYXV4RG9EMQswCQYDVQQLEwJQVzERMA8GA1UEAxMIRmF1eCBE
b0QxHjAcBgkqhkiG9w0BCQEWD2ZhdXhkb2RAZG9kLmNvbTAeFw0xODA3MjQyMDM0
MDJaFw0xOTA3MjQyMDM0MDJaMIGeMQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVu
bnN5bHZhbmlhMRUwEwYDVQQHEwxQaGlsYWRlbHBoaWExDDAKBgNVBAoTA0RvRDEL
MAkGA1UECxMCUFcxITAfBgNVBAMTGEFSVC5HQVJGVU5LRUwuMTIzNDU2Nzg5MDEj
MCEGCSqGSIb3DQEJARYUYWdhcmZ1bmtlbEBzYW5kZy5jb20wggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQDCRftouylCKDN9GoKRJMWA3gnfEshRxi4P1xU9
xm0qgPIzTpeZCNUcDbSQzovXQ58ElrDTdUeMv0OV/RLOnNFKgPSAd2f1F4BE1rJR
WHLFjG6mPj769Wl1BhGAwOY/zdhfHYjTKZSApUfP6MuKsD2nciOnJFlqJ439R4LC
S8Fv3RnnKMQlSTMiudOMhtzr8v1poDlxVzu9IF7i/MZKCBFz3e1G2LFIr5ZL+djg
4rsI4lNPVNt1sRiXy/+kltBY8RIbPPP70iT+zmrr6PmEeDSwDSKgW+TBCGK3yFCr
kPXjMZhWOt+7eLanL2KJNrohEkJFzI3tb7zVm6zg5SC1GTIFAgMBAAEwDQYJKoZI
hvcNAQELBQADggEBAKm4W2mAqtRUpwCstCqJCdoOsIgW9pZKTczLERbODHvbfXZA
MfGGnYQiuoOddu9K9UJQIHZLMUYmF9gj9HdY60ttNWeH5XRXIXn6t1Pn8W7q042Q
RqeJ/uOtNG1UXRtHQhK1j73xD3ZSTGw7rTIA2qDgRQMp1h28405kZIiNVRFdNjFh
irAvtQkIXWhIGSr/Lwop98RmTsV17v4iK14Uf2i5QUjdIECiGqGSlk9Jmj8dajzN
cSarkhWDuQmlCplF1lTNcXenC66d1bE/KXb3dEGg+h99KfZVw1+9c5DbWag6IVgG
Xts4GcPhuiKF/pJWRO11L2CfyCveoGM9Osz/vvc=
-----END CERTIFICATE-----

View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIC5DCCAcwCAQAwgZ4xCzAJBgNVBAYTAlVTMRUwEwYDVQQIEwxQZW5uc3lsdmFu
aWExFTATBgNVBAcTDFBoaWxhZGVscGhpYTEMMAoGA1UEChMDRG9EMQswCQYDVQQL
EwJQVzEhMB8GA1UEAxMYQVJULkdBUkZVTktFTC4xMjM0NTY3ODkwMSMwIQYJKoZI
hvcNAQkBFhRhZ2FyZnVua2VsQHNhbmRnLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAMJF+2i7KUIoM30agpEkxYDeCd8SyFHGLg/XFT3GbSqA8jNO
l5kI1RwNtJDOi9dDnwSWsNN1R4y/Q5X9Es6c0UqA9IB3Z/UXgETWslFYcsWMbqY+
Pvr1aXUGEYDA5j/N2F8diNMplIClR8/oy4qwPadyI6ckWWonjf1HgsJLwW/dGeco
xCVJMyK504yG3Ovy/WmgOXFXO70gXuL8xkoIEXPd7UbYsUivlkv52ODiuwjiU09U
23WxGJfL/6SW0FjxEhs88/vSJP7Oauvo+YR4NLANIqBb5MEIYrfIUKuQ9eMxmFY6
37t4tqcvYok2uiESQkXMje1vvNWbrODlILUZMgUCAwEAAaAAMA0GCSqGSIb3DQEB
BQUAA4IBAQCvnKvR8agOyJmLFcrROWGWLdGsr6CFmkcQe1eJ2GFP9XsIbuIjxssn
K2yEK1hY6BAAPl76Arh3WkHOXVQjuzW3hlsu+uwKJnYDecG3I9btP+NkPNyKWrbr
S2GIqa71oKadncV/P9DKsc2+KL2BFo8+IbvwVSPGj63JlJh2T9JFPeAqxeKCUiuO
ac+dgxNtMQRSEYwE1kgdaJu5yRBfepZaeNGJ2KjCivQdsgnlVllPCNwtjciIRLWl
UBdt8kh6Dx0RVIkck5fViFiJodxbfw9filjYITgRuANEJHytNzo3ChsWflZ0UYi/
j8jAvoqL2d+D/a2ijaxlQeCqu5MUB4wR
-----END CERTIFICATE REQUEST-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAwkX7aLspQigzfRqCkSTFgN4J3xLIUcYuD9cVPcZtKoDyM06X
mQjVHA20kM6L10OfBJaw03VHjL9Dlf0SzpzRSoD0gHdn9ReARNayUVhyxYxupj4+
+vVpdQYRgMDmP83YXx2I0ymUgKVHz+jLirA9p3IjpyRZaieN/UeCwkvBb90Z5yjE
JUkzIrnTjIbc6/L9aaA5cVc7vSBe4vzGSggRc93tRtixSK+WS/nY4OK7COJTT1Tb
dbEYl8v/pJbQWPESGzzz+9Ik/s5q6+j5hHg0sA0ioFvkwQhit8hQq5D14zGYVjrf
u3i2py9iiTa6IRJCRcyN7W+81Zus4OUgtRkyBQIDAQABAoIBAQC0+nSmsBRTaRfu
J1AS3mqPDkmr4ddzNmeaogdLsRnpSo5WdZSMH8pHhAz+CSwEsR3mLGs10j+BQnw3
sbZfe38NJOyg8JuLmwUHG+qqFPd2SMibXclWCGDhf3G2u/zC24QBt4XLESUiYtZv
PLLA1EXbQ10rS5VwasC/fmq1jdT52yEi4viXdMOSfjYCWg+xIwBsyCQs/lWLsqWD
ZKLYfUAFsYqQ1Axz96yiscgNfPfoPRMoTvU3TuQlGhiQ1ygG5f4xlLEuHXpj1yWw
/liSZVq/a+WQlVAKdlA4IXiC8szPagNSa/beEaj3R+ifoCad5hp/Fsj2JQlHmm30
D8PAAVFRAoGBAPnRytHsicCuqck0oa2c2nE7gExrZhrO+rZoDHPBXjQiTFstil8r
wK1OCjeeX9TV18szPhkimCVel+goNhSmW4n3BcAM1HTFdZlKY7dwVN/tvtl852Sw
gVhGd3kFDkjUBTlK7W+IW7dzW3KwoSbcBpPRtIX9kKR5Braek4h4pUv3AoGBAMcU
ZMqlHB6k1HH+3bZhyTk1BBAF2PcocqOkI9ahSQjDmVGMSa+nVxC7qE0l+hRRpaFd
Ck6zn41p87Yos1nwNwOBcT3AIk0CNYGTJXJQQnkVzjB2yTdKDC/nAH++WOE2daw6
0n1kIygOeL6na6r+jCQbsmwmORlqZ1nLPjCIPlrjAoGASpNiJICkLqz1amcXzKgC
XcMRbb6x4FbhaQpujS+wW4fRm3Zg1EBPaGzfh/LzUKn1nWdSplY5bQ5r8pXubwOq
V+kyAj7SPXmkvXoDgoM6Ew755hrvSJOYSS3gBHSJ6xu/43aGosDmAEGjjv1DXkJY
hFAZv9YOE8s9Qc7c4+SAE8kCgYEAm+JPLhJtW11r8LtF9orJWt81iCpcAuSMJ7De
UzDFlHQ8uIsmI8Hfvf2DQq2rDYAFNr441Pl3xO6i5A8oqRMcsMUJ2/V3pl9FcGm9
F67a7h9x7acF1iJIOrYiQOTWibrwF2WT7pWbpcD3MSq9dw6Mw7VgV6jyawFTXg90
aeI1GUsCgYEA2/7tNN0Of5W/Ff/2lm9ePhYsZSr+9NoBBQvai7+m5qpSzvoE304Q
1qPW+T5pA4Da34nG+fGJMop0QX9rRTdyE9Ct++8ybIdLFAf35fDxgciohkziji8+
0BHK7f+GqTDF+KoIZDZYPQgJX17/h8XNtBBSbP7WX8WZHIco/0BtOrc=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDljCCAn4CCQDe7V0Kcecn2TANBgkqhkiG9w0BAQUFADCBjTELMAkGA1UEBhMC
VVMxFTATBgNVBAgTDFBlbm5zeWx2YW5pYTEVMBMGA1UEBxMMUGhpbGFkZWxwaGlh
MRAwDgYDVQQKEwdGYXV4RG9EMQswCQYDVQQLEwJQVzERMA8GA1UEAxMIRmF1eCBE
b0QxHjAcBgkqhkiG9w0BCQEWD2ZhdXhkb2RAZG9kLmNvbTAeFw0xODA2MjAyMDQz
MTlaFw0xODA3MjAyMDQzMTlaMIGLMQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVu
bnN5bHZhbmlhMRUwEwYDVQQHEwxQaGlsYWRlbHBoaWExDDAKBgNVBAoUA1MmRzEe
MBwGA1UEAxMVU0lNT04uUEFVTC4zODU2MTM1OTAxMSAwHgYJKoZIhvcNAQkBFhFz
aW1vbkBzX2FuZF9nLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AKao36qwC9Sk5hMujFQm5h7B6WoRFqbWpTw7VSAZeW2ykwWbOBmsEAfgOJ+ctyHq
oMG0S23zJxSkfjO1PLXvu9r1ML0zXtm0uUiTJOMEMyrUBbfCV8zJ3TMuA7voWLi7
QKsXh1bHdDIXbYP6dLC3w3CnBnr9VihzLth5KLEpz9ePX5gZljHVGldNY4ZR3UbD
IeL7GD0z/jdcNuHxLYsI9gnnfxrOx8LmzDHDwTNsvKYNRjkdu+pja0ojDrE3T61g
nKrWQsDwP9T7v27AfhrF1sxy+5K3YiQkDGtbvwFtKBIG3DJBw8qAqEPbtXw9FpYt
7p8Ti/QYM5SGr/+w3yOgvrkCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAgVarfkoj
YtZ4X/uNzaSTYO10nyPblebmCGdJW4Cwgk7tcyB+ufLKPrWaC0+Y6XPZwCkAM8UF
KqwYVnMWTkYdUI2ff1vst9tZRiANGXuQLgbdGAP2TrcBk/N5Glm6J4wrpT5VAXjR
gBeVxMIWkGb5geDXISJujzrQU26roxEm3F4oUwvAgvMQd/Ha/pzXioaLycc0k91J
apCafD39u5A+X/Y4QG/GfLG0kqOS2ioJDIlb+EJRzIL7s4cvv530p+VLu+AYEgKx
MmGOnmML3qO/+oeL3Y32TP4Hzm2asNScseoi8a1ygyV88rLjLaVrsj7CFp9zJL0O
Ksoovip0wuSVdQ==
-----END CERTIFICATE-----

View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIC0TCCAbkCAQAwgYsxCzAJBgNVBAYTAlVTMRUwEwYDVQQIEwxQZW5uc3lsdmFu
aWExFTATBgNVBAcTDFBoaWxhZGVscGhpYTEMMAoGA1UEChQDUyZHMR4wHAYDVQQD
ExVTSU1PTi5QQVVMLjM4NTYxMzU5MDExIDAeBgkqhkiG9w0BCQEWEXNpbW9uQHNf
YW5kX2cuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApqjfqrAL
1KTmEy6MVCbmHsHpahEWptalPDtVIBl5bbKTBZs4GawQB+A4n5y3IeqgwbRLbfMn
FKR+M7U8te+72vUwvTNe2bS5SJMk4wQzKtQFt8JXzMndMy4Du+hYuLtAqxeHVsd0
Mhdtg/p0sLfDcKcGev1WKHMu2HkosSnP149fmBmWMdUaV01jhlHdRsMh4vsYPTP+
N1w24fEtiwj2Ced/Gs7HwubMMcPBM2y8pg1GOR276mNrSiMOsTdPrWCcqtZCwPA/
1Pu/bsB+GsXWzHL7krdiJCQMa1u/AW0oEgbcMkHDyoCoQ9u1fD0Wli3unxOL9Bgz
lIav/7DfI6C+uQIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAHQg3idmnhAX9CyO
xbzfrTQ989vs110lTRh8VY+64ufkS2bxGO4fQik+VfSi9wFshTGaUlhtgiBrdfAt
9udaQprWmZabBmiDnoUWiM5srJfYHL5yytrYynwpVe7Y3kPvPT/Zd+B9NBr+G0aq
SxIDce7236vAcjocgCv8gmkdrfkpOTR87gx5q3b1BBv/we4+dUKysloC1Aw23/de
Fi49SH9Xt8ZWUBsW5MesrmTfCXPTauYgYRt8bKtA0qvzzmiE5Ydihpi9HilGuCMr
2LKBQETR6m4FgNXNcsRIlqPR+EY8llTYMEu7LvvHn2RmVpeIT2v5TADV0AighQyB
++ZbkbE=
-----END CERTIFICATE REQUEST-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEApqjfqrAL1KTmEy6MVCbmHsHpahEWptalPDtVIBl5bbKTBZs4
GawQB+A4n5y3IeqgwbRLbfMnFKR+M7U8te+72vUwvTNe2bS5SJMk4wQzKtQFt8JX
zMndMy4Du+hYuLtAqxeHVsd0Mhdtg/p0sLfDcKcGev1WKHMu2HkosSnP149fmBmW
MdUaV01jhlHdRsMh4vsYPTP+N1w24fEtiwj2Ced/Gs7HwubMMcPBM2y8pg1GOR27
6mNrSiMOsTdPrWCcqtZCwPA/1Pu/bsB+GsXWzHL7krdiJCQMa1u/AW0oEgbcMkHD
yoCoQ9u1fD0Wli3unxOL9BgzlIav/7DfI6C+uQIDAQABAoIBAQCJUEiAzO3idT7v
fQG38BjYLLLRZmUAb4fS2Zvoh7SpsmE6VEpjtIW8x3w/3hJxSmzLTG59l8KSWnl0
xxXPXUetPym6KZIz05h5eGsC9Jnn5qsTXXeTzpqHKZmAAA7hnb7JeOhUkp9lCjJ8
dCYi2DWaIrPPL94GE+j8CM+DMM0Db9QmShQC5XbZPgsHiHvvffuvd4G90XcANEM/
KHuwSoZ9xgySZDG+ENlqYu93GGrL3DYUozjMUChzVKZYyYySxII1ja11oznXcAyG
nj5xeBmKv6KzYD5LOMIapWfVTNHLG1FM7bhVccrWKIAVKW4Lqd+gcMC+/wU2YIx9
K9WGV8RlAoGBANWTrGCvaM9r2piXlGB6VB/KmZXFI9R7wjE+waW+3XBZ6YVZiMGQ
jebeT+PPbeaggaDMIxZ70vJ+rNbS2MYrI44AIIueq636PoT7JtjfhakgZ7LBqc37
F56rvObPTuFCElVKS1/nIaNAnvoNUoSqt42t7+VzkNfLYCalkHHpsXxHAoGBAMfD
eUhuUaPTDT02NVjrNAA0yIkRIoyrbv7KGKuJStoPy7W4L6aZC0iFWZmwXTYBuC73
ulZQ88X3bexKS0NkfQJBLPTQFYNUYS/H+OCwkuFj160tysZbG8rx/IsfZwWqoitH
wR1Bgz++k5AApcgjMEWmt8l0NT5Mr6M0waylWGz/AoGBALQa3giCo14XU7XOTZ+2
SO6uSSoVnwt2eeJRS7fb5pzyFY0QXdTtc9y2qKQxrjoILIhO3V/+d3tq+5IFKCyl
AEylKszSt2/1UXeO28mTZQGkhA4oZmt/TQHPTXNOavRmZVNrXXi4TpN+0RGI3odl
93gQr/bMp95ycNjmUZLeQX/NAoGBAI6PT5SDNjwFuCMA9p1YbSnggWRgGBnvliy6
qVRxjDuGnkg3A7qO6eB9We42UK7kFz9dh1tmNjIHXCkO9BtKMXRUcvLbNR8eLqVc
vp4LJSc4i4iJb3aTOohgnWvjozAGD+l3MbfhMvtg1AomjCkCA8cRLYPVLNIjBA0i
7zx4W1ydAoGBAKS26yBJT9ZbIKLtoqZ6wOdz0l4r+ZaHmO+LjiGuFUh7w2s2MsPR
Q1JwE5aXaXP9gY7md/gz7Fcm3ebjwRkdcvGvIQyncv4mF64b+FFnpgjQFHg5+OqD
A57e0VDFI2LYhFstVHNZ1sRA+tBKQygd7Hzlz4BZdSD6EY7fvWNSJ7/j
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIElTCCA32gAwIBAgIJAN5qDki+VlfPMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD
VQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRUwEwYDVQQHEwxQaGlsYWRl
bHBoaWExEDAOBgNVBAoTB0ZhdXhEb0QxCzAJBgNVBAsTAlBXMREwDwYDVQQDEwhG
YXV4IERvRDEeMBwGCSqGSIb3DQEJARYPZmF1eGRvZEBkb2QuY29tMB4XDTE4MDYy
MDIwMzg0N1oXDTE5MDYyMDIwMzg0N1owgY0xCzAJBgNVBAYTAlVTMRUwEwYDVQQI
EwxQZW5uc3lsdmFuaWExFTATBgNVBAcTDFBoaWxhZGVscGhpYTEQMA4GA1UEChMH
RmF1eERvRDELMAkGA1UECxMCUFcxETAPBgNVBAMTCEZhdXggRG9EMR4wHAYJKoZI
hvcNAQkBFg9mYXV4ZG9kQGRvZC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQDyQUFcuQ+YKOEJtv4XjKOTpOLp8IdbsaFwU8YgenMdvAc1ONZRL/2o
jaCZx+kB2QSCVH2jaLUQ/2i4uz4rE21Ngpx+EHa1hgDQANle3d5CWrn2Q10/pdPe
rJHYkMSiZ3cNWfFPBfHDtJrLlRUwJkgy+lUSLnOaipmBZMYXbV8/qUh69nWJQNXi
AvmSUw8jwUPfTrpQVzftkOYz+0HVJyvKijTsj1LaPZTR3D8OhbFnvZWIlhIUjJZO
jap/xQ3YEOcNF+gfx8hDQG2SnltWgecPsgiBRXmZK2IqDv39DE2DNiukEclZLhbN
SpTibNZwkVzcTSRV2mSOHKXqTcH0wTvpAgMBAAGjgfUwgfIwHQYDVR0OBBYEFAo/
6auHcKMK1ItTElg1Kk4MyoB5MIHCBgNVHSMEgbowgbeAFAo/6auHcKMK1ItTElg1
Kk4MyoB5oYGTpIGQMIGNMQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZh
bmlhMRUwEwYDVQQHEwxQaGlsYWRlbHBoaWExEDAOBgNVBAoTB0ZhdXhEb0QxCzAJ
BgNVBAsTAlBXMREwDwYDVQQDEwhGYXV4IERvRDEeMBwGCSqGSIb3DQEJARYPZmF1
eGRvZEBkb2QuY29tggkA3moOSL5WV88wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
AQUFAAOCAQEAp4fVYeSKYJICBQt37NOF6qZ+dv8GBDI+oZy7vC+VcjiRaODkiz9w
IO5dBZxx/ldH5sD24Oc2SH+48S6UjE/D5kDpM/nIddfVfL2f222sE14RsqgrhmbG
qRaEB8NXWiSQyKOKX63v8scioUqb9hFY+gtwb8HDFiOZFx+67L/NaXSh6VA8BbLj
o55EafjTgr+Yad7SrZI5f6Q2iQ+uuHcJsf7fEe3Kts5Uwt5KXBBfMxeaSyQRxNX+
JBBmy6MaxddPtus3MH+eIgI2Wp2rofH/PtGnSoizBj5IZXBkc18x1DG5pAJL4205
EKQoicsafE27XBw45dK3cRBLXPWt8JrCBg==
-----END CERTIFICATE-----

Binary file not shown.

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA8kFBXLkPmCjhCbb+F4yjk6Ti6fCHW7GhcFPGIHpzHbwHNTjW
US/9qI2gmcfpAdkEglR9o2i1EP9ouLs+KxNtTYKcfhB2tYYA0ADZXt3eQlq59kNd
P6XT3qyR2JDEomd3DVnxTwXxw7Say5UVMCZIMvpVEi5zmoqZgWTGF21fP6lIevZ1
iUDV4gL5klMPI8FD3066UFc37ZDmM/tB1Scryoo07I9S2j2U0dw/DoWxZ72ViJYS
FIyWTo2qf8UN2BDnDRfoH8fIQ0Btkp5bVoHnD7IIgUV5mStiKg79/QxNgzYrpBHJ
WS4WzUqU4mzWcJFc3E0kVdpkjhyl6k3B9ME76QIDAQABAoIBAQCC7bnBv1MqTY2y
jnAtkhkmRstM3G6LpCk4aE6AZy2oOGM85IcQQfu6CTFva5gHI59IQRnWI1UY5rFW
hfxHk6xTY+/oQkWmPdJamNriZs8k1ZwD+MyBBcLIakQ446UikQDK+n1s1C2iNA4l
UWGuMEJ9KsanmOtp7tagFDLrnnUIFgyfQv5JI/QBZSMd9UReRv3xQrdv+KK58zJE
/zsuFFO00YS0xzDYwikuwabDXaWCt8/9rDDlthIEaJRzTxZiLK90k6DaywRnO7rJ
Q4Q/1WUGzdA7wfkQOWLozP1To2d6Q/KK1TiRaY0uieGvTvT7kXVDne4+lb3zAmAW
IxdyNYBxAoGBAPtgEjGapRzPLcgJqVuup5W+/gc8NWcSK4NJWAwFWN9n6wOf+jQu
YkwVUoF0KN0g9a1rymvnv+fHdvqQ+uDtdqCMcU3DNVx1uwTf0V/kPsxSwhTjrQ3h
4tMXL4EzOUhYV1us/PtrmSlKS1SuQXbBdgNM7n71X0zWgsHeDvIf4YQTAoGBAPa2
OqNOUFiA8Yz7wG/Aw1LiPX+DJZVmH05yXDSWicwSyrhorxktweMNd1e+syYW+5Qe
GFu3qaxmOlPL9M5IvbUiAV7nmiVcezBnLxBLmOdc9rk8CU8qakZDESsy/pC741/U
y6MQZzsbKIhxG4djbl+9Mr8wom+DGQtkFJ7RvqeTAoGBAOrJRLUIGAfcioo4W/LC
Isz+4w2m8soecn3hV1eC9wtTaHKuTWfHmxAtKi63bCN90Xn1H8/BWcEG0N4f4/OK
WC6Efp9/IKwHWnKnCkxiRzVYZuZT8SLyRIWdNkWarnof6Rg7bt72FMw4FDw3tfVR
pQRYKrpyPFzsTpz850DG/j/5AoGAZ5BxpxH96lkejRc1XfQmSknMlRWBlmiLJcwd
5rl22OLelHDlaAVsSZriiUP1Qj0NmMzVXtMHd+Zl/70zY9DnSf0fZC6G574dvGDk
QcvqQN0mePW51rCwchQ/RcofULR+q0DRxv7gxtAMwNHyQ3A66herENUiqvr2bXCy
s0TK6t8CgYEA7IS8e3x9SvXwfjGyJslbxhI4P7cBuVU5aL1SqYpaNx61JdmnPct4
ruQntKHL5DvPNNRwFUvySkH93zjvjOWqF1g8kSO2ZPDj+WajStAHwA1TmVYIfkpV
xfv5mlcKUfyLmoJ6nKuCf/pt49Gmp3vRsmxZrEcbBqGAVBI7LslQQr4=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,13 @@
-----BEGIN X509 CRL-----
MIIB8jCB2zANBgkqhkiG9w0BAQQFADCBjTELMAkGA1UEBhMCVVMxFTATBgNVBAgT
DFBlbm5zeWx2YW5pYTEVMBMGA1UEBxMMUGhpbGFkZWxwaGlhMRAwDgYDVQQKEwdG
YXV4RG9EMQswCQYDVQQLEwJQVzERMA8GA1UEAxMIRmF1eCBEb0QxHjAcBgkqhkiG
9w0BCQEWD2ZhdXhkb2RAZG9kLmNvbRcNMTgwNzMwMjEyOTAxWhcNMTgwODI5MjEy
OTAxWjAcMBoCCQDe7V0Kcecn2RcNMTgwNjIwMjA0NjExWjANBgkqhkiG9w0BAQQF
AAOCAQEAfZSS51Axnx04iSfMd1k5/TvH1R6NvUM20S/rZjJYt/uLqRElnJd7R7aI
lLQQzSdbsuHm8HcfcMS7ZUMv989chKXMbPml+ZXkK/zp7LdjaL5THs09ek0NOM2l
yhJcdE3K5bntk6qSgbsUpBOWLHzrp20is3BPl6gY+JRb0nnZ/SXTr4zfDctGfcot
fSAGs3QA0Q/dpJOlSkGzxlzjB7dXDuoHTaJwy2s48IriNvvtVktM2AS+B/843vMC
ToI5ZUh3RkSCgGvKexobg85Ke1QwWTYuj392JhakpIu/Qc71BK0jtbY9mVuFLwqW
RFXDKIzRiL4S7iZWu/bpqTYyqmCmeA==
-----END X509 CRL-----

View File

@ -0,0 +1,26 @@
[ ca ]
default_ca = CA_default # The default ca section
[ CA_default ]
dir = ./ # top dir
database = $dir/index.txt # index file.
# new_certs_dir = $dir/newcerts # new certs dir
default_days = 365 # how long to certify for
default_crl_days= 30 # how long before next CRL
default_md = md5 # md to use
policy = policy_any # default policy
email_in_dn = no # Don't add the email into cert DN
name_opt = ca_default # Subject name display option
cert_opt = ca_default # Certificate display option
copy_extensions = none # Don't copy extensions from request
[ policy_any ]
countryName = supplied
stateOrProvinceName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

View File

@ -0,0 +1 @@
R 180720204319Z 180620204611Z DEED5D0A71E727D9 unknown /C=US/ST=Pennsylvania/L=Philadelphia/O=S&G/CN=SIMON.PAUL.3856135901/emailAddress=simon@s_and_g.com

View File

@ -0,0 +1 @@
unique_subject = yes

View File

18
ssl/make-certs.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
# Generate the root (GIVE IT A PASSWORD IF YOU'RE NOT AUTOMATING SIGNING!):
echo 'MAKING CA'
openssl genrsa -out certificate-authority/ca.key 2048
openssl req -new -x509 -days 7300 -key certificate-authority/ca.key -sha256 -extensions v3_ca -out certificate-authority/ca.crt
# Generate the domain key:
openssl genrsa -out server-certs/dev.cac.atat.codes.key 2048
echo 'MAKING CSR'
# Generate the certificate signing request
openssl req -nodes -sha256 -new -key server-certs/dev.cac.atat.codes.key -out server-certs/dev.cac.atat.codes.csr -reqexts SAN -config <(cat req.cnf <(printf "[SAN]\nsubjectAltName=DNS.1:dev.cac.atat.codes,DNS.2:cac.atat.codes,DNS.3:backend"))
# Sign the request with your root key
openssl x509 -sha256 -req -in server-certs/dev.cac.atat.codes.csr -CA certificate-authority/ca.crt -CAkey certificate-authority/ca.key -CAcreateserial -out server-certs/dev.cac.atat.codes.crt -days 7300 -extfile <(cat req.cnf <(printf "[SAN]\nsubjectAltName=DNS.1:dev.cac.atat.codes,DNS.2:cac.atat.codes,DNS.3:backend")) -extensions SAN
# Check your homework:
openssl verify -CAfile certificate-authority/ca.crt server-certs/dev.cac.atat.codes.crt

26
ssl/req.cnf Normal file
View File

@ -0,0 +1,26 @@
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = US
ST = VA
L = SomeCity
O = MyCompany
OU = MyDivision
CN = dev.cac.atat.codes
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = dev.cac.atat.codes
DNS.2 = cac.atat.codes
DNS.3 = backend

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDmTCCAoGgAwIBAgIJAPTXTxYH3TyDMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
BAYTAlVTMRUwEwYDVQQIEwxQZW5uc3lsdmFuaWExFTATBgNVBAcTDFBoaWxhZGVs
cGhpYTEMMAoGA1UEChMDRG9EMQwwCgYDVQQLEwNERFMxEDAOBgNVBAMTB0FUQVQg
Q0EwHhcNMTgwNjAxMTk0NjIyWhcNMzgwNTI3MTk0NjIyWjBzMQswCQYDVQQGEwJV
UzELMAkGA1UECAwCVkExETAPBgNVBAcMCFNvbWVDaXR5MRIwEAYDVQQKDAlNeUNv
bXBhbnkxEzARBgNVBAsMCk15RGl2aXNpb24xGzAZBgNVBAMMEmRldi5jYWMuYXRh
dC5jb2RlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOGjINZSzsSH
MP0e+PxJhuM7v0juaB51UyzEp1tnrICKygQ+JaVLUmvZTiwsILfAFJ0Atxr50nga
CG5R+QN+JAOBJ6v1tKW/NQ7kWCM8tyC5f1aU0rf5Yvl6dhZXJKszOTROG+3Qg/cH
DM57/YrbOBnm30PaEAFD/s13hKEZklVgX3wYyaB2oRmXBbvWog9uFl3PLQBmnCPr
q7aFlr2S3THO46PqHITtijwEDQViXgE+K5wm6ogi/cXzuD4aYq0PVk+6WaorXBOZ
pOWnRyyX2OaJMyFzjuTBKQ80NMivcuh+fRQIhviSepeNuXeyZkWzQtixZPh22/Rv
3lmWW4mY3D0CAwEAAaM6MDgwNgYDVR0RBC8wLYISZGV2LmNhYy5hdGF0LmNvZGVz
gg5jYWMuYXRhdC5jb2Rlc4IHYmFja2VuZDANBgkqhkiG9w0BAQsFAAOCAQEALeJM
LAPCxoqi/RirJcY5beiHZgLGLgolDHJEE8ZzKtuNqJvGWPwrTRGmr+mm31Qnl8IP
M/skIC5CtYTdJRHD3AYNyFOFWmTuDS929mWxg50eZr8xdpS5sQ5AqiBclToXgOTI
qRje/ojofTVl8RdT1q1gH0f+Ul60fywckngtSzJu2EkMTjy1xRCzmm137PakGuwc
IZE+4trl2adE7GVWhYsF+SaroiLMIxFCcJqeqtbPK3OfuGMLUUr20O42fWfZskqa
xenWST0R4M5ixMx1L3mou3vqQxHjihRpCaFDgpVJ0EbHbw2j3gqSiVF7q6N0mxFk
RZ088LtbYUr/LL3TCg==
-----END CERTIFICATE-----

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIDATCCAekCAQAwczELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMREwDwYDVQQH
DAhTb21lQ2l0eTESMBAGA1UECgwJTXlDb21wYW55MRMwEQYDVQQLDApNeURpdmlz
aW9uMRswGQYDVQQDDBJkZXYuY2FjLmF0YXQuY29kZXMwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDhoyDWUs7EhzD9Hvj8SYbjO79I7mgedVMsxKdbZ6yA
isoEPiWlS1Jr2U4sLCC3wBSdALca+dJ4GghuUfkDfiQDgSer9bSlvzUO5FgjPLcg
uX9WlNK3+WL5enYWVySrMzk0Thvt0IP3BwzOe/2K2zgZ5t9D2hABQ/7Nd4ShGZJV
YF98GMmgdqEZlwW71qIPbhZdzy0AZpwj66u2hZa9kt0xzuOj6hyE7Yo8BA0FYl4B
PiucJuqIIv3F87g+GmKtD1ZPulmqK1wTmaTlp0csl9jmiTMhc47kwSkPNDTIr3Lo
fn0UCIb4knqXjbl3smZFs0LYsWT4dtv0b95ZlluJmNw9AgMBAAGgSTBHBgkqhkiG
9w0BCQ4xOjA4MDYGA1UdEQQvMC2CEmRldi5jYWMuYXRhdC5jb2Rlc4IOY2FjLmF0
YXQuY29kZXOCB2JhY2tlbmQwDQYJKoZIhvcNAQELBQADggEBAJgWiXenFnBMAL+H
tM3RvgsVXVd5ccAL0tiiRplm88JrtEPylDmN4HG1pp7Y11ziMoZvP5TZBJEVrArw
ONT6VacOs+5UBw9lQDU7KYNbUEZlcCfPBA/cfxdWUgV0pDV/tOVUeB16HOZjIrNA
3s6r2GhI7fnUEWhbmEKe7DvUyX0seMmpMl/E48b7FQ4i+1frhSjH5SC1GwKJLM3P
Sq5JALYUFUdn9yNCMc4tGtRwrJkPoUAzUQRlczJ4KsHl0ma5uAQ+B80H3spWgb/j
/25+mQl8vzLE3m/mVcCGikAapJTyA56EQhxp2Zrmy29bXsWhaR7xzRxWtrIGUXlE
g8vKEEc=
-----END CERTIFICATE REQUEST-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA4aMg1lLOxIcw/R74/EmG4zu/SO5oHnVTLMSnW2esgIrKBD4l
pUtSa9lOLCwgt8AUnQC3GvnSeBoIblH5A34kA4Enq/W0pb81DuRYIzy3ILl/VpTS
t/li+Xp2FlckqzM5NE4b7dCD9wcMznv9its4GebfQ9oQAUP+zXeEoRmSVWBffBjJ
oHahGZcFu9aiD24WXc8tAGacI+urtoWWvZLdMc7jo+ochO2KPAQNBWJeAT4rnCbq
iCL9xfO4PhpirQ9WT7pZqitcE5mk5adHLJfY5okzIXOO5MEpDzQ0yK9y6H59FAiG
+JJ6l425d7JmRbNC2LFk+Hbb9G/eWZZbiZjcPQIDAQABAoIBAQDDpyxGLC/XAlNc
aYsFWMx6JcjMeM4X+yxQWYW1IMTYAYEDBNCn8BRcKGY8r1b/frNhIMmlvpLeSdSd
tL70ZGDeGRRJbBlkz9Q2QZKbm35ABhmA/jNqC/ni0mmrHY1SVmx4CnL1WCXWAmr8
cU99JHIVI7jdoSzXrBo6GDUNbJsTI4kLmJO31zTnO3A10r6eTG6+t0kqdprcG3oE
pTpJnycy+z5cXf/8gv+yMicDpSzts0YjeSFvG0TPNVrvINsoXK837M0DycotUsik
0I1TyWMR66b8ceP34A86eWPjVQw/jU0kdDD0AN4Key/EymS21vGVml1vVNP3ZvkH
A6pE9scRAoGBAPmwb6VfRtJRPIieexqatilO6hH5B+A05FvopGdFEs7aDdnsBKXS
FalEp0ArR0DRu65c4nF0NNBUkciq5VrEdoNyz5OKIV6ypl+FOtoDpaDlgQEGctE/
fjjw4EAt0uvCJYMjrSpeiS6aIHb9QPU5kTEjfJrIvkorRxGa4XQzvcBPAoGBAOdX
EZEEZP9Qy0GE60j12VQ9jkVrmCfWyDd1qR09iN0fXFcTr1mj5E+nQimI+3OX1ymg
EgPOqTxxGj7W9erEEpT2xlERuBVP9CnBQPjl4llTJtMNpYsX76OLCRe9QYvCkVsE
1rvCu9Kdbrok/GZIyZxPShMsxg0SnFsEKvpu2AuzAoGBAPCuv2AUcEspnYU/5wBl
I7S76et7NrlLothpb5hQP+n+zR1EYdKJqPGqSOIVFbEIurY/uNOOJZ6v9nsNKNqO
yIK6+BaLLtF+udsXrPwcSdrHf8vCMIk9f+lZX4Dd6xPw6IH5sOFHkUrHrQWl56i6
XheU0nbNjIgoIXB58Fs3yPAHAoGAGYdCKP6TJpmD1HcWf7ahhOpGCOMWp07MSVJy
lwdzUvNi/Tju4LV1PFT4uBylotveonlHg6QKiODyRHz0JjP82PNibw/FgJSSHQl2
YgD8OV8zqZaX7gF2MFXnavc3hHS0FZczGwUiNNuqnF/4elEN7nHResw2Drs/Bcwv
8fLJZIECgYEA17uHOALkj6m9oFeepMddgVzfTtEzhjwJIB3/dhv9K5Y6UkxOrwvE
d813gvlXziZ6StMMbbwW+TPU87Z8B92y2rMP/e3ui4Z3/ObMfeSCWX6892M3jfAu
RB4FLpR4us5RWZ7rpSyZzmA4/4NaBMxz8b6fD8vImjbbwYfJbq7oKFI=
-----END RSA PRIVATE KEY-----

39
ssl/ssl.conf Normal file
View File

@ -0,0 +1,39 @@
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /etc/ssl/dev.cac.atat.codes.crt;
ssl_certificate_key /etc/ssl/dev.cac.atat.codes.key;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_prefer_server_ciphers on;
ssl_verify_client optional;
ssl_verify_depth 10;
ssl_client_certificate /etc/ssl/ca-chain.pem;
error_log /var/log/nginx/authnid.error.log debug;
add_header Strict-Transport-Security max-age=15768000;
#ssl_stapling on;
#ssl_stapling_verify on;
location / {
try_files $uri @app;
}
location @app {
include uwsgi_params;
uwsgi_pass unix:///tmp/uwsgi.sock;
uwsgi_param HTTP_X_SSL_CLIENT_VERIFY $ssl_client_verify;
uwsgi_param HTTP_X_SSL_CLIENT_CERT $ssl_client_raw_cert;
uwsgi_param HTTP_X_SSL_CLIENT_S_DN $ssl_client_s_dn;
}
location /static {
alias /app/static;
}
}

38
templates/error_base.html Normal file
View File

@ -0,0 +1,38 @@
{# TODO: set this context elsewhere #}
{# set context='workspace' #}
{% set context=g.navigationContext %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}JEDI{% endblock %}</title>
{% assets "css" %}
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
{% endassets %}
<link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
</head>
<body class="{% if g.modalOpen %} modalOpen{% endif %}">
{% block template_vars %}{% endblock %}
<div class='global-layout'>
<div class='global-panel-container'>
{% block sidenav %}{% endblock %}
{% block content %}
these are not the droids you are looking for
{% endblock %}
</div>
</div>
{% include 'footer.html' %}
{% block modal %}{% endblock %}
{% assets "js_all" %}
<script src="{{ ASSET_URL }}"></script>
{% endassets %}
</body>
</html>

View File

@ -18,7 +18,7 @@
icon="document", icon="document",
active=g.matchesPath('/requests'), active=g.matchesPath('/requests'),
subnav=[ subnav=[
{"label":"New Request", "href":"/requests/new", "icon": "plus", "active": g.matchesPath('/requests/new')}, {"label":"New Request", "href":"/requests/new/1", "icon": "plus", "active": g.matchesPath('/requests/new')},
] ]
) }} ) }}
{{ SidenavItem("Workspaces", href="/workspaces", icon="cloud", active=g.matchesPath('/workspaces')) }} {{ SidenavItem("Workspaces", href="/workspaces", icon="cloud", active=g.matchesPath('/workspaces')) }}

View File

@ -17,7 +17,7 @@
<h1 class="usa-display">JEDI</h1> <h1 class="usa-display">JEDI</h1>
<a class="usa-button" href='{{ config.get('cac_url','https://cac.atat.codes') }}'><span>Sign In with CAC</span></a> <a class="usa-button" href='{{ config.get('CAC_URL','https://cac.atat.codes') }}'><span>Sign In with CAC</span></a>
<button class="usa-button" disabled>Sign In via MFA</button> <button class="usa-button" disabled>Sign In via MFA</button>
{% if g.dev %} {% if g.dev %}
<a class="usa-button usa-button-secondary" href='/login-dev'><span>DEV Login</span></a> <a class="usa-button usa-button-secondary" href='/login-dev'><span>DEV Login</span></a>

View File

@ -0,0 +1,12 @@
{% extends "error_base.html" %}
{% block content %}
<main class="usa-section usa-content">
<h1>Unauthorized</h1>
</main>
{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "base_workspace.html.to" %} {% extends "base_workspace.html" %}
{% from "components/empty_state.html" import EmptyState %} {% from "components/empty_state.html" import EmptyState %}
@ -61,17 +61,17 @@
{% for m in members %} {% for m in members %}
<tr> <tr>
<td><a href="/workspaces/123456/members/789/edit" class="icon-link icon-link--large">{{ m['first_name'] }} {{ m['last_name'] }}</a></td> <td><a href="/workspaces/123456/members/789/edit" class="icon-link icon-link--large">{{ m['first_name'] }} {{ m['last_name'] }}</a></td>
<td class='table-cell--shrink'>{% if m['num_projects'] == '0' %} <span class="label label--info">No Project Access</span> {% end %}</td> <td class='table-cell--shrink'>{% if m['num_projects'] == '0' %} <span class="label label--info">No Project Access</span> {% endif %}</td>
<td>{{ m['status'] }}</a></td> <td>{{ m['status'] }}</a></td>
<td>{{ m['workspace_role'] }}</a></td> <td>{{ m['workspace_role'] }}</a></td>
</tr> </tr>
{% end %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% end %} {% endif %}
{% end %} {% endblock %}

View File

@ -2,14 +2,10 @@ import os
import pytest import pytest
import alembic.config import alembic.config
import alembic.command import alembic.command
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from atst.app import make_app, make_config from atst.app import make_app, make_config
from tests.mocks import MockApiClient
from atst.sessions import DictSessions
from atst.models import Base
from atst.database import db as _db from atst.database import db as _db
from .mocks import MOCK_USER
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
@ -21,10 +17,9 @@ def app(request):
ctx = _app.app_context() ctx = _app.app_context()
ctx.push() ctx.push()
def teardown(): yield _app
ctx.pop()
return _app ctx.pop()
def apply_migrations(): def apply_migrations():
@ -39,9 +34,6 @@ def apply_migrations():
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def db(app, request): def db(app, request):
def teardown():
_db.drop_all()
_db.app = app _db.app = app
apply_migrations() apply_migrations()
@ -88,3 +80,11 @@ def dummy_form():
@pytest.fixture @pytest.fixture
def dummy_field(): def dummy_field():
return DummyField() return DummyField()
@pytest.fixture
def user_session(monkeypatch):
def set_user_session(user = MOCK_USER):
monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: user)
return set_user_session

View File

@ -0,0 +1,86 @@
# Import installed packages
import pytest
import re
import os
import shutil
from OpenSSL import crypto, SSL
from atst.domain.authnid.crl.validator import Validator
import atst.domain.authnid.crl.util as util
class MockX509Store():
def __init__(self):
self.crls = []
self.certs = []
def add_crl(self, crl):
self.crls.append(crl)
def add_cert(self, cert):
self.certs.append(cert)
def set_flags(self, flag):
pass
def test_can_build_crl_list(monkeypatch):
location = 'ssl/client-certs/client-ca.der.crl'
validator = Validator(crl_locations=[location], base_store=MockX509Store)
assert len(validator.store.crls) == 1
def test_can_build_trusted_root_list():
location = 'ssl/server-certs/ca-chain.pem'
validator = Validator(roots=[location], base_store=MockX509Store)
with open(location) as f:
content = f.read()
assert len(validator.store.certs) == content.count('BEGIN CERT')
def test_can_validate_certificate():
validator = Validator(
roots=['ssl/server-certs/ca-chain.pem'],
crl_locations=['ssl/client-certs/client-ca.der.crl']
)
good_cert = open('ssl/client-certs/atat.mil.crt', 'rb').read()
bad_cert = open('ssl/client-certs/bad-atat.mil.crt', 'rb').read()
assert validator.validate(good_cert)
assert validator.validate(bad_cert) == False
def test_can_dynamically_update_crls(tmpdir):
crl_file = tmpdir.join('test.crl')
shutil.copyfile('ssl/client-certs/client-ca.der.crl', crl_file)
validator = Validator(
roots=['ssl/server-certs/ca-chain.pem'],
crl_locations=[crl_file]
)
cert = open('ssl/client-certs/atat.mil.crt', 'rb').read()
assert validator.validate(cert)
# override the original CRL with one that revokes atat.mil.crt
shutil.copyfile('tests/fixtures/test.der.crl', crl_file)
assert validator.validate(cert) == False
def test_parse_disa_pki_list():
with open('tests/fixtures/disa-pki.html') as disa:
disa_html = disa.read()
crl_list = util.crl_list_from_disa_html(disa_html)
href_matches = re.findall('DOD(ROOT|EMAIL|ID)?CA', disa_html)
assert len(crl_list) > 0
assert len(crl_list) == len(href_matches)
class MockStreamingResponse():
def __init__(self, content_chunks):
self.content_chunks = content_chunks
def iter_content(self, chunk_size=0):
return self.content_chunks
def __enter__(self):
return self
def __exit__(self, *args):
pass
def test_write_crl(tmpdir, monkeypatch):
monkeypatch.setattr('requests.get', lambda u, **kwargs: MockStreamingResponse([b'it worked']))
crl = 'crl_1'
util.write_crl(tmpdir, crl)
assert [p.basename for p in tmpdir.listdir()] == [crl]
assert [p.read() for p in tmpdir.listdir()] == ['it worked']

View File

@ -0,0 +1,16 @@
import pytest
import atst.domain.authnid.utils as utils
from tests.mocks import DOD_SDN
def test_parse_sdn():
parsed = utils.parse_sdn(DOD_SDN)
assert parsed.get('first_name') == 'ART'
assert parsed.get('last_name') == 'GARFUNKEL'
assert parsed.get('dod_id') == '5892460358'
def test_parse_bad_sdn():
with pytest.raises(ValueError):
utils.parse_sdn('this has nothing to do with anything')
with pytest.raises(ValueError):
utils.parse_sdn(None)

View File

@ -7,10 +7,6 @@ from atst.domain.requests import Requests
from tests.factories import RequestFactory from tests.factories import RequestFactory
@pytest.fixture()
def requests(session):
return Requests()
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def new_request(session): def new_request(session):
created_request = RequestFactory.create() created_request = RequestFactory.create()
@ -20,33 +16,33 @@ def new_request(session):
return created_request return created_request
def test_can_get_request(requests, new_request): def test_can_get_request(new_request):
request = requests.get(new_request.id) request = Requests.get(new_request.id)
assert request.id == new_request.id assert request.id == new_request.id
def test_nonexistent_request_raises(requests): def test_nonexistent_request_raises():
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
requests.get(uuid4()) Requests.get(uuid4())
def test_auto_approve_less_than_1m(requests, new_request): def test_auto_approve_less_than_1m(new_request):
new_request.body = {"details_of_use": {"dollar_value": 999999}} new_request.body = {"details_of_use": {"dollar_value": 999999}}
request = requests.submit(new_request) request = Requests.submit(new_request)
assert request.status == 'approved' assert request.status == 'approved'
def test_dont_auto_approve_if_dollar_value_is_1m_or_above(requests, new_request): def test_dont_auto_approve_if_dollar_value_is_1m_or_above(new_request):
new_request.body = {"details_of_use": {"dollar_value": 1000000}} new_request.body = {"details_of_use": {"dollar_value": 1000000}}
request = requests.submit(new_request) request = Requests.submit(new_request)
assert request.status == 'submitted' assert request.status == 'submitted'
def test_dont_auto_approve_if_no_dollar_value_specified(requests, new_request): def test_dont_auto_approve_if_no_dollar_value_specified(new_request):
new_request.body = {"details_of_use": {}} new_request.body = {"details_of_use": {}}
request = requests.submit(new_request) request = Requests.submit(new_request)
assert request.status == 'submitted' assert request.status == 'submitted'

View File

@ -3,21 +3,16 @@ from atst.domain.roles import Roles
from atst.domain.exceptions import NotFoundError from atst.domain.exceptions import NotFoundError
@pytest.fixture() def test_get_all_roles():
def roles_repo(session): roles = Roles.get_all()
return Roles(session)
def test_get_all_roles(roles_repo):
roles = roles_repo.get_all()
assert roles assert roles
def test_get_existing_role(roles_repo): def test_get_existing_role():
role = roles_repo.get("developer") role = Roles.get("developer")
assert role.name == "developer" assert role.name == "developer"
def test_get_nonexistent_role(roles_repo): def test_get_nonexistent_role():
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
roles_repo.get("nonexistent") Roles.get("nonexistent")

View File

@ -6,10 +6,6 @@ from atst.domain.task_orders import TaskOrders
from tests.factories import TaskOrderFactory from tests.factories import TaskOrderFactory
@pytest.fixture()
def task_orders(session):
return TaskOrders(session)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def new_task_order(session): def new_task_order(session):
def make_task_order(**kwargs): def make_task_order(**kwargs):
@ -22,13 +18,13 @@ def new_task_order(session):
return make_task_order return make_task_order
def test_can_get_task_order(task_orders, new_task_order): def test_can_get_task_order(new_task_order):
new_to = new_task_order(number="0101969F") new_to = new_task_order(number="0101969F")
to = task_orders.get(new_to.number) to = TaskOrders.get(new_to.number)
assert to.id == to.id assert to.id == to.id
def test_nonexistent_task_order_raises(task_orders): def test_nonexistent_task_order_raises():
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
task_orders.get("some fake number") TaskOrders.get("some fake number")

View File

@ -2,72 +2,64 @@ import pytest
from uuid import uuid4 from uuid import uuid4
from atst.domain.users import Users from atst.domain.users import Users
from atst.domain.exceptions import NotFoundError, AlreadyExistsError from atst.domain.exceptions import NotFoundError
DOD_ID = "my_dod_id"
@pytest.fixture() def test_create_user():
def users_repo(session): user = Users.create("developer")
return Users(session) assert user.atat_role.name == "developer"
@pytest.fixture(scope="function") def test_create_user_with_nonexistent_role():
def user_id():
return uuid4()
def test_create_user(users_repo, user_id):
user = users_repo.create(user_id, "developer")
assert user.id == user_id
def test_create_user_with_nonexistent_role(users_repo, user_id):
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
users_repo.create(user_id, "nonexistent") Users.create("nonexistent")
def test_create_already_existing_user(users_repo, user_id): def test_get_or_create_nonexistent_user():
users_repo.create(user_id, "developer") user = Users.get_or_create_by_dod_id(DOD_ID, atat_role_name="developer")
with pytest.raises(AlreadyExistsError): assert user.dod_id == DOD_ID
users_repo.create(user_id, "developer")
def test_get_or_create_nonexistent_user(users_repo, user_id): def test_get_or_create_existing_user():
user = users_repo.get_or_create(user_id, atat_role_name="developer") Users.get_or_create_by_dod_id(DOD_ID, atat_role_name="developer")
assert user.id == user_id user = Users.get_or_create_by_dod_id(DOD_ID, atat_role_name="developer")
def test_get_or_create_existing_user(users_repo, user_id):
users_repo.get_or_create(user_id, atat_role_name="developer")
user = users_repo.get_or_create(user_id, atat_role_name="developer")
assert user assert user
def test_get_user(users_repo, user_id): def test_get_user():
users_repo.create(user_id, "developer") new_user = Users.create("developer")
user = users_repo.get(user_id) user = Users.get(new_user.id)
assert user.id == user_id assert user.id == new_user.id
def test_get_nonexistent_user(users_repo, user_id): def test_get_nonexistent_user():
users_repo.create(user_id, "developer") Users.create("developer")
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
users_repo.get(uuid4()) Users.get(uuid4())
def test_update_user(users_repo, user_id): def test_get_user_by_dod_id():
users_repo.create(user_id, "developer") new_user = Users.create("developer", dod_id=DOD_ID)
updated_user = users_repo.update(user_id, "ccpo") user = Users.get_by_dod_id(DOD_ID)
assert user == new_user
def test_update_user():
new_user = Users.create("developer")
updated_user = Users.update(new_user.id, "ccpo")
assert updated_user.atat_role.name == "ccpo" assert updated_user.atat_role.name == "ccpo"
def test_update_nonexistent_user(users_repo, user_id): def test_update_nonexistent_user():
users_repo.create(user_id, "developer") Users.create("developer")
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
users_repo.update(uuid4(), "ccpo") Users.update(uuid4(), "ccpo")
def test_update_existing_user_with_nonexistent_role(users_repo, user_id): def test_update_existing_user_with_nonexistent_role():
users_repo.create(user_id, "developer") new_user = Users.create("developer")
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
users_repo.update(user_id, "nonexistent") Users.update(new_user.id, "nonexistent")

View File

@ -1,41 +1,30 @@
import pytest
from uuid import uuid4 from uuid import uuid4
from atst.domain.workspace_users import WorkspaceUsers from atst.domain.workspace_users import WorkspaceUsers
from atst.domain.users import Users from atst.domain.users import Users
@pytest.fixture() def test_can_create_new_workspace_user():
def users_repo(session):
return Users(session)
@pytest.fixture()
def workspace_users_repo(session):
return WorkspaceUsers(session)
def test_can_create_new_workspace_user(users_repo, workspace_users_repo):
workspace_id = uuid4() workspace_id = uuid4()
user = users_repo.create(uuid4(), "developer") user = Users.create("developer")
workspace_user_dicts = [{"id": user.id, "workspace_role": "owner"}] workspace_user_dicts = [{"id": user.id, "workspace_role": "owner"}]
workspace_users = workspace_users_repo.add_many(workspace_id, workspace_user_dicts) workspace_users = WorkspaceUsers.add_many(workspace_id, workspace_user_dicts)
assert workspace_users[0].user.id == user.id assert workspace_users[0].user.id == user.id
assert workspace_users[0].user.atat_role.name == "developer" assert workspace_users[0].user.atat_role.name == "developer"
assert workspace_users[0].workspace_role.role.name == "owner" assert workspace_users[0].workspace_role.role.name == "owner"
def test_can_update_existing_workspace_user(users_repo, workspace_users_repo): def test_can_update_existing_workspace_user():
workspace_id = uuid4() workspace_id = uuid4()
user = users_repo.create(uuid4(), "developer") user = Users.create("developer")
workspace_users_repo.add_many( WorkspaceUsers.add_many(
workspace_id, [{"id": user.id, "workspace_role": "owner"}] workspace_id, [{"id": user.id, "workspace_role": "owner"}]
) )
workspace_users = workspace_users_repo.add_many( workspace_users = WorkspaceUsers.add_many(
workspace_id, [{"id": user.id, "workspace_role": "developer"}] workspace_id, [{"id": user.id, "workspace_role": "developer"}]
) )

View File

@ -1,9 +1,11 @@
import factory import factory
from uuid import uuid4 from uuid import uuid4
from atst.models import Request, RequestStatusEvent from atst.models import Request
from atst.models.pe_number import PENumber from atst.models.pe_number import PENumber
from atst.models.task_order import TaskOrder from atst.models.task_order import TaskOrder
from atst.models.user import User
from atst.models.role import Role
class RequestFactory(factory.Factory): class RequestFactory(factory.Factory):
@ -19,3 +21,20 @@ class PENumberFactory(factory.Factory):
class TaskOrderFactory(factory.Factory): class TaskOrderFactory(factory.Factory):
class Meta: class Meta:
model = TaskOrder model = TaskOrder
class RoleFactory(factory.Factory):
class Meta:
model = Role
permissions = []
class UserFactory(factory.Factory):
class Meta:
model = User
id = factory.Sequence(lambda x: uuid4())
email = "fake.user@mail.com"
first_name = "Fake"
last_name = "User"
atat_role = factory.SubFactory(RoleFactory)

BIN
tests/fixtures/crl/client-ca.der.crl vendored Normal file

Binary file not shown.

75
tests/fixtures/disa-pki.html vendored Normal file
View File

@ -0,0 +1,75 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html lang="en">
<head>
<title>DoD PKI CRLDPs</title>
</head>
<body>
<p>UNCLASSIFIED<br>
<p>This list is provided by DoD PKE Engineering. It is updated as new CAs come online.<br>
This is a list of CRL Distribution Points (CRLDPs) for all DoD CAs.</p>
<p><small>Updated April 5, 2018</small></p>
<a href="http://crl.disa.mil/crl/DODROOTCA2.crl">DoD Root CA 2</a><br>
<a href="http://crl.disa.mil/crl/DODROOTCA3.crl">DoD Root CA 3</a><br>
<a href="http://crl.disa.mil/crl/DODROOTCA4.crl">DoD Root CA 4</a><br>
<a href="http://crl.disa.mil/crl/DODROOTCA5.crl">DoD Root CA 5</a><br>
<a href="http://crl.disa.mil/crl/DODINTEROPERABILITYROOTCA1.crl">DoD Interoperability Root CA 1</a><br>
<a href="http://crl.disa.mil/crl/DODINTEROPERABILITYROOTCA2.crl">DoD Interoperability Root CA 2</a><br>
<a href="https://crl.gds.disa.mil/getcrl?DOD+NIPR+INTERNAL+NPE+ROOT+CA+1">NIPR INTERNAL NPE ROOT CA 1</a><br>
<a href="http://crl.disa.mil/crl/DODNPEROOTCA1.crl">DoD NPE Root CA 1</a><br>
<a href="http://crl.disa.mil/crl/DODWCFROOTCA1.crl">DoD WCF Root CA 1</a><br>
<a href="http://crl.disa.mil/crl/USDODCCEBINTEROPERABILITYROOTCA1.crl">DoD CCEB Interoperability Root CA 1</a><br>
<a href="http://crl.disa.mil/crl/USDODCCEBINTEROPERABILITYROOTCA2.crl">DoD CCEB Interoperability Root CA 2</a><br>
<a href="http://crl.disa.mil/crl/DMDNSIGNINGCA_1.crl">DoD DMDN Signing CA 1</a><br>
<a href="http://crl.disa.mil/crl/DODCA_31.crl">DoD CA-31</a><br>
<a href="http://crl.disa.mil/crl/DODCA_32.crl">DoD CA-32</a><br>
<a href="http://crl.disa.mil/crl/DODCA_33.crl">DoD ID CA-33</a><br>
<a href="http://crl.disa.mil/crl/DODCA_34.crl">DoD ID CA-34</a><br>
<a href="http://crl.disa.mil/crl/DODCA_35.crl">DoD ID SW CA-35</a><br>
<a href="http://crl.disa.mil/crl/DODCA_36.crl">DoD ID SW CA-36</a><br>
<a href="http://crl.disa.mil/crl/DODCA_37.crl">DoD ID SW CA-37</a><br>
<a href="http://crl.disa.mil/crl/DODCA_38.crl">DoD ID SW CA-38</a><br>
<a href="http://crl.disa.mil/crl/DODCA_39.crl">DoD ID CA-39</a><br>
<a href="http://crl.disa.mil/crl/DODCA_40.crl">DoD ID CA-40</a><br>
<a href="http://crl.disa.mil/crl/DODCA_41.crl">DoD ID CA-41</a><br>
<a href="http://crl.disa.mil/crl/DODCA_42.crl">DoD ID CA-42</a><br>
<a href="http://crl.disa.mil/crl/DODCA_43.crl">DoD ID CA-43</a><br>
<a href="http://crl.disa.mil/crl/DODCA_44.crl">DoD ID CA-44</a><br>
<a href="http://crl.disa.mil/crl/DODIDSWCA_45.crl">DoD ID SW CA-45</a><br>
<a href="http://crl.disa.mil/crl/DODIDSWCA_46.crl">DoD ID SW CA-46</a><br>
<a href="http://crl.disa.mil/crl/DODIDSWCA_47.crl">DoD ID SW CA-47</a><br>
<a href="http://crl.disa.mil/crl/DODIDSWCA_48.crl">DoD ID SW CA-48</a><br>
<a href="http://crl.disa.mil/crl/DODIDCA_49.crl">DoD ID CA-49</a><br>
<a href="http://crl.disa.mil/crl/DODIDCA_50.crl">DoD ID CA-50</a><br>
<a href="http://crl.disa.mil/crl/DODIDCA_51.crl">DoD ID CA-51</a><br>
<a href="http://crl.disa.mil/crl/DODIDCA_52.crl">DoD ID CA-52</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_31.crl">DoD EMAIL CA-31</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_32.crl">DoD EMAIL CA-32</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_33.crl">DoD EMAIL CA-33</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_34.crl">DoD EMAIL CA-34</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_39.crl">DoD EMAIL CA-39</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_40.crl">DoD EMAIL CA-40</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_41.crl">DoD EMAIL CA-41</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_42.crl">DoD EMAIL CA-42</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_43.crl">DoD EMAIL CA-43</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_44.crl">DoD EMAIL CA-44</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_49.crl">DoD EMAIL CA-49</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_50.crl">DoD EMAIL CA-50</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_51.crl">DoD EMAIL CA-51</a><br>
<a href="http://crl.disa.mil/crl/DODEMAILCA_52.crl">DoD EMAIL CA-52</a><br>
<a href="http://crl.disa.mil/crl/DODSWCA_53.crl">DoD SW CA-53</a><br>
<a href="http://crl.disa.mil/crl/DODSWCA_54.crl">DoD SW CA-54</a><br>
<a href="http://crl.disa.mil/crl/DODSWCA_55.crl">DoD SW CA-55</a><br>
<a href="http://crl.disa.mil/crl/DODSWCA_56.crl">DoD SW CA-56</a><br>
<a href="http://crl.disa.mil/crl/DODSWCA_57.crl">DoD SW CA-57</a><br>
<a href="http://crl.disa.mil/crl/DODSWCA_58.crl">DoD SW CA-58</a><br>
<p>UNCLASSIFIED<br>
</body>
</html>

14
tests/fixtures/test.der.crl vendored Normal file
View File

@ -0,0 +1,14 @@
-----BEGIN X509 CRL-----
MIICDjCB9zANBgkqhkiG9w0BAQQFADCBjTELMAkGA1UEBhMCVVMxFTATBgNVBAgT
DFBlbm5zeWx2YW5pYTEVMBMGA1UEBxMMUGhpbGFkZWxwaGlhMRAwDgYDVQQKEwdG
YXV4RG9EMQswCQYDVQQLEwJQVzERMA8GA1UEAxMIRmF1eCBEb0QxHjAcBgkqhkiG
9w0BCQEWD2ZhdXhkb2RAZG9kLmNvbRcNMTgwNzMwMjEzMzQ3WhcNMTgwODI5MjEz
MzQ3WjA4MBoCCQCoSzDcVuoXYxcNMTgwNzMwMjEzMzAxWjAaAgkA3u1dCnHnJ9kX
DTE4MDYyMDIwNDYxMVowDQYJKoZIhvcNAQEEBQADggEBAIYH2GbZUfqbqAaNJW2W
jREAbHnk2x5PSUri/YL9nH7ZAviZARtjuy5WKmu4hhAc/RwarwITT3NtP3BddLTF
RCd1vdsKWh4s7QqEZQSXaXb4/uEP2rsLVmbWoZxIp2gXrQXSA5kkKx0N3pY3kETg
vuMax8E2GdoJLNJe0xm0+hk4C9HcOf+WPL26n1+J4ZIhKf67BfZli0eFZue1PeVA
Ow2XBnKI/yw4GA9+OFcZ4JzJnRMdx/O9bjbzj3gkx9t22Ukzo66BVklplqWmb4YQ
PaRl0LxZtP/GLE6Ej8QmwK2SC26M60F6ceIFtgY3gor5J3oWmXGYz5xm4PWLj5fp
v2w=
-----END X509 CRL-----

View File

@ -2,23 +2,24 @@ import tornado.gen
from tornado.httpclient import HTTPRequest, HTTPResponse from tornado.httpclient import HTTPRequest, HTTPResponse
from atst.api_client import ApiClient from atst.api_client import ApiClient
from tests.factories import RequestFactory from tests.factories import RequestFactory, UserFactory
MOCK_USER = { MOCK_USER = UserFactory.create()
"id": "9cb348f0-8102-4962-88c4-dac8180c904c",
"email": "fake.user@mail.com",
"first_name": "Fake",
"last_name": "User",
}
MOCK_REQUEST = RequestFactory.create( MOCK_REQUEST = RequestFactory.create(
creator=MOCK_USER["id"], creator=MOCK_USER.id,
body={ body={
"financial_verification": { "financial_verification": {
"pe_id": "0203752A", "pe_id": "0203752A",
}, },
} }
) )
DOD_SDN_INFO = {
'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"
class MockApiClient(ApiClient): class MockApiClient(ApiClient):

View File

@ -32,6 +32,7 @@ class TestPENumberInForm:
def _set_monkeypatches(self, monkeypatch): def _set_monkeypatches(self, monkeypatch):
monkeypatch.setattr("atst.forms.financial.FinancialForm.validate", lambda s: True) monkeypatch.setattr("atst.forms.financial.FinancialForm.validate", lambda s: True)
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST) monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST)
monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: MOCK_USER)
def submit_data(self, client, data): def submit_data(self, client, data):
response = client.post( response = client.post(

View File

@ -1,21 +1,14 @@
import re import re
import pytest import pytest
import urllib import urllib
from tests.mocks import MOCK_USER from tests.mocks import MOCK_USER, MOCK_REQUEST
from tests.factories import RequestFactory from tests.factories import RequestFactory
ERROR_CLASS = "alert--error" ERROR_CLASS = "alert--error"
MOCK_REQUEST = RequestFactory.create(
creator=MOCK_USER["id"],
body={
"financial_verification": {
"pe_id": "0203752A",
},
}
)
def test_submit_invalid_request_form(monkeypatch, client, user_session):
def test_submit_invalid_request_form(monkeypatch, client): user_session()
response = client.post( response = client.post(
"/requests/new/1", "/requests/new/1",
headers={"Content-Type": "application/x-www-form-urlencoded"}, headers={"Content-Type": "application/x-www-form-urlencoded"},
@ -24,7 +17,8 @@ def test_submit_invalid_request_form(monkeypatch, client):
assert re.search(ERROR_CLASS, response.data.decode()) assert re.search(ERROR_CLASS, response.data.decode())
def test_submit_valid_request_form(monkeypatch, client): def test_submit_valid_request_form(monkeypatch, client, user_session):
user_session()
monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True) monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True)
response = client.post( response = client.post(

View File

@ -1,5 +1,4 @@
import pytest import pytest
import tornado
from tests.mocks import MOCK_USER from tests.mocks import MOCK_USER
from tests.factories import RequestFactory from tests.factories import RequestFactory
@ -8,7 +7,8 @@ def _mock_func(*args, **kwargs):
return RequestFactory.create() return RequestFactory.create()
def test_submit_reviewed_request(monkeypatch, client): def test_submit_reviewed_request(monkeypatch, client, 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", "pending") monkeypatch.setattr("atst.models.request.Request.status", "pending")
@ -23,7 +23,8 @@ def test_submit_reviewed_request(monkeypatch, client):
assert "modal" not in response.headers["Location"] assert "modal" not in response.headers["Location"]
def test_submit_autoapproved_reviewed_request(monkeypatch, client): def test_submit_autoapproved_reviewed_request(monkeypatch, client, 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", "approved")

View File

@ -1,78 +1,90 @@
import re from flask import session, url_for
import pytest from .mocks import DOD_SDN
MOCK_USER = {"id": "438567dd-25fa-4d83-a8cc-8aa8366cb24a"} MOCK_USER = {"id": "438567dd-25fa-4d83-a8cc-8aa8366cb24a"}
def _fetch_user_info(c, t): def _fetch_user_info(c, t):
return MOCK_USER return MOCK_USER
@pytest.mark.skip
def test_redirects_when_not_logged_in(): def test_successful_login_redirect(client, monkeypatch):
pass monkeypatch.setattr("atst.routes._is_valid_certificate", lambda *args: True)
# response = yield http_client.fetch(
# base_url + "/home", raise_error=False, follow_redirects=False resp = client.get(
# ) "/login-redirect",
# location = response.headers["Location"] environ_base={
# assert response.code == 302 "HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS", "HTTP_X_SSL_CLIENT_S_DN": DOD_SDN
# assert response.error },
# assert re.match("/\??", location) )
assert resp.status_code == 302
assert "home" in resp.headers["Location"]
assert session["user_id"]
# @pytest.mark.skip def test_unsuccessful_login_redirect(client, monkeypatch):
# def test_redirects_when_session_does_not_exist(): resp = client.get("/login-redirect")
# monkeypatch.setattr("atst.handlers.main.Main.get_secure_cookie", lambda s,c: 'stale cookie!')
# response = yield http_client.fetch( assert resp.status_code == 302
# base_url + "/home", raise_error=False, follow_redirects=False assert "unauthorized" in resp.headers["Location"]
# ) assert "user_id" not in session
# location = response.headers["Location"]
# cookie = response.headers._dict.get('Set-Cookie')
# # should clear session cookie
# assert 'atat=""' in cookie
# assert response.code == 302
# assert response.error
# assert re.match("/\??", location)
# @pytest.mark.skip # checks that all of the routes in the app are protected by auth
# def test_login_with_valid_bearer_token():
# monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info)
# response = client.fetch( def test_routes_are_protected(client, app):
# base_url + "/login-redirect?bearer-token=abc-123", for rule in app.url_map.iter_rules():
# follow_redirects=False, args = [1] * len(rule.arguments)
# raise_error=False, mock_args = dict(zip(rule.arguments, args))
# ) _n, route = rule.build(mock_args)
# assert response.headers["Set-Cookie"].startswith("atat") if route in UNPROTECTED_ROUTES or "/static" in route:
# assert response.headers["Location"] == "/home" continue
# assert response.code == 302
# if "GET" in rule.methods:
# resp = client.get(route)
# @pytest.mark.skip assert resp.status_code == 302
# def test_login_via_dev_endpoint(): assert resp.headers["Location"] == "http://localhost/"
# response = yield http_client.fetch(
# base_url + "/login-dev", raise_error=False, follow_redirects=False if "POST" in rule.methods:
# ) resp = client.post(route)
# assert response.headers["Set-Cookie"].startswith("atat") assert resp.status_code == 302
# assert response.code == 302 assert resp.headers["Location"] == "http://localhost/"
# assert response.headers["Location"] == "/home"
#
# UNPROTECTED_ROUTES = ["/", "/login-dev", "/login-redirect", "/unauthorized"]
# @pytest.mark.skip
# def test_login_with_invalid_bearer_token(): # this implicitly relies on the test config and test CRL in tests/fixtures/crl
# _response = yield http_client.fetch(
# base_url + "/home",
# raise_error=False, def test_crl_validation_on_login(client):
# headers={"Cookie": "bearer-token=anything"}, good_cert = open("ssl/client-certs/atat.mil.crt", "rb").read()
# ) bad_cert = open("ssl/client-certs/bad-atat.mil.crt", "rb").read()
#
# @pytest.mark.skip # bad cert is on the test CRL
# def test_valid_login_creates_session(): resp = client.get(
# monkeypatch.setattr("atst.handlers.login_redirect.LoginRedirect._fetch_user_info", _fetch_user_info) "/login-redirect",
# assert len(app.sessions.sessions) == 0 environ_base={
# yield http_client.fetch( "HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
# base_url + "/login-redirect?bearer-token=abc-123", "HTTP_X_SSL_CLIENT_S_DN": DOD_SDN,
# follow_redirects=False, "HTTP_X_SSL_CLIENT_CERT": bad_cert.decode(),
# raise_error=False, },
# ) )
# assert len(app.sessions.sessions) == 1 assert resp.status_code == 302
# session = list(app.sessions.sessions.values())[0] assert "unauthorized" in resp.headers["Location"]
# assert "atat_permissions" in session["user"] assert "user_id" not in session
# assert isinstance(session["user"]["atat_permissions"], list)
# good cert is not on the test CRL, passes
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": good_cert.decode(),
},
)
assert resp.status_code == 302
assert "home" in resp.headers["Location"]
assert session["user_id"]

View File

@ -1,19 +1,17 @@
import pytest import pytest
@pytest.mark.parametrize("path", (
def test_routes(client):
for path in (
"/", "/",
"/home", "/home",
"/workspaces", "/workspaces",
"/requests", "/requests",
"/requests/new", "/requests/new/1",
"/requests/new/2",
"/users", "/users",
"/reports", "/reports",
"/calculator", "/calculator",
): ))
response = client.get(path) def test_routes(path, client, user_session):
if response.status_code == 404: user_session()
__import__('ipdb').set_trace()
assert response.status_code == 200 response = client.get(path)
assert response.status_code == 200