From 2cfd3398f6481318f6e54aa93de22c3f6fcf3dff Mon Sep 17 00:00:00 2001 From: gruebel Date: Sat, 21 Sep 2019 16:55:43 +0200 Subject: [PATCH] Add ParameterFilters to SSM describe_parameters --- moto/ssm/exceptions.py | 34 ++ moto/ssm/models.py | 233 ++++++++++++- moto/ssm/responses.py | 36 +- tests/test_ssm/test_ssm_boto3.py | 567 +++++++++++++++++++++++++++++-- 4 files changed, 796 insertions(+), 74 deletions(-) create mode 100644 moto/ssm/exceptions.py diff --git a/moto/ssm/exceptions.py b/moto/ssm/exceptions.py new file mode 100644 index 000000000..4e01843fb --- /dev/null +++ b/moto/ssm/exceptions.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals +from moto.core.exceptions import JsonRESTError + + +class InvalidFilterKey(JsonRESTError): + code = 400 + + def __init__(self, message): + super(InvalidFilterKey, self).__init__( + "InvalidFilterKey", message) + + +class InvalidFilterOption(JsonRESTError): + code = 400 + + def __init__(self, message): + super(InvalidFilterOption, self).__init__( + "InvalidFilterOption", message) + + +class InvalidFilterValue(JsonRESTError): + code = 400 + + def __init__(self, message): + super(InvalidFilterValue, self).__init__( + "InvalidFilterValue", message) + + +class ValidationException(JsonRESTError): + code = 400 + + def __init__(self, message): + super(ValidationException, self).__init__( + "ValidationException", message) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index 2f316a3ac..39bd63ede 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import re from collections import defaultdict from moto.core import BaseBackend, BaseModel @@ -12,6 +13,8 @@ import time import uuid import itertools +from .exceptions import ValidationException, InvalidFilterValue, InvalidFilterOption, InvalidFilterKey + class Parameter(BaseModel): def __init__(self, name, value, type, description, allowed_pattern, keyid, @@ -25,12 +28,15 @@ class Parameter(BaseModel): self.version = version if self.type == 'SecureString': + if not self.keyid: + self.keyid = 'alias/aws/ssm' + self.value = self.encrypt(value) else: self.value = value def encrypt(self, value): - return 'kms:{}:'.format(self.keyid or 'default') + value + return 'kms:{}:'.format(self.keyid) + value def decrypt(self, value): if self.type != 'SecureString': @@ -217,6 +223,7 @@ class SimpleSystemManagerBackend(BaseBackend): self._parameters = {} self._resource_tags = defaultdict(lambda: defaultdict(dict)) self._commands = [] + self._errors = [] # figure out what region we're in for region, backend in ssm_backends.items(): @@ -239,6 +246,179 @@ class SimpleSystemManagerBackend(BaseBackend): pass return result + def describe_parameters(self, filters, parameter_filters): + if filters and parameter_filters: + raise ValidationException('You can use either Filters or ParameterFilters in a single request.') + + self._validate_parameter_filters(parameter_filters, by_path=False) + + result = [] + for param in self._parameters: + ssm_parameter = self._parameters[param] + if not self._match_filters(ssm_parameter, parameter_filters): + continue + + if filters: + for filter in filters: + if filter['Key'] == 'Name': + k = ssm_parameter.name + for v in filter['Values']: + if k.startswith(v): + result.append(ssm_parameter) + break + elif filter['Key'] == 'Type': + k = ssm_parameter.type + for v in filter['Values']: + if k == v: + result.append(ssm_parameter) + break + elif filter['Key'] == 'KeyId': + k = ssm_parameter.keyid + if k: + for v in filter['Values']: + if k == v: + result.append(ssm_parameter) + break + continue + + result.append(ssm_parameter) + + return result + + def _validate_parameter_filters(self, parameter_filters, by_path): + for index, filter_obj in enumerate(parameter_filters or []): + key = filter_obj['Key'] + values = filter_obj.get('Values', []) + + if key == 'Path': + option = filter_obj.get('Option', 'OneLevel') + else: + option = filter_obj.get('Option', 'Equals') + + if not re.match(r'^tag:.+|Name|Type|KeyId|Path|Label|Tier$', key): + self._errors.append(self._format_error( + key='parameterFilters.{index}.member.key'.format(index=(index + 1)), + value=key, + constraint='Member must satisfy regular expression pattern: tag:.+|Name|Type|KeyId|Path|Label|Tier', + )) + + if len(key) > 132: + self._errors.append(self._format_error( + key='parameterFilters.{index}.member.key'.format(index=(index + 1)), + value=key, + constraint='Member must have length less than or equal to 132', + )) + + if len(option) > 10: + self._errors.append(self._format_error( + key='parameterFilters.{index}.member.option'.format(index=(index + 1)), + value='over 10 chars', + constraint='Member must have length less than or equal to 10', + )) + + if len(values) > 50: + self._errors.append(self._format_error( + key='parameterFilters.{index}.member.values'.format(index=(index + 1)), + value=values, + constraint='Member must have length less than or equal to 50', + )) + + if any(len(value) > 1024 for value in values): + self._errors.append(self._format_error( + key='parameterFilters.{index}.member.values'.format(index=(index + 1)), + value=values, + constraint='[Member must have length less than or equal to 1024, Member must have length greater than or equal to 1]', + )) + + self._raise_errors() + + filter_keys = [] + for filter_obj in (parameter_filters or []): + key = filter_obj['Key'] + values = filter_obj.get('Values') + + if key == 'Path': + option = filter_obj.get('Option', 'OneLevel') + else: + option = filter_obj.get('Option', 'Equals') + + if not by_path and key == 'Label': + raise InvalidFilterKey('The following filter key is not valid: Label. Valid filter keys include: [Path, Name, Type, KeyId, Tier].') + + if not values: + raise InvalidFilterValue('The following filter values are missing : null for filter key Name.') + + if key in filter_keys: + raise InvalidFilterKey( + 'The following filter is duplicated in the request: Name. A request can contain only one occurrence of a specific filter.' + ) + + if key == 'Path': + if option not in ['Recursive', 'OneLevel']: + raise InvalidFilterOption( + 'The following filter option is not valid: {option}. Valid options include: [Recursive, OneLevel].'.format(option=option) + ) + if any(value.lower().startswith(('/aws', '/ssm')) for value in values): + raise ValidationException( + 'Filters for common parameters can\'t be prefixed with "aws" or "ssm" (case-insensitive). ' + 'When using global parameters, please specify within a global namespace.' + ) + for value in values: + if value.lower().startswith(('/aws', '/ssm')): + raise ValidationException( + 'Filters for common parameters can\'t be prefixed with "aws" or "ssm" (case-insensitive). ' + 'When using global parameters, please specify within a global namespace.' + ) + if ('//' in value or + not value.startswith('/') or + not re.match('^[a-zA-Z0-9_.-/]*$', value)): + raise ValidationException( + 'The parameter doesn\'t meet the parameter name requirements. The parameter name must begin with a forward slash "/". ' + 'It can\'t be prefixed with \"aws\" or \"ssm\" (case-insensitive). ' + 'It must use only letters, numbers, or the following symbols: . (period), - (hyphen), _ (underscore). ' + 'Special characters are not allowed. All sub-paths, if specified, must use the forward slash symbol "/". ' + 'Valid example: /get/parameters2-/by1./path0_.' + ) + + if key == 'Tier': + for value in values: + if value not in ['Standard', 'Advanced', 'Intelligent-Tiering']: + raise InvalidFilterOption( + 'The following filter value is not valid: {value}. Valid values include: [Standard, Advanced, Intelligent-Tiering].'.format(value=value) + ) + + if key == 'Type': + for value in values: + if value not in ['String', 'StringList', 'SecureString']: + raise InvalidFilterOption( + 'The following filter value is not valid: {value}. Valid values include: [String, StringList, SecureString].'.format(value=value) + ) + + if key != 'Path' and option not in ['Equals', 'BeginsWith']: + raise InvalidFilterOption( + 'The following filter option is not valid: {option}. Valid options include: [BeginsWith, Equals].'.format(option=option) + ) + + filter_keys.append(key) + + def _format_error(self, key, value, constraint): + return 'Value "{value}" at "{key}" failed to satisfy constraint: {constraint}'.format( + constraint=constraint, + key=key, + value=value, + ) + + def _raise_errors(self): + if self._errors: + count = len(self._errors) + plural = "s" if len(self._errors) > 1 else "" + errors = "; ".join(self._errors) + self._errors = [] # reset collected errors + + raise ValidationException('{count} validation error{plural} detected: {errors}'.format( + count=count, plural=plural, errors=errors, + )) + def get_all_parameters(self): result = [] for k, _ in self._parameters.items(): @@ -269,26 +449,53 @@ class SimpleSystemManagerBackend(BaseBackend): return result - @staticmethod - def _match_filters(parameter, filters=None): + def _match_filters(self, parameter, filters=None): """Return True if the given parameter matches all the filters""" for filter_obj in (filters or []): key = filter_obj['Key'] - option = filter_obj.get('Option', 'Equals') values = filter_obj.get('Values', []) - what = None - if key == 'Type': - what = parameter.type - elif key == 'KeyId': - what = parameter.keyid + if key == 'Path': + option = filter_obj.get('Option', 'OneLevel') + else: + option = filter_obj.get('Option', 'Equals') - if option == 'Equals'\ - and not any(what == value for value in values): + what = None + if key == 'KeyId': + what = parameter.keyid + elif key == 'Name': + what = '/' + parameter.name.lstrip('/') + values = ['/' + value.lstrip('/') for value in values] + elif key == 'Path': + what = '/' + parameter.name.lstrip('/') + values = ['/' + value.strip('/') for value in values] + elif key == 'Type': + what = parameter.type + + if what is None: return False - elif option == 'BeginsWith'\ - and not any(what.startswith(value) for value in values): + elif (option == 'BeginsWith' and + not any(what.startswith(value) for value in values)): return False + elif (option == 'Equals' and + not any(what == value for value in values)): + return False + elif option == 'OneLevel': + if any(value == '/' and len(what.split('/')) == 2 for value in values): + continue + elif any(value != '/' and + what.startswith(value + '/') and + len(what.split('/')) - 1 == len(value.split('/')) for value in values): + continue + else: + return False + elif option == 'Recursive': + if any(value == '/' for value in values): + continue + elif any(what.startswith(value + '/') for value in values): + continue + else: + return False # True if no false match (or no filters at all) return True diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py index c47d4127a..27a5f8e35 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -104,6 +104,7 @@ class SimpleSystemManagerResponse(BaseResponse): def describe_parameters(self): page_size = 10 filters = self._get_param('Filters') + parameter_filters = self._get_param('ParameterFilters') token = self._get_param('NextToken') if hasattr(token, 'strip'): token = token.strip() @@ -111,42 +112,17 @@ class SimpleSystemManagerResponse(BaseResponse): token = '0' token = int(token) - result = self.ssm_backend.get_all_parameters() + result = self.ssm_backend.describe_parameters( + filters, parameter_filters + ) + response = { 'Parameters': [], } end = token + page_size for parameter in result[token:]: - param_data = parameter.describe_response_object(False) - add = False - - if filters: - for filter in filters: - if filter['Key'] == 'Name': - k = param_data['Name'] - for v in filter['Values']: - if k.startswith(v): - add = True - break - elif filter['Key'] == 'Type': - k = param_data['Type'] - for v in filter['Values']: - if k == v: - add = True - break - elif filter['Key'] == 'KeyId': - k = param_data.get('KeyId') - if k: - for v in filter['Values']: - if k == v: - add = True - break - else: - add = True - - if add: - response['Parameters'].append(param_data) + response['Parameters'].append(parameter.describe_response_object(False)) token = token + 1 if len(response['Parameters']) == page_size: diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index 77d439d83..33870e383 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -7,7 +7,7 @@ import datetime import uuid import json -from botocore.exceptions import ClientError +from botocore.exceptions import ClientError, ParamValidationError from nose.tools import assert_raises from moto import mock_ssm, mock_cloudformation @@ -336,10 +336,11 @@ def test_describe_parameters(): response = client.describe_parameters() - len(response['Parameters']).should.equal(1) - response['Parameters'][0]['Name'].should.equal('test') - response['Parameters'][0]['Type'].should.equal('String') - response['Parameters'][0]['AllowedPattern'].should.equal(r'.*') + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('test') + parameters[0]['Type'].should.equal('String') + parameters[0]['AllowedPattern'].should.equal(r'.*') @mock_ssm @@ -354,28 +355,28 @@ def test_describe_parameters_paging(): ) response = client.describe_parameters() - len(response['Parameters']).should.equal(10) + response['Parameters'].should.have.length_of(10) response['NextToken'].should.equal('10') response = client.describe_parameters(NextToken=response['NextToken']) - len(response['Parameters']).should.equal(10) + response['Parameters'].should.have.length_of(10) response['NextToken'].should.equal('20') response = client.describe_parameters(NextToken=response['NextToken']) - len(response['Parameters']).should.equal(10) + response['Parameters'].should.have.length_of(10) response['NextToken'].should.equal('30') response = client.describe_parameters(NextToken=response['NextToken']) - len(response['Parameters']).should.equal(10) + response['Parameters'].should.have.length_of(10) response['NextToken'].should.equal('40') response = client.describe_parameters(NextToken=response['NextToken']) - len(response['Parameters']).should.equal(10) + response['Parameters'].should.have.length_of(10) response['NextToken'].should.equal('50') response = client.describe_parameters(NextToken=response['NextToken']) - len(response['Parameters']).should.equal(0) - ''.should.equal(response.get('NextToken', '')) + response['Parameters'].should.have.length_of(0) + response.should_not.have.key('NextToken') @mock_ssm @@ -399,10 +400,12 @@ def test_describe_parameters_filter_names(): 'Values': ['param-22'] }, ]) - len(response['Parameters']).should.equal(1) - response['Parameters'][0]['Name'].should.equal('param-22') - response['Parameters'][0]['Type'].should.equal('String') - ''.should.equal(response.get('NextToken', '')) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('param-22') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') @mock_ssm @@ -426,9 +429,11 @@ def test_describe_parameters_filter_type(): 'Values': ['SecureString'] }, ]) - len(response['Parameters']).should.equal(10) - response['Parameters'][0]['Type'].should.equal('SecureString') - '10'.should.equal(response.get('NextToken', '')) + + parameters = response['Parameters'] + parameters.should.have.length_of(10) + parameters[0]['Type'].should.equal('SecureString') + response.should.have.key('NextToken').which.should.equal('10') @mock_ssm @@ -452,10 +457,508 @@ def test_describe_parameters_filter_keyid(): 'Values': ['key:10'] }, ]) - len(response['Parameters']).should.equal(1) - response['Parameters'][0]['Name'].should.equal('param-10') - response['Parameters'][0]['Type'].should.equal('SecureString') - ''.should.equal(response.get('NextToken', '')) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('param-10') + parameters[0]['Type'].should.equal('SecureString') + response.should_not.have.key('NextToken') + + +@mock_ssm +def test_describe_parameters_with_parameter_filters_keyid(): + client = boto3.client('ssm', region_name='us-east-1') + client.put_parameter( + Name='secure-param', + Value='secure-value', + Type='SecureString' + ) + client.put_parameter( + Name='custom-secure-param', + Value='custom-secure-value', + Type='SecureString', + KeyId='alias/custom' + ) + client.put_parameter( + Name = 'param', + Value = 'value', + Type = 'String' + ) + + response = client.describe_parameters( + ParameterFilters=[{ + 'Key': 'KeyId', + 'Values': ['alias/aws/ssm'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('secure-param') + parameters[0]['Type'].should.equal('SecureString') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'KeyId', + 'Values': ['alias/custom'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('custom-secure-param') + parameters[0]['Type'].should.equal('SecureString') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'KeyId', + 'Option': 'BeginsWith', + 'Values': ['alias'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(2) + response.should_not.have.key('NextToken') + + +@mock_ssm +def test_describe_parameters_with_parameter_filters_name(): + client = boto3.client('ssm', region_name='us-east-1') + client.put_parameter( + Name='param', + Value='value', + Type='String' + ) + client.put_parameter( + Name = '/param-2', + Value = 'value-2', + Type = 'String' + ) + + response = client.describe_parameters( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': ['param'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('param') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': ['/param'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('param') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': ['param-2'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('/param-2') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Name', + 'Option': 'BeginsWith', + 'Values': ['param'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(2) + response.should_not.have.key('NextToken') + + +@mock_ssm +def test_describe_parameters_with_parameter_filters_path(): + client = boto3.client('ssm', region_name='us-east-1') + client.put_parameter( + Name='/foo/name1', + Value='value1', + Type='String') + + client.put_parameter( + Name='/foo/name2', + Value='value2', + Type='String') + + client.put_parameter( + Name='/bar/name3', + Value='value3', + Type='String') + + client.put_parameter( + Name='/bar/name3/name4', + Value='value4', + Type='String') + + client.put_parameter( + Name='foo', + Value='bar', + Type='String') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Values': ['/fo'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(0) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Values': ['/'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('foo') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Values': ['/', '/foo'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(3) + {parameter['Name'] for parameter in response['Parameters']}.should.equal( + {'/foo/name1', '/foo/name2', 'foo'} + ) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Values': ['/foo/'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(2) + {parameter['Name'] for parameter in response['Parameters']}.should.equal( + {'/foo/name1', '/foo/name2'} + ) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'OneLevel', + 'Values': ['/bar/name3'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('/bar/name3/name4') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'Recursive', + 'Values': ['/fo'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(0) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'Recursive', + 'Values': ['/'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(5) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'Recursive', + 'Values': ['/foo', '/bar'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(4) + {parameter['Name'] for parameter in response['Parameters']}.should.equal( + {'/foo/name1', '/foo/name2', '/bar/name3', '/bar/name3/name4'} + ) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'Recursive', + 'Values': ['/foo/'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(2) + {parameter['Name'] for parameter in response['Parameters']}.should.equal( + {'/foo/name1', '/foo/name2'} + ) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'Recursive', + 'Values': ['/bar/name3'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('/bar/name3/name4') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + +@mock_ssm +def test_describe_parameters_invalid_parameter_filters(): + client = boto3.client('ssm', region_name='us-east-1') + + client.describe_parameters.when.called_with( + Filters=[{ + 'Key': 'Name', + 'Values': ['test'] + }], + ParameterFilters=[{ + 'Key': 'Name', + 'Values': ['test'] + }] + ).should.throw( + ClientError, + 'You can use either Filters or ParameterFilters in a single request.' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{}] + ).should.throw( + ParamValidationError, + 'Parameter validation failed:\nMissing required parameter in ParameterFilters[0]: "Key"' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'key', + }] + ).should.throw( + ClientError, + '1 validation error detected: Value "key" at "parameterFilters.1.member.key" failed to satisfy constraint: ' + 'Member must satisfy regular expression pattern: tag:.+|Name|Type|KeyId|Path|Label|Tier' + ) + + long_key = 'tag:' + 't' * 129 + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': long_key, + }] + ).should.throw( + ClientError, + '1 validation error detected: Value "{value}" at "parameterFilters.1.member.key" failed to satisfy constraint: ' + 'Member must have length less than or equal to 132'.format(value=long_key) + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Option': 'over 10 chars' + }] + ).should.throw( + ClientError, + '1 validation error detected: Value "over 10 chars" at "parameterFilters.1.member.option" failed to satisfy constraint: ' + 'Member must have length less than or equal to 10' + ) + + many_values = ['test'] * 51 + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': many_values + }] + ).should.throw( + ClientError, + '1 validation error detected: Value "{value}" at "parameterFilters.1.member.values" failed to satisfy constraint: ' + 'Member must have length less than or equal to 50'.format(value=many_values) + ) + + long_value = ['t' * 1025] + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': long_value + }] + ).should.throw( + ClientError, + '1 validation error detected: Value "{value}" at "parameterFilters.1.member.values" failed to satisfy constraint: ' + '[Member must have length less than or equal to 1024, Member must have length greater than or equal to 1]'.format(value=long_value) + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Option': 'over 10 chars' + },{ + 'Key': 'key', + }] + ).should.throw( + ClientError, + '2 validation errors detected: ' + 'Value "over 10 chars" at "parameterFilters.1.member.option" failed to satisfy constraint: ' + 'Member must have length less than or equal to 10; ' + 'Value "key" at "parameterFilters.2.member.key" failed to satisfy constraint: ' + 'Member must satisfy regular expression pattern: tag:.+|Name|Type|KeyId|Path|Label|Tier' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Label', + }] + ).should.throw( + ClientError, + 'The following filter key is not valid: Label. Valid filter keys include: [Path, Name, Type, KeyId, Tier].' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + }] + ).should.throw( + ClientError, + 'The following filter values are missing : null for filter key Name.' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': [] + }] + ).should.throw( + ParamValidationError, + 'Invalid length for parameter ParameterFilters[0].Values, value: 0, valid range: 1-inf' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': ['test'] + },{ + 'Key': 'Name', + 'Values': ['test test'] + }] + ).should.throw( + ClientError, + 'The following filter is duplicated in the request: Name. A request can contain only one occurrence of a specific filter.' + ) + + for value in ['/###', '//', 'test']: + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Path', + 'Values': [value] + }] + ).should.throw( + ClientError, + 'The parameter doesn\'t meet the parameter name requirements. The parameter name must begin with a forward slash "/". ' + 'It can\'t be prefixed with \"aws\" or \"ssm\" (case-insensitive). ' + 'It must use only letters, numbers, or the following symbols: . (period), - (hyphen), _ (underscore). ' + 'Special characters are not allowed. All sub-paths, if specified, must use the forward slash symbol "/". ' + 'Valid example: /get/parameters2-/by1./path0_.' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Path', + 'Values': ['/aws', '/ssm'] + }] + ).should.throw( + ClientError, + 'Filters for common parameters can\'t be prefixed with "aws" or "ssm" (case-insensitive). ' + 'When using global parameters, please specify within a global namespace.' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Path', + 'Option': 'Equals', + 'Values': ['test'] + }] + ).should.throw( + ClientError, + 'The following filter option is not valid: Equals. Valid options include: [Recursive, OneLevel].' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Tier', + 'Values': ['test'] + }] + ).should.throw( + ClientError, + 'The following filter value is not valid: test. Valid values include: [Standard, Advanced, Intelligent-Tiering]' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Type', + 'Values': ['test'] + }] + ).should.throw( + ClientError, + 'The following filter value is not valid: test. Valid values include: [String, StringList, SecureString]' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Option': 'option', + 'Values': ['test'] + }] + ).should.throw( + ClientError, + 'The following filter option is not valid: option. Valid options include: [BeginsWith, Equals].' + ) @mock_ssm @@ -476,15 +979,17 @@ def test_describe_parameters_attributes(): ) response = client.describe_parameters() - len(response['Parameters']).should.equal(2) - response['Parameters'][0]['Description'].should.equal('my description') - response['Parameters'][0]['Version'].should.equal(1) - response['Parameters'][0]['LastModifiedDate'].should.be.a(datetime.date) - response['Parameters'][0]['LastModifiedUser'].should.equal('N/A') + parameters = response['Parameters'] + parameters.should.have.length_of(2) - response['Parameters'][1].get('Description').should.be.none - response['Parameters'][1]['Version'].should.equal(1) + parameters[0]['Description'].should.equal('my description') + parameters[0]['Version'].should.equal(1) + parameters[0]['LastModifiedDate'].should.be.a(datetime.date) + parameters[0]['LastModifiedUser'].should.equal('N/A') + + parameters[1].should_not.have.key('Description') + parameters[1]['Version'].should.equal(1) @mock_ssm @@ -519,7 +1024,7 @@ def test_put_parameter_secure_default_kms(): 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]['Value'].should.equal('kms:alias/aws/ssm:value') response['Parameters'][0]['Type'].should.equal('SecureString') response = client.get_parameters(