diff --git a/.circleci/config.yml b/.circleci/config.yml index af450fad..082ff825 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -293,6 +293,11 @@ workflows: - integration-tests: requires: - docker-build + filters: + branches: + only: + - staging + - master - deploy-staging: requires: - test diff --git a/.gitignore b/.gitignore index 19f4acc5..d8e2290d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ static/buildinfo.* log/* config/dev.ini +.env* # CRLs /crl diff --git a/.secrets.baseline b/.secrets.baseline index 07353d5a..7a81d3cb 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -161,7 +161,7 @@ "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "is_secret": false, "is_verified": false, - "line_number": 32, + "line_number": 31, "type": "Hex High Entropy String" } ], @@ -170,7 +170,7 @@ "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "is_secret": false, "is_verified": false, - "line_number": 656, + "line_number": 657, "type": "Hex High Entropy String" } ] diff --git a/alembic/versions/67a2151d6269_update_schema_based_on_business_logic.py b/alembic/versions/67a2151d6269_update_schema_based_on_business_logic.py new file mode 100644 index 00000000..06fd5d40 --- /dev/null +++ b/alembic/versions/67a2151d6269_update_schema_based_on_business_logic.py @@ -0,0 +1,198 @@ +"""update schema based on business logic + +Revision ID: 67a2151d6269 +Revises: 687fd43489d6 +Create Date: 2019-12-02 14:16:24.902108 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '67a2151d6269' # pragma: allowlist secret +down_revision = '687fd43489d6' # pragma: allowlist secret +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('application_invitations', 'application_role_id', + existing_type=postgresql.UUID(), + nullable=False) + op.alter_column('application_invitations', 'dod_id', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('application_invitations', 'expiration_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + nullable=False) + op.alter_column('application_invitations', 'first_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('application_invitations', 'inviter_id', + existing_type=postgresql.UUID(), + nullable=False) + op.alter_column('application_invitations', 'last_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('application_invitations', 'token', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('application_roles', 'status', + existing_type=sa.VARCHAR(length=8), + nullable=False) + op.alter_column('clins', 'end_date', + existing_type=sa.DATE(), + nullable=False) + op.alter_column('clins', 'jedi_clin_type', + existing_type=sa.VARCHAR(length=11), + nullable=False) + op.alter_column('clins', 'number', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('clins', 'obligated_amount', + existing_type=sa.NUMERIC(), + nullable=False) + op.alter_column('clins', 'start_date', + existing_type=sa.DATE(), + nullable=False) + op.alter_column('clins', 'total_amount', + existing_type=sa.NUMERIC(), + nullable=False) + op.alter_column('environment_roles', 'status', + existing_type=sa.VARCHAR(length=9), + nullable=False) + op.alter_column('portfolio_invitations', 'dod_id', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('portfolio_invitations', 'expiration_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + nullable=False) + op.alter_column('portfolio_invitations', 'first_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('portfolio_invitations', 'inviter_id', + existing_type=postgresql.UUID(), + nullable=False) + op.alter_column('portfolio_invitations', 'last_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('portfolio_invitations', 'portfolio_role_id', + existing_type=postgresql.UUID(), + nullable=False) + op.alter_column('portfolio_invitations', 'token', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('portfolio_roles', 'status', + existing_type=sa.VARCHAR(length=8), + nullable=False) + op.alter_column('portfolios', 'defense_component', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('portfolios', 'name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('task_orders', 'portfolio_id', + existing_type=postgresql.UUID(), + nullable=False) + op.drop_constraint('task_orders_user_id_fkey', 'task_orders', type_='foreignkey') + op.drop_column('task_orders', 'user_id') + op.alter_column('users', 'first_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('users', 'last_name', + existing_type=sa.VARCHAR(), + nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('users', 'last_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('users', 'first_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.add_column('task_orders', sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=True)) + op.create_foreign_key('task_orders_user_id_fkey', 'task_orders', 'users', ['user_id'], ['id']) + op.alter_column('task_orders', 'portfolio_id', + existing_type=postgresql.UUID(), + nullable=True) + op.alter_column('portfolios', 'name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('portfolios', 'defense_component', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('portfolio_roles', 'status', + existing_type=sa.VARCHAR(length=8), + nullable=True) + op.alter_column('portfolio_invitations', 'token', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('portfolio_invitations', 'portfolio_role_id', + existing_type=postgresql.UUID(), + nullable=True) + op.alter_column('portfolio_invitations', 'last_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('portfolio_invitations', 'inviter_id', + existing_type=postgresql.UUID(), + nullable=True) + op.alter_column('portfolio_invitations', 'first_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('portfolio_invitations', 'expiration_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + nullable=True) + op.alter_column('portfolio_invitations', 'dod_id', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('environment_roles', 'status', + existing_type=sa.VARCHAR(length=9), + nullable=True) + op.alter_column('clins', 'total_amount', + existing_type=sa.NUMERIC(), + nullable=True) + op.alter_column('clins', 'start_date', + existing_type=sa.DATE(), + nullable=True) + op.alter_column('clins', 'obligated_amount', + existing_type=sa.NUMERIC(), + nullable=True) + op.alter_column('clins', 'number', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('clins', 'jedi_clin_type', + existing_type=sa.VARCHAR(length=11), + nullable=True) + op.alter_column('clins', 'end_date', + existing_type=sa.DATE(), + nullable=True) + op.alter_column('application_roles', 'status', + existing_type=sa.VARCHAR(length=8), + nullable=True) + op.alter_column('application_invitations', 'token', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('application_invitations', 'last_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('application_invitations', 'inviter_id', + existing_type=postgresql.UUID(), + nullable=True) + op.alter_column('application_invitations', 'first_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('application_invitations', 'expiration_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + nullable=True) + op.alter_column('application_invitations', 'dod_id', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('application_invitations', 'application_role_id', + existing_type=postgresql.UUID(), + nullable=True) + # ### end Alembic commands ### diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 93521843..0c67e1d4 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -11,10 +11,8 @@ class TaskOrders(BaseDomainClass): resource_name = "task_order" @classmethod - def create(cls, creator, portfolio_id, number, clins, pdf): - task_order = TaskOrder( - portfolio_id=portfolio_id, creator=creator, number=number, pdf=pdf - ) + def create(cls, portfolio_id, number, clins, pdf): + task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf) db.session.add(task_order) db.session.commit() diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 3ac5640b..2a1b8ad2 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -7,7 +7,7 @@ from wtforms.fields import ( HiddenField, ) from wtforms.fields.html5 import DateField -from wtforms.validators import Required, Optional, Length, NumberRange, ValidationError +from wtforms.validators import Required, Length, NumberRange, ValidationError from flask_wtf import FlaskForm from numbers import Number @@ -61,9 +61,7 @@ class CLINForm(FlaskForm): coerce=coerce_enum, ) - number = StringField( - label=translate("task_orders.form.clin_number_label"), validators=[Optional()] - ) + number = StringField(label=translate("task_orders.form.clin_number_label")) start_date = DateField( translate("task_orders.form.pop_start"), description=translate("task_orders.form.pop_example"), diff --git a/atst/models/application_invitation.py b/atst/models/application_invitation.py index d24cc54d..02be5e14 100644 --- a/atst/models/application_invitation.py +++ b/atst/models/application_invitation.py @@ -12,7 +12,10 @@ class ApplicationInvitation( __tablename__ = "application_invitations" application_role_id = Column( - UUID(as_uuid=True), ForeignKey("application_roles.id"), index=True + UUID(as_uuid=True), + ForeignKey("application_roles.id"), + index=True, + nullable=False, ) role = relationship( "ApplicationRole", diff --git a/atst/models/application_role.py b/atst/models/application_role.py index f8f7f201..d65ceac7 100644 --- a/atst/models/application_role.py +++ b/atst/models/application_role.py @@ -46,7 +46,9 @@ class ApplicationRole( UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True ) - status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) + status = Column( + SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False + ) permission_sets = relationship( "PermissionSet", secondary=application_roles_permission_sets diff --git a/atst/models/clin.py b/atst/models/clin.py index 2802e292..0624d985 100644 --- a/atst/models/clin.py +++ b/atst/models/clin.py @@ -23,12 +23,12 @@ class CLIN(Base, mixins.TimestampsMixin): task_order_id = Column(ForeignKey("task_orders.id"), nullable=False) task_order = relationship("TaskOrder") - number = Column(String, nullable=True) - start_date = Column(Date, nullable=True) - end_date = Column(Date, nullable=True) - total_amount = Column(Numeric(scale=2), nullable=True) - obligated_amount = Column(Numeric(scale=2), nullable=True) - jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=True) + number = Column(String, nullable=False) + start_date = Column(Date, nullable=False) + end_date = Column(Date, nullable=False) + total_amount = Column(Numeric(scale=2), nullable=False) + obligated_amount = Column(Numeric(scale=2), nullable=False) + jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False) # # NOTE: For now obligated CLINS are CLIN 1 + CLIN 3 diff --git a/atst/models/environment_role.py b/atst/models/environment_role.py index 541b6d40..5b3a2c27 100644 --- a/atst/models/environment_role.py +++ b/atst/models/environment_role.py @@ -43,7 +43,9 @@ class EnvironmentRole( COMPLETED = "completed" DISABLED = "disabled" - status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) + status = Column( + SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False + ) def __repr__(self): return "".format( diff --git a/atst/models/mixins/invites.py b/atst/models/mixins/invites.py index 69e016f8..18916dc4 100644 --- a/atst/models/mixins/invites.py +++ b/atst/models/mixins/invites.py @@ -31,23 +31,29 @@ class InvitesMixin(object): @declared_attr def inviter_id(cls): - return Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) + return Column( + UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=False + ) @declared_attr def inviter(cls): return relationship("User", foreign_keys=[cls.inviter_id]) - status = Column(SQLAEnum(Status, native_enum=False, default=Status.PENDING)) + status = Column( + SQLAEnum(Status, native_enum=False, default=Status.PENDING, nullable=False) + ) - expiration_time = Column(TIMESTAMP(timezone=True)) + expiration_time = Column(TIMESTAMP(timezone=True), nullable=False) - token = Column(String, index=True, default=lambda: secrets.token_urlsafe()) + token = Column( + String, index=True, default=lambda: secrets.token_urlsafe(), nullable=False + ) email = Column(String, nullable=False) - dod_id = Column(String) - first_name = Column(String) - last_name = Column(String) + dod_id = Column(String, nullable=False) + first_name = Column(String, nullable=False) + last_name = Column(String, nullable=False) phone_number = Column(String) phone_ext = Column(String) diff --git a/atst/models/portfolio.py b/atst/models/portfolio.py index 08a65f1c..ed26b4d4 100644 --- a/atst/models/portfolio.py +++ b/atst/models/portfolio.py @@ -18,8 +18,10 @@ class Portfolio( __tablename__ = "portfolios" id = types.Id() - name = Column(String) - defense_component = Column(String) # Department of Defense Component + name = Column(String, nullable=False) + defense_component = Column( + String, nullable=False + ) # Department of Defense Component app_migration = Column(String) # App Migration complexity = Column(ARRAY(String)) # Application Complexity diff --git a/atst/models/portfolio_invitation.py b/atst/models/portfolio_invitation.py index 55d895c6..4ab9088d 100644 --- a/atst/models/portfolio_invitation.py +++ b/atst/models/portfolio_invitation.py @@ -12,7 +12,7 @@ class PortfolioInvitation( __tablename__ = "portfolio_invitations" portfolio_role_id = Column( - UUID(as_uuid=True), ForeignKey("portfolio_roles.id"), index=True + UUID(as_uuid=True), ForeignKey("portfolio_roles.id"), index=True, nullable=False ) role = relationship( "PortfolioRole", diff --git a/atst/models/portfolio_role.py b/atst/models/portfolio_role.py index 500a0ddd..53204e82 100644 --- a/atst/models/portfolio_role.py +++ b/atst/models/portfolio_role.py @@ -52,7 +52,9 @@ class PortfolioRole( UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True ) - status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) + status = Column( + SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False + ) permission_sets = relationship( "PermissionSet", secondary=portfolio_roles_permission_sets diff --git a/atst/models/task_order.py b/atst/models/task_order.py index c79d3b83..85bf363a 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -33,12 +33,9 @@ class TaskOrder(Base, mixins.TimestampsMixin): id = types.Id() - portfolio_id = Column(ForeignKey("portfolios.id")) + portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False) portfolio = relationship("Portfolio") - user_id = Column(ForeignKey("users.id")) - creator = relationship("User", foreign_keys="TaskOrder.user_id") - pdf_attachment_id = Column(ForeignKey("attachments.id")) _pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id]) number = Column(String) # Task Order Number diff --git a/atst/models/user.py b/atst/models/user.py index 4ba23895..29b377d6 100644 --- a/atst/models/user.py +++ b/atst/models/user.py @@ -56,8 +56,8 @@ class User( email = Column(String) dod_id = Column(String, unique=True, nullable=False) - first_name = Column(String) - last_name = Column(String) + first_name = Column(String, nullable=False) + last_name = Column(String, nullable=False) phone_number = Column(String) phone_ext = Column(String) service_branch = Column(String) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 24460204..dca751e3 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -66,7 +66,7 @@ def update_task_order( task_order = TaskOrders.update(task_order_id, **form.data) portfolio_id = task_order.portfolio_id else: - task_order = TaskOrders.create(g.current_user, portfolio_id, **form.data) + task_order = TaskOrders.create(portfolio_id, **form.data) return redirect(url_for(next_page, task_order_id=task_order.id)) else: @@ -181,9 +181,7 @@ def cancel_edit(task_order_id=None, portfolio_id=None): if task_order_id: task_order = TaskOrders.update(task_order_id, **form.data) else: - task_order = TaskOrders.create( - g.current_user, portfolio_id, **form.data - ) + task_order = TaskOrders.create(portfolio_id, **form.data) elif not save and task_order_id: TaskOrders.delete(task_order_id) diff --git a/deploy/README.md b/deploy/README.md index be66290d..25380293 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -14,6 +14,7 @@ The production configuration (azure.atat.code.mil, currently) is reflected in th - AUTH_DOMAIN: The host domain for the authentication endpoint for the environment. - KV_MI_ID: the fully qualified id (path) of the managed identity for the key vault (instructions on retrieving this are down in section on [Setting up FlexVol](#configuring-the-identity)). Example: /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/RESOURCE_GROUP_NAME/providers/Microsoft.ManagedIdentity/userAssignedIdentities/MANAGED_IDENTITY_NAME - KV_MI_CLIENT_ID: The client id of the managed identity for the key vault. This is a GUID. +- TENANT_ID: The id of the active directory tenant in which the cluster and it's associated users exist. This is a GUID. We use envsubst to substitute values for these variables. There is a wrapper script (script/k8s_config) that will output the compiled configuration, using a combination of kustomize and envsubst. @@ -169,6 +170,12 @@ Then: kubectl -n atat create secret tls azure-atat-code-mil-tls --key="[path to the private key]" --cert="[path to the full chain]" ``` +### Create the Diffie-Hellman parameters + +Diffie-Hellman parameters allow per-session encryption of SSL traffic to help improve security. We currently store our parameters in KeyVault, the value can be updated using the following command. Note: Generating the new paramter can take over 10 minutes and there won't be any output while it's running. +``` +az keyvault secret set --vault-name --name --value "$(openssl genpkey -genparam -algorithm DH -outform pem -pkeyopt dh_paramgen_prime_len:4096 2> /dev/null)" +``` --- # Setting Up FlexVol for Secrets @@ -217,3 +224,45 @@ Example values: 5. The file `deploy/azure/aadpodidentity.yml` is templated via Kustomize, so you'll need to include clientId (as `KV_MI_CLIENT_ID`) and id (as `KV_MI_ID`) of the managed identity as part of the call to Kustomize. +## Using the FlexVol + +There are 3 steps to using the FlexVol to access secrets from KeyVault + +1. For the resource in which you would like to mount a FlexVol, add a metadata label with the selector from `aadpodidentity.yml` + ``` + metadata: + labels: + app: atst + role: web + aadpodidbinding: atat-kv-id-binding + ``` + +2. Register the FlexVol as a mount and specifiy which secrets you want to mount, along with the file name they should have. The `keyvaultobjectnames`, `keyvaultobjectaliases`, and `keyvaultobjecttypes` correspond to one another, positionally. They are passed as semicolon delimited strings, examples below. + + ``` + - name: volume-of-secrets + flexVolume: + driver: "azure/kv" + options: + usepodidentity: "true" + keyvaultname: "" + keyvaultobjectnames: "mysecret;mykey;mycert" + keyvaultobjectaliases: "mysecret.pem;mykey.txt;mycert.crt" + keyvaultobjecttypes: "secret;key;cert" + tenantid: $TENANT_ID + ``` + +3. Tell the resource where to mount your new volume, using the same name that you specified for the volume above. + ``` + - name: nginx-secret + mountPath: "/usr/secrets/" + readOnly: true + ``` + +4. Once applied, the directory specified in the `mountPath` argument will contain the files you specified in the flexVolume. In our case, you would be able to do this: + ``` + $ kubectl exec -it CONTAINER_NAME -c atst ls /usr/secrets + mycert.crt + mykey.txt + mysecret.pem + ``` diff --git a/deploy/azure/atst-nginx-configmap.yml b/deploy/azure/atst-nginx-configmap.yml index e19c6d54..5f51c7d6 100644 --- a/deploy/azure/atst-nginx-configmap.yml +++ b/deploy/azure/atst-nginx-configmap.yml @@ -5,8 +5,10 @@ metadata: name: atst-nginx namespace: atat data: - nginx-config: |- + atst.conf: |- server { + access_log /var/log/nginx/access.log json; + listen ${PORT_PREFIX}342; server_name ${MAIN_DOMAIN}; root /usr/share/nginx/html; @@ -18,6 +20,8 @@ data: } } server { + access_log /var/log/nginx/access.log json; + listen ${PORT_PREFIX}343; server_name ${AUTH_DOMAIN}; root /usr/share/nginx/html; @@ -29,12 +33,17 @@ data: } } server { + access_log /var/log/nginx/access.log json; + server_name ${MAIN_DOMAIN}; # access_log /var/log/nginx/access.log json; listen ${PORT_PREFIX}442 ssl; listen [::]:${PORT_PREFIX}442 ssl ipv6only=on; - ssl_certificate /etc/ssl/private/atat.crt; - ssl_certificate_key /etc/ssl/private/atat.key; + ssl_certificate /etc/ssl/atat.crt; + ssl_certificate_key /etc/ssl/atat.key; + # additional SSL/TLS settings + include /etc/nginx/snippets/ssl.conf; + location /login-redirect { return 301 https://auth-azure.atat.code.mil$request_uri; } @@ -58,18 +67,20 @@ data: } } server { - # access_log /var/log/nginx/access.log json; + access_log /var/log/nginx/access.log json; + server_name ${AUTH_DOMAIN}; listen ${PORT_PREFIX}443 ssl; listen [::]:${PORT_PREFIX}443 ssl ipv6only=on; - ssl_certificate /etc/ssl/private/atat.crt; - ssl_certificate_key /etc/ssl/private/atat.key; + ssl_certificate /etc/ssl/atat.crt; + ssl_certificate_key /etc/ssl/atat.key; # Request and validate client certificate ssl_verify_client on; ssl_verify_depth 10; ssl_client_certificate /etc/ssl/client-ca-bundle.pem; - # Guard against HTTPS -> HTTP downgrade - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always"; + # additional SSL/TLS settings + include /etc/nginx/snippets/ssl.conf; + location / { return 301 https://azure.atat.code.mil$request_uri; } @@ -88,3 +99,18 @@ data: uwsgi_param HTTP_X_REQUEST_ID $request_id; } } + 00json_log.conf: |- + log_format json escape=json + '{' + '"timestamp":"$time_iso8601",' + '"msec":"$msec",' + '"request_id":"$request_id",' + '"remote_addr":"$remote_addr",' + '"remote_user":"$remote_user",' + '"request":"$request",' + '"status":$status,' + '"body_bytes_sent":$body_bytes_sent,' + '"referer":"$http_referer",' + '"user_agent":"$http_user_agent",' + '"http_x_forwarded_for":"$http_x_forwarded_for"' + '}'; diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index 8d46fa4b..02952029 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -23,6 +23,7 @@ spec: labels: app: atst role: web + aadpodidbinding: atat-kv-id-binding spec: securityContext: fsGroup: 101 @@ -30,8 +31,8 @@ spec: - name: atst image: $CONTAINER_IMAGE envFrom: - - configMapRef: - name: atst-envvars + - configMapRef: + name: atst-envvars volumeMounts: - name: atst-config mountPath: "/opt/atat/atst/atst-overrides.ini" @@ -62,37 +63,39 @@ spec: name: auth volumeMounts: - name: nginx-config - mountPath: "/etc/nginx/conf.d/atst.conf" - subPath: atst.conf + mountPath: "/etc/nginx/conf.d/" - name: uwsgi-socket-dir mountPath: "/var/run/uwsgi" - name: nginx-htpasswd mountPath: "/etc/nginx/.htpasswd" subPath: .htpasswd - - name: tls - mountPath: "/etc/ssl/private" - name: nginx-client-ca-bundle - mountPath: "/etc/ssl/" + mountPath: "/etc/ssl/client-ca-bundle.pem" + subPath: "client-ca-bundle.pem" - name: acme mountPath: "/usr/share/nginx/html/.well-known/acme-challenge/" + - name: snippets + mountPath: "/etc/nginx/snippets/" + - name: nginx-secret + mountPath: "/etc/ssl/" volumes: - name: atst-config secret: secretName: atst-config-ini items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 + - key: override.ini + path: atst-overrides.ini + mode: 0644 - name: nginx-client-ca-bundle configMap: name: nginx-client-ca-bundle - defaultMode: 0666 + defaultMode: 0444 + items: + - key: "client-ca-bundle.pem" + path: "client-ca-bundle.pem" - name: nginx-config configMap: name: atst-nginx - items: - - key: nginx-config - path: atst.conf - name: uwsgi-socket-dir emptyDir: medium: Memory @@ -100,19 +103,9 @@ spec: secret: secretName: atst-nginx-htpasswd items: - - key: htpasswd - path: .htpasswd - mode: 0640 - - name: tls - secret: - secretName: azure-atat-code-mil-tls - items: - - key: tls.crt - path: atat.crt - mode: 0644 - - key: tls.key - path: atat.key - mode: 0640 + - key: htpasswd + path: .htpasswd + mode: 0640 - name: crls-vol persistentVolumeClaim: claimName: crls-vol-claim @@ -120,9 +113,9 @@ spec: configMap: name: pgsslrootcert items: - - key: cert - path: pgsslrootcert.crt - mode: 0666 + - key: cert + path: pgsslrootcert.crt + mode: 0666 - name: acme configMap: name: acme-challenges @@ -132,9 +125,22 @@ spec: name: uwsgi-config defaultMode: 0666 items: - - key: uwsgi.ini - path: uwsgi.ini - mode: 0644 + - key: uwsgi.ini + path: uwsgi.ini + mode: 0644 + - name: snippets + configMap: + name: nginx-snippets + - name: nginx-secret + flexVolume: + driver: "azure/kv" + options: + usepodidentity: "true" + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "dhparam4096;master-cert;master-cert" + keyvaultobjectaliases: "dhparam.pem;atat.key;atat.crt" + keyvaultobjecttypes: "secret;secret;secret" + tenantid: $TENANT_ID --- apiVersion: extensions/v1beta1 kind: Deployment @@ -161,19 +167,20 @@ spec: containers: - name: atst-worker image: $CONTAINER_IMAGE - args: [ - "/opt/atat/atst/.venv/bin/python", - "/opt/atat/atst/.venv/bin/celery", - "-A", - "celery_worker.celery", - "worker", - "--loglevel=info" - ] + args: + [ + "/opt/atat/atst/.venv/bin/python", + "/opt/atat/atst/.venv/bin/celery", + "-A", + "celery_worker.celery", + "worker", + "--loglevel=info", + ] envFrom: - - configMapRef: - name: atst-envvars - - configMapRef: - name: atst-worker-envvars + - configMapRef: + name: atst-envvars + - configMapRef: + name: atst-worker-envvars volumeMounts: - name: atst-config mountPath: "/opt/atat/atst/atst-overrides.ini" @@ -186,16 +193,16 @@ spec: secret: secretName: atst-config-ini items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 + - key: override.ini + path: atst-overrides.ini + mode: 0644 - name: pgsslrootcert configMap: name: pgsslrootcert items: - - key: cert - path: pgsslrootcert.crt - mode: 0666 + - key: cert + path: pgsslrootcert.crt + mode: 0666 --- apiVersion: extensions/v1beta1 kind: Deployment @@ -222,19 +229,20 @@ spec: containers: - name: atst-beat image: $CONTAINER_IMAGE - args: [ - "/opt/atat/atst/.venv/bin/python", - "/opt/atat/atst/.venv/bin/celery", - "-A", - "celery_worker.celery", - "beat", - "--loglevel=info" - ] + args: + [ + "/opt/atat/atst/.venv/bin/python", + "/opt/atat/atst/.venv/bin/celery", + "-A", + "celery_worker.celery", + "beat", + "--loglevel=info", + ] envFrom: - - configMapRef: - name: atst-envvars - - configMapRef: - name: atst-worker-envvars + - configMapRef: + name: atst-envvars + - configMapRef: + name: atst-worker-envvars volumeMounts: - name: atst-config mountPath: "/opt/atat/atst/atst-overrides.ini" @@ -247,16 +255,16 @@ spec: secret: secretName: atst-config-ini items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 + - key: override.ini + path: atst-overrides.ini + mode: 0644 - name: pgsslrootcert configMap: name: pgsslrootcert items: - - key: cert - path: pgsslrootcert.crt - mode: 0666 + - key: cert + path: pgsslrootcert.crt + mode: 0666 --- apiVersion: v1 kind: Service @@ -268,12 +276,12 @@ metadata: spec: loadBalancerIP: 13.92.235.6 ports: - - port: 80 - targetPort: 8342 - name: http - - port: 443 - targetPort: 8442 - name: https + - port: 80 + targetPort: 8342 + name: http + - port: 443 + targetPort: 8442 + name: https selector: role: web type: LoadBalancer @@ -288,12 +296,12 @@ metadata: spec: loadBalancerIP: 23.100.24.41 ports: - - port: 80 - targetPort: 8343 - name: http - - port: 443 - targetPort: 8443 - name: https + - port: 80 + targetPort: 8343 + name: http + - port: 443 + targetPort: 8443 + name: https selector: role: web type: LoadBalancer diff --git a/deploy/azure/kustomization.yaml b/deploy/azure/kustomization.yaml index 43e6f813..9dee809c 100644 --- a/deploy/azure/kustomization.yaml +++ b/deploy/azure/kustomization.yaml @@ -11,3 +11,4 @@ resources: - nginx-client-ca-bundle.yml - acme-challenges.yml - aadpodidentity.yml + - nginx-snippets.yml diff --git a/deploy/azure/nginx-snippets.yml b/deploy/azure/nginx-snippets.yml new file mode 100644 index 00000000..916d9524 --- /dev/null +++ b/deploy/azure/nginx-snippets.yml @@ -0,0 +1,24 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-snippets + namespace: atat +data: + ssl.conf: |- + # Guard against HTTPS -> HTTP downgrade + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always"; + # Set SSL protocols, ciphers, and related options + ssl_protocols TLSv1.3 TLSv1.2; + ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers on; + ssl_ecdh_curve X25519:prime256v1: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; diff --git a/deploy/overlays/staging/flex_vol.yml b/deploy/overlays/staging/flex_vol.yml new file mode 100644 index 00000000..0ebeea84 --- /dev/null +++ b/deploy/overlays/staging/flex_vol.yml @@ -0,0 +1,13 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: atst +spec: + template: + spec: + volumes: + - name: nginx-secret + flexVolume: + options: + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "dhparam4096;staging-cert;staging-cert" diff --git a/deploy/overlays/staging/kustomization.yaml b/deploy/overlays/staging/kustomization.yaml index 83450cf5..38251002 100644 --- a/deploy/overlays/staging/kustomization.yaml +++ b/deploy/overlays/staging/kustomization.yaml @@ -7,6 +7,7 @@ patchesStrategicMerge: - replica_count.yml - ports.yml - envvars.yml + - flex_vol.yml patchesJson6902: - target: group: extensions diff --git a/script/k8s_config b/script/k8s_config index ee3c9878..b489c942 100755 --- a/script/k8s_config +++ b/script/k8s_config @@ -13,6 +13,7 @@ SETTINGS=( AUTH_DOMAIN KV_MI_ID KV_MI_CLIENT_ID + TENANT_ID ) # Loop all expected settings. Track ones that are missing and build diff --git a/script/seed_sample.py b/script/seed_sample.py index 11a45530..a9cec40b 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -30,6 +30,8 @@ from atst.domain.users import Users from atst.routes.dev import _DEV_USERS as DEV_USERS +from atst.utils import pick + from tests.factories import ( random_service_branch, TaskOrderFactory, @@ -238,6 +240,7 @@ def add_applications_to_portfolio(portfolio): None, first_name=user_data["first_name"], last_name=user_data["last_name"], + email=user_data["email"], ) app_role = ApplicationRoles.create( @@ -263,7 +266,23 @@ def add_applications_to_portfolio(portfolio): def create_demo_portfolio(name, data): try: - portfolio_owner = Users.get_or_create_by_dod_id("2345678901") # Amanda + portfolio_owner = Users.get_or_create_by_dod_id( + "2345678901", + **pick( + [ + "permission_sets", + "first_name", + "last_name", + "email", + "service_branch", + "phone_number", + "citizenship", + "designation", + "date_latest_training", + ], + DEV_USERS["amanda"], + ), + ) # Amanda # auditor = Users.get_by_dod_id("3453453453") # Sally except NotFoundError: print( diff --git a/tests/domain/test_portfolios.py b/tests/domain/test_portfolios.py index 80dade92..41e3fc81 100644 --- a/tests/domain/test_portfolios.py +++ b/tests/domain/test_portfolios.py @@ -9,6 +9,7 @@ from atst.domain.portfolios import ( ) from atst.domain.portfolio_roles import PortfolioRoles from atst.domain.applications import Applications +from atst.domain.application_roles import ApplicationRoles from atst.domain.environments import Environments from atst.domain.permission_sets import PermissionSets, PORTFOLIO_PERMISSION_SETS from atst.models.application_role import Status as ApplicationRoleStatus diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index 089741d5..8b1eb724 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -96,7 +96,6 @@ def test_create_adds_clins(): }, ] task_order = TaskOrders.create( - creator=portfolio.owner, portfolio_id=portfolio.id, number="0123456789", clins=clins, @@ -127,7 +126,6 @@ def test_update_adds_clins(): }, ] task_order = TaskOrders.create( - creator=task_order.creator, portfolio_id=task_order.portfolio_id, number="0000000000", clins=clins, diff --git a/tests/domain/test_users.py b/tests/domain/test_users.py index b5a24058..20bd8266 100644 --- a/tests/domain/test_users.py +++ b/tests/domain/test_users.py @@ -4,36 +4,41 @@ from uuid import uuid4 from atst.domain.users import Users from atst.domain.exceptions import NotFoundError, AlreadyExistsError, UnauthorizedError +from atst.utils import pick from tests.factories import UserFactory DOD_ID = "my_dod_id" +REQUIRED_KWARGS = {"first_name": "Luke", "last_name": "Skywalker"} def test_create_user(): - user = Users.create(DOD_ID) + user = Users.create(DOD_ID, **REQUIRED_KWARGS) assert user.dod_id == DOD_ID def test_create_user_with_existing_email(): - Users.create(DOD_ID, email="thisusersemail@usersRus.com") + Users.create(DOD_ID, email="thisusersemail@usersRus.com", **REQUIRED_KWARGS) with pytest.raises(AlreadyExistsError): Users.create(DOD_ID, email="thisusersemail@usersRus.com") def test_create_user_with_nonexistent_permission_set(): with pytest.raises(NotFoundError): - Users.create(DOD_ID, permission_sets=["nonexistent"]) + Users.create(DOD_ID, permission_sets=["nonexistent"], **REQUIRED_KWARGS) def test_get_or_create_nonexistent_user(): - user = Users.get_or_create_by_dod_id(DOD_ID) + user = Users.get_or_create_by_dod_id(DOD_ID, **REQUIRED_KWARGS) assert user.dod_id == DOD_ID def test_get_or_create_existing_user(): fact_user = UserFactory.create() - user = Users.get_or_create_by_dod_id(fact_user.dod_id) + user = Users.get_or_create_by_dod_id( + fact_user.dod_id, + **pick(["first_name", "last_name"], fact_user.to_dictionary()), + ) assert user == fact_user diff --git a/tests/factories.py b/tests/factories.py index efb6fb82..9c8f8c02 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -236,9 +236,17 @@ class ApplicationRoleFactory(Base): @classmethod def _create(cls, model_class, *args, **kwargs): with_invite = kwargs.pop("invite", True) - app_role = super()._create(model_class, *args, **kwargs) + app_role = model_class(*args, **kwargs) - if with_invite: + if with_invite and app_role.user: + ApplicationInvitationFactory.create( + role=app_role, + dod_id=app_role.user.dod_id, + first_name=app_role.user.first_name, + last_name=app_role.user.last_name, + email=app_role.user.email, + ) + elif with_invite: ApplicationInvitationFactory.create(role=app_role) return app_role @@ -260,6 +268,14 @@ class PortfolioInvitationFactory(Base): email = factory.Faker("email") status = InvitationStatus.PENDING expiration_time = PortfolioInvitations.current_expiration_time() + dod_id = factory.LazyFunction(random_dod_id) + first_name = factory.Faker("first_name") + last_name = factory.Faker("last_name") + + @classmethod + def _create(cls, model_class, *args, **kwargs): + inviter_id = kwargs.pop("inviter_id", UserFactory.create().id) + return super()._create(model_class, inviter_id=inviter_id, *args, **kwargs) class ApplicationInvitationFactory(Base): @@ -270,6 +286,14 @@ class ApplicationInvitationFactory(Base): status = InvitationStatus.PENDING expiration_time = PortfolioInvitations.current_expiration_time() role = factory.SubFactory(ApplicationRoleFactory, invite=False) + dod_id = factory.LazyFunction(random_dod_id) + first_name = factory.Faker("first_name") + last_name = factory.Faker("last_name") + + @classmethod + def _create(cls, model_class, *args, **kwargs): + inviter_id = kwargs.pop("inviter_id", UserFactory.create().id) + return super()._create(model_class, inviter_id=inviter_id, *args, **kwargs) class AttachmentFactory(Base): @@ -284,11 +308,8 @@ class TaskOrderFactory(Base): class Meta: model = TaskOrder - portfolio = factory.SubFactory( - PortfolioFactory, owner=factory.SelfAttribute("..creator") - ) + portfolio = factory.SubFactory(PortfolioFactory) number = factory.LazyFunction(random_task_order_number) - creator = factory.SubFactory(UserFactory) signed_at = None _pdf = factory.SubFactory(AttachmentFactory) diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index cb1ca7de..91fee15b 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -76,7 +76,7 @@ class TestTaskOrderStatus: @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock) def test_draft_status(self, is_signed, is_completed): # Given that I have a TO that is neither completed nor signed - to = TaskOrder() + to = TaskOrderFactory.create() is_signed.return_value = False is_completed.return_value = False @@ -89,7 +89,7 @@ class TestTaskOrderStatus: def test_active_status(self, is_signed, is_completed, start_date, end_date): # Given that I have a signed TO and today is within its start_date and end_date today = pendulum.today().date() - to = TaskOrder() + to = TaskOrderFactory.create() start_date.return_value = today.subtract(days=1) end_date.return_value = today.add(days=1) @@ -105,7 +105,7 @@ class TestTaskOrderStatus: @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock) def test_upcoming_status(self, is_signed, is_completed, start_date, end_date): # Given that I have a signed TO and today is before its start_date - to = TaskOrder() + to = TaskOrderFactory.create() start_date.return_value = pendulum.today().add(days=1).date() end_date.return_value = pendulum.today().add(days=2).date() is_signed.return_value = True @@ -120,7 +120,7 @@ class TestTaskOrderStatus: @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock) def test_expired_status(self, is_signed, is_completed, end_date, start_date): # Given that I have a signed TO and today is after its expiration date - to = TaskOrder() + to = TaskOrderFactory.create() end_date.return_value = pendulum.today().subtract(days=1).date() start_date.return_value = pendulum.today().subtract(days=2).date() is_signed.return_value = True @@ -143,7 +143,7 @@ class TestTaskOrderStatus: class TestBudget: def test_total_contract_amount(self): - to = TaskOrder() + to = TaskOrderFactory.create() assert to.total_contract_amount == 0 clin1 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_1) @@ -156,7 +156,7 @@ class TestBudget: ) def test_total_obligated_funds(self): - to = TaskOrder() + to = TaskOrderFactory.create() assert to.total_obligated_funds == 0 clin1 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_1) diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index c800ce25..708b2bdc 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -30,7 +30,7 @@ def task_order(): portfolio = PortfolioFactory.create(owner=user) attachment = Attachment(filename="sample_attachment", object_name="sample") - return TaskOrderFactory.create(creator=user, portfolio=portfolio) + return TaskOrderFactory.create(portfolio=portfolio) def test_review_task_order_not_draft(client, user_session, task_order): diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py index 61a97b82..4d2f5fdb 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -20,14 +20,13 @@ def task_order(): user = UserFactory.create() portfolio = PortfolioFactory.create(owner=user) - return TaskOrderFactory.create(creator=user, portfolio=portfolio) + return TaskOrderFactory.create(portfolio=portfolio) @pytest.fixture def completed_task_order(): portfolio = PortfolioFactory.create() task_order = TaskOrderFactory.create( - creator=portfolio.owner, portfolio=portfolio, create_clins=[{"number": "1234567890123456789012345678901234567890123"}], ) @@ -68,7 +67,7 @@ def test_task_orders_submit_form_step_one_add_pdf(client, user_session, portfoli def test_task_orders_form_step_one_add_pdf_existing_to( client, user_session, task_order ): - user_session(task_order.creator) + user_session(task_order.portfolio.owner) response = client.get( url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order.id) ) @@ -77,7 +76,7 @@ def test_task_orders_form_step_one_add_pdf_existing_to( def test_task_orders_submit_form_step_one_add_pdf_existing_to(client, user_session): task_order = TaskOrderFactory.create() - user_session(task_order.creator) + user_session(task_order.portfolio.owner) response = client.post( url_for( "task_orders.submit_form_step_one_add_pdf", task_order_id=task_order.id @@ -140,7 +139,7 @@ def test_task_orders_submit_form_step_one_validates_object_name( def test_task_orders_form_step_two_add_number(client, user_session, task_order): - user_session(task_order.creator) + user_session(task_order.portfolio.owner) response = client.get( url_for("task_orders.form_step_two_add_number", task_order_id=task_order.id) ) @@ -148,7 +147,7 @@ def test_task_orders_form_step_two_add_number(client, user_session, task_order): def test_task_orders_submit_form_step_two_add_number(client, user_session, task_order): - user_session(task_order.creator) + user_session(task_order.portfolio.owner) form_data = {"number": "1234567890"} response = client.post( url_for( @@ -164,7 +163,7 @@ def test_task_orders_submit_form_step_two_add_number(client, user_session, task_ def test_task_orders_submit_form_step_two_add_number_existing_to( client, user_session, task_order ): - user_session(task_order.creator) + user_session(task_order.portfolio.owner) form_data = {"number": "0000000000"} original_number = task_order.number response = client.post( @@ -179,7 +178,7 @@ def test_task_orders_submit_form_step_two_add_number_existing_to( def test_task_orders_form_step_three_add_clins(client, user_session, task_order): - user_session(task_order.creator) + user_session(task_order.portfolio.owner) response = client.get( url_for("task_orders.form_step_three_add_clins", task_order_id=task_order.id) ) @@ -187,7 +186,7 @@ def test_task_orders_form_step_three_add_clins(client, user_session, task_order) def test_task_orders_submit_form_step_three_add_clins(client, user_session, task_order): - user_session(task_order.creator) + user_session(task_order.portfolio.owner) form_data = { "clins-0-jedi_clin_type": "JEDI_CLIN_1", "clins-0-clin_number": "12312", @@ -237,7 +236,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to( TaskOrders.create_clins(task_order.id, clin_list) assert len(task_order.clins) == 2 - user_session(task_order.creator) + user_session(task_order.portfolio.owner) form_data = { "clins-0-jedi_clin_type": "JEDI_CLIN_1", "clins-0-clin_number": "12312", @@ -258,7 +257,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to( def test_task_orders_form_step_four_review(client, user_session, completed_task_order): - user_session(completed_task_order.creator) + user_session(completed_task_order.portfolio.owner) response = client.get( url_for( "task_orders.form_step_four_review", task_order_id=completed_task_order.id @@ -270,7 +269,7 @@ def test_task_orders_form_step_four_review(client, user_session, completed_task_ def test_task_orders_form_step_four_review_incomplete_to( client, user_session, task_order ): - user_session(task_order.creator) + user_session(task_order.portfolio.owner) response = client.get( url_for("task_orders.form_step_four_review", task_order_id=task_order.id) ) @@ -280,7 +279,7 @@ def test_task_orders_form_step_four_review_incomplete_to( def test_task_orders_form_step_five_confirm_signature( client, user_session, completed_task_order ): - user_session(completed_task_order.creator) + user_session(completed_task_order.portfolio.owner) response = client.get( url_for( "task_orders.form_step_five_confirm_signature", @@ -293,7 +292,7 @@ def test_task_orders_form_step_five_confirm_signature( def test_task_orders_form_step_five_confirm_signature_incomplete_to( client, user_session, task_order ): - user_session(task_order.creator) + user_session(task_order.portfolio.owner) response = client.get( url_for( "task_orders.form_step_five_confirm_signature", task_order_id=task_order.id @@ -340,9 +339,7 @@ def test_task_orders_submit_task_order(client, user_session, task_order): def test_task_orders_edit_redirects_to_latest_incomplete_step( client, user_session, portfolio, to_factory_args, expected_step ): - task_order = TaskOrderFactory.create( - portfolio=portfolio, creator=portfolio.owner, **to_factory_args - ) + task_order = TaskOrderFactory.create(portfolio=portfolio, **to_factory_args) user_session(portfolio.owner) response = client.get(url_for("task_orders.edit", task_order_id=task_order.id)) @@ -414,8 +411,7 @@ def test_task_orders_update_invalid_data(client, user_session, portfolio): @pytest.mark.skip(reason="Update after implementing errors on TO form") def test_task_order_form_shows_errors(client, user_session, task_order): - creator = task_order.creator - user_session(creator) + user_session(task_order.portfolio.owner) task_order_data = TaskOrderFactory.dictionary() funding_data = slice_data_for_section(task_order_data, "funding") diff --git a/tests/test_access.py b/tests/test_access.py index 8f3d201f..24948ec2 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -487,7 +487,9 @@ def test_portfolios_resend_invitation_access(post_url_assert_status): portfolio = PortfolioFactory.create(owner=owner) prr = PortfolioRoleFactory.create(user=invitee, portfolio=portfolio) - invite = PortfolioInvitationFactory.create(user=UserFactory.create(), role=prr) + invite = PortfolioInvitationFactory.create( + user=UserFactory.create(), role=prr, inviter_id=owner.id + ) url = url_for( "portfolios.resend_invitation", @@ -651,7 +653,6 @@ def test_task_orders_new_get_routes(get_url_assert_status): portfolio = PortfolioFactory.create(owner=owner) task_order = TaskOrderFactory.create( - creator=owner, portfolio=portfolio, create_clins=[{"number": "1234567890123456789012345678901234567890123"}], ) @@ -689,7 +690,7 @@ def test_task_orders_new_post_routes(post_url_assert_status): rando = user_with() portfolio = PortfolioFactory.create(owner=owner) - task_order = TaskOrderFactory.create(portfolio=portfolio, creator=owner) + task_order = TaskOrderFactory.create(portfolio=portfolio) for route, data in post_routes: url = url_for(route, task_order_id=task_order.id)