Repository: getmoto/moto

Issue:
SSM describe_parameters does not support ParameterFilters
### How to reproduce the issue

```python
from pprint import pprint

import boto3
import moto


def create_params_describe_params():
    ssm = boto3.client('ssm')

    ssm.put_parameter(Name='aaabbb', Value='As and Bs',
                      Type='String', Overwrite=True)
    ssm.put_parameter(Name='aaaccc', Value='As and Cs',
                      Type='String', Overwrite=True)
    ssm.put_parameter(Name='bbbccc', Value='Bs and Cs',
                      Type='String', Overwrite=True)

    response = ssm.describe_parameters(ParameterFilters=[
        {'Key': 'Name',
         'Option': 'BeginsWith',
         'Values': ['aaa']}])
    return [p['Name'] for p in response['Parameters']]


if __name__ == '__main__':
    print("Without moto:")
    pprint(create_params_describe_params())

    with moto.mock_ssm():
        print("With moto:")
        print(create_params_describe_params())
```

Output:

```
Without moto:
['aaabbb', 'aaaccc']
With moto:
['aaabbb', 'aaaccc', 'bbbccc']
```

### What I expect to happen

The name "bbbccc" shouldn't be returned as it does not start with "aaa".

### What does happen

"bbbccc" is returned.

Note that I originally thought this was a duplicate of #1517, but I now realise that it's a separate issue.


Discussion:
Thanks; setting as a feature request.

Resolving patch:
diff --git a/moto/ssm/exceptions.py b/moto/ssm/exceptions.py
new file mode 100644
--- /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
--- 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 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 @@ def __init__(self, name, value, type, description, allowed_pattern, keyid,
         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 @@ def __init__(self):
         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 @@ def delete_parameters(self, names):
                 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 @@ def get_parameters_by_path(self, path, with_decryption, recursive, filters=None)
 
         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', [])
 
+            if key == 'Path':
+                option = filter_obj.get('Option', 'OneLevel')
+            else:
+                option = filter_obj.get('Option', 'Equals')
+
             what = None
-            if key == 'Type':
-                what = parameter.type
-            elif key == 'KeyId':
+            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 option == 'Equals'\
-                    and not any(what == value for value in values):
+            if what is None:
+                return False
+            elif (option == 'BeginsWith' and
+                    not any(what.startswith(value) for value in values)):
                 return False
-            elif option == 'BeginsWith'\
-                    and not any(what.startswith(value) for value in values):
+            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
--- a/moto/ssm/responses.py
+++ b/moto/ssm/responses.py
@@ -104,6 +104,7 @@ def get_parameters_by_path(self):
     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 @@ def describe_parameters(self):
             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:


Test patch:
diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py
--- a/tests/test_ssm/test_ssm_boto3.py
+++ b/tests/test_ssm/test_ssm_boto3.py
@@ -7,7 +7,7 @@
 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)
+
+    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')
 
-    response['Parameters'][1].get('Description').should.be.none
-    response['Parameters'][1]['Version'].should.equal(1)
+    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(