Skip to content

Commit c59bb57

Browse files
committed
POC: new lambda ARN attribute to EC2 proxy
1 parent 99d5a72 commit c59bb57

File tree

8 files changed

+379
-6
lines changed

8 files changed

+379
-6
lines changed

cfn-resources/project/cmd/resource/model.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cfn-resources/project/cmd/resource/resource.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import (
2020
"fmt"
2121
"reflect"
2222

23+
"go.mongodb.org/atlas-sdk/v20231115014/admin"
24+
2325
"github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler"
2426
"github.com/aws/aws-sdk-go/service/cloudformation"
27+
2528
"github.com/mongodb/mongodbatlas-cloudformation-resources/util"
2629
"github.com/mongodb/mongodbatlas-cloudformation-resources/util/constants"
2730
"github.com/mongodb/mongodbatlas-cloudformation-resources/util/progressevent"
2831
"github.com/mongodb/mongodbatlas-cloudformation-resources/util/validator"
29-
"go.mongodb.org/atlas-sdk/v20231115014/admin"
3032
)
3133

3234
var CreateRequiredFields = []string{constants.OrgID, constants.Name}
@@ -45,7 +47,8 @@ func initEnvWithLatestClient(req handler.Request, currentModel *Model, requiredF
4547
return nil, errEvent
4648
}
4749

48-
client, peErr := util.NewAtlasClient(&req, currentModel.Profile)
50+
// client, peErr := util.NewAtlasClient(&req, currentModel.Profile)
51+
client, peErr := util.NewAtlasClientWithLambdaProxySupport(&req, currentModel.Profile, currentModel.LambdaProxyArn)
4952
if peErr != nil {
5053
return nil, peErr
5154
}

cfn-resources/project/docs/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy
1818
"<a href="#withdefaultalertssettings" title="WithDefaultAlertsSettings">WithDefaultAlertsSettings</a>" : <i>Boolean</i>,
1919
"<a href="#projectsettings" title="ProjectSettings">ProjectSettings</a>" : <i><a href="projectsettings.md">projectSettings</a></i>,
2020
"<a href="#profile" title="Profile">Profile</a>" : <i>String</i>,
21+
"<a href="#lambdaproxyarn" title="LambdaProxyArn">LambdaProxyArn</a>" : <i>String</i>,
2122
"<a href="#projectteams" title="ProjectTeams">ProjectTeams</a>" : <i>[ <a href="projectteam.md">projectTeam</a>, ... ]</i>,
2223
"<a href="#projectapikeys" title="ProjectApiKeys">ProjectApiKeys</a>" : <i>[ <a href="projectapikey.md">projectApiKey</a>, ... ]</i>,
2324
"<a href="#regionusagerestrictions" title="RegionUsageRestrictions">RegionUsageRestrictions</a>" : <i>String</i>,
@@ -37,6 +38,7 @@ Properties:
3738
<a href="#withdefaultalertssettings" title="WithDefaultAlertsSettings">WithDefaultAlertsSettings</a>: <i>Boolean</i>
3839
<a href="#projectsettings" title="ProjectSettings">ProjectSettings</a>: <i><a href="projectsettings.md">projectSettings</a></i>
3940
<a href="#profile" title="Profile">Profile</a>: <i>String</i>
41+
<a href="#lambdaproxyarn" title="LambdaProxyArn">LambdaProxyArn</a>: <i>String</i>
4042
<a href="#projectteams" title="ProjectTeams">ProjectTeams</a>: <i>
4143
- <a href="projectteam.md">projectTeam</a></i>
4244
<a href="#projectapikeys" title="ProjectApiKeys">ProjectApiKeys</a>: <i>
@@ -105,6 +107,16 @@ _Type_: String
105107

106108
_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
107109

110+
#### LambdaProxyArn
111+
112+
lambda arn
113+
114+
_Required_: No
115+
116+
_Type_: String
117+
118+
_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
119+
108120
#### ProjectTeams
109121

110122
Teams to which the authenticated user has access in the project specified using its unique 24-hexadecimal digit identifier.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from flask import Flask, request, Response
2+
import requests
3+
import logging
4+
5+
app = Flask(__name__)
6+
7+
logging.basicConfig(level=logging.DEBUG)
8+
logger = logging.getLogger(__name__)
9+
10+
# NOTE: additional configuration would be required to also support Realm
11+
TARGET_SERVER = "https://cloud-dev.mongodb.com"
12+
logger.debug(f"EC2 Proxy configured with TARGET_SERVER: {TARGET_SERVER}")
13+
14+
@app.route('/', defaults={'path': ''}, methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
15+
@app.route('/<path:path>', methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
16+
def proxy(path):
17+
logger.debug(f"Received request for path: {path}")
18+
# Build the target URL
19+
target_url = f"{TARGET_SERVER}/{path}"
20+
logger.debug(f"Target URL: {target_url}")
21+
22+
# Copy the incoming headers
23+
headers = {key: value for key, value in request.headers if key.lower() != 'host'}
24+
logger.debug(f"Request headers: {headers}")
25+
26+
# Forward the request to Atlas
27+
try:
28+
resp = requests.request(
29+
method=request.method,
30+
url=target_url,
31+
headers=headers,
32+
data=request.get_data(),
33+
cookies=request.cookies,
34+
allow_redirects=False
35+
)
36+
# logger.debug(f"Received response from target server - Status: {resp.status_code}, Headers: {resp.headers}")
37+
except Exception as e:
38+
logger.exception("Error forwarding the request to the target server:")
39+
return Response("Error forwarding request", status=500)
40+
41+
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
42+
response_headers = [(name, value) for (name, value) in resp.raw.headers.items()
43+
if name.lower() not in excluded_headers]
44+
45+
response = Response(resp.content, resp.status_code, response_headers)
46+
logger.debug(f"Returning proxied response with status: {resp.status_code}")
47+
return response
48+
49+
if __name__ == '__main__':
50+
logger.debug("Starting EC2 Proxy Flask app on 0.0.0.0:80")
51+
app.run(host='0.0.0.0', port=80)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import json
2+
import logging
3+
import requests # this requires adding request layer to lambda function
4+
from urllib.parse import urlparse, urlunparse
5+
6+
logging.basicConfig(level=logging.DEBUG)
7+
logger = logging.getLogger()
8+
9+
EC2_PROXY_ENDPOINT = "http://XX.X.X.XX" # Replace with private IP of EC2 proxy running Python/Flask
10+
if not EC2_PROXY_ENDPOINT:
11+
logger.error("EC2_PROXY_ENDPOINT is not set")
12+
raise ValueError("EC2_PROXY_ENDPOINT is not set")
13+
logger.debug(f"EC2_PROXY_ENDPOINT: {EC2_PROXY_ENDPOINT}")
14+
15+
def lambda_handler(event, context):
16+
"""
17+
Expected event example:
18+
{
19+
"method": "GET",
20+
"url": "https://cloud-dev.mongodb.com/api/atlas/v2/groups",
21+
"headers": {"Header-Key": "value", ...},
22+
"body": "request body as string"
23+
}
24+
25+
This Lambda function should be deployed in a private subnet. It's corresponding SG
26+
only allows traffic to EC2 proxy running Python/Flask
27+
"""
28+
logger.debug(f"Received event: {json.dumps(event)}") # TODO: remove
29+
try:
30+
method = event.get("method")
31+
url = event.get("url")
32+
headers = event.get("headers", {})
33+
body = event.get("body", None)
34+
35+
if method is None or url is None:
36+
msg = "Missing 'method' or 'url' in the event payload"
37+
logger.error(msg)
38+
raise ValueError(msg)
39+
40+
logger.debug(f"Forwarding request - Method: {method}, URL: {url}, Headers: {headers}, Body: {body}")
41+
42+
parsed_url = urlparse(url)
43+
if parsed_url.scheme and parsed_url.netloc:
44+
# Extract the path and query string because incoming URL will be in format:
45+
# https://www.cloud.mongodb.com/api/atlas/v2/groups?param=value
46+
path = parsed_url.path or ""
47+
query = f"?{parsed_url.query}" if parsed_url.query else ""
48+
new_url = path + query
49+
logger.debug(f"Extracted relative URL: {new_url} from absolute URL: {url}")
50+
else:
51+
new_url = url
52+
logger.debug(f"URL is relative: {new_url}")
53+
54+
# Construct URL for the EC2 proxy
55+
full_url = EC2_PROXY_ENDPOINT.rstrip("/") + new_url
56+
logger.debug(f"Constructed full URL for proxy: {full_url}")
57+
58+
# Forward request to the EC2 proxy
59+
response = requests.request(method, full_url, headers=headers, data=body)
60+
logger.debug(f"Response from EC2 proxy - Status: {response.status_code}, Headers: {dict(response.headers)}, Body: {response.text}")
61+
62+
return {
63+
"statusCode": response.status_code,
64+
"headers": dict(response.headers),
65+
"body": response.text
66+
}
67+
except Exception as e:
68+
logger.exception("Error processing the request:")
69+
return {
70+
"statusCode": 500,
71+
"headers": {},
72+
"body": json.dumps({"error": str(e)})
73+
}

cfn-resources/project/mongodb-atlas-project.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@
127127
"description": "Profile used to provide credentials information, (a secret with the cfn/atlas/profile/{Profile}, is required), if not provided default is used",
128128
"default": "default"
129129
},
130+
"LambdaProxyArn": {
131+
"type": "string",
132+
"description": "lambda arn"
133+
},
130134
"ProjectTeams": {
131135
"items": {
132136
"$ref": "#/definitions/projectTeam"
@@ -177,22 +181,30 @@
177181
"handlers": {
178182
"create": {
179183
"permissions": [
180-
"secretsmanager:GetSecretValue"
184+
"secretsmanager:GetSecretValue",
185+
"lambda:InvokeFunction",
186+
"lambda:GetFunction"
181187
]
182188
},
183189
"read": {
184190
"permissions": [
185-
"secretsmanager:GetSecretValue"
191+
"secretsmanager:GetSecretValue",
192+
"lambda:InvokeFunction",
193+
"lambda:GetFunction"
186194
]
187195
},
188196
"update": {
189197
"permissions": [
190-
"secretsmanager:GetSecretValue"
198+
"secretsmanager:GetSecretValue",
199+
"lambda:InvokeFunction",
200+
"lambda:GetFunction"
191201
]
192202
},
193203
"delete": {
194204
"permissions": [
195-
"secretsmanager:GetSecretValue"
205+
"secretsmanager:GetSecretValue",
206+
"lambda:InvokeFunction",
207+
"lambda:GetFunction"
196208
]
197209
}
198210
},

cfn-resources/project/resource-role.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Resources:
3030
Statement:
3131
- Effect: Allow
3232
Action:
33+
- "lambda:GetFunction"
34+
- "lambda:InvokeFunction"
3335
- "secretsmanager:GetSecretValue"
3436
Resource: "*"
3537
Outputs:

0 commit comments

Comments
 (0)