From 126f5a5155af519673faf8aef4dcc3232c969447 Mon Sep 17 00:00:00 2001 From: Jordan Reiter Date: Tue, 28 Jul 2020 05:17:35 -0400 Subject: [PATCH] Implement Filter: Contains functionality for describe_params (#3189) * Implement Filter: Contains functionality for describe_params This commit adds the Contains functionality. Tests were created to mimic behavior in AWS/boto3, including that filters with values in the form of `/name` will match parameters named `/name/match` but not parameters named `match/with/other-name`. In the test example, a Contains filter with the value `/tan` would match: `/tangent-3` and `tangram-4` but not `standby-5`. * Enforce parameter filter restrictions on get_parameters_by_path According to the boto3 documentation [1], `Name`, `Path`, and `Tier` are not allowed values for `Key` in a parameter filter for `get_parameters_by_path`. This commit enforces this by calling `_validate_parameter_filters` from the `get_parameters_by_path` method, and adding a check to `_validate_parameter_filters`. I added 3 test cases to `test_get_parameters_by_path` which check for the correct exception when calling with a parameter filter using any of these keys. * Code formatted to match style * Refactored logic --- moto/ssm/models.py | 20 ++++++++++++-- tests/test_ssm/test_ssm_boto3.py | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index 28175bb06..3c29097e8 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -965,6 +965,13 @@ class SimpleSystemManagerBackend(BaseBackend): "The following filter key is not valid: Label. Valid filter keys include: [Path, Name, Type, KeyId, Tier]." ) + if by_path and key in ["Name", "Path", "Tier"]: + raise InvalidFilterKey( + "The following filter key is not valid: {key}. Valid filter keys include: [Type, KeyId].".format( + key=key + ) + ) + if not values: raise InvalidFilterValue( "The following filter values are missing : null for filter key Name." @@ -1024,7 +1031,10 @@ class SimpleSystemManagerBackend(BaseBackend): ) ) - if key != "Path" and option not in ["Equals", "BeginsWith"]: + allowed_options = ["Equals", "BeginsWith"] + if key == "Name": + allowed_options += ["Contains"] + if key != "Path" and option not in allowed_options: raise InvalidFilterOption( "The following filter option is not valid: {option}. Valid options include: [BeginsWith, Equals].".format( option=option @@ -1084,6 +1094,9 @@ class SimpleSystemManagerBackend(BaseBackend): max_results=10, ): """Implement the get-parameters-by-path-API in the backend.""" + + self._validate_parameter_filters(filters, by_path=True) + result = [] # path could be with or without a trailing /. we handle this # difference here. @@ -1134,7 +1147,8 @@ class SimpleSystemManagerBackend(BaseBackend): what = parameter.keyid elif key == "Name": what = "/" + parameter.name.lstrip("/") - values = ["/" + value.lstrip("/") for value in values] + if option != "Contains": + values = ["/" + value.lstrip("/") for value in values] elif key == "Path": what = "/" + parameter.name.lstrip("/") values = ["/" + value.strip("/") for value in values] @@ -1147,6 +1161,8 @@ class SimpleSystemManagerBackend(BaseBackend): what.startswith(value) for value in values ): return False + elif option == "Contains" and not any(value in what for value in values): + return False elif option == "Equals" and not any(what == value for value in values): return False elif option == "OneLevel": diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index 837f81bf5..e899613e0 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -198,6 +198,33 @@ def test_get_parameters_by_path(): len(response["Parameters"]).should.equal(1) response.should_not.have.key("NextToken") + filters = [{"Key": "Name", "Values": ["error"]}] + client.get_parameters_by_path.when.called_with( + Path="/baz", ParameterFilters=filters + ).should.throw( + ClientError, + "The following filter key is not valid: Name. " + "Valid filter keys include: [Type, KeyId].", + ) + + filters = [{"Key": "Path", "Values": ["/error"]}] + client.get_parameters_by_path.when.called_with( + Path="/baz", ParameterFilters=filters + ).should.throw( + ClientError, + "The following filter key is not valid: Path. " + "Valid filter keys include: [Type, KeyId].", + ) + + filters = [{"Key": "Tier", "Values": ["Standard"]}] + client.get_parameters_by_path.when.called_with( + Path="/baz", ParameterFilters=filters + ).should.throw( + ClientError, + "The following filter key is not valid: Tier. " + "Valid filter keys include: [Type, KeyId].", + ) + @mock_ssm def test_put_parameter(): @@ -504,6 +531,9 @@ 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") + client.put_parameter(Name="/tangent-3", Value="value-3", Type="String") + client.put_parameter(Name="tangram-4", Value="value-4", Type="String") + client.put_parameter(Name="standby-5", Value="value-5", Type="String") response = client.describe_parameters( ParameterFilters=[{"Key": "Name", "Values": ["param"]}] @@ -543,6 +573,22 @@ def test_describe_parameters_with_parameter_filters_name(): parameters.should.have.length_of(2) response.should_not.have.key("NextToken") + response = client.describe_parameters( + ParameterFilters=[{"Key": "Name", "Option": "Contains", "Values": ["ram"]}] + ) + + parameters = response["Parameters"] + parameters.should.have.length_of(3) + response.should_not.have.key("NextToken") + + response = client.describe_parameters( + ParameterFilters=[{"Key": "Name", "Option": "Contains", "Values": ["/tan"]}] + ) + + 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():