diff --git a/AUTHORS.md b/AUTHORS.md index 5d5b99a06..f4160146c 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -46,3 +46,4 @@ Moto is written by Steve Pulec with contributions from: * [Justin Wiley](https://github.com/SectorNine50) * [Adam Stauffer](https://github.com/adamstauffer) * [Guy Templeton](https://github.com/gjtempleton) +* [Michael van Tellingen](https://github.com/mvantellingen) diff --git a/moto/__init__.py b/moto/__init__.py index 7cc9594fb..8101a4332 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -31,6 +31,7 @@ from .ses import mock_ses, mock_ses_deprecated # flake8: noqa from .sns import mock_sns, mock_sns_deprecated # flake8: noqa from .sqs import mock_sqs, mock_sqs_deprecated # flake8: noqa from .sts import mock_sts, mock_sts_deprecated # flake8: noqa +from .ssm import mock_ssm # flake8: noqa from .route53 import mock_route53, mock_route53_deprecated # flake8: noqa from .swf import mock_swf, mock_swf_deprecated # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index 94c7f4849..eae94db75 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -27,6 +27,7 @@ from moto.s3 import s3_backends from moto.ses import ses_backends from moto.sns import sns_backends from moto.sqs import sqs_backends +from moto.ssm import ssm_backends from moto.sts import sts_backends BACKENDS = { @@ -56,6 +57,7 @@ BACKENDS = { 'ses': ses_backends, 'sns': sns_backends, 'sqs': sqs_backends, + 'ssm': ssm_backends, 'sts': sts_backends, 'route53': route53_backends, 'lambda': lambda_backends, diff --git a/moto/ssm/__init__.py b/moto/ssm/__init__.py new file mode 100644 index 000000000..c42f3b780 --- /dev/null +++ b/moto/ssm/__init__.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +from .models import ssm_backends +from ..core.models import base_decorator + +ssm_backend = ssm_backends['us-east-1'] +mock_ssm = base_decorator(ssm_backends) diff --git a/moto/ssm/models.py b/moto/ssm/models.py new file mode 100644 index 000000000..3344623dd --- /dev/null +++ b/moto/ssm/models.py @@ -0,0 +1,65 @@ +from __future__ import unicode_literals + +from moto.core import BaseBackend, BaseModel +from moto.ec2 import ec2_backends + + +class Parameter(BaseModel): + def __init__(self, name, value, type, description, keyid): + self.name = name + self.type = type + self.description = description + self.keyid = keyid + + if self.type == 'SecureString': + self.value = self.encrypt(value) + else: + self.value = value + + def encrypt(self, value): + return 'kms:{}:'.format(self.keyid or 'default') + value + + def decrypt(self, value): + if self.type != 'SecureString': + return value + + prefix = 'kms:{}:'.format(self.keyid or 'default') + if value.startswith(prefix): + return value[len(prefix):] + + def response_object(self, decrypt=False): + return { + 'Name': self.name, + 'Type': self.type, + 'Value': self.decrypt(self.value) if decrypt else self.value + } + + +class SimpleSystemManagerBackend(BaseBackend): + + def __init__(self): + self._parameters = {} + + def delete_parameter(self, name): + try: + del self._parameters[name] + except KeyError: + pass + + def get_parameters(self, names, with_decryption): + result = [] + for name in names: + if name in self._parameters: + result.append(self._parameters[name]) + return result + + def put_parameter(self, name, description, value, type, keyid, overwrite): + if not overwrite and name in self._parameters: + return + self._parameters[name] = Parameter( + name, value, type, description, keyid) + + +ssm_backends = {} +for region, ec2_backend in ec2_backends.items(): + ssm_backends[region] = SimpleSystemManagerBackend() diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py new file mode 100644 index 000000000..ee21d7380 --- /dev/null +++ b/moto/ssm/responses.py @@ -0,0 +1,56 @@ +from __future__ import unicode_literals +import json + +from moto.core.responses import BaseResponse +from .models import ssm_backends + + +class SimpleSystemManagerResponse(BaseResponse): + + @property + def ssm_backend(self): + return ssm_backends[self.region] + + @property + def request_params(self): + try: + return json.loads(self.body) + except ValueError: + return {} + + def _get_param(self, param, default=None): + return self.request_params.get(param, default) + + def delete_parameter(self): + name = self._get_param('Name') + self.ssm_backend.delete_parameter(name) + return json.dumps({}) + + def get_parameters(self): + names = self._get_param('Names') + with_decryption = self._get_param('WithDecryption') + + result = self.ssm_backend.get_parameters(names, with_decryption) + + response = { + 'Parameters': [], + 'InvalidParameters': [], + } + + for parameter in result: + param_data = parameter.response_object(with_decryption) + response['Parameters'].append(param_data) + + return json.dumps(response) + + def put_parameter(self): + name = self._get_param('Name') + description = self._get_param('Description') + value = self._get_param('Value') + type_ = self._get_param('Type') + keyid = self._get_param('KeyId') + overwrite = self._get_param('Overwrite', False) + + self.ssm_backend.put_parameter( + name, description, value, type_, keyid, overwrite) + return json.dumps({}) diff --git a/moto/ssm/urls.py b/moto/ssm/urls.py new file mode 100644 index 000000000..d22866486 --- /dev/null +++ b/moto/ssm/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import SimpleSystemManagerResponse + +url_bases = [ + "https?://ssm.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': SimpleSystemManagerResponse.dispatch, +} diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py new file mode 100644 index 000000000..6b8a1a369 --- /dev/null +++ b/tests/test_ssm/test_ssm_boto3.py @@ -0,0 +1,114 @@ +from __future__ import unicode_literals + +import boto3 +import sure # noqa + +from moto import mock_ssm + + +@mock_ssm +def test_delete_parameter(): + client = boto3.client('ssm', region_name='us-east-1') + + client.put_parameter( + Name='test', + Description='A test parameter', + Value='value', + Type='String') + + response = client.get_parameters(Names=['test']) + len(response['Parameters']).should.equal(1) + + client.delete_parameter(Name='test') + + response = client.get_parameters(Names=['test']) + len(response['Parameters']).should.equal(0) + + +@mock_ssm +def test_put_parameter(): + client = boto3.client('ssm', region_name='us-east-1') + + client.put_parameter( + Name='test', + Description='A test parameter', + Value='value', + Type='String') + + response = client.get_parameters( + Names=[ + 'test' + ], + WithDecryption=False) + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Value'].should.equal('value') + response['Parameters'][0]['Type'].should.equal('String') + + +@mock_ssm +def test_put_parameter_secure_default_kms(): + client = boto3.client('ssm', region_name='us-east-1') + + client.put_parameter( + Name='test', + Description='A test parameter', + Value='value', + Type='SecureString') + + response = client.get_parameters( + Names=[ + 'test' + ], + WithDecryption=False) + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Value'].should.equal('kms:default:value') + response['Parameters'][0]['Type'].should.equal('SecureString') + + response = client.get_parameters( + Names=[ + 'test' + ], + WithDecryption=True) + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Value'].should.equal('value') + response['Parameters'][0]['Type'].should.equal('SecureString') + + +@mock_ssm +def test_put_parameter_secure_custom_kms(): + client = boto3.client('ssm', region_name='us-east-1') + + client.put_parameter( + Name='test', + Description='A test parameter', + Value='value', + Type='SecureString', + KeyId='foo') + + response = client.get_parameters( + Names=[ + 'test' + ], + WithDecryption=False) + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Value'].should.equal('kms:foo:value') + response['Parameters'][0]['Type'].should.equal('SecureString') + + response = client.get_parameters( + Names=[ + 'test' + ], + WithDecryption=True) + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Value'].should.equal('value') + response['Parameters'][0]['Type'].should.equal('SecureString')