Merge branch 'staging' into cloudzero-k8s
This commit is contained in:
commit
2ab9790f3e
@ -152,7 +152,7 @@
|
||||
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 665,
|
||||
"line_number": 649,
|
||||
"type": "Hex High Entropy String"
|
||||
}
|
||||
]
|
||||
|
@ -73,7 +73,8 @@ RUN apk update && \
|
||||
postgresql-client \
|
||||
postgresql-dev \
|
||||
postgresql-libs \
|
||||
uwsgi-logfile
|
||||
uwsgi-logfile \
|
||||
uwsgi-python3
|
||||
|
||||
COPY --from=builder /install/.venv/ ./.venv/
|
||||
COPY --from=builder /install/alembic/ ./alembic/
|
||||
|
@ -159,6 +159,7 @@ def map_config(config):
|
||||
"ENV": config["default"]["ENVIRONMENT"],
|
||||
"BROKER_URL": config["default"]["REDIS_URI"],
|
||||
"DEBUG": config["default"].getboolean("DEBUG"),
|
||||
"DEBUG_MAILER": config["default"].getboolean("DEBUG_MAILER"),
|
||||
"SQLALCHEMY_ECHO": config["default"].getboolean("SQLALCHEMY_ECHO"),
|
||||
"PORT": int(config["default"]["PORT"]),
|
||||
"SQLALCHEMY_DATABASE_URI": config["default"]["DATABASE_URI"],
|
||||
@ -289,7 +290,7 @@ def make_crl_validator(app):
|
||||
|
||||
|
||||
def make_mailer(app):
|
||||
if app.config["DEBUG"]:
|
||||
if app.config["DEBUG"] or app.config["DEBUG_MAILER"]:
|
||||
mailer_connection = mailer.RedisConnection(app.redis)
|
||||
else:
|
||||
mailer_connection = mailer.SMTPConnection(
|
||||
|
@ -19,9 +19,6 @@ from atst.domain.exceptions import UnauthorizedError
|
||||
|
||||
def filter_perm_sets_data(member):
|
||||
perm_sets_data = {
|
||||
"perms_portfolio_mgmt": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||
),
|
||||
"perms_app_mgmt": bool(
|
||||
member.has_permission_set(
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
|
||||
@ -33,24 +30,43 @@ def filter_perm_sets_data(member):
|
||||
"perms_reporting": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS)
|
||||
),
|
||||
"perms_portfolio_mgmt": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||
),
|
||||
}
|
||||
|
||||
return perm_sets_data
|
||||
|
||||
|
||||
def filter_members_data(members_list, portfolio):
|
||||
def filter_members_data(members_list):
|
||||
members_data = []
|
||||
for member in members_list:
|
||||
members_data.append(
|
||||
{
|
||||
"role_id": member.id,
|
||||
"user_name": member.user_name,
|
||||
"permission_sets": filter_perm_sets_data(member),
|
||||
"status": member.display_status,
|
||||
"ppoc": PermissionSets.PORTFOLIO_POC in member.permission_sets,
|
||||
# add in stuff here for forms
|
||||
}
|
||||
permission_sets = filter_perm_sets_data(member)
|
||||
ppoc = (
|
||||
PermissionSets.get(PermissionSets.PORTFOLIO_POC) in member.permission_sets
|
||||
)
|
||||
member_data = {
|
||||
"role_id": member.id,
|
||||
"user_name": member.user_name,
|
||||
"permission_sets": filter_perm_sets_data(member),
|
||||
"status": member.display_status,
|
||||
"ppoc": ppoc,
|
||||
"form": member_forms.PermissionsForm(permission_sets),
|
||||
}
|
||||
|
||||
if not ppoc:
|
||||
member_data["update_invite_form"] = (
|
||||
member_forms.NewForm(user_data=member.latest_invitation)
|
||||
if member.latest_invitation and member.latest_invitation.can_resend
|
||||
else member_forms.NewForm()
|
||||
)
|
||||
member_data["invite_token"] = (
|
||||
member.latest_invitation.token
|
||||
if member.latest_invitation and member.latest_invitation.can_resend
|
||||
else None
|
||||
)
|
||||
|
||||
members_data.append(member_data)
|
||||
|
||||
return sorted(members_data, key=lambda member: member["user_name"])
|
||||
|
||||
@ -75,7 +91,7 @@ def render_admin_page(portfolio, form=None):
|
||||
"portfolios/admin.html",
|
||||
form=form,
|
||||
portfolio_form=portfolio_form,
|
||||
members=filter_members_data(member_list, portfolio),
|
||||
members=filter_members_data(member_list),
|
||||
new_manager_form=member_forms.NewForm(),
|
||||
assign_ppoc_form=assign_ppoc_form,
|
||||
portfolio=portfolio,
|
||||
@ -93,26 +109,27 @@ def admin(portfolio_id):
|
||||
return render_admin_page(portfolio)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/update_ppoc", methods=["POST"])
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
|
||||
def update_ppoc(portfolio_id):
|
||||
role_id = http_request.form.get("role_id")
|
||||
|
||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
new_ppoc_role = PortfolioRoles.get_by_id(role_id)
|
||||
|
||||
PortfolioRoles.make_ppoc(portfolio_role=new_ppoc_role)
|
||||
|
||||
flash("primary_point_of_contact_changed", ppoc_name=new_ppoc_role.full_name)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.admin",
|
||||
portfolio_id=portfolio.id,
|
||||
fragment="primary-point-of-contact",
|
||||
_anchor="primary-point-of-contact",
|
||||
)
|
||||
)
|
||||
# Updating PPoC is a post-MVP feature
|
||||
# @portfolios_bp.route("/portfolios/<portfolio_id>/update_ppoc", methods=["POST"])
|
||||
# @user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
|
||||
# def update_ppoc(portfolio_id): # pragma: no cover
|
||||
# role_id = http_request.form.get("role_id")
|
||||
#
|
||||
# portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
# new_ppoc_role = PortfolioRoles.get_by_id(role_id)
|
||||
#
|
||||
# PortfolioRoles.make_ppoc(portfolio_role=new_ppoc_role)
|
||||
#
|
||||
# flash("primary_point_of_contact_changed", ppoc_name=new_ppoc_role.full_name)
|
||||
#
|
||||
# return redirect(
|
||||
# url_for(
|
||||
# "portfolios.admin",
|
||||
# portfolio_id=portfolio.id,
|
||||
# fragment="primary-point-of-contact",
|
||||
# _anchor="primary-point-of-contact",
|
||||
# )
|
||||
# )
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/edit", methods=["POST"])
|
||||
@ -166,3 +183,30 @@ def remove_member(portfolio_id, portfolio_role_id):
|
||||
fragment="portfolio-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/members/<portfolio_role_id>", methods=["POST"]
|
||||
)
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="update portfolio members")
|
||||
def update_member(portfolio_id, portfolio_role_id):
|
||||
form_data = http_request.form
|
||||
form = member_forms.PermissionsForm(formdata=form_data)
|
||||
portfolio_role = PortfolioRoles.get_by_id(portfolio_role_id)
|
||||
portfolio = Portfolios.get(user=g.current_user, portfolio_id=portfolio_id)
|
||||
|
||||
if form.validate() and portfolio.owner_role != portfolio_role:
|
||||
PortfolioRoles.update(portfolio_role, form.data["permission_sets"])
|
||||
flash("update_portfolio_member", member_name=portfolio_role.full_name)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.admin",
|
||||
portfolio_id=portfolio_id,
|
||||
_anchor="portfolio-members",
|
||||
fragment="portfolio-members",
|
||||
)
|
||||
)
|
||||
else:
|
||||
flash("update_portfolio_member_error", member_name=portfolio_role.full_name)
|
||||
return (render_admin_page(portfolio), 400)
|
||||
|
@ -54,13 +54,22 @@ def revoke_invitation(portfolio_id, portfolio_token):
|
||||
)
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="resend invitation")
|
||||
def resend_invitation(portfolio_id, portfolio_token):
|
||||
invite = PortfolioInvitations.resend(g.current_user, portfolio_token)
|
||||
send_portfolio_invitation(
|
||||
invitee_email=invite.email,
|
||||
inviter_name=g.current_user.full_name,
|
||||
token=invite.token,
|
||||
)
|
||||
flash("resend_portfolio_invitation", user_name=invite.user_name)
|
||||
form = member_forms.NewForm(http_request.form)
|
||||
|
||||
if form.validate():
|
||||
invite = PortfolioInvitations.resend(
|
||||
g.current_user, portfolio_token, form.data["user_data"]
|
||||
)
|
||||
send_portfolio_invitation(
|
||||
invitee_email=invite.email,
|
||||
inviter_name=g.current_user.full_name,
|
||||
token=invite.token,
|
||||
)
|
||||
flash("resend_portfolio_invitation", user_name=invite.user_name)
|
||||
else:
|
||||
user_name = f"{form['user_data']['first_name'].data} {form['user_data']['last_name'].data}"
|
||||
flash("resend_portfolio_invitation_error", user_name=user_name)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.admin",
|
||||
|
@ -128,6 +128,11 @@ MESSAGES = {
|
||||
"message": "flash.portfolio_invite.resent.message",
|
||||
"category": "success",
|
||||
},
|
||||
"resend_portfolio_invitation_error": {
|
||||
"title": "flash.portfolio_invite.error.title",
|
||||
"message": "flash.portfolio_invite.error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"revoked_portfolio_access": {
|
||||
"title": "flash.portfolio_member.revoked.title",
|
||||
"message": "flash.portfolio_member.revoked.message",
|
||||
@ -153,6 +158,16 @@ MESSAGES = {
|
||||
"message": "flash.task_order.submitted.message",
|
||||
"category": "success",
|
||||
},
|
||||
"update_portfolio_member": {
|
||||
"title": "flash.portfolio_member.update.title",
|
||||
"message": "flash.portfolio_member.update.message",
|
||||
"category": "success",
|
||||
},
|
||||
"update_portfolio_member_error": {
|
||||
"title": "flash.portfolio_member.update_error.title",
|
||||
"message": "flash.portfolio_member.update_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"updated_application_team_settings": {
|
||||
"title": "flash.success",
|
||||
"message": "flash.updated_application_team_settings",
|
||||
|
@ -15,6 +15,7 @@ CRL_FAIL_OPEN = false
|
||||
CRL_STORAGE_CONTAINER = crls
|
||||
CSP=mock
|
||||
DEBUG = true
|
||||
DEBUG_MAILER = false
|
||||
DISABLE_CRL_CHECK = false
|
||||
ENVIRONMENT = dev
|
||||
LIMIT_CONCURRENT_SESSIONS = false
|
||||
|
@ -29,6 +29,13 @@ spec:
|
||||
containers:
|
||||
- name: atst
|
||||
image: $CONTAINER_IMAGE
|
||||
env:
|
||||
- name: UWSGI_PROCESSES
|
||||
value: "2"
|
||||
- name: UWSGI_THREADS
|
||||
value: "2"
|
||||
- name: UWSGI_ENABLE_THREADS
|
||||
value: "1"
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: atst-envvars
|
||||
@ -50,11 +57,11 @@ spec:
|
||||
mountPath: "/config"
|
||||
resources:
|
||||
requests:
|
||||
memory: 200Mi
|
||||
cpu: 400m
|
||||
memory: 400Mi
|
||||
cpu: 940m
|
||||
limits:
|
||||
memory: 200Mi
|
||||
cpu: 400m
|
||||
memory: 400Mi
|
||||
cpu: 940m
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
@ -86,10 +93,10 @@ spec:
|
||||
resources:
|
||||
requests:
|
||||
memory: 20Mi
|
||||
cpu: 10m
|
||||
cpu: 25m
|
||||
limits:
|
||||
memory: 20Mi
|
||||
cpu: 10m
|
||||
cpu: 25m
|
||||
volumes:
|
||||
- name: nginx-client-ca-bundle
|
||||
configMap:
|
||||
@ -309,6 +316,7 @@ metadata:
|
||||
namespace: atat
|
||||
spec:
|
||||
loadBalancerIP: 13.92.235.6
|
||||
externalTrafficPolicy: Local
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8342
|
||||
@ -329,6 +337,7 @@ metadata:
|
||||
namespace: atat
|
||||
spec:
|
||||
loadBalancerIP: 23.100.24.41
|
||||
externalTrafficPolicy: Local
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8343
|
||||
|
@ -10,6 +10,7 @@ data:
|
||||
callable = app
|
||||
module = app
|
||||
socket = /var/run/uwsgi/uwsgi.socket
|
||||
plugins-dir = /usr/lib/uwsgi
|
||||
plugin = python3
|
||||
plugin = logfile
|
||||
virtualenv = /opt/atat/atst/.venv
|
||||
|
16
deploy/overlays/staging/autoscaling.yml
Normal file
16
deploy/overlays/staging/autoscaling.yml
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: atst
|
||||
spec:
|
||||
minReplicas: 1
|
||||
maxReplicas: 2
|
||||
---
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: atst-worker
|
||||
spec:
|
||||
minReplicas: 1
|
||||
maxReplicas: 2
|
@ -5,6 +5,7 @@ resources:
|
||||
- namespace.yml
|
||||
- reset-cron-job.yml
|
||||
patchesStrategicMerge:
|
||||
- autoscaling.yml
|
||||
- ports.yml
|
||||
- envvars.yml
|
||||
- flex_vol.yml
|
||||
|
@ -40,9 +40,7 @@ reset_db() {
|
||||
local database_name="${1}"
|
||||
|
||||
# If the DB exists, drop it
|
||||
set +e
|
||||
dropdb "${database_name}"
|
||||
set -e
|
||||
dropdb --if-exists "${database_name}"
|
||||
|
||||
# Create a fresh DB
|
||||
createdb "${database_name}"
|
||||
|
@ -22,7 +22,7 @@ check_for_existing_virtual_environment() {
|
||||
local target_python_version_regex="^Python ${python_version}"
|
||||
|
||||
# Check for existing venv, and if one exists, save the Python version string
|
||||
existing_venv_version=$($(pipenv --py) --version)
|
||||
existing_venv_version=$($(pipenv --py 2> /dev/null) --version 2> /dev/null)
|
||||
if [ "$?" = "0" ]; then
|
||||
# Existing venv; see if the Python version matches
|
||||
if [[ "${existing_venv_version}" =~ ${target_python_version_regex} ]]; then
|
||||
|
@ -6,7 +6,7 @@
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
# create upload directory for app
|
||||
mkdir uploads | true
|
||||
mkdir -p uploads
|
||||
|
||||
# Enable database resetting
|
||||
RESET_DB="true"
|
||||
|
@ -39,6 +39,7 @@
|
||||
@import "components/sticky_cta.scss";
|
||||
@import "components/error_page.scss";
|
||||
@import "components/member_form.scss";
|
||||
@import "components/toggle_menu.scss";
|
||||
|
||||
@import "sections/login";
|
||||
@import "sections/home";
|
||||
|
@ -130,10 +130,6 @@
|
||||
&--th {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
&--td {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
@ -154,55 +150,6 @@
|
||||
margin-right: $gap * 6;
|
||||
}
|
||||
}
|
||||
|
||||
.app-member-menu {
|
||||
position: absolute;
|
||||
top: $gap;
|
||||
right: $gap * 2;
|
||||
|
||||
.accordion-table__item__toggler {
|
||||
padding: $gap / 3;
|
||||
border: 1px solid $color-gray-lighter;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&--active {
|
||||
background-color: $color-aqua-lightest;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: $gap / 2;
|
||||
}
|
||||
}
|
||||
|
||||
&__toggle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 30px;
|
||||
background-color: $color-white;
|
||||
border: 1px solid $color-gray-light;
|
||||
z-index: 1;
|
||||
margin-top: 0;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: $gap;
|
||||
border-bottom: 1px solid $color-gray-lighter;
|
||||
text-decoration: none;
|
||||
color: $color-black;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-aqua-lightest;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#add-new-env {
|
||||
|
58
styles/components/_toggle_menu.scss
Normal file
58
styles/components/_toggle_menu.scss
Normal file
@ -0,0 +1,58 @@
|
||||
.toggle-menu {
|
||||
position: absolute;
|
||||
top: $gap;
|
||||
right: $gap * 2;
|
||||
|
||||
&__container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.accordion-table__item__toggler {
|
||||
padding: $gap / 3;
|
||||
border: 1px solid $color-gray-lighter;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&--active {
|
||||
background-color: $color-aqua-lightest;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: $gap / 2;
|
||||
}
|
||||
}
|
||||
|
||||
&__toggle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 30px;
|
||||
background-color: $color-white;
|
||||
border: 1px solid $color-gray-light;
|
||||
z-index: 1;
|
||||
margin-top: 0;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: $gap;
|
||||
border-bottom: 1px solid $color-gray-lighter;
|
||||
text-decoration: none;
|
||||
color: $color-black;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-aqua-lightest;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $color-gray;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -12,10 +12,13 @@
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: $color-white !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: $topbar-height;
|
||||
@ -23,20 +26,28 @@
|
||||
text-decoration: none;
|
||||
|
||||
&-label {
|
||||
@include h5;
|
||||
text-decoration: underline;
|
||||
padding-left: $gap;
|
||||
font-size: $h5-font-size;
|
||||
font-weight: $font-semibold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
margin-left: $gap;
|
||||
|
||||
margin: 0 $gap 0 0;
|
||||
@include icon-color($color-white);
|
||||
}
|
||||
|
||||
.icon--logout {
|
||||
margin: 0 0 0 $gap;
|
||||
}
|
||||
|
||||
&--home {
|
||||
padding-left: $gap / 2;
|
||||
padding: 0 ($gap * 2);
|
||||
|
||||
.topbar__link-label {
|
||||
font-size: $base-font-size;
|
||||
font-weight: $font-bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -5,6 +5,7 @@
|
||||
{% from "components/modal.html" import Modal %}
|
||||
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||
{% from "components/save_button.html" import SaveButton %}
|
||||
{% from "components/toggle_menu.html" import ToggleMenu %}
|
||||
|
||||
{% macro MemberManagementTemplate(
|
||||
application,
|
||||
@ -38,16 +39,17 @@
|
||||
{% call Modal(modal_name, classes="form-content--app-mem") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
||||
<hr class="full-width">
|
||||
</div>
|
||||
<base-form inline-template>
|
||||
<form id='{{ modal_name }}' method="POST" action="{{ url_for(action_update, application_id=application.id, application_role_id=member.role_id,) }}">
|
||||
{{ member.form.csrf_token }}
|
||||
{{ member_fields.PermsFields(form=member.form, member_role_id=member.role_id) }}
|
||||
<div class="action-group">
|
||||
{{ SaveButton(text='Update', element='input', additional_classes='action-group__action') }}
|
||||
<a class='action-group__action usa-button usa-button-secondary' v-on:click="closeModal('{{ modal_name }}')">{{ "common.cancel" | translate }}</a>
|
||||
</div>
|
||||
{{ member_form.SubmitStep(
|
||||
name=modal_name,
|
||||
form=member_fields.PermsFields(form=member.form, member_role_id=member.role_id),
|
||||
submit_text="Update",
|
||||
previous=False,
|
||||
modal=modal_name,
|
||||
) }}
|
||||
</form>
|
||||
</base-form>
|
||||
{% endcall %}
|
||||
@ -57,16 +59,17 @@
|
||||
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
||||
<hr class="full-width">
|
||||
</div>
|
||||
<base-form inline-template :enable-save="true">
|
||||
<form id='{{ resend_invite_modal }}' method="POST" action="{{ url_for('applications.resend_invite', application_id=application.id, application_role_id=member.role_id) }}">
|
||||
{{ member.update_invite_form.csrf_token }}
|
||||
{{ member_fields.InfoFields(member.update_invite_form) }}
|
||||
<div class="action-group">
|
||||
{{ SaveButton(text="Resend Invite")}}
|
||||
<a class='action-group__action' v-on:click="closeModal('{{ resend_invite_modal }}')">{{ "common.cancel" | translate }}</a>
|
||||
</div>
|
||||
{{ member_form.SubmitStep(
|
||||
name=resend_invite_modal,
|
||||
form=member_fields.InfoFields(member.update_invite_form),
|
||||
submit_text="Resend Invite",
|
||||
previous=False,
|
||||
modal=resend_invite_modal,
|
||||
) }}
|
||||
</form>
|
||||
</base-form>
|
||||
{% endcall %}
|
||||
@ -119,7 +122,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="env_role--td">
|
||||
<td class="toggle-menu__container">
|
||||
{% for env in member.environment_roles %}
|
||||
<div class="row">
|
||||
<span class="env-role__environment">
|
||||
@ -131,32 +134,21 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if user_can(permissions.EDIT_APPLICATION_MEMBER) -%}
|
||||
<toggle-menu inline-template v-cloak>
|
||||
<div class="app-member-menu">
|
||||
<span v-if="isVisible" class="accordion-table__item__toggler accordion-table__item__toggler--active">
|
||||
{{ Icon('ellipsis')}}
|
||||
</span>
|
||||
<span v-else class="accordion-table__item__toggler">
|
||||
{{ Icon('ellipsis')}}
|
||||
</span>
|
||||
|
||||
<div v-show="isVisible" class="accordion-table__item-toggle-content app-member-menu__toggle">
|
||||
<a v-on:click="openModal('{{ perms_modal }}')">
|
||||
{{ "portfolios.applications.members.menu.edit" | translate }}
|
||||
</a>
|
||||
{% if invite_pending or invite_expired -%}
|
||||
{% set revoke_invite_modal = "revoke_invite_{}".format(member.role_id) %}
|
||||
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
|
||||
<a v-on:click='openModal("{{ resend_invite_modal }}")'>
|
||||
{{ "portfolios.applications.members.menu.resend" | translate }}
|
||||
</a>
|
||||
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) -%}
|
||||
<a v-on:click='openModal("{{ revoke_invite_modal }}")'>{{ 'invites.revoke' | translate }}</a>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
</div>
|
||||
</div>
|
||||
</toggle-menu>
|
||||
{% call ToggleMenu() %}
|
||||
<a v-on:click="openModal('{{ perms_modal }}')">
|
||||
{{ "portfolios.applications.members.menu.edit" | translate }}
|
||||
</a>
|
||||
{% if invite_pending or invite_expired -%}
|
||||
{% set revoke_invite_modal = "revoke_invite_{}".format(member.role_id) %}
|
||||
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
|
||||
<a v-on:click='openModal("{{ resend_invite_modal }}")'>
|
||||
{{ "portfolios.applications.members.menu.resend" | translate }}
|
||||
</a>
|
||||
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) -%}
|
||||
<a v-on:click='openModal("{{ revoke_invite_modal }}")'>{{ 'invites.revoke' | translate }}</a>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{% endcall %}
|
||||
{%- endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -25,7 +25,7 @@
|
||||
|
||||
<div class='usa-alert-body'>
|
||||
{% if vue_template %}
|
||||
<h3 class='usa-alert-heading' v-html='title'></h3>
|
||||
<h3 class='usa-alert-heading' v-text='title'></h3>
|
||||
{% elif title %}
|
||||
<h3 class='usa-alert-heading'>{{ title | safe }}</h3>
|
||||
{% endif %}
|
||||
|
@ -57,7 +57,7 @@
|
||||
<span class='usa-input__message'>{{ "forms.task_order.clin_funding_errors.obligated_amount_error" | translate }}</span>
|
||||
</template>
|
||||
<template v-else-if='showError'>
|
||||
<span class='usa-input__message' v-html='validationError'></span>
|
||||
<span class='usa-input__message' v-text='validationError'></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class='usa-input__message'></span>
|
||||
|
@ -68,7 +68,7 @@
|
||||
<input type='hidden' v-bind:value='rawValue' :name='name' />
|
||||
|
||||
<template v-if='showError'>
|
||||
<span class='usa-input__message' v-html='validationError'></span>
|
||||
<span class='usa-input__message' v-text='validationError'></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class='usa-input__message'></span>
|
||||
|
@ -54,7 +54,7 @@
|
||||
|
||||
|
||||
<template v-if='showError'>
|
||||
<span class='usa-input__message' v-html='validationError'></span>
|
||||
<span class='usa-input__message' v-text='validationError'></span>
|
||||
</template>
|
||||
|
||||
</fieldset>
|
||||
|
@ -48,7 +48,7 @@
|
||||
{{ field(disabled=disabled) }}
|
||||
|
||||
<template v-if='showError'>
|
||||
<span class='usa-input__message' v-html='validationError'></span>
|
||||
<span class='usa-input__message' v-text='validationError'></span>
|
||||
</template>
|
||||
|
||||
</fieldset>
|
||||
|
@ -107,7 +107,7 @@
|
||||
/>
|
||||
|
||||
{% if show_validation %}
|
||||
<span v-if='showError' class='usa-input__message' v-html='validationError'></span>
|
||||
<span v-if='showError' class='usa-input__message' v-text='validationError'></span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
17
templates/components/toggle_menu.html
Normal file
17
templates/components/toggle_menu.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% macro ToggleMenu() %}
|
||||
<toggle-menu inline-template v-cloak>
|
||||
<div class="toggle-menu">
|
||||
<span v-if="isVisible" class="accordion-table__item__toggler accordion-table__item__toggler--active">
|
||||
{{ Icon('ellipsis')}}
|
||||
</span>
|
||||
<span v-else class="accordion-table__item__toggler">
|
||||
{{ Icon('ellipsis')}}
|
||||
</span>
|
||||
<div v-show="isVisible" class="accordion-table__item-toggle-content toggle-menu__toggle">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
</div>
|
||||
</toggle-menu>
|
||||
{% endmacro %}
|
@ -59,10 +59,6 @@
|
||||
|
||||
<hr>
|
||||
|
||||
{% if user_can(permissions.VIEW_PORTFOLIO_POC) %}
|
||||
{% include "portfolios/fragments/primary_point_of_contact.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% if user_can(permissions.VIEW_PORTFOLIO_USERS) %}
|
||||
{% include "portfolios/fragments/portfolio_members.html" %}
|
||||
{% endif %}
|
||||
|
@ -1,80 +0,0 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/options_input.html" import OptionsInput %}
|
||||
|
||||
{% set step_one %}
|
||||
<hr class="full-width">
|
||||
<h1>{{ "fragments.ppoc.update_ppoc_title" | translate }}</h1>
|
||||
|
||||
{{
|
||||
Alert(
|
||||
level="warning",
|
||||
title=("fragments.ppoc.alert.title" | translate),
|
||||
message=("fragments.ppoc.alert.message" | translate),
|
||||
)
|
||||
}}
|
||||
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{
|
||||
OptionsInput(
|
||||
assign_ppoc_form.role_id,
|
||||
optional=False
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class='form-col form-col--half'>
|
||||
</div>
|
||||
</div>
|
||||
<div class='action-group'>
|
||||
<input
|
||||
type='button'
|
||||
v-on:click="next()"
|
||||
v-bind:disabled="!canSave"
|
||||
class='action-group__action usa-button'
|
||||
value='{{ "fragments.ppoc.assign_user_button_text" | translate }}'>
|
||||
<a class='action-group__action icon-link icon-link--default' v-on:click="closeModal('change-ppoc-form')">
|
||||
{{ "common.cancel" | translate }}
|
||||
</a>
|
||||
</div>
|
||||
{% endset %}
|
||||
|
||||
{% set step_two %}
|
||||
<hr class="full-width">
|
||||
<h1>{{ "fragments.ppoc.update_ppoc_confirmation_title" | translate }}</h1>
|
||||
|
||||
{{
|
||||
Alert(
|
||||
level="info",
|
||||
title=("fragments.ppoc.confirm_alert.title" | translate),
|
||||
)
|
||||
}}
|
||||
|
||||
<div class='action-group'>
|
||||
<input
|
||||
type="submit"
|
||||
class='action-group__action usa-button'
|
||||
form="change-ppoc-form"
|
||||
value='{{ "common.confirm" | translate }}'>
|
||||
<a class='action-group__action icon-link icon-link--default' v-on:click="closeModal('change-ppoc-form')">
|
||||
{{ "common.cancel" | translate }}
|
||||
</a>
|
||||
</div>
|
||||
{% endset %}
|
||||
|
||||
<div class="flex-reverse-row">
|
||||
{% set disable_ppoc_button = 1 == portfolio.members |length %}
|
||||
<button type="button" class="usa-button usa-button-primary" v-on:click="openModal('change-ppoc-form')" {% if disable_ppoc_button %}disabled{% endif %}>
|
||||
{{ "fragments.ppoc.update_btn" | translate }}
|
||||
</button>
|
||||
{{
|
||||
MultiStepModalForm(
|
||||
'change-ppoc-form',
|
||||
assign_ppoc_form,
|
||||
form_action=url_for("portfolios.update_ppoc", portfolio_id=portfolio.id),
|
||||
steps=[step_one, step_two],
|
||||
)
|
||||
}}
|
||||
</div>
|
@ -5,6 +5,92 @@
|
||||
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||
{% from 'components/save_button.html' import SaveButton %}
|
||||
{% import "portfolios/fragments/member_form_fields.html" as member_form_fields %}
|
||||
{% from "components/toggle_menu.html" import ToggleMenu %}
|
||||
|
||||
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) -%}
|
||||
{% for member in members -%}
|
||||
{% if not member.ppoc -%}
|
||||
{% set invite_pending = member.status == 'invite_pending' %}
|
||||
{% set invite_expired = member.status == 'invite_expired' %}
|
||||
|
||||
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
||||
{% call Modal(modal_name, classes="form-content--app-mem") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
||||
</div>
|
||||
<base-form inline-template>
|
||||
<form id='{{ modal_name }}' method="POST" action="{{ url_for('portfolios.update_member', portfolio_id=portfolio.id, portfolio_role_id=member.role_id) }}">
|
||||
{{ member.form.csrf_token }}
|
||||
{{ member_form.SubmitStep(
|
||||
name=modal_name,
|
||||
form=member_form_fields.PermsFields(member.form, member_role_id=member.role_id),
|
||||
submit_text="Save Changes",
|
||||
previous=False,
|
||||
modal=modal_name,
|
||||
) }}
|
||||
</form>
|
||||
</base-form>
|
||||
{% endcall %}
|
||||
|
||||
{% if invite_pending or invite_expired -%}
|
||||
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
|
||||
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
||||
</div>
|
||||
<base-form inline-template :enable-save="true">
|
||||
<form id='{{ resend_invite_modal }}' method="POST" action="{{ url_for('portfolios.resend_invitation', portfolio_id=portfolio.id, portfolio_token=member.invite_token) }}">
|
||||
{{ member.update_invite_form.csrf_token }}
|
||||
{{ member_form.SubmitStep(
|
||||
name=resend_invite_modal,
|
||||
form=member_form_fields.InfoFields(member.update_invite_form.user_data),
|
||||
submit_text="Resend Invite",
|
||||
previous=False,
|
||||
modal=resend_invite_modal
|
||||
) }}
|
||||
</form>
|
||||
</base-form>
|
||||
{% endcall %}
|
||||
|
||||
{% set revoke_invite_modal = "revoke_invite-{}".format(member.role_id) %}
|
||||
{% call Modal(name=revoke_invite_modal) %}
|
||||
<form method="post" action="{{ url_for('portfolios.revoke_invitation', portfolio_id=portfolio.id, portfolio_token=member.invite_token) }}">
|
||||
{{ member.form.csrf_token }}
|
||||
<h1>{{ "invites.revoke" | translate }}</h1>
|
||||
<hr class="full-width">
|
||||
{{ "invites.revoke_modal_text" | translate({"application": portfolio.name}) }}
|
||||
<div class="action-group">
|
||||
<button class="action-group__action usa-button usa-button-primary" type="submit">{{ "invites.revoke" | translate }}</button>
|
||||
<button class='action-group__action usa-button usa-button-secondary' v-on:click='closeModal("{{revoke_invite_modal}}")' type="button">{{ "common.cancel" | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{% set remove_manager_modal = "remove_manager-{}".format(member.role_id) %}
|
||||
{% call Modal(name=remove_manager_modal, dismissable=False) %}
|
||||
<h1>{{ "portfolios.admin.alert_header" | translate }}</h1>
|
||||
<hr class="full-width">
|
||||
{{
|
||||
Alert(
|
||||
title="portfolios.admin.alert_title" | translate,
|
||||
message="portfolios.admin.alert_message" | translate,
|
||||
level="warning"
|
||||
)
|
||||
}}
|
||||
<div class="action-group">
|
||||
<form method="POST" action="{{ url_for('portfolios.remove_member', portfolio_id=portfolio.id, portfolio_role_id=member.role_id)}}">
|
||||
{{ member.form.csrf_token }}
|
||||
<button class="usa-button usa-button-danger">
|
||||
{{ "portfolios.members.archive_button" | translate }}
|
||||
</button>
|
||||
</form>
|
||||
<a v-on:click="closeModal('{{ modal_id }}')" class="action-group__action icon-link icon-link--default">{{ "common.cancel" | translate }}</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
|
||||
<h3>Portfolio Managers</h3>
|
||||
<div class="panel">
|
||||
@ -19,6 +105,14 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in members -%}
|
||||
{% set invite_pending = member.status == 'invite_pending' %}
|
||||
{% set invite_expired = member.status == 'invite_expired' %}
|
||||
{% set current_user = current_member_id == member.role_id %}
|
||||
{% set perms_modal = "edit_member-{}".format(loop.index) %}
|
||||
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
|
||||
{% set revoke_invite_modal = "revoke_invite-{}".format(member.role_id) %}
|
||||
{% set remove_manager_modal = "remove_manager-{}".format(member.role_id) %}
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ member.user_name }}{% if member.role_id == current_member_id %} (You){% endif %}</strong>
|
||||
@ -28,14 +122,33 @@
|
||||
{% endif %}
|
||||
{{ Label(type=member.status, classes='label--below')}}
|
||||
</td>
|
||||
<td>
|
||||
<td class="toggle-menu__container">
|
||||
{% for perm, value in member.permission_sets.items() -%}
|
||||
<div>
|
||||
{% if value -%}
|
||||
{% if value -%}
|
||||
<div>
|
||||
{{ ("portfolios.admin.members.{}.{}".format(perm, value)) | translate }}
|
||||
{%- endif %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%-endfor %}
|
||||
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) -%}
|
||||
{% call ToggleMenu() %}
|
||||
<a
|
||||
{% if not member.ppoc %}v-on:click="openModal('{{ perms_modal }}')"{% endif %}
|
||||
class="{% if member.ppoc %}disabled{% endif %}">
|
||||
Edit Permissions
|
||||
</a>
|
||||
{% if invite_pending or invite_expired -%}
|
||||
<a v-on:click="openModal('{{ resend_invite_modal }}')">Resend Invite</a>
|
||||
<a v-on:click="openModal('{{ revoke_invite_modal }}')">Revoke Invite</a>
|
||||
{% else %}
|
||||
<a
|
||||
{% if not current_user %}v-on:click="openModal('{{ remove_manager_modal }}')"{% endif %}
|
||||
class="{% if current_user %}disabled{% endif %}">
|
||||
Remove Manager
|
||||
</a>
|
||||
{%- endif %}
|
||||
{% endcall %}
|
||||
{%- endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
@ -60,13 +173,13 @@
|
||||
form=member_form_fields.InfoFields(new_manager_form.user_data),
|
||||
next_button_text="Next: Permissions",
|
||||
previous=False,
|
||||
modal=new_manager_modal_name,
|
||||
modal=new_manager_modal,
|
||||
),
|
||||
member_form.SubmitStep(
|
||||
name=new_manager_modal,
|
||||
form=member_form_fields.PermsFields(new_manager_form),
|
||||
submit_text="Add Mananger",
|
||||
modal=new_manager_modal_name,
|
||||
modal=new_manager_modal,
|
||||
)
|
||||
],
|
||||
) }}
|
||||
|
@ -1,25 +0,0 @@
|
||||
<section id="primary-point-of-contact" class="panel">
|
||||
<div class="panel__content">
|
||||
{% if g.matchesPath("primary-point-of-contact") %}
|
||||
{% include "fragments/flash.html" %}
|
||||
{% endif %}
|
||||
|
||||
<h2>{{ "fragments.ppoc.title" | translate }}</h2>
|
||||
<p>{{ "fragments.ppoc.subtitle" | translate }}</p>
|
||||
|
||||
<p>
|
||||
<strong>
|
||||
{{ portfolio.owner.first_name }}
|
||||
{{ portfolio.owner.last_name }}
|
||||
</strong>
|
||||
<br />
|
||||
{{ portfolio.owner.email }}
|
||||
<br />
|
||||
{{ portfolio.owner.phone_number | usPhone }}
|
||||
</p>
|
||||
|
||||
{% if user_can(permissions.EDIT_PORTFOLIO_POC) %}
|
||||
{% include "portfolios/fragments/change_ppoc.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
1
terraform/.gitignore
vendored
1
terraform/.gitignore
vendored
@ -1 +1,2 @@
|
||||
.terraform
|
||||
.vscode/
|
||||
|
@ -57,6 +57,7 @@ To create all the resources we need for this environment we'll need to enable so
|
||||
This registers the specific feature for _SystemAssigned_ principals
|
||||
```
|
||||
az feature register --namespace Microsoft.ContainerService --name MSIPreview
|
||||
az feature register --namespace Microsoft.ContainerService --name NodePublicIPPreview
|
||||
```
|
||||
|
||||
To apply the registration, run the following
|
||||
@ -133,6 +134,42 @@ module "keyvault" {
|
||||
}
|
||||
```
|
||||
|
||||
## Setting the Redis key in KeyVault
|
||||
Redis auth is provided by a simple key that is randomly generated by Azure. This is a simple task for `secrets-tool`.
|
||||
|
||||
First, get the key from the portal. You can navigate to the redis cluster, and click on either "Show Keys", or "Access Keys"
|
||||
|
||||

|
||||
|
||||
In order to set the secret, make sure you specify the keyvault that is used by the application. In dev, its simply called "keyvault", where the operator keyvault has a different name.
|
||||
|
||||
```
|
||||
secrets-tool secrets --keyvault https://cloudzero-dev-keyvault.vault.azure.net/ create --key REDIS-PASSWORD --value "<redis key>"
|
||||
```
|
||||
You'll see output similar to the following if it was successful
|
||||
|
||||
```
|
||||
2020-01-17 14:04:42,996 - utils.keyvault.secrets - DEBUG - Set value for key: REDIS-PASSWORD
|
||||
```
|
||||
|
||||
## Setting the Azure Storage Key
|
||||
Azure storage is very similar to how Redis has a generated key. This generated key is what is used at the time of writing this doc.
|
||||
|
||||
Grab the key from the "Access Keys" tab on the cloud storage bucket
|
||||
|
||||

|
||||
|
||||
Now create the secret in KeyVault. This secret should also be in the application specific KeyVault.
|
||||
|
||||
```
|
||||
secrets-tool secrets --keyvault https://cloudzero-dev-keyvault.vault.azure.net/ create --key AZURE-STORAGE-KEY --value "<storage key>"
|
||||
```
|
||||
You'll see output similar to the following if it was successful
|
||||
|
||||
```
|
||||
2020-01-17 14:14:59,426 - utils.keyvault.secrets - DEBUG - Set value for key: AZURE-STORAGE-KEY
|
||||
```
|
||||
|
||||
# Shutting down and environment
|
||||
To shutdown and remove an environment completely as to not incur any costs you would need to run a `terraform destroy`.
|
||||
|
||||
@ -171,3 +208,76 @@ TODO
|
||||
|
||||
## Downloading a client profile
|
||||
TODO
|
||||
|
||||
# Quick Steps
|
||||
Copy paste (mostly)
|
||||
|
||||
*Register Preview features*
|
||||
See [Registering Features](#Preview_Features)
|
||||
|
||||
*Edit provider.tf and turn off remote bucket temporarily (comment out backend {} section)*
|
||||
```
|
||||
provider "azurerm" {
|
||||
version = "=1.40.0"
|
||||
}
|
||||
|
||||
provider "azuread" {
|
||||
# Whilst version is optional, we /strongly recommend/ using it to pin the version of the Provider being used
|
||||
version = "=0.7.0"
|
||||
}
|
||||
|
||||
terraform {
|
||||
#backend "azurerm" {
|
||||
#resource_group_name = "cloudzero-dev-tfstate"
|
||||
#storage_account_name = "cloudzerodevtfstate"
|
||||
#container_name = "tfstate"
|
||||
#key = "dev.terraform.tfstate"
|
||||
#}
|
||||
}
|
||||
```
|
||||
|
||||
`terraform init`
|
||||
|
||||
`terraform plan -target=module.tf_state`
|
||||
|
||||
Ensure the state bucket is created.
|
||||
|
||||
*create the container in the portal (or cli).*
|
||||
This simply involves going to the bucket in the azure portal and creating the container.
|
||||
|
||||
Now is the tricky part. For this, we will be switching from local state (files) to remote state (stored in the azure bucket)
|
||||
|
||||
Uncomment the `backend {}` section in the `provider.tf` file. Once uncommented, we will re-run the init. This will attempt to copy the local state to the remote bucket.
|
||||
|
||||
`terraform init`
|
||||
|
||||
*Say `yes` to the question*
|
||||
|
||||
Now we need to update the Update `variables.tf` with the principals for the users in `admin_users` variable map. If these are not defined yet, just leave it as an empty set.
|
||||
|
||||
Next, we'll create the operator keyvault.
|
||||
|
||||
`terraform plan -target=module.operator_keyvault`
|
||||
|
||||
Next, we'll pre-populate some secrets using the secrets-tool. Follow the install/setup section in the README.md first. Then populate the secrets with a definition file as described in the following link.
|
||||
|
||||
https://github.com/dod-ccpo/atst/tree/staging/terraform/secrets-tool#populating-secrets-from-secrets-definition-file
|
||||
|
||||
*Create service principal for AKS*
|
||||
```
|
||||
az ad sp create-for-rbac
|
||||
```
|
||||
Take note of the output, you'll need it in the next step to store the secret and `client_id` in keyvault.
|
||||
|
||||
This also involves using secrets-tool. Substitute your keyvault url.
|
||||
```
|
||||
secrets-tool secrets --keyvault https://ops-jedidev-keyvault.vault.azure.net/ create --key k8s-client-id --value [value]
|
||||
secrets-tool secrets --keyvault https://ops-jedidev-keyvault.vault.azure.net/ create --key k8s-client-secret --value [value]
|
||||
```
|
||||
|
||||
*Next we'll apply the rest of the TF configuration*
|
||||
|
||||
`terraform plan` # Make sure this looks correct
|
||||
|
||||
`terraform apply`
|
||||
|
||||
|
BIN
terraform/images/azure-storage.png
Normal file
BIN
terraform/images/azure-storage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 325 KiB |
BIN
terraform/images/redis-keys.png
Normal file
BIN
terraform/images/redis-keys.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 249 KiB |
@ -19,7 +19,8 @@ resource "azurerm_key_vault" "keyvault" {
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_key_vault_access_policy" "keyvault" {
|
||||
resource "azurerm_key_vault_access_policy" "keyvault_k8s_policy" {
|
||||
count = length(var.principal_id) > 0 ? 1 : 0
|
||||
key_vault_id = azurerm_key_vault.keyvault.id
|
||||
|
||||
tenant_id = data.azurerm_client_config.current.tenant_id
|
||||
@ -34,3 +35,38 @@ resource "azurerm_key_vault_access_policy" "keyvault" {
|
||||
]
|
||||
}
|
||||
|
||||
# Admin Access
|
||||
resource "azurerm_key_vault_access_policy" "keyvault_admin_policy" {
|
||||
for_each = var.admin_principals
|
||||
key_vault_id = azurerm_key_vault.keyvault.id
|
||||
|
||||
tenant_id = data.azurerm_client_config.current.tenant_id
|
||||
object_id = each.value
|
||||
|
||||
key_permissions = [
|
||||
"get",
|
||||
"list",
|
||||
"create",
|
||||
"update",
|
||||
"delete",
|
||||
]
|
||||
|
||||
secret_permissions = [
|
||||
"get",
|
||||
"list",
|
||||
"set",
|
||||
]
|
||||
|
||||
# backup create delete deleteissuers get getissuers import list listissuers managecontacts manageissuers purge recover restore setissuers update
|
||||
certificate_permissions = [
|
||||
"get",
|
||||
"list",
|
||||
"create",
|
||||
"import",
|
||||
"listissuers",
|
||||
"manageissuers",
|
||||
"deleteissuers",
|
||||
"backup",
|
||||
"update",
|
||||
]
|
||||
}
|
7
terraform/modules/keyvault/outputs.tf
Normal file
7
terraform/modules/keyvault/outputs.tf
Normal file
@ -0,0 +1,7 @@
|
||||
output "id" {
|
||||
value = azurerm_key_vault.keyvault.id
|
||||
}
|
||||
|
||||
output "url" {
|
||||
value = azurerm_key_vault.keyvault.vault_uri
|
||||
}
|
@ -27,3 +27,8 @@ variable "principal_id" {
|
||||
type = string
|
||||
description = "The service principal_id of the k8s cluster"
|
||||
}
|
||||
|
||||
variable "admin_principals" {
|
||||
type = map
|
||||
description = "A list of user principals who need access to manage the keyvault"
|
||||
}
|
||||
|
@ -75,13 +75,11 @@ variable "storage_auto_grow" {
|
||||
variable "administrator_login" {
|
||||
type = string
|
||||
description = "Administrator login"
|
||||
default = "atat_master" # FIXME - Remove with wrapper using KeyVault
|
||||
}
|
||||
|
||||
variable "administrator_login_password" {
|
||||
type = string
|
||||
description = "Administrator password"
|
||||
default = "eI0l7yswwtuhHpwzoVjwRKdAcuGNsg" # FIXME - Remove with wrapper using KeyVault
|
||||
}
|
||||
|
||||
variable "postgres_version" {
|
||||
|
@ -1,9 +1,11 @@
|
||||
module "keyvault" {
|
||||
source = "../../modules/keyvault"
|
||||
name = var.name
|
||||
region = var.region
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
tenant_id = var.tenant_id
|
||||
principal_id = "f9bcbe58-8b73-4957-aee2-133dc3e58063"
|
||||
source = "../../modules/keyvault"
|
||||
name = var.name
|
||||
region = var.region
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
tenant_id = var.tenant_id
|
||||
principal_id = "f9bcbe58-8b73-4957-aee2-133dc3e58063"
|
||||
admin_principals = var.admin_users
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,20 @@
|
||||
module "sql" {
|
||||
source = "../../modules/postgres"
|
||||
name = var.name
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
region = var.region
|
||||
subnet_id = module.vpc.subnets # FIXME - Should be a map of subnets and specify private
|
||||
data "azurerm_key_vault_secret" "postgres_username" {
|
||||
name = "postgres-root-user"
|
||||
key_vault_id = module.operator_keyvault.id
|
||||
}
|
||||
|
||||
data "azurerm_key_vault_secret" "postgres_password" {
|
||||
name = "postgres-root-password"
|
||||
key_vault_id = module.operator_keyvault.id
|
||||
}
|
||||
|
||||
module "sql" {
|
||||
source = "../../modules/postgres"
|
||||
name = var.name
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
region = var.region
|
||||
subnet_id = module.vpc.subnets # FIXME - Should be a map of subnets and specify private
|
||||
administrator_login = data.azurerm_key_vault_secret.postgres_username.value
|
||||
administrator_login_password = data.azurerm_key_vault_secret.postgres_password.value
|
||||
}
|
||||
|
10
terraform/providers/dev/secrets.tf
Normal file
10
terraform/providers/dev/secrets.tf
Normal file
@ -0,0 +1,10 @@
|
||||
module "operator_keyvault" {
|
||||
source = "../../modules/keyvault"
|
||||
name = "operator"
|
||||
region = var.region
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
tenant_id = var.tenant_id
|
||||
principal_id = ""
|
||||
admin_principals = var.admin_users
|
||||
}
|
@ -71,3 +71,11 @@ variable "tenant_id" {
|
||||
type = string
|
||||
default = "b5ab0e1e-09f8-4258-afb7-fb17654bc5b3"
|
||||
}
|
||||
|
||||
variable "admin_users" {
|
||||
type = map
|
||||
default = {
|
||||
"Rob Gil" = "2ca63d41-d058-4e06-aef6-eb517a53b631"
|
||||
"Daniel Corrigan" = "d5bb69c2-3b88-4e96-b1a2-320400f1bf1b"
|
||||
}
|
||||
}
|
||||
|
4
terraform/secrets-tool/.gitignore
vendored
Normal file
4
terraform/secrets-tool/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
bin/
|
||||
include/
|
||||
lib/
|
||||
|
63
terraform/secrets-tool/Pipfile
Normal file
63
terraform/secrets-tool/Pipfile
Normal file
@ -0,0 +1,63 @@
|
||||
[[source]]
|
||||
url = "https://pypi.python.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
requests = "==2.22.0"
|
||||
adal = "==1.2.2"
|
||||
antlr4-python3-runtime = "==4.7.2"
|
||||
applicationinsights = "==0.11.9"
|
||||
argcomplete = "==1.10.3"
|
||||
astroid = "==2.3.3"
|
||||
azure-cli-core = "==2.0.77"
|
||||
azure-cli-nspkg = "==3.0.4"
|
||||
azure-cli-telemetry = "==1.0.4"
|
||||
azure-common = "==1.1.23"
|
||||
azure-core = "==1.1.1"
|
||||
azure-identity = "==1.1.0"
|
||||
azure-keyvault = "==4.0.0"
|
||||
azure-keyvault-keys = "==4.0.0"
|
||||
azure-keyvault-secrets = "==4.0.0"
|
||||
azure-mgmt-resource = "==4.0.0"
|
||||
azure-nspkg = "==3.0.2"
|
||||
bcrypt = "==3.1.7"
|
||||
certifi = "==2019.11.28"
|
||||
cffi = "==1.13.2"
|
||||
chardet = "==3.0.4"
|
||||
click = "==7.0"
|
||||
colorama = "==0.4.3"
|
||||
coloredlogs = "==10.0"
|
||||
cryptography = "==2.8"
|
||||
humanfriendly = "==4.18"
|
||||
idna = "==2.8"
|
||||
isodate = "==0.6.0"
|
||||
isort = "==4.3.21"
|
||||
jmespath = "==0.9.4"
|
||||
knack = "==0.6.3"
|
||||
lazy-object-proxy = "==1.4.3"
|
||||
mccabe = "==0.6.1"
|
||||
msal = "==1.0.0"
|
||||
msal-extensions = "==0.1.3"
|
||||
msrest = "==0.6.10"
|
||||
msrestazure = "==0.6.2"
|
||||
oauthlib = "==3.1.0"
|
||||
paramiko = "==2.7.1"
|
||||
portalocker = "==1.5.2"
|
||||
pycparser = "==2.19"
|
||||
Pygments = "==2.5.2"
|
||||
PyJWT = "==1.7.1"
|
||||
pylint = "==2.4.4"
|
||||
PyNaCl = "==1.3.0"
|
||||
pyOpenSSL = "==19.1.0"
|
||||
python-dateutil = "==2.8.1"
|
||||
PyYAML = "==5.2"
|
||||
requests-oauthlib = "==1.3.0"
|
||||
six = "==1.13.0"
|
||||
tabulate = "==0.8.6"
|
||||
typed-ast = "==1.4.0"
|
||||
urllib3 = "==1.25.7"
|
||||
wrapt = "==1.11.2"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
670
terraform/secrets-tool/Pipfile.lock
generated
Normal file
670
terraform/secrets-tool/Pipfile.lock
generated
Normal file
@ -0,0 +1,670 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "83c0cc35bbf74c0b7620a5b7f4f9fab4324e86442e12284d1b9ffcc12a371696"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.python.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"adal": {
|
||||
"hashes": [
|
||||
"sha256:5a7f1e037c6290c6d7609cab33a9e5e988c2fbec5c51d1c4c649ee3faff37eaf",
|
||||
"sha256:fd17e5661f60634ddf96a569b95d34ccb8a98de60593d729c28bdcfe360eaad1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.2.2"
|
||||
},
|
||||
"antlr4-python3-runtime": {
|
||||
"hashes": [
|
||||
"sha256:168cdcec8fb9152e84a87ca6fd261b3d54c8f6358f42ab3b813b14a7193bb50b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.0'",
|
||||
"version": "==4.7.2"
|
||||
},
|
||||
"applicationinsights": {
|
||||
"hashes": [
|
||||
"sha256:30a11aafacea34f8b160fbdc35254c9029c7e325267874e3c68f6bdbcd6ed2c3",
|
||||
"sha256:b88bc5a41385d8e516489128d5e63f8c52efe597a3579b1718d1ab2f7cf150a2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.11.9"
|
||||
},
|
||||
"argcomplete": {
|
||||
"hashes": [
|
||||
"sha256:a37f522cf3b6a34abddfedb61c4546f60023b3799b22d1cd971eacdc0861530a",
|
||||
"sha256:d8ea63ebaec7f59e56e7b2a386b1d1c7f1a7ae87902c9ee17d377eaa557f06fa"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.10.3"
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a",
|
||||
"sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.3.3"
|
||||
},
|
||||
"azure-cli-core": {
|
||||
"hashes": [
|
||||
"sha256:4281b71cf9a8278f665765c97eb3dae61fbf2dac916fc032c4acdf5ed2065210",
|
||||
"sha256:d14a733dd6d6019c23dbd0b459026fef0978901d263382a1fdb71852293b9e6a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.77"
|
||||
},
|
||||
"azure-cli-nspkg": {
|
||||
"hashes": [
|
||||
"sha256:1bde56090f548c6435bd3093995cf88e4c445fb040604df8b5b5f70780d79181",
|
||||
"sha256:9a1e4f3197183470e4afecfdd45c92320f6753555b06a70651f89972332ffaf6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"azure-cli-telemetry": {
|
||||
"hashes": [
|
||||
"sha256:1f239d544d309c29e827982cc20113eb57037dba16db6cdd2e0283e437e0e577",
|
||||
"sha256:7b18d7520e35e134136a0f7de38403a7dbce7b1e835065bd9e965579815ddf2f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.4"
|
||||
},
|
||||
"azure-common": {
|
||||
"hashes": [
|
||||
"sha256:53b1195b8f20943ccc0e71a17849258f7781bc6db1c72edc7d6c055f79bd54e3",
|
||||
"sha256:99ef36e74b6395329aada288764ce80504da16ecc8206cb9a72f55fb02e8b484"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.23"
|
||||
},
|
||||
"azure-core": {
|
||||
"hashes": [
|
||||
"sha256:4d047fd4e46a958c9b63f9d5cb52e6bf7dfc5c2a1c2a81b968499335a94bb5cb",
|
||||
"sha256:b44fe5b46d2bb0260cafb737ab5ee89a16d478fc1885dabe21c426c4df205502"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"azure-identity": {
|
||||
"hashes": [
|
||||
"sha256:0c8e540e1b75d48c54e5cd8d599f7ea5ccf4dae260c35bebb0c8d34d22b7c4f6",
|
||||
"sha256:75f4ad9abfd191bd5f3de4c6dc29980b138bf5dfbbef9bca5c548a6048473bde"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"azure-keyvault": {
|
||||
"hashes": [
|
||||
"sha256:76f75cb83929f312a08616d426ad6f597f1beae180131cf445876fb88f2c8ef1",
|
||||
"sha256:e85f5bd6cb4f10b3248b99bbf02e3acc6371d366846897027d4153f18025a2d7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"azure-keyvault-keys": {
|
||||
"hashes": [
|
||||
"sha256:2983fa42e20a0e6bf6b87976716129c108e613e0292d34c5b0f0c8dc1d488e89",
|
||||
"sha256:38c27322637a2c52620a8b96da1942ad6a8d22d09b5a01f6fa257f7a51e52ed0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"azure-keyvault-secrets": {
|
||||
"hashes": [
|
||||
"sha256:2eae9264a8f6f59277e1a9bfdbc8b0a15969ee5a80d8efe403d7744805b4a481",
|
||||
"sha256:97a602406a833e8f117c540c66059c818f4321a35168dd17365fab1e4527d718"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"azure-mgmt-resource": {
|
||||
"hashes": [
|
||||
"sha256:2b909f137469c7bfa541554c3d22eb918e9191c07667a42f2c6fc684e24ac83f",
|
||||
"sha256:5022263349e66dba5ddadd3bf36208d82a00e0f1bb3288e32822fc821ccd1f76"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"azure-nspkg": {
|
||||
"hashes": [
|
||||
"sha256:1d0bbb2157cf57b1bef6c8c8e5b41133957364456c43b0a43599890023cca0a8",
|
||||
"sha256:31a060caca00ed1ebd369fc7fe01a56768c927e404ebc92268f4d9d636435e28",
|
||||
"sha256:e7d3cea6af63e667d87ba1ca4f8cd7cb4dfca678e4c55fc1cedb320760e39dd0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"bcrypt": {
|
||||
"hashes": [
|
||||
"sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89",
|
||||
"sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42",
|
||||
"sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294",
|
||||
"sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161",
|
||||
"sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752",
|
||||
"sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31",
|
||||
"sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5",
|
||||
"sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c",
|
||||
"sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0",
|
||||
"sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de",
|
||||
"sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e",
|
||||
"sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052",
|
||||
"sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09",
|
||||
"sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105",
|
||||
"sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133",
|
||||
"sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1",
|
||||
"sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7",
|
||||
"sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.7"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
|
||||
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2019.11.28"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42",
|
||||
"sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04",
|
||||
"sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5",
|
||||
"sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54",
|
||||
"sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba",
|
||||
"sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57",
|
||||
"sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396",
|
||||
"sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12",
|
||||
"sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97",
|
||||
"sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43",
|
||||
"sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db",
|
||||
"sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3",
|
||||
"sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b",
|
||||
"sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579",
|
||||
"sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346",
|
||||
"sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159",
|
||||
"sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652",
|
||||
"sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e",
|
||||
"sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a",
|
||||
"sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506",
|
||||
"sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f",
|
||||
"sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d",
|
||||
"sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c",
|
||||
"sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20",
|
||||
"sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858",
|
||||
"sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc",
|
||||
"sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a",
|
||||
"sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3",
|
||||
"sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e",
|
||||
"sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410",
|
||||
"sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25",
|
||||
"sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b",
|
||||
"sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.13.2"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.0"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
|
||||
"sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"coloredlogs": {
|
||||
"hashes": [
|
||||
"sha256:34fad2e342d5a559c31b6c889e8d14f97cb62c47d9a2ae7b5ed14ea10a79eff8",
|
||||
"sha256:b869a2dda3fa88154b9dd850e27828d8755bfab5a838a1c97fbc850c6e377c36"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==10.0"
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
|
||||
"sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
|
||||
"sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
|
||||
"sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
|
||||
"sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
|
||||
"sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
|
||||
"sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
|
||||
"sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
|
||||
"sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
|
||||
"sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
|
||||
"sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
|
||||
"sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
|
||||
"sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
|
||||
"sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
|
||||
"sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
|
||||
"sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
|
||||
"sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
|
||||
"sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
|
||||
"sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
|
||||
"sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
|
||||
"sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8"
|
||||
},
|
||||
"humanfriendly": {
|
||||
"hashes": [
|
||||
"sha256:23057b10ad6f782e7bc3a20e3cb6768ab919f619bbdc0dd75691121bbde5591d",
|
||||
"sha256:33ee8ceb63f1db61cce8b5c800c531e1a61023ac5488ccde2ba574a85be00a85"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.18"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8"
|
||||
},
|
||||
"isodate": {
|
||||
"hashes": [
|
||||
"sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8",
|
||||
"sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.21"
|
||||
},
|
||||
"jmespath": {
|
||||
"hashes": [
|
||||
"sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6",
|
||||
"sha256:bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.9.4"
|
||||
},
|
||||
"knack": {
|
||||
"hashes": [
|
||||
"sha256:b1ac92669641b902e1aef97138666a21b8852f65d83cbde03eb9ddebf82ce121",
|
||||
"sha256:bd240163d4e2ce9fc8535f77519358da0afd6c0ca19f001c639c3160b57630a9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.6.3"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
"hashes": [
|
||||
"sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d",
|
||||
"sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449",
|
||||
"sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08",
|
||||
"sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a",
|
||||
"sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50",
|
||||
"sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd",
|
||||
"sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239",
|
||||
"sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb",
|
||||
"sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea",
|
||||
"sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e",
|
||||
"sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156",
|
||||
"sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142",
|
||||
"sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442",
|
||||
"sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62",
|
||||
"sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db",
|
||||
"sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531",
|
||||
"sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383",
|
||||
"sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a",
|
||||
"sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357",
|
||||
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
|
||||
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"msal": {
|
||||
"hashes": [
|
||||
"sha256:c944b833bf686dfbc973e9affdef94b77e616cb52ab397e76cde82e26b8a3373",
|
||||
"sha256:ecbe3f5ac77facad16abf08eb9d8562af3bc7184be5d4d90c9ef4db5bde26340"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"msal-extensions": {
|
||||
"hashes": [
|
||||
"sha256:59e171a9a4baacdbf001c66915efeaef372fb424421f1a4397115a3ddd6205dc",
|
||||
"sha256:c5a32b8e1dce1c67733dcdf8aa8bebcff5ab123e779ef7bc14e416bd0da90037"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.1.3"
|
||||
},
|
||||
"msrest": {
|
||||
"hashes": [
|
||||
"sha256:56b8b5b4556fb2a92cac640df267d560889bdc9e2921187772d4691d97bc4e8d",
|
||||
"sha256:f5153bfe60ee757725816aedaa0772cbfe0bddb52cd2d6db4cb8b4c3c6c6f928"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.6.10"
|
||||
},
|
||||
"msrestazure": {
|
||||
"hashes": [
|
||||
"sha256:63db9f646fffc9244b332090e679d1e5f283ac491ee0cc321f5116f9450deb4a",
|
||||
"sha256:fecb6a72a3eb5483e4deff38210d26ae42d3f6d488a7a275bd2423a1a014b22c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.6.2"
|
||||
},
|
||||
"oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
||||
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"paramiko": {
|
||||
"hashes": [
|
||||
"sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f",
|
||||
"sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.7.1"
|
||||
},
|
||||
"portalocker": {
|
||||
"hashes": [
|
||||
"sha256:6f57aabb25ba176462dc7c63b86c42ad6a9b5bd3d679a9d776d0536bfb803d54",
|
||||
"sha256:dac62e53e5670cb40d2ee4cdc785e6b829665932c3ee75307ad677cf5f7d2e9f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.5.2"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.19"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
|
||||
"sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.5.2"
|
||||
},
|
||||
"pyjwt": {
|
||||
"hashes": [
|
||||
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
|
||||
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.7.1"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd",
|
||||
"sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.4.4"
|
||||
},
|
||||
"pynacl": {
|
||||
"hashes": [
|
||||
"sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255",
|
||||
"sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c",
|
||||
"sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e",
|
||||
"sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae",
|
||||
"sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621",
|
||||
"sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56",
|
||||
"sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39",
|
||||
"sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310",
|
||||
"sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1",
|
||||
"sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5",
|
||||
"sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a",
|
||||
"sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786",
|
||||
"sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b",
|
||||
"sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b",
|
||||
"sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f",
|
||||
"sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20",
|
||||
"sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415",
|
||||
"sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715",
|
||||
"sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92",
|
||||
"sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1",
|
||||
"sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"pyopenssl": {
|
||||
"hashes": [
|
||||
"sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504",
|
||||
"sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.1.0"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
|
||||
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
|
||||
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
|
||||
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
|
||||
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
|
||||
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
|
||||
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
|
||||
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
|
||||
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
|
||||
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.2"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.22.0"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
|
||||
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.13.0"
|
||||
},
|
||||
"tabulate": {
|
||||
"hashes": [
|
||||
"sha256:5470cc6687a091c7042cee89b2946d9235fe9f6d49c193a4ae2ac7bf386737c8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.8.6"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
|
||||
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
|
||||
"sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e",
|
||||
"sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0",
|
||||
"sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c",
|
||||
"sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47",
|
||||
"sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631",
|
||||
"sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4",
|
||||
"sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34",
|
||||
"sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b",
|
||||
"sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2",
|
||||
"sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e",
|
||||
"sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a",
|
||||
"sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233",
|
||||
"sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1",
|
||||
"sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36",
|
||||
"sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d",
|
||||
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
|
||||
"sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
|
||||
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "implementation_name == 'cpython' and python_version < '3.8'",
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
|
||||
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.25.7"
|
||||
},
|
||||
"wheel": {
|
||||
"hashes": [
|
||||
"sha256:9515fe0a94e823fd90b08d22de45d7bde57c90edce705b22f5e1ecf7e1b653c8",
|
||||
"sha256:e721e53864f084f956f40f96124a74da0631ac13fbbd1ba99e8e2b5e9cafdf64"
|
||||
],
|
||||
"version": "==0.30.0"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
"sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.11.2"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
||||
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
|
||||
"sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
|
||||
],
|
||||
"version": "==8.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb",
|
||||
"sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"
|
||||
],
|
||||
"version": "==20.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
|
||||
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
|
||||
],
|
||||
"version": "==1.8.1"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
|
||||
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
|
||||
],
|
||||
"version": "==2.4.6"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa",
|
||||
"sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.3.2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.13.0"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
|
||||
"sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
|
||||
],
|
||||
"version": "==0.1.8"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
||||
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
79
terraform/secrets-tool/README.md
Normal file
79
terraform/secrets-tool/README.md
Normal file
@ -0,0 +1,79 @@
|
||||
# secrets-tool
|
||||
secrets-tool is a group of utilities used to manage secrets in Azure environments.
|
||||
|
||||
*Features:*
|
||||
- Generate secrets based on definitions defined in yaml
|
||||
- Load secrets in to Azure KeyVault
|
||||
- Wrapper for terraform to inject KeyVault secrets as environment variables
|
||||
|
||||
# Use Cases
|
||||
## Populating KeyVault with initial secrets
|
||||
In many environments, a complete list of secrets is sometimes forgotten or not well defined. With secrets-tool, all those secrets can be defined programatically and generated when creating new environments. This avoids putting in "test" values for passwords and guessible username/password combinations. Even usernames can be generated.
|
||||
|
||||
With both usernames and passwords generated, the application only needs to make a call out to KeyVault for the key that it needs (assuming the application, host, or vm has access to the secret)
|
||||
|
||||
Ex.
|
||||
```
|
||||
{
|
||||
'postgres_root_user': 'EzTEzSNLKQPHuJyPdPloIDCAlcibbl',
|
||||
'postgres_root_password': "2+[A@E4:C=ubb/#R#'n<p|wCW-|%q^"
|
||||
}
|
||||
```
|
||||
|
||||
## Rotating secrets
|
||||
Rotating passwords is a snap! Just re-run secrets-tool and it will generate and populate new secrets.
|
||||
|
||||
**Be careful!! There is no safeguard to prevent you from accidentally overwriting secrets!! - To be added if desired**
|
||||
|
||||
## Terraform Secrets
|
||||
Terraform typically expects user defined secrets to be stored in either a file, or in another service such as keyvault. The terraform wrapper feature, injects secrets from keyvault in to the environment and then runs terraform.
|
||||
|
||||
This provides a number of security benefits. First, secrets are not on disk. Secondly, users/operators never see the secrets fly by (passerbys or voyeurs that like to look over your shoulder when deploying to production)
|
||||
|
||||
# Setup
|
||||
|
||||
*Requirements*
|
||||
- Python 3.7+
|
||||
- pipenv
|
||||
|
||||
```
|
||||
cd secrets-tool
|
||||
pipenv install
|
||||
pipenv shell
|
||||
```
|
||||
|
||||
You will also need to make sure secrets-tool is in your PATH
|
||||
|
||||
```
|
||||
echo 'PATH=$PATH:<path to secrets-tool>' > ~/.bash_profile
|
||||
. ~/.bash_profile
|
||||
```
|
||||
|
||||
`$ which secrets-tool` should show the full path
|
||||
|
||||
# Usage
|
||||
## Defining secrets
|
||||
The schema for defining secrets is very simplistic for the moment.
|
||||
```yaml
|
||||
---
|
||||
- postgres-root-user:
|
||||
type: 'username'
|
||||
length: 30
|
||||
- postgres-root-password:
|
||||
type: 'password'
|
||||
length: 30
|
||||
```
|
||||
In this example we're randomly generating both the username and password. `secrets-tool` is smart enough to know that a username can't have symbols in it. Passwords contain symbols, upper/lower case, and numbers. This could be made more flexible and configurable in the future.
|
||||
|
||||
|
||||
## Populating secrets from secrets definition file
|
||||
This process is as simple as specifying the keyvault and the definitions file.
|
||||
```
|
||||
secrets-tool secrets --keyvault https://operator-dev-keyvault.vault.azure.net/ load -f ./sample-secrets.yaml
|
||||
```
|
||||
|
||||
## Running terraform with KeyVault secrets
|
||||
This will fetch all secrets from the keyvault specified. `secrets-tool` then converts the keys to a variable name that terraform will look for. Essentially it prepends the keys found in KeyVault with `TF_VAR` and then executes terraform as a subprocess with the injected environment variables.
|
||||
```
|
||||
secrets-tool terraform --keyvault https://operator-dev-keyvault.vault.azure.net/ plan
|
||||
```
|
0
terraform/secrets-tool/commands/__init__.py
Normal file
0
terraform/secrets-tool/commands/__init__.py
Normal file
47
terraform/secrets-tool/commands/secrets.py
Normal file
47
terraform/secrets-tool/commands/secrets.py
Normal file
@ -0,0 +1,47 @@
|
||||
import click
|
||||
import logging
|
||||
from utils.keyvault.secrets import SecretsClient
|
||||
from utils.keyvault.secrets import SecretsLoader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict]
|
||||
#print(loggers)
|
||||
|
||||
@click.group()
|
||||
@click.option('--keyvault', required=True, help="Specify the keyvault to operate on")
|
||||
@click.pass_context
|
||||
def secrets(ctx, keyvault):
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj['keyvault'] = keyvault
|
||||
|
||||
@click.command('create')
|
||||
@click.option('--key', 'key', required=True, help="Key for the secret to create")
|
||||
@click.option('--value', 'value', required=True, prompt=True, hide_input=True, confirmation_prompt=True, help="Value for the secret to create")
|
||||
@click.pass_context
|
||||
def create_secret(ctx, key, value):
|
||||
"""Creates a secret in the specified KeyVault"""
|
||||
keyvault = SecretsClient(vault_url=ctx.obj['keyvault'])
|
||||
keyvault.set_secret(key, value)
|
||||
|
||||
@click.command('list')
|
||||
@click.pass_context
|
||||
def list_secrets(ctx):
|
||||
"""Lists the secrets in the specified KeyVault"""
|
||||
keyvault = SecretsClient(vault_url=ctx.obj['keyvault'])
|
||||
click.echo(keyvault.list_secrets())
|
||||
|
||||
@click.command('load')
|
||||
@click.option('-f', 'file', required=True, help="YAML file with secrets definitions")
|
||||
@click.pass_context
|
||||
def load_secrets(ctx, file):
|
||||
"""Generate and load secrets from yaml definition"""
|
||||
keyvault = SecretsClient(vault_url=ctx.obj['keyvault'])
|
||||
loader = SecretsLoader(yaml_file=file, keyvault=keyvault)
|
||||
loader.load_secrets()
|
||||
|
||||
|
||||
|
||||
secrets.add_command(create_secret)
|
||||
secrets.add_command(list_secrets)
|
||||
secrets.add_command(load_secrets)
|
49
terraform/secrets-tool/commands/terraform.py
Normal file
49
terraform/secrets-tool/commands/terraform.py
Normal file
@ -0,0 +1,49 @@
|
||||
import click
|
||||
import logging
|
||||
|
||||
from utils.keyvault.secrets import SecretsClient
|
||||
from utils.terraform.wrapper import TFWrapper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROCESS='terraform'
|
||||
|
||||
@click.group()
|
||||
@click.option('--keyvault', required=True, help="Specify the keyvault to operate on")
|
||||
@click.pass_context
|
||||
def terraform(ctx, keyvault):
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj['keyvault'] = keyvault
|
||||
|
||||
@click.command('plan')
|
||||
@click.pass_context
|
||||
def plan(ctx):
|
||||
keyvault = SecretsClient(vault_url=ctx.obj['keyvault'])
|
||||
tf = TFWrapper(keyvault)
|
||||
tf.plan()
|
||||
|
||||
@click.command('apply')
|
||||
@click.pass_context
|
||||
def apply(ctx):
|
||||
keyvault = SecretsClient(vault_url=ctx.obj['keyvault'])
|
||||
tf = TFWrapper(keyvault)
|
||||
tf.apply()
|
||||
|
||||
@click.command('destroy')
|
||||
@click.pass_context
|
||||
def destroy(ctx):
|
||||
keyvault = SecretsClient(vault_url=ctx.obj['keyvault'])
|
||||
tf = TFWrapper(keyvault)
|
||||
tf.destroy()
|
||||
|
||||
@click.command('init')
|
||||
@click.pass_context
|
||||
def init(ctx):
|
||||
keyvault = SecretsClient(vault_url=ctx.obj['keyvault'])
|
||||
tf = TFWrapper(keyvault)
|
||||
tf.init()
|
||||
|
||||
terraform.add_command(plan)
|
||||
terraform.add_command(apply)
|
||||
terraform.add_command(destroy)
|
||||
terraform.add_command(init)
|
28
terraform/secrets-tool/config.py
Normal file
28
terraform/secrets-tool/config.py
Normal file
@ -0,0 +1,28 @@
|
||||
import os
|
||||
import yaml
|
||||
import logging.config
|
||||
import logging
|
||||
import coloredlogs
|
||||
|
||||
LOGGING_PATH=os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def setup_logging(default_path='{}/logging.yaml'.format(LOGGING_PATH), default_level=logging.INFO, env_key='LOG_CFG'):
|
||||
path = default_path
|
||||
value = os.getenv(env_key, None)
|
||||
if value:
|
||||
path = value
|
||||
if os.path.exists(path):
|
||||
with open(path, 'rt') as f:
|
||||
try:
|
||||
config = yaml.safe_load(f.read())
|
||||
logging.config.dictConfig(config)
|
||||
coloredlogs.install()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print('Error in Logging Configuration. Using default configs')
|
||||
logging.basicConfig(level=default_level)
|
||||
coloredlogs.install(level=default_level)
|
||||
else:
|
||||
logging.basicConfig(level=default_level)
|
||||
coloredlogs.install(level=default_level)
|
||||
print('Failed to load configuration file. Using default configs')
|
64
terraform/secrets-tool/logging.yaml
Normal file
64
terraform/secrets-tool/logging.yaml
Normal file
@ -0,0 +1,64 @@
|
||||
version: 1
|
||||
disable_existing_loggers: true
|
||||
|
||||
formatters:
|
||||
standard:
|
||||
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
error:
|
||||
format: "%(levelname)s <PID %(process)d:%(processName)s> %(name)s.%(funcName)s(): %(message)s"
|
||||
|
||||
handlers:
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
level: DEBUG
|
||||
formatter: standard
|
||||
stream: ext://sys.stdout
|
||||
|
||||
file_handler:
|
||||
class: logging.handlers.RotatingFileHandler
|
||||
level: INFO
|
||||
formatter: standard
|
||||
filename: secrets-tool.log
|
||||
maxBytes: 10485760 # 10MB
|
||||
backupCount: 20
|
||||
encoding: utf8
|
||||
|
||||
root:
|
||||
level: INFO
|
||||
handlers: [console]
|
||||
propogate: yes
|
||||
|
||||
loggers:
|
||||
root:
|
||||
level: INFO
|
||||
handlers: [console]
|
||||
propogate: no
|
||||
click:
|
||||
level: INFO
|
||||
handlers: [console]
|
||||
propogate: yes
|
||||
azure.keyvault:
|
||||
level: INFO
|
||||
handlers: [console]
|
||||
propogate: yes
|
||||
azure.core:
|
||||
level: ERROR
|
||||
handlers: [console]
|
||||
propogate: no
|
||||
utils.keyvault.secrets:
|
||||
level: DEBUG
|
||||
handlers: [console]
|
||||
propogate: yes
|
||||
utils.terraform.wrapper:
|
||||
level: DEBUG
|
||||
handlers: [console]
|
||||
propogate: yes
|
||||
commands:
|
||||
level: INFO
|
||||
handlers: [console]
|
||||
propogate: yes
|
||||
main:
|
||||
level: INFO
|
||||
handlers: [console]
|
||||
propogate: no
|
||||
|
54
terraform/secrets-tool/requirements.txt
Normal file
54
terraform/secrets-tool/requirements.txt
Normal file
@ -0,0 +1,54 @@
|
||||
adal==1.2.2
|
||||
antlr4-python3-runtime==4.7.2
|
||||
applicationinsights==0.11.9
|
||||
argcomplete==1.10.3
|
||||
astroid==2.3.3
|
||||
azure-cli-core==2.0.77
|
||||
azure-cli-nspkg==3.0.4
|
||||
azure-cli-telemetry==1.0.4
|
||||
azure-common==1.1.23
|
||||
azure-core==1.1.1
|
||||
azure-identity==1.1.0
|
||||
azure-keyvault==4.0.0
|
||||
azure-keyvault-keys==4.0.0
|
||||
azure-keyvault-secrets==4.0.0
|
||||
azure-mgmt-resource==4.0.0
|
||||
azure-nspkg==3.0.2
|
||||
bcrypt==3.1.7
|
||||
certifi==2019.11.28
|
||||
cffi==1.13.2
|
||||
chardet==3.0.4
|
||||
Click==7.0
|
||||
colorama==0.4.3
|
||||
coloredlogs==10.0
|
||||
cryptography==2.8
|
||||
humanfriendly==4.18
|
||||
idna==2.8
|
||||
isodate==0.6.0
|
||||
isort==4.3.21
|
||||
jmespath==0.9.4
|
||||
knack==0.6.3
|
||||
lazy-object-proxy==1.4.3
|
||||
mccabe==0.6.1
|
||||
msal==1.0.0
|
||||
msal-extensions==0.1.3
|
||||
msrest==0.6.10
|
||||
msrestazure==0.6.2
|
||||
oauthlib==3.1.0
|
||||
paramiko==2.7.1
|
||||
portalocker==1.5.2
|
||||
pycparser==2.19
|
||||
Pygments==2.5.2
|
||||
PyJWT==1.7.1
|
||||
pylint==2.4.4
|
||||
PyNaCl==1.3.0
|
||||
pyOpenSSL==19.1.0
|
||||
python-dateutil==2.8.1
|
||||
PyYAML==5.2
|
||||
requests==2.22.0
|
||||
requests-oauthlib==1.3.0
|
||||
six==1.13.0
|
||||
tabulate==0.8.6
|
||||
typed-ast==1.4.0
|
||||
urllib3==1.25.7
|
||||
wrapt==1.11.2
|
7
terraform/secrets-tool/sample-secrets.yaml
Normal file
7
terraform/secrets-tool/sample-secrets.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
- postgres-root-user:
|
||||
type: 'username'
|
||||
length: 30
|
||||
- postgres-root-password:
|
||||
type: 'password'
|
||||
length: 30
|
52
terraform/secrets-tool/secrets-tool
Executable file
52
terraform/secrets-tool/secrets-tool
Executable file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python
|
||||
# CLI
|
||||
import click
|
||||
|
||||
import config
|
||||
import logging
|
||||
|
||||
from commands.secrets import secrets
|
||||
from commands.terraform import terraform
|
||||
|
||||
config.setup_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROCESS='terraform'
|
||||
|
||||
# Define core command group
|
||||
@click.group()
|
||||
def cli():
|
||||
pass
|
||||
|
||||
# Add additional command groups
|
||||
cli.add_command(secrets)
|
||||
cli.add_command(terraform)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
cli()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
'''
|
||||
try:
|
||||
keyvault = secrets(vault_url="https://cloudzero-dev-keyvault.vault.azure.net/")
|
||||
keyvault.set_secret('dbuser','foo')
|
||||
#print(keyvault.get_secret('db-user').value)
|
||||
|
||||
# Set env variables for TF
|
||||
for secret in keyvault.list_secrets():
|
||||
name = 'TF_VAR_' + secret
|
||||
val = keyvault.get_secret(secret)
|
||||
#print(val)
|
||||
os.environ[name] = val
|
||||
env = os.environ.copy()
|
||||
command = "{} {}".format(PROCESS, sys.argv[1])
|
||||
with subprocess.Popen(command, env=env, stdout=subprocess.PIPE, shell=True) as proc:
|
||||
for line in proc.stdout:
|
||||
logging.info(line.decode("utf-8") )
|
||||
|
||||
except Exception as e:
|
||||
print(e, traceback.print_stack)
|
||||
'''
|
0
terraform/secrets-tool/utils/__init__.py
Normal file
0
terraform/secrets-tool/utils/__init__.py
Normal file
0
terraform/secrets-tool/utils/keyvault/__init__.py
Normal file
0
terraform/secrets-tool/utils/keyvault/__init__.py
Normal file
10
terraform/secrets-tool/utils/keyvault/auth.py
Normal file
10
terraform/secrets-tool/utils/keyvault/auth.py
Normal file
@ -0,0 +1,10 @@
|
||||
import logging
|
||||
|
||||
from azure.identity import InteractiveBrowserCredential
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Auth:
|
||||
def __init__(self, vault_url, *args, **kwargs):
|
||||
self.credentials = InteractiveBrowserCredential()
|
||||
self.vault_url = vault_url
|
19
terraform/secrets-tool/utils/keyvault/keys.py
Normal file
19
terraform/secrets-tool/utils/keyvault/keys.py
Normal file
@ -0,0 +1,19 @@
|
||||
import logging
|
||||
from azure.keyvault.keys import KeyClient
|
||||
from .auth import Auth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
KEY_SIZE=2048
|
||||
KEY_TYPE='rsa'
|
||||
|
||||
class keys(Auth):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(keys, self).__init__(*args, **kwargs)
|
||||
self.key_client = KeyClient(vault_url=self.vault_url, credential=self.credentials)
|
||||
|
||||
def get_key(self):
|
||||
return self.key_client
|
||||
|
||||
def create_key(self):
|
||||
pass
|
109
terraform/secrets-tool/utils/keyvault/secrets.py
Normal file
109
terraform/secrets-tool/utils/keyvault/secrets.py
Normal file
@ -0,0 +1,109 @@
|
||||
import logging
|
||||
import yaml
|
||||
import secrets
|
||||
import string
|
||||
from pathlib import Path
|
||||
from .auth import Auth
|
||||
from azure.keyvault.secrets import SecretClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SecretsClient(Auth):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SecretsClient, self).__init__(*args, **kwargs)
|
||||
self.secret_client = SecretClient(vault_url=self.vault_url, credential=self.credentials)
|
||||
|
||||
def get_secret(self, key):
|
||||
secret = self.secret_client.get_secret(key)
|
||||
return secret.value
|
||||
|
||||
def set_secret(self, key: str, value: str):
|
||||
secret = self.secret_client.set_secret(key, value)
|
||||
logger.debug('Set value for key: {}'.format(key))
|
||||
return secret
|
||||
|
||||
def list_secrets(self):
|
||||
secrets = list()
|
||||
secret_properties = self.secret_client.list_properties_of_secrets()
|
||||
for secret in secret_properties:
|
||||
secrets.append(secret.name)
|
||||
return secrets
|
||||
|
||||
class SecretsLoader():
|
||||
"""
|
||||
Helper class to load secrets definition, generate
|
||||
the secrets a defined by the defintion, and then
|
||||
load the secrets in to keyvault
|
||||
"""
|
||||
def __init__(self, yaml_file: str, keyvault: object):
|
||||
assert Path(yaml_file).exists()
|
||||
self.yaml_file = yaml_file
|
||||
self.keyvault = keyvault
|
||||
self.config = dict()
|
||||
|
||||
self._load_yaml()
|
||||
self._generate_secrets()
|
||||
|
||||
def _load_yaml(self):
|
||||
with open(self.yaml_file) as handle:
|
||||
self.config = yaml.load(handle, Loader=yaml.FullLoader)
|
||||
|
||||
def _generate_secrets(self):
|
||||
secrets = GenerateSecrets(self.config).process_definition()
|
||||
self.secrets = secrets
|
||||
|
||||
def load_secrets(self):
|
||||
for key, val in self.secrets.items():
|
||||
print('{} {}'.format(key,val))
|
||||
self.keyvault.set_secret(key=key, value=val)
|
||||
|
||||
|
||||
class GenerateSecrets():
|
||||
"""
|
||||
Read the secrets definition and generate requiesite
|
||||
secrets based on the type of secret and arguments
|
||||
provided
|
||||
"""
|
||||
def __init__(self, definitions: dict):
|
||||
self.definitions = definitions
|
||||
|
||||
def process_definition(self):
|
||||
"""
|
||||
Processes a simple definiton such as the following
|
||||
```
|
||||
- postgres_root_user:
|
||||
type: 'username'
|
||||
length: 30
|
||||
- postgres_root_password:
|
||||
type: 'password'
|
||||
length: 30
|
||||
```
|
||||
This should be broken out to a function per definition type
|
||||
if the scope extends in to tokens, salts, or other specialized
|
||||
definitions.
|
||||
"""
|
||||
try:
|
||||
secrets = dict()
|
||||
for definition in self.definitions:
|
||||
key = list(definition)
|
||||
def_name = key[0]
|
||||
secret = definition[key[0]]
|
||||
assert len(str(secret['length'])) > 0
|
||||
method = getattr(self, '_generate_'+secret['type'])
|
||||
value = method(secret['length'])
|
||||
#print('{}: {}'.format(key[0], value))
|
||||
secrets.update({def_name: value})
|
||||
logger.debug('Setting secrets to: {}'.format(secrets))
|
||||
return secrets
|
||||
except KeyError as e:
|
||||
logger.error('Missing the {} key in the definition'.format(e))
|
||||
|
||||
# Types. Can be usernames, passwords, or in the future things like salted
|
||||
# tokens, uuid, or other specialized types
|
||||
def _generate_password(self, length: int):
|
||||
self.password_characters = string.ascii_letters + string.digits + string.punctuation
|
||||
return ''.join(secrets.choice(self.password_characters) for i in range(length))
|
||||
|
||||
def _generate_username(self, length: int):
|
||||
self.username_characters = string.ascii_letters
|
||||
return ''.join(secrets.choice(self.username_characters) for i in range(length))
|
0
terraform/secrets-tool/utils/terraform/__init__.py
Normal file
0
terraform/secrets-tool/utils/terraform/__init__.py
Normal file
61
terraform/secrets-tool/utils/terraform/wrapper.py
Normal file
61
terraform/secrets-tool/utils/terraform/wrapper.py
Normal file
@ -0,0 +1,61 @@
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TFWrapper:
|
||||
"""
|
||||
Command wrapper for terraform that injects secrets
|
||||
from keyvault in to environment variables which
|
||||
can then be used by terraform
|
||||
"""
|
||||
def __init__(self, keyvault: object):
|
||||
self.keyvault = keyvault
|
||||
self.env = ''
|
||||
self.terraform_path = 'terraform'
|
||||
|
||||
self._set_env()
|
||||
|
||||
def _set_env(self):
|
||||
# Prefix variables with TF_VAR_
|
||||
for secret in self.keyvault.list_secrets():
|
||||
name = 'TF_VAR_' + secret
|
||||
val = self.keyvault.get_secret(secret)
|
||||
os.environ[name] = val
|
||||
# Set the environment with new vars
|
||||
self.env = os.environ.copy()
|
||||
return None
|
||||
|
||||
def _run_tf(self, option: str):
|
||||
try:
|
||||
command = '{} {}'.format(self.terraform_path, option)
|
||||
with subprocess.Popen(command, env=self.env, stdout=subprocess.PIPE, shell=True) as proc:
|
||||
for line in proc.stdout:
|
||||
logging.info(line.decode("utf-8"))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def plan(self):
|
||||
"""
|
||||
terraform plan
|
||||
"""
|
||||
self._run_tf(option='plan')
|
||||
|
||||
def init(self):
|
||||
"""
|
||||
terraform init
|
||||
"""
|
||||
self._run_tf(option='init')
|
||||
|
||||
def apply(self):
|
||||
"""
|
||||
terraform apply
|
||||
"""
|
||||
self._run_tf(option='apply -auto-approve')
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
terraform destroy
|
||||
"""
|
||||
self._run_tf(option='destroy')
|
@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
from flask import url_for
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@ -11,29 +12,6 @@ from atst.utils.localization import translate
|
||||
from tests.factories import PortfolioFactory, PortfolioRoleFactory, UserFactory
|
||||
|
||||
|
||||
def test_member_table_access(client, user_session):
|
||||
admin = UserFactory.create()
|
||||
portfolio = PortfolioFactory.create(owner=admin)
|
||||
rando = UserFactory.create()
|
||||
PortfolioRoleFactory.create(
|
||||
user=rando,
|
||||
portfolio=portfolio,
|
||||
permission_sets=[PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_ADMIN)],
|
||||
)
|
||||
|
||||
url = url_for("portfolios.admin", portfolio_id=portfolio.id)
|
||||
|
||||
# editable
|
||||
user_session(admin)
|
||||
edit_resp = client.get(url)
|
||||
assert "<select" in edit_resp.data.decode()
|
||||
|
||||
# not editable
|
||||
user_session(rando)
|
||||
view_resp = client.get(url)
|
||||
assert "<select" not in view_resp.data.decode()
|
||||
|
||||
|
||||
def test_update_portfolio_name_and_description(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
user_session(portfolio.owner)
|
||||
@ -47,6 +25,7 @@ def test_update_portfolio_name_and_description(client, user_session):
|
||||
assert portfolio.description == "a portfolio for things"
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Out of scope for MVP")
|
||||
def updating_ppoc_successfully(client, old_ppoc, new_ppoc, portfolio):
|
||||
response = client.post(
|
||||
url_for("portfolios.update_ppoc", portfolio_id=portfolio.id, _external=True),
|
||||
@ -67,6 +46,7 @@ def updating_ppoc_successfully(client, old_ppoc, new_ppoc, portfolio):
|
||||
assert Permissions.EDIT_PORTFOLIO_POC not in old_ppoc.permissions
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Out of scope for MVP")
|
||||
def test_update_ppoc_no_user_id_specified(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
|
||||
@ -80,6 +60,7 @@ def test_update_ppoc_no_user_id_specified(client, user_session):
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Out of scope for MVP")
|
||||
def test_update_ppoc_to_member_not_on_portfolio(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
original_ppoc = portfolio.owner
|
||||
@ -97,6 +78,7 @@ def test_update_ppoc_to_member_not_on_portfolio(client, user_session):
|
||||
assert portfolio.owner.id == original_ppoc.id
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Out of scope for MVP")
|
||||
def test_update_ppoc_when_ppoc(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
original_ppoc = portfolio.owner_role
|
||||
@ -113,7 +95,8 @@ def test_update_ppoc_when_ppoc(client, user_session):
|
||||
)
|
||||
|
||||
|
||||
def test_update_ppoc_when_cpo(client, user_session):
|
||||
@pytest.mark.skip(reason="Out of scope for MVP")
|
||||
def test_update_ppoc_when_ccpo(client, user_session):
|
||||
ccpo = UserFactory.create_ccpo()
|
||||
portfolio = PortfolioFactory.create()
|
||||
original_ppoc = portfolio.owner_role
|
||||
@ -130,6 +113,7 @@ def test_update_ppoc_when_cpo(client, user_session):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Out of scope for MVP")
|
||||
def test_update_ppoc_when_not_ppoc(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
new_owner = UserFactory.create()
|
||||
@ -145,15 +129,6 @@ def test_update_ppoc_when_not_ppoc(client, user_session):
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_portfolio_admin_screen_when_ppoc(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
user_session(portfolio.owner)
|
||||
response = client.get(url_for("portfolios.admin", portfolio_id=portfolio.id))
|
||||
assert response.status_code == 200
|
||||
assert portfolio.name in response.data.decode()
|
||||
assert translate("fragments.ppoc.update_btn").encode("utf8") in response.data
|
||||
|
||||
|
||||
def test_portfolio_admin_screen_when_not_ppoc(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
user = UserFactory.create()
|
||||
@ -254,3 +229,54 @@ def test_remove_portfolio_member_ppoc(client, user_session):
|
||||
PortfolioRoles.get(portfolio_id=portfolio.id, user_id=portfolio.owner.id).status
|
||||
== PortfolioRoleStatus.ACTIVE
|
||||
)
|
||||
|
||||
|
||||
def test_portfolios_update_member(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
portfolio_role = PortfolioRoleFactory.create(
|
||||
portfolio=portfolio,
|
||||
permission_sets=[PermissionSets.get(PermissionSets.EDIT_PORTFOLIO_ADMIN)],
|
||||
)
|
||||
|
||||
form_data = {
|
||||
"perms_app_mgmt": "y",
|
||||
}
|
||||
|
||||
user_session(portfolio.owner)
|
||||
response = client.post(
|
||||
url_for(
|
||||
"portfolios.update_member",
|
||||
portfolio_id=portfolio.id,
|
||||
portfolio_role_id=portfolio_role.id,
|
||||
),
|
||||
data=form_data,
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert portfolio_role.has_permission_set(
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
|
||||
)
|
||||
assert not portfolio_role.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||
|
||||
|
||||
def test_can_not_update_ppoc_permissions(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
owner = portfolio.owner
|
||||
|
||||
form_data = {
|
||||
"perms_app_mgmt": "y",
|
||||
}
|
||||
|
||||
user_session(owner)
|
||||
response = client.post(
|
||||
url_for(
|
||||
"portfolios.update_member",
|
||||
portfolio_id=portfolio.id,
|
||||
portfolio_role_id=portfolio.owner_role.id,
|
||||
),
|
||||
data=form_data,
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
@ -219,11 +219,11 @@ def test_resend_invitation_sends_email(monkeypatch, client, user_session):
|
||||
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
|
||||
user = UserFactory.create()
|
||||
portfolio = PortfolioFactory.create()
|
||||
ws_role = PortfolioRoleFactory.create(
|
||||
portfolio_role = PortfolioRoleFactory.create(
|
||||
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
||||
)
|
||||
invite = PortfolioInvitationFactory.create(
|
||||
user_id=user.id, role=ws_role, status=InvitationStatus.PENDING
|
||||
user_id=user.id, role=portfolio_role, status=InvitationStatus.PENDING
|
||||
)
|
||||
user_session(portfolio.owner)
|
||||
client.post(
|
||||
@ -231,48 +231,23 @@ def test_resend_invitation_sends_email(monkeypatch, client, user_session):
|
||||
"portfolios.resend_invitation",
|
||||
portfolio_id=portfolio.id,
|
||||
portfolio_token=invite.token,
|
||||
)
|
||||
),
|
||||
data={
|
||||
"user_data-dod_id": user.dod_id,
|
||||
"user_data-first_name": user.first_name,
|
||||
"user_data-last_name": user.last_name,
|
||||
"user_data-email": user.email,
|
||||
},
|
||||
)
|
||||
|
||||
assert job_mock.called
|
||||
|
||||
|
||||
def test_existing_member_invite_resent_to_email_submitted_in_form(
|
||||
monkeypatch, client, user_session
|
||||
):
|
||||
job_mock = Mock()
|
||||
monkeypatch.setattr("atst.jobs.send_mail.delay", job_mock)
|
||||
portfolio = PortfolioFactory.create()
|
||||
user = UserFactory.create()
|
||||
ws_role = PortfolioRoleFactory.create(
|
||||
user=user, portfolio=portfolio, status=PortfolioRoleStatus.PENDING
|
||||
)
|
||||
invite = PortfolioInvitationFactory.create(
|
||||
user_id=user.id,
|
||||
role=ws_role,
|
||||
status=InvitationStatus.PENDING,
|
||||
email="example@example.com",
|
||||
)
|
||||
user_session(portfolio.owner)
|
||||
client.post(
|
||||
url_for(
|
||||
"portfolios.resend_invitation",
|
||||
portfolio_id=portfolio.id,
|
||||
portfolio_token=invite.token,
|
||||
)
|
||||
)
|
||||
|
||||
assert user.email != "example@example.com"
|
||||
ordered_args, _unordered_args = job_mock.call_args
|
||||
recipients, _subject, _message = ordered_args
|
||||
assert recipients[0] == "example@example.com"
|
||||
|
||||
|
||||
_DEFAULT_PERMS_FORM_DATA = {
|
||||
"permission_sets-perms_app_mgmt": False,
|
||||
"permission_sets-perms_funding": False,
|
||||
"permission_sets-perms_reporting": False,
|
||||
"permission_sets-perms_portfolio_mgmt": False,
|
||||
"permission_sets-perms_app_mgmt": "n",
|
||||
"permission_sets-perms_funding": "n",
|
||||
"permission_sets-perms_reporting": "n",
|
||||
"permission_sets-perms_portfolio_mgmt": "n",
|
||||
}
|
||||
|
||||
|
||||
|
@ -373,6 +373,24 @@ def test_portfolios_edit_access(post_url_assert_status):
|
||||
post_url_assert_status(rando, url, 404)
|
||||
|
||||
|
||||
# portfolios.update_member
|
||||
def test_portfolios_update_member_access(post_url_assert_status):
|
||||
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||
owner = user_with()
|
||||
rando = user_with()
|
||||
portfolio = PortfolioFactory.create(owner=owner)
|
||||
portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio)
|
||||
|
||||
url = url_for(
|
||||
"portfolios.update_member",
|
||||
portfolio_id=portfolio.id,
|
||||
portfolio_role_id=portfolio_role.id,
|
||||
)
|
||||
post_url_assert_status(ccpo, url, 302)
|
||||
post_url_assert_status(owner, url, 302)
|
||||
post_url_assert_status(rando, url, 404)
|
||||
|
||||
|
||||
# applications.new
|
||||
def test_applications_new_access(get_url_assert_status):
|
||||
ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)
|
||||
|
@ -16,15 +16,10 @@ base_public:
|
||||
login: Log in
|
||||
title_tag: JEDI Cloud
|
||||
home:
|
||||
about_cloud:
|
||||
part1: Procuring portfolio-level cloud services via a JEDI Cloud Task Order provides numerous benefits.
|
||||
part2: Portfolio purchases allow for centralized management of task orders, which reduces administrative workload on individual programs or other sub-component activities.
|
||||
add_portfolio_button_text: Add New Portfolio
|
||||
new_portfolio: New Portfolio
|
||||
get_started: Get Started
|
||||
head: JEDI Cloud Services
|
||||
your_project: Your Project
|
||||
your_project_descrip: Your portfolio is where all task orders pertaining to a specific project or set of related projects live. In JEDI, every task order in your portfolio has four components.
|
||||
funding_descrip: The Task Orders section allows you to enter, manage, and edit awarded TOs associated to a specific Portfolio.
|
||||
applications_descrip: The Applications section allows you to easily create and define new Applications within a Portfolio, as well as manage user permissions and Environments.
|
||||
reports_descrip: The Reports section allows you to view and monitor funding usage within a specific Portfolio.
|
||||
@ -37,12 +32,10 @@ ccpo:
|
||||
confirm_user_title: Confirm new CCPO user
|
||||
confirm_user_text: Please confirm that the user details below match the user being given CCPO permissions.
|
||||
confirm_button: Confirm and Add User
|
||||
return_link: Return to list of CCPO users
|
||||
user_not_found_title: User not found
|
||||
user_not_found_text: To add someone as a CCPO user, they must already have an ATAT account.
|
||||
disable_user:
|
||||
alert_message: "Confirm removing CCPO superuser access from {user_name}"
|
||||
remove_button: Remove Access
|
||||
common:
|
||||
applications: Applications
|
||||
cancel: Cancel
|
||||
@ -50,41 +43,24 @@ common:
|
||||
confirm: Confirm
|
||||
continue: Continue
|
||||
delete: Delete
|
||||
deactivate: Deactivate
|
||||
delete_confirm: 'Please type the word {word} to confirm:'
|
||||
dod_id: DoD ID
|
||||
disable: Disable
|
||||
edit: Edit
|
||||
email: Email
|
||||
lorem: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
members: Members
|
||||
name: Name
|
||||
next: Next
|
||||
'yes': 'Yes'
|
||||
'no': 'No'
|
||||
previous: Previous
|
||||
response_label: Response required
|
||||
save: Save
|
||||
save_changes: Save Changes
|
||||
task_orders: Task Orders
|
||||
undo: Undo
|
||||
view: View
|
||||
resource_names:
|
||||
environments: Environments
|
||||
choose_role: Choose a role
|
||||
components:
|
||||
date_selector:
|
||||
day: Day
|
||||
month: Month
|
||||
year: Year
|
||||
modal:
|
||||
destructive_message: You will no longer be able to access this {resource}
|
||||
destructive_title: Warning! This action is permanent
|
||||
totals_box:
|
||||
obligated_funds: Total Obligated Funds
|
||||
obligated_text: This amount strictly calculates Base CLINs, and may represent 100% of your total task order budget, or just a portion if you also have Optional Base or Optional CLINs.
|
||||
total_amount: Total Possible Task Order Funds
|
||||
total_text: This amount represents the total value of all Base and Option CLINs, including any extensions listed within your task order.
|
||||
usa_header:
|
||||
flag_image_alt: U.S. Flag
|
||||
official_message: An official website of the United States government
|
||||
@ -113,7 +89,6 @@ flash:
|
||||
title: Application Saved
|
||||
message: '{application_name} has been successfully created. You may continue on to provision environments and assign team members now, or come back and complete these tasks at a later time.'
|
||||
updated: 'You have successfully updated the {application_name} application.'
|
||||
deleted: 'You have successfully deleted the {application_name} application. To view the retained activity log, visit the portfolio administration page.'
|
||||
name_error:
|
||||
message: 'The application name {name} has already been used in this portfolio. Please enter a unique name.'
|
||||
env_name_error:
|
||||
@ -169,10 +144,19 @@ flash:
|
||||
revoked:
|
||||
title: Removed portfolio access
|
||||
message: Portfolio access successfully removed from {member_name}.
|
||||
update:
|
||||
title: Success!
|
||||
message: You have successfully updated access permissions for {member_name}.
|
||||
update_error:
|
||||
title: Permissions for {member_name} could not be updated
|
||||
message: An unexpected problem occurred with your request, please try again. If the problem persists, contact an administrator.
|
||||
portfolio_invite:
|
||||
resent:
|
||||
title: Invitation resent
|
||||
message: Successfully sent a new invitation to {user_name}.
|
||||
error:
|
||||
title: Portfolio invitation error
|
||||
message: There was an error processing the invitation for {user_name}.
|
||||
session_expired:
|
||||
title: Session Expired
|
||||
message: Your session expired due to inactivity. Please log in again to continue.
|
||||
@ -270,7 +254,6 @@ forms:
|
||||
</p>
|
||||
</div>
|
||||
defense_component:
|
||||
label: "Select DoD component(s) funding your Portfolio:"
|
||||
choices:
|
||||
air_force: Air Force
|
||||
army: Army
|
||||
@ -293,8 +276,6 @@ forms:
|
||||
upload_error: There was an error uploading your file. Please try again. If you encounter repeated problems uploading this file, please contact CCPO.
|
||||
size_error: The file you have selected is too large. Please choose a file no larger than 64MB.
|
||||
filename_error: File names can only contain the characters A-Z, 0-9, space, hyphen, underscore, and period.
|
||||
defense_component_label: Select DoD component(s) funding your Portfolio
|
||||
file_format_not_allowed: Only PDF or PNG files can be uploaded.
|
||||
number_description: Task order number (13 digits)
|
||||
pop_errors:
|
||||
date_order: PoP start date must be before end date.
|
||||
@ -306,35 +287,15 @@ forms:
|
||||
clin_funding_errors:
|
||||
obligated_amount_error: Obligated amount must be less than or equal to total amount
|
||||
funding_range_error: Dollar amount must be from $0.00 to $1,000,000,000.00
|
||||
team_experience:
|
||||
built_1: Built, migrated, or consulted on 1-2 applications
|
||||
built_3: Built, migrated, or consulted on 3-5 applications
|
||||
built_many: Built, migrated, or consulted on more than 5 applications
|
||||
description: How much combined experience does your team have with development in the cloud?
|
||||
label: Team experience
|
||||
none: No previous experience
|
||||
planned: Researched or planned a cloud build or migration
|
||||
validators:
|
||||
is_number_message: Please enter a valid number.
|
||||
is_required: This field is required.
|
||||
list_item_required_message: Please provide at least one.
|
||||
list_items_unique_message: Items must be unique
|
||||
name_message: 'This field accepts letters, numbers, commas, apostrophes, hyphens, and periods.'
|
||||
phone_number_message: Please enter a valid 5 or 10 digit phone number.
|
||||
file_length: Your file may not exceed 50 MB.
|
||||
fragments:
|
||||
delete_portfolio:
|
||||
title: Deactivate Portfolio
|
||||
subtitle: Portfolio deactivation is available only if there are no applications in the portfolio.
|
||||
edit_application_form:
|
||||
explain: AT-AT allows you to create multiple applications within a portfolio. Each application can then be broken down into its own customizable environments.
|
||||
edit_environment_team_form:
|
||||
delete_environment_title: Are you sure you want to delete this environment?
|
||||
add_new_member_text: Need to add someone new to the team?
|
||||
add_new_member_link: Jump to Team Settings
|
||||
unassigned_title: Unassigned (No Access)
|
||||
no_members: Currently no members are in this role
|
||||
no_access: No Access
|
||||
edit_user_form:
|
||||
save_details_button: Save
|
||||
portfolio_admin:
|
||||
@ -376,16 +337,10 @@ navigation:
|
||||
portfolios:
|
||||
admin:
|
||||
activity_log_title: Activity log
|
||||
add_new_member: Add a new member
|
||||
alert_header: Are you sure you want to delete this member?
|
||||
alert_message: 'The member will be removed from the portfolio, but their log history will be retained.'
|
||||
alert_title: Warning! You are about to delete a member from the portfolio.
|
||||
defense_component_label: Department of Defense Component
|
||||
no_members: There are currently no members in this portfolio.
|
||||
permissions_info: Learn more about these permissions
|
||||
portfolio_members_subheading: These members have different levels of access to the portfolio.
|
||||
portfolio_members_title: Portfolio members
|
||||
settings_info: Learn more about these settings
|
||||
portfolio_name: Portfolio name
|
||||
members:
|
||||
perms_portfolio_mgmt:
|
||||
@ -401,11 +356,9 @@ portfolios:
|
||||
'False': View Reporting
|
||||
'True': Edit Reporting
|
||||
applications:
|
||||
add_application_text: Add a new application
|
||||
add_environment: Create an Environment
|
||||
add_member: Add New Member
|
||||
add_another_environment: Add another environment
|
||||
app_settings_text: App settings
|
||||
create_button: Create Application
|
||||
empty_state:
|
||||
header: You don't have any Applications yet
|
||||
@ -445,49 +398,18 @@ portfolios:
|
||||
step_2_button_text: "Next: Add Members"
|
||||
step_3_header: Add Members to {application_name}
|
||||
step_3_description: "To proceed, you will need each member's email address and DOD ID. Within this section, you will also assign Application-level permissions and environment-level roles for each member."
|
||||
step_3_button_text: Save Application
|
||||
create_new_env: Create a new environment.
|
||||
create_new_env_info: Creating an environment gives you access to the Cloud Service Provider. This environment will function within the constraints of the task order, and any costs will be billed against the portfolio.
|
||||
csp_console_text: CSP console
|
||||
csp_link: Cloud Service Provider Link
|
||||
remove_member:
|
||||
alert:
|
||||
message: '{user_name} will no longer be able to access this application or any of its environments'
|
||||
button: Remove member
|
||||
header: Are you sure you want to remove this team member?
|
||||
delete:
|
||||
alert:
|
||||
message: You will lose access to this application and all environments will be removed from the CSP. Your reporting and activity will still be accessible.
|
||||
button: Delete application
|
||||
header: Are you sure you want to delete this application?
|
||||
text: "If you delete {application_name}, all environments will be gone forever.<br>This action can not be undone."
|
||||
subheading: Delete Application
|
||||
enter_env_name: "Enter environment name:"
|
||||
environments:
|
||||
name: Name
|
||||
delete:
|
||||
button: Delete environment
|
||||
edit_name: Edit name
|
||||
environments_heading: Application Environments
|
||||
existing_application_title: '{application_name} Application Settings'
|
||||
member_count: '{count} members'
|
||||
new_application_title: New Application
|
||||
settings_heading: Application Settings
|
||||
settings:
|
||||
name_description: Application name and description
|
||||
team_members: Application Team
|
||||
environments: Application Environments
|
||||
team_settings:
|
||||
environments: Environments
|
||||
section:
|
||||
table:
|
||||
delete_access: Delete Access
|
||||
environment_management: Environment Management
|
||||
team_management: Team Management
|
||||
title: '{application_name} Team'
|
||||
title: '{application_name} Team Settings'
|
||||
add_to_environment: Add to existing environment
|
||||
team_text: Team
|
||||
members:
|
||||
blank_slate: This Application has no members
|
||||
form:
|
||||
@ -512,19 +434,11 @@ portfolios:
|
||||
title: Application Permissions
|
||||
description: Application permissions allow users to provision and modify applications and teams. <a href="#"> Learn More </a>
|
||||
edit_access_header: "Manage {user}'s Access"
|
||||
env_access_text: "Add or revoke Environment access. Additional controls are available in the CSP console."
|
||||
step_2_title: Set Permissions and Roles
|
||||
add_member: Add Member
|
||||
menu:
|
||||
edit: Edit Roles and Permissions
|
||||
resend: Resend Invite
|
||||
new:
|
||||
assign_roles: Assign Member Environments and Roles
|
||||
learn_more: Learn more about these roles
|
||||
manage_perms: 'Manage permissions for {application_name}'
|
||||
manage_envs: 'Allow member to <strong>add</strong> and <strong>rename environments</strong> within the application.'
|
||||
delete_envs: 'Allow member to <strong>delete environments</strong> within the application.'
|
||||
manage_team: 'Allow member to <strong>add, update,</strong> and <strong>remove members</strong> from the application team.'
|
||||
verify: Verify Member Information
|
||||
perms_team_mgmt:
|
||||
'False': View Team
|
||||
@ -535,20 +449,9 @@ portfolios:
|
||||
perms_del_env:
|
||||
'False': ""
|
||||
'True': Delete Application
|
||||
index:
|
||||
empty:
|
||||
start_button: Start a new JEDI portfolio
|
||||
title: You have no apps yet
|
||||
header: PORTFOLIO
|
||||
members:
|
||||
archive_button: Delete member
|
||||
permissions:
|
||||
app_mgmt: App management
|
||||
edit_access: Edit
|
||||
funding: Funding
|
||||
name: Name
|
||||
portfolio_mgmt: Portfolio management
|
||||
reporting: Reporting
|
||||
reports:
|
||||
estimate_warning: Reports displayed in JEDI are estimates and not a system of record.
|
||||
empty_state:
|
||||
@ -557,17 +460,9 @@ portfolios:
|
||||
can_create_applications: This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.
|
||||
cannot_create_applications: This portfolio has no cloud environments set up, so there is no spending data to report. Contact the portfolio owner to set up some cloud environments.
|
||||
action_label: 'Add a new application'
|
||||
|
||||
task_orders:
|
||||
add_new_button: Add New Task Order
|
||||
review:
|
||||
pdf_title: Approved Task Order
|
||||
review_your_funding: Review your funding
|
||||
funding_summary: CLIN Summary
|
||||
task_order_number: Task Order Number
|
||||
check_paragraph: Check to make sure the information you entered is correct. After submission, you will confirm this task order was signed by a contracting officer. Thereafter, you will be informed as soon as CCPO completes their review.
|
||||
supporting_document:
|
||||
title: Supporting document
|
||||
clins:
|
||||
number: TO CLIN
|
||||
type: CLIN Type
|
||||
@ -588,7 +483,6 @@ task_orders:
|
||||
add_clin: Add another CLIN
|
||||
add_to_header: Add your task order
|
||||
add_to_description: Now, refer to your document to find the 13-digit task order number. It should be located at lorem ipsum dolar. From now on we'll refer to this portion of funding by the recorded task order number.
|
||||
base_clin_title: CLIN
|
||||
clin_title: Enter Contract Line Items
|
||||
clin_description: "Refer to your task order to locate your Contract Line Item Numbers (CLINs)."
|
||||
clin_details: CLIN Details
|
||||
@ -598,8 +492,6 @@ task_orders:
|
||||
clin_remove_text: 'Do you want to remove '
|
||||
clin_remove_confirm: Yes, remove CLIN
|
||||
clin_remove_cancel: No, go back
|
||||
cloud_funding_header: Add the summary of cloud funding
|
||||
cloud_funding_text: Data must match with what is in your uploaded document.
|
||||
draft_alert_title: Your information has been saved
|
||||
draft_alert_message: You can return to the Task Order Builder to enter missing information. Once you are finished, you’ll be ready to submit this request.
|
||||
total_funds_label: Total CLIN Value
|
||||
@ -609,16 +501,13 @@ task_orders:
|
||||
pop_end_alert: "A CLIN's period of performance must end before {end_date}."
|
||||
pop_example: "For example: 07 04 1776"
|
||||
pop_start: Start Date
|
||||
review_button: Review task order
|
||||
supporting_docs_header: Upload your supporting documentation
|
||||
supporting_docs_size_limit: Your file may not exceed 64MB
|
||||
supporting_docs_text: Upload a single PDF containing all relevant information.
|
||||
step_3:
|
||||
next_button: 'Next: Review Task Order'
|
||||
step_5:
|
||||
title: Confirm Signature
|
||||
description: Prior to submitting the Task Order, you must acknowledge, by marking the appropriate box below, that the uploaded Task Order is signed by an appropriate, duly warranted Contracting Officer who has the authority to execute the uploaded Task Order on your Agency’s behalf and has authorized you to upload the Task Order in accordance with Agency policy and procedures. You must further acknowledge, by marking the appropriate box below, that all information entered herein matches that of the submitted Task Order.
|
||||
alert_message: All task orders require a Contracting Officer signature.
|
||||
next_button: 'Confirm & Submit'
|
||||
sticky_header_text: 'Add a Task Order'
|
||||
sticky_header_review_text: Review Changes
|
||||
@ -628,12 +517,6 @@ task_orders:
|
||||
message: Upload your approved Task Order here. You are required to confirm you have the appropriate signature. You will have the ability to add additional approved Task Orders with more funding to this Portfolio in the future.
|
||||
button_text: Add Task Order
|
||||
view_only_text: Contact your portfolio administrator to add a Task Order.
|
||||
new:
|
||||
form_help_text: Before you can begin work in the cloud, you'll need to complete the information below and upload your approved task order for reference by the CCPO.
|
||||
app_info:
|
||||
project_title: Project details
|
||||
subtitle: Who will be involved in the work funded by this task order?
|
||||
team_title: Your team
|
||||
sign:
|
||||
digital_signature_description: I confirm the uploaded Task Order is signed by the appropriate, duly warranted Agency Contracting Officer who authorized me to upload the Task Order.
|
||||
confirmation_description: I confirm that the information entered here in matches that of the submitted Task Order.
|
||||
|
Loading…
x
Reference in New Issue
Block a user