# -*- coding: utf-8 -*-
# autosetup.py - Waqas Bhatti (wbhatti@astro.princeton.edu) - Aug 2018
# License: MIT - see the LICENSE file for the full text.
"""This contains functions to set up the authnzerver automatically on
first-start.
"""
#############
## LOGGING ##
#############
import logging
from typing import Optional
# get a logger
LOGGER = logging.getLogger(__name__)
#############
## IMPORTS ##
#############
import os
import os.path
import shutil
from .modtools import object_from_string
# this is the module path
modpath = os.path.abspath(os.path.dirname(__file__))
[docs]def autogen_secrets_authdb(
basedir: str,
database_url: str = None,
interactive: bool = False,
generate_envfile: bool = True,
):
"""This automatically generates secrets files and an authentication DB.
Run this only once on the first start of an authnzerver.
Parameters
----------
basedir : str
The base directory of the authnzerver.
- The authentication database will be written to a file called
``.authdb.sqlite`` in this directory.
- The secret token to authenticate HTTP communications between the
authnzerver and a frontend server will be written to a file called
``.authnzerver-secret-key`` in this directory.
- Credentials for a superuser that can be used to edit various
authnzerver options, and users will be written to
``.authnzerver-admin-credentials`` in this directory.
- A random salt value will be written to ``.authnzerver-random-salt``
in this directory. This is used to hash user IDs and other PII in
logs.
database_url : str or None
If this is a str, must be a valid SQLAlchemy database URL to use to
connect to a database and make the necessary tables for authentication
info. If this is None, will create a new SQLite database in the
``<basedir>/.authdb.sqlite`` file.
interactive : bool
If True, will ask the user for an admin email address and
password. Otherwise, will auto-generate both.
generate_envfile : bool
If True, generates an .env file in the basedir containing all the
required information for the next start up of the server.
Returns
-------
(authdb_path, creds, secret_file, salt_file, env_file) : tuple of str
The names of the files written by this function will be returned as a
tuple of strings.
"""
if not os.path.exists(basedir):
os.makedirs(os.path.abspath(basedir))
import getpass
from .authdb import (
create_sqlite_authdb,
initial_authdb_inserts,
create_authdb,
)
from cryptography.fernet import Fernet
#
# get the default permissions JSON
#
mod_dir = os.path.dirname(__file__)
permissions_json = os.path.abspath(
os.path.join(mod_dir, "default-permissions-model.json")
)
if interactive:
#
# get the auth DB URL
#
LOGGER.warning(
"Enter a valid SQLAlchemy database URL to use for the auth DB."
)
print("If you leave this blank and hit Enter, an SQLite auth DB")
print("will be created in the base directory: %s" % basedir)
input_db_url = input("Auth DB URL [default: auto generated]: ")
if not input_db_url or len(input_db_url) == 0:
database_url = None
LOGGER.warning(
"Enter the path to the permissions policy JSON file to use."
)
print("If you leave this blank and hit Enter, the default permissions")
print(
"policy JSON shipped with authnzerver will be used: %s"
% permissions_json
)
input_permissions_json = input(
"Permission JSON path [default: included permissions JSON]: "
)
if input_permissions_json and len(input_permissions_json) > 0:
permissions_json = input_permissions_json
# if no database_url is specified, create our auth DB in the basedir
if database_url is None:
authdb_path = os.path.join(basedir, ".authdb.sqlite")
if not os.path.exists(authdb_path):
LOGGER.warning(
"No existing authentication DB was found, "
"making a new SQLite DB in authnzerver basedir: %s"
% authdb_path
)
# generate the initial DB
create_sqlite_authdb(authdb_path, echo=False, returnconn=False)
database_url = "sqlite:///%s" % authdb_path
# otherwise, if there's an SQLite DB URL provided,
# create it at the specified path
elif database_url is not None and "sqlite:///" in database_url:
authdb_path = os.path.abspath(database_url.replace("sqlite:///", ""))
if not os.path.exists(authdb_path):
create_sqlite_authdb(authdb_path, echo=False, returnconn=False)
# otherwise, use normal auth DB creation
else:
authdb_path = None
create_authdb(database_url, echo=False, returnconn=False)
# ask the user for their email address and password the default
# email address will be used for the superuser if the email address
# is None, we'll use the user's UNIX ID@localhost if the password is
# None, a random one will be generated
try:
userid = "%s@localhost" % getpass.getuser()
except Exception:
userid = "serveradmin@localhost"
if interactive:
inp_userid = input("\nAdmin email address [default: %s]: " % userid)
if inp_userid and len(inp_userid.strip()) > 0:
userid = inp_userid
inp_pass = getpass.getpass(
"Admin password [default: randomly generated]: "
)
if inp_pass and len(inp_pass.strip()) > 0:
password = inp_pass
else:
password = None
else:
password = None
u, p = None, None
try:
# generate the admin users and initial DB info
u, p = initial_authdb_inserts(
database_url,
permissions_json=permissions_json,
superuser_email=userid,
superuser_pass=password,
)
if u is None:
LOGGER.error("Could not do initial inserts into the auth DB.")
return None, None, None
except Exception:
LOGGER.warning(
"Auth DB is already set up at the provided database URL. "
"Not overwriting..."
)
creds = os.path.join(basedir, ".authnzerver-admin-credentials")
if os.path.exists(creds):
LOGGER.warning(
"Admin credentials file already exists. " "Not overwriting..."
)
elif u and p:
with open(creds, "w") as outfd:
outfd.write("%s %s\n" % (u, p))
os.chmod(creds, 0o100400)
if p:
LOGGER.warning(
"Generated random admin password, "
"credentials written to: %s\n" % creds
)
# we'll generate the server secrets now so we don't have to deal
# with them later
LOGGER.info("Generating server secret tokens...")
fernet_secret = Fernet.generate_key()
fernet_secret_file = os.path.join(basedir, ".authnzerver-secret-key")
if os.path.exists(fernet_secret_file):
LOGGER.warning(
"Authnzerver communication secrets file already "
"exists. Not overwriting..."
)
else:
with open(fernet_secret_file, "wb") as outfd:
outfd.write(fernet_secret)
os.chmod(fernet_secret_file, 0o100400)
# finally, we'll generate the server PII random salt
LOGGER.info("Generating server PII random salt...")
salt = Fernet.generate_key()
salt_file = os.path.join(basedir, ".authnzerver-salt")
if os.path.exists(salt_file):
LOGGER.warning(
"Authnzerver salt file already " "exists. Not overwriting..."
)
else:
with open(salt_file, "wb") as outfd:
outfd.write(salt)
os.chmod(salt_file, 0o100400)
# copy over the permission model and confvars
LOGGER.info(
"Copying default-permissions-model.json to basedir: %s" % basedir
)
shutil.copy(
os.path.join(modpath, "default-permissions-model.json"), basedir
)
LOGGER.info("Copying confvars.py to basedir: %s" % basedir)
shutil.copy(os.path.join(modpath, "confvars.py"), basedir)
# generate the env file if asked for
if generate_envfile:
LOGGER.info(
"Generating an envfile: %s" % os.path.join(basedir, ".env")
)
envfile = generate_env(
database_url if database_url is not None else authdb_path,
fernet_secret_file,
salt_file,
basedir,
)
else:
envfile = generate_env(
database_url,
fernet_secret_file,
salt_file,
basedir,
)
#
# return everything
#
if database_url is not None:
return database_url, creds, fernet_secret_file, salt_file, envfile
else:
return authdb_path, creds, fernet_secret_file, salt_file, envfile
[docs]def generate_env(
database_path: str,
fernet_secret_file: str,
salt_file: str,
basedir: str,
) -> Optional[str]:
"""This generates environment variables containing the required items for
authnzrv start up after autosetup is complete.
If ``write_env_file`` is True, will write these to an ``.env`` file in the
``basedir``.
Parameters
----------
database_path : str
The SQLAlchemy URL of the database to use, or the path on disk to an
SQLite database. If ``database_path`` points to a file on disk, this
function will assume it's an SQLite file and construct the appropriate
SQLAlchemy database URL.
fernet_secret_file : str
The path to the shared secret key needed to secure authnzerver-frontend
communications.
salt_file : str
The path to the file containing the PII salt to encrypt PII in
authnzerver logs.
basedir : str
The path to the authnzerver's basedir.
Returns
-------
environ_file
Returns the path to the ``.env`` file generated in the ``basedir`` as a
string.
"""
# first, figure out the database URL
if os.path.exists(database_path):
database_url = "sqlite:///%s" % os.path.abspath(database_path)
elif "://" in database_path:
database_url = database_path
else:
LOGGER.error("Could not understand the database_path provided.")
return None
# get the confvar.py file and generate the env variables in it
confvars = object_from_string(
"%s::CONF" % os.path.join(basedir, "confvars.py")
)
env_file = os.path.abspath(os.path.join(basedir, ".env"))
with open(env_file, "w") as outfd:
for key, val in confvars.items():
if key == "authdb":
env_key, env_val = val["env"], database_url
elif key == "secret":
env_key, env_val = (
val["env"],
os.path.abspath(fernet_secret_file),
)
elif key == "piisalt":
env_key, env_val = (val["env"], os.path.abspath(salt_file))
elif key == "permissions":
env_key, env_val = (
val["env"],
os.path.abspath(
os.path.join(basedir, "default-permissions-model.json")
),
)
else:
env_key, env_val = val["env"], val["default"]
# handle multiple env keys by assigning them all to the value
if isinstance(env_key, (list, tuple)):
for key_item in env_key:
outfd.write("%s=%s\n" % (key_item, env_val))
else:
outfd.write("%s=%s\n" % (env_key, env_val))
return env_file