Repository: localstack/localstack

Issue:
Lambda AddPermissions returning 404 Not Found
I'm trying to setup an lambda function triggered by S3 events within a localstack docker-compose environment.
I've got the latest localstack image running in one service with S3, Lambda and APIGATEWAY and the second service is using `awslocal` to communicate with the localstack.

I have gotten as far as successfully creating a lambda function but when I attempt to `add-permission` I get a 404 Not Found error.

My command is:
```
awslocal lambda add-permission 
--function-name my-lambda-function 
--action lambda:InvokeFunction 
--statement-id s3 
--principal s3.amazonaws.com 
--source-arn arn:aws:s3:::event-source-bucket
```

This works against AWS so I'm not sure what I need to have setup within localstack to get it working.  Thanks for any insight you may have.


Discussion:
Also getting the same issue.
+1. Any solution?
+1
+1
so this is still an issue? anyone out there? i'm guessing it has to do something with the --principal being as how s3 is running localstack
+1
I'm getting the same error, did anyone manage to work around this?
I've also run into this issue. Turning on debug logging points to a 404 when retrieving the IAM policy:

`DEBUG - http://localhost:4574 "GET /2015-03-31/functions/MyApi/policy HTTP/1.1" 404 232`

I was able to reproduce the same 404 error by running `awslocal lambda get-policy --function-name MyApi --debug`.

For the record, the function is definitely there and I can access it by running `awslocal lambda get-function --function-name MyApi`

I haven't found any workarounds but maybe this can help point someone in the right direction?
+1
+1 

Resolving patch:
diff --git a/localstack/services/awslambda/lambda_api.py b/localstack/services/awslambda/lambda_api.py
--- a/localstack/services/awslambda/lambda_api.py
+++ b/localstack/services/awslambda/lambda_api.py
@@ -81,6 +81,9 @@
 # lambda executor instance
 LAMBDA_EXECUTOR = lambda_executors.AVAILABLE_EXECUTORS.get(config.LAMBDA_EXECUTOR, lambda_executors.DEFAULT_EXECUTOR)
 
+# IAM version number
+IAM_POLICY_VERSION = '2012-10-17'
+
 # Marker name to indicate that a bucket represents the local file system. This is used for testing
 # Serverless applications where we mount the Lambda code directly into the container from the host OS.
 BUCKET_MARKER_LOCAL = '__local__'
@@ -874,6 +877,50 @@ def update_function_configuration(function):
     return jsonify(result)
 
 
+@app.route('%s/functions/<function>/policy' % PATH_ROOT, methods=['POST'])
+def add_permission(function):
+    data = json.loads(to_str(request.data))
+    iam_client = aws_stack.connect_to_service('iam')
+    sid = short_uid()
+    policy = {
+        'Version': IAM_POLICY_VERSION,
+        'Id': 'LambdaFuncAccess-%s' % sid,
+        'Statement': [{
+            'Sid': sid,
+            'Effect': 'Allow',
+            # TODO: 'Principal' in policies not yet supported in upstream moto
+            # 'Principal': data.get('Principal') or {'AWS': TEST_AWS_ACCOUNT_ID},
+            'Action': data.get('Action'),
+            'Resource': func_arn(function)
+        }]
+    }
+    iam_client.create_policy(PolicyName='lambda_policy_%s' % function,
+        PolicyDocument=json.dumps(policy), Description='Policy for Lambda function "%s"' % function)
+    result = {'Statement': sid}
+    return jsonify(result)
+
+
+@app.route('%s/functions/<function>/policy' % PATH_ROOT, methods=['GET'])
+def get_policy(function):
+    iam_client = aws_stack.connect_to_service('iam')
+    policies = iam_client.list_policies(Scope='Local', MaxItems=500)['Policies']
+    docs = []
+    for p in policies:
+        # TODO: Cache policy documents instead of running N+1 API calls here!
+        versions = iam_client.list_policy_versions(PolicyArn=p['Arn'])['Versions']
+        default_version = [v for v in versions if v.get('IsDefaultVersion')]
+        versions = default_version or versions
+        doc = versions[0]['Document']
+        doc = doc if isinstance(doc, dict) else json.loads(doc)
+        if not isinstance(doc['Statement'], list):
+            doc['Statement'] = [doc['Statement']]
+        docs.append(doc)
+    policy = [d for d in docs if d['Statement'][0]['Resource'] == func_arn(function)]
+    if not policy:
+        return jsonify({}), 404
+    return jsonify({'Policy': policy[0]})
+
+
 @app.route('%s/functions/<function>/invocations' % PATH_ROOT, methods=['POST'])
 def invoke_function(function):
     """ Invoke an existing function
diff --git a/localstack/services/iam/iam_listener.py b/localstack/services/iam/iam_listener.py
--- a/localstack/services/iam/iam_listener.py
+++ b/localstack/services/iam/iam_listener.py
@@ -32,9 +32,8 @@ def _fix_date_format(self, response):
         self._replace(response, pattern, replacement)
 
     def _fix_account_id(self, response):
-        pattern = r'<([^>]*)Arn>\s*arn:aws:iam::([0-9]+):([^<]+)</\1Arn>'
-        replacement = r'<\1Arn>arn:aws:iam::%s:\3</\1Arn>' % TEST_AWS_ACCOUNT_ID
-        self._replace(response, pattern, replacement)
+        return aws_stack.fix_account_id_in_arns(
+            response, existing=MOTO_ACCOUNT_ID, replace=TEST_AWS_ACCOUNT_ID)
 
     def _reset_account_id(self, data):
         """ Fix account ID in request payload. All external-facing responses contain our


Test patch:
diff --git a/tests/integration/test_lambda.py b/tests/integration/test_lambda.py
--- a/tests/integration/test_lambda.py
+++ b/tests/integration/test_lambda.py
@@ -76,7 +76,7 @@ def num_items():
             return len((run_safe(ddb_client.scan, TableName=db_table) or {'Items': []})['Items'])
 
         items_before = num_items()
-        self.run_forward_to_fallback_url('dynamodb://%s' % db_table)
+        self._run_forward_to_fallback_url('dynamodb://%s' % db_table)
         items_after = num_items()
         self.assertEqual(items_after, items_before + 3)
 
@@ -91,12 +91,12 @@ def forward_request(self, method, path, data, headers):
         proxy = start_proxy(local_port, backend_url=None, update_listener=MyUpdateListener())
 
         items_before = len(records)
-        self.run_forward_to_fallback_url('%s://localhost:%s' % (get_service_protocol(), local_port))
+        self._run_forward_to_fallback_url('%s://localhost:%s' % (get_service_protocol(), local_port))
         items_after = len(records)
         self.assertEqual(items_after, items_before + 3)
         proxy.stop()
 
-    def run_forward_to_fallback_url(self, url, num_requests=3):
+    def _run_forward_to_fallback_url(self, url, num_requests=3):
         lambda_client = aws_stack.connect_to_service('lambda')
         config.LAMBDA_FALLBACK_URL = url
         try:
@@ -161,6 +161,23 @@ def test_invocation_type_dry_run(self):
 
         self.assertEqual(result['StatusCode'], 204)
 
+    def test_add_lambda_permission(self):
+        iam_client = aws_stack.connect_to_service('iam')
+        # create lambda permission
+        action = 'lambda:InvokeFunction'
+        resp = self.lambda_client.add_permission(FunctionName=TEST_LAMBDA_NAME_PY, Action=action,
+            StatementId='s3', Principal='s3.amazonaws.com', SourceArn=aws_stack.s3_bucket_arn('test-bucket'))
+        self.assertIn('Statement', resp)
+        # fetch lambda policy
+        policy = self.lambda_client.get_policy(FunctionName=TEST_LAMBDA_NAME_PY)['Policy']
+        self.assertEqual(policy['Statement'][0]['Action'], action)
+        self.assertEqual(policy['Statement'][0]['Resource'], lambda_api.func_arn(TEST_LAMBDA_NAME_PY))
+        # fetch IAM policy
+        policies = iam_client.list_policies(Scope='Local', MaxItems=500)['Policies']
+        matching = [p for p in policies if p['PolicyName'] == 'lambda_policy_%s' % TEST_LAMBDA_NAME_PY]
+        self.assertEqual(len(matching), 1)
+        self.assertIn(':policy/', matching[0]['Arn'])
+
     def test_lambda_environment(self):
         vars = {'Hello': 'World'}
         zip_file = testutil.create_lambda_archive(