From 033578fa21ced05b2879cee299466c0798a439af Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 7 Aug 2018 15:16:28 -0400 Subject: [PATCH 001/101] implement database sessions for test factories --- tests/conftest.py | 23 +++++++++++++++------ tests/domain/test_pe_numbers.py | 16 ++------------ tests/domain/test_requests.py | 6 +----- tests/domain/test_task_orders.py | 16 ++------------ tests/factories.py | 19 ++++++++++++----- tests/mocks.py | 4 ++-- tests/routes/test_financial_verification.py | 4 +--- 7 files changed, 39 insertions(+), 49 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index bdc59286..c3add296 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,9 +6,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 +28,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 +44,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 +55,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() @@ -66,6 +75,7 @@ class DummyForm(dict): class DummyField(object): + def __init__(self, data=None, errors=(), raw_data=None): self.data = data self.errors = list(errors) @@ -81,10 +91,11 @@ def dummy_form(): def dummy_field(): return DummyField() + @pytest.fixture def user_session(monkeypatch): - def set_user_session(user = MOCK_USER): + def set_user_session(user=MOCK_USER): monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: user) return set_user_session diff --git a/tests/domain/test_pe_numbers.py b/tests/domain/test_pe_numbers.py index 60c410b5..98b90470 100644 --- a/tests/domain/test_pe_numbers.py +++ b/tests/domain/test_pe_numbers.py @@ -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 diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py index 901caf73..0597714a 100644 --- a/tests/domain/test_requests.py +++ b/tests/domain/test_requests.py @@ -9,11 +9,7 @@ from tests.factories import RequestFactory @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): diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index 2f03a6d0..ba422bc5 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -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 diff --git a/tests/factories.py b/tests/factories.py index 11b11a1b..5cefc1eb 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -8,27 +8,36 @@ from atst.models.user import User from atst.models.role import Role -class RequestFactory(factory.Factory): +class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: model = Request id = factory.Sequence(lambda x: uuid4()) -class PENumberFactory(factory.Factory): + +class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: model = PENumber -class TaskOrderFactory(factory.Factory): + +class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory): + 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 diff --git a/tests/mocks.py b/tests/mocks.py index 1e44f96e..0307997e 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -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": { diff --git a/tests/routes/test_financial_verification.py b/tests/routes/test_financial_verification.py index 60f0f9b0..e15638df 100644 --- a/tests/routes/test_financial_verification.py +++ b/tests/routes/test_financial_verification.py @@ -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 From 52bd76e6ee4ee220697f6e4997a11d00364e639b Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 15:16:38 -0400 Subject: [PATCH 002/101] Use proper status names for requests --- atst/domain/requests.py | 22 ++++++++++++---------- tests/domain/test_requests.py | 11 ++++++++--- tests/models/__init__.py | 0 tests/models/test_requests.py | 5 +++++ 4 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 tests/models/__init__.py create mode 100644 tests/models/test_requests.py diff --git a/atst/domain/requests.py b/atst/domain/requests.py index 5c04d3c7..f9e83fe3 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -26,6 +26,12 @@ def deep_merge(source, destination: dict): return _deep_merge(source, dict(destination)) +class RequestStatuses(object): + @classmethod + def new(cls, status_name): + return RequestStatusEvent(new_status=status_name) + + class Requests(object): AUTO_APPROVE_THRESHOLD = 1000000 @@ -33,8 +39,7 @@ class Requests(object): 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) + request.status_events.append(RequestStatuses.new("started")) db.session.add(request) db.session.commit() @@ -74,10 +79,12 @@ class Requests(object): @classmethod def submit(cls, request): - request.status_events.append(RequestStatusEvent(new_status="submitted")) - if Requests.should_auto_approve(request): - request.status_events.append(RequestStatusEvent(new_status="approved")) + request.status_events.append( + RequestStatuses.new("pending_financial_verification") + ) + else: + request.status_events.append(RequestStatuses.new("pending_ccpo_approval")) db.session.add(request) db.session.commit() @@ -100,11 +107,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") diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py index 0597714a..14c4127e 100644 --- a/tests/domain/test_requests.py +++ b/tests/domain/test_requests.py @@ -23,22 +23,27 @@ def test_nonexistent_request_raises(): Requests.get(uuid4()) +def test_new_request_has_started_status(): + request = Requests.create(uuid4(), {}) + assert request.status == "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 == "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 == "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 == "pending_ccpo_approval" diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py new file mode 100644 index 00000000..245c587e --- /dev/null +++ b/tests/models/test_requests.py @@ -0,0 +1,5 @@ +from tests.factories import RequestFactory + + +def test_started_request_requires_mo_action(): + request = RequestFactory.create() From 0c9005aaf6b681d0997f7e445045e45fa8fd3305 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 16:22:24 -0400 Subject: [PATCH 003/101] Fix and refactor action_required_by --- atst/domain/requests.py | 41 ++++++++++++++++++++++++++--------- atst/models/request.py | 10 ++------- tests/factories.py | 9 +++++++- tests/models/test_requests.py | 16 ++++++++++++++ 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/atst/domain/requests.py b/atst/domain/requests.py index f9e83fe3..17ceb9b0 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -1,3 +1,4 @@ +from enum import Enum from sqlalchemy import exists, and_ from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.attributes import flag_modified @@ -26,10 +27,11 @@ def deep_merge(source, destination: dict): return _deep_merge(source, dict(destination)) -class RequestStatuses(object): - @classmethod - def new(cls, status_name): - return RequestStatusEvent(new_status=status_name) +class RequestStatus(Enum): + STARTED = "started" + PENDING_FINANCIAL_VERIFICATION = "pending_financial_verification" + PENDING_CCPO_APPROVAL = "pending_ccpo_approval" + APPROVED = "approved" class Requests(object): @@ -38,8 +40,7 @@ class Requests(object): @classmethod def create(cls, creator_id, body): request = Request(creator=creator_id, body=body) - - request.status_events.append(RequestStatuses.new("started")) + request = Requests.set_status(request, RequestStatus.STARTED) db.session.add(request) db.session.commit() @@ -79,12 +80,13 @@ class Requests(object): @classmethod def submit(cls, request): + new_status = None if Requests.should_auto_approve(request): - request.status_events.append( - RequestStatuses.new("pending_financial_verification") - ) + new_status = RequestStatus.PENDING_FINANCIAL_VERIFICATION else: - request.status_events.append(RequestStatuses.new("pending_ccpo_approval")) + new_status = RequestStatus.PENDING_CCPO_APPROVAL + + request = Requests.set_status(request, new_status) db.session.add(request) db.session.commit() @@ -114,6 +116,25 @@ 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.value) + request.status_events.append(status_event) + return request + + @classmethod + def action_required_by(cls, request): + try: + status = RequestStatus(request.status) + except ValueError: + return None + + return { + RequestStatus.STARTED: "mission_owner", + RequestStatus.PENDING_FINANCIAL_VERIFICATION: "mission_owner", + RequestStatus.PENDING_CCPO_APPROVAL: "ccpo" + }.get(status) + @classmethod def should_auto_approve(cls, request): try: diff --git a/atst/models/request.py b/atst/models/request.py index e8690332..36b9b77c 100644 --- a/atst/models/request.py +++ b/atst/models/request.py @@ -22,11 +22,5 @@ class Request(Base): 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 set_status(self, status): + self.status_events.append(status) diff --git a/tests/factories.py b/tests/factories.py index 5cefc1eb..65395479 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,19 +1,26 @@ import factory from uuid import uuid4 -from atst.models import Request +from atst.models import Request, RequestStatusEvent 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 +class RequestStatusFactory(factory.alchemy.SQLAlchemyModelFactory): + + class Meta: + model = RequestStatusEvent + + class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = Request id = factory.Sequence(lambda x: uuid4()) + status_events = factory.RelatedFactory(RequestStatusFactory, "request", new_status="started") class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory): diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py index 245c587e..a1990a35 100644 --- a/tests/models/test_requests.py +++ b/tests/models/test_requests.py @@ -1,5 +1,21 @@ from tests.factories import RequestFactory +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" From 49b9fca79399c2a5fb82dd94de5d7b52516a2062 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 16:41:47 -0400 Subject: [PATCH 004/101] Use Enum for request statuses --- atst/domain/requests.py | 13 +++---------- atst/models/request.py | 1 + atst/models/request_status_event.py | 14 +++++++++++--- tests/domain/test_requests.py | 9 +++++---- tests/factories.py | 14 +++++--------- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/atst/domain/requests.py b/atst/domain/requests.py index 17ceb9b0..4c1d7bef 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -1,9 +1,9 @@ -from enum import Enum from sqlalchemy import exists, and_ 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 @@ -27,13 +27,6 @@ def deep_merge(source, destination: dict): return _deep_merge(source, dict(destination)) -class RequestStatus(Enum): - STARTED = "started" - PENDING_FINANCIAL_VERIFICATION = "pending_financial_verification" - PENDING_CCPO_APPROVAL = "pending_ccpo_approval" - APPROVED = "approved" - - class Requests(object): AUTO_APPROVE_THRESHOLD = 1000000 @@ -118,7 +111,7 @@ class Requests(object): @classmethod def set_status(cls, request: Request, status: RequestStatus): - status_event = RequestStatusEvent(new_status=status.value) + status_event = RequestStatusEvent(new_status=status) request.status_events.append(status_event) return request diff --git a/atst/models/request.py b/atst/models/request.py index 36b9b77c..147d28f1 100644 --- a/atst/models/request.py +++ b/atst/models/request.py @@ -24,3 +24,4 @@ class Request(Base): def set_status(self, status): self.status_events.append(status) + diff --git a/atst/models/request_status_event.py b/atst/models/request_status_event.py index 4f65a2ec..7eec2716 100644 --- a/atst/models/request_status_event.py +++ b/atst/models/request_status_event.py @@ -1,5 +1,6 @@ -from sqlalchemy import Column, func, ForeignKey -from sqlalchemy.types import DateTime, String, BigInteger +from enum import Enum, auto +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,18 @@ from atst.models import Base from atst.models.types import Id +class RequestStatus(Enum): + STARTED = auto() + PENDING_FINANCIAL_VERIFICATION = auto() + PENDING_CCPO_APPROVAL = auto() + APPROVED = auto() + + 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") diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py index 14c4127e..1d696097 100644 --- a/tests/domain/test_requests.py +++ b/tests/domain/test_requests.py @@ -3,6 +3,7 @@ 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 @@ -25,25 +26,25 @@ def test_nonexistent_request_raises(): def test_new_request_has_started_status(): request = Requests.create(uuid4(), {}) - assert request.status == "started" + 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 == "pending_financial_verification" + 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 == "pending_ccpo_approval" + 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 == "pending_ccpo_approval" + assert request.status == RequestStatus.PENDING_CCPO_APPROVAL diff --git a/tests/factories.py b/tests/factories.py index 65395479..dc68eb5c 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,7 +1,8 @@ import factory from uuid import uuid4 -from atst.models import Request, RequestStatusEvent +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 @@ -9,34 +10,31 @@ from atst.models.role import Role class RequestStatusFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: model = RequestStatusEvent class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: model = Request id = factory.Sequence(lambda x: uuid4()) - status_events = factory.RelatedFactory(RequestStatusFactory, "request", new_status="started") + status_events = factory.RelatedFactory( + RequestStatusFactory, "request", new_status=RequestStatus.STARTED + ) class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: model = PENumber class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: model = TaskOrder class RoleFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: model = Role @@ -44,7 +42,6 @@ class RoleFactory(factory.alchemy.SQLAlchemyModelFactory): class UserFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: model = User @@ -53,4 +50,3 @@ class UserFactory(factory.alchemy.SQLAlchemyModelFactory): first_name = "Fake" last_name = "User" atat_role = factory.SubFactory(RoleFactory) - From 5381e68a8d245b7487389ea8e6d6e8e43e8295ab Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 17:01:13 -0400 Subject: [PATCH 005/101] Add a migration to convert request statuses --- .../77b065750596_new_request_statuses.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 alembic/versions/77b065750596_new_request_statuses.py diff --git a/alembic/versions/77b065750596_new_request_statuses.py b/alembic/versions/77b065750596_new_request_statuses.py new file mode 100644 index 00000000..28cce335 --- /dev/null +++ b/alembic/versions/77b065750596_new_request_statuses.py @@ -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 ValueError: + query = sa.text("DELETE FROM request_status_events WHERE id = :id") + db.execute(query, id=status_event["id"]) + + +def downgrade(): + pass From e437b0d6baf0ebcefc71ccdf88efbd6541025027 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 17:07:25 -0400 Subject: [PATCH 006/101] str -> RequestStatus coercion is no longer necessary --- atst/domain/requests.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/atst/domain/requests.py b/atst/domain/requests.py index 4c1d7bef..e7b64e5d 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -117,16 +117,11 @@ class Requests(object): @classmethod def action_required_by(cls, request): - try: - status = RequestStatus(request.status) - except ValueError: - return None - return { RequestStatus.STARTED: "mission_owner", RequestStatus.PENDING_FINANCIAL_VERIFICATION: "mission_owner", RequestStatus.PENDING_CCPO_APPROVAL: "ccpo" - }.get(status) + }.get(request.status) @classmethod def should_auto_approve(cls, request): From cf9fc7efbc776c77debb99e8bf1878c60bab0b44 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 8 Aug 2018 09:41:42 -0400 Subject: [PATCH 007/101] use "prod" for production environment --- atst/app.py | 2 +- script/sync-crls | 2 +- script/sync-dod-certs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/atst/app.py b/atst/app.py index 4b7a0806..c86ab9bf 100644 --- a/atst/app.py +++ b/atst/app.py @@ -48,7 +48,7 @@ def make_app(config): 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) diff --git a/script/sync-crls b/script/sync-crls index 93ec6772..3c02ac93 100755 --- a/script/sync-crls +++ b/script/sync-crls @@ -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 diff --git a/script/sync-dod-certs b/script/sync-dod-certs index 043629c1..9d7263d8 100755 --- a/script/sync-dod-certs +++ b/script/sync-dod-certs @@ -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 From 45392a81196a3538051b410d452e82bfbc764280 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 8 Aug 2018 09:43:29 -0400 Subject: [PATCH 008/101] specify domain for production session cookies --- config/prod.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/config/prod.ini b/config/prod.ini index fbaaa394..bbbf8f8b 100644 --- a/config/prod.ini +++ b/config/prod.ini @@ -1,2 +1,3 @@ [default] SESSION_COOKIE_SECURE=True +SESSION_COOKIE_DOMAIN=atat.codes From ee4458edd1d1213832890b2d93607a3c4f4515db Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 8 Aug 2018 09:49:55 -0400 Subject: [PATCH 009/101] alphabetize base config --- config/base.ini | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/config/base.ini b/config/base.ini index f256fa3a..749b9b2b 100644 --- a/config/base.ini +++ b/config/base.ini @@ -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 From ba432e8891910a7d0a9de8e48cdb2fa4b90bcd9f Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Thu, 5 Jul 2018 17:56:14 -0400 Subject: [PATCH 010/101] Add initial kubernetes test file --- kubernetes/atst.yml | 67 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 kubernetes/atst.yml diff --git a/kubernetes/atst.yml b/kubernetes/atst.yml new file mode 100644 index 00000000..94743aec --- /dev/null +++ b/kubernetes/atst.yml @@ -0,0 +1,67 @@ +--- +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: + containers: + - name: atst + image: registry.atat.codes:443/atst-prod:c06b0f6 + ports: + - containerPort: 8000 + imagePullSecrets: + - name: regcred +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: atst + name: atst + namespace: atat +spec: + ports: + - name: "80" + port: 80 + targetPort: 8000 + 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 From c756afec8e03ef9d389b077166a04522d651b3bd Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 25 Jul 2018 10:44:53 -0400 Subject: [PATCH 011/101] Move kubernetes directory --- {kubernetes => deploy/kubernetes}/atst.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {kubernetes => deploy/kubernetes}/atst.yml (100%) diff --git a/kubernetes/atst.yml b/deploy/kubernetes/atst.yml similarity index 100% rename from kubernetes/atst.yml rename to deploy/kubernetes/atst.yml From 87f3c1117ed02a58be98ee304aaf0dba0afd4a06 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Fri, 27 Jul 2018 10:55:56 -0400 Subject: [PATCH 012/101] Add second ingress for atst that is passthrough Used for CAC auth so SSL termination and client cert validation can be done by the app --- deploy/kubernetes/atst.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index 94743aec..c7152663 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -65,3 +65,21 @@ spec: backend: serviceName: atst servicePort: 80 +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: atst-cac + namespace: atat + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: 10m + ingress.kubernetes.io/ssl-passthrough: "true" +spec: + rules: + - host: cac.atat.codes + http: + paths: + - path: / + backend: + serviceName: atst + servicePort: 443 From 91d56fff0e982c8d696ed6dc9fcfe91533b5f234 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Mon, 6 Aug 2018 11:15:19 -0400 Subject: [PATCH 013/101] Switch client cert checking subdomain to auth (from cac) --- deploy/kubernetes/atst.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index c7152663..33c0f6eb 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -67,16 +67,17 @@ spec: servicePort: 80 --- apiVersion: extensions/v1beta1 +apiVersion: extensions/v1beta1 kind: Ingress metadata: - name: atst-cac + name: atst-auth namespace: atat annotations: nginx.ingress.kubernetes.io/proxy-body-size: 10m ingress.kubernetes.io/ssl-passthrough: "true" spec: rules: - - host: cac.atat.codes + - host: auth.atat.codes http: paths: - path: / From ea853a7b28834da5d6cc24c8d9a08d07db5dfd90 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 10:51:24 -0400 Subject: [PATCH 014/101] Add configmap for ATST settings and env vars --- deploy/kubernetes/atst-configmap.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 deploy/kubernetes/atst-configmap.yml diff --git a/deploy/kubernetes/atst-configmap.yml b/deploy/kubernetes/atst-configmap.yml new file mode 100644 index 00000000..4953bfe9 --- /dev/null +++ b/deploy/kubernetes/atst-configmap.yml @@ -0,0 +1,22 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: atst-config + namespace: atat +data: + uwsgi-config: |- + [uwsgi] + module = main + callable = app + socket = /var/run/uwsgi/uwsgi.socket + plugins = python3 + atst-config: |- + [default] + CAC_URL=https://auth.atat.codes + COOKIE_SECRET=a12c87558f85566f846ea53fd3f1611dd207d71677966d4d04a8e59d4d8c6737 + ENVIRONMENT=production + PGHOST=postgres-master.atat.svc.cluster.local + REDIS_URI=redis://redis-master.atat.svc.cluster.local:6379 + SECRET=92d2ef2aedf518e1e04a2a99445e6649539a91f06c977f3d69980c63e4e0fb45 + SECRET_KEY=beb178f9e4e83066ec0baa471cea36151f26bd3779902ae2c24eb5bb66e28c15 From 9262c0f346198375d0dc7cf775c8801c627e22d7 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 10:51:55 -0400 Subject: [PATCH 015/101] Add configmap for nginx settings and config --- deploy/kubernetes/atst-nginx-configmap.yml | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 deploy/kubernetes/atst-nginx-configmap.yml diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml new file mode 100644 index 00000000..47b6d5b0 --- /dev/null +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -0,0 +1,73 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: atst-nginx + namespace: atat +data: + htpasswd: | + atat:$apr1$D9ZQ6.AS$BwgIHxTMbRQsv4LbbAGAT/ + nginx-config: |- + server { + server_name www.atat.codes atat.codes; + listen 80 http2; + #if ($http_x_forwarded_proto != 'https') { + # return 301 https://$host$request_uri; + #} + location /login-dev { + auth_basic "Developer Access"; + auth_basic_user_file /etc/nginx/.htpasswd; + try_files $uri @app; + } + location / { + try_files $uri @app; + } + location @app { + include uwsgi_params; + uwsgi_pass unix:///var/run/atst_uwsgi.sock; + } + } + server { + server_name auth.atat.codes; + listen 443 ssl http2; + listen [::]:443 ssl http2 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/atst_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; + 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; + } + } From d7cf09decc37dc69a7bcd47d59af7bf7ff646a00 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 10:52:19 -0400 Subject: [PATCH 016/101] Add helperscript showing dhparam secret creation --- deploy/kubernetes/create_secret.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 deploy/kubernetes/create_secret.sh diff --git a/deploy/kubernetes/create_secret.sh b/deploy/kubernetes/create_secret.sh new file mode 100644 index 00000000..90ab3552 --- /dev/null +++ b/deploy/kubernetes/create_secret.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +kubectl -n atat create secret generic dhparam-4096 --from-file=./dhparam.pem From 2369d839e49d231743489f9c772cc24f0c362b09 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 10:53:19 -0400 Subject: [PATCH 017/101] Add nginx container and volume mounts --- deploy/kubernetes/atst.yml | 71 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index 33c0f6eb..9591db2a 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -23,10 +23,72 @@ spec: containers: - name: atst image: registry.atat.codes:443/atst-prod:c06b0f6 + volumeMounts: + - name: atst-config + mountPath: "/opt/atat" + - name: uswgi-socket-dir + mountPath: "/var/run/uwsgi" + - name: atst-nginx + image: nginx:alpine ports: - - containerPort: 8000 + - containerPort: 8080 + name: http + - containerPort: 8443 + name: http + volumeMounts: + - name: nginx-auth-tls + mountPath: "/etc/ssl/private" + - name: nginx-config + mountPath: "/etc/nginx/conf.d" + - name: nginx-dhparam + mountPath: "/etc/ssl" + - name: nginx-htpasswd + mountPath: "/etc/nginx" + - name: uswgi-socket-dir + mountPath: "/var/run/uwsgi" imagePullSecrets: - name: regcred + volumes: + - name: atst-config + configMap: + name: atst + items: + - key: atst-config + 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.crt + 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 + configMap: + name: atst-nginx + items: + - key: httpasswd + path: .htpasswd + mode: 0640 + - name: uswgi-socket-dir + emptyDir: + medium: Memory --- apiVersion: v1 kind: Service @@ -37,9 +99,12 @@ metadata: namespace: atat spec: ports: - - name: "80" + - name: "http" port: 80 - targetPort: 8000 + targetPort: 8080 + - name: "https" + port: 443 + targetPort: 8443 selector: app: atst --- From 525a629993c1c0a71db510c8f67e9c40409a9367 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 10:58:01 -0400 Subject: [PATCH 018/101] Copy over virtualenv but not deploy dir --- .dockerignore | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index e9ff2bac..8bad9296 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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 From 319ac897a72819efb53c043bf3f3fec6f0097465 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 10:58:24 -0400 Subject: [PATCH 019/101] Update entry to launch uwsgi server --- deploy/docker/prod/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/docker/prod/Dockerfile b/deploy/docker/prod/Dockerfile index ea4bdbcc..cad745fa 100644 --- a/deploy/docker/prod/Dockerfile +++ b/deploy/docker/prod/Dockerfile @@ -21,7 +21,7 @@ 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 ############################################# From cffb99ca2aae020aead15a3ad1b50d2fd10d4cd3 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 11:00:12 -0400 Subject: [PATCH 020/101] Update to latest scriptz master --- script/include | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/include b/script/include index 8cf96c97..7b768d8e 160000 --- a/script/include +++ b/script/include @@ -1 +1 @@ -Subproject commit 8cf96c9776e7fd73c11d57160d26fc1715bf00da +Subproject commit 7b768d8e19f1b475553eecbb280b318ebf85a66c From f17fe77e4d76835c91a5e8cab1932fee0f83a005 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 11:41:20 -0400 Subject: [PATCH 021/101] Rearrange keys (alpha) --- deploy/kubernetes/atst-configmap.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy/kubernetes/atst-configmap.yml b/deploy/kubernetes/atst-configmap.yml index 4953bfe9..21049ee6 100644 --- a/deploy/kubernetes/atst-configmap.yml +++ b/deploy/kubernetes/atst-configmap.yml @@ -5,12 +5,6 @@ metadata: name: atst-config namespace: atat data: - uwsgi-config: |- - [uwsgi] - module = main - callable = app - socket = /var/run/uwsgi/uwsgi.socket - plugins = python3 atst-config: |- [default] CAC_URL=https://auth.atat.codes @@ -20,3 +14,9 @@ data: REDIS_URI=redis://redis-master.atat.svc.cluster.local:6379 SECRET=92d2ef2aedf518e1e04a2a99445e6649539a91f06c977f3d69980c63e4e0fb45 SECRET_KEY=beb178f9e4e83066ec0baa471cea36151f26bd3779902ae2c24eb5bb66e28c15 + uwsgi-config: |- + [uwsgi] + module = main + callable = app + socket = /var/run/uwsgi/uwsgi.socket + plugins = python3 From 2eeb492c5ca7f15c1fcede5665a647bc13e33bdc Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 11:42:51 -0400 Subject: [PATCH 022/101] Fix types; add uwsgi config; add env vars --- deploy/kubernetes/atst.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index 9591db2a..8918bb59 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -23,10 +23,15 @@ spec: containers: - name: atst image: registry.atat.codes:443/atst-prod:c06b0f6 + envFrom: + - configMapRef: + name: atst-envvars volumeMounts: - name: atst-config - mountPath: "/opt/atat" - - name: uswgi-socket-dir + mountPath: "/opt/atat/atst" + - name: uwsgi-config + mountPath: "/opt/atat/atst" + - name: uwsgi-socket-dir mountPath: "/var/run/uwsgi" - name: atst-nginx image: nginx:alpine @@ -44,7 +49,7 @@ spec: mountPath: "/etc/ssl" - name: nginx-htpasswd mountPath: "/etc/nginx" - - name: uswgi-socket-dir + - name: uwsgi-socket-dir mountPath: "/var/run/uwsgi" imagePullSecrets: - name: regcred @@ -86,7 +91,14 @@ spec: - key: httpasswd path: .htpasswd mode: 0640 - - name: uswgi-socket-dir + - name: uwsgi-config + configMap: + name: atst-config + items: + - key: uwsgi-config + path: uwsgi-config.ini + mode: 0644 + - name: uwsgi-socket-dir emptyDir: medium: Memory --- From a86eb405ead72c924565a404dfe073dce505b72b Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 11:43:33 -0400 Subject: [PATCH 023/101] Add configmap for atst env vars --- deploy/kubernetes/atst-envvars-configmap.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 deploy/kubernetes/atst-envvars-configmap.yml diff --git a/deploy/kubernetes/atst-envvars-configmap.yml b/deploy/kubernetes/atst-envvars-configmap.yml new file mode 100644 index 00000000..7e6df66c --- /dev/null +++ b/deploy/kubernetes/atst-envvars-configmap.yml @@ -0,0 +1,9 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: atst-envvars + namespace: atat +data: + OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst-overrides.ini + UWSGI_CONFIG_FULLPATH: /opt/atat/uwsgi-config.ini From f0a84ceb8ab2262785110f7fe1a7366502dc8e80 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 11:44:38 -0400 Subject: [PATCH 024/101] Add postgres client libs to containers --- script/alpine_setup | 3 +++ 1 file changed, 3 insertions(+) diff --git a/script/alpine_setup b/script/alpine_setup index 28f836c2..70b3922d 100755 --- a/script/alpine_setup +++ b/script/alpine_setup @@ -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" + # Run the shared alpine setup script source ./script/include/run_alpine_setup From e17c4ca0a272624481587618052fb741c81942fe Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 11:44:57 -0400 Subject: [PATCH 025/101] Add uwsgi server launching script --- script/uwsgi_server | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100755 script/uwsgi_server diff --git a/script/uwsgi_server b/script/uwsgi_server new file mode 100755 index 00000000..e9633892 --- /dev/null +++ b/script/uwsgi_server @@ -0,0 +1,8 @@ +#!/bin/bash + +# script/uwsgi_server: Launch the UWSGI server + +source "$(dirname "${0}")"/../script/include/global_header.inc.sh + +# Launch UWSGI +run_command "uwsgi --ini ${UWSGI_CONFIG_FULLPATH}" From 609719025fa2a341eb4ec7904d45df4087719f64 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 12:17:09 -0400 Subject: [PATCH 026/101] Add FLASK_ENV var --- deploy/kubernetes/atst-envvars-configmap.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/kubernetes/atst-envvars-configmap.yml b/deploy/kubernetes/atst-envvars-configmap.yml index 7e6df66c..06bdf0ee 100644 --- a/deploy/kubernetes/atst-envvars-configmap.yml +++ b/deploy/kubernetes/atst-envvars-configmap.yml @@ -5,5 +5,6 @@ metadata: name: atst-envvars namespace: atat data: + FLASK_ENV: prod OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst-overrides.ini UWSGI_CONFIG_FULLPATH: /opt/atat/uwsgi-config.ini From f2a4d59e0ae23613f3315d8c3fff5b05a4652650 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 13:40:23 -0400 Subject: [PATCH 027/101] Add uwsgi packages to container --- script/alpine_setup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/alpine_setup b/script/alpine_setup index 70b3922d..e95d3134 100755 --- a/script/alpine_setup +++ b/script/alpine_setup @@ -10,7 +10,7 @@ APP_USER="atst" APP_UID="8010" # Add additional packages required by app dependencies -ADDITIONAL_PACKAGES="postgresql-libs" +ADDITIONAL_PACKAGES="postgresql-libs uwsgi uwsgi-python3" # Run the shared alpine setup script source ./script/include/run_alpine_setup From c995a232c6e13d4de308ea80fb9fb1523849c97b Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 13:41:38 -0400 Subject: [PATCH 028/101] Add local/bin files to the container so pipenv works --- deploy/docker/prod/Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deploy/docker/prod/Dockerfile b/deploy/docker/prod/Dockerfile index cad745fa..480812c6 100644 --- a/deploy/docker/prod/Dockerfile +++ b/deploy/docker/prod/Dockerfile @@ -7,12 +7,12 @@ ARG APP_USER=atst ARG APP_GROUP=atat ARG APP_DIR=/opt/atat/atst ARG APP_PORT=8000 +ARG LOCAL_BIN_DIR=/usr/local/bin ARG SITE_PACKAGES_DIR=/usr/local/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}" @@ -28,6 +28,9 @@ CMD ["bash", "-c", "${APP_DIR}/script/uwsgi_server"] # 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}" From 91857d7779d8439f4729911b7ed11ddde757c976 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 15:14:32 -0400 Subject: [PATCH 029/101] Switch to generic alpine for base image and fix python dirs --- deploy/docker/prod/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/docker/prod/Dockerfile b/deploy/docker/prod/Dockerfile index 480812c6..37a36859 100644 --- a/deploy/docker/prod/Dockerfile +++ b/deploy/docker/prod/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.6.5-alpine +FROM alpine:3.8 ### Very low chance of changing ############################### @@ -7,8 +7,8 @@ ARG APP_USER=atst ARG APP_GROUP=atat ARG APP_DIR=/opt/atat/atst ARG APP_PORT=8000 -ARG LOCAL_BIN_DIR=/usr/local/bin -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}" From 6e6dddb507ba9ccef4c976feb6d4d109b6ada582 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 15:14:55 -0400 Subject: [PATCH 030/101] Fix module name and set venv location --- deploy/kubernetes/atst-configmap.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/kubernetes/atst-configmap.yml b/deploy/kubernetes/atst-configmap.yml index 21049ee6..a8c8a11e 100644 --- a/deploy/kubernetes/atst-configmap.yml +++ b/deploy/kubernetes/atst-configmap.yml @@ -16,7 +16,8 @@ data: SECRET_KEY=beb178f9e4e83066ec0baa471cea36151f26bd3779902ae2c24eb5bb66e28c15 uwsgi-config: |- [uwsgi] - module = main callable = app + module = app socket = /var/run/uwsgi/uwsgi.socket plugins = python3 + virtualenv = /opt/atat/atst/.venv From d1f99765e0aad8ccf6db03a989b5172ff477571d Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 15:15:14 -0400 Subject: [PATCH 031/101] Add python3 package --- script/alpine_setup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/alpine_setup b/script/alpine_setup index e95d3134..b9eeb9a7 100755 --- a/script/alpine_setup +++ b/script/alpine_setup @@ -10,7 +10,7 @@ APP_USER="atst" APP_UID="8010" # Add additional packages required by app dependencies -ADDITIONAL_PACKAGES="postgresql-libs uwsgi uwsgi-python3" +ADDITIONAL_PACKAGES="postgresql-libs python3 uwsgi uwsgi-python3" # Run the shared alpine setup script source ./script/include/run_alpine_setup From c7c812be05bb7090d8623c054233ca15f1cd8a82 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 15:40:00 -0400 Subject: [PATCH 032/101] Fix collisions and typos --- deploy/kubernetes/atst.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index 8918bb59..abba5086 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -22,15 +22,15 @@ spec: spec: containers: - name: atst - image: registry.atat.codes:443/atst-prod:c06b0f6 + image: registry.atat.codes:443/atst-prod:cc680fa envFrom: - configMapRef: name: atst-envvars volumeMounts: - name: atst-config - mountPath: "/opt/atat/atst" + mountPath: "/opt/atat/atst/atst-overrides.ini" - name: uwsgi-config - mountPath: "/opt/atat/atst" + mountPath: "/opt/atat/atst/uwsgi-config.ini" - name: uwsgi-socket-dir mountPath: "/var/run/uwsgi" - name: atst-nginx @@ -39,7 +39,7 @@ spec: - containerPort: 8080 name: http - containerPort: 8443 - name: http + name: https volumeMounts: - name: nginx-auth-tls mountPath: "/etc/ssl/private" @@ -56,7 +56,7 @@ spec: volumes: - name: atst-config configMap: - name: atst + name: atst-config items: - key: atst-config path: atst-overrides.ini @@ -88,7 +88,7 @@ spec: configMap: name: atst-nginx items: - - key: httpasswd + - key: htpasswd path: .htpasswd mode: 0640 - name: uwsgi-config From fb155fbc614e94aa41cf34957b4ea75f492d9471 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 15:40:12 -0400 Subject: [PATCH 033/101] Switch nginx listeners to proper ports --- deploy/kubernetes/atst-nginx-configmap.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index 47b6d5b0..e7ac0bb5 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -10,7 +10,7 @@ data: nginx-config: |- server { server_name www.atat.codes atat.codes; - listen 80 http2; + listen 8080 http2; #if ($http_x_forwarded_proto != 'https') { # return 301 https://$host$request_uri; #} @@ -29,7 +29,7 @@ data: } server { server_name auth.atat.codes; - listen 443 ssl http2; + listen 8443 ssl http2; listen [::]:443 ssl http2 ipv6only=on; # SSL server certificate and private key ssl_certificate /etc/ssl/private/auth.atat.crt From a53c480b58574d0e74f7d3f60dfb64d1c972380c Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 15:47:31 -0400 Subject: [PATCH 034/101] Add missing semicolons --- deploy/kubernetes/atst-nginx-configmap.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index e7ac0bb5..70b91042 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -32,8 +32,8 @@ data: listen 8443 ssl http2; listen [::]:443 ssl http2 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 + 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'; From 97569985a865f8dbff166a70ed19326ae3da68cc Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 16:01:59 -0400 Subject: [PATCH 035/101] Fix mount paths and typos --- deploy/kubernetes/atst.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index abba5086..34d05fe2 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -29,8 +29,10 @@ spec: 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 @@ -44,11 +46,14 @@ spec: - name: nginx-auth-tls mountPath: "/etc/ssl/private" - name: nginx-config - mountPath: "/etc/nginx/conf.d" + mountPath: "/etc/nginx/conf.d/atst.conf" + subPath: atst.conf - name: nginx-dhparam mountPath: "/etc/ssl" + subPath: dhparam.pem - name: nginx-htpasswd - mountPath: "/etc/nginx" + mountPath: "/etc/nginx/.htpasswd" + subPath: .htpasswd - name: uwsgi-socket-dir mountPath: "/var/run/uwsgi" imagePullSecrets: @@ -69,7 +74,7 @@ spec: path: auth.atat.crt mode: 0644 - key: tls.key - path: auth.atat.crt + path: auth.atat.key mode: 0640 - name: nginx-config configMap: From 23d6f6bf5290915180f9baad75759ad605e8b12a Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 16:38:59 -0400 Subject: [PATCH 036/101] Update uwsgi socket location --- deploy/kubernetes/atst-nginx-configmap.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index 70b91042..50bc622b 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -24,7 +24,7 @@ data: } location @app { include uwsgi_params; - uwsgi_pass unix:///var/run/atst_uwsgi.sock; + uswgi_pass unix:///var/run/uwsgi/uwsgi.socket } } server { @@ -62,7 +62,7 @@ data: } location @app { include uwsgi_params; - uwsgi_pass unix:///var/run/atst_uwsgi.sock; + uswgi_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; From bddb59630e6cd180c896af0b1f0e1eddb46ecc17 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 16:40:42 -0400 Subject: [PATCH 037/101] Add missing semi-colons --- deploy/kubernetes/atst-nginx-configmap.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index 50bc622b..2d6a8eb6 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -24,7 +24,7 @@ data: } location @app { include uwsgi_params; - uswgi_pass unix:///var/run/uwsgi/uwsgi.socket + uswgi_pass unix:///var/run/uwsgi/uwsgi.socket; } } server { @@ -62,7 +62,7 @@ data: } location @app { include uwsgi_params; - uswgi_pass unix:///var/run/uwsgi/uwsgi.socket + uswgi_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; From 35be729378b3e86b5473f7b66c55beaa913ce59b Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 16:41:08 -0400 Subject: [PATCH 038/101] Update config file paths --- deploy/kubernetes/atst-envvars-configmap.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/kubernetes/atst-envvars-configmap.yml b/deploy/kubernetes/atst-envvars-configmap.yml index 06bdf0ee..23a25061 100644 --- a/deploy/kubernetes/atst-envvars-configmap.yml +++ b/deploy/kubernetes/atst-envvars-configmap.yml @@ -6,5 +6,5 @@ metadata: namespace: atat data: FLASK_ENV: prod - OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst-overrides.ini - UWSGI_CONFIG_FULLPATH: /opt/atat/uwsgi-config.ini + OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst/atst-overrides.ini + UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi-config.ini From 10efcb98b8ec881a59b8014e8d59c572278586fc Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 16:41:25 -0400 Subject: [PATCH 039/101] Fix mountPath to be file not directory --- deploy/kubernetes/atst.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index 34d05fe2..3c552854 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -49,7 +49,7 @@ spec: mountPath: "/etc/nginx/conf.d/atst.conf" subPath: atst.conf - name: nginx-dhparam - mountPath: "/etc/ssl" + mountPath: "/etc/ssl/dhparam.pem" subPath: dhparam.pem - name: nginx-htpasswd mountPath: "/etc/nginx/.htpasswd" From 55c08d11881e11aa5748d9e95777e1ac81dffa80 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 16:43:00 -0400 Subject: [PATCH 040/101] Fix typo --- deploy/kubernetes/atst-nginx-configmap.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index 2d6a8eb6..8f5a0d8a 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -24,7 +24,7 @@ data: } location @app { include uwsgi_params; - uswgi_pass unix:///var/run/uwsgi/uwsgi.socket; + uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket; } } server { @@ -62,7 +62,7 @@ data: } location @app { include uwsgi_params; - uswgi_pass unix:///var/run/uwsgi/uwsgi.socket; + 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; From 9cb5f88239932a9db48c381ab48102af40edb467 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 17:01:40 -0400 Subject: [PATCH 041/101] Move http traffic to port 8442 --- deploy/kubernetes/atst-nginx-configmap.yml | 5 +++-- deploy/kubernetes/atst.yml | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index 8f5a0d8a..2d7fbd93 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -10,7 +10,8 @@ data: nginx-config: |- server { server_name www.atat.codes atat.codes; - listen 8080 http2; + listen 8442 http2; + listen [::]:8442 http2 ipv6only=on; #if ($http_x_forwarded_proto != 'https') { # return 301 https://$host$request_uri; #} @@ -30,7 +31,7 @@ data: server { server_name auth.atat.codes; listen 8443 ssl http2; - listen [::]:443 ssl http2 ipv6only=on; + listen [::]:8443 ssl http2 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; diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index 3c552854..b26aa989 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -38,7 +38,7 @@ spec: - name: atst-nginx image: nginx:alpine ports: - - containerPort: 8080 + - containerPort: 8442 name: http - containerPort: 8443 name: https @@ -118,7 +118,7 @@ spec: ports: - name: "http" port: 80 - targetPort: 8080 + targetPort: 8442 - name: "https" port: 443 targetPort: 8443 From d74609798166a463da3026757a341e0e597f1d69 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 17:02:17 -0400 Subject: [PATCH 042/101] Allow nginx and atst socket access --- deploy/kubernetes/atst-configmap.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/kubernetes/atst-configmap.yml b/deploy/kubernetes/atst-configmap.yml index a8c8a11e..f3d428cb 100644 --- a/deploy/kubernetes/atst-configmap.yml +++ b/deploy/kubernetes/atst-configmap.yml @@ -21,3 +21,4 @@ data: socket = /var/run/uwsgi/uwsgi.socket plugins = python3 virtualenv = /opt/atat/atst/.venv + chmod-socket = 666 From b83b62aea0625abf7d3e11ca5b7ab7b9d34f8107 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Tue, 7 Aug 2018 18:05:57 -0400 Subject: [PATCH 043/101] Downgrade from http2 for testing --- deploy/kubernetes/atst-nginx-configmap.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index 2d7fbd93..ca80bec7 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -10,8 +10,8 @@ data: nginx-config: |- server { server_name www.atat.codes atat.codes; - listen 8442 http2; - listen [::]:8442 http2 ipv6only=on; + listen 8442; + listen [::]:8442 ipv6only=on; #if ($http_x_forwarded_proto != 'https') { # return 301 https://$host$request_uri; #} @@ -30,8 +30,8 @@ data: } server { server_name auth.atat.codes; - listen 8443 ssl http2; - listen [::]:8443 ssl http2 ipv6only=on; + 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; From e9c43f61e33b1847706bebd8567e295ba3a0cc18 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 08:27:38 -0400 Subject: [PATCH 044/101] Temp: remove basic auth for testing --- deploy/kubernetes/atst-nginx-configmap.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index ca80bec7..d210d7c3 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -15,11 +15,9 @@ data: #if ($http_x_forwarded_proto != 'https') { # return 301 https://$host$request_uri; #} - location /login-dev { - auth_basic "Developer Access"; - auth_basic_user_file /etc/nginx/.htpasswd; - try_files $uri @app; - } + #location /login-dev { + # try_files $uri @appbasicauth; + #} location / { try_files $uri @app; } @@ -27,6 +25,12 @@ data: 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; From f628460001ecf79c9ec2f77c8546879569f3601d Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 08:28:09 -0400 Subject: [PATCH 045/101] Rename reference script --- deploy/kubernetes/{create_secret.sh => set_dhparam_secret.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename deploy/kubernetes/{create_secret.sh => set_dhparam_secret.sh} (100%) diff --git a/deploy/kubernetes/create_secret.sh b/deploy/kubernetes/set_dhparam_secret.sh similarity index 100% rename from deploy/kubernetes/create_secret.sh rename to deploy/kubernetes/set_dhparam_secret.sh From 84b0bda701fbfc7400948949614e7d6c28c45223 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 08:28:52 -0400 Subject: [PATCH 046/101] Move atst ini config into secret --- deploy/kubernetes/atst-configmap.yml | 9 --------- deploy/kubernetes/set_atstconfig_secret.sh | 3 +++ 2 files changed, 3 insertions(+), 9 deletions(-) create mode 100644 deploy/kubernetes/set_atstconfig_secret.sh diff --git a/deploy/kubernetes/atst-configmap.yml b/deploy/kubernetes/atst-configmap.yml index f3d428cb..a9584fc5 100644 --- a/deploy/kubernetes/atst-configmap.yml +++ b/deploy/kubernetes/atst-configmap.yml @@ -5,15 +5,6 @@ metadata: name: atst-config namespace: atat data: - atst-config: |- - [default] - CAC_URL=https://auth.atat.codes - COOKIE_SECRET=a12c87558f85566f846ea53fd3f1611dd207d71677966d4d04a8e59d4d8c6737 - ENVIRONMENT=production - PGHOST=postgres-master.atat.svc.cluster.local - REDIS_URI=redis://redis-master.atat.svc.cluster.local:6379 - SECRET=92d2ef2aedf518e1e04a2a99445e6649539a91f06c977f3d69980c63e4e0fb45 - SECRET_KEY=beb178f9e4e83066ec0baa471cea36151f26bd3779902ae2c24eb5bb66e28c15 uwsgi-config: |- [uwsgi] callable = app diff --git a/deploy/kubernetes/set_atstconfig_secret.sh b/deploy/kubernetes/set_atstconfig_secret.sh new file mode 100644 index 00000000..926fa1a0 --- /dev/null +++ b/deploy/kubernetes/set_atstconfig_secret.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +kubectl -n atat create secret generic atst-config-ini --from-file=${1} From e1a49b2e729b9c703036eca9d09e9139c3ebc459 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 08:29:38 -0400 Subject: [PATCH 047/101] Switch auth.atat to direct nodeport service --- deploy/kubernetes/atst.yml | 43 +++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index b26aa989..bb3b19e0 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -22,7 +22,7 @@ spec: spec: containers: - name: atst - image: registry.atat.codes:443/atst-prod:cc680fa + image: registry.atat.codes:443/atst-prod:e9b6f76 envFrom: - configMapRef: name: atst-envvars @@ -116,12 +116,26 @@ metadata: namespace: atat spec: ports: - - name: "http" + - name: http port: 80 targetPort: 8442 - - name: "https" - port: 443 - targetPort: 8443 + 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 --- @@ -147,22 +161,3 @@ spec: backend: serviceName: atst servicePort: 80 ---- -apiVersion: extensions/v1beta1 -apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - name: atst-auth - namespace: atat - annotations: - nginx.ingress.kubernetes.io/proxy-body-size: 10m - ingress.kubernetes.io/ssl-passthrough: "true" -spec: - rules: - - host: auth.atat.codes - http: - paths: - - path: / - backend: - serviceName: atst - servicePort: 443 From 880b14574783e484133727b99649ed5cbb60410a Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 08:49:58 -0400 Subject: [PATCH 048/101] Make helper scripts executable --- deploy/kubernetes/set_atstconfig_secret.sh | 0 deploy/kubernetes/set_dhparam_secret.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 deploy/kubernetes/set_atstconfig_secret.sh mode change 100644 => 100755 deploy/kubernetes/set_dhparam_secret.sh diff --git a/deploy/kubernetes/set_atstconfig_secret.sh b/deploy/kubernetes/set_atstconfig_secret.sh old mode 100644 new mode 100755 diff --git a/deploy/kubernetes/set_dhparam_secret.sh b/deploy/kubernetes/set_dhparam_secret.sh old mode 100644 new mode 100755 From e5567bf3c3e035eaed1bf83ddf16dc41d38524d0 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 08:50:34 -0400 Subject: [PATCH 049/101] Switch to passing in the file name --- deploy/kubernetes/set_dhparam_secret.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/kubernetes/set_dhparam_secret.sh b/deploy/kubernetes/set_dhparam_secret.sh index 90ab3552..ac93d348 100755 --- a/deploy/kubernetes/set_dhparam_secret.sh +++ b/deploy/kubernetes/set_dhparam_secret.sh @@ -1,3 +1,3 @@ #!/bin/bash -kubectl -n atat create secret generic dhparam-4096 --from-file=./dhparam.pem +kubectl -n atat create secret generic dhparam-4096 --from-file=${1} From 3a377dcb11cc94c8861d66a6abdbc61e577c7703 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 08:55:35 -0400 Subject: [PATCH 050/101] Convert atst ini config into a secret --- deploy/kubernetes/atst.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index bb3b19e0..150518ae 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -60,10 +60,10 @@ spec: - name: regcred volumes: - name: atst-config - configMap: - name: atst-config + secret: + secretName: atst-config-ini items: - - key: atst-config + - key: atst-overrides.ini path: atst-overrides.ini mode: 0644 - name: nginx-auth-tls From 90970367c830572374b77275e8d71de3c3b9771b Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 08:56:07 -0400 Subject: [PATCH 051/101] Add yarn build after setup is completed --- script/setup | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/setup b/script/setup index 23a2fce3..f3b5b616 100755 --- a/script/setup +++ b/script/setup @@ -16,3 +16,5 @@ source ./script/include/run_setup # Fetch and import the PE numbers run_command "python script/ingest_pe_numbers.py" + +yarn build From bde339871a98227d1fe20555c424b9a6109ab2ee Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 09:57:30 -0400 Subject: [PATCH 052/101] Add delete secret before recreating --- deploy/kubernetes/set_atstconfig_secret.sh | 3 ++- deploy/kubernetes/set_dhparam_secret.sh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deploy/kubernetes/set_atstconfig_secret.sh b/deploy/kubernetes/set_atstconfig_secret.sh index 926fa1a0..0dcb90b0 100755 --- a/deploy/kubernetes/set_atstconfig_secret.sh +++ b/deploy/kubernetes/set_atstconfig_secret.sh @@ -1,3 +1,4 @@ #!/bin/bash -kubectl -n atat create secret generic atst-config-ini --from-file=${1} +kubectl -n atat delete secret atst-config-ini +kubectl -n atat create secret generic atst-config-ini --from-file="${1}" diff --git a/deploy/kubernetes/set_dhparam_secret.sh b/deploy/kubernetes/set_dhparam_secret.sh index ac93d348..dfc9401a 100755 --- a/deploy/kubernetes/set_dhparam_secret.sh +++ b/deploy/kubernetes/set_dhparam_secret.sh @@ -1,3 +1,4 @@ #!/bin/bash -kubectl -n atat create secret generic dhparam-4096 --from-file=${1} +kubectl -n atat delete secret dhparam-4096 +kubectl -n atat create secret generic dhparam-4096 --from-file="${1}" From 2822ff16c56eb82e2b97c90223d4eefdf98d76fb Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 09:58:01 -0400 Subject: [PATCH 053/101] Set FLASK_DEV to dev for staging --- deploy/kubernetes/atst-envvars-configmap.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/kubernetes/atst-envvars-configmap.yml b/deploy/kubernetes/atst-envvars-configmap.yml index 23a25061..e7bfec14 100644 --- a/deploy/kubernetes/atst-envvars-configmap.yml +++ b/deploy/kubernetes/atst-envvars-configmap.yml @@ -5,6 +5,6 @@ metadata: name: atst-envvars namespace: atat data: - FLASK_ENV: prod + FLASK_ENV: dev OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst/atst-overrides.ini UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi-config.ini From ae2c6b01ffb5dd8b6156ba577e07b083e6f52b36 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 09:58:25 -0400 Subject: [PATCH 054/101] Enable basic auth for /login-dev --- deploy/kubernetes/atst-nginx-configmap.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index d210d7c3..b30ff73d 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -15,9 +15,9 @@ data: #if ($http_x_forwarded_proto != 'https') { # return 301 https://$host$request_uri; #} - #location /login-dev { - # try_files $uri @appbasicauth; - #} + location /login-dev { + try_files $uri @appbasicauth; + } location / { try_files $uri @app; } @@ -28,8 +28,8 @@ data: location @appbasicauth { include uwsgi_params; uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket; - #auth_basic "Developer Access"; - #auth_basic_user_file /etc/nginx/.htpasswd; + auth_basic "Developer Access"; + auth_basic_user_file /etc/nginx/.htpasswd; } } server { From aecb310a9be0e3dbaf35ca589c4761317c5ea435 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 10:04:19 -0400 Subject: [PATCH 055/101] Set gid for mounted files to 101 (nginx) --- deploy/kubernetes/atst.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index 150518ae..842558dc 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -20,6 +20,8 @@ spec: labels: app: atst spec: + securityContext: + fsGroup: 101 containers: - name: atst image: registry.atat.codes:443/atst-prod:e9b6f76 From 3277386ae1d8802210752699ebb655df7c31a271 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 10:10:37 -0400 Subject: [PATCH 056/101] Add reference script for setting the htpasswd file contents --- deploy/kubernetes/set_htpasswd_secret.sh | 4 ++++ 1 file changed, 4 insertions(+) create mode 100755 deploy/kubernetes/set_htpasswd_secret.sh diff --git a/deploy/kubernetes/set_htpasswd_secret.sh b/deploy/kubernetes/set_htpasswd_secret.sh new file mode 100755 index 00000000..540048ca --- /dev/null +++ b/deploy/kubernetes/set_htpasswd_secret.sh @@ -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}" From 5c647a5c4126d64ca24908a213b34a412342b171 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 10:11:51 -0400 Subject: [PATCH 057/101] Switch htpasswd to use a secret --- deploy/kubernetes/atst-nginx-configmap.yml | 2 -- deploy/kubernetes/atst.yml | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index b30ff73d..eef7b377 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -5,8 +5,6 @@ metadata: name: atst-nginx namespace: atat data: - htpasswd: | - atat:$apr1$D9ZQ6.AS$BwgIHxTMbRQsv4LbbAGAT/ nginx-config: |- server { server_name www.atat.codes atat.codes; diff --git a/deploy/kubernetes/atst.yml b/deploy/kubernetes/atst.yml index 842558dc..c302f8af 100644 --- a/deploy/kubernetes/atst.yml +++ b/deploy/kubernetes/atst.yml @@ -92,8 +92,8 @@ spec: path: dhparam.pem mode: 0640 - name: nginx-htpasswd - configMap: - name: atst-nginx + secret: + secretName: atst-nginx-htpasswd items: - key: htpasswd path: .htpasswd From df6c563262bba6ae7b89ca012ad105f52b342a21 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 10:49:26 -0400 Subject: [PATCH 058/101] Enable redirects for login route and non-ssl traffic --- deploy/kubernetes/atst-nginx-configmap.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/deploy/kubernetes/atst-nginx-configmap.yml b/deploy/kubernetes/atst-nginx-configmap.yml index eef7b377..6e2b1d69 100644 --- a/deploy/kubernetes/atst-nginx-configmap.yml +++ b/deploy/kubernetes/atst-nginx-configmap.yml @@ -10,9 +10,12 @@ data: 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; - #} + 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; } From 22a7c53db66acaa166382b05b8e731adad994ef9 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 10:52:40 -0400 Subject: [PATCH 059/101] Fastforward script/include to most recent master --- script/include | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/include b/script/include index 7b768d8e..c44ca507 160000 --- a/script/include +++ b/script/include @@ -1 +1 @@ -Subproject commit 7b768d8e19f1b475553eecbb280b318ebf85a66c +Subproject commit c44ca5070da78fd522a2e485aaa225cc638e11d3 From fc7ef59e9bf8fc0589eb8cf626dcff648e75495e Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 8 Aug 2018 11:18:18 -0400 Subject: [PATCH 060/101] Remove unused Request.set_status --- atst/models/request.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/atst/models/request.py b/atst/models/request.py index 147d28f1..66e5bf1e 100644 --- a/atst/models/request.py +++ b/atst/models/request.py @@ -21,7 +21,3 @@ class Request(Base): @property def status(self): return self.status_events[-1].new_status - - def set_status(self, status): - self.status_events.append(status) - From d545fb9d957a3881c916f59a6ad848bd282f1ba7 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 11:43:38 -0400 Subject: [PATCH 061/101] Remove SASS gem install (now using parcel) --- script/setup | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/script/setup b/script/setup index f3b5b616..1e9c872e 100755 --- a/script/setup +++ b/script/setup @@ -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" @@ -17,4 +14,5 @@ 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 From b9e73b2f08890e9f052f708f8395a7424401be13 Mon Sep 17 00:00:00 2001 From: Devon Mackay Date: Wed, 8 Aug 2018 12:03:38 -0400 Subject: [PATCH 062/101] Add migrate db before server startup Ensures a fresh deploy properly updates the DB before starting the server and accepting traffic --- script/uwsgi_server | 3 +++ 1 file changed, 3 insertions(+) diff --git a/script/uwsgi_server b/script/uwsgi_server index e9633892..275b3b93 100755 --- a/script/uwsgi_server +++ b/script/uwsgi_server @@ -4,5 +4,8 @@ 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}" From 4d3889c14428b6e07b03007ade5bfd7df32fe428 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 8 Aug 2018 13:04:40 -0400 Subject: [PATCH 063/101] Hardcode enum values instead of using auto() --- atst/models/request_status_event.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/atst/models/request_status_event.py b/atst/models/request_status_event.py index 7eec2716..757d033d 100644 --- a/atst/models/request_status_event.py +++ b/atst/models/request_status_event.py @@ -1,4 +1,4 @@ -from enum import Enum, auto +from enum import Enum from sqlalchemy import Column, func, ForeignKey, Enum as SQLAEnum from sqlalchemy.types import DateTime, BigInteger from sqlalchemy.schema import Sequence @@ -9,10 +9,10 @@ from atst.models.types import Id class RequestStatus(Enum): - STARTED = auto() - PENDING_FINANCIAL_VERIFICATION = auto() - PENDING_CCPO_APPROVAL = auto() - APPROVED = auto() + STARTED = "started" + PENDING_FINANCIAL_VERIFICATION = "pending_financial_verification" + PENDING_CCPO_APPROVAL = "pending_ccpo_approval" + APPROVED = "approved" class RequestStatusEvent(Base): From 1f41b717bfcfade3971ab3391e254251634ba042 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 8 Aug 2018 13:05:03 -0400 Subject: [PATCH 064/101] Add expired and deleted statuses --- atst/models/request_status_event.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/atst/models/request_status_event.py b/atst/models/request_status_event.py index 757d033d..2e492ac4 100644 --- a/atst/models/request_status_event.py +++ b/atst/models/request_status_event.py @@ -13,6 +13,8 @@ class RequestStatus(Enum): PENDING_FINANCIAL_VERIFICATION = "pending_financial_verification" PENDING_CCPO_APPROVAL = "pending_ccpo_approval" APPROVED = "approved" + EXPIRED = "expired" + DELETED = "deleted" class RequestStatusEvent(Base): From e99ddd491aa3519904bf3c0ccc4a69bf7e19e738 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 21:03:27 -0400 Subject: [PATCH 065/101] Create Request.creator relationship - Rename creator_id to user_id --- .../05d6272bdb43_rename_request_creator_.py | 43 ++++++++++++++++ atst/domain/requests.py | 6 +-- atst/models/request.py | 8 +-- atst/routes/requests/index.py | 2 +- atst/routes/requests/jedi_request_flow.py | 2 +- tests/domain/test_requests.py | 4 +- tests/factories.py | 51 ++++++++++--------- tests/models/test_requests.py | 9 +++- 8 files changed, 89 insertions(+), 36 deletions(-) create mode 100644 alembic/versions/05d6272bdb43_rename_request_creator_.py diff --git a/alembic/versions/05d6272bdb43_rename_request_creator_.py b/alembic/versions/05d6272bdb43_rename_request_creator_.py new file mode 100644 index 00000000..c8a3966e --- /dev/null +++ b/alembic/versions/05d6272bdb43_rename_request_creator_.py @@ -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') diff --git a/atst/domain/requests.py b/atst/domain/requests.py index e7b64e5d..f9127aa9 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -58,10 +58,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) diff --git a/atst/models/request.py b/atst/models/request.py index 66e5bf1e..4e7a2832 100644 --- a/atst/models/request.py +++ b/atst/models/request.py @@ -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,13 +11,15 @@ 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 diff --git a/atst/routes/requests/index.py b/atst/routes/requests/index.py index 7e098b47..15ab8fee 100644 --- a/atst/routes/requests/index.py +++ b/atst/routes/requests/index.py @@ -28,7 +28,7 @@ def requests_index(): ): 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] diff --git a/atst/routes/requests/jedi_request_flow.py b/atst/routes/requests/jedi_request_flow.py index e750a9e0..af08251b 100644 --- a/atst/routes/requests/jedi_request_flow.py +++ b/atst/routes/requests/jedi_request_flow.py @@ -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 diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py index 1d696097..68835138 100644 --- a/tests/domain/test_requests.py +++ b/tests/domain/test_requests.py @@ -5,7 +5,7 @@ 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") @@ -25,7 +25,7 @@ def test_nonexistent_request_raises(): def test_new_request_has_started_status(): - request = Requests.create(uuid4(), {}) + request = Requests.create(UserFactory.build(), {}) assert request.status == RequestStatus.STARTED diff --git a/tests/factories.py b/tests/factories.py index dc68eb5c..af5f6971 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -9,31 +9,6 @@ from atst.models.user import User from atst.models.role import Role -class RequestStatusFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: - model = RequestStatusEvent - - -class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: - model = Request - - id = factory.Sequence(lambda x: uuid4()) - status_events = factory.RelatedFactory( - RequestStatusFactory, "request", new_status=RequestStatus.STARTED - ) - - -class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: - model = PENumber - - -class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: - model = TaskOrder - - class RoleFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = Role @@ -50,3 +25,29 @@ class UserFactory(factory.alchemy.SQLAlchemyModelFactory): first_name = "Fake" last_name = "User" atat_role = factory.SubFactory(RoleFactory) + + +class RequestStatusFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = RequestStatusEvent + + +class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = Request + + id = factory.Sequence(lambda x: uuid4()) + status_events = factory.RelatedFactory( + RequestStatusFactory, "request", new_status=RequestStatus.STARTED + ) + creator = factory.SubFactory(UserFactory) + + +class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = PENumber + + +class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = TaskOrder diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py index a1990a35..90038d4e 100644 --- a/tests/models/test_requests.py +++ b/tests/models/test_requests.py @@ -1,4 +1,4 @@ -from tests.factories import RequestFactory +from tests.factories import RequestFactory, UserFactory from atst.domain.requests import Requests, RequestStatus @@ -19,3 +19,10 @@ def test_pending_ccpo_approval_requires_ccpo(): 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 From dcb45c64e8b434a8231bd06deac44edc9b440943 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 21:26:05 -0400 Subject: [PATCH 066/101] Alter old migrations so that they can downgrade The Role -> User foreign key constraint was preventing roles from being deleted once there were existing users referencing the role. I realized it was best to just pass on the downgrade and allow the tables to be deleted. --- .../versions/4ea5917e7781_add_default_atat_role.py | 4 +--- .../96a9f3537996_add_roles_and_permissions.py | 13 +------------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/alembic/versions/4ea5917e7781_add_default_atat_role.py b/alembic/versions/4ea5917e7781_add_default_atat_role.py index 78b6ef55..21b03166 100644 --- a/alembic/versions/4ea5917e7781_add_default_atat_role.py +++ b/alembic/versions/4ea5917e7781_add_default_atat_role.py @@ -34,6 +34,4 @@ def upgrade(): def downgrade(): - db = op.get_bind() - db.execute("DELETE FROM roles WHERE name = 'default'") - + pass diff --git a/alembic/versions/96a9f3537996_add_roles_and_permissions.py b/alembic/versions/96a9f3537996_add_roles_and_permissions.py index 4380208a..0729127e 100644 --- a/alembic/versions/96a9f3537996_add_roles_and_permissions.py +++ b/alembic/versions/96a9f3537996_add_roles_and_permissions.py @@ -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 From f80668c6384299a50213e2dfd85e09d5a961afa0 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 21:32:02 -0400 Subject: [PATCH 067/101] Add script/seed.py for convenience --- atst/routes/dev.py | 20 ++++++++++++-------- script/seed.py | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 script/seed.py diff --git a/atst/routes/dev.py b/atst/routes/dev.py index cbd02cea..181ddc3d 100644 --- a/atst/routes/dev.py +++ b/atst/routes/dev.py @@ -9,37 +9,37 @@ _DEV_USERS = { "dod_id": "1234567890", "first_name": "Sam", "last_name": "Seeceepio", - "atat_role": "ccpo", + "atat_role_name": "ccpo", }, "amanda": { "dod_id": "2345678901", "first_name": "Amanda", "last_name": "Adamson", - "atat_role": "default", + "atat_role_name": "default", }, "brandon": { "dod_id": "3456789012", "first_name": "Brandon", "last_name": "Buchannan", - "atat_role": "default", + "atat_role_name": "default", }, "christina": { "dod_id": "4567890123", "first_name": "Christina", "last_name": "Collins", - "atat_role": "default", + "atat_role_name": "default", }, "dominick": { "dod_id": "5678901234", "first_name": "Dominick", "last_name": "Domingo", - "atat_role": "default", + "atat_role_name": "default", }, "erica": { "dod_id": "6789012345", "first_name": "Erica", "last_name": "Eichner", - "atat_role": "default", + "atat_role_name": "default", }, } @@ -48,8 +48,12 @@ _DEV_USERS = { 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) + 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_name"], basic_data + ) session["user_id"] = user.id return redirect(url_for("atst.home")) diff --git a/script/seed.py b/script/seed.py new file mode 100644 index 00000000..81be936f --- /dev/null +++ b/script/seed.py @@ -0,0 +1,25 @@ +# 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.routes.dev import _DEV_USERS as DEV_USERS + + +def seed_db(): + users = [Users.create(**dev_user) for (_, dev_user) in DEV_USERS.items()] + + for user in users: + [Requests.create(user, {}) for _ in range(5)] + + +if __name__ == "__main__": + config = make_config() + app = make_app(config) + with app.app_context(): + seed_db() From 04b9ae9f5364e4c7c02aa5159c739fa87b1511f2 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 21:32:47 -0400 Subject: [PATCH 068/101] Display creator's name and human-readable status --- atst/routes/requests/index.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/atst/routes/requests/index.py b/atst/routes/requests/index.py index 15ab8fee..5881c1e6 100644 --- a/atst/routes/requests/index.py +++ b/atst/routes/requests/index.py @@ -5,17 +5,17 @@ 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() return { "order_id": request.id, "is_new": is_new, - "status": request.status, + "status": request.status.name.capitalize(), "app_count": 1, "date": time_created.format("M/DD/YYYY"), - "full_name": user.full_name + "full_name": request.creator.full_name } @@ -30,6 +30,6 @@ def requests_index(): else: 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) From fdb7c699eebc201bc41b45a23184f6d69d893dac Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 21:41:50 -0400 Subject: [PATCH 069/101] Simplify dev user login --- atst/routes/dev.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/atst/routes/dev.py b/atst/routes/dev.py index 181ddc3d..947cc55f 100644 --- a/atst/routes/dev.py +++ b/atst/routes/dev.py @@ -48,15 +48,11 @@ _DEV_USERS = { 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_name"], 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"], ) 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) From 6dccf50e832a14336ac5fd72f982e89ace3c2020 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 21:51:30 -0400 Subject: [PATCH 070/101] Title case request status name --- atst/routes/requests/index.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/atst/routes/requests/index.py b/atst/routes/requests/index.py index 5881c1e6..1f26e6de 100644 --- a/atst/routes/requests/index.py +++ b/atst/routes/requests/index.py @@ -6,26 +6,25 @@ from atst.domain.requests import Requests def map_request(request): + + status_display_name = request.status.name.replace("_", " ").title() time_created = pendulum.instance(request.time_created) is_new = time_created.add(days=1) > pendulum.now() return { "order_id": request.id, "is_new": is_new, - "status": request.status.name.capitalize(), + "status": status_display_name, "app_count": 1, "date": time_created.format("M/DD/YYYY"), - "full_name": request.creator.full_name + "full_name": request.creator.full_name, } @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=g.current_user) From 47a4635eddd38c91d72f50db480a02d361ff76fa Mon Sep 17 00:00:00 2001 From: richard-dds Date: Tue, 7 Aug 2018 21:58:51 -0400 Subject: [PATCH 071/101] Display Request's "Total Apps" --- atst/routes/requests/index.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/atst/routes/requests/index.py b/atst/routes/requests/index.py index 1f26e6de..0a6e1524 100644 --- a/atst/routes/requests/index.py +++ b/atst/routes/requests/index.py @@ -10,12 +10,13 @@ def map_request(request): status_display_name = request.status.name.replace("_", " ").title() 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) return { "order_id": request.id, "is_new": is_new, "status": status_display_name, - "app_count": 1, + "app_count": app_count, "date": time_created.format("M/DD/YYYY"), "full_name": request.creator.full_name, } From 0c378ba07c45192124d449c1616ad2482bc8e1d7 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 8 Aug 2018 11:13:22 -0400 Subject: [PATCH 072/101] Declare helper properties on Request and RequestStatusEvent --- atst/models/request.py | 4 ++++ atst/models/request_status_event.py | 4 ++++ atst/routes/requests/index.py | 4 +--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/atst/models/request.py b/atst/models/request.py index 4e7a2832..bd14b5db 100644 --- a/atst/models/request.py +++ b/atst/models/request.py @@ -23,3 +23,7 @@ class Request(Base): @property def status(self): return self.status_events[-1].new_status + + @property + def status_displayname(self): + return self.status_events[-1].displayname diff --git a/atst/models/request_status_event.py b/atst/models/request_status_event.py index 2e492ac4..26d042fa 100644 --- a/atst/models/request_status_event.py +++ b/atst/models/request_status_event.py @@ -29,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.name.replace("_", " ").title() diff --git a/atst/routes/requests/index.py b/atst/routes/requests/index.py index 0a6e1524..ec9b566c 100644 --- a/atst/routes/requests/index.py +++ b/atst/routes/requests/index.py @@ -6,8 +6,6 @@ from atst.domain.requests import Requests def map_request(request): - - status_display_name = request.status.name.replace("_", " ").title() 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) @@ -15,7 +13,7 @@ def map_request(request): return { "order_id": request.id, "is_new": is_new, - "status": status_display_name, + "status": request.status_displayname, "app_count": app_count, "date": time_created.format("M/DD/YYYY"), "full_name": request.creator.full_name, From cb45db291e4fc95faf5b3a25ee97e9622788c960 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 8 Aug 2018 13:42:10 -0400 Subject: [PATCH 073/101] Rollback transaction if user creation fails --- atst/domain/users.py | 1 + 1 file changed, 1 insertion(+) diff --git a/atst/domain/users.py b/atst/domain/users.py index 54cf4ae0..bc3d972f 100644 --- a/atst/domain/users.py +++ b/atst/domain/users.py @@ -37,6 +37,7 @@ class Users(object): db.session.add(user) db.session.commit() except IntegrityError: + db.session.rollback() raise AlreadyExistsError("user") return user From 5f59b9f24a5cf23ebb7b34a6dc2cdf1aef5efa66 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 8 Aug 2018 13:42:28 -0400 Subject: [PATCH 074/101] Add emails to test users --- atst/routes/dev.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/atst/routes/dev.py b/atst/routes/dev.py index 947cc55f..f66e3f08 100644 --- a/atst/routes/dev.py +++ b/atst/routes/dev.py @@ -10,40 +10,45 @@ _DEV_USERS = { "first_name": "Sam", "last_name": "Seeceepio", "atat_role_name": "ccpo", + "email": "sam@test.com" }, "amanda": { "dod_id": "2345678901", "first_name": "Amanda", "last_name": "Adamson", "atat_role_name": "default", + "email": "amanda@test.com" }, "brandon": { "dod_id": "3456789012", "first_name": "Brandon", "last_name": "Buchannan", "atat_role_name": "default", + "email": "brandon@test.com" }, "christina": { "dod_id": "4567890123", "first_name": "Christina", "last_name": "Collins", "atat_role_name": "default", + "email": "christina@test.com" }, "dominick": { "dod_id": "5678901234", "first_name": "Dominick", "last_name": "Domingo", "atat_role_name": "default", + "email": "dominick@test.com" }, "erica": { "dod_id": "6789012345", "first_name": "Erica", "last_name": "Eichner", "atat_role_name": "default", + "email": "erica@test.com" }, } - @bp.route("/login-dev") def login_dev(): role = request.args.get("username", "amanda") @@ -53,6 +58,7 @@ def login_dev(): 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")) From df93db4cd838bc0f4f4d5f51397545107184400b Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 8 Aug 2018 13:42:48 -0400 Subject: [PATCH 075/101] Create more varied requests in seed script --- Pipfile.lock | 12 ++++++------ script/seed.py | 16 ++++++++++++++-- tests/factories.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index b9b8cab6..7f8a00cb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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" }, diff --git a/script/seed.py b/script/seed.py index 81be936f..d865b9d7 100644 --- a/script/seed.py +++ b/script/seed.py @@ -8,14 +8,26 @@ 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 = [Users.create(**dev_user) for (_, dev_user) in DEV_USERS.items()] + users = [] + for dev_user in DEV_USERS.values(): + try: + user = Users.create(**dev_user) + users.append(user) + except AlreadyExistsError: + pass for user in users: - [Requests.create(user, {}) for _ in range(5)] + 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__": diff --git a/tests/factories.py b/tests/factories.py index af5f6971..71dc1422 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -9,6 +9,7 @@ from atst.models.user import User from atst.models.role import Role + class RoleFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = Role @@ -41,6 +42,47 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): RequestStatusFactory, "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): @@ -51,3 +93,4 @@ class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory): class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = TaskOrder + From 17e9344f6e770e6e2f76f11e24b10e20c9a4d6a0 Mon Sep 17 00:00:00 2001 From: luis cielak Date: Wed, 8 Aug 2018 13:46:48 -0400 Subject: [PATCH 076/101] Style form labels for inputs correctly --- styles/elements/_inputs.scss | 22 +++++++++++++--------- templates/components/text_input.html | 2 -- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index 459e907d..7ce1c714 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -65,20 +65,12 @@ } label { - padding: 0 0 $gap 0; + padding: 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 { @@ -103,6 +103,10 @@ .icon { vertical-align: middle; } + + .usa-input__help { + font-weight: $font-bold; + } } ul { diff --git a/templates/components/text_input.html b/templates/components/text_input.html index 0f50ca89..8145c366 100644 --- a/templates/components/text_input.html +++ b/templates/components/text_input.html @@ -1,6 +1,5 @@ {% macro TextInput(field, placeholder='') -%}
- {{ field(placeholder=placeholder) | safe }} From 41a6d28e8879720413615d6c923f03decfec9d5d Mon Sep 17 00:00:00 2001 From: luis cielak Date: Wed, 8 Aug 2018 13:47:31 -0400 Subject: [PATCH 077/101] Add subtitle style --- styles/elements/_typography.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/elements/_typography.scss b/styles/elements/_typography.scss index fcdc97d9..b1af1be1 100644 --- a/styles/elements/_typography.scss +++ b/styles/elements/_typography.scss @@ -20,6 +20,7 @@ h1, h2, h3, h4, h5, h6 { + .subtitle * { margin-top: 0; + color: $color-gray; } } From d582cf2a412365f81c740b38cd4c0957be17ef60 Mon Sep 17 00:00:00 2001 From: luis cielak Date: Wed, 8 Aug 2018 13:50:48 -0400 Subject: [PATCH 078/101] Increase some padding --- styles/elements/_inputs.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index 7ce1c714..69750caf 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -65,7 +65,7 @@ } label { - padding: 0; + padding: 0 0 $gap/2 0; margin: 0; @include h4; @include line-max; From 1a5800cbc5493ea58ab7455ceb2d7756420674d4 Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 7 Aug 2018 15:40:51 -0400 Subject: [PATCH 079/101] Requests domain module can determine if user can view request --- atst/domain/requests.py | 8 ++++++++ tests/domain/test_requests.py | 10 +++++++++- tests/factories.py | 9 ++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/atst/domain/requests.py b/atst/domain/requests.py index e7b64e5d..843c7243 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -143,3 +143,11 @@ class Requests(object): return request.status == "incomplete" and all( section in existing_request_sections for section in all_request_sections ) + + @classmethod + def is_creator(cls, request_id, user_id): + try: + db.session.query(Request).filter_by(id=request_id, creator=user_id).one() + return True + except NoResultFound: + return False diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py index 1d696097..609dae90 100644 --- a/tests/domain/test_requests.py +++ b/tests/domain/test_requests.py @@ -5,7 +5,7 @@ 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") @@ -48,3 +48,11 @@ def test_dont_auto_approve_if_no_dollar_value_specified(new_request): request = Requests.submit(new_request) assert request.status == RequestStatus.PENDING_CCPO_APPROVAL + + +def test_can_check_if_user_created_request(session): + user_allowed = UserFactory.create() + user_denied = UserFactory.create() + request = RequestFactory.create(creator=user_allowed.id) + assert Requests.is_creator(request.id, user_allowed.id) + assert not Requests.is_creator(request.id, user_denied.id) diff --git a/tests/factories.py b/tests/factories.py index dc68eb5c..d731c42f 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,3 +1,5 @@ +import random +import string import factory from uuid import uuid4 @@ -46,7 +48,8 @@ class UserFactory(factory.alchemy.SQLAlchemyModelFactory): 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))) From 74fc5f0ec91f9ff3daf4b87804af6efc35249385 Mon Sep 17 00:00:00 2001 From: luis cielak Date: Wed, 8 Aug 2018 13:54:10 -0400 Subject: [PATCH 080/101] Make sure the label style for input text and radios match --- atst/forms/request.py | 2 +- styles/elements/_inputs.scss | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/atst/forms/request.py b/atst/forms/request.py index f15e9140..fb179587 100644 --- a/atst/forms/request.py +++ b/atst/forms/request.py @@ -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")], ) diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index 69750caf..e0cab5be 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -100,6 +100,10 @@ padding: 0 0 $gap 0; @include h4; + label { + font-weight: $font-bold; + } + .icon { vertical-align: middle; } From 2cfc1424175108601c49baf5ba793caa558afadf Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 7 Aug 2018 16:15:34 -0400 Subject: [PATCH 081/101] simple implementation of request view authorization --- atst/routes/requests/requests_form.py | 16 ++++++++++++ tests/factories.py | 11 ++++++++ tests/routes/test_request_new.py | 37 ++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py index 63a14224..391d49ed 100644 --- a/atst/routes/requests/requests_form.py +++ b/atst/routes/requests/requests_form.py @@ -3,6 +3,7 @@ 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 @requests_bp.route("/requests/new/", methods=["GET"]) @@ -25,6 +26,9 @@ def requests_form_new(screen): ) @requests_bp.route("/requests/new//", methods=["GET"]) def requests_form_update(screen=1, request_id=None): + if request_id and not _can_view_request(request_id): + return redirect(url_for("atst.unauthorized")) + request = Requests.get(request_id) if request_id is not None else None jedi_flow = JEDIRequestFlow(screen, request, request_id=request_id) @@ -79,10 +83,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 +100,15 @@ 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 _can_view_request(request_id): + return ( + Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST in g.current_user.atat_permissions + or Requests.is_creator(request_id, g.current_user.id) + ) diff --git a/tests/factories.py b/tests/factories.py index d731c42f..12c41fd4 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -9,6 +9,8 @@ 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 RequestStatusFactory(factory.alchemy.SQLAlchemyModelFactory): @@ -24,6 +26,7 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): status_events = factory.RelatedFactory( RequestStatusFactory, "request", new_status=RequestStatus.STARTED ) + body = {} class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory): @@ -53,3 +56,11 @@ class UserFactory(factory.alchemy.SQLAlchemyModelFactory): 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()) diff --git a/tests/routes/test_request_new.py b/tests/routes/test_request_new.py index 0927f1be..a7030e6f 100644 --- a/tests/routes/test_request_new.py +++ b/tests/routes/test_request_new.py @@ -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, RequestStatusEventFactory +from atst.domain.roles import Roles ERROR_CLASS = "alert--error" @@ -27,3 +28,37 @@ 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.id) + status = RequestStatusEventFactory.create(request_id=request.id) + + 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() + status = RequestStatusEventFactory.create(request_id=request.id) + + response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True) + + assert response.status_code == 401 + + +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() + status = RequestStatusEventFactory.create(request_id=request.id) + + response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True) + + assert response.status_code == 200 From 7b8934e0cb3ec9ea9396217e44be879d80f2606a Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 8 Aug 2018 11:26:54 -0400 Subject: [PATCH 082/101] add Flask error handlers --- atst/app.py | 2 ++ atst/routes/__init__.py | 12 ++---------- atst/routes/errors.py | 13 +++++++++++++ atst/routes/requests/requests_form.py | 4 ++-- templates/not_found.html | 12 ++++++++++++ tests/routes/test_request_new.py | 2 +- tests/test_auth.py | 7 ++----- 7 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 atst/routes/errors.py create mode 100644 templates/not_found.html diff --git a/atst/app.py b/atst/app.py index c86ab9bf..d095a8bf 100644 --- a/atst/app.py +++ b/atst/app.py @@ -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,6 +46,7 @@ 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) diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index 1884d28f..4e346fa9 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template, g, redirect, session, url_for, request +from flask import Blueprint, abort, render_template, g, redirect, session, url_for, request from flask import current_app as app import pendulum @@ -39,15 +39,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 + return abort(401) def _is_valid_certificate(request): diff --git a/atst/routes/errors.py b/atst/routes/errors.py new file mode 100644 index 00000000..261c654c --- /dev/null +++ b/atst/routes/errors.py @@ -0,0 +1,13 @@ +from flask import render_template + + +def make_error_pages(app): + @app.errorhandler(404) + def not_found(e): + return render_template("not_found.html"), 404 + + + @app.errorhandler(401) + def unauthorized(e): + return render_template('unauthorized.html'), 401 + diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py index 391d49ed..688a4ce0 100644 --- a/atst/routes/requests/requests_form.py +++ b/atst/routes/requests/requests_form.py @@ -1,4 +1,4 @@ -from flask import g, redirect, render_template, url_for, request as http_request +from flask import abort, g, redirect, render_template, url_for, request as http_request from . import requests_bp from atst.domain.requests import Requests @@ -27,7 +27,7 @@ def requests_form_new(screen): @requests_bp.route("/requests/new//", methods=["GET"]) def requests_form_update(screen=1, request_id=None): if request_id and not _can_view_request(request_id): - return redirect(url_for("atst.unauthorized")) + abort(404) request = Requests.get(request_id) if request_id is not None else None jedi_flow = JEDIRequestFlow(screen, request, request_id=request_id) diff --git a/templates/not_found.html b/templates/not_found.html new file mode 100644 index 00000000..59cc223f --- /dev/null +++ b/templates/not_found.html @@ -0,0 +1,12 @@ +{% extends "error_base.html" %} + +{% block content %} + +
+ +

Not Found

+ +
+ +{% endblock %} + diff --git a/tests/routes/test_request_new.py b/tests/routes/test_request_new.py index a7030e6f..1a722ca4 100644 --- a/tests/routes/test_request_new.py +++ b/tests/routes/test_request_new.py @@ -49,7 +49,7 @@ def test_non_owner_cannot_view_request(client, user_session): response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True) - assert response.status_code == 401 + assert response.status_code == 404 def test_ccpo_can_view_request(client, user_session): diff --git a/tests/test_auth.py b/tests/test_auth.py index 69cb3166..7e2b483d 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -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 From d5ed99089c94807aae1378edc54421917b867802 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 8 Aug 2018 12:09:23 -0400 Subject: [PATCH 083/101] raise exceptions, map to http error codes --- atst/domain/exceptions.py | 14 ++++++++++++++ atst/routes/__init__.py | 8 ++++++-- atst/routes/errors.py | 10 ++++++++-- atst/routes/requests/requests_form.py | 20 ++++++++++++-------- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/atst/domain/exceptions.py b/atst/domain/exceptions.py index 802997d4..b92e64c1 100644 --- a/atst/domain/exceptions.py +++ b/atst/domain/exceptions.py @@ -14,3 +14,17 @@ 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): + pass diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index 4e346fa9..965b4b37 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -1,10 +1,11 @@ -from flask import Blueprint, abort, render_template, g, redirect, session, url_for, request +from flask import Blueprint, render_template, g, redirect, session, url_for, request from flask import current_app as app 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,7 +43,7 @@ def login_redirect(): return redirect(url_for("atst.home")) else: - return abort(401) + raise UnauthenticatedError() def _is_valid_certificate(request): diff --git a/atst/routes/errors.py b/atst/routes/errors.py index 261c654c..0d0211b8 100644 --- a/atst/routes/errors.py +++ b/atst/routes/errors.py @@ -1,13 +1,19 @@ from flask import render_template +import atst.domain.exceptions as exceptions + def make_error_pages(app): - @app.errorhandler(404) + @app.errorhandler(exceptions.NotFoundError) + @app.errorhandler(exceptions.UnauthorizedError) + # pylint: disable=unused-variable def not_found(e): return render_template("not_found.html"), 404 - @app.errorhandler(401) + @app.errorhandler(exceptions.UnauthenticatedError) + # pylint: disable=unused-variable def unauthorized(e): return render_template('unauthorized.html'), 401 + return app diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py index 688a4ce0..ee03c155 100644 --- a/atst/routes/requests/requests_form.py +++ b/atst/routes/requests/requests_form.py @@ -1,9 +1,10 @@ -from flask import abort, g, redirect, render_template, url_for, request as http_request +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/", methods=["GET"]) @@ -26,8 +27,8 @@ def requests_form_new(screen): ) @requests_bp.route("/requests/new//", methods=["GET"]) def requests_form_update(screen=1, request_id=None): - if request_id and not _can_view_request(request_id): - abort(404) + 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) @@ -107,8 +108,11 @@ def requests_submit(request_id=None): # TODO: generalize this, along with other authorizations, into a policy-pattern # for authorization in the application -def _can_view_request(request_id): - return ( - Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST in g.current_user.atat_permissions - or Requests.is_creator(request_id, g.current_user.id) - ) +def _check_can_view_request(request_id): + if Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST in g.current_user.atat_permissions: + pass + elif Requests.is_creator(request_id, g.current_user.id): + pass + else: + raise UnauthorizedError(g.current_user, "view request {}".format(request_id)) + From 337dd2414b41e28a308a0c6954fa0d590bc4856a Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 8 Aug 2018 14:05:59 -0400 Subject: [PATCH 084/101] catch bad request_id in request form GET --- atst/domain/requests.py | 4 ++-- tests/routes/test_request_new.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/atst/domain/requests.py b/atst/domain/requests.py index 843c7243..c4b29cd9 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -1,4 +1,4 @@ -from sqlalchemy import exists, and_ +from sqlalchemy import exists, and_, exc from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.attributes import flag_modified @@ -149,5 +149,5 @@ class Requests(object): try: db.session.query(Request).filter_by(id=request_id, creator=user_id).one() return True - except NoResultFound: + except (NoResultFound, exc.DataError): return False diff --git a/tests/routes/test_request_new.py b/tests/routes/test_request_new.py index 1a722ca4..6a0ccb86 100644 --- a/tests/routes/test_request_new.py +++ b/tests/routes/test_request_new.py @@ -62,3 +62,10 @@ def test_ccpo_can_view_request(client, user_session): 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 From 2b56ce6260aee9eb2ce4242c391ac7a398e7abb5 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 8 Aug 2018 14:11:11 -0400 Subject: [PATCH 085/101] use existing Requests function for creator check --- atst/domain/requests.py | 21 ++++++++------------- atst/routes/requests/requests_form.py | 2 +- tests/domain/test_requests.py | 6 +++--- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/atst/domain/requests.py b/atst/domain/requests.py index c4b29cd9..891266ed 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -42,11 +42,14 @@ class Requests(object): @classmethod def exists(cls, request_id, creator_id): - return db.session.query( - exists().where( - and_(Request.id == request_id, Request.creator == creator_id) - ) - ).scalar() + try: + return db.session.query( + exists().where( + and_(Request.id == request_id, Request.creator == creator_id) + ) + ).scalar() + except exc.DataError: + return False @classmethod def get(cls, request_id): @@ -143,11 +146,3 @@ class Requests(object): return request.status == "incomplete" and all( section in existing_request_sections for section in all_request_sections ) - - @classmethod - def is_creator(cls, request_id, user_id): - try: - db.session.query(Request).filter_by(id=request_id, creator=user_id).one() - return True - except (NoResultFound, exc.DataError): - return False diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py index ee03c155..ad415775 100644 --- a/atst/routes/requests/requests_form.py +++ b/atst/routes/requests/requests_form.py @@ -111,7 +111,7 @@ def requests_submit(request_id=None): def _check_can_view_request(request_id): if Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST in g.current_user.atat_permissions: pass - elif Requests.is_creator(request_id, g.current_user.id): + elif Requests.exists(request_id, g.current_user.id): pass else: raise UnauthorizedError(g.current_user, "view request {}".format(request_id)) diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py index 609dae90..d82c8ce4 100644 --- a/tests/domain/test_requests.py +++ b/tests/domain/test_requests.py @@ -50,9 +50,9 @@ def test_dont_auto_approve_if_no_dollar_value_specified(new_request): assert request.status == RequestStatus.PENDING_CCPO_APPROVAL -def test_can_check_if_user_created_request(session): +def test_exists(session): user_allowed = UserFactory.create() user_denied = UserFactory.create() request = RequestFactory.create(creator=user_allowed.id) - assert Requests.is_creator(request.id, user_allowed.id) - assert not Requests.is_creator(request.id, user_denied.id) + assert Requests.exists(request.id, user_allowed.id) + assert not Requests.exists(request.id, user_denied.id) From 486b89fa83751b868823315c7399ab77cd7984ba Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 8 Aug 2018 14:25:30 -0400 Subject: [PATCH 086/101] Catch KeyError instead of ValueError --- alembic/versions/77b065750596_new_request_statuses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alembic/versions/77b065750596_new_request_statuses.py b/alembic/versions/77b065750596_new_request_statuses.py index 28cce335..23b22c29 100644 --- a/alembic/versions/77b065750596_new_request_statuses.py +++ b/alembic/versions/77b065750596_new_request_statuses.py @@ -40,7 +40,7 @@ def upgrade(): WHERE id = :id""" ) db.execute(query, id=status_event["id"], status=status.name) - except ValueError: + except KeyError: query = sa.text("DELETE FROM request_status_events WHERE id = :id") db.execute(query, id=status_event["id"]) From dec8d920762e905b6ca395e2e9d2448bf4199b88 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 8 Aug 2018 14:45:51 -0400 Subject: [PATCH 087/101] update tests to rely on newest RequestFactory --- tests/routes/test_request_new.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/routes/test_request_new.py b/tests/routes/test_request_new.py index 6a0ccb86..e402e622 100644 --- a/tests/routes/test_request_new.py +++ b/tests/routes/test_request_new.py @@ -2,7 +2,7 @@ import re import pytest import urllib from tests.mocks import MOCK_USER, MOCK_REQUEST -from tests.factories import RequestFactory, UserFactory, RequestStatusEventFactory +from tests.factories import RequestFactory, UserFactory from atst.domain.roles import Roles @@ -34,7 +34,6 @@ def test_owner_can_view_request(client, user_session): user = UserFactory.create() user_session(user) request = RequestFactory.create(creator=user.id) - status = RequestStatusEventFactory.create(request_id=request.id) response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True) @@ -45,7 +44,6 @@ def test_non_owner_cannot_view_request(client, user_session): user = UserFactory.create() user_session(user) request = RequestFactory.create() - status = RequestStatusEventFactory.create(request_id=request.id) response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True) @@ -57,7 +55,6 @@ def test_ccpo_can_view_request(client, user_session): user = UserFactory.create(atat_role=ccpo) user_session(user) request = RequestFactory.create() - status = RequestStatusEventFactory.create(request_id=request.id) response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True) From d307a255b1e256a579534e87157ea6a52b475972 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 8 Aug 2018 15:46:16 -0400 Subject: [PATCH 088/101] Fix some issues from a tricky merge --- atst/domain/requests.py | 8 ++++---- atst/routes/requests/requests_form.py | 2 +- tests/conftest.py | 12 ++++++------ tests/domain/test_requests.py | 6 +++--- tests/factories.py | 4 ++-- tests/routes/test_request_new.py | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/atst/domain/requests.py b/atst/domain/requests.py index fcdd78bb..85fb688a 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -31,8 +31,8 @@ class Requests(object): AUTO_APPROVE_THRESHOLD = 1000000 @classmethod - def create(cls, creator_id, body): - request = Request(creator=creator_id, body=body) + def create(cls, creator, body): + request = Request(creator=creator, body=body) request = Requests.set_status(request, RequestStatus.STARTED) db.session.add(request) @@ -41,11 +41,11 @@ class Requests(object): return request @classmethod - def exists(cls, request_id, creator_id): + def exists(cls, request_id, creator): try: return db.session.query( exists().where( - and_(Request.id == request_id, Request.creator == creator_id) + and_(Request.id == request_id, Request.creator == creator) ) ).scalar() except exc.DataError: diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py index ad415775..d384abdf 100644 --- a/atst/routes/requests/requests_form.py +++ b/atst/routes/requests/requests_form.py @@ -111,7 +111,7 @@ def requests_submit(request_id=None): 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.id): + elif Requests.exists(request_id, g.current_user): pass else: raise UnauthorizedError(g.current_user, "view request {}".format(request_id)) diff --git a/tests/conftest.py b/tests/conftest.py index c3add296..ab912679 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,6 @@ 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 @@ -75,7 +74,6 @@ class DummyForm(dict): class DummyField(object): - def __init__(self, data=None, errors=(), raw_data=None): self.data = data self.errors = list(errors) @@ -93,9 +91,11 @@ def dummy_field(): @pytest.fixture -def user_session(monkeypatch): - - def set_user_session(user=MOCK_USER): - monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: user) +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 diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py index fad23c51..ebdc64a0 100644 --- a/tests/domain/test_requests.py +++ b/tests/domain/test_requests.py @@ -53,6 +53,6 @@ def test_dont_auto_approve_if_no_dollar_value_specified(new_request): def test_exists(session): user_allowed = UserFactory.create() user_denied = UserFactory.create() - request = RequestFactory.create(creator=user_allowed.id) - assert Requests.exists(request.id, user_allowed.id) - assert not Requests.exists(request.id, user_denied.id) + request = RequestFactory.create(creator=user_allowed) + assert Requests.exists(request.id, user_allowed) + assert not Requests.exists(request.id, user_denied) diff --git a/tests/factories.py b/tests/factories.py index 68209045..cf81fa71 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -20,7 +20,7 @@ class RoleFactory(factory.alchemy.SQLAlchemyModelFactory): permissions = [] - + class UserFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = User @@ -47,7 +47,7 @@ class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): id = factory.Sequence(lambda x: uuid4()) status_events = factory.RelatedFactory( - RequestStatusFactory, "request", new_status=RequestStatus.STARTED + RequestStatusEventFactory, "request", new_status=RequestStatus.STARTED ) creator = factory.SubFactory(UserFactory) body = factory.LazyAttribute(lambda r: RequestFactory.build_request_body(r.creator)) diff --git a/tests/routes/test_request_new.py b/tests/routes/test_request_new.py index e402e622..e31aae79 100644 --- a/tests/routes/test_request_new.py +++ b/tests/routes/test_request_new.py @@ -33,7 +33,7 @@ def test_submit_valid_request_form(monkeypatch, client, user_session): def test_owner_can_view_request(client, user_session): user = UserFactory.create() user_session(user) - request = RequestFactory.create(creator=user.id) + request = RequestFactory.create(creator=user) response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True) From dd849df388e46476108b15c1947c06e10e6f7bc6 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 8 Aug 2018 16:18:12 -0400 Subject: [PATCH 089/101] Use enum value to store status displayname --- atst/models/request_status_event.py | 14 +++++----- tests/models/test_requests.py | 42 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/atst/models/request_status_event.py b/atst/models/request_status_event.py index 26d042fa..81505f2a 100644 --- a/atst/models/request_status_event.py +++ b/atst/models/request_status_event.py @@ -9,12 +9,12 @@ 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" + STARTED = "Started" + PENDING_FINANCIAL_VERIFICATION = "Pending Financial Verification" + PENDING_CCPO_APPROVAL = "Pending CCPO Approval" + APPROVED = "Approved" + EXPIRED = "Expired" + DELETED = "Deleted" class RequestStatusEvent(Base): @@ -32,4 +32,4 @@ class RequestStatusEvent(Base): @property def displayname(self): - return self.new_status.name.replace("_", " ").title() + return self.new_status.value diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py index 90038d4e..d6592a25 100644 --- a/tests/models/test_requests.py +++ b/tests/models/test_requests.py @@ -26,3 +26,45 @@ def test_request_has_creator(): 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" From 993a6635b2d4192aefd1ad1feac04f2dd4181fd8 Mon Sep 17 00:00:00 2001 From: luis cielak Date: Wed, 8 Aug 2018 17:39:03 -0400 Subject: [PATCH 090/101] Render error validation message with proper color --- templates/components/options_input.html | 2 +- templates/components/text_input.html | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/components/options_input.html b/templates/components/options_input.html index ff0d8814..06f4c3db 100644 --- a/templates/components/options_input.html +++ b/templates/components/options_input.html @@ -1,7 +1,7 @@ {% from "components/icon.html" import Icon %} {% macro OptionsInput(field, inline=False) -%} -
+
diff --git a/templates/components/text_input.html b/templates/components/text_input.html index 8145c366..0f50ca89 100644 --- a/templates/components/text_input.html +++ b/templates/components/text_input.html @@ -1,5 +1,6 @@ {% macro TextInput(field, placeholder='') -%}
+ {{ field(placeholder=placeholder) | safe }} From 5b102651268c8c131efd2fe59c9d3134fb71e4c0 Mon Sep 17 00:00:00 2001 From: luis cielak Date: Wed, 8 Aug 2018 17:44:37 -0400 Subject: [PATCH 091/101] Render error message with proper color for input text --- templates/components/text_input.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/components/text_input.html b/templates/components/text_input.html index 0f50ca89..1f272934 100644 --- a/templates/components/text_input.html +++ b/templates/components/text_input.html @@ -1,5 +1,5 @@ {% macro TextInput(field, placeholder='') -%} -
+
+ + + {%- endmacro %} From c0f3c0f8a640d836c91361c40ce895eb6968e85d Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 9 Aug 2018 10:11:44 -0400 Subject: [PATCH 097/101] Fix financial verification form submission --- atst/routes/requests/financial_verification.py | 2 +- ...ubmitted.html.to => financial_verification_submitted.html} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename templates/requests/{financial_verification_submitted.html.to => financial_verification_submitted.html} (84%) diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py index 38420287..ce70eaee 100644 --- a/atst/routes/requests/financial_verification.py +++ b/atst/routes/requests/financial_verification.py @@ -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") diff --git a/templates/requests/financial_verification_submitted.html.to b/templates/requests/financial_verification_submitted.html similarity index 84% rename from templates/requests/financial_verification_submitted.html.to rename to templates/requests/financial_verification_submitted.html index 21088724..01d6ef89 100644 --- a/templates/requests/financial_verification_submitted.html.to +++ b/templates/requests/financial_verification_submitted.html @@ -1,4 +1,4 @@ -{% extends "../base.html.to" %} +{% extends "base.html" %} {% block content %} @@ -15,4 +15,4 @@
-{% end %} +{% endblock %} From 994af840d9707aff8b79c9069711314646856cbf Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 9 Aug 2018 10:26:58 -0400 Subject: [PATCH 098/101] Fix NewlineListField displaying too many newlines --- atst/forms/fields.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/atst/forms/fields.py b/atst/forms/fields.py index bc542bf5..00e53529 100644 --- a/atst/forms/fields.py +++ b/atst/forms/fields.py @@ -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 "" From edcd8bb92148569d762305faaf95af76d9efe57e Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Thu, 9 Aug 2018 10:51:04 -0400 Subject: [PATCH 099/101] Require PE id to be defined in financial verification form --- atst/forms/financial.py | 2 +- atst/routes/requests/financial_verification.py | 2 +- tests/routes/test_financial_verification.py | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/atst/forms/financial.py b/atst/forms/financial.py index 68196837..674eace4 100644 --- a/atst/forms/financial.py +++ b/atst/forms/financial.py @@ -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") diff --git a/atst/routes/requests/financial_verification.py b/atst/routes/requests/financial_verification.py index ce70eaee..f6b8cfda 100644 --- a/atst/routes/requests/financial_verification.py +++ b/atst/routes/requests/financial_verification.py @@ -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: diff --git a/tests/routes/test_financial_verification.py b/tests/routes/test_financial_verification.py index e15638df..8e62cc48 100644 --- a/tests/routes/test_financial_verification.py +++ b/tests/routes/test_financial_verification.py @@ -72,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 From fc436af1349807cc26f1069258b09247e6f648f2 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 9 Aug 2018 11:48:33 -0400 Subject: [PATCH 100/101] log errors in error handlers --- atst/domain/exceptions.py | 4 +++- atst/routes/errors.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/atst/domain/exceptions.py b/atst/domain/exceptions.py index b92e64c1..ec574232 100644 --- a/atst/domain/exceptions.py +++ b/atst/domain/exceptions.py @@ -27,4 +27,6 @@ class UnauthorizedError(Exception): class UnauthenticatedError(Exception): - pass + @property + def message(self): + return str(self) diff --git a/atst/routes/errors.py b/atst/routes/errors.py index 0d0211b8..e9dcafcf 100644 --- a/atst/routes/errors.py +++ b/atst/routes/errors.py @@ -8,12 +8,14 @@ def make_error_pages(app): @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('unauthorized.html'), 401 return app From 3f01d455bb440cee7b1d1e9791e3b94af3315ef0 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 9 Aug 2018 11:57:37 -0400 Subject: [PATCH 101/101] better name, text for unauthenticated page --- atst/routes/errors.py | 2 +- templates/{unauthorized.html => unauthenticated.html} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename templates/{unauthorized.html => unauthenticated.html} (83%) diff --git a/atst/routes/errors.py b/atst/routes/errors.py index e9dcafcf..5fdac3d1 100644 --- a/atst/routes/errors.py +++ b/atst/routes/errors.py @@ -16,6 +16,6 @@ def make_error_pages(app): # pylint: disable=unused-variable def unauthorized(e): app.logger.error(e.message) - return render_template('unauthorized.html'), 401 + return render_template('unauthenticated.html'), 401 return app diff --git a/templates/unauthorized.html b/templates/unauthenticated.html similarity index 83% rename from templates/unauthorized.html rename to templates/unauthenticated.html index efaa3b95..8fabbdf9 100644 --- a/templates/unauthorized.html +++ b/templates/unauthenticated.html @@ -4,7 +4,7 @@
-

Unauthorized

+

Log in Failed