diff --git a/moto/apigateway/exceptions.py b/moto/apigateway/exceptions.py index 434ebc467..2a306ab99 100644 --- a/moto/apigateway/exceptions.py +++ b/moto/apigateway/exceptions.py @@ -39,7 +39,7 @@ class InvalidResourcePathException(BadRequestException): def __init__(self): super(InvalidResourcePathException, self).__init__( "BadRequestException", - "Resource's path part only allow a-zA-Z0-9._- and curly braces at the beginning and the end.", + "Resource's path part only allow a-zA-Z0-9._- and curly braces at the beginning and the end and an optional plus sign before the closing brace.", ) diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index fd2fb7064..748a09e0f 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -556,7 +556,7 @@ class APIGatewayBackend(BaseBackend): return resource def create_resource(self, function_id, parent_resource_id, path_part): - if not re.match("^\\{?[a-zA-Z0-9._-]+\\}?$", path_part): + if not re.match("^\\{?[a-zA-Z0-9._-]+\\+?\\}?$", path_part): raise InvalidResourcePathException() api = self.get_rest_api(function_id) child = api.add_child(path=path_part, parent_id=parent_resource_id) diff --git a/moto/awslambda/exceptions.py b/moto/awslambda/exceptions.py index 1a82977c3..08d13dce5 100644 --- a/moto/awslambda/exceptions.py +++ b/moto/awslambda/exceptions.py @@ -1,4 +1,5 @@ from botocore.client import ClientError +from moto.core.exceptions import JsonRESTError class LambdaClientError(ClientError): @@ -29,3 +30,12 @@ class InvalidRoleFormat(LambdaClientError): role, InvalidRoleFormat.pattern ) super(InvalidRoleFormat, self).__init__("ValidationException", message) + + +class PreconditionFailedException(JsonRESTError): + code = 412 + + def __init__(self, message): + super(PreconditionFailedException, self).__init__( + "PreconditionFailedException", message + ) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 38ff81fb2..939952d5e 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -25,6 +25,7 @@ import requests.adapters from boto3 import Session +from moto.awslambda.policy import Policy from moto.core import BaseBackend, BaseModel from moto.core.exceptions import RESTError from moto.iam.models import iam_backend @@ -47,7 +48,6 @@ from moto.core import ACCOUNT_ID logger = logging.getLogger(__name__) - try: from tempfile import TemporaryDirectory except ImportError: @@ -161,7 +161,8 @@ class LambdaFunction(BaseModel): self.logs_backend = logs_backends[self.region] self.environment_vars = spec.get("Environment", {}).get("Variables", {}) self.docker_client = docker.from_env() - self.policy = "" + self.policy = None + self.state = "Active" # Unfortunately mocking replaces this method w/o fallback enabled, so we # need to replace it if we detect it's been mocked @@ -271,11 +272,11 @@ class LambdaFunction(BaseModel): "MemorySize": self.memory_size, "Role": self.role, "Runtime": self.run_time, + "State": self.state, "Timeout": self.timeout, "Version": str(self.version), "VpcConfig": self.vpc_config, } - if self.environment_vars: config["Environment"] = {"Variables": self.environment_vars} @@ -394,6 +395,7 @@ class LambdaFunction(BaseModel): env_vars.update(self.environment_vars) container = output = exit_code = None + log_config = docker.types.LogConfig(type=docker.types.LogConfig.types.JSON) with _DockerDataVolumeContext(self) as data_vol: try: run_kwargs = ( @@ -409,6 +411,7 @@ class LambdaFunction(BaseModel): volumes=["{}:/var/task".format(data_vol.name)], environment=env_vars, detach=True, + log_config=log_config, **run_kwargs ) finally: @@ -472,7 +475,7 @@ class LambdaFunction(BaseModel): payload["result"] = response_headers["x-amz-log-result"] result = res.encode("utf-8") else: - result = json.dumps(payload) + result = res if errored: response_headers["x-amz-function-error"] = "Handled" @@ -701,7 +704,8 @@ class LambdaStorage(object): "versions": [], "alias": weakref.WeakValueDictionary(), } - + # instantiate a new policy for this version of the lambda + fn.policy = Policy(fn) self._arns[fn.function_arn] = fn def publish_function(self, name): @@ -1002,8 +1006,21 @@ class LambdaBackend(BaseBackend): return True return False - def add_policy(self, function_name, policy): - self.get_function(function_name).policy = policy + def add_policy_statement(self, function_name, raw): + fn = self.get_function(function_name) + fn.policy.add_statement(raw) + + def del_policy_statement(self, function_name, sid, revision=""): + fn = self.get_function(function_name) + fn.policy.del_statement(sid, revision) + + def get_policy(self, function_name): + fn = self.get_function(function_name) + return fn.policy.get_policy() + + def get_policy_wire_format(self, function_name): + fn = self.get_function(function_name) + return fn.policy.wire_format() def update_function_code(self, function_name, qualifier, body): fn = self.get_function(function_name, qualifier) diff --git a/moto/awslambda/policy.py b/moto/awslambda/policy.py new file mode 100644 index 000000000..495e2cee6 --- /dev/null +++ b/moto/awslambda/policy.py @@ -0,0 +1,134 @@ +from __future__ import unicode_literals + +import json +import uuid + +from six import string_types + +from moto.awslambda.exceptions import PreconditionFailedException + + +class Policy: + def __init__(self, parent): + self.revision = str(uuid.uuid4()) + self.statements = [] + self.parent = parent + + def wire_format(self): + p = self.get_policy() + p["Policy"] = json.dumps(p["Policy"]) + return json.dumps(p) + + def get_policy(self): + return { + "Policy": { + "Version": "2012-10-17", + "Id": "default", + "Statement": self.statements, + }, + "RevisionId": self.revision, + } + + # adds the raw JSON statement to the policy + def add_statement(self, raw): + policy = json.loads(raw, object_hook=self.decode_policy) + if len(policy.revision) > 0 and self.revision != policy.revision: + raise PreconditionFailedException( + "The RevisionId provided does not match the latest RevisionId" + " for the Lambda function or alias. Call the GetFunction or the GetAlias API to retrieve" + " the latest RevisionId for your resource." + ) + self.statements.append(policy.statements[0]) + self.revision = str(uuid.uuid4()) + + # removes the statement that matches 'sid' from the policy + def del_statement(self, sid, revision=""): + if len(revision) > 0 and self.revision != revision: + raise PreconditionFailedException( + "The RevisionId provided does not match the latest RevisionId" + " for the Lambda function or alias. Call the GetFunction or the GetAlias API to retrieve" + " the latest RevisionId for your resource." + ) + for statement in self.statements: + if "Sid" in statement and statement["Sid"] == sid: + self.statements.remove(statement) + + # converts AddPermission request to PolicyStatement + # https://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html + def decode_policy(self, obj): + # import pydevd + # pydevd.settrace("localhost", port=5678) + policy = Policy(self.parent) + policy.revision = obj.get("RevisionId", "") + + # set some default values if these keys are not set + self.ensure_set(obj, "Effect", "Allow") + self.ensure_set(obj, "Resource", self.parent.function_arn + ":$LATEST") + self.ensure_set(obj, "StatementId", str(uuid.uuid4())) + + # transform field names and values + self.transform_property(obj, "StatementId", "Sid", self.nop_formatter) + self.transform_property(obj, "Principal", "Principal", self.principal_formatter) + + self.transform_property( + obj, "SourceArn", "SourceArn", self.source_arn_formatter + ) + self.transform_property( + obj, "SourceAccount", "SourceAccount", self.source_account_formatter + ) + + # remove RevisionId and EventSourceToken if they are set + self.remove_if_set(obj, ["RevisionId", "EventSourceToken"]) + + # merge conditional statements into a single map under the Condition key + self.condition_merge(obj) + + # append resulting statement to policy.statements + policy.statements.append(obj) + + return policy + + def nop_formatter(self, obj): + return obj + + def ensure_set(self, obj, key, value): + if key not in obj: + obj[key] = value + + def principal_formatter(self, obj): + if isinstance(obj, string_types): + if obj.endswith(".amazonaws.com"): + return {"Service": obj} + if obj.endswith(":root"): + return {"AWS": obj} + return obj + + def source_account_formatter(self, obj): + return {"StringEquals": {"AWS:SourceAccount": obj}} + + def source_arn_formatter(self, obj): + return {"ArnLike": {"AWS:SourceArn": obj}} + + def transform_property(self, obj, old_name, new_name, formatter): + if old_name in obj: + obj[new_name] = formatter(obj[old_name]) + if new_name != old_name: + del obj[old_name] + + def remove_if_set(self, obj, keys): + for key in keys: + if key in obj: + del obj[key] + + def condition_merge(self, obj): + if "SourceArn" in obj: + if "Condition" not in obj: + obj["Condition"] = {} + obj["Condition"].update(obj["SourceArn"]) + del obj["SourceArn"] + + if "SourceAccount" in obj: + if "Condition" not in obj: + obj["Condition"] = {} + obj["Condition"].update(obj["SourceAccount"]) + del obj["SourceAccount"] diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 46203c10d..e1713ce52 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -120,8 +120,12 @@ class LambdaResponse(BaseResponse): self.setup_class(request, full_url, headers) if request.method == "GET": return self._get_policy(request, full_url, headers) - if request.method == "POST": + elif request.method == "POST": return self._add_policy(request, full_url, headers) + elif request.method == "DELETE": + return self._del_policy(request, full_url, headers, self.querystring) + else: + raise ValueError("Cannot handle {0} request".format(request.method)) def configuration(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -141,9 +145,9 @@ class LambdaResponse(BaseResponse): path = request.path if hasattr(request, "path") else path_url(request.url) function_name = path.split("/")[-2] if self.lambda_backend.get_function(function_name): - policy = self.body - self.lambda_backend.add_policy(function_name, policy) - return 200, {}, json.dumps(dict(Statement=policy)) + statement = self.body + self.lambda_backend.add_policy_statement(function_name, statement) + return 200, {}, json.dumps({"Statement": statement}) else: return 404, {}, "{}" @@ -151,14 +155,21 @@ class LambdaResponse(BaseResponse): path = request.path if hasattr(request, "path") else path_url(request.url) function_name = path.split("/")[-2] if self.lambda_backend.get_function(function_name): - lambda_function = self.lambda_backend.get_function(function_name) - return ( - 200, - {}, - json.dumps( - dict(Policy='{"Statement":[' + lambda_function.policy + "]}") - ), + out = self.lambda_backend.get_policy_wire_format(function_name) + return 200, {}, out + else: + return 404, {}, "{}" + + def _del_policy(self, request, full_url, headers, querystring): + path = request.path if hasattr(request, "path") else path_url(request.url) + function_name = path.split("/")[-3] + statement_id = path.split("/")[-1].split("?")[0] + revision = querystring.get("RevisionId", "") + if self.lambda_backend.get_function(function_name): + self.lambda_backend.del_policy_statement( + function_name, statement_id, revision ) + return 204, {}, "{}" else: return 404, {}, "{}" diff --git a/moto/awslambda/urls.py b/moto/awslambda/urls.py index da7346817..6c9b736a6 100644 --- a/moto/awslambda/urls.py +++ b/moto/awslambda/urls.py @@ -6,7 +6,7 @@ url_bases = ["https?://lambda.(.+).amazonaws.com"] response = LambdaResponse() url_paths = { - "{0}/(?P[^/]+)/functions/?$": response.root, + r"{0}/(?P[^/]+)/functions/?$": response.root, r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/?$": response.function, r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/versions/?$": response.versions, r"{0}/(?P[^/]+)/event-source-mappings/?$": response.event_source_mappings, @@ -14,6 +14,7 @@ url_paths = { r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/invocations/?$": response.invoke, r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/invoke-async/?$": response.invoke_async, r"{0}/(?P[^/]+)/tags/(?P.+)": response.tag, + r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/policy/(?P[\w_-]+)$": response.policy, r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/policy/?$": response.policy, r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/configuration/?$": response.configuration, r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/code/?$": response.code, diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 06561d4c4..12501769e 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -563,6 +563,10 @@ class IamResponse(BaseResponse): def create_access_key(self): user_name = self._get_param("UserName") + if not user_name: + access_key_id = self.get_current_user() + access_key = iam_backend.get_access_key_last_used(access_key_id) + user_name = access_key["user_name"] key = iam_backend.create_access_key(user_name) template = self.response_template(CREATE_ACCESS_KEY_TEMPLATE) @@ -572,6 +576,10 @@ class IamResponse(BaseResponse): user_name = self._get_param("UserName") access_key_id = self._get_param("AccessKeyId") status = self._get_param("Status") + if not user_name: + access_key = iam_backend.get_access_key_last_used(access_key_id) + user_name = access_key["user_name"] + iam_backend.update_access_key(user_name, access_key_id, status) template = self.response_template(GENERIC_EMPTY_TEMPLATE) return template.render(name="UpdateAccessKey") @@ -587,6 +595,11 @@ class IamResponse(BaseResponse): def list_access_keys(self): user_name = self._get_param("UserName") + if not user_name: + access_key_id = self.get_current_user() + access_key = iam_backend.get_access_key_last_used(access_key_id) + user_name = access_key["user_name"] + keys = iam_backend.get_all_access_keys(user_name) template = self.response_template(LIST_ACCESS_KEYS_TEMPLATE) return template.render(user_name=user_name, keys=keys) @@ -594,6 +607,9 @@ class IamResponse(BaseResponse): def delete_access_key(self): user_name = self._get_param("UserName") access_key_id = self._get_param("AccessKeyId") + if not user_name: + access_key = iam_backend.get_access_key_last_used(access_key_id) + user_name = access_key["user_name"] iam_backend.delete_access_key(access_key_id, user_name) template = self.response_template(GENERIC_EMPTY_TEMPLATE) diff --git a/moto/sqs/exceptions.py b/moto/sqs/exceptions.py index 01123d777..77d7b9fb2 100644 --- a/moto/sqs/exceptions.py +++ b/moto/sqs/exceptions.py @@ -99,3 +99,28 @@ class InvalidAttributeName(RESTError): super(InvalidAttributeName, self).__init__( "InvalidAttributeName", "Unknown Attribute {}.".format(attribute_name) ) + + +class InvalidParameterValue(RESTError): + code = 400 + + def __init__(self, message): + super(InvalidParameterValue, self).__init__("InvalidParameterValue", message) + + +class MissingParameter(RESTError): + code = 400 + + def __init__(self): + super(MissingParameter, self).__init__( + "MissingParameter", "The request must contain the parameter Actions." + ) + + +class OverLimit(RESTError): + code = 403 + + def __init__(self, count): + super(OverLimit, self).__init__( + "OverLimit", "{} Actions were found, maximum allowed is 7.".format(count) + ) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 8b8263e3c..8fbe90108 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -30,6 +30,9 @@ from .exceptions import ( BatchEntryIdsNotDistinct, TooManyEntriesInBatchRequest, InvalidAttributeName, + InvalidParameterValue, + MissingParameter, + OverLimit, ) from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID @@ -183,6 +186,7 @@ class Queue(BaseModel): "MaximumMessageSize", "MessageRetentionPeriod", "QueueArn", + "Policy", "RedrivePolicy", "ReceiveMessageWaitTimeSeconds", "VisibilityTimeout", @@ -195,6 +199,8 @@ class Queue(BaseModel): "DeleteMessage", "GetQueueAttributes", "GetQueueUrl", + "ListDeadLetterSourceQueues", + "PurgeQueue", "ReceiveMessage", "SendMessage", ) @@ -273,7 +279,7 @@ class Queue(BaseModel): if key in bool_fields: value = value == "true" - if key == "RedrivePolicy" and value is not None: + if key in ["Policy", "RedrivePolicy"] and value is not None: continue setattr(self, camelcase_to_underscores(key), value) @@ -281,6 +287,9 @@ class Queue(BaseModel): if attributes.get("RedrivePolicy", None): self._setup_dlq(attributes["RedrivePolicy"]) + if attributes.get("Policy"): + self.policy = attributes["Policy"] + self.last_modified_timestamp = now def _setup_dlq(self, policy): @@ -472,6 +481,24 @@ class Queue(BaseModel): return self.name raise UnformattedGetAttTemplateException() + @property + def policy(self): + if self._policy_json.get("Statement"): + return json.dumps(self._policy_json) + else: + return None + + @policy.setter + def policy(self, policy): + if policy: + self._policy_json = json.loads(policy) + else: + self._policy_json = { + "Version": "2012-10-17", + "Id": "{}/SQSDefaultPolicy".format(self.queue_arn), + "Statement": [], + } + class SQSBackend(BaseBackend): def __init__(self, region_name): @@ -802,25 +829,75 @@ class SQSBackend(BaseBackend): def add_permission(self, queue_name, actions, account_ids, label): queue = self.get_queue(queue_name) - if actions is None or len(actions) == 0: - raise RESTError("InvalidParameterValue", "Need at least one Action") - if account_ids is None or len(account_ids) == 0: - raise RESTError("InvalidParameterValue", "Need at least one Account ID") + if not actions: + raise MissingParameter() - if not all([item in Queue.ALLOWED_PERMISSIONS for item in actions]): - raise RESTError("InvalidParameterValue", "Invalid permissions") + if not account_ids: + raise InvalidParameterValue( + "Value [] for parameter PrincipalId is invalid. Reason: Unable to verify." + ) - queue.permissions[label] = (account_ids, actions) + count = len(actions) + if count > 7: + raise OverLimit(count) + + invalid_action = next( + (action for action in actions if action not in Queue.ALLOWED_PERMISSIONS), + None, + ) + if invalid_action: + raise InvalidParameterValue( + "Value SQS:{} for parameter ActionName is invalid. " + "Reason: Only the queue owner is allowed to invoke this action.".format( + invalid_action + ) + ) + + policy = queue._policy_json + statement = next( + ( + statement + for statement in policy["Statement"] + if statement["Sid"] == label + ), + None, + ) + if statement: + raise InvalidParameterValue( + "Value {} for parameter Label is invalid. " + "Reason: Already exists.".format(label) + ) + + principals = [ + "arn:aws:iam::{}:root".format(account_id) for account_id in account_ids + ] + actions = ["SQS:{}".format(action) for action in actions] + + statement = { + "Sid": label, + "Effect": "Allow", + "Principal": {"AWS": principals[0] if len(principals) == 1 else principals}, + "Action": actions[0] if len(actions) == 1 else actions, + "Resource": queue.queue_arn, + } + + queue._policy_json["Statement"].append(statement) def remove_permission(self, queue_name, label): queue = self.get_queue(queue_name) - if label not in queue.permissions: - raise RESTError( - "InvalidParameterValue", "Permission doesnt exist for the given label" + statements = queue._policy_json["Statement"] + statements_new = [ + statement for statement in statements if statement["Sid"] != label + ] + + if len(statements) == len(statements_new): + raise InvalidParameterValue( + "Value {} for parameter Label is invalid. " + "Reason: can't find label on existing policy.".format(label) ) - del queue.permissions[label] + queue._policy_json["Statement"] = statements_new def tag_queue(self, queue_name, tags): queue = self.get_queue(queue_name) diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py index 59c0c07f6..601aa2952 100644 --- a/tests/test_apigateway/test_apigateway.py +++ b/tests/test_apigateway/test_apigateway.py @@ -58,15 +58,15 @@ def test_create_resource__validate_name(): 0 ]["id"] - invalid_names = ["/users", "users/", "users/{user_id}", "us{er"] - valid_names = ["users", "{user_id}", "user_09", "good-dog"] + invalid_names = ["/users", "users/", "users/{user_id}", "us{er", "us+er"] + valid_names = ["users", "{user_id}", "{proxy+}", "user_09", "good-dog"] # All invalid names should throw an exception for name in invalid_names: with assert_raises(ClientError) as ex: client.create_resource(restApiId=api_id, parentId=root_id, pathPart=name) ex.exception.response["Error"]["Code"].should.equal("BadRequestException") ex.exception.response["Error"]["Message"].should.equal( - "Resource's path part only allow a-zA-Z0-9._- and curly braces at the beginning and the end." + "Resource's path part only allow a-zA-Z0-9._- and curly braces at the beginning and the end and an optional plus sign before the closing brace." ) # All valid names should go through for name in valid_names: diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 2835729f8..dfd6431e7 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -148,7 +148,7 @@ def test_invoke_event_function(): FunctionName="testFunction", InvocationType="Event", Payload=json.dumps(in_data) ) success_result["StatusCode"].should.equal(202) - json.loads(success_result["Payload"].read().decode("utf-8")).should.equal({}) + json.loads(success_result["Payload"].read().decode("utf-8")).should.equal(in_data) if settings.TEST_SERVER_MODE: @@ -305,6 +305,7 @@ def test_create_function_from_aws_bucket(): "VpcId": "vpc-123abc", }, "ResponseMetadata": {"HTTPStatusCode": 201}, + "State": "Active", } ) @@ -348,6 +349,7 @@ def test_create_function_from_zipfile(): "Version": "1", "VpcConfig": {"SecurityGroupIds": [], "SubnetIds": []}, "ResponseMetadata": {"HTTPStatusCode": 201}, + "State": "Active", } ) @@ -612,6 +614,7 @@ def test_list_create_list_get_delete_list(): "Timeout": 3, "Version": "$LATEST", "VpcConfig": {"SecurityGroupIds": [], "SubnetIds": []}, + "State": "Active", }, "ResponseMetadata": {"HTTPStatusCode": 200}, } @@ -808,6 +811,7 @@ def test_get_function_created_with_zipfile(): "Timeout": 3, "Version": "$LATEST", "VpcConfig": {"SecurityGroupIds": [], "SubnetIds": []}, + "State": "Active", } ) @@ -1417,6 +1421,7 @@ def test_update_function_zip(): "Timeout": 3, "Version": "2", "VpcConfig": {"SecurityGroupIds": [], "SubnetIds": []}, + "State": "Active", } ) @@ -1479,6 +1484,7 @@ def test_update_function_s3(): "Timeout": 3, "Version": "2", "VpcConfig": {"SecurityGroupIds": [], "SubnetIds": []}, + "State": "Active", } ) diff --git a/tests/test_awslambda/test_policy.py b/tests/test_awslambda/test_policy.py new file mode 100644 index 000000000..92ef0087b --- /dev/null +++ b/tests/test_awslambda/test_policy.py @@ -0,0 +1,49 @@ +from __future__ import unicode_literals + +import json +import sure + +from moto.awslambda.policy import Policy + + +class MockLambdaFunction: + def __init__(self, arn): + self.function_arn = arn + self.policy = None + + +def test_policy(): + policy = Policy(MockLambdaFunction("arn")) + statement = { + "StatementId": "statement0", + "Action": "lambda:InvokeFunction", + "FunctionName": "function_name", + "Principal": "events.amazonaws.com", + "SourceArn": "arn:aws:events:us-east-1:111111111111:rule/rule_name", + "SourceAccount": "111111111111", + } + + expected = { + "Action": "lambda:InvokeFunction", + "FunctionName": "function_name", + "Principal": {"Service": "events.amazonaws.com"}, + "Effect": "Allow", + "Resource": "arn:$LATEST", + "Sid": "statement0", + "Condition": { + "ArnLike": { + "AWS:SourceArn": "arn:aws:events:us-east-1:111111111111:rule/rule_name", + }, + "StringEquals": {"AWS:SourceAccount": "111111111111"}, + }, + } + + policy.add_statement(json.dumps(statement)) + expected.should.be.equal(policy.statements[0]) + + sid = statement.get("StatementId", None) + if sid == None: + raise "TestCase.statement does not contain StatementId" + + policy.del_statement(sid) + [].should.be.equal(policy.statements) diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 9a2c1f0dd..995895437 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -785,7 +785,7 @@ def test_delete_login_profile(): conn.delete_login_profile("my-user") -@mock_iam() +@mock_iam def test_create_access_key(): conn = boto3.client("iam", region_name="us-east-1") with assert_raises(ClientError): @@ -798,6 +798,19 @@ def test_create_access_key(): access_key["AccessKeyId"].should.have.length_of(20) access_key["SecretAccessKey"].should.have.length_of(40) assert access_key["AccessKeyId"].startswith("AKIA") + conn = boto3.client( + "iam", + region_name="us-east-1", + aws_access_key_id=access_key["AccessKeyId"], + aws_secret_access_key=access_key["SecretAccessKey"], + ) + access_key = conn.create_access_key()["AccessKey"] + ( + datetime.utcnow() - access_key["CreateDate"].replace(tzinfo=None) + ).seconds.should.be.within(0, 10) + access_key["AccessKeyId"].should.have.length_of(20) + access_key["SecretAccessKey"].should.have.length_of(40) + assert access_key["AccessKeyId"].startswith("AKIA") @mock_iam_deprecated() @@ -825,8 +838,35 @@ def test_get_all_access_keys(): ) +@mock_iam +def test_list_access_keys(): + conn = boto3.client("iam", region_name="us-east-1") + conn.create_user(UserName="my-user") + response = conn.list_access_keys(UserName="my-user") + assert_equals( + response["AccessKeyMetadata"], [], + ) + access_key = conn.create_access_key(UserName="my-user")["AccessKey"] + response = conn.list_access_keys(UserName="my-user") + assert_equals( + sorted(response["AccessKeyMetadata"][0].keys()), + sorted(["Status", "CreateDate", "UserName", "AccessKeyId"]), + ) + conn = boto3.client( + "iam", + region_name="us-east-1", + aws_access_key_id=access_key["AccessKeyId"], + aws_secret_access_key=access_key["SecretAccessKey"], + ) + response = conn.list_access_keys() + assert_equals( + sorted(response["AccessKeyMetadata"][0].keys()), + sorted(["Status", "CreateDate", "UserName", "AccessKeyId"]), + ) + + @mock_iam_deprecated() -def test_delete_access_key(): +def test_delete_access_key_deprecated(): conn = boto.connect_iam() conn.create_user("my-user") access_key_id = conn.create_access_key("my-user")["create_access_key_response"][ @@ -835,6 +875,16 @@ def test_delete_access_key(): conn.delete_access_key(access_key_id, "my-user") +@mock_iam +def test_delete_access_key(): + conn = boto3.client("iam", region_name="us-east-1") + conn.create_user(UserName="my-user") + key = conn.create_access_key(UserName="my-user")["AccessKey"] + conn.delete_access_key(AccessKeyId=key["AccessKeyId"], UserName="my-user") + key = conn.create_access_key(UserName="my-user")["AccessKey"] + conn.delete_access_key(AccessKeyId=key["AccessKeyId"]) + + @mock_iam() def test_mfa_devices(): # Test enable device @@ -1326,6 +1376,9 @@ def test_update_access_key(): ) resp = client.list_access_keys(UserName=username) resp["AccessKeyMetadata"][0]["Status"].should.equal("Inactive") + client.update_access_key(AccessKeyId=key["AccessKeyId"], Status="Active") + resp = client.list_access_keys(UserName=username) + resp["AccessKeyMetadata"][0]["Status"].should.equal("Active") @mock_iam diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 1eb511db0..93d388117 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -132,6 +132,35 @@ def test_create_queue_with_tags(): ) +@mock_sqs +def test_create_queue_with_policy(): + client = boto3.client("sqs", region_name="us-east-1") + response = client.create_queue( + QueueName="test-queue", + Attributes={ + "Policy": json.dumps( + { + "Version": "2012-10-17", + "Id": "test", + "Statement": [{"Effect": "Allow", "Principal": "*", "Action": "*"}], + } + ) + }, + ) + queue_url = response["QueueUrl"] + + response = client.get_queue_attributes( + QueueUrl=queue_url, AttributeNames=["Policy"] + ) + json.loads(response["Attributes"]["Policy"]).should.equal( + { + "Version": "2012-10-17", + "Id": "test", + "Statement": [{"Effect": "Allow", "Principal": "*", "Action": "*"}], + } + ) + + @mock_sqs def test_get_queue_url(): client = boto3.client("sqs", region_name="us-east-1") @@ -1186,18 +1215,169 @@ def test_permissions(): Actions=["SendMessage"], ) - with assert_raises(ClientError): - client.add_permission( - QueueUrl=queue_url, - Label="account2", - AWSAccountIds=["222211111111"], - Actions=["SomeRubbish"], - ) + response = client.get_queue_attributes( + QueueUrl=queue_url, AttributeNames=["Policy"] + ) + policy = json.loads(response["Attributes"]["Policy"]) + policy["Version"].should.equal("2012-10-17") + policy["Id"].should.equal( + "arn:aws:sqs:us-east-1:123456789012:test-dlr-queue.fifo/SQSDefaultPolicy" + ) + sorted(policy["Statement"], key=lambda x: x["Sid"]).should.equal( + [ + { + "Sid": "account1", + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::111111111111:root"}, + "Action": "SQS:*", + "Resource": "arn:aws:sqs:us-east-1:123456789012:test-dlr-queue.fifo", + }, + { + "Sid": "account2", + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::222211111111:root"}, + "Action": "SQS:SendMessage", + "Resource": "arn:aws:sqs:us-east-1:123456789012:test-dlr-queue.fifo", + }, + ] + ) client.remove_permission(QueueUrl=queue_url, Label="account2") - with assert_raises(ClientError): - client.remove_permission(QueueUrl=queue_url, Label="non_existent") + response = client.get_queue_attributes( + QueueUrl=queue_url, AttributeNames=["Policy"] + ) + json.loads(response["Attributes"]["Policy"]).should.equal( + { + "Version": "2012-10-17", + "Id": "arn:aws:sqs:us-east-1:123456789012:test-dlr-queue.fifo/SQSDefaultPolicy", + "Statement": [ + { + "Sid": "account1", + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::111111111111:root"}, + "Action": "SQS:*", + "Resource": "arn:aws:sqs:us-east-1:123456789012:test-dlr-queue.fifo", + }, + ], + } + ) + + +@mock_sqs +def test_add_permission_errors(): + client = boto3.client("sqs", region_name="us-east-1") + response = client.create_queue(QueueName="test-queue") + queue_url = response["QueueUrl"] + client.add_permission( + QueueUrl=queue_url, + Label="test", + AWSAccountIds=["111111111111"], + Actions=["ReceiveMessage"], + ) + + with assert_raises(ClientError) as e: + client.add_permission( + QueueUrl=queue_url, + Label="test", + AWSAccountIds=["111111111111"], + Actions=["ReceiveMessage", "SendMessage"], + ) + ex = e.exception + ex.operation_name.should.equal("AddPermission") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidParameterValue") + ex.response["Error"]["Message"].should.equal( + "Value test for parameter Label is invalid. " "Reason: Already exists." + ) + + with assert_raises(ClientError) as e: + client.add_permission( + QueueUrl=queue_url, + Label="test-2", + AWSAccountIds=["111111111111"], + Actions=["RemovePermission"], + ) + ex = e.exception + ex.operation_name.should.equal("AddPermission") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidParameterValue") + ex.response["Error"]["Message"].should.equal( + "Value SQS:RemovePermission for parameter ActionName is invalid. " + "Reason: Only the queue owner is allowed to invoke this action." + ) + + with assert_raises(ClientError) as e: + client.add_permission( + QueueUrl=queue_url, + Label="test-2", + AWSAccountIds=["111111111111"], + Actions=[], + ) + ex = e.exception + ex.operation_name.should.equal("AddPermission") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("MissingParameter") + ex.response["Error"]["Message"].should.equal( + "The request must contain the parameter Actions." + ) + + with assert_raises(ClientError) as e: + client.add_permission( + QueueUrl=queue_url, + Label="test-2", + AWSAccountIds=[], + Actions=["ReceiveMessage"], + ) + ex = e.exception + ex.operation_name.should.equal("AddPermission") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidParameterValue") + ex.response["Error"]["Message"].should.equal( + "Value [] for parameter PrincipalId is invalid. Reason: Unable to verify." + ) + + with assert_raises(ClientError) as e: + client.add_permission( + QueueUrl=queue_url, + Label="test-2", + AWSAccountIds=["111111111111"], + Actions=[ + "ChangeMessageVisibility", + "DeleteMessage", + "GetQueueAttributes", + "GetQueueUrl", + "ListDeadLetterSourceQueues", + "PurgeQueue", + "ReceiveMessage", + "SendMessage", + ], + ) + ex = e.exception + ex.operation_name.should.equal("AddPermission") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(403) + ex.response["Error"]["Code"].should.contain("OverLimit") + ex.response["Error"]["Message"].should.equal( + "8 Actions were found, maximum allowed is 7." + ) + + +@mock_sqs +def test_remove_permission_errors(): + client = boto3.client("sqs", region_name="us-east-1") + response = client.create_queue(QueueName="test-queue") + queue_url = response["QueueUrl"] + + with assert_raises(ClientError) as e: + client.remove_permission(QueueUrl=queue_url, Label="test") + ex = e.exception + ex.operation_name.should.equal("RemovePermission") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidParameterValue") + ex.response["Error"]["Message"].should.equal( + "Value test for parameter Label is invalid. " + "Reason: can't find label on existing policy." + ) @mock_sqs