Merge branch 'staging' into upload-timing-bug

This commit is contained in:
leigh-mil 2019-12-03 10:26:27 -05:00 committed by GitHub
commit dd6d516d2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 548 additions and 172 deletions

View File

@ -293,6 +293,11 @@ workflows:
- integration-tests: - integration-tests:
requires: requires:
- docker-build - docker-build
filters:
branches:
only:
- staging
- master
- deploy-staging: - deploy-staging:
requires: requires:
- test - test

1
.gitignore vendored
View File

@ -33,6 +33,7 @@ static/buildinfo.*
log/* log/*
config/dev.ini config/dev.ini
.env*
# CRLs # CRLs
/crl /crl

View File

@ -161,7 +161,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false, "is_secret": false,
"is_verified": false, "is_verified": false,
"line_number": 32, "line_number": 31,
"type": "Hex High Entropy String" "type": "Hex High Entropy String"
} }
], ],
@ -170,7 +170,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false, "is_secret": false,
"is_verified": false, "is_verified": false,
"line_number": 656, "line_number": 657,
"type": "Hex High Entropy String" "type": "Hex High Entropy String"
} }
] ]

View File

@ -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 ###

View File

@ -11,10 +11,8 @@ class TaskOrders(BaseDomainClass):
resource_name = "task_order" resource_name = "task_order"
@classmethod @classmethod
def create(cls, creator, portfolio_id, number, clins, pdf): def create(cls, portfolio_id, number, clins, pdf):
task_order = TaskOrder( task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf)
portfolio_id=portfolio_id, creator=creator, number=number, pdf=pdf
)
db.session.add(task_order) db.session.add(task_order)
db.session.commit() db.session.commit()

View File

@ -7,7 +7,7 @@ from wtforms.fields import (
HiddenField, HiddenField,
) )
from wtforms.fields.html5 import DateField 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 flask_wtf import FlaskForm
from numbers import Number from numbers import Number
@ -61,9 +61,7 @@ class CLINForm(FlaskForm):
coerce=coerce_enum, coerce=coerce_enum,
) )
number = StringField( number = StringField(label=translate("task_orders.form.clin_number_label"))
label=translate("task_orders.form.clin_number_label"), validators=[Optional()]
)
start_date = DateField( start_date = DateField(
translate("task_orders.form.pop_start"), translate("task_orders.form.pop_start"),
description=translate("task_orders.form.pop_example"), description=translate("task_orders.form.pop_example"),

View File

@ -12,7 +12,10 @@ class ApplicationInvitation(
__tablename__ = "application_invitations" __tablename__ = "application_invitations"
application_role_id = Column( 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( role = relationship(
"ApplicationRole", "ApplicationRole",

View File

@ -46,7 +46,9 @@ class ApplicationRole(
UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True 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( permission_sets = relationship(
"PermissionSet", secondary=application_roles_permission_sets "PermissionSet", secondary=application_roles_permission_sets

View File

@ -23,12 +23,12 @@ class CLIN(Base, mixins.TimestampsMixin):
task_order_id = Column(ForeignKey("task_orders.id"), nullable=False) task_order_id = Column(ForeignKey("task_orders.id"), nullable=False)
task_order = relationship("TaskOrder") task_order = relationship("TaskOrder")
number = Column(String, nullable=True) number = Column(String, nullable=False)
start_date = Column(Date, nullable=True) start_date = Column(Date, nullable=False)
end_date = Column(Date, nullable=True) end_date = Column(Date, nullable=False)
total_amount = Column(Numeric(scale=2), nullable=True) total_amount = Column(Numeric(scale=2), nullable=False)
obligated_amount = Column(Numeric(scale=2), nullable=True) obligated_amount = Column(Numeric(scale=2), nullable=False)
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=True) jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False)
# #
# NOTE: For now obligated CLINS are CLIN 1 + CLIN 3 # NOTE: For now obligated CLINS are CLIN 1 + CLIN 3

View File

@ -43,7 +43,9 @@ class EnvironmentRole(
COMPLETED = "completed" COMPLETED = "completed"
DISABLED = "disabled" 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): def __repr__(self):
return "<EnvironmentRole(role='{}', user='{}', environment='{}', id='{}')>".format( return "<EnvironmentRole(role='{}', user='{}', environment='{}', id='{}')>".format(

View File

@ -31,23 +31,29 @@ class InvitesMixin(object):
@declared_attr @declared_attr
def inviter_id(cls): 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 @declared_attr
def inviter(cls): def inviter(cls):
return relationship("User", foreign_keys=[cls.inviter_id]) 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) email = Column(String, nullable=False)
dod_id = Column(String) dod_id = Column(String, nullable=False)
first_name = Column(String) first_name = Column(String, nullable=False)
last_name = Column(String) last_name = Column(String, nullable=False)
phone_number = Column(String) phone_number = Column(String)
phone_ext = Column(String) phone_ext = Column(String)

View File

@ -18,8 +18,10 @@ class Portfolio(
__tablename__ = "portfolios" __tablename__ = "portfolios"
id = types.Id() id = types.Id()
name = Column(String) name = Column(String, nullable=False)
defense_component = Column(String) # Department of Defense Component defense_component = Column(
String, nullable=False
) # Department of Defense Component
app_migration = Column(String) # App Migration app_migration = Column(String) # App Migration
complexity = Column(ARRAY(String)) # Application Complexity complexity = Column(ARRAY(String)) # Application Complexity

View File

@ -12,7 +12,7 @@ class PortfolioInvitation(
__tablename__ = "portfolio_invitations" __tablename__ = "portfolio_invitations"
portfolio_role_id = Column( 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( role = relationship(
"PortfolioRole", "PortfolioRole",

View File

@ -52,7 +52,9 @@ class PortfolioRole(
UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True 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( permission_sets = relationship(
"PermissionSet", secondary=portfolio_roles_permission_sets "PermissionSet", secondary=portfolio_roles_permission_sets

View File

@ -33,12 +33,9 @@ class TaskOrder(Base, mixins.TimestampsMixin):
id = types.Id() id = types.Id()
portfolio_id = Column(ForeignKey("portfolios.id")) portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False)
portfolio = relationship("Portfolio") 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_attachment_id = Column(ForeignKey("attachments.id"))
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id]) _pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
number = Column(String) # Task Order Number number = Column(String) # Task Order Number

View File

@ -56,8 +56,8 @@ class User(
email = Column(String) email = Column(String)
dod_id = Column(String, unique=True, nullable=False) dod_id = Column(String, unique=True, nullable=False)
first_name = Column(String) first_name = Column(String, nullable=False)
last_name = Column(String) last_name = Column(String, nullable=False)
phone_number = Column(String) phone_number = Column(String)
phone_ext = Column(String) phone_ext = Column(String)
service_branch = Column(String) service_branch = Column(String)

View File

@ -66,7 +66,7 @@ def update_task_order(
task_order = TaskOrders.update(task_order_id, **form.data) task_order = TaskOrders.update(task_order_id, **form.data)
portfolio_id = task_order.portfolio_id portfolio_id = task_order.portfolio_id
else: 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)) return redirect(url_for(next_page, task_order_id=task_order.id))
else: else:
@ -181,9 +181,7 @@ def cancel_edit(task_order_id=None, portfolio_id=None):
if task_order_id: if task_order_id:
task_order = TaskOrders.update(task_order_id, **form.data) task_order = TaskOrders.update(task_order_id, **form.data)
else: else:
task_order = TaskOrders.create( task_order = TaskOrders.create(portfolio_id, **form.data)
g.current_user, portfolio_id, **form.data
)
elif not save and task_order_id: elif not save and task_order_id:
TaskOrders.delete(task_order_id) TaskOrders.delete(task_order_id)

View File

@ -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. - 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_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. - 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. 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]" 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 <VAULT NAME> --name <NAME OF PARAM> --value "$(openssl genpkey -genparam -algorithm DH -outform pem -pkeyopt dh_paramgen_prime_len:4096 2> /dev/null)"
```
--- ---
# Setting Up FlexVol for Secrets # 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. 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: "<NAME OF KEY VAULT>"
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
```

View File

@ -5,8 +5,10 @@ metadata:
name: atst-nginx name: atst-nginx
namespace: atat namespace: atat
data: data:
nginx-config: |- atst.conf: |-
server { server {
access_log /var/log/nginx/access.log json;
listen ${PORT_PREFIX}342; listen ${PORT_PREFIX}342;
server_name ${MAIN_DOMAIN}; server_name ${MAIN_DOMAIN};
root /usr/share/nginx/html; root /usr/share/nginx/html;
@ -18,6 +20,8 @@ data:
} }
} }
server { server {
access_log /var/log/nginx/access.log json;
listen ${PORT_PREFIX}343; listen ${PORT_PREFIX}343;
server_name ${AUTH_DOMAIN}; server_name ${AUTH_DOMAIN};
root /usr/share/nginx/html; root /usr/share/nginx/html;
@ -29,12 +33,17 @@ data:
} }
} }
server { server {
access_log /var/log/nginx/access.log json;
server_name ${MAIN_DOMAIN}; server_name ${MAIN_DOMAIN};
# access_log /var/log/nginx/access.log json; # access_log /var/log/nginx/access.log json;
listen ${PORT_PREFIX}442 ssl; listen ${PORT_PREFIX}442 ssl;
listen [::]:${PORT_PREFIX}442 ssl ipv6only=on; listen [::]:${PORT_PREFIX}442 ssl ipv6only=on;
ssl_certificate /etc/ssl/private/atat.crt; ssl_certificate /etc/ssl/atat.crt;
ssl_certificate_key /etc/ssl/private/atat.key; ssl_certificate_key /etc/ssl/atat.key;
# additional SSL/TLS settings
include /etc/nginx/snippets/ssl.conf;
location /login-redirect { location /login-redirect {
return 301 https://auth-azure.atat.code.mil$request_uri; return 301 https://auth-azure.atat.code.mil$request_uri;
} }
@ -58,18 +67,20 @@ data:
} }
} }
server { server {
# access_log /var/log/nginx/access.log json; access_log /var/log/nginx/access.log json;
server_name ${AUTH_DOMAIN}; server_name ${AUTH_DOMAIN};
listen ${PORT_PREFIX}443 ssl; listen ${PORT_PREFIX}443 ssl;
listen [::]:${PORT_PREFIX}443 ssl ipv6only=on; listen [::]:${PORT_PREFIX}443 ssl ipv6only=on;
ssl_certificate /etc/ssl/private/atat.crt; ssl_certificate /etc/ssl/atat.crt;
ssl_certificate_key /etc/ssl/private/atat.key; ssl_certificate_key /etc/ssl/atat.key;
# Request and validate client certificate # Request and validate client certificate
ssl_verify_client on; ssl_verify_client on;
ssl_verify_depth 10; ssl_verify_depth 10;
ssl_client_certificate /etc/ssl/client-ca-bundle.pem; ssl_client_certificate /etc/ssl/client-ca-bundle.pem;
# Guard against HTTPS -> HTTP downgrade # additional SSL/TLS settings
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always"; include /etc/nginx/snippets/ssl.conf;
location / { location / {
return 301 https://azure.atat.code.mil$request_uri; return 301 https://azure.atat.code.mil$request_uri;
} }
@ -88,3 +99,18 @@ data:
uwsgi_param HTTP_X_REQUEST_ID $request_id; 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"'
'}';

View File

@ -23,6 +23,7 @@ spec:
labels: labels:
app: atst app: atst
role: web role: web
aadpodidbinding: atat-kv-id-binding
spec: spec:
securityContext: securityContext:
fsGroup: 101 fsGroup: 101
@ -30,8 +31,8 @@ spec:
- name: atst - name: atst
image: $CONTAINER_IMAGE image: $CONTAINER_IMAGE
envFrom: envFrom:
- configMapRef: - configMapRef:
name: atst-envvars name: atst-envvars
volumeMounts: volumeMounts:
- name: atst-config - name: atst-config
mountPath: "/opt/atat/atst/atst-overrides.ini" mountPath: "/opt/atat/atst/atst-overrides.ini"
@ -62,37 +63,39 @@ spec:
name: auth name: auth
volumeMounts: volumeMounts:
- name: nginx-config - name: nginx-config
mountPath: "/etc/nginx/conf.d/atst.conf" mountPath: "/etc/nginx/conf.d/"
subPath: atst.conf
- name: uwsgi-socket-dir - name: uwsgi-socket-dir
mountPath: "/var/run/uwsgi" mountPath: "/var/run/uwsgi"
- name: nginx-htpasswd - name: nginx-htpasswd
mountPath: "/etc/nginx/.htpasswd" mountPath: "/etc/nginx/.htpasswd"
subPath: .htpasswd subPath: .htpasswd
- name: tls
mountPath: "/etc/ssl/private"
- name: nginx-client-ca-bundle - name: nginx-client-ca-bundle
mountPath: "/etc/ssl/" mountPath: "/etc/ssl/client-ca-bundle.pem"
subPath: "client-ca-bundle.pem"
- name: acme - name: acme
mountPath: "/usr/share/nginx/html/.well-known/acme-challenge/" mountPath: "/usr/share/nginx/html/.well-known/acme-challenge/"
- name: snippets
mountPath: "/etc/nginx/snippets/"
- name: nginx-secret
mountPath: "/etc/ssl/"
volumes: volumes:
- name: atst-config - name: atst-config
secret: secret:
secretName: atst-config-ini secretName: atst-config-ini
items: items:
- key: override.ini - key: override.ini
path: atst-overrides.ini path: atst-overrides.ini
mode: 0644 mode: 0644
- name: nginx-client-ca-bundle - name: nginx-client-ca-bundle
configMap: configMap:
name: nginx-client-ca-bundle name: nginx-client-ca-bundle
defaultMode: 0666 defaultMode: 0444
items:
- key: "client-ca-bundle.pem"
path: "client-ca-bundle.pem"
- name: nginx-config - name: nginx-config
configMap: configMap:
name: atst-nginx name: atst-nginx
items:
- key: nginx-config
path: atst.conf
- name: uwsgi-socket-dir - name: uwsgi-socket-dir
emptyDir: emptyDir:
medium: Memory medium: Memory
@ -100,19 +103,9 @@ spec:
secret: secret:
secretName: atst-nginx-htpasswd secretName: atst-nginx-htpasswd
items: items:
- key: htpasswd - key: htpasswd
path: .htpasswd path: .htpasswd
mode: 0640 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
- name: crls-vol - name: crls-vol
persistentVolumeClaim: persistentVolumeClaim:
claimName: crls-vol-claim claimName: crls-vol-claim
@ -120,9 +113,9 @@ spec:
configMap: configMap:
name: pgsslrootcert name: pgsslrootcert
items: items:
- key: cert - key: cert
path: pgsslrootcert.crt path: pgsslrootcert.crt
mode: 0666 mode: 0666
- name: acme - name: acme
configMap: configMap:
name: acme-challenges name: acme-challenges
@ -132,9 +125,22 @@ spec:
name: uwsgi-config name: uwsgi-config
defaultMode: 0666 defaultMode: 0666
items: items:
- key: uwsgi.ini - key: uwsgi.ini
path: uwsgi.ini path: uwsgi.ini
mode: 0644 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 apiVersion: extensions/v1beta1
kind: Deployment kind: Deployment
@ -161,19 +167,20 @@ spec:
containers: containers:
- name: atst-worker - name: atst-worker
image: $CONTAINER_IMAGE image: $CONTAINER_IMAGE
args: [ args:
"/opt/atat/atst/.venv/bin/python", [
"/opt/atat/atst/.venv/bin/celery", "/opt/atat/atst/.venv/bin/python",
"-A", "/opt/atat/atst/.venv/bin/celery",
"celery_worker.celery", "-A",
"worker", "celery_worker.celery",
"--loglevel=info" "worker",
] "--loglevel=info",
]
envFrom: envFrom:
- configMapRef: - configMapRef:
name: atst-envvars name: atst-envvars
- configMapRef: - configMapRef:
name: atst-worker-envvars name: atst-worker-envvars
volumeMounts: volumeMounts:
- name: atst-config - name: atst-config
mountPath: "/opt/atat/atst/atst-overrides.ini" mountPath: "/opt/atat/atst/atst-overrides.ini"
@ -186,16 +193,16 @@ spec:
secret: secret:
secretName: atst-config-ini secretName: atst-config-ini
items: items:
- key: override.ini - key: override.ini
path: atst-overrides.ini path: atst-overrides.ini
mode: 0644 mode: 0644
- name: pgsslrootcert - name: pgsslrootcert
configMap: configMap:
name: pgsslrootcert name: pgsslrootcert
items: items:
- key: cert - key: cert
path: pgsslrootcert.crt path: pgsslrootcert.crt
mode: 0666 mode: 0666
--- ---
apiVersion: extensions/v1beta1 apiVersion: extensions/v1beta1
kind: Deployment kind: Deployment
@ -222,19 +229,20 @@ spec:
containers: containers:
- name: atst-beat - name: atst-beat
image: $CONTAINER_IMAGE image: $CONTAINER_IMAGE
args: [ args:
"/opt/atat/atst/.venv/bin/python", [
"/opt/atat/atst/.venv/bin/celery", "/opt/atat/atst/.venv/bin/python",
"-A", "/opt/atat/atst/.venv/bin/celery",
"celery_worker.celery", "-A",
"beat", "celery_worker.celery",
"--loglevel=info" "beat",
] "--loglevel=info",
]
envFrom: envFrom:
- configMapRef: - configMapRef:
name: atst-envvars name: atst-envvars
- configMapRef: - configMapRef:
name: atst-worker-envvars name: atst-worker-envvars
volumeMounts: volumeMounts:
- name: atst-config - name: atst-config
mountPath: "/opt/atat/atst/atst-overrides.ini" mountPath: "/opt/atat/atst/atst-overrides.ini"
@ -247,16 +255,16 @@ spec:
secret: secret:
secretName: atst-config-ini secretName: atst-config-ini
items: items:
- key: override.ini - key: override.ini
path: atst-overrides.ini path: atst-overrides.ini
mode: 0644 mode: 0644
- name: pgsslrootcert - name: pgsslrootcert
configMap: configMap:
name: pgsslrootcert name: pgsslrootcert
items: items:
- key: cert - key: cert
path: pgsslrootcert.crt path: pgsslrootcert.crt
mode: 0666 mode: 0666
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@ -268,12 +276,12 @@ metadata:
spec: spec:
loadBalancerIP: 13.92.235.6 loadBalancerIP: 13.92.235.6
ports: ports:
- port: 80 - port: 80
targetPort: 8342 targetPort: 8342
name: http name: http
- port: 443 - port: 443
targetPort: 8442 targetPort: 8442
name: https name: https
selector: selector:
role: web role: web
type: LoadBalancer type: LoadBalancer
@ -288,12 +296,12 @@ metadata:
spec: spec:
loadBalancerIP: 23.100.24.41 loadBalancerIP: 23.100.24.41
ports: ports:
- port: 80 - port: 80
targetPort: 8343 targetPort: 8343
name: http name: http
- port: 443 - port: 443
targetPort: 8443 targetPort: 8443
name: https name: https
selector: selector:
role: web role: web
type: LoadBalancer type: LoadBalancer

View File

@ -11,3 +11,4 @@ resources:
- nginx-client-ca-bundle.yml - nginx-client-ca-bundle.yml
- acme-challenges.yml - acme-challenges.yml
- aadpodidentity.yml - aadpodidentity.yml
- nginx-snippets.yml

View File

@ -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;

View File

@ -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"

View File

@ -7,6 +7,7 @@ patchesStrategicMerge:
- replica_count.yml - replica_count.yml
- ports.yml - ports.yml
- envvars.yml - envvars.yml
- flex_vol.yml
patchesJson6902: patchesJson6902:
- target: - target:
group: extensions group: extensions

View File

@ -13,6 +13,7 @@ SETTINGS=(
AUTH_DOMAIN AUTH_DOMAIN
KV_MI_ID KV_MI_ID
KV_MI_CLIENT_ID KV_MI_CLIENT_ID
TENANT_ID
) )
# Loop all expected settings. Track ones that are missing and build # Loop all expected settings. Track ones that are missing and build

View File

@ -30,6 +30,8 @@ from atst.domain.users import Users
from atst.routes.dev import _DEV_USERS as DEV_USERS from atst.routes.dev import _DEV_USERS as DEV_USERS
from atst.utils import pick
from tests.factories import ( from tests.factories import (
random_service_branch, random_service_branch,
TaskOrderFactory, TaskOrderFactory,
@ -238,6 +240,7 @@ def add_applications_to_portfolio(portfolio):
None, None,
first_name=user_data["first_name"], first_name=user_data["first_name"],
last_name=user_data["last_name"], last_name=user_data["last_name"],
email=user_data["email"],
) )
app_role = ApplicationRoles.create( app_role = ApplicationRoles.create(
@ -263,7 +266,23 @@ def add_applications_to_portfolio(portfolio):
def create_demo_portfolio(name, data): def create_demo_portfolio(name, data):
try: 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 # auditor = Users.get_by_dod_id("3453453453") # Sally
except NotFoundError: except NotFoundError:
print( print(

View File

@ -9,6 +9,7 @@ from atst.domain.portfolios import (
) )
from atst.domain.portfolio_roles import PortfolioRoles from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.application_roles import ApplicationRoles
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.permission_sets import PermissionSets, PORTFOLIO_PERMISSION_SETS from atst.domain.permission_sets import PermissionSets, PORTFOLIO_PERMISSION_SETS
from atst.models.application_role import Status as ApplicationRoleStatus from atst.models.application_role import Status as ApplicationRoleStatus

View File

@ -96,7 +96,6 @@ def test_create_adds_clins():
}, },
] ]
task_order = TaskOrders.create( task_order = TaskOrders.create(
creator=portfolio.owner,
portfolio_id=portfolio.id, portfolio_id=portfolio.id,
number="0123456789", number="0123456789",
clins=clins, clins=clins,
@ -127,7 +126,6 @@ def test_update_adds_clins():
}, },
] ]
task_order = TaskOrders.create( task_order = TaskOrders.create(
creator=task_order.creator,
portfolio_id=task_order.portfolio_id, portfolio_id=task_order.portfolio_id,
number="0000000000", number="0000000000",
clins=clins, clins=clins,

View File

@ -4,36 +4,41 @@ from uuid import uuid4
from atst.domain.users import Users from atst.domain.users import Users
from atst.domain.exceptions import NotFoundError, AlreadyExistsError, UnauthorizedError from atst.domain.exceptions import NotFoundError, AlreadyExistsError, UnauthorizedError
from atst.utils import pick
from tests.factories import UserFactory from tests.factories import UserFactory
DOD_ID = "my_dod_id" DOD_ID = "my_dod_id"
REQUIRED_KWARGS = {"first_name": "Luke", "last_name": "Skywalker"}
def test_create_user(): def test_create_user():
user = Users.create(DOD_ID) user = Users.create(DOD_ID, **REQUIRED_KWARGS)
assert user.dod_id == DOD_ID assert user.dod_id == DOD_ID
def test_create_user_with_existing_email(): 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): with pytest.raises(AlreadyExistsError):
Users.create(DOD_ID, email="thisusersemail@usersRus.com") Users.create(DOD_ID, email="thisusersemail@usersRus.com")
def test_create_user_with_nonexistent_permission_set(): def test_create_user_with_nonexistent_permission_set():
with pytest.raises(NotFoundError): 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(): 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 assert user.dod_id == DOD_ID
def test_get_or_create_existing_user(): def test_get_or_create_existing_user():
fact_user = UserFactory.create() 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 assert user == fact_user

View File

@ -236,9 +236,17 @@ class ApplicationRoleFactory(Base):
@classmethod @classmethod
def _create(cls, model_class, *args, **kwargs): def _create(cls, model_class, *args, **kwargs):
with_invite = kwargs.pop("invite", True) 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) ApplicationInvitationFactory.create(role=app_role)
return app_role return app_role
@ -260,6 +268,14 @@ class PortfolioInvitationFactory(Base):
email = factory.Faker("email") email = factory.Faker("email")
status = InvitationStatus.PENDING status = InvitationStatus.PENDING
expiration_time = PortfolioInvitations.current_expiration_time() 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): class ApplicationInvitationFactory(Base):
@ -270,6 +286,14 @@ class ApplicationInvitationFactory(Base):
status = InvitationStatus.PENDING status = InvitationStatus.PENDING
expiration_time = PortfolioInvitations.current_expiration_time() expiration_time = PortfolioInvitations.current_expiration_time()
role = factory.SubFactory(ApplicationRoleFactory, invite=False) 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): class AttachmentFactory(Base):
@ -284,11 +308,8 @@ class TaskOrderFactory(Base):
class Meta: class Meta:
model = TaskOrder model = TaskOrder
portfolio = factory.SubFactory( portfolio = factory.SubFactory(PortfolioFactory)
PortfolioFactory, owner=factory.SelfAttribute("..creator")
)
number = factory.LazyFunction(random_task_order_number) number = factory.LazyFunction(random_task_order_number)
creator = factory.SubFactory(UserFactory)
signed_at = None signed_at = None
_pdf = factory.SubFactory(AttachmentFactory) _pdf = factory.SubFactory(AttachmentFactory)

View File

@ -76,7 +76,7 @@ class TestTaskOrderStatus:
@patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock) @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock)
def test_draft_status(self, is_signed, is_completed): def test_draft_status(self, is_signed, is_completed):
# Given that I have a TO that is neither completed nor signed # Given that I have a TO that is neither completed nor signed
to = TaskOrder() to = TaskOrderFactory.create()
is_signed.return_value = False is_signed.return_value = False
is_completed.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): 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 # Given that I have a signed TO and today is within its start_date and end_date
today = pendulum.today().date() today = pendulum.today().date()
to = TaskOrder() to = TaskOrderFactory.create()
start_date.return_value = today.subtract(days=1) start_date.return_value = today.subtract(days=1)
end_date.return_value = today.add(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) @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock)
def test_upcoming_status(self, is_signed, is_completed, start_date, end_date): 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 # 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() start_date.return_value = pendulum.today().add(days=1).date()
end_date.return_value = pendulum.today().add(days=2).date() end_date.return_value = pendulum.today().add(days=2).date()
is_signed.return_value = True is_signed.return_value = True
@ -120,7 +120,7 @@ class TestTaskOrderStatus:
@patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock) @patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock)
def test_expired_status(self, is_signed, is_completed, end_date, start_date): 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 # 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() end_date.return_value = pendulum.today().subtract(days=1).date()
start_date.return_value = pendulum.today().subtract(days=2).date() start_date.return_value = pendulum.today().subtract(days=2).date()
is_signed.return_value = True is_signed.return_value = True
@ -143,7 +143,7 @@ class TestTaskOrderStatus:
class TestBudget: class TestBudget:
def test_total_contract_amount(self): def test_total_contract_amount(self):
to = TaskOrder() to = TaskOrderFactory.create()
assert to.total_contract_amount == 0 assert to.total_contract_amount == 0
clin1 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_1) clin1 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_1)
@ -156,7 +156,7 @@ class TestBudget:
) )
def test_total_obligated_funds(self): def test_total_obligated_funds(self):
to = TaskOrder() to = TaskOrderFactory.create()
assert to.total_obligated_funds == 0 assert to.total_obligated_funds == 0
clin1 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_1) clin1 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_1)

View File

@ -30,7 +30,7 @@ def task_order():
portfolio = PortfolioFactory.create(owner=user) portfolio = PortfolioFactory.create(owner=user)
attachment = Attachment(filename="sample_attachment", object_name="sample") 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): def test_review_task_order_not_draft(client, user_session, task_order):

View File

@ -20,14 +20,13 @@ def task_order():
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user) portfolio = PortfolioFactory.create(owner=user)
return TaskOrderFactory.create(creator=user, portfolio=portfolio) return TaskOrderFactory.create(portfolio=portfolio)
@pytest.fixture @pytest.fixture
def completed_task_order(): def completed_task_order():
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
task_order = TaskOrderFactory.create( task_order = TaskOrderFactory.create(
creator=portfolio.owner,
portfolio=portfolio, portfolio=portfolio,
create_clins=[{"number": "1234567890123456789012345678901234567890123"}], 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( def test_task_orders_form_step_one_add_pdf_existing_to(
client, user_session, task_order client, user_session, task_order
): ):
user_session(task_order.creator) user_session(task_order.portfolio.owner)
response = client.get( response = client.get(
url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order.id) 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): def test_task_orders_submit_form_step_one_add_pdf_existing_to(client, user_session):
task_order = TaskOrderFactory.create() task_order = TaskOrderFactory.create()
user_session(task_order.creator) user_session(task_order.portfolio.owner)
response = client.post( response = client.post(
url_for( url_for(
"task_orders.submit_form_step_one_add_pdf", task_order_id=task_order.id "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): 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( response = client.get(
url_for("task_orders.form_step_two_add_number", task_order_id=task_order.id) 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): 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"} form_data = {"number": "1234567890"}
response = client.post( response = client.post(
url_for( 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( def test_task_orders_submit_form_step_two_add_number_existing_to(
client, user_session, task_order client, user_session, task_order
): ):
user_session(task_order.creator) user_session(task_order.portfolio.owner)
form_data = {"number": "0000000000"} form_data = {"number": "0000000000"}
original_number = task_order.number original_number = task_order.number
response = client.post( 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): 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( response = client.get(
url_for("task_orders.form_step_three_add_clins", task_order_id=task_order.id) 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): 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 = { form_data = {
"clins-0-jedi_clin_type": "JEDI_CLIN_1", "clins-0-jedi_clin_type": "JEDI_CLIN_1",
"clins-0-clin_number": "12312", "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) TaskOrders.create_clins(task_order.id, clin_list)
assert len(task_order.clins) == 2 assert len(task_order.clins) == 2
user_session(task_order.creator) user_session(task_order.portfolio.owner)
form_data = { form_data = {
"clins-0-jedi_clin_type": "JEDI_CLIN_1", "clins-0-jedi_clin_type": "JEDI_CLIN_1",
"clins-0-clin_number": "12312", "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): 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( response = client.get(
url_for( url_for(
"task_orders.form_step_four_review", task_order_id=completed_task_order.id "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( def test_task_orders_form_step_four_review_incomplete_to(
client, user_session, task_order client, user_session, task_order
): ):
user_session(task_order.creator) user_session(task_order.portfolio.owner)
response = client.get( response = client.get(
url_for("task_orders.form_step_four_review", task_order_id=task_order.id) 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( def test_task_orders_form_step_five_confirm_signature(
client, user_session, completed_task_order client, user_session, completed_task_order
): ):
user_session(completed_task_order.creator) user_session(completed_task_order.portfolio.owner)
response = client.get( response = client.get(
url_for( url_for(
"task_orders.form_step_five_confirm_signature", "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( def test_task_orders_form_step_five_confirm_signature_incomplete_to(
client, user_session, task_order client, user_session, task_order
): ):
user_session(task_order.creator) user_session(task_order.portfolio.owner)
response = client.get( response = client.get(
url_for( url_for(
"task_orders.form_step_five_confirm_signature", task_order_id=task_order.id "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( def test_task_orders_edit_redirects_to_latest_incomplete_step(
client, user_session, portfolio, to_factory_args, expected_step client, user_session, portfolio, to_factory_args, expected_step
): ):
task_order = TaskOrderFactory.create( task_order = TaskOrderFactory.create(portfolio=portfolio, **to_factory_args)
portfolio=portfolio, creator=portfolio.owner, **to_factory_args
)
user_session(portfolio.owner) user_session(portfolio.owner)
response = client.get(url_for("task_orders.edit", task_order_id=task_order.id)) 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") @pytest.mark.skip(reason="Update after implementing errors on TO form")
def test_task_order_form_shows_errors(client, user_session, task_order): def test_task_order_form_shows_errors(client, user_session, task_order):
creator = task_order.creator user_session(task_order.portfolio.owner)
user_session(creator)
task_order_data = TaskOrderFactory.dictionary() task_order_data = TaskOrderFactory.dictionary()
funding_data = slice_data_for_section(task_order_data, "funding") funding_data = slice_data_for_section(task_order_data, "funding")

View File

@ -487,7 +487,9 @@ def test_portfolios_resend_invitation_access(post_url_assert_status):
portfolio = PortfolioFactory.create(owner=owner) portfolio = PortfolioFactory.create(owner=owner)
prr = PortfolioRoleFactory.create(user=invitee, portfolio=portfolio) 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( url = url_for(
"portfolios.resend_invitation", "portfolios.resend_invitation",
@ -651,7 +653,6 @@ def test_task_orders_new_get_routes(get_url_assert_status):
portfolio = PortfolioFactory.create(owner=owner) portfolio = PortfolioFactory.create(owner=owner)
task_order = TaskOrderFactory.create( task_order = TaskOrderFactory.create(
creator=owner,
portfolio=portfolio, portfolio=portfolio,
create_clins=[{"number": "1234567890123456789012345678901234567890123"}], create_clins=[{"number": "1234567890123456789012345678901234567890123"}],
) )
@ -689,7 +690,7 @@ def test_task_orders_new_post_routes(post_url_assert_status):
rando = user_with() rando = user_with()
portfolio = PortfolioFactory.create(owner=owner) 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: for route, data in post_routes:
url = url_for(route, task_order_id=task_order.id) url = url_for(route, task_order_id=task_order.id)