diff --git a/.isort.cfg b/.isort.cfg index 4321048..6b946c6 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -7,6 +7,6 @@ import_heading_thirdparty=Third-Party Libraries import_heading_firstparty=cisagov Libraries # Should be auto-populated by seed-isort-config hook -known_third_party=docopt,github,nacl,requests,setuptools,schema +known_third_party=docopt,github,keyring,nacl,requests,setuptools,schema # These must be manually set to correctly separate them from third party libraries known_first_party= diff --git a/project_setup/scripts/terraform-to-secrets b/project_setup/scripts/terraform-to-secrets index 93ec47d..00c8fbc 100755 --- a/project_setup/scripts/terraform-to-secrets +++ b/project_setup/scripts/terraform-to-secrets @@ -28,20 +28,24 @@ state directory, within a GitHub project. It will attempt to detect the reposit name from the project's git origin. Options exist to provide the repository name or Terraform state manually. -It requires a Personal Access Token from GitHub that has "repo" access scope. +It requires a Personal Access Token from GitHub that has "repo" access scope. Tokens +can be saved to the keychain service for future use by using the "save" command. Usage: - terraform-to-secrets [options] + terraform-to-secrets [options] + terraform-to-secrets save + terraform-to-secrets (-h | --help) Options: -d --dry-run Don't create secrets. Just log what would be created. -h --help Show this message. - --log-level=LEVEL If specified, then the log level will be set to + -l --log-level=LEVEL If specified, then the log level will be set to the specified value. Valid values are "debug", "info", "warning", "error", and "critical". [default: info] - --repo=REPONAME Use provided repository name instead of detecting it. - --state=JSONFILE Read state from a file instead of asking Terraform. + -r --repo=REPONAME Use provided repository name instead of detecting it. + -s --state=JSONFILE Read state from a file instead of asking Terraform. + -t --token=PAT Specify a GitHub personal access token (PAT). """ # Standard Python Libraries @@ -55,6 +59,7 @@ from typing import Any, Dict, Generator, Optional, Tuple, Union # Third-Party Libraries import docopt +import keyring from nacl import encoding, public import requests from schema import And, Or, Schema, SchemaError, Use @@ -63,6 +68,8 @@ from schema import And, Or, Schema, SchemaError, Use GIT_URL_RE: re.Pattern = re.compile("(?:git@|https://)github.com[:/](.*).git") GITHUB_SECRET_NAME_TAG: str = "GitHub_Secret_Name" GITHUB_SECRET_TERRAFORM_LOOKUP_TAG: str = "GitHub_Secret_Terraform_Lookup" +KEYRING_SERVICE = "terraform-to-secrets" +KEYRING_USERNAME = "GitHub PAT" def get_terraform_state(filename: str = None) -> Dict: @@ -279,17 +286,20 @@ def main() -> int: # Validate and convert arguments as needed schema: Schema = Schema( { - "": And( - str, - lambda n: len(n) == 40, - error=" must be a 40 character personal access token.", + "": Or( + None, + And( + str, + lambda n: len(n) == 40, + error="--token must be a 40 character personal access token.", + ), ), "--log-level": And( str, Use(str.lower), lambda n: n in ("debug", "info", "warning", "error", "critical"), error="Possible values for --log-level are " - + "debug, info, warning, error, and critical.", + "debug, info, warning, error, and critical.", ), "--repo": Or( None, @@ -299,6 +309,14 @@ def main() -> int: error='Repository names must contain a "/"', ), ), + "--token": Or( + None, + And( + str, + lambda n: len(n) == 40, + error="--token must be a 40 character personal access token.", + ), + ), str: object, # Don't care about other keys, if any } ) @@ -312,21 +330,40 @@ def main() -> int: # Assign validated arguments to variables dry_run: bool = validated_args["--dry-run"] - github_token: str = validated_args[""] + github_token_to_save: str = validated_args[""] log_level: str = validated_args["--log-level"] repo_name: str = validated_args["--repo"] state_filename: str = validated_args["--state"] + github_token: str = validated_args["--token"] # Set up logging logging.basicConfig( format="%(asctime)-15s %(levelname)s %(message)s", level=log_level.upper() ) + # Just save the GitHub token to the keyring and exit. + if validated_args["save"]: + logging.info("Saving the GitHub personal access token to the keyring.") + keyring.set_password(KEYRING_SERVICE, KEYRING_USERNAME, github_token_to_save) + logging.info("Success!") + return 0 + # If the user does not provide a repo name we'll try to determine it from git if not repo_name: repo_name = get_repo_name() logging.info(f"Using GitHub repository name: {repo_name}") + if github_token is None: + logging.debug("GitHub token not provided in arguments. Checking keyring.") + github_token = keyring.get_password(KEYRING_SERVICE, KEYRING_USERNAME) + if github_token is None: + logging.critical( + "GitHub token not provided on command line or found in keychain." + ) + return -1 + else: + logging.info("GitHub token retrieved from keyring.") + # Get the state from Terraform or a json file terraform_state: Dict = get_terraform_state(state_filename) diff --git a/setup.py b/setup.py index 59f906e..8ca49da 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,14 @@ def package_vars(version_file): python_requires=">=3.6", # What does your project relate to? keywords="documentation", - install_requires=["docopt", "PyNaCl", "setuptools >= 24.2.0", "schema", "PyGithub"], + install_requires=[ + "docopt", + "keyring", + "PyNaCl", + "setuptools >= 24.2.0", + "schema", + "PyGithub", + ], extras_require={ "test": [ "pre-commit",