Skip to content
This repository was archived by the owner on Nov 3, 2025. It is now read-only.

Commit 9e421aa

Browse files
authored
Resolve security group bug (#176)
* nfs access function integrated and test * web configuration to check and warn of missing valid security groups
1 parent 58fb28c commit 9e421aa

File tree

6 files changed

+173
-18
lines changed

6 files changed

+173
-18
lines changed

deployment/simple-file-manager-for-amazon-efs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Resources:
7373
- "ec2:DescribeSecurityGroups"
7474
- "ec2:DescribeSubnets"
7575
- "ec2:DescribeVpcs"
76+
- "ec2:DescribeSecurityGroupRules"
7677
Resource: "*"
7778
- Effect: Allow
7879
Action:

source/api/app.py

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"""API for Simple File Manager for Amazon EFS"""
1414
import os
1515
import logging
16+
import ipaddress
1617
import json
1718
import botocore
1819
from botocore.config import Config
@@ -41,6 +42,7 @@
4142
EFS = boto3.client('efs', config=CONFIG)
4243
SERVERLESS = boto3.client('lambda', config=CONFIG)
4344
CFN = boto3.client('cloudformation', config=CONFIG)
45+
EC2 = boto3.client('ec2', config=CONFIG)
4446

4547

4648
# Helper functions
@@ -246,6 +248,52 @@ def format_operation_response(result, error_message):
246248
return response
247249

248250

251+
def test_nfs_access(group, mount_target_ip):
252+
rules = EC2.describe_security_group_rules(
253+
Filters=[
254+
{
255+
'Name': 'group-id',
256+
'Values': [group]
257+
}
258+
]
259+
)
260+
261+
contains_valid_rule = False
262+
263+
for rule in rules['SecurityGroupRules']:
264+
# check if rule is inbound
265+
if rule['IsEgress'] is False:
266+
if 'ReferencedGroupInfo' in rule:
267+
# references a sg in the rule
268+
ref_group_id = rule['ReferencedGroupInfo']['GroupId']
269+
if ref_group_id == rule['GroupId']:
270+
if rule['IpProtocol'] == '-1' or rule['IpProtocol'] == 'tcp':
271+
if rule['FromPort'] and rule['ToPort'] == 2049 or rule['FromPort'] and rule['ToPort'] == -1:
272+
contains_valid_rule = True
273+
elif rule['FromPort'] <= 2049 <= rule['ToPort']:
274+
contains_valid_rule = True
275+
else:
276+
pass
277+
elif 'CidrIpv4' in rule:
278+
# references an ipv4 subnet
279+
subnet = rule['CidrIpv4']
280+
if ipaddress.ip_address(mount_target_ip) in ipaddress.ip_network(subnet):
281+
if rule['IpProtocol'] == '-1' or rule['IpProtocol'] == 'tcp':
282+
if rule['FromPort'] and rule['ToPort'] == 2049 or rule['FromPort'] and rule['ToPort'] == -1:
283+
contains_valid_rule = True
284+
elif rule['FromPort'] <= 2049 <= rule['ToPort']:
285+
contains_valid_rule = True
286+
else:
287+
pass
288+
else:
289+
# bad request
290+
pass
291+
else:
292+
# outbound rule
293+
pass
294+
295+
return contains_valid_rule
296+
249297
# Routes
250298

251299
@app.route('/filesystems', methods=["GET"], cors=True, authorizer=AUTHORIZER)
@@ -324,13 +372,13 @@ def describe_filesystem(filesystem_id):
324372
authorizer=AUTHORIZER)
325373
def get_netinfo_for_filesystem(filesystem_id):
326374
"""
327-
Retrieves network info for a specified filesystem
375+
Retrieves mount target network info for a specified filesystem
328376
329377
:param filesystem_id: The filesystem to get net info for
330-
:returns: Filesystem network info
378+
:returns: Filesystem mount target network info
331379
:raises ChaliceViewError
332380
"""
333-
netinfo = []
381+
mount_target_info = []
334382
try:
335383
response = EFS.describe_mount_targets(
336384
FileSystemId=filesystem_id
@@ -343,6 +391,8 @@ def get_netinfo_for_filesystem(filesystem_id):
343391
app.log.debug(mount_targets)
344392
for target in mount_targets:
345393
mount_target_id = target['MountTargetId']
394+
mount_target_ip = target['IpAddress']
395+
346396
try:
347397
response = EFS.describe_mount_target_security_groups(
348398
MountTargetId=mount_target_id
@@ -352,11 +402,25 @@ def get_netinfo_for_filesystem(filesystem_id):
352402
raise ChaliceViewError
353403
else:
354404
security_groups = response['SecurityGroups']
355-
vpc_item = {'{id}'.format(id=mount_target_id): {'security_groups': \
356-
security_groups, 'subnet_id': target['SubnetId']}}
357-
netinfo.append(vpc_item)
358-
359-
return netinfo
405+
406+
# test security groups to see if the mount target can be used
407+
valid_security_groups = []
408+
for group in security_groups:
409+
is_valid_group = test_nfs_access(group, mount_target_ip)
410+
if is_valid_group:
411+
valid_security_groups.append(group)
412+
else:
413+
pass
414+
415+
mount_target_item = {'{id}'.format(id=mount_target_id): {'security_groups': \
416+
valid_security_groups, 'subnet_id': target['SubnetId']}}
417+
418+
mount_target_info.append(mount_target_item)
419+
420+
if len(mount_target_info) == 0:
421+
raise BadRequestError('No mount target available with required network configuration. See the deployment guide for configuration details.')
422+
else:
423+
return mount_target_info
360424

361425

362426
@app.route('/filesystems/{filesystem_id}/lambda', methods=['POST'], cors=True, \

source/web/src/routes/Configure.vue

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,15 @@
3535
<b-form-invalid-feedback :state="valid">
3636
UID, GID, and Path must not be empty. The path must begin with a forward slash: /
3737
</b-form-invalid-feedback>
38-
<b-button :disabled='!valid' type="submit">
39-
Submit
40-
<b-icon icon="Hammer" aria-hidden="true"></b-icon>
41-
</b-button>
38+
<div v-if="!mountTargetNetinfo.securityGroups.length">
39+
<b-alert show variant="danger">No mount targets available for the filesystem with security group(s) configured for NFS access. See the deployment guide for further details.</b-alert>
40+
</div>
41+
<div v-else>
42+
<b-button :disabled='!valid' type="submit">
43+
Submit
44+
<b-icon icon="Hammer" aria-hidden="true"></b-icon>
45+
</b-button>
46+
</div>
4247
</b-form>
4348
</b-col>
4449
<b-col>
@@ -59,20 +64,24 @@ export default {
5964
processing: false,
6065
uid: '1000',
6166
gid: '1000',
62-
path: '/efs'
67+
path: '/efs',
68+
mountTargetNetinfo: null
6369
}
6470
},
6571
computed: {
6672
valid() {
6773
return this.uid != "" && this.gid != "" && this.path != "" && this.path.charAt(0) == '/'
6874
}
6975
},
76+
mounted: function () {
77+
this.getFilesystemNetinfo()
78+
},
7079
methods: {
7180
async onSubmit(evt) {
7281
evt.preventDefault();
7382
7483
if (this.valid) {
75-
let mountTargetNetinfo = await this.getFilesystemNetinfo()
84+
let mountTargetNetinfo = this.mountTargetNetinfo
7685
7786
mountTargetNetinfo['uid'] = this.uid
7887
mountTargetNetinfo['gid'] = this.gid
@@ -115,7 +124,7 @@ export default {
115124
try {
116125
let response = await API.get('fileManagerApi', '/api/filesystems/' + this.$route.params.id + '/netinfo')
117126
let formattedNetinfo = this.formatNetinfo(response)
118-
return formattedNetinfo
127+
this.mountTargetNetinfo = formattedNetinfo
119128
}
120129
catch (error) {
121130
alert('Unable to retrieve filesystem netinfo, check api logs')

test/unit/api/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,12 @@ def cfn_client_stub(mock_env_variables):
3232
def lambda_client_stub(mock_env_variables):
3333
from app import SERVERLESS
3434
with Stubber(SERVERLESS) as stubber:
35+
yield stubber
36+
stubber.assert_no_pending_responses()
37+
38+
@pytest.fixture
39+
def ec2_client_stub(mock_env_variables):
40+
from app import EC2
41+
with Stubber(EC2) as stubber:
3542
yield stubber
3643
stubber.assert_no_pending_responses()

test/unit/api/service_responses.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,73 @@
66

77
test_stack_name = f'testStackPrefix-ManagedResources-{test_filesystem_id}'
88

9+
ec2_describe_security_group_rules_response = {
10+
'SecurityGroupRules': [{
11+
'SecurityGroupRuleId': 'sgr-0123abc',
12+
'GroupId': 'sg-4567abcd',
13+
'GroupOwnerId': '',
14+
'IsEgress': False,
15+
'IpProtocol': '-1',
16+
'FromPort': -1,
17+
'ToPort': -1,
18+
'ReferencedGroupInfo': {
19+
'GroupId': 'sg-4567abcd',
20+
'UserId': ''
21+
},
22+
'Tags': []
23+
}, {
24+
'SecurityGroupRuleId': 'sgr-0123abc',
25+
'GroupId': 'sg-4567abcd',
26+
'GroupOwnerId': '',
27+
'IsEgress': False,
28+
'IpProtocol': 'tcp',
29+
'FromPort': 2049,
30+
'ToPort': 2049,
31+
'ReferencedGroupInfo': {
32+
'GroupId': 'sg-4567abcd',
33+
'UserId': ''
34+
},
35+
'Tags': []
36+
}, {
37+
'SecurityGroupRuleId': 'sgr-0123abc',
38+
'GroupId': 'sg-4567abcd',
39+
'GroupOwnerId': '',
40+
'IsEgress': True,
41+
'IpProtocol': '-1',
42+
'FromPort': -1,
43+
'ToPort': -1,
44+
'CidrIpv4': '0.0.0.0/0',
45+
'Tags': []
46+
},
47+
{
48+
'SecurityGroupRuleId': 'sgr-0123abc',
49+
'GroupId': 'sg-4567abcd',
50+
'GroupOwnerId': '',
51+
'IsEgress': False,
52+
'IpProtocol': '-1',
53+
'FromPort': -1,
54+
'ToPort': -1,
55+
'CidrIpv4': '0.0.0.0/0',
56+
'Tags': []
57+
}
58+
],
59+
'ResponseMetadata': {
60+
'RequestId': '',
61+
'HTTPStatusCode': 200,
62+
'HTTPHeaders': {
63+
'x-amzn-requestid': '',
64+
'cache-control': 'no-cache, no-store',
65+
'strict-transport-security': 'max-age=31536000; includeSubDomains',
66+
'vary': 'accept-encoding',
67+
'content-type': 'text/xml;charset=UTF-8',
68+
'transfer-encoding': 'chunked',
69+
'date': 'Wed, 21 Sep 2022 20:02:50 GMT',
70+
'server': 'AmazonEC2'
71+
},
72+
'RetryAttempts': 0
73+
}
74+
}
75+
976
efs_describe_file_systems_no_marker_response = {
1077
'FileSystems': [
1178
{
@@ -172,4 +239,5 @@
172239

173240
EFS = {'describe_file_systems_no_marker': efs_describe_file_systems_no_marker_response, 'describe_mount_targets': efs_describe_mount_targets_response, 'describe_mount_target_security_groups': efs_describe_mount_target_security_groups_response}
174241
CFN = {'describe_stacks': cfn_describe_stacks_response, 'create_stack': cfn_create_stack_response}
175-
LAMBDA = {'upload': lambda_invoke_upload_response, 'delete': lambda_invoke_delete_response, 'list': lambda_invoke_list_response, 'make_dir': lambda_invoke_make_dir_response, 'download': lambda_invoke_download_response}
242+
LAMBDA = {'upload': lambda_invoke_upload_response, 'delete': lambda_invoke_delete_response, 'list': lambda_invoke_list_response, 'make_dir': lambda_invoke_make_dir_response, 'download': lambda_invoke_download_response}
243+
EC2 = {'describe_sec_rules': ec2_describe_security_group_rules_response}

test/unit/api/test_app.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import botocore.stub
33

4-
from service_responses import EFS, CFN, LAMBDA
4+
from service_responses import EFS, CFN, LAMBDA, EC2
55

66
test_filesystem_id = 'fs-01234567'
77

@@ -46,7 +46,7 @@ def test_list_filesystems(test_client, efs_client_stub, cfn_client_stub):
4646
print('PASS')
4747

4848

49-
def test_get_netinfo_for_filesystem(test_client, efs_client_stub):
49+
def test_get_netinfo_for_filesystem(test_client, efs_client_stub, ec2_client_stub):
5050
print(f'GET /filesystems/{test_filesystem_id}/netinfo')
5151

5252
efs_client_stub.add_response(
@@ -61,6 +61,12 @@ def test_get_netinfo_for_filesystem(test_client, efs_client_stub):
6161
service_response=EFS['describe_mount_target_security_groups']
6262
)
6363

64+
ec2_client_stub.add_response(
65+
'describe_security_group_rules',
66+
expected_params={'Filters': [{'Name': 'group-id', 'Values': ['sg-4567abcd']}]},
67+
service_response=EC2['describe_sec_rules']
68+
)
69+
6470
response = test_client.http.get(f'/filesystems/{test_filesystem_id}/netinfo')
6571

6672
formatted_response = json.loads(response.body)

0 commit comments

Comments
 (0)