Merge branch 'master' into ui/input-field-frontend-validation
This commit is contained in:
commit
3f5d961513
@ -16,12 +16,14 @@ log/*
|
||||
LICENSE
|
||||
*.md
|
||||
|
||||
# Skip pipenv/virtualenv related things
|
||||
# Skip envrc
|
||||
.envrc
|
||||
.venv
|
||||
|
||||
# Skip ansible-container stuff
|
||||
ansible*
|
||||
container.yml
|
||||
meta.yml
|
||||
requirements.yml
|
||||
|
||||
# Skip kubernetes and Docker config stuff
|
||||
deploy
|
||||
|
12
Pipfile.lock
generated
12
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "2b149e0d8c23814a2c701b53f5c75b36714a2ccd4e2a2769924ef6e2a3f09e97"
|
||||
"sha256": "5fc8273838354406366b401529a6f512a73ac6a8ecea6699afa4ab7b4996bf13"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -271,6 +271,7 @@
|
||||
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
|
||||
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
|
||||
],
|
||||
"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"
|
||||
},
|
||||
"redis": {
|
||||
@ -501,6 +502,7 @@
|
||||
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
|
||||
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
|
||||
],
|
||||
"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"
|
||||
},
|
||||
"itsdangerous": {
|
||||
@ -618,6 +620,7 @@
|
||||
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
||||
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
||||
],
|
||||
"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"
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
@ -640,6 +643,7 @@
|
||||
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
|
||||
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
|
||||
],
|
||||
"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"
|
||||
},
|
||||
"pygments": {
|
||||
@ -689,15 +693,11 @@
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:1cbc199009e78f92d9edf554be4fe40fb7b0bef71ba688602a00e97a51909110",
|
||||
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
|
||||
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
|
||||
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
|
||||
"sha256:6f89b5c95e93945b597776163403d47af72d243f366bf4622ff08bdfd1c950b7",
|
||||
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
|
||||
"sha256:be622cc81696e24d0836ba71f6272a2b5767669b0d79fdcf0295d51ac2e156c8",
|
||||
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b",
|
||||
"sha256:f39411e380e2182ad33be039e8ee5770a5d9efe01a2bfb7ae58d9ba31c4a2a9d"
|
||||
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b"
|
||||
],
|
||||
"version": "==4.2b4"
|
||||
},
|
||||
|
43
alembic/versions/05d6272bdb43_rename_request_creator_.py
Normal file
43
alembic/versions/05d6272bdb43_rename_request_creator_.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""rename request creator
|
||||
|
||||
Revision ID: 05d6272bdb43
|
||||
Revises: 77b065750596
|
||||
Create Date: 2018-08-07 20:21:22.559283
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '05d6272bdb43'
|
||||
down_revision = '77b065750596'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
db = op.get_bind()
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('requests', sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True))
|
||||
op.create_foreign_key('requests_user_id_fk', 'requests', 'users', ['user_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
db.execute("UPDATE requests SET user_id = creator")
|
||||
|
||||
op.alter_column('requests', 'user_id', nullable=False)
|
||||
op.drop_column('requests', 'creator')
|
||||
|
||||
|
||||
|
||||
def downgrade():
|
||||
db = op.get_bind()
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('requests', sa.Column('creator', postgresql.UUID(), autoincrement=False, nullable=True))
|
||||
op.drop_constraint('requests_user_id_fk', 'requests', type_='foreignkey')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
db.execute("UPDATE requests SET creator = user_id")
|
||||
op.drop_column('requests', 'user_id')
|
@ -34,6 +34,4 @@ def upgrade():
|
||||
|
||||
|
||||
def downgrade():
|
||||
db = op.get_bind()
|
||||
db.execute("DELETE FROM roles WHERE name = 'default'")
|
||||
|
||||
pass
|
||||
|
49
alembic/versions/77b065750596_new_request_statuses.py
Normal file
49
alembic/versions/77b065750596_new_request_statuses.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""new request statuses
|
||||
|
||||
Revision ID: 77b065750596
|
||||
Revises: 1f57f784ed5b
|
||||
Create Date: 2018-08-07 16:42:11.502361
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm.session import sessionmaker
|
||||
|
||||
from atst.models.request_status_event import RequestStatus
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '77b065750596'
|
||||
down_revision = '1f57f784ed5b'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
"""
|
||||
Update all existing request statuses so that the state of the
|
||||
table reflects the statuses listed in RequestStatus.
|
||||
|
||||
This involves fixing the casing on existing statuses, and
|
||||
deleting statuses that have no match.
|
||||
"""
|
||||
|
||||
db = op.get_bind()
|
||||
|
||||
status_events = db.execute("SELECT * FROM request_status_events").fetchall()
|
||||
for status_event in status_events:
|
||||
try:
|
||||
status = RequestStatus[status_event["new_status"].upper()]
|
||||
query = sa.text("""
|
||||
UPDATE request_status_events
|
||||
SET new_status = :status
|
||||
WHERE id = :id"""
|
||||
)
|
||||
db.execute(query, id=status_event["id"], status=status.name)
|
||||
except KeyError:
|
||||
query = sa.text("DELETE FROM request_status_events WHERE id = :id")
|
||||
db.execute(query, id=status_event["id"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -169,15 +169,4 @@ def upgrade():
|
||||
|
||||
|
||||
def downgrade():
|
||||
db = op.get_bind()
|
||||
db.execute("""
|
||||
DELETE FROM roles
|
||||
WHERE name IN (
|
||||
'ccpo',
|
||||
'owner',
|
||||
'admin',
|
||||
'developer',
|
||||
'billing_auditor',
|
||||
'security_auditor'
|
||||
);
|
||||
""")
|
||||
pass
|
||||
|
@ -15,6 +15,7 @@ from atst.routes import bp
|
||||
from atst.routes.workspaces import bp as workspace_routes
|
||||
from atst.routes.requests import requests_bp
|
||||
from atst.routes.dev import bp as dev_routes
|
||||
from atst.routes.errors import make_error_pages
|
||||
from atst.domain.authnid.crl.validator import Validator
|
||||
from atst.domain.auth import apply_authentication
|
||||
|
||||
@ -45,10 +46,11 @@ def make_app(config):
|
||||
Session(app)
|
||||
assets_environment.init_app(app)
|
||||
|
||||
make_error_pages(app)
|
||||
app.register_blueprint(bp)
|
||||
app.register_blueprint(workspace_routes)
|
||||
app.register_blueprint(requests_bp)
|
||||
if ENV != "production":
|
||||
if ENV != "prod":
|
||||
app.register_blueprint(dev_routes)
|
||||
|
||||
apply_authentication(app)
|
||||
|
@ -14,3 +14,19 @@ class AlreadyExistsError(Exception):
|
||||
@property
|
||||
def message(self):
|
||||
return "{} already exists".format(self.resource_name)
|
||||
|
||||
|
||||
class UnauthorizedError(Exception):
|
||||
def __init__(self, user, action):
|
||||
self.user = user
|
||||
self.action = action
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return "User {} not authorized to {}".format(self.user.id, self.action)
|
||||
|
||||
|
||||
class UnauthenticatedError(Exception):
|
||||
@property
|
||||
def message(self):
|
||||
return str(self)
|
||||
|
@ -1,8 +1,9 @@
|
||||
from sqlalchemy import exists, and_
|
||||
from sqlalchemy import exists, and_, exc
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from atst.models import Request, RequestStatusEvent
|
||||
from atst.models.request import Request
|
||||
from atst.models.request_status_event import RequestStatusEvent, RequestStatus
|
||||
from atst.database import db
|
||||
|
||||
from .exceptions import NotFoundError
|
||||
@ -30,11 +31,9 @@ class Requests(object):
|
||||
AUTO_APPROVE_THRESHOLD = 1000000
|
||||
|
||||
@classmethod
|
||||
def create(cls, creator_id, body):
|
||||
request = Request(creator=creator_id, body=body)
|
||||
|
||||
status_event = RequestStatusEvent(new_status="incomplete")
|
||||
request.status_events.append(status_event)
|
||||
def create(cls, creator, body):
|
||||
request = Request(creator=creator, body=body)
|
||||
request = Requests.set_status(request, RequestStatus.STARTED)
|
||||
|
||||
db.session.add(request)
|
||||
db.session.commit()
|
||||
@ -42,12 +41,15 @@ class Requests(object):
|
||||
return request
|
||||
|
||||
@classmethod
|
||||
def exists(cls, request_id, creator_id):
|
||||
return db.session.query(
|
||||
exists().where(
|
||||
and_(Request.id == request_id, Request.creator == creator_id)
|
||||
)
|
||||
).scalar()
|
||||
def exists(cls, request_id, creator):
|
||||
try:
|
||||
return db.session.query(
|
||||
exists().where(
|
||||
and_(Request.id == request_id, Request.creator == creator)
|
||||
)
|
||||
).scalar()
|
||||
except exc.DataError:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get(cls, request_id):
|
||||
@ -59,10 +61,10 @@ class Requests(object):
|
||||
return request
|
||||
|
||||
@classmethod
|
||||
def get_many(cls, creator_id=None):
|
||||
def get_many(cls, creator=None):
|
||||
filters = []
|
||||
if creator_id:
|
||||
filters.append(Request.creator == creator_id)
|
||||
if creator:
|
||||
filters.append(Request.creator == creator)
|
||||
|
||||
requests = (
|
||||
db.session.query(Request)
|
||||
@ -74,10 +76,13 @@ class Requests(object):
|
||||
|
||||
@classmethod
|
||||
def submit(cls, request):
|
||||
request.status_events.append(RequestStatusEvent(new_status="submitted"))
|
||||
|
||||
new_status = None
|
||||
if Requests.should_auto_approve(request):
|
||||
request.status_events.append(RequestStatusEvent(new_status="approved"))
|
||||
new_status = RequestStatus.PENDING_FINANCIAL_VERIFICATION
|
||||
else:
|
||||
new_status = RequestStatus.PENDING_CCPO_APPROVAL
|
||||
|
||||
request = Requests.set_status(request, new_status)
|
||||
|
||||
db.session.add(request)
|
||||
db.session.commit()
|
||||
@ -100,11 +105,6 @@ class Requests(object):
|
||||
|
||||
request.body = deep_merge(request_delta, request.body)
|
||||
|
||||
if Requests.should_allow_submission(request):
|
||||
request.status_events.append(
|
||||
RequestStatusEvent(new_status="pending_submission")
|
||||
)
|
||||
|
||||
# Without this, sqlalchemy won't notice the change to request.body,
|
||||
# since it doesn't track dictionary mutations by default.
|
||||
flag_modified(request, "body")
|
||||
@ -112,6 +112,20 @@ class Requests(object):
|
||||
db.session.add(request)
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def set_status(cls, request: Request, status: RequestStatus):
|
||||
status_event = RequestStatusEvent(new_status=status)
|
||||
request.status_events.append(status_event)
|
||||
return request
|
||||
|
||||
@classmethod
|
||||
def action_required_by(cls, request):
|
||||
return {
|
||||
RequestStatus.STARTED: "mission_owner",
|
||||
RequestStatus.PENDING_FINANCIAL_VERIFICATION: "mission_owner",
|
||||
RequestStatus.PENDING_CCPO_APPROVAL: "ccpo"
|
||||
}.get(request.status)
|
||||
|
||||
@classmethod
|
||||
def should_auto_approve(cls, request):
|
||||
try:
|
||||
@ -129,6 +143,10 @@ class Requests(object):
|
||||
"primary_poc",
|
||||
]
|
||||
existing_request_sections = request.body.keys()
|
||||
return request.status == "incomplete" and all(
|
||||
return request.status == RequestStatus.STARTED and all(
|
||||
section in existing_request_sections for section in all_request_sections
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_pending_financial_verification(cls, request):
|
||||
return request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
|
||||
|
@ -37,6 +37,7 @@ class Users(object):
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
raise AlreadyExistsError("user")
|
||||
|
||||
return user
|
||||
|
@ -29,8 +29,10 @@ class NewlineListField(Field):
|
||||
widget = TextArea()
|
||||
|
||||
def _value(self):
|
||||
if self.data:
|
||||
return "\n".join(self.data)
|
||||
if isinstance(self.data, list):
|
||||
return '\n'.join(self.data)
|
||||
elif self.data:
|
||||
return self.data
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
@ -70,7 +70,7 @@ class FinancialForm(ValidatedForm):
|
||||
"Unique Item Identifier (UII)s related to your application(s) if you already have them."
|
||||
)
|
||||
|
||||
pe_id = StringField("Program Element (PE) Number related to your request")
|
||||
pe_id = StringField("Program Element (PE) Number related to your request", validators=[Required()])
|
||||
|
||||
treasury_code = StringField("Program Treasury Code")
|
||||
|
||||
|
@ -62,7 +62,7 @@ class RequestForm(ValidatedForm):
|
||||
)
|
||||
|
||||
engineering_assessment = RadioField(
|
||||
description="Have you completed an engineering assessment of your software systems for cloud readiness?",
|
||||
"Have you completed an engineering assessment of your software systems for cloud readiness?",
|
||||
choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")],
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from sqlalchemy import Column, func
|
||||
from sqlalchemy import Column, func, ForeignKey
|
||||
from sqlalchemy.types import DateTime
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from atst.models import Base
|
||||
@ -11,22 +11,19 @@ class Request(Base):
|
||||
__tablename__ = "requests"
|
||||
|
||||
id = Id()
|
||||
creator = Column(UUID(as_uuid=True))
|
||||
time_created = Column(DateTime(timezone=True), server_default=func.now())
|
||||
body = Column(JSONB)
|
||||
status_events = relationship(
|
||||
"RequestStatusEvent", backref="request", order_by="RequestStatusEvent.sequence"
|
||||
)
|
||||
|
||||
user_id = Column(ForeignKey("users.id"), nullable=False)
|
||||
creator = relationship("User")
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self.status_events[-1].new_status
|
||||
|
||||
@property
|
||||
def action_required_by(self):
|
||||
return {
|
||||
"incomplete": "mission_owner",
|
||||
"pending_submission": "mission_owner",
|
||||
"submitted": "ccpo",
|
||||
"approved": "mission_owner",
|
||||
}.get(self.status)
|
||||
def status_displayname(self):
|
||||
return self.status_events[-1].displayname
|
||||
|
@ -1,5 +1,6 @@
|
||||
from sqlalchemy import Column, func, ForeignKey
|
||||
from sqlalchemy.types import DateTime, String, BigInteger
|
||||
from enum import Enum
|
||||
from sqlalchemy import Column, func, ForeignKey, Enum as SQLAEnum
|
||||
from sqlalchemy.types import DateTime, BigInteger
|
||||
from sqlalchemy.schema import Sequence
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
@ -7,11 +8,20 @@ from atst.models import Base
|
||||
from atst.models.types import Id
|
||||
|
||||
|
||||
class RequestStatus(Enum):
|
||||
STARTED = "Started"
|
||||
PENDING_FINANCIAL_VERIFICATION = "Pending Financial Verification"
|
||||
PENDING_CCPO_APPROVAL = "Pending CCPO Approval"
|
||||
APPROVED = "Approved"
|
||||
EXPIRED = "Expired"
|
||||
DELETED = "Deleted"
|
||||
|
||||
|
||||
class RequestStatusEvent(Base):
|
||||
__tablename__ = "request_status_events"
|
||||
|
||||
id = Id()
|
||||
new_status = Column(String())
|
||||
new_status = Column(SQLAEnum(RequestStatus))
|
||||
time_created = Column(DateTime(timezone=True), server_default=func.now())
|
||||
request_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("requests.id", ondelete="CASCADE")
|
||||
@ -19,3 +29,7 @@ class RequestStatusEvent(Base):
|
||||
sequence = Column(
|
||||
BigInteger, Sequence("request_status_events_sequence_seq"), nullable=False
|
||||
)
|
||||
|
||||
@property
|
||||
def displayname(self):
|
||||
return self.new_status.value
|
||||
|
@ -5,6 +5,7 @@ import pendulum
|
||||
from atst.domain.requests import Requests
|
||||
from atst.domain.users import Users
|
||||
from atst.domain.authnid.utils import parse_sdn
|
||||
from atst.domain.exceptions import UnauthenticatedError
|
||||
|
||||
bp = Blueprint("atst", __name__)
|
||||
|
||||
@ -29,6 +30,9 @@ def catch_all(path):
|
||||
return render_template("{}.html".format(path))
|
||||
|
||||
|
||||
# TODO: this should be partly consolidated into a domain function that takes
|
||||
# all the necessary UWSGI environment values as args and either returns a user
|
||||
# or raises the UnauthenticatedError
|
||||
@bp.route('/login-redirect')
|
||||
def login_redirect():
|
||||
if request.environ.get('HTTP_X_SSL_CLIENT_VERIFY') == 'SUCCESS' and _is_valid_certificate(request):
|
||||
@ -39,15 +43,7 @@ def login_redirect():
|
||||
|
||||
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
|
||||
raise UnauthenticatedError()
|
||||
|
||||
|
||||
def _is_valid_certificate(request):
|
||||
|
@ -9,50 +9,56 @@ _DEV_USERS = {
|
||||
"dod_id": "1234567890",
|
||||
"first_name": "Sam",
|
||||
"last_name": "Seeceepio",
|
||||
"atat_role": "ccpo",
|
||||
"atat_role_name": "ccpo",
|
||||
"email": "sam@test.com"
|
||||
},
|
||||
"amanda": {
|
||||
"dod_id": "2345678901",
|
||||
"first_name": "Amanda",
|
||||
"last_name": "Adamson",
|
||||
"atat_role": "default",
|
||||
"atat_role_name": "default",
|
||||
"email": "amanda@test.com"
|
||||
},
|
||||
"brandon": {
|
||||
"dod_id": "3456789012",
|
||||
"first_name": "Brandon",
|
||||
"last_name": "Buchannan",
|
||||
"atat_role": "default",
|
||||
"atat_role_name": "default",
|
||||
"email": "brandon@test.com"
|
||||
},
|
||||
"christina": {
|
||||
"dod_id": "4567890123",
|
||||
"first_name": "Christina",
|
||||
"last_name": "Collins",
|
||||
"atat_role": "default",
|
||||
"atat_role_name": "default",
|
||||
"email": "christina@test.com"
|
||||
},
|
||||
"dominick": {
|
||||
"dod_id": "5678901234",
|
||||
"first_name": "Dominick",
|
||||
"last_name": "Domingo",
|
||||
"atat_role": "default",
|
||||
"atat_role_name": "default",
|
||||
"email": "dominick@test.com"
|
||||
},
|
||||
"erica": {
|
||||
"dod_id": "6789012345",
|
||||
"first_name": "Erica",
|
||||
"last_name": "Eichner",
|
||||
"atat_role": "default",
|
||||
"atat_role_name": "default",
|
||||
"email": "erica@test.com"
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
user = Users.get_or_create_by_dod_id(
|
||||
user_data["dod_id"],
|
||||
atat_role_name=user_data["atat_role_name"],
|
||||
first_name=user_data["first_name"],
|
||||
last_name=user_data["last_name"],
|
||||
email=user_data["email"]
|
||||
)
|
||||
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)
|
||||
|
21
atst/routes/errors.py
Normal file
21
atst/routes/errors.py
Normal file
@ -0,0 +1,21 @@
|
||||
from flask import render_template
|
||||
|
||||
import atst.domain.exceptions as exceptions
|
||||
|
||||
|
||||
def make_error_pages(app):
|
||||
@app.errorhandler(exceptions.NotFoundError)
|
||||
@app.errorhandler(exceptions.UnauthorizedError)
|
||||
# pylint: disable=unused-variable
|
||||
def not_found(e):
|
||||
app.logger.error(e.message)
|
||||
return render_template("not_found.html"), 404
|
||||
|
||||
|
||||
@app.errorhandler(exceptions.UnauthenticatedError)
|
||||
# pylint: disable=unused-variable
|
||||
def unauthorized(e):
|
||||
app.logger.error(e.message)
|
||||
return render_template('unauthenticated.html'), 401
|
||||
|
||||
return app
|
@ -25,10 +25,10 @@ def update_financial_verification(request_id):
|
||||
|
||||
if form.validate():
|
||||
request_data = {"financial_verification": post_data}
|
||||
Requests.update(request_id, request_data)
|
||||
valid = form.perform_extra_validation(
|
||||
existing_request.body.get("financial_verification")
|
||||
)
|
||||
Requests.update(request_id, request_data)
|
||||
if valid:
|
||||
return redirect(url_for("requests.financial_verification_submitted"))
|
||||
else:
|
||||
@ -41,4 +41,4 @@ def update_financial_verification(request_id):
|
||||
|
||||
@requests_bp.route("/requests/financial_verification_submitted")
|
||||
def financial_verification_submitted():
|
||||
pass
|
||||
return render_template("requests/financial_verification_submitted.html")
|
||||
|
@ -1,35 +1,36 @@
|
||||
import pendulum
|
||||
from flask import render_template, g
|
||||
from flask import render_template, g, url_for
|
||||
|
||||
from . import requests_bp
|
||||
from atst.domain.requests import Requests
|
||||
|
||||
|
||||
def map_request(user, request):
|
||||
def map_request(request):
|
||||
time_created = pendulum.instance(request.time_created)
|
||||
is_new = time_created.add(days=1) > pendulum.now()
|
||||
app_count = request.body.get("details_of_use", {}).get("num_software_systems", 0)
|
||||
update_url = url_for('requests.requests_form_update', screen=1, request_id=request.id)
|
||||
verify_url = url_for('requests.financial_verification', request_id=request.id)
|
||||
|
||||
return {
|
||||
"order_id": request.id,
|
||||
"is_new": is_new,
|
||||
"status": request.status,
|
||||
"app_count": 1,
|
||||
"status": request.status_displayname,
|
||||
"app_count": app_count,
|
||||
"date": time_created.format("M/DD/YYYY"),
|
||||
"full_name": user.full_name
|
||||
"full_name": request.creator.full_name,
|
||||
"edit_link": verify_url if Requests.is_pending_financial_verification(request) else update_url
|
||||
}
|
||||
|
||||
|
||||
@requests_bp.route("/requests", methods=["GET"])
|
||||
def requests_index():
|
||||
requests = []
|
||||
if (
|
||||
"review_and_approve_jedi_workspace_request"
|
||||
in g.current_user.atat_permissions
|
||||
):
|
||||
if "review_and_approve_jedi_workspace_request" in g.current_user.atat_permissions:
|
||||
requests = Requests.get_many()
|
||||
else:
|
||||
requests = Requests.get_many(creator_id=g.current_user.id)
|
||||
requests = Requests.get_many(creator=g.current_user)
|
||||
|
||||
mapped_requests = [map_request(g.current_user, r) for r in requests]
|
||||
mapped_requests = [map_request(r) for r in requests]
|
||||
|
||||
return render_template("requests.html", requests=mapped_requests)
|
||||
|
@ -76,7 +76,7 @@ class JEDIRequestFlow(object):
|
||||
|
||||
@property
|
||||
def can_submit(self):
|
||||
return self.request and self.request.status != "incomplete"
|
||||
return self.request and Requests.should_allow_submission(self.request)
|
||||
|
||||
@property
|
||||
def next_screen(self):
|
||||
@ -124,5 +124,5 @@ class JEDIRequestFlow(object):
|
||||
if self.request_id:
|
||||
Requests.update(self.request_id, request_data)
|
||||
else:
|
||||
request = Requests.create(self.current_user.id, request_data)
|
||||
request = Requests.create(self.current_user, request_data)
|
||||
self.request_id = request.id
|
||||
|
@ -3,6 +3,8 @@ from flask import g, redirect, render_template, url_for, request as http_request
|
||||
from . import requests_bp
|
||||
from atst.domain.requests import Requests
|
||||
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
|
||||
from atst.models.permissions import Permissions
|
||||
from atst.domain.exceptions import UnauthorizedError
|
||||
|
||||
|
||||
@requests_bp.route("/requests/new/<int:screen>", methods=["GET"])
|
||||
@ -25,6 +27,9 @@ def requests_form_new(screen):
|
||||
)
|
||||
@requests_bp.route("/requests/new/<int:screen>/<string:request_id>", methods=["GET"])
|
||||
def requests_form_update(screen=1, request_id=None):
|
||||
if request_id:
|
||||
_check_can_view_request(request_id)
|
||||
|
||||
request = Requests.get(request_id) if request_id is not None else None
|
||||
jedi_flow = JEDIRequestFlow(screen, request, request_id=request_id)
|
||||
|
||||
@ -79,10 +84,12 @@ def requests_update(screen=1, request_id=None):
|
||||
request_id=jedi_flow.request_id,
|
||||
)
|
||||
return redirect(where)
|
||||
|
||||
else:
|
||||
return render_template(
|
||||
"requests/screen-%d.html" % int(screen), **rerender_args
|
||||
)
|
||||
|
||||
else:
|
||||
return render_template("requests/screen-%d.html" % int(screen), **rerender_args)
|
||||
|
||||
@ -94,5 +101,18 @@ def requests_submit(request_id=None):
|
||||
|
||||
if request.status == "approved":
|
||||
return redirect("/requests?modal=True")
|
||||
|
||||
else:
|
||||
return redirect("/requests")
|
||||
|
||||
|
||||
# TODO: generalize this, along with other authorizations, into a policy-pattern
|
||||
# for authorization in the application
|
||||
def _check_can_view_request(request_id):
|
||||
if Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST in g.current_user.atat_permissions:
|
||||
pass
|
||||
elif Requests.exists(request_id, g.current_user):
|
||||
pass
|
||||
else:
|
||||
raise UnauthorizedError(g.current_user, "view request {}".format(request_id))
|
||||
|
||||
|
@ -1,23 +1,23 @@
|
||||
[default]
|
||||
PORT=8000
|
||||
ENVIRONMENT = dev
|
||||
DEBUG = true
|
||||
COOKIE_SECRET = some-secret-please-replace
|
||||
SECRET = change_me_into_something_secret
|
||||
SECRET_KEY = change_me_into_something_secret
|
||||
CAC_URL = http://localhost:8000/login-redirect
|
||||
CA_CHAIN = ssl/server-certs/ca-chain.pem
|
||||
COOKIE_SECRET = some-secret-please-replace
|
||||
CRL_DIRECTORY = crl
|
||||
DEBUG = true
|
||||
ENVIRONMENT = dev
|
||||
PERMANENT_SESSION_LIFETIME = 600
|
||||
PE_NUMBER_CSV_URL = http://c95e1ebb198426ee57b8-174bb05a294821bedbf46b6384fe9b1f.r31.cf5.rackcdn.com/penumbers.csv
|
||||
REDIS_URI = redis://localhost:6379
|
||||
PGAPPNAME = atst
|
||||
PGDATABASE = atat
|
||||
PGHOST = localhost
|
||||
PGPASSWORD = postgres
|
||||
PGPORT = 5432
|
||||
PGUSER = postgres
|
||||
PGPASSWORD = postgres
|
||||
PGDATABASE = atat
|
||||
SESSION_TYPE = redis
|
||||
PORT=8000
|
||||
REDIS_URI = redis://localhost:6379
|
||||
SECRET = change_me_into_something_secret
|
||||
SECRET_KEY = change_me_into_something_secret
|
||||
SESSION_COOKIE_NAME=atat
|
||||
SESSION_TYPE = redis
|
||||
SESSION_USE_SIGNER = True
|
||||
PERMANENT_SESSION_LIFETIME = 600
|
||||
CRL_DIRECTORY = crl
|
||||
CA_CHAIN = ssl/server-certs/ca-chain.pem
|
||||
WTF_CSRF_ENABLED = true
|
||||
|
@ -1,2 +1,3 @@
|
||||
[default]
|
||||
SESSION_COOKIE_SECURE=True
|
||||
SESSION_COOKIE_DOMAIN=atat.codes
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.6.5-alpine
|
||||
FROM alpine:3.8
|
||||
|
||||
### Very low chance of changing
|
||||
###############################
|
||||
@ -7,12 +7,12 @@ ARG APP_USER=atst
|
||||
ARG APP_GROUP=atat
|
||||
ARG APP_DIR=/opt/atat/atst
|
||||
ARG APP_PORT=8000
|
||||
ARG SITE_PACKAGES_DIR=/usr/local/lib/python3.6/site-packages
|
||||
ARG LOCAL_BIN_DIR=/usr/bin
|
||||
ARG SITE_PACKAGES_DIR=/usr/lib/python3.6/site-packages
|
||||
|
||||
ENV APP_USER "${APP_USER}"
|
||||
ENV APP_GROUP "${APP_GROUP}"
|
||||
ENV APP_DIR "${APP_DIR}"
|
||||
ENV SKIP_PIPENV true
|
||||
|
||||
# Set port to open
|
||||
EXPOSE "${APP_PORT}"
|
||||
@ -21,13 +21,16 @@ EXPOSE "${APP_PORT}"
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
|
||||
# Default command is to launch the server
|
||||
CMD ["bash", "-c", "${APP_DIR}/script/server"]
|
||||
CMD ["bash", "-c", "${APP_DIR}/script/uwsgi_server"]
|
||||
|
||||
### Items that will change almost every build
|
||||
#############################################
|
||||
# Copy installed python packages from the tester image
|
||||
COPY --from=atst-tester:latest "${SITE_PACKAGES_DIR}" "${SITE_PACKAGES_DIR}"
|
||||
|
||||
# Copy local bin directory (contains python system package wrappers)
|
||||
COPY --from=atst-tester:latest "${LOCAL_BIN_DIR}" "${LOCAL_BIN_DIR}"
|
||||
|
||||
# Copy the app directory contents from the tester image (includes node modules)
|
||||
COPY --from=atst-tester:latest "${APP_DIR}" "${APP_DIR}"
|
||||
|
||||
|
15
deploy/kubernetes/atst-configmap.yml
Normal file
15
deploy/kubernetes/atst-configmap.yml
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: atst-config
|
||||
namespace: atat
|
||||
data:
|
||||
uwsgi-config: |-
|
||||
[uwsgi]
|
||||
callable = app
|
||||
module = app
|
||||
socket = /var/run/uwsgi/uwsgi.socket
|
||||
plugins = python3
|
||||
virtualenv = /opt/atat/atst/.venv
|
||||
chmod-socket = 666
|
10
deploy/kubernetes/atst-envvars-configmap.yml
Normal file
10
deploy/kubernetes/atst-envvars-configmap.yml
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: atst-envvars
|
||||
namespace: atat
|
||||
data:
|
||||
FLASK_ENV: dev
|
||||
OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst/atst-overrides.ini
|
||||
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi-config.ini
|
79
deploy/kubernetes/atst-nginx-configmap.yml
Normal file
79
deploy/kubernetes/atst-nginx-configmap.yml
Normal file
@ -0,0 +1,79 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: atst-nginx
|
||||
namespace: atat
|
||||
data:
|
||||
nginx-config: |-
|
||||
server {
|
||||
server_name www.atat.codes atat.codes;
|
||||
listen 8442;
|
||||
listen [::]:8442 ipv6only=on;
|
||||
if ($http_x_forwarded_proto != 'https') {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
location /login-redirect {
|
||||
return 301 https://auth.atat.codes$request_uri;
|
||||
}
|
||||
location /login-dev {
|
||||
try_files $uri @appbasicauth;
|
||||
}
|
||||
location / {
|
||||
try_files $uri @app;
|
||||
}
|
||||
location @app {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket;
|
||||
}
|
||||
location @appbasicauth {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket;
|
||||
auth_basic "Developer Access";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
}
|
||||
}
|
||||
server {
|
||||
server_name auth.atat.codes;
|
||||
listen 8443 ssl;
|
||||
listen [::]:8443 ssl ipv6only=on;
|
||||
# SSL server certificate and private key
|
||||
ssl_certificate /etc/ssl/private/auth.atat.crt;
|
||||
ssl_certificate_key /etc/ssl/private/auth.atat.key;
|
||||
# Set SSL protocols, ciphers, and related options
|
||||
ssl_protocols TLSv1.3 TLSv1.2;
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ecdh_curve secp384r1;
|
||||
ssl_dhparam /etc/ssl/dhparam.pem;
|
||||
# SSL session options
|
||||
ssl_session_timeout 4h;
|
||||
ssl_session_cache shared:SSL:10m; # 1mb = ~4000 sessions
|
||||
ssl_session_tickets off;
|
||||
# OCSP Stapling
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
resolver 8.8.8.8 8.8.4.4;
|
||||
# Request and validate client certificate
|
||||
#ssl_verify_client on;
|
||||
#ssl_verify_depth 10;
|
||||
#ssl_client_certificate /etc/nginx/ssl/ca/client-ca.pem;
|
||||
# Guard against HTTPS -> HTTP downgrade
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always";
|
||||
location / {
|
||||
return 301 https://www.atat.codes$request_uri;
|
||||
}
|
||||
location /login-redirect {
|
||||
try_files $uri @app;
|
||||
}
|
||||
location @app {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket;
|
||||
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;
|
||||
uwsgi_param HTTP_X_SSL_CLIENT_S_DN_LEGACY $ssl_client_s_dn_legacy;
|
||||
uwsgi_param HTTP_X_SSL_CLIENT_I_DN $ssl_client_i_dn;
|
||||
uwsgi_param HTTP_X_SSL_CLIENT_I_DN_LEGACY $ssl_client_i_dn_legacy;
|
||||
}
|
||||
}
|
165
deploy/kubernetes/atst.yml
Normal file
165
deploy/kubernetes/atst.yml
Normal file
@ -0,0 +1,165 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: atat
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
name: atst
|
||||
namespace: atat
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 101
|
||||
containers:
|
||||
- name: atst
|
||||
image: registry.atat.codes:443/atst-prod:e9b6f76
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: atst-envvars
|
||||
volumeMounts:
|
||||
- name: atst-config
|
||||
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
||||
subPath: atst-overrides.ini
|
||||
- name: uwsgi-config
|
||||
mountPath: "/opt/atat/atst/uwsgi-config.ini"
|
||||
subPath: uwsgi-config.ini
|
||||
- name: uwsgi-socket-dir
|
||||
mountPath: "/var/run/uwsgi"
|
||||
- name: atst-nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- containerPort: 8442
|
||||
name: http
|
||||
- containerPort: 8443
|
||||
name: https
|
||||
volumeMounts:
|
||||
- name: nginx-auth-tls
|
||||
mountPath: "/etc/ssl/private"
|
||||
- name: nginx-config
|
||||
mountPath: "/etc/nginx/conf.d/atst.conf"
|
||||
subPath: atst.conf
|
||||
- name: nginx-dhparam
|
||||
mountPath: "/etc/ssl/dhparam.pem"
|
||||
subPath: dhparam.pem
|
||||
- name: nginx-htpasswd
|
||||
mountPath: "/etc/nginx/.htpasswd"
|
||||
subPath: .htpasswd
|
||||
- name: uwsgi-socket-dir
|
||||
mountPath: "/var/run/uwsgi"
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
volumes:
|
||||
- name: atst-config
|
||||
secret:
|
||||
secretName: atst-config-ini
|
||||
items:
|
||||
- key: atst-overrides.ini
|
||||
path: atst-overrides.ini
|
||||
mode: 0644
|
||||
- name: nginx-auth-tls
|
||||
secret:
|
||||
secretName: auth-atst-ingress-tls
|
||||
items:
|
||||
- key: tls.crt
|
||||
path: auth.atat.crt
|
||||
mode: 0644
|
||||
- key: tls.key
|
||||
path: auth.atat.key
|
||||
mode: 0640
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: atst-nginx
|
||||
items:
|
||||
- key: nginx-config
|
||||
path: atst.conf
|
||||
- name: nginx-dhparam
|
||||
secret:
|
||||
secretName: dhparam-4096
|
||||
items:
|
||||
- key: dhparam.pem
|
||||
path: dhparam.pem
|
||||
mode: 0640
|
||||
- name: nginx-htpasswd
|
||||
secret:
|
||||
secretName: atst-nginx-htpasswd
|
||||
items:
|
||||
- key: htpasswd
|
||||
path: .htpasswd
|
||||
mode: 0640
|
||||
- name: uwsgi-config
|
||||
configMap:
|
||||
name: atst-config
|
||||
items:
|
||||
- key: uwsgi-config
|
||||
path: uwsgi-config.ini
|
||||
mode: 0644
|
||||
- name: uwsgi-socket-dir
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
name: atst
|
||||
namespace: atat
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8442
|
||||
selector:
|
||||
app: atst
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
name: atst-auth
|
||||
namespace: atat
|
||||
spec:
|
||||
type: NodePort
|
||||
ports:
|
||||
- name: https
|
||||
protocol: TCP
|
||||
nodePort: 32751
|
||||
port: 8443
|
||||
selector:
|
||||
app: atst
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: atst
|
||||
namespace: atat
|
||||
annotations:
|
||||
kubernetes.io/tls-acme: "true"
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: 10m
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- www.atat.codes
|
||||
secretName: atst-ingress-tls
|
||||
rules:
|
||||
- host: www.atat.codes
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
backend:
|
||||
serviceName: atst
|
||||
servicePort: 80
|
4
deploy/kubernetes/set_atstconfig_secret.sh
Executable file
4
deploy/kubernetes/set_atstconfig_secret.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
kubectl -n atat delete secret atst-config-ini
|
||||
kubectl -n atat create secret generic atst-config-ini --from-file="${1}"
|
4
deploy/kubernetes/set_dhparam_secret.sh
Executable file
4
deploy/kubernetes/set_dhparam_secret.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
kubectl -n atat delete secret dhparam-4096
|
||||
kubectl -n atat create secret generic dhparam-4096 --from-file="${1}"
|
4
deploy/kubernetes/set_htpasswd_secret.sh
Executable file
4
deploy/kubernetes/set_htpasswd_secret.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
kubectl -n atat delete secret atst-nginx-htpasswd
|
||||
kubectl -n atat create secret generic atst-nginx-htpasswd --from-file="${1}"
|
@ -9,5 +9,8 @@ source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
APP_USER="atst"
|
||||
APP_UID="8010"
|
||||
|
||||
# Add additional packages required by app dependencies
|
||||
ADDITIONAL_PACKAGES="postgresql-libs python3 uwsgi uwsgi-python3"
|
||||
|
||||
# Run the shared alpine setup script
|
||||
source ./script/include/run_alpine_setup
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 8cf96c9776e7fd73c11d57160d26fc1715bf00da
|
||||
Subproject commit c44ca5070da78fd522a2e485aaa225cc638e11d3
|
37
script/seed.py
Normal file
37
script/seed.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Add root project dir to the python path
|
||||
import os
|
||||
import sys
|
||||
|
||||
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from atst.app import make_config, make_app
|
||||
from atst.domain.users import Users
|
||||
from atst.domain.requests import Requests
|
||||
from atst.domain.exceptions import AlreadyExistsError
|
||||
from tests.factories import RequestFactory
|
||||
from atst.routes.dev import _DEV_USERS as DEV_USERS
|
||||
|
||||
|
||||
def seed_db():
|
||||
users = []
|
||||
for dev_user in DEV_USERS.values():
|
||||
try:
|
||||
user = Users.create(**dev_user)
|
||||
users.append(user)
|
||||
except AlreadyExistsError:
|
||||
pass
|
||||
|
||||
for user in users:
|
||||
for dollar_value in [1, 200, 3000, 40000, 500000, 1000000]:
|
||||
request = Requests.create(
|
||||
user, RequestFactory.build_request_body(user, dollar_value)
|
||||
)
|
||||
Requests.submit(request)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = make_config()
|
||||
app = make_app(config)
|
||||
with app.app_context():
|
||||
seed_db()
|
@ -5,9 +5,6 @@
|
||||
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# Turn on sass compiler installation
|
||||
INSTALL_SASS="true"
|
||||
|
||||
# Enable database resetting
|
||||
RESET_DB="true"
|
||||
|
||||
@ -16,3 +13,6 @@ source ./script/include/run_setup
|
||||
|
||||
# Fetch and import the PE numbers
|
||||
run_command "python script/ingest_pe_numbers.py"
|
||||
|
||||
# Compile assets and generate hash-named static files
|
||||
yarn build
|
||||
|
@ -10,7 +10,7 @@ mkdir -p crl
|
||||
rsync -rq crl-tmp/. crl/.
|
||||
rm -rf crl-tmp
|
||||
|
||||
if [[ $FLASK_ENV != "production" ]]; then
|
||||
if [[ $FLASK_ENV != "prod" ]]; then
|
||||
# place our test CRL there
|
||||
cp ssl/client-certs/client-ca.der.crl crl/
|
||||
fi
|
||||
|
@ -9,7 +9,7 @@ echo "Resetting CA bundle..."
|
||||
rm ssl/server-certs/ca-chain.pem &> /dev/null || true
|
||||
touch $CA_CHAIN
|
||||
|
||||
if [[ $FLASK_ENV != "production" ]]; then
|
||||
if [[ $FLASK_ENV != "prod" ]]; then
|
||||
# only for testing and development
|
||||
echo "Copy in testing client CA..."
|
||||
cat ssl/client-certs/client-ca.crt >> $CA_CHAIN
|
||||
|
11
script/uwsgi_server
Executable file
11
script/uwsgi_server
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# script/uwsgi_server: Launch the UWSGI server
|
||||
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# Before starting the server, apply any pending migrations to the DB
|
||||
migrate_db
|
||||
|
||||
# Launch UWSGI
|
||||
run_command "uwsgi --ini ${UWSGI_CONFIG_FULLPATH}"
|
@ -65,20 +65,12 @@
|
||||
}
|
||||
|
||||
label {
|
||||
padding: 0 0 $gap 0;
|
||||
padding: 0 0 $gap/2 0;
|
||||
margin: 0;
|
||||
@include h4;
|
||||
@include line-max;
|
||||
position: relative;
|
||||
|
||||
.usa-input__help {
|
||||
display: block;
|
||||
@include h5;
|
||||
font-weight: normal;
|
||||
padding-top: $gap / 2;
|
||||
@include line-max;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
@ -88,6 +80,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.usa-input__help {
|
||||
display: block;
|
||||
@include h4;
|
||||
font-weight: normal;
|
||||
padding: $gap/2 0;
|
||||
@include line-max;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
@ -100,9 +100,14 @@
|
||||
padding: 0 0 $gap 0;
|
||||
@include h4;
|
||||
|
||||
label {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ul {
|
||||
|
@ -20,6 +20,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
|
||||
+ .subtitle * {
|
||||
margin-top: 0;
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% macro OptionsInput(field, inline=False) -%}
|
||||
<div class='usa-input {% if errors %}usa-input--error{% endif %}'>
|
||||
<div class='usa-input {% if field.errors %}usa-input--error{% endif %}'>
|
||||
|
||||
<fieldset class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
|
||||
<legend>
|
||||
{{ field.label }}
|
||||
{{ field.label | striptags}}
|
||||
|
||||
{% if field.description %}
|
||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||
@ -26,4 +26,7 @@
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{%- endmacro %}
|
||||
|
@ -17,7 +17,7 @@
|
||||
<span class='usa-input__help'>{{ field.description | safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if errors %}
|
||||
{% if field.errors %}
|
||||
{{ Icon('alert') }}
|
||||
{% endif %}
|
||||
|
||||
@ -54,6 +54,7 @@
|
||||
placeholder='{{ placeholder }}'
|
||||
{% if field.errors %}aria-invalid='true'{% endif %}>
|
||||
</masked-input>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<main class="usa-section usa-content">
|
||||
|
||||
<h1>Unauthorized</h1>
|
||||
<h1>Not Found</h1>
|
||||
|
||||
</main>
|
||||
|
@ -88,7 +88,7 @@
|
||||
{% for r in requests %}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<a class='icon-link icon-link--large' href="{{ url_for('requests.requests_form_update', screen=1, request_id=r['order_id']) if r["status"] != "approved" else url_for('requests.financial_verification', request_id=r['order_id']) }}">{{ r['order_id'] }}</a>
|
||||
<a class='icon-link icon-link--large' href="{{ r['edit_link'] }}">{{ r['order_id'] }}</a>
|
||||
{% if r['is_new'] %}<span class="usa-label">New</span>
|
||||
</th>
|
||||
{% endif %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends "../base.html.to" %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -15,4 +15,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% end %}
|
||||
{% endblock %}
|
@ -1,13 +1,21 @@
|
||||
<div class="progress-menu progress-menu--four">
|
||||
<ul>
|
||||
{% for s in screens %}
|
||||
<li class="progress-menu__item">
|
||||
<a href="{{ url_for('requests.requests_form_update', screen=loop.index, request_id=request_id) if request_id else url_for('requests.requests_form_new', screen=loop.index) }}"
|
||||
{% if g.matchesPath('/requests/new/{{ loop.index + 1 }}') %}class="active"{% endif %}
|
||||
>
|
||||
{{ s['title'] }}
|
||||
</a>
|
||||
</li>
|
||||
{% if loop.index < current %}
|
||||
{% set step_indicator = 'complete' %}
|
||||
{% elif loop.index == current %}
|
||||
{% set step_indicator = 'active' %}
|
||||
{% else %}
|
||||
{% set step_indicator = 'incomplete' %}
|
||||
{% endif %}
|
||||
|
||||
<li class="progress-menu__item progress-menu__item--{{ step_indicator }}">
|
||||
<a href="{{ url_for('requests.requests_form_update', screen=loop.index, request_id=request_id) if request_id else url_for('requests.requests_form_new', screen=loop.index) }}"
|
||||
{% if g.matchesPath('/requests/new/{{ loop.index + 1 }}') %}class="active"{% endif %}
|
||||
>
|
||||
{{ s['title'] }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
12
templates/unauthenticated.html
Normal file
12
templates/unauthenticated.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "error_base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<main class="usa-section usa-content">
|
||||
|
||||
<h1>Log in Failed</h1>
|
||||
|
||||
</main>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -5,10 +5,10 @@ import alembic.command
|
||||
|
||||
from atst.app import make_app, make_config
|
||||
from atst.database import db as _db
|
||||
from .mocks import MOCK_USER
|
||||
import tests.factories as factories
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture(scope="session")
|
||||
def app(request):
|
||||
config = make_config()
|
||||
|
||||
@ -27,11 +27,11 @@ def apply_migrations():
|
||||
alembic_config = os.path.join(os.path.dirname(__file__), "../", "alembic.ini")
|
||||
config = alembic.config.Config(alembic_config)
|
||||
app_config = make_config()
|
||||
config.set_main_option('sqlalchemy.url', app_config["DATABASE_URI"])
|
||||
alembic.command.upgrade(config, 'head')
|
||||
config.set_main_option("sqlalchemy.url", app_config["DATABASE_URI"])
|
||||
alembic.command.upgrade(config, "head")
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture(scope="session")
|
||||
def db(app, request):
|
||||
|
||||
_db.app = app
|
||||
@ -43,7 +43,7 @@ def db(app, request):
|
||||
_db.drop_all()
|
||||
|
||||
|
||||
@pytest.fixture(scope='function', autouse=True)
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def session(db, request):
|
||||
"""Creates a new database session for a test."""
|
||||
connection = db.engine.connect()
|
||||
@ -54,6 +54,14 @@ def session(db, request):
|
||||
|
||||
db.session = session
|
||||
|
||||
factory_list = [
|
||||
cls
|
||||
for _name, cls in factories.__dict__.items()
|
||||
if isinstance(cls, type) and cls.__module__ == "tests.factories"
|
||||
]
|
||||
for factory in factory_list:
|
||||
factory._meta.sqlalchemy_session = session
|
||||
|
||||
yield session
|
||||
|
||||
transaction.rollback()
|
||||
@ -81,10 +89,13 @@ def dummy_form():
|
||||
def dummy_field():
|
||||
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)
|
||||
@pytest.fixture
|
||||
def user_session(monkeypatch, session):
|
||||
def set_user_session(user=None):
|
||||
monkeypatch.setattr(
|
||||
"atst.domain.auth.get_current_user",
|
||||
lambda *args: user or factories.UserFactory.build(),
|
||||
)
|
||||
|
||||
return set_user_session
|
||||
|
@ -6,20 +6,8 @@ from atst.domain.pe_numbers import PENumbers
|
||||
from tests.factories import PENumberFactory
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def new_pe_number(session):
|
||||
def make_pe_number(**kwargs):
|
||||
pen = PENumberFactory.create(**kwargs)
|
||||
session.add(pen)
|
||||
session.commit()
|
||||
|
||||
return pen
|
||||
|
||||
return make_pe_number
|
||||
|
||||
|
||||
def test_can_get_pe_number(new_pe_number):
|
||||
new_pen = new_pe_number(number="0701367F", description="Combat Support - Offensive")
|
||||
def test_can_get_pe_number():
|
||||
new_pen = PENumberFactory.create(number="0701367F", description="Combat Support - Offensive")
|
||||
pen = PENumbers.get(new_pen.number)
|
||||
|
||||
assert pen.number == new_pen.number
|
||||
|
@ -3,17 +3,14 @@ from uuid import uuid4
|
||||
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
from atst.domain.requests import Requests
|
||||
from atst.models.request_status_event import RequestStatus
|
||||
|
||||
from tests.factories import RequestFactory
|
||||
from tests.factories import RequestFactory, UserFactory
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def new_request(session):
|
||||
created_request = RequestFactory.create()
|
||||
session.add(created_request)
|
||||
session.commit()
|
||||
|
||||
return created_request
|
||||
return RequestFactory.create()
|
||||
|
||||
|
||||
def test_can_get_request(new_request):
|
||||
@ -27,22 +24,42 @@ def test_nonexistent_request_raises():
|
||||
Requests.get(uuid4())
|
||||
|
||||
|
||||
def test_new_request_has_started_status():
|
||||
request = Requests.create(UserFactory.build(), {})
|
||||
assert request.status == RequestStatus.STARTED
|
||||
|
||||
|
||||
def test_auto_approve_less_than_1m(new_request):
|
||||
new_request.body = {"details_of_use": {"dollar_value": 999999}}
|
||||
request = Requests.submit(new_request)
|
||||
|
||||
assert request.status == 'approved'
|
||||
assert request.status == RequestStatus.PENDING_FINANCIAL_VERIFICATION
|
||||
|
||||
|
||||
def test_dont_auto_approve_if_dollar_value_is_1m_or_above(new_request):
|
||||
new_request.body = {"details_of_use": {"dollar_value": 1000000}}
|
||||
request = Requests.submit(new_request)
|
||||
|
||||
assert request.status == 'submitted'
|
||||
assert request.status == RequestStatus.PENDING_CCPO_APPROVAL
|
||||
|
||||
|
||||
def test_dont_auto_approve_if_no_dollar_value_specified(new_request):
|
||||
new_request.body = {"details_of_use": {}}
|
||||
request = Requests.submit(new_request)
|
||||
|
||||
assert request.status == 'submitted'
|
||||
assert request.status == RequestStatus.PENDING_CCPO_APPROVAL
|
||||
|
||||
|
||||
def test_should_allow_submission(new_request):
|
||||
assert Requests.should_allow_submission(new_request)
|
||||
|
||||
del new_request.body['details_of_use']
|
||||
assert not Requests.should_allow_submission(new_request)
|
||||
|
||||
|
||||
def test_exists(session):
|
||||
user_allowed = UserFactory.create()
|
||||
user_denied = UserFactory.create()
|
||||
request = RequestFactory.create(creator=user_allowed)
|
||||
assert Requests.exists(request.id, user_allowed)
|
||||
assert not Requests.exists(request.id, user_denied)
|
||||
|
@ -6,20 +6,8 @@ from atst.domain.task_orders import TaskOrders
|
||||
from tests.factories import TaskOrderFactory
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def new_task_order(session):
|
||||
def make_task_order(**kwargs):
|
||||
to = TaskOrderFactory.create(**kwargs)
|
||||
session.add(to)
|
||||
session.commit()
|
||||
|
||||
return to
|
||||
|
||||
return make_task_order
|
||||
|
||||
|
||||
def test_can_get_task_order(new_task_order):
|
||||
new_to = new_task_order(number="0101969F")
|
||||
def test_can_get_task_order():
|
||||
new_to = TaskOrderFactory.create(number="0101969F")
|
||||
to = TaskOrders.get(new_to.number)
|
||||
|
||||
assert to.id == to.id
|
||||
|
@ -1,40 +1,102 @@
|
||||
import random
|
||||
import string
|
||||
import factory
|
||||
from uuid import uuid4
|
||||
|
||||
from atst.models import Request
|
||||
from atst.models.request import Request
|
||||
from atst.models.request_status_event import RequestStatusEvent, RequestStatus
|
||||
from atst.models.pe_number import PENumber
|
||||
from atst.models.task_order import TaskOrder
|
||||
from atst.models.user import User
|
||||
from atst.models.role import Role
|
||||
from atst.models.request_status_event import RequestStatusEvent
|
||||
from atst.domain.roles import Roles
|
||||
|
||||
|
||||
class RequestFactory(factory.Factory):
|
||||
class Meta:
|
||||
model = Request
|
||||
|
||||
id = factory.Sequence(lambda x: uuid4())
|
||||
|
||||
class PENumberFactory(factory.Factory):
|
||||
class Meta:
|
||||
model = PENumber
|
||||
|
||||
class TaskOrderFactory(factory.Factory):
|
||||
class Meta:
|
||||
model = TaskOrder
|
||||
|
||||
class RoleFactory(factory.Factory):
|
||||
class RoleFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
class Meta:
|
||||
model = Role
|
||||
|
||||
permissions = []
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
|
||||
class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
id = factory.Sequence(lambda x: uuid4())
|
||||
email = "fake.user@mail.com"
|
||||
first_name = "Fake"
|
||||
last_name = "User"
|
||||
email = factory.Faker("email")
|
||||
first_name = factory.Faker("first_name")
|
||||
last_name = factory.Faker("last_name")
|
||||
atat_role = factory.SubFactory(RoleFactory)
|
||||
dod_id = factory.LazyFunction(lambda: "".join(random.choices(string.digits, k=10)))
|
||||
|
||||
|
||||
class RequestStatusEventFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
|
||||
class Meta:
|
||||
model = RequestStatusEvent
|
||||
|
||||
id = factory.Sequence(lambda x: uuid4())
|
||||
|
||||
|
||||
class RequestFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
class Meta:
|
||||
model = Request
|
||||
|
||||
id = factory.Sequence(lambda x: uuid4())
|
||||
status_events = factory.RelatedFactory(
|
||||
RequestStatusEventFactory, "request", new_status=RequestStatus.STARTED
|
||||
)
|
||||
creator = factory.SubFactory(UserFactory)
|
||||
body = factory.LazyAttribute(lambda r: RequestFactory.build_request_body(r.creator))
|
||||
|
||||
@classmethod
|
||||
def build_request_body(cls, user, dollar_value=1000000):
|
||||
return {
|
||||
"primary_poc": {
|
||||
"dodid_poc": user.dod_id,
|
||||
"email_poc": user.email,
|
||||
"fname_poc": user.first_name,
|
||||
"lname_poc": user.last_name
|
||||
},
|
||||
"details_of_use": {
|
||||
"jedi_usage": "adf",
|
||||
"start_date": "2018-08-08",
|
||||
"cloud_native": "yes",
|
||||
"dollar_value": dollar_value,
|
||||
"dod_component": "us_navy",
|
||||
"data_transfers": "less_than_100gb",
|
||||
"jedi_migration": "yes",
|
||||
"num_software_systems": 1,
|
||||
"number_user_sessions": 2,
|
||||
"average_daily_traffic": 1,
|
||||
"engineering_assessment": "yes",
|
||||
"technical_support_team": "yes",
|
||||
"estimated_monthly_spend": 100,
|
||||
"expected_completion_date": "less_than_1_month",
|
||||
"rationalization_software_systems": "yes",
|
||||
"organization_providing_assistance": "in_house_staff"
|
||||
},
|
||||
"information_about_you": {
|
||||
"citizenship": "United States",
|
||||
"designation": "military",
|
||||
"phone_number": "1234567890",
|
||||
"email_request": user.email,
|
||||
"fname_request": user.first_name,
|
||||
"lname_request": user.last_name,
|
||||
"service_branch": "ads",
|
||||
"date_latest_training": "2018-08-06"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
class Meta:
|
||||
model = PENumber
|
||||
|
||||
|
||||
class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
class Meta:
|
||||
model = TaskOrder
|
||||
|
@ -1,8 +1,8 @@
|
||||
from tests.factories import RequestFactory, UserFactory
|
||||
|
||||
|
||||
MOCK_USER = UserFactory.create()
|
||||
MOCK_REQUEST = RequestFactory.create(
|
||||
MOCK_USER = UserFactory.build()
|
||||
MOCK_REQUEST = RequestFactory.build(
|
||||
creator=MOCK_USER.id,
|
||||
body={
|
||||
"financial_verification": {
|
||||
|
0
tests/models/__init__.py
Normal file
0
tests/models/__init__.py
Normal file
70
tests/models/test_requests.py
Normal file
70
tests/models/test_requests.py
Normal file
@ -0,0 +1,70 @@
|
||||
from tests.factories import RequestFactory, UserFactory
|
||||
from atst.domain.requests import Requests, RequestStatus
|
||||
|
||||
|
||||
def test_started_request_requires_mo_action():
|
||||
request = RequestFactory.create()
|
||||
assert Requests.action_required_by(request) == "mission_owner"
|
||||
|
||||
|
||||
def test_pending_financial_requires_mo_action():
|
||||
request = RequestFactory.create()
|
||||
request = Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION)
|
||||
|
||||
assert Requests.action_required_by(request) == "mission_owner"
|
||||
|
||||
|
||||
def test_pending_ccpo_approval_requires_ccpo():
|
||||
request = RequestFactory.create()
|
||||
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL)
|
||||
|
||||
assert Requests.action_required_by(request) == "ccpo"
|
||||
|
||||
|
||||
def test_request_has_creator():
|
||||
user = UserFactory.create()
|
||||
request = RequestFactory.create(creator=user)
|
||||
|
||||
assert request.creator == user
|
||||
|
||||
|
||||
def test_request_status_started_displayname():
|
||||
request = RequestFactory.create()
|
||||
request = Requests.set_status(request, RequestStatus.STARTED)
|
||||
|
||||
assert request.status_displayname == "Started"
|
||||
|
||||
|
||||
def test_request_status_pending_financial_displayname():
|
||||
request = RequestFactory.create()
|
||||
request = Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION)
|
||||
|
||||
assert request.status_displayname == "Pending Financial Verification"
|
||||
|
||||
|
||||
def test_request_status_pending_ccpo_displayname():
|
||||
request = RequestFactory.create()
|
||||
request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL)
|
||||
|
||||
assert request.status_displayname == "Pending CCPO Approval"
|
||||
|
||||
|
||||
def test_request_status_pending_approved_displayname():
|
||||
request = RequestFactory.create()
|
||||
request = Requests.set_status(request, RequestStatus.APPROVED)
|
||||
|
||||
assert request.status_displayname == "Approved"
|
||||
|
||||
|
||||
def test_request_status_pending_expired_displayname():
|
||||
request = RequestFactory.create()
|
||||
request = Requests.set_status(request, RequestStatus.EXPIRED)
|
||||
|
||||
assert request.status_displayname == "Expired"
|
||||
|
||||
|
||||
def test_request_status_pending_deleted_displayname():
|
||||
request = RequestFactory.create()
|
||||
request = Requests.set_status(request, RequestStatus.DELETED)
|
||||
|
||||
assert request.status_displayname == "Deleted"
|
@ -61,11 +61,9 @@ class TestPENumberInForm:
|
||||
assert response.status_code == 302
|
||||
assert "/requests/financial_verification_submitted" in response.headers.get("Location")
|
||||
|
||||
def test_submit_request_form_with_new_valid_pe_id(self, session, monkeypatch, client):
|
||||
def test_submit_request_form_with_new_valid_pe_id(self, monkeypatch, client):
|
||||
self._set_monkeypatches(monkeypatch)
|
||||
pe = PENumberFactory.create(number="8675309U", description="sample PE number")
|
||||
session.add(pe)
|
||||
session.commit()
|
||||
|
||||
data = dict(self.required_data)
|
||||
data['pe_id'] = pe.number
|
||||
@ -74,3 +72,14 @@ class TestPENumberInForm:
|
||||
|
||||
assert response.status_code == 302
|
||||
assert "/requests/financial_verification_submitted" in response.headers.get("Location")
|
||||
|
||||
def test_submit_request_form_with_missing_pe_id(self, monkeypatch, client):
|
||||
self._set_monkeypatches(monkeypatch)
|
||||
|
||||
data = dict(self.required_data)
|
||||
data['pe_id'] = ''
|
||||
|
||||
response = self.submit_data(client, data)
|
||||
|
||||
assert "There were some errors, see below" in response.data.decode()
|
||||
assert response.status_code == 200
|
||||
|
@ -2,7 +2,8 @@ import re
|
||||
import pytest
|
||||
import urllib
|
||||
from tests.mocks import MOCK_USER, MOCK_REQUEST
|
||||
from tests.factories import RequestFactory
|
||||
from tests.factories import RequestFactory, UserFactory
|
||||
from atst.domain.roles import Roles
|
||||
|
||||
|
||||
ERROR_CLASS = "alert--error"
|
||||
@ -27,3 +28,41 @@ def test_submit_valid_request_form(monkeypatch, client, user_session):
|
||||
data="meaning=42",
|
||||
)
|
||||
assert "/requests/new/2" in response.headers.get("Location")
|
||||
|
||||
|
||||
def test_owner_can_view_request(client, user_session):
|
||||
user = UserFactory.create()
|
||||
user_session(user)
|
||||
request = RequestFactory.create(creator=user)
|
||||
|
||||
response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_non_owner_cannot_view_request(client, user_session):
|
||||
user = UserFactory.create()
|
||||
user_session(user)
|
||||
request = RequestFactory.create()
|
||||
|
||||
response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_ccpo_can_view_request(client, user_session):
|
||||
ccpo = Roles.get("ccpo")
|
||||
user = UserFactory.create(atat_role=ccpo)
|
||||
user_session(user)
|
||||
request = RequestFactory.create()
|
||||
|
||||
response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_nonexistent_request(client, user_session):
|
||||
user_session()
|
||||
response = client.get("/requests/new/1/foo", follow_redirects=True)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
@ -27,8 +27,7 @@ def test_successful_login_redirect(client, monkeypatch):
|
||||
def test_unsuccessful_login_redirect(client, monkeypatch):
|
||||
resp = client.get("/login-redirect")
|
||||
|
||||
assert resp.status_code == 302
|
||||
assert "unauthorized" in resp.headers["Location"]
|
||||
assert resp.status_code == 401
|
||||
assert "user_id" not in session
|
||||
|
||||
|
||||
@ -55,7 +54,6 @@ def test_routes_are_protected(client, app):
|
||||
|
||||
|
||||
UNPROTECTED_ROUTES = ["/", "/login-dev", "/login-redirect", "/unauthorized"]
|
||||
|
||||
# this implicitly relies on the test config and test CRL in tests/fixtures/crl
|
||||
|
||||
|
||||
@ -72,8 +70,7 @@ def test_crl_validation_on_login(client):
|
||||
"HTTP_X_SSL_CLIENT_CERT": bad_cert.decode(),
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert "unauthorized" in resp.headers["Location"]
|
||||
assert resp.status_code == 401
|
||||
assert "user_id" not in session
|
||||
|
||||
# good cert is not on the test CRL, passes
|
||||
|
Loading…
x
Reference in New Issue
Block a user