Skip to content

Commit dd1e5b1

Browse files
committed
Add SFTP service bucket to those accessible by the application
Ended up having to move some of the sftp code into the assets module, like the sftp bucket, to resolve an otherwise circular dependency between assets and sftp.
1 parent 4e92b64 commit dd1e5b1

File tree

3 files changed

+215
-250
lines changed

3 files changed

+215
-250
lines changed

stack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import os
22

3+
from . import sftp # noqa: F401
34
from . import assets # noqa: F401
45
from . import cache # noqa: F401
56
from . import database # noqa: F401
67
from . import logs # noqa: F401
78
from . import vpc # noqa: F401
8-
from . import sftp # noqa: F401
99
from . import template
1010

1111
if os.environ.get('USE_GOVCLOUD') != 'on':

stack/assets.py

Lines changed: 214 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Output,
1414
Ref,
1515
Split,
16+
Condition,
1617
)
1718
from troposphere.certificatemanager import Certificate, DomainValidationOption
1819
from troposphere.cloudfront import (
@@ -43,6 +44,7 @@
4344
use_cmk_arn
4445
)
4546
from .domain import domain_name, domain_name_alternates, no_alt_domains
47+
from .sftp import use_sftp_condition, use_sftp_with_kms_condition
4648
from .template import template
4749
from .utils import ParameterWithDefaults as Parameter
4850

@@ -122,11 +124,13 @@
122124

123125

124126
# Output S3 asset bucket name
125-
template.add_output(Output(
126-
"AssetsBucketDomainName",
127-
Description="Assets bucket domain name",
128-
Value=GetAtt(assets_bucket, "DomainName")
129-
))
127+
template.add_output(
128+
Output(
129+
"AssetsBucketDomainName",
130+
Description="Assets bucket domain name",
131+
Value=GetAtt(assets_bucket, "DomainName"),
132+
)
133+
)
130134

131135

132136
# Create an S3 bucket that holds user uploads or other non-public files
@@ -158,41 +162,104 @@
158162
)
159163
)
160164

165+
# Output S3 private assets bucket name
166+
template.add_output(
167+
Output(
168+
"PrivateAssetsBucketDomainName",
169+
Description="Private assets bucket domain name",
170+
Value=GetAtt(private_assets_bucket, "DomainName"),
171+
)
172+
)
161173

162-
# Output S3 asset bucket name
163-
template.add_output(Output(
164-
"PrivateAssetsBucketDomainName",
165-
Description="Private assets bucket domain name",
166-
Value=GetAtt(private_assets_bucket, "DomainName")
167-
))
174+
# Bucket for SFTP service
175+
sftp_assets_bucket = Bucket(
176+
"SFTPAssetsBucket",
177+
Condition=use_sftp_condition,
178+
AccessControl=Private,
179+
PublicAccessBlockConfiguration=PublicAccessBlockConfiguration(
180+
BlockPublicAcls=True,
181+
BlockPublicPolicy=True,
182+
IgnorePublicAcls=True,
183+
RestrictPublicBuckets=True,
184+
),
185+
BucketEncryption=If(
186+
use_aes256_encryption_cond,
187+
BucketEncryption(
188+
ServerSideEncryptionConfiguration=[
189+
ServerSideEncryptionRule(
190+
ServerSideEncryptionByDefault=ServerSideEncryptionByDefault(
191+
SSEAlgorithm=If(use_cmk_arn, "aws:kms", "AES256"),
192+
KMSMasterKeyID=If(
193+
use_cmk_arn, Ref(cmk_arn), Ref("AWS::NoValue")
194+
),
195+
)
196+
)
197+
]
198+
),
199+
NoValue,
200+
),
201+
**common_bucket_conf,
202+
)
203+
template.add_resource(sftp_assets_bucket)
204+
205+
# Output SFTP asset bucket name
206+
template.add_output(
207+
Output(
208+
"SFTPBucketDomainName",
209+
Condition=use_sftp_condition,
210+
Description="SFTP bucket domain name",
211+
Value=GetAtt(sftp_assets_bucket, "DomainName"),
212+
)
213+
)
214+
215+
assets_management_policy_statements = [
216+
dict(
217+
Effect="Allow",
218+
Action=["s3:ListBucket"],
219+
Resource=Join("", [arn_prefix, ":s3:::", Ref(assets_bucket)]),
220+
),
221+
dict(
222+
Effect="Allow",
223+
Action=["s3:*"],
224+
Resource=Join("", [arn_prefix, ":s3:::", Ref(assets_bucket), "/*"]),
225+
),
226+
dict(
227+
Effect="Allow",
228+
Action=["s3:ListBucket"],
229+
Resource=Join("", [arn_prefix, ":s3:::", Ref(private_assets_bucket)]),
230+
),
231+
dict(
232+
Effect="Allow",
233+
Action=["s3:*"],
234+
Resource=Join("", [arn_prefix, ":s3:::", Ref(private_assets_bucket), "/*"]),
235+
),
236+
]
168237

238+
assets_management_policy_statements_including_sftp_bucket = (
239+
assets_management_policy_statements
240+
+ [
241+
dict(
242+
Effect="Allow",
243+
Action=["s3:ListBucket"],
244+
Resource=Join("", [arn_prefix, ":s3:::", Ref(sftp_assets_bucket)]),
245+
),
246+
dict(
247+
Effect="Allow",
248+
Action=["s3:*"],
249+
Resource=Join("", [arn_prefix, ":s3:::", Ref(sftp_assets_bucket), "/*"]),
250+
),
251+
]
252+
)
169253

170254
# central asset management policy for use in instance roles
171255
assets_management_policy = iam.Policy(
172256
PolicyName="AssetsManagementPolicy",
173257
PolicyDocument=dict(
174-
Statement=[
175-
dict(
176-
Effect="Allow",
177-
Action=["s3:ListBucket"],
178-
Resource=Join("", [arn_prefix, ":s3:::", Ref(assets_bucket)]),
179-
),
180-
dict(
181-
Effect="Allow",
182-
Action=["s3:*"],
183-
Resource=Join("", [arn_prefix, ":s3:::", Ref(assets_bucket), "/*"]),
184-
),
185-
dict(
186-
Effect="Allow",
187-
Action=["s3:ListBucket"],
188-
Resource=Join("", [arn_prefix, ":s3:::", Ref(private_assets_bucket)]),
189-
),
190-
dict(
191-
Effect="Allow",
192-
Action=["s3:*"],
193-
Resource=Join("", [arn_prefix, ":s3:::", Ref(private_assets_bucket), "/*"]),
194-
),
195-
],
258+
Statement=If(
259+
Condition(use_sftp_condition),
260+
assets_management_policy_statements_including_sftp_bucket,
261+
assets_management_policy_statements,
262+
)
196263
),
197264
)
198265

@@ -314,11 +381,119 @@
314381
)
315382

316383
# Output CloudFront url
317-
template.add_output(Output(
318-
"AssetsDistributionDomainName",
319-
Description="The assets CDN domain name",
320-
Value=GetAtt(distribution, "DomainName"),
321-
Condition=assets_use_cloudfront_condition,
322-
))
384+
template.add_output(
385+
Output(
386+
"AssetsDistributionDomainName",
387+
Description="The assets CDN domain name",
388+
Value=GetAtt(distribution, "DomainName"),
389+
Condition=assets_use_cloudfront_condition,
390+
)
391+
)
323392
else:
324393
distribution = None
394+
395+
# The scopedown policy is used to restrict a user's access to the parts of the bucket
396+
# we don't want them to access.
397+
common_sftp_scopedown_policy_statements = [
398+
{
399+
"Sid": "AllowListingOfSFTPUserFolder",
400+
"Action": ["s3:ListBucket"],
401+
"Effect": "Allow",
402+
"Resource": ["arn:aws:s3:::${transfer:HomeBucket}"],
403+
"Condition": {
404+
"StringLike": {
405+
"s3:prefix": ["${transfer:UserName}/*", "${transfer:UserName}"]
406+
}
407+
},
408+
},
409+
{
410+
"Sid": "HomeDirObjectAccess",
411+
"Effect": "Allow",
412+
"Action": [
413+
"s3:PutObject",
414+
"s3:GetObject",
415+
"s3:DeleteObjectVersion",
416+
"s3:DeleteObject",
417+
"s3:GetObjectVersion",
418+
],
419+
"Resource": [
420+
Join("/", [GetAtt(sftp_assets_bucket, "Arn"), "${transfer:UserName}"]),
421+
Join("/", [GetAtt(sftp_assets_bucket, "Arn"), "${transfer:UserName}/*"]),
422+
],
423+
},
424+
]
425+
426+
sftp_kms_policy_statement = dict(
427+
Effect="Allow",
428+
Action=["kms:DescribeKey", "kms:GenerateDataKey", "kms:Encrypt", "kms:Decrypt"],
429+
Resource=Ref(cmk_arn),
430+
)
431+
432+
sftp_scopedown_policy = iam.ManagedPolicy(
433+
# This is for applying when adding users to the transfer server. It's not used directly in the stack creation,
434+
# other than adding it to IAM for later use.
435+
"SFTPUserScopeDownPolicy",
436+
PolicyDocument=dict(
437+
Version="2012-10-17",
438+
Statement=If(
439+
use_sftp_with_kms_condition,
440+
common_sftp_scopedown_policy_statements + [sftp_kms_policy_statement],
441+
common_sftp_scopedown_policy_statements,
442+
),
443+
),
444+
)
445+
template.add_resource(sftp_scopedown_policy)
446+
447+
# The ROLE is applied to users to let them access the bucket in general,
448+
# without regart to who they are.
449+
common_sftp_user_role_statements = [
450+
dict(
451+
Effect="Allow",
452+
Action=["s3:ListBucket", "s3:GetBucketLocation"],
453+
Resource=Join("", [arn_prefix, ":s3:::", Ref(sftp_assets_bucket)]),
454+
),
455+
dict(
456+
Effect="Allow",
457+
Action=[
458+
"s3:PutObject",
459+
"s3:GetObject",
460+
"s3:DeleteObject",
461+
"s3:DeleteObjectVersion",
462+
"s3:GetObjectVersion",
463+
"s3:GetObjectACL",
464+
"s3:PutObjectACL",
465+
],
466+
Resource=Join("", [arn_prefix, ":s3:::", Ref(sftp_assets_bucket), "/*"]),
467+
),
468+
]
469+
470+
sftp_user_role = iam.Role(
471+
# This also is not used directly during the stack setup, but is put into IAM
472+
# to be used later when adding users to the transfer server.
473+
"SFTPUserRole",
474+
template=template,
475+
AssumeRolePolicyDocument=dict(
476+
Statement=[
477+
dict(
478+
Effect="Allow",
479+
Principal=dict(Service=["transfer.amazonaws.com"]),
480+
Action=["sts:AssumeRole"],
481+
)
482+
]
483+
),
484+
Policies=[
485+
iam.Policy(
486+
"SFTPSUserRolePolicy",
487+
PolicyName="SFTPSUserRolePolicy",
488+
PolicyDocument=dict(
489+
Version="2012-10-17",
490+
Statement=If(
491+
use_sftp_with_kms_condition,
492+
common_sftp_user_role_statements + [sftp_kms_policy_statement],
493+
common_sftp_user_role_statements,
494+
),
495+
),
496+
)
497+
],
498+
RoleName=Join("-", [Ref("AWS::StackName"), "SFTPUserRole"]),
499+
)

0 commit comments

Comments
 (0)