diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c59c49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +#idea files +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e33881 --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# ssm-simple-cli + +[![CircleCI](https://circleci.com/gh/circleci/circleci-docs.svg?style=shield)](https://circleci.com/gh/circleci/circleci-docs) + +A Python based CLI that works with [AWS Systems Manager Parameter Store] in the simplest way possible. +Powered by [Boto3] & [Click]! + +## Main Features +- Simple, intuitive and easy to use CLI to use as password managemant tool! +- Levraging AWS password managemant capabilities and without cost! Free of charge! +- Quick installation & lightweight +- Duality - Allows using both as CLI for developer to utilize and as also as a Python client to run in CI +- Hight test coverage for a reltively simple CLI + +> The ssm-simple-cli is designated to act primarily as a CLI but it also allows using it as a Python client. +> Most of the instructions below will help you setup & use the ssm-simple-cli as a plain CLI. +> If you are interested in using it as a client within Python code you can read more in the [Development](##development) section. + +## Installation +### Please make sure: +- You have Python 3 version installed. +- You are NOT on any virtualenv +- You have configured AWS credentials with the neccessary user profile permissions. See [AWS Permissions section](#aws-permissions--credentials) for more information. + +```sh +$ pip install --editable . +``` +## Initial Setup + +> Since ssm-simple-cli uses [AWS Systems Manager Parameter Store] behind the scenes, you must have a valid AWS account with crednetials set up. See [AWS Permissions section](#aws-permissions--credentials) for more info. + +After a fresh install, you will have to configure you cli for the first time: + +```sh +$ ssm configure +``` + +This will require you to enter you AWS account credentials. You can also configure the ssm-simple-cli to point to a specific named AWS profile: + +```sh +$ ssm configure -p +``` + +> To read more about multiple AWS profiles support [click here](#####multiple-aws-profile-support!). + +## Usage + +Once ssm-siple-cli was configure, simply type `ssm` in you desired shell to show the main help menu: + +```console +$ ssm + +Usage: ssm [OPTIONS] COMMAND [ARGS]... + +Options: + --help Show this message and exit. + +Commands: + configure Setup an initial configuration for this cli + describe Retrieve a list of available secrets + get Retrieve a specific secret. + put Submit a new secret to the SSM store +``` + +You can also use the `--help` flag on a specific command to find our more about it and its parameters: + +```sh +$ ssm describe --help + +Usage: ssm describe [OPTIONS] + + Retrieve a list of available secrets + +Options: + -c, --copy Copies the selected secret value to your clipboard + -g, --get Give you a prompt to choose which secret to get + -p, --path TEXT Describe only parameters located in a specific path (must start with "/") + + --help Show this message and exit. + +``` + +## AWS permissions & credentials + +In order to run the ssm-simple-cli you must have a valid AWS account, AWS credentials and basic permissions to the [AWS Systems Manager Parameter Store]. Please read below in order to get more details. + +### AWS Account +Just go to the [AWS homepage](https://aws.amazon.com/) and create an account. No initial sum requied! + +### Credentials + +AWS credentials are usually configured using the [aws-cli]. you should install the cli and then configure your AWS credentials using this command: + +```sh +$ aws configure +``` + +[Click here](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html#cli-quick-configuration-creds) for more information on how to obtain such credentials + +Once you have these credentials in place you will be able to [setup](##initial-setup) the ssm-simple-cli. + +##### Multiple AWS Profile Support! +When you setup the [aws-cli], credentials are stored in a default AWS profile however if you use multiple [named AWS profiles](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) the ssm-simple-cli totally supports this! + +> By default the ssm-simple-cli uses the "default" AWS profile + +To view what current AWS profile I'm using simply run + +```sh +$ ssm configure -s +``` + +This will print the curent AWS named profile the ssm-simple-cli is using. + +If you wish to configure a different named AWS profile: + +```sh +$ ssm configure -p +``` + +This will direct the ssm-simple-cli to use crednetials for this specific named AWS profile + +### Permissions + +Your user must have the proper permissions in AWS Systems Manager Parameter Store. See [this example](example/aws/policy/README.md) in this repo for more information. + + +## Develpoment + +### Running within Python code +> Can be run in CI! + +The ssm-simple-cli also allows running the actual commands within Python code. This provides a more holistic use of the same password mangemant tool & code in both the developer terminal & the actual CI build. Highly recommended! + +To import the Python client code simply import the following classes: +```python +from cli.src.ssm_cli import CliConfiguration +from cli.src.ssm_client import SSMClient +``` + +To initialize the client simply configure a `CLIConfiguration` and provide it to the client + +```python + # by default the ssm-simple-cli configuration file sits at '~/.ssm/config' but can be anywhere +config = CliConfiguration('') +client = SSMClient(config) +``` + + +### Todos + + - Add MORE Tests + - Consider adding "delete" capability (Did not do it since it may be harmful) + +License +---- + +MIT + +[//]: # + [Boto3]: + [Click]: + [aws-cli]: + [AWS Systems Manager Parameter Store]: + [git-repo-url]: \ No newline at end of file diff --git a/cli/__init__.py b/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/src/__init__.py b/cli/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/src/ssm_cli.py b/cli/src/ssm_cli.py new file mode 100644 index 0000000..44aa651 --- /dev/null +++ b/cli/src/ssm_cli.py @@ -0,0 +1,174 @@ +from collections import defaultdict +from pathlib import Path +from botocore.exceptions import ClientError +from configparser import ConfigParser +import click +import pyperclip + +from cli.src.ssm_client import SSMClient + +DEFAULT_CONFIG_PATH = '~/.ssm/config' +DEFAULT_AWS_CONFIG_PATH = '~/.aws/credentials' +DEFAULT_SSM_CONFIG_PARAMS = { + 'profile_name': 'default', + 'auto_decrypt_secret_value': True, + 'auto_encrypt_secret_value': True +} + +pass_client = click.make_pass_decorator(SSMClient) + + +class CliConfiguration: + DEFAULT_PROFILE_NAME = 'default' + + def __init__(self, path): + self.config_path = Path(path) + self.config = ConfigParser() + + self.config.read(self.config_path.expanduser()) + + def exists(self): + return self.config_path.expanduser().exists() + + def has_multiple_profiles(self): + return len(self.config.sections()) > 1 + + def setup(self, **config_properties): + self.config[self.DEFAULT_PROFILE_NAME] = config_properties + + self.config_path.parent.expanduser().mkdir(parents=True, exist_ok=True) + + with self.config_path.expanduser().open(mode='w') as configfile: + self.config.write(configfile) + + self.config.read(self.config_path.expanduser()) + + def default_profile(self): + return self.config[self.DEFAULT_PROFILE_NAME] + + def profiles(self): + return self.config.sections() + + def get_bool_value(self, key, section=DEFAULT_PROFILE_NAME): + return self.config.getboolean(section, key) + + def should_auto_encrypt_secret_value(self, section=DEFAULT_PROFILE_NAME): + return self.get_bool_value('auto_encrypt_secret_value', section) + + def should_auto_decrypt_secret_value(self, section=DEFAULT_PROFILE_NAME): + return self.get_bool_value('auto_decrypt_secret_value', section) + + +@click.group() +@click.option('--config_path', default=DEFAULT_CONFIG_PATH, + help="Path to the cli configuration file. Default is '{}'".format(DEFAULT_CONFIG_PATH), hidden=True) +@click.pass_context +def cli(ctx, config_path): + config = CliConfiguration(config_path) + + if not config.exists(): + click.secho("You are running this cli with default aws configuration. It is recommended you run 'ssm configure'" + " to better define your aws creds.", fg='yellow') + ctx.obj = SSMClient(config) + else: + ctx.obj = SSMClient(config, profile_name=config.default_profile()['profile_name']) + + +@cli.command(name='configure', help='Setup an initial configuration for this cli') +@click.option('-s', '--show_configuration', is_flag=True, help="Shows the current configuration if already set.") +@click.option('-p', '--profile_name', help="Sets the specific AWS profile you wish to work with." + "If you just have one AWS profile this option is irrelevant") +def configure_client(profile_name, show_configuration=False, path=DEFAULT_CONFIG_PATH): + aws_config = CliConfiguration(DEFAULT_AWS_CONFIG_PATH) + + if not aws_config.exists(): + click.secho("Could not find an aws configure file." + " Please run 'aws configure' and enter your aws credentials", fg='red') + click.secho("If you do not have aws-cli installed. You can read this article and follow accordingly: " + "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html", fg='red') + raise click.Abort + + ssm_config = CliConfiguration(path) + + if show_configuration: + click.echo(dict(ssm_config.default_profile())) + return + + ssm_config_parameters = defaultdict(object, DEFAULT_SSM_CONFIG_PARAMS) + + if profile_name: + ssm_config_parameters['profile_name'] = profile_name + elif aws_config.has_multiple_profiles(): + click.echo("It seems you are using multiple profiles in your aws credentials file.") + selected_profile_name = click.prompt('Which AWS Profile would you like to use?', + type=click.Choice(aws_config.profiles(), case_sensitive=False)) + ssm_config_parameters['profile_name'] = selected_profile_name + + ssm_config.setup(**ssm_config_parameters) + + click.echo("Will be using aws credentials from profile '{}'" + .format(click.style(ssm_config_parameters.get('profile_name'), fg='green'))) + click.secho("New configuration saved in '{}'".format(DEFAULT_CONFIG_PATH), fg='green') + + +@cli.command(name='get', help='Retrieve a specific secret.') +@click.option('-c', '--copy', is_flag=True, help='''Copies the secret value to your clipboard''') +@click.argument('secret_key') +@pass_client +def get_secret(client, secret_key, copy): + try: + value = client.get(secret_key) + + if copy: + pyperclip.copy(value) + click.secho("Secret copied to clipboard!", fg='green') + else: + click.secho(value, fg='green') + except ClientError: + raise click.UsageError(click.style("Parameter Key '{}' not found!".format(secret_key), fg='red')) + + +@cli.command(name='describe', help='Retrieve a list of available secrets') +@click.option('-c', '--copy', is_flag=True, help='''Copies the selected secret value to your clipboard''') +@click.option('-g', '--get', is_flag=True, help='''Give you a prompt to choose which secret to get''') +@click.option('-p', '--path', default=None, help='''Describe only parameters located in a specific path \ + (must start with "/")''') +@pass_client +@click.pass_context +def describe_secrets(ctx, client, path, get, copy): + secrets = client.describe(path) + + if not secrets: + raise click.UsageError(click.style("Could not find any secrets to describe!", fg='red')) + + if not get: + click.secho("\n".join(secrets), fg='green') + else: + click.echo("Select the secret index to get it's value") + + for index, value in enumerate(secrets): + click.echo("[{}] {}".format(index + 1, value)) + + secret_index = click.prompt("\nSelect a Secret by its index") + + selected_secret = secrets[int(secret_index) - 1] + + ctx.invoke(get_secret, secret_key=selected_secret, copy=copy) + + +@cli.command(name='put', help='Submit a new secret to the SSM store. Please enter the key name.' + ' Once submitted, a prompt will be opened to enter the actual secret value') +@click.argument('secret_key') +@pass_client +def put_secret(client, secret_key): + value = click.prompt("What should be the value of the secret key '{}'? ".format(secret_key), + hide_input=True) + description = click.prompt("Would you like to add a description to the '{}' secret? ".format(secret_key)) + + client.put(secret_key, value, description) + + click.secho("Successfully created secret '{}'!".format(secret_key), fg='green') + + +if __name__ == '__main__': + cli() diff --git a/cli/src/ssm_client.py b/cli/src/ssm_client.py new file mode 100644 index 0000000..59cd6cb --- /dev/null +++ b/cli/src/ssm_client.py @@ -0,0 +1,41 @@ +import boto3 + + +class SSMClient: + def __init__(self, config, **client_kwargs): + self.session = boto3.Session(**client_kwargs) + + self.ssm_config = config + self.profile_name = 'default' + + def client(self): + return self.session.client('ssm') + + def get(self, name): + return self.client().get_parameter( + Name=name, + WithDecryption=self.ssm_config.should_auto_decrypt_secret_value() + )['Parameter']['Value'] + + def describe(self, parameters_path=''): + if parameters_path: + params = self.client().describe_parameters(ParameterFilters=[ + { + 'Key': 'Path', + 'Values': [ + parameters_path, + ] + }, + ])['Parameters'] + else: + params = self.client().describe_parameters()['Parameters'] + + return [key['Name'] for key in params] + + def put(self, key, value, description): + self.client().put_parameter( + Name=key, + Description=description, + Value=value, + Type='SecureString' if self.ssm_config.should_auto_encrypt_secret_value() else 'String' + ) diff --git a/cli/src/~/.ssm/config.ini b/cli/src/~/.ssm/config.ini new file mode 100644 index 0000000..fc5ae22 --- /dev/null +++ b/cli/src/~/.ssm/config.ini @@ -0,0 +1,3 @@ +[DEFAULT] +profile_name = eu3 + diff --git a/cli/tests/__init__.py b/cli/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/tests/int/__init__.py b/cli/tests/int/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/tests/int/test_ssm_cli.py b/cli/tests/int/test_ssm_cli.py new file mode 100644 index 0000000..0a92bd8 --- /dev/null +++ b/cli/tests/int/test_ssm_cli.py @@ -0,0 +1,86 @@ +import os + +import boto3 +import pytest +from click.testing import CliRunner +from moto import mock_ssm + +from cli.src import ssm_cli +from cli.src.ssm_cli import CliConfiguration, DEFAULT_SSM_CONFIG_PARAMS + + +@pytest.fixture(scope='function') +def aws_credentials(tmpdir): + fake_aws_creds = tmpdir.join("fake_aws_credentials_file") + fake_aws_creds.write("[default]\nAWS_ACCESS_KEY_ID=testing\nAWS_SECRET_ACCESS_KEY=testing\nregion=eu-west-1") + + """Mocked AWS Credentials for moto.""" + os.environ['AWS_ACCESS_KEY_ID'] = 'testing' + os.environ['AWS_SECRET_ACCESS_KEY'] = 'testing' + os.environ['AWS_SECURITY_TOKEN'] = 'testing' + os.environ['AWS_SESSION_TOKEN'] = 'testing' + os.environ['AWS_SHARED_CREDENTIALS_FILE'] = str(fake_aws_creds) + os.environ['AWS_CONFIG_FILE'] = str(fake_aws_creds) + os.environ['AWS_DEFAULT_REGION'] = 'us-east-1' + + yield fake_aws_creds + + +@pytest.fixture +def fake_ssm_cli_config(tmpdir): + config_tmpdir = tmpdir.join("fake_ssm_credentials_file") + config = CliConfiguration(config_tmpdir) + config.setup(**DEFAULT_SSM_CONFIG_PARAMS) + yield config_tmpdir + + +@pytest.fixture +def fake_ssm_boto_client(aws_credentials): + with mock_ssm(): + yield boto3.client('ssm') + + +# noinspection PyUnusedLocal +def test_should_get_value_when_found(fake_ssm_boto_client, fake_ssm_cli_config): + fake_ssm_boto_client.put_parameter(Name='some-param', Value='some-value', Type='SecureString') + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(ssm_cli.cli, ['--config_path', fake_ssm_cli_config, 'get', 'some-param']) + assert result.output == 'some-value\n' + assert result.exit_code == 0 + + +# noinspection PyUnusedLocal +def test_should_return_not_found_message_when_parameter_not_found(fake_ssm_boto_client, fake_ssm_cli_config): + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(ssm_cli.cli, ['--config_path', fake_ssm_cli_config, 'get', 'some-unknown-value']) + assert 'some-unknown-value' in result.output + assert 'not found!' in result.output + assert result.exit_code == 2 + + +# noinspection PyUnusedLocal +def test_should_put_value_successfully(fake_ssm_boto_client, fake_ssm_cli_config): + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(ssm_cli.cli, ['--config_path', fake_ssm_cli_config, 'put', 'some-param'], + input='some-value\nsome-desc\n') + returned_parameter = fake_ssm_boto_client.get_parameter(Name='some-param', WithDecryption=True) + + assert returned_parameter['Parameter']['Value'] == 'some-value' + assert result.exit_code == 0 + + +# noinspection PyUnusedLocal +def test_should_describe_all_parameters_by_name_and_path(fake_ssm_boto_client, fake_ssm_cli_config): + fake_ssm_boto_client.put_parameter(Name='/some-path/some-param1', Value='not-relevant', Type='SecureString') + fake_ssm_boto_client.put_parameter(Name='some-param2', Value='not-relevant', Type='SecureString') + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(ssm_cli.cli, ['--config_path', fake_ssm_cli_config, 'describe']) + + assert result.output == '/some-path/some-param1\nsome-param2\n' + assert result.exit_code == 0 diff --git a/cli/tests/unit/__init__.py b/cli/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/tests/unit/test_ssm_client.py b/cli/tests/unit/test_ssm_client.py new file mode 100644 index 0000000..1fef049 --- /dev/null +++ b/cli/tests/unit/test_ssm_client.py @@ -0,0 +1,126 @@ +import os + +import boto3 +from botocore.exceptions import ClientError + +import pytest + +from moto import mock_ssm + +from cli.src import ssm_client +from cli.src.ssm_cli import CliConfiguration, DEFAULT_SSM_CONFIG_PARAMS + + +@pytest.fixture(scope='function') +def aws_credentials(tmpdir): + fake_aws_creds = tmpdir.join("fake_aws_credentials_file") + fake_aws_creds.write("[default]\nAWS_ACCESS_KEY_ID=testing\nAWS_SECRET_ACCESS_KEY=testing\nregion=eu-west-1") + + """Mocked AWS Credentials for moto.""" + os.environ['AWS_ACCESS_KEY_ID'] = 'testing' + os.environ['AWS_SECRET_ACCESS_KEY'] = 'testing' + os.environ['AWS_SECURITY_TOKEN'] = 'testing' + os.environ['AWS_SESSION_TOKEN'] = 'testing' + os.environ['AWS_SHARED_CREDENTIALS_FILE'] = str(fake_aws_creds) + os.environ['AWS_CONFIG_FILE'] = str(fake_aws_creds) + os.environ['AWS_DEFAULT_REGION'] = 'us-east-1' + + +@pytest.fixture +def fake_ssm_cli_config(tmpdir): + config_tmpdir = tmpdir.join("fake_ssm_credentials_file") + config = CliConfiguration(config_tmpdir) + config.setup(**DEFAULT_SSM_CONFIG_PARAMS) + yield config + + +@pytest.fixture +def fake_ssm_boto_client(aws_credentials): + with mock_ssm(): + yield boto3.client('ssm') + + +# noinspection PyUnusedLocal +def test_should_get_value_when_found(fake_ssm_boto_client, fake_ssm_cli_config): + fake_ssm_boto_client.put_parameter(Name='some-param', Value='some-value', Type='SecureString') + + client = ssm_client.SSMClient(fake_ssm_cli_config) + + client_result = client.get('some-param') + assert client_result == 'some-value' + + +def test_should_get_value_not_decrypted_when_configured(fake_ssm_boto_client, fake_ssm_cli_config): + fake_ssm_cli_config.default_profile()['auto_decrypt_secret_value'] = 'False' + + fake_ssm_boto_client.put_parameter(Name='some-param', Value='some-value', Type='SecureString') + + client = ssm_client.SSMClient(fake_ssm_cli_config) + + client_result = client.get('some-param') + assert client_result == 'kms:alias/aws/ssm:some-value' + + +def test_should_throw_boto_client_error_when_parameter_not_found(fake_ssm_boto_client, fake_ssm_cli_config): + with pytest.raises(ClientError): + client = ssm_client.SSMClient(fake_ssm_cli_config) + client.get('some-param') + + +def test_should_return_empty_list_when_no_parameters_to_describe(fake_ssm_boto_client, fake_ssm_cli_config): + client = ssm_client.SSMClient(fake_ssm_cli_config) + + client_result = client.describe("/nothing/here/to/describe") + assert len(client_result) == 0 + assert client_result == [] + + +def test_should_describe_all_when_no_path_given(fake_ssm_boto_client, fake_ssm_cli_config): + fake_ssm_boto_client.put_parameter(Name='some-param1', Value='some-value1', Type='SecureString') + fake_ssm_boto_client.put_parameter(Name='some-param2', Value='some-value2', Type='SecureString') + + client = ssm_client.SSMClient(fake_ssm_cli_config) + + client_result = client.describe() + assert len(client_result) == 2 + assert client_result == ['some-param1', 'some-param2'] + + +def test_should_describe_for_a_specific_given_path(fake_ssm_boto_client, fake_ssm_cli_config): + fake_ssm_boto_client.put_parameter(Name='expected_path/some-param1', Value='some-value1', Type='SecureString') + fake_ssm_boto_client.put_parameter(Name='expected_path/some-param2', Value='some-value2', Type='SecureString') + fake_ssm_boto_client.put_parameter(Name='other_path/some-param3', Value='some-value3', Type='SecureString') + + client = ssm_client.SSMClient(fake_ssm_cli_config) + + client_result = client.describe('/expected_path') + assert len(client_result) == 2 + assert client_result == ['expected_path/some-param1', 'expected_path/some-param2'] + + +def test_should_throw_boto_client_error_when_describe_incorrectly(fake_ssm_boto_client, fake_ssm_cli_config): + with pytest.raises(ClientError): + client = ssm_client.SSMClient(fake_ssm_cli_config) + client.describe('this-path-is-wrong-you-will-get-an-error') + + +def test_should_put_encrypted_value_by_default(fake_ssm_boto_client, fake_ssm_cli_config): + client = ssm_client.SSMClient(fake_ssm_cli_config) + + client.put('some-param', 'some-value', 'some-description') + + expected_result = fake_ssm_boto_client.get_parameter(Name='some-param', WithDecryption=True) + + assert expected_result['Parameter']['Value'] == 'some-value' + + +def test_should_put_non_encrypted_value_when_configured(fake_ssm_boto_client, fake_ssm_cli_config): + fake_ssm_cli_config.default_profile()['auto_encrypt_secret_value'] = 'False' + + client = ssm_client.SSMClient(fake_ssm_cli_config) + + client.put('some-param', 'some-value', 'some-description') + + expected_result = fake_ssm_boto_client.get_parameter(Name='some-param', WithDecryption=False) + + assert expected_result['Parameter']['Value'] == 'some-value' diff --git a/examples/aws/policy/README.md b/examples/aws/policy/README.md new file mode 100644 index 0000000..2fd0514 --- /dev/null +++ b/examples/aws/policy/README.md @@ -0,0 +1,4 @@ +this is an example of a required AWS policy to allow a user to access the AWS Systems Manager Parameter Store + +More information about different permissions and capabilities can be found here: +https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html \ No newline at end of file diff --git a/examples/aws/policy/policy.json b/examples/aws/policy/policy.json new file mode 100644 index 0000000..5ed2e88 --- /dev/null +++ b/examples/aws/policy/policy.json @@ -0,0 +1,23 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "ssm:PutParameter", + "ssm:GetParameterHistory", + "ssm:GetParametersByPath", + "ssm:GetParameters", + "ssm:GetParameter" + ], + "Resource": "*" + }, + { + "Sid": "VisualEditor1", + "Effect": "Allow", + "Action": "ssm:DescribeParameters", + "Resource": "*" + } + ] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e488207 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +pytest +ipython +boto3 +moto +click +pyperclip \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..224a779 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..253bfae --- /dev/null +++ b/setup.py @@ -0,0 +1,35 @@ +from setuptools import setup, find_packages + +setup( + name='ssm', + version='0.1', + license='MIT', + description = 'A simplistic CLI that works with AWS Systems Manager Parameter Store', + author='Eyal Stoler', + author_email='eyalstoler@gmail.com', + url='https://github.com/eyalstoler/ssm-simple-cli', + download_url='https://github.com/eyalstoler/ssm-simple-cli/archive/v_01.tar.gz', # I explain this later on + keywords=['python', 'cli', 'aws-cli', 'ssm', 'ssm-cli'], + packages=find_packages(), + include_package_data=True, + install_requires=[ + 'Click', + 'boto3', + 'pyperclip' + ], + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Build Tools', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + ], + entry_points=''' + [console_scripts] + ssm=cli.src.ssm_cli:cli + ''', +) \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..0a2cd28 --- /dev/null +++ b/setup.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +virtualenv venv -p python3 +source venv/bin/activate +pip install -r requirements.txt \ No newline at end of file