Merge branch 'staging' into disable-pod-escalation

This commit is contained in:
dandds 2020-01-28 16:48:45 -05:00 committed by GitHub
commit 81a41a632a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 358 additions and 34 deletions

View File

@ -21,11 +21,8 @@ LICENSE
# Skip envrc # Skip envrc
.envrc .envrc
# Skip ansible-container stuff # Skip terraform
ansible* terraform
container.yml
meta.yml
requirements.yml
# Skip kubernetes and Docker config stuff # Skip kubernetes and Docker config stuff
deploy deploy

View File

@ -93,10 +93,13 @@ class Users(object):
return user return user
@classmethod @classmethod
def give_ccpo_perms(cls, user): def give_ccpo_perms(cls, user, commit=True):
user.permission_sets = PermissionSets.get_all() user.permission_sets = PermissionSets.get_all()
db.session.add(user) db.session.add(user)
if commit:
db.session.commit() db.session.commit()
return user return user
@classmethod @classmethod

41
script/create_database.py Normal file
View File

@ -0,0 +1,41 @@
# Add root application dir to the python path
import os
import sys
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(parent_dir)
import sqlalchemy
from atst.app import make_config
def _root_connection(config, root_db):
# Assemble DATABASE_URI value
database_uri = "postgresql://{}:{}@{}:{}/{}".format( # pragma: allowlist secret
config.get("PGUSER"),
config.get("PGPASSWORD"),
config.get("PGHOST"),
config.get("PGPORT"),
root_db,
)
engine = sqlalchemy.create_engine(database_uri)
return engine.connect()
def create_database(conn, dbname):
conn.execute("commit")
conn.execute(f"CREATE DATABASE {dbname};")
conn.close()
return True
if __name__ == "__main__":
dbname = sys.argv[1]
config = make_config()
conn = _root_connection(config, "postgres")
print(f"Creating database {dbname}")
create_database(conn, dbname)

76
script/database_setup.py Normal file
View File

@ -0,0 +1,76 @@
# Add root application dir to the python path
import os
import sys
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(parent_dir)
import sqlalchemy
import yaml
from atst.app import make_config, make_app
from atst.database import db
from atst.domain.users import Users
from atst.models import User
from reset_database import reset_database
def database_setup(username, password, dbname, ccpo_users):
print(
f"Creating Postgres user role for '{username}' and granting all privileges to database '{dbname}'."
)
try:
_create_database_user(username, password, dbname)
except sqlalchemy.exc.ProgrammingError as err:
print(f"Postgres user role '{username}' already exists.")
print("Applying schema and seeding roles and permissions.")
reset_database()
print("Creating initial set of CCPO users.")
_add_ccpo_users(ccpo_users)
def _create_database_user(username, password, dbname):
conn = db.engine.connect()
meta = sqlalchemy.MetaData(bind=conn)
meta.reflect()
trans = conn.begin()
engine = trans.connection.engine
engine.execute(
f"CREATE ROLE {username} WITH LOGIN NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION PASSWORD '{password}';\n"
f"GRANT ALL PRIVILEGES ON DATABASE {dbname} TO {username};\n"
f"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO {username}; \n"
f"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO {username}; \n"
f"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO {username}; \n"
)
trans.commit()
def _add_ccpo_users(ccpo_users):
for user_data in ccpo_users:
user = User(**user_data)
Users.give_ccpo_perms(user, commit=False)
db.session.add(user)
db.session.commit()
def _load_yaml(file_):
with open(file_) as f:
return yaml.safe_load(f)
if __name__ == "__main__":
config = make_config({"DISABLE_CRL_CHECK": True, "DEBUG": False})
app = make_app(config)
with app.app_context():
dbname = config.get("PGDATABASE", "atat")
username = sys.argv[1]
password = sys.argv[2]
ccpo_user_file = sys.argv[3]
ccpo_users = _load_yaml(ccpo_user_file)
database_setup(username, password, dbname, ccpo_users)

View File

@ -16,7 +16,9 @@ from atst.app import make_config, make_app
def reset_database(): def reset_database():
conn = db.engine.connect() conn = db.engine.connect()
meta = sqlalchemy.MetaData(bind=conn, reflect=True) meta = sqlalchemy.MetaData(bind=conn)
meta.reflect()
trans = conn.begin() trans = conn.begin()
# drop all tables # drop all tables

View File

@ -282,3 +282,24 @@ secrets-tool secrets --keyvault https://ops-jedidev-keyvault.vault.azure.net/ cr
`terraform apply` `terraform apply`
*[Configure AD for MFA](https://docs.microsoft.com/en-us/azure/vpn-gateway/openvpn-azure-ad-mfa)* *[Configure AD for MFA](https://docs.microsoft.com/en-us/azure/vpn-gateway/openvpn-azure-ad-mfa)*
*Then we need an instance of the container*
Change directories to the repo root. Ensure that you've checked out the staging or master branch:
`docker build . --build-arg CSP=azure -f ./Dockerfile -t atat:latest`
*Create secrets for ATAT database user*
Change directories back to terraform/secrets-tool. There is a sample file there. Make sure you know the URL for the aplication Key Vault (distinct from the operator Key Vault). Run:
`secrets-tool secrets --keyvault [application key vault URL] load -f ./postgres-user.yaml
*Create the database, database user, schema, and initial data set*
This is discussed in more detail [here](https://github.com/dod-ccpo/atst/tree/staging/terraform/secrets-tool#setting-up-the-initial-atat-database). Be sure to read the requirements section.
```
secrets-tool database --keyvault [operator key vault URL] provision --app-keyvault [application key vault URL] --dbname jedidev-atat --dbhost [database host name] --ccpo-users /full/path/to/users.yml
```

View File

@ -35,11 +35,3 @@ resource "azurerm_postgresql_virtual_network_rule" "sql" {
subnet_id = var.subnet_id subnet_id = var.subnet_id
ignore_missing_vnet_service_endpoint = true ignore_missing_vnet_service_endpoint = true
} }
resource "azurerm_postgresql_database" "db" {
name = "${var.name}-${var.environment}-atat"
resource_group_name = azurerm_resource_group.sql.name
server_name = azurerm_postgresql_server.sql.name
charset = "UTF8"
collation = "en-US"
}

View File

@ -1,3 +0,0 @@
output "db_name" {
value = azurerm_postgresql_database.db.name
}

View File

@ -30,6 +30,51 @@ Terraform typically expects user defined secrets to be stored in either a file,
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) 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)
## Setting up the initial ATAT database
This handles bootstrapping the ATAT database with a user, schema, and initial data.
It does the following:
- Sources the Postgres root user credentials
- Source the Postgres ATAT user password
- Runs a script inside an ATAT docker container to set up the initial database user, schema, and seed data in the database
Requirements:
- docker
- A copy of the ATAT docker image. This can be built in the repo root with: `docker build . --build-arg CSP=azure -f ./Dockerfile -t atat:latest`
- You need to know the hostname for the Postgres database. Your IP must either be whitelisted in its firewall rules or you must be behind the VPN.
- You will need a YAML file listing all the CCPO users to be added to the database, with the format:
```
- dod_id: "2323232323"
first_name: "Luke"
last_name: "Skywalker"
- dod_id: "5656565656"
first_name: "Han"
last_name: "Solo"
```
- There should be a password for the ATAT database user in the application Key Vault, preferably named `PGPASSWORD`. You can load this by running `secrets-tool --keyvault [operator key vault url] load -f postgres-user.yml` and supplying YAML like:
```
---
- PGPASSWORD:
type: 'password'
length: 30
```
This command takes a lot of arguments. Run `secrets-tool database --keyvault [operator key vault url] provision -- help` to see the full list of available options.
The command supplies some defaults by assuming you've followed the patterns in sample-secrets.yml and elsewhere.
An example would be:
```
secrets-tool database --keyvault [operator key vault URL] provision --app-keyvault [application key vault URL] --dbname jedidev-atat --dbhost [database host name] --ccpo-users /full/path/to/users.yml
```
# Setup # Setup
*Requirements* *Requirements*

View File

@ -0,0 +1,143 @@
import os
import click
import logging
import subprocess
from utils.keyvault.secrets import SecretsClient
logger = logging.getLogger(__name__)
def _run_cmd(command):
try:
env = os.environ.copy()
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)
@click.group()
@click.option("--keyvault", required=True, help="Specify the keyvault to operate on")
@click.pass_context
def database(ctx, keyvault):
ctx.ensure_object(dict)
ctx.obj["keyvault"] = keyvault
# root password, root username
@click.command("provision")
@click.option(
"--app-keyvault",
"app_keyvault",
required=True,
help="The username for the new Postgres user.",
)
@click.option(
"--user-username",
"user_username",
default="atat",
required=True,
help="The username for the new Postgres user.",
)
@click.option(
"--user-password-key",
"user_password_key",
default="PGPASSWORD",
required=True,
help="The name of the user's password key in the specified vault.",
)
@click.option(
"--root-username-key",
"root_username_key",
default="postgres-root-user",
required=True,
help="The name of the user's password key in the specified vault.",
)
@click.option(
"--root-password-key",
"root_password_key",
default="postgres-root-password",
required=True,
help="The name of the user's password key in the specified vault.",
)
@click.option(
"--dbname",
"dbname",
required=True,
help="The name of the database the user will be given full access to.",
)
@click.option(
"--dbhost",
"dbhost",
required=True,
help="The name of the database the user will be given full access to.",
)
@click.option(
"--container",
"container",
default="atat:latest",
required=True,
help="The container to run the provisioning command in.",
)
@click.option(
"--ccpo-users",
"ccpo_users",
required=True,
help="The full path to a YAML file listing CCPO users to be seeded to the database.",
)
@click.pass_context
def provision(
ctx,
app_keyvault,
user_username,
user_password_key,
root_username_key,
root_password_key,
dbname,
dbhost,
container,
ccpo_users,
):
"""
Set up the initial ATAT database.
"""
logger.info("obtaining postgres root user credentials")
operator_keyvault = SecretsClient(vault_url=ctx.obj["keyvault"])
root_password = operator_keyvault.get_secret(root_password_key)
root_name = operator_keyvault.get_secret(root_username_key)
logger.info("obtaining postgres database user password")
app_keyvault = SecretsClient(vault_url=app_keyvault)
user_password = app_keyvault.get_secret(user_password_key)
logger.info("starting docker process")
create_database_cmd = (
f"docker run -e PGHOST='{dbhost}'"
f" -e PGPASSWORD='{root_password}'"
f" -e PGUSER='{root_name}@{dbhost}'"
f" -e PGDATABASE='{dbname}'"
f" -e PGSSLMODE=require"
f" {container}"
f" .venv/bin/python script/create_database.py {dbname}"
)
_run_cmd(create_database_cmd)
seed_database_cmd = (
f"docker run -e PGHOST='{dbhost}'"
f" -e PGPASSWORD='{root_password}'"
f" -e PGUSER='{root_name}@{dbhost}'"
f" -e PGDATABASE='{dbname}'"
f" -e PGSSLMODE=require"
f" -v {ccpo_users}:/opt/atat/atst/users.yml"
f" {container}"
f" .venv/bin/python script/database_setup.py {user_username} '{user_password}' users.yml"
)
_run_cmd(seed_database_cmd)
database.add_command(provision)

View File

@ -0,0 +1,4 @@
---
- PGPASSWORD:
type: 'password'
length: 30

View File

@ -7,6 +7,7 @@ import logging
from commands.secrets import secrets from commands.secrets import secrets
from commands.terraform import terraform from commands.terraform import terraform
from commands.database import database
config.setup_logging() config.setup_logging()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,6 +22,7 @@ def cli():
# Add additional command groups # Add additional command groups
cli.add_command(secrets) cli.add_command(secrets)
cli.add_command(terraform) cli.add_command(terraform)
cli.add_command(database)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -66,6 +66,8 @@ class GenerateSecrets():
""" """
def __init__(self, definitions: dict): def __init__(self, definitions: dict):
self.definitions = definitions self.definitions = definitions
most_punctuation = string.punctuation.replace("'", "").replace('"', "")
self.password_characters = string.ascii_letters + string.digits + most_punctuation
def process_definition(self): def process_definition(self):
""" """
@ -101,7 +103,6 @@ class GenerateSecrets():
# Types. Can be usernames, passwords, or in the future things like salted # Types. Can be usernames, passwords, or in the future things like salted
# tokens, uuid, or other specialized types # tokens, uuid, or other specialized types
def _generate_password(self, length: int): 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)) return ''.join(secrets.choice(self.password_characters) for i in range(length))
def _generate_username(self, length: int): def _generate_username(self, length: int):