Merge branch 'master' into move-scope

This commit is contained in:
rachel-dtr 2019-04-02 11:19:27 -04:00 committed by GitHub
commit 3d8cea71f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 405 additions and 102 deletions

View File

@ -29,11 +29,16 @@ from atst.utils.form_cache import FormCache
from atst.utils.json import CustomJSONEncoder
from atst.queue import queue
from logging.config import dictConfig
from atst.utils.logging import JsonFormatter, RequestContextFilter
ENV = os.getenv("FLASK_ENV", "dev")
def make_app(config):
if ENV == "prod" or config.get("LOG_JSON"):
apply_json_logger()
parent_dir = Path().parent
@ -143,6 +148,7 @@ def map_config(config):
"RQ_QUEUES": [config["default"]["RQ_QUEUES"]],
"DISABLE_CRL_CHECK": config.getboolean("default", "DISABLE_CRL_CHECK"),
"CRL_FAIL_OPEN": config.getboolean("default", "CRL_FAIL_OPEN"),
"LOG_JSON": config.getboolean("default", "LOG_JSON"),
}
@ -228,3 +234,22 @@ def make_mailer(app):
)
sender = app.config.get("MAIL_SENDER")
app.mailer = mailer.Mailer(mailer_connection, sender)
def apply_json_logger():
dictConfig(
{
"version": 1,
"formatters": {"default": {"()": lambda *a, **k: JsonFormatter()}},
"filters": {"requests": {"()": lambda *a, **k: RequestContextFilter()}},
"handlers": {
"wsgi": {
"class": "logging.StreamHandler",
"stream": "ext://flask.logging.wsgi_errors_stream",
"formatter": "default",
"filters": ["requests"],
}
},
"root": {"level": "INFO", "handlers": ["wsgi"]},
}
)

View File

@ -2,6 +2,7 @@ import sys
import os
import re
import hashlib
import logging
from flask import current_app as app
from datetime import datetime
from OpenSSL import crypto, SSL
@ -30,9 +31,9 @@ class CRLInterface:
def __init__(self, *args, logger=None, **kwargs):
self.logger = logger
def _log_info(self, message):
def _log(self, message, level=logging.INFO):
if self.logger:
self.logger.info(message)
self.logger.log(level, message, extras={"tags": ["authorization", "crl"]})
def crl_check(self, cert):
raise NotImplementedError()
@ -50,7 +51,7 @@ class NoOpCRLCache(CRLInterface):
def crl_check(self, cert):
cn = self._get_cn(cert)
self._log_info(
self._log(
"Did not perform CRL validation for certificate with Common Name '{}'".format(
cn
)
@ -111,11 +112,14 @@ class CRLCache(CRLInterface):
try:
return crypto.load_crl(crypto.FILETYPE_ASN1, crl_file.read())
except crypto.Error:
self._log_info("Could not load CRL at location {}".format(crl_location))
self._log(
"Could not load CRL at location {}".format(crl_location),
level=logging.WARNING,
)
def _build_store(self, issuer):
store = self.store_class()
self._log_info("STORE ID: {}. Building store.".format(id(store)))
self._log("STORE ID: {}. Building store.".format(id(store)))
store.set_flags(crypto.X509StoreFlags.CRL_CHECK)
crl_info = self.crl_cache.get(issuer.der(), {})
issuer_name = get_common_name(issuer)
@ -132,7 +136,7 @@ class CRLCache(CRLInterface):
crl = self._load_crl(crl_info["location"])
store.add_crl(crl)
self._log_info(
self._log(
"STORE ID: {}. Adding CRL with issuer Common Name {}".format(
id(store), issuer_name
)
@ -158,7 +162,7 @@ class CRLCache(CRLInterface):
def _add_certificate_chain_to_store(self, store, issuer):
ca = self.certificate_authorities.get(issuer.der())
store.add_cert(ca)
self._log_info(
self._log(
"STORE ID: {}. Adding CA with subject {}".format(
id(store), ca.get_subject()
)
@ -182,10 +186,11 @@ class CRLCache(CRLInterface):
except crypto.X509StoreContextError as err:
if err.args[0][0] == CRL_EXPIRED_ERROR_CODE:
if app.config.get("CRL_FAIL_OPEN"):
self._log_info(
self._log(
"Encountered expired CRL for certificate with CN {} and issuer CN {}, failing open.".format(
parsed.get_subject().CN, parsed.get_issuer().CN
)
),
level=logging.WARNING,
)
return True
else:

View File

@ -45,17 +45,19 @@ def user_can_access_decorator(permission, message=None, override=None):
try:
check_access(permission, message, override, *args, **kwargs)
app.logger.info(
"[access] User {} accessed {} {}".format(
"User {} accessed {} {}".format(
g.current_user.id, request.method, request.path
)
),
extra={"tags": ["access", "success"]},
)
return f(*args, **kwargs)
except UnauthorizedError as err:
app.logger.warning(
"[access] User {} denied access {} {}".format(
"User {} denied access {} {}".format(
g.current_user.id, request.method, request.path
)
),
extra={"tags": ["access", "failure"]},
)
raise (err)

View File

@ -1,6 +1,6 @@
from wtforms.fields import StringField, FormField, FieldList
from wtforms.fields.html5 import EmailField, TelField
from wtforms.validators import Required, Email, Length, Optional
from wtforms.fields import StringField, FormField, FieldList, HiddenField
from atst.domain.permission_sets import PermissionSets
from .forms import BaseForm
@ -11,32 +11,33 @@ from atst.utils.localization import translate
class PermissionsForm(BaseForm):
member = StringField()
user_id = HiddenField()
perms_app_mgmt = SelectField(
None,
choices=[
(PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT, "View Only"),
(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT, "Edit Access"),
(PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT, "View only"),
(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT, "Edit access"),
],
)
perms_funding = SelectField(
None,
choices=[
(PermissionSets.VIEW_PORTFOLIO_FUNDING, "View Only"),
(PermissionSets.EDIT_PORTFOLIO_FUNDING, "Edit Access"),
(PermissionSets.VIEW_PORTFOLIO_FUNDING, "View only"),
(PermissionSets.EDIT_PORTFOLIO_FUNDING, "Edit access"),
],
)
perms_reporting = SelectField(
None,
choices=[
(PermissionSets.VIEW_PORTFOLIO_REPORTS, "View Only"),
(PermissionSets.EDIT_PORTFOLIO_REPORTS, "Edit Access"),
(PermissionSets.VIEW_PORTFOLIO_REPORTS, "View only"),
(PermissionSets.EDIT_PORTFOLIO_REPORTS, "Edit access"),
],
)
perms_portfolio_mgmt = SelectField(
None,
choices=[
(PermissionSets.VIEW_PORTFOLIO_ADMIN, "View Only"),
(PermissionSets.EDIT_PORTFOLIO_ADMIN, "Edit Access"),
(PermissionSets.VIEW_PORTFOLIO_ADMIN, "View only"),
(PermissionSets.EDIT_PORTFOLIO_ADMIN, "Edit access"),
],
)

View File

@ -15,7 +15,7 @@ from atst.utils.flash import formatted_flash as flash
def log_error(e):
error_message = e.message if hasattr(e, "message") else str(e)
current_app.logger.error(error_message)
current_app.logger.exception(error_message)
def handle_error(e, message="Not Found", code=404):

View File

@ -2,9 +2,12 @@ from datetime import date, timedelta
from flask import render_template, request as http_request, g, redirect, url_for
from atst.utils.flash import formatted_flash as flash
from . import portfolios_bp
from atst.domain.reports import Reports
from atst.domain.portfolios import Portfolios
from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.audit_log import AuditLog
from atst.domain.common import Paginator
from atst.forms.portfolio import PortfolioForm
@ -34,6 +37,7 @@ def permission_str(member, edit_perm_set, view_perm_set):
def serialize_member_form_data(member):
return {
"member": member.user.full_name,
"user_id": member.user_id,
"perms_app_mgmt": permission_str(
member,
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
@ -86,6 +90,33 @@ def portfolio_admin(portfolio_id):
return render_admin_page(portfolio)
@portfolios_bp.route("/portfolios/<portfolio_id>/admin", methods=["POST"])
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="view portfolio admin page")
def edit_portfolio_members(portfolio_id):
portfolio = Portfolios.get_for_update(portfolio_id)
member_perms_form = member_forms.MembersPermissionsForm(http_request.form)
if member_perms_form.validate():
for subform in member_perms_form.members_permissions:
new_perm_set = subform.data["permission_sets"]
user_id = subform.user_id.data
portfolio_role = PortfolioRoles.get(portfolio.id, user_id)
PortfolioRoles.update(portfolio_role, new_perm_set)
flash("update_portfolio_members", portfolio=portfolio)
return redirect(
url_for(
"portfolios.portfolio_admin",
portfolio_id=portfolio.id,
fragment="portfolio-members",
_anchor="portfolio-members",
)
)
else:
return render_admin_page(portfolio)
@portfolios_bp.route("/portfolios/<portfolio_id>/edit", methods=["POST"])
@user_can(Permissions.EDIT_PORTFOLIO_NAME, message="edit portfolio")
def edit_portfolio(portfolio_id):

View File

@ -52,7 +52,7 @@ class Invitation:
inviter,
member,
email,
subject="{} has invited you to a JEDI Cloud Portfolio",
subject="{} has invited you to a JEDI cloud portfolio",
email_template="emails/invitation.txt",
):
self.inviter = inviter

View File

@ -10,7 +10,7 @@ MESSAGES = {
"task_order_draft": {
"title_template": translate("task_orders.form.draft_alert_title"),
"message_template": """
<p>Please complete your Task Order before submitting it for approval.</p>
<p>Please complete your task order before submitting it for approval.</p>
""",
"category": "warning",
},
@ -21,6 +21,13 @@ MESSAGES = {
""",
"category": "success",
},
"update_portfolio_members": {
"title_template": "Success!",
"message_template": """
<p>You have successfully updated access permissions for members of {{ portfolio.name }}.</p>
""",
"category": "success",
},
"new_portfolio_member": {
"title_template": "Success!",
"message_template": """
@ -73,7 +80,7 @@ MESSAGES = {
"portfolio_member_dod_id_error": {
"title_template": "CAC ID Error",
"message_template": """
The member attempted to accept this invite, but their CAC ID did not match the CAC ID you specified on the invite. Please confirm that the DOD ID is accurate.
The member attempted to accept this invite, but their CAC ID did not match the CAC ID you specified on the invite. Please confirm that the DoD ID is accurate.
""",
"category": "error",
},
@ -127,7 +134,7 @@ MESSAGES = {
"task_order_incomplete": {
"title_template": "Task Order Incomplete",
"message_template": """
You must complete your Task Order form before submitting.
You must complete your task order form before submitting.
""",
"category": "error",
},

47
atst/utils/logging.py Normal file
View File

@ -0,0 +1,47 @@
import datetime
import json
import logging
from flask import g, request, has_request_context
class RequestContextFilter(logging.Filter):
def filter(self, record):
if has_request_context():
if getattr(g, "current_user", None):
record.user_id = str(g.current_user.id)
if request.environ.get("HTTP_X_REQUEST_ID"):
record.request_id = request.environ.get("HTTP_X_REQUEST_ID")
return True
def epoch_to_iso8601(ts):
dt = datetime.datetime.utcfromtimestamp(ts)
return dt.replace(tzinfo=datetime.timezone.utc).isoformat()
class JsonFormatter(logging.Formatter):
_DEFAULT_RECORD_FIELDS = [
("timestamp", lambda r: epoch_to_iso8601(r.created)),
("version", lambda r: 1),
("request_id", lambda r: r.__dict__.get("request_id")),
("user_id", lambda r: r.__dict__.get("user_id")),
("severity", lambda r: r.levelname),
("tags", lambda r: r.__dict__.get("tags")),
("message", lambda r: r.msg),
]
def format(self, record):
message_dict = {}
for field, func in self._DEFAULT_RECORD_FIELDS:
message_dict[field] = func(record)
if record.__dict__.get("exc_info") is not None:
message_dict["details"] = {
"backtrace": self.formatException(record.exc_info),
"exception": str(record.exc_info[1]),
}
return json.dumps(message_dict)

View File

@ -10,6 +10,7 @@ DISABLE_CRL_CHECK = false
CRL_FAIL_OPEN = false
DEBUG = true
ENVIRONMENT = dev
LOG_JSON = false
PERMANENT_SESSION_LIFETIME = 600
PE_NUMBER_CSV_URL = http://c95e1ebb198426ee57b8-174bb05a294821bedbf46b6384fe9b1f.r31.cf5.rackcdn.com/penumbers.csv
PGAPPNAME = atst

View File

@ -17,7 +17,7 @@
{% set step_one %}
<div class="modal__form--header">
<h1>Invite New Portfolio Member</h1>
<h1>Invite new portfolio member</h1>
</div>
<div class='form-row'>
<div class='form-col form-col--half'>
@ -48,14 +48,14 @@
v-on:click="next()"
v-bind:disabled="invalid"
class='action-group__action usa-button'
value='Next Step'>
value='Next'>
<a class='action-group__action icon-link icon-link--default' v-on:click="closeModal('{{ new_port_mem }}')">Cancel</a>
</div>
{% endset %}
{% set step_two %}
<div class="modal__form--padded">
<div class="modal__form--header">
<h1>Assign Member Permissions</h1>
<h1>Assign member permissions</h1>
<a class='icon-link'>
{{ Icon('info') }}
{{ "portfolios.admin.permissions_info" | translate }}
@ -70,7 +70,7 @@
type="submit"
class='action-group__action usa-button'
form="add-port-mem"
value='Invite Member'>
value='Invite member'>
<a class='action-group__action icon-link icon-link--default' v-on:click="closeModal('{{ new_port_mem }}')">Cancel</a>
</div>
</div>

View File

@ -16,5 +16,6 @@
<td><button type="button" class='{{ archive_button_class }}'>{{ "portfolios.members.archive_button" | translate }}</button>
</td>
{{ subform.user_id() }}
</tr>
{% endfor %}

View File

@ -6,18 +6,20 @@
{% if g.matchesPath("portfolio-members") %}
{% include "fragments/flash.html" %}
{% endif %}
<form method='POST' id="member-perms" autocomplete="off" enctype="multipart/form-data">
<div class='member-list-header'>
<div class='left'>
<div class='h3'>{{ "portfolios.admin.portfolio_members_title" | translate }}</div>
<div class='subheading'>
{{ "portfolios.admin.portfolio_members_subheading" | translate }}
</div>
<form method='POST' id="member-perms" action='{{ url_for("portfolios.edit_portfolio_members", portfolio_id=portfolio.id) }}' autocomplete="off" enctype="multipart/form-data">
{{ member_perms_form.csrf_token }}
<div class='member-list-header'>
<div class='left'>
<div class='h3'>{{ "portfolios.admin.portfolio_members_title" | translate }}</div>
<div class='subheading'>
{{ "portfolios.admin.portfolio_members_subheading" | translate }}
</div>
<a class='icon-link'>
{{ Icon('info') }}
{{ "portfolios.admin.settings_info" | translate }}
</a>
</div>
<a class='icon-link'>
{{ Icon('info') }}
{{ "portfolios.admin.settings_info" | translate }}
</a>
</div>
{% if not portfolio.members %}

View File

@ -31,7 +31,7 @@
</ul>
<div class="sidenav__divider--small"></div>
<a class="sidenav__link sidenav__link--add" href="{{ url_for("task_orders.get_started") }}" title="Fund a New Portfolio">
<span class="sidenav__link-label">Fund a New Portfolio</span>
<span class="sidenav__link-label">Fund a new portfolio</span>
{{ Icon("plus", classes="sidenav__link-icon icon--circle") }}
</a>
</div>

View File

@ -93,7 +93,7 @@
<div class="portfolio-funding">
<div class='portfolio-funding__header row'>
<a href="{{ url_for("task_orders.new", screen=1, portfolio_id=portfolio.id) }}" class="usa-button">Start a New Task Order</a>
<a href="{{ url_for("task_orders.new", screen=1, portfolio_id=portfolio.id) }}" class="usa-button">Start a new task order</a>
</div>
{% for task_order in pending_task_orders %}

View File

@ -27,7 +27,7 @@
{% if complete %}
<span class="label label--success">Completed</span>
{% else %}
<span class="label">Not Started</span>
<span class="label">Not started</span>
{% endif %}
</div>
<div class="task-order-next-steps__text col col--grow">
@ -118,7 +118,7 @@
</dd>
</div>
<div class="task-order-heading__value col">
<dt>Task Order Value</dt>
<dt>Task order value</dt>
<dd>{{ task_order.budget | dollars }}</dd>
</div>
</div>

View File

@ -26,3 +26,107 @@ def test_member_table_access(client, user_session):
user_session(rando)
view_resp = client.get(url)
assert "<select" not in view_resp.data.decode()
def test_update_member_permissions(client, user_session):
portfolio = PortfolioFactory.create()
rando = UserFactory.create()
rando_pf_role = PortfolioRoleFactory.create(
user=rando,
portfolio=portfolio,
permission_sets=[PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_ADMIN)],
)
user = UserFactory.create()
PortfolioRoleFactory.create(
user=user,
portfolio=portfolio,
permission_sets=PermissionSets.get_many(
[PermissionSets.EDIT_PORTFOLIO_ADMIN, PermissionSets.VIEW_PORTFOLIO_ADMIN]
),
)
user_session(user)
form_data = {
"members_permissions-0-user_id": rando.id,
"members_permissions-0-perms_app_mgmt": "edit_portfolio_application_management",
"members_permissions-0-perms_funding": "view_portfolio_funding",
"members_permissions-0-perms_reporting": "view_portfolio_reports",
"members_permissions-0-perms_portfolio_mgmt": "view_portfolio_admin",
}
response = client.post(
url_for("portfolios.edit_portfolio_members", portfolio_id=portfolio.id),
data=form_data,
follow_redirects=True,
)
assert response.status_code == 200
assert rando_pf_role.has_permission_set(
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
)
def test_no_update_member_permissions_without_edit_access(client, user_session):
portfolio = PortfolioFactory.create()
rando = UserFactory.create()
rando_pf_role = PortfolioRoleFactory.create(
user=rando,
portfolio=portfolio,
permission_sets=[PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_ADMIN)],
)
user = UserFactory.create()
PortfolioRoleFactory.create(
user=user,
portfolio=portfolio,
permission_sets=[PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_ADMIN)],
)
user_session(user)
form_data = {
"members_permissions-0-user_id": rando.id,
"members_permissions-0-perms_app_mgmt": "edit_portfolio_application_management",
"members_permissions-0-perms_funding": "view_portfolio_funding",
"members_permissions-0-perms_reporting": "view_portfolio_reports",
"members_permissions-0-perms_portfolio_mgmt": "view_portfolio_admin",
}
response = client.post(
url_for("portfolios.edit_portfolio_members", portfolio_id=portfolio.id),
data=form_data,
follow_redirects=True,
)
assert response.status_code == 404
assert not rando_pf_role.has_permission_set(
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
)
def test_rerender_admin_page_if_member_perms_form_does_not_validate(
client, user_session
):
portfolio = PortfolioFactory.create()
user = UserFactory.create()
PortfolioRoleFactory.create(
user=user,
portfolio=portfolio,
permission_sets=[PermissionSets.get(PermissionSets.EDIT_PORTFOLIO_ADMIN)],
)
user_session(user)
form_data = {
"members_permissions-0-user_id": user.id,
"members_permissions-0-perms_app_mgmt": "bad input",
"members_permissions-0-perms_funding": "view_portfolio_funding",
"members_permissions-0-perms_reporting": "view_portfolio_reports",
"members_permissions-0-perms_portfolio_mgmt": "view_portfolio_admin",
}
response = client.post(
url_for("portfolios.edit_portfolio_members", portfolio_id=portfolio.id),
data=form_data,
follow_redirects=True,
)
assert response.status_code == 200
assert "Portfolio Administration" in response.data.decode()

View File

@ -20,11 +20,14 @@ class FakeLogger:
def __init__(self):
self.messages = []
def info(self, msg):
def log(self, _lvl, msg, *args, **kwargs):
self.messages.append(msg)
def warning(self, msg):
def info(self, msg, *args, **kwargs):
self.messages.append(msg)
def error(self, msg):
def warning(self, msg, *args, **kwargs):
self.messages.append(msg)
def error(self, msg, *args, **kwargs):
self.messages.append(msg)

View File

@ -0,0 +1,74 @@
from io import StringIO
import json
import logging
from uuid import uuid4
import pytest
from atst.utils.logging import JsonFormatter, RequestContextFilter
from tests.factories import UserFactory
@pytest.fixture
def log_stream():
return StringIO()
@pytest.fixture
def log_stream_content(log_stream):
def _log_stream_content():
log_stream.seek(0)
return log_stream.read()
return _log_stream_content
@pytest.fixture
def logger(log_stream):
logger = logging.getLogger()
for handler in logger.handlers:
logger.removeHandler(handler)
logHandler = logging.StreamHandler(log_stream)
formatter = JsonFormatter()
logHandler.setFormatter(formatter)
logger.setLevel(logging.INFO)
logger.addFilter(RequestContextFilter())
logger.addHandler(logHandler)
return logger
def test_json_formatter(logger, log_stream_content):
logger.warning("do or do not", extra={"tags": ["wisdom", "jedi"]})
log = json.loads(log_stream_content())
assert log["tags"] == ["wisdom", "jedi"]
assert log["message"] == "do or do not"
assert log["severity"] == "WARNING"
assert log.get("details") is None
def test_json_formatter_for_exceptions(logger, log_stream_content):
try:
raise Exception()
except Exception:
logger.exception("you found the ventilation shaft!")
log = json.loads(log_stream_content())
assert log["severity"] == "ERROR"
assert log.get("details")
def test_request_context_filter(logger, log_stream_content, request_ctx):
user = UserFactory.create()
uuid = str(uuid4())
request_ctx.g.current_user = user
request_ctx.request.environ["HTTP_X_REQUEST_ID"] = uuid
logger.info("this user is doing something")
log = json.loads(log_stream_content())
assert log["user_id"] == str(user.id)
assert log["request_id"] == uuid

View File

@ -48,7 +48,7 @@ components:
footer:
about_link_text: Joint Enterprise Defense Infrastructure
browser_support: JEDI Cloud supported on these web browsers
jedi_help_link_text: Questions? Contact your CCPO Representative
jedi_help_link_text: Questions? Contact your CCPO representative
forms:
ccpo_review:
comment_description: Provide instructions or notes for additional information that is necessary to approve the request here. The requestor may then re-submit the updated request or initiate contact outside of AT-AT if further discussion is required. <strong>This message will be shared with the person making the JEDI request.</strong>.
@ -146,18 +146,18 @@ forms:
average_daily_traffic_label: Average Daily Traffic (Number of Requests)
cloud_native_description: Are your software systems being developed cloud native?
data_transfers_description: How much data is being transferred to the cloud?
dod_component_description: Identify the DoD component that is requesting access to the JEDI Cloud
dod_component_description: Identify the DoD component that is requesting access to the JEDI cloud
dod_component_label: DoD Component
dodid_poc_label: DoD ID
dollar_value_description: What is your total expected budget for this JEDI Cloud Request?
dollar_value_description: What is your total expected budget for this JEDI cloud Request?
dollar_value_label: Total Spend
email_poc_label: Email Address
engineering_assessment_description: Have you completed an engineering assessment of your systems for cloud readiness?
estimated_monthly_spend_description: 'Use the <a href="/jedi-csp-calculator" target="_blank" class="icon-link">JEDI CSP Calculator</a> to estimate your <b>monthly</b> cloud resource usage and enter the dollar amount below. Note these estimates are for initial approval only. After the request is approved, you will be asked to provide a valid task order number with specific CLIN amounts for cloud services.'
estimated_monthly_spend_label: Estimated Monthly Spend
expected_completion_date_description: When do you expect to complete your migration to the JEDI Cloud?
expected_completion_date_description: When do you expect to complete your migration to the JEDI cloud?
fname_poc_label: First Name
jedi_migration_description: Are you using the JEDI Cloud to migrate existing systems?
jedi_migration_description: Are you using the JEDI cloud to migrate existing systems?
jedi_migration_label: JEDI Migration
jedi_usage_description: Your answer will help us provide tangible examples to DoD leadership how and why commercial cloud resources are accelerating the Department's missions
jedi_usage_label: JEDI Usage
@ -165,14 +165,14 @@ forms:
name_description: This name serves as a reference for your initial request and the associated portfolio that will be created once this request is approved. You may edit this name later.
name_label: Name Your Request
name_length_validation_message: Request names must be at least 4 and not more than 100 characters
num_software_systems_description: Estimate the number of software systems that will be supported by this JEDI Cloud access request
num_software_systems_description: Estimate the number of software systems that will be supported by this JEDI cloud access request
num_software_systems_label: Number of Software Systems
number_user_sessions_description: How many user sessions do you expect on these systems each day?
organization_providing_assistance_description: 'If you are receiving migration assistance, what is the type of organization providing assistance?'
rationalization_software_systems_description: Have you completed a “rationalization” of your software systems to move to the cloud?
reviewed_label: I have reviewed this data and it is correct.
start_date_date_range_validation_message: Must be a date in the future.
start_date_label: When do you expect to start using the JEDI Cloud (not for billing purposes)?
start_date_label: When do you expect to start using the JEDI cloud (not for billing purposes)?
technical_support_team_description: Are you working with a technical support team experienced in cloud migrations?
application:
description_label: Description
@ -181,13 +181,13 @@ forms:
environment_names_unique_validation_message: Environment names must be unique.
name_label: Name
task_order:
portfolio_name_label: Organization Portfolio Name
portfolio_name_label: Portfolio name
portfolio_name_description: The name of your office or organization. You can add multiple applications to your portfolio. Your task orders are used to pay for these applications and their environments.
scope_label: Cloud Project Scope
scope_label: Cloud project scope
scope_description: Your team's plan for using the cloud, such as migrating an existing application or creating a prototype. <p>Not sure how to describe your scope? <a href="#">Read some examples</a> to get some inspiration.</p>
defense_component_label: Department of Defense Component
defense_component_label: Department of Defense component
app_migration:
label: App Migration
label: App migration
description: Do you plan to migrate one or more existing application(s) to the cloud?
on_premise: Yes, migrating from an <strong>on-premise data center</strong>
cloud: Yes, migrating from <strong>another cloud provider</strong>
@ -195,43 +195,43 @@ forms:
none: Not planning to migrate any applications
not_sure: "Not sure"
native_apps:
label: Native Apps
label: Native apps
description: Do you plan to develop any applications natively in the cloud?
'yes': Yes, planning to develop natively in the cloud
'no': No, not planning to develop natively in the cloud
not_sure: Not sure, unsure if planning to develop natively in the cloud
complexity:
label: Project Complexity
label: Project complexity
description: Which of these describes how complex your team's use of the cloud will be? Select all that apply.
storage: Storage
data_analytics: Data Analytics
conus: CONUS Access
oconus: OCONUS Access
tactical_edge: Tactical Edge Access
data_analytics: Data analytics
conus: CONUS access
oconus: OCONUS access
tactical_edge: Tactical edge access
not_sure: Not sure
other: Other
complexity_other_label: Project Complexity Other
dev_team:
label: Development Team
label: Development team
description: Who will be completing the development work for your cloud application(s)? Select all that apply.
civilians: Government Civilians
civilians: Government civilians
military: Military
contractor: Contractor
other: "Other <em class='description'>(E.g. University or other partner)</em>"
dev_team_other_label: Development Team Other
team_experience:
label: Team Experience
description: How much experience does your team have with development in the cloud?
label: Team experience
description: How much experience does your team have with development in the cloud?
none: No previous experience
planned: Researched or planned a cloud build or migration
built_1: Built or migrated 1-2 applications
built_3: Built or migrated 3-5 applications
built_many: Built or migrated many applications, or consulted on several such projects
performance_length:
label: Period of Performance length
label: Period of performance length
start_date_label: Start Date
end_date_label: End Date
csp_estimate_label: Upload a copy of your CSP Cost Estimate Research
csp_estimate_label: Upload a copy of your CSP cost estimate research
csp_estimate_description: Upload a PDF or screenshot of your usage estimate from the calculator.
file_format_not_allowed: Only PDF or PNG files can be uploaded.
clin_01_label: 'CLIN 01 : Unclassified'
@ -273,7 +273,7 @@ forms:
phone_number_message: Please enter a valid 5 or 10 digit phone number.
is_required: This field is required.
portfolio:
name_label: Portfolio Name
name_label: Portfolio name
name_length_validation_message: Portfolio names can be between 4-100 characters
officers:
contracting_officer_invite: Invite KO to Task Order Builder
@ -287,15 +287,15 @@ fragments:
date_last_training_tooltip: When was the last time you completed the IA training? <br> Information Assurance (IA) training is an important step in cyber awareness.
save_details_button: Save Details
pending_ccpo_acceptance_alert:
learn_more_link_text: Learn more about the JEDI Cloud task order and the financial verification process.
learn_more_link_text: Learn more about the JEDI cloud task order and the financial verification process.
paragraph_1: The CCPO will review and respond to your request in 3 business days. Youll be notified via email or phone. Please note if your request is for over $1M of JEDI cloud resources it will require a manual review by the CCPO.
paragraph_2: 'While your request is being reviewed, your next step is to create a task order (TO) associated with the JEDI Cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.'
paragraph_2: 'While your request is being reviewed, your next step is to create a task order (TO) associated with the JEDI cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.'
pending_ccpo_approval_modal:
paragraph_1: The CCPO will review and respond to your Financial Verification submission in 3 business days. You will be notified via email or phone.
paragraph_2: Once the financial verification is approved you will be invited to create your JEDI Portfolio and set-up your applications. Click here for more details.
pending_financial_verification:
learn_more_link_text: Learn more about the JEDI Cloud task order and the financial verification process.
paragraph_1: 'The next step is to create a task order associated with JEDI Cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.'
learn_more_link_text: Learn more about the JEDI cloud task order and the financial verification process.
paragraph_1: 'The next step is to create a task order associated with JEDI cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.'
paragraph_2: 'Once the task order has been created, you will be asked to provide details about the task order in the Financial Verification step.'
ko_review_message:
title: Steps
@ -314,13 +314,13 @@ login:
learn_more: Learn more
message: 'When you are prompted to select a certificate, please select Email Certificate from the provided choices.'
title: Certificate Selection
h1_title: Access the JEDI Cloud
h1_title: Access the JEDI cloud
login_button: Sign in with CAC
title_tag: Sign in | JEDI Cloud
title_tag: Sign in | JEDI cloud
navigation:
topbar:
jedi_cloud_link_text: JEDI
logout_link_title: Log out of JEDI Cloud
logout_link_title: Log out of JEDI cloud
named_portfolio: 'Portfolio {portfolio}'
no_other_active_portfolios: You have no other active JEDI portfolios.
other_active_portfolios: Other Active Portfolios
@ -329,7 +329,7 @@ navigation:
add_new_member_label: Add new member
add_new_application_label: Add new application
budget_report: Budget Report
activity_log: Activity Log
activity_log: Activity log
members: Members
applications: Applications
portfolio_funding: Funding
@ -456,44 +456,44 @@ task_orders:
project_title: About your project
team_title: About your team
market_research_title: Market research
market_research_paragraph: 'The JEDI Cloud Computing Program Office (CCPO) has completed the market research requirements for all related task orders. The Department of Defense CIO has approved this research.<br /><a href="#">View JEDI Market Research Memo</a>'
market_research_paragraph: 'The JEDI Cloud Computing Program Office (CCPO) has completed the market research requirements for all related task orders. The Department of Defense CIO has approved this research.<br /><a href="#">View JEDI market research memo</a>'
funding:
section_title: Funding
performance_period_title: Period of Performance
performance_period_title: Period of performance
performance_period_description: Choose the length of time your task order will cover.
performance_period_paragraph: Be aware that your funds will be lost if you dont use them. Because of this, we strongly recommend submitting small, short-duration task orders, usually around a three month period. Well notify you when your period of performance is nearing its end so you can request your next set of funds with a new task order.
estimate_usage_title: Estimate Your Cloud Usage
estimate_usage_title: Estimate your cloud usage
estimate_usage_description: Calculate how much your cloud usage will cost. A technical representative from your team should help you complete this calculation. These calculations will become your CLINs.
estimate_usage_paragraph: This is only an estimation tool to help you make an informed evaluation of what you expect to use. While you're tied to the dollar amount you specify in your task order, you're not obligated by the resources you indicate in the calculator.
cloud_calculations_title: Cloud Usage Calculations
cloud_calculations_title: Cloud usage calculations
cloud_calculations_paragraph: Enter the results of your cloud usage calculations below.
cloud_offerings_title: Cloud Offerings
cloud_offerings_title: Cloud offerings
cloud_offerings_paragraph: Infrastructure as a Service (IaaS) and Platform as a Service (PaaS) offerings
support_assistance_title: Cloud Support and Assistance
support_assistance_title: Cloud support and assistance
support_assistance_paragraph: Technical guidance from the cloud service provider, including architecture, configuration of IaaS and PaaS, integration, troubleshooting assistance, and other services.
total: 'Total Task Order Value:'
total: 'Total task order value:'
oversight:
section_title: Oversight
ko_info_title: Contracting Officer (KO) Information
ko_info_paragraph: Your KO will need to approve funding for this task order by logging into the JEDI Cloud Portal, submitting the task order documents within their official system of record, and then finally providing a digital signature. It might be helpful to work with your program's Financial Manager to get your TO documents moving.
ko_info_paragraph: Your KO will need to approve funding for this task order by logging into the JEDI cloud portal, submitting the task order documents within their official system of record, and then finally providing a digital signature. It might be helpful to work with your program's Financial Manager to get your TO documents moving.
skip_ko_label: "Skip for now (We'll remind you to enter one later)"
dod_id_tooltip: "The DoD ID is needed to verify the identity of the indicated officer or representative."
cor_info_title: Contracting Officer Representative (COR) Information
cor_info_paragraph: Your COR may assist in submitting the task order documents within their official system of record. They may also be invited to log in and manage the Task Order entry within the JEDI Cloud portal.
cor_info_paragraph: Your COR may assist in submitting the task order documents within their official system of record. They may also be invited to log in and manage the Task Order entry within the JEDI cloud portal.
so_info_title: Security Officer Information
so_info_paragraph: Your Security Officer will need to answer some security configuration questions in order to generate a DD-254 document, then electronically sign.
review:
section_title: Review Your Task Order
app_info: What you're making
portfolio: Portfolio
dod: DoD Component
scope: Scope (Statement of Work)
dod: DoD component
scope: Scope (statement of work)
reporting: Reporting
complexity: Project complexity
team: Development team
funding: Funding
performance_period: Period of performance
usage_est_link: View Usage Estimate
usage_est_link: View usage estimate
to_value: Task order value
clin_1: 'CLIN #1: Unclassified Cloud'
clin_2: 'CLIN #2: Classified Cloud'
@ -546,7 +546,7 @@ portfolios:
index:
empty:
title: You have no apps yet
start_button: Start a New JEDI Portfolio
start_button: Start a new JEDI portfolio
applications:
add_application_text: Add a new application
app_settings_text: App settings
@ -558,7 +558,7 @@ portfolios:
environments_heading: Environments
environments_description: Each environment created within an application is logically separated from one another for easier management and security.
update_button_text: Save
create_button_text: Create Application
create_button_text: Create
team_management:
title: '{application_name} Team Management'
subheading: Team Management
@ -566,20 +566,20 @@ portfolios:
portfolio_members_title: Portfolio Members
portfolio_members_subheading: These members have different levels of access to the portfolio.
settings_info: Learn more about these settings
add_member: Add a New Member
add_member: Add a new member
permissions_info: Learn more about these permissions
activity_log_title: Activity Log
activity_log_title: Activity log
add_new_member: Add a new member
members:
archive_button: Archive User
archive_button: Archive user
permissions:
name: Name
app_mgmt: App Mgmt
app_mgmt: App management
funding: Funding
reporting: Reporting
portfolio_mgmt: Portfolio Mgmt
view_only: View Only
edit_access: Edit Access
portfolio_mgmt: Portfolio management
view_only: View only
edit_access: Edit access
testing:
example_string: Hello World
example_with_variables: 'Hello, {name}!'