__init__.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. # -*- coding: utf-8 -*-
  2. import sys
  3. import os.path
  4. import json
  5. import fnmatch
  6. from collections import Counter, OrderedDict
  7. import boto3
  8. import botocore
  9. import click
  10. from joblib import Parallel, delayed
  11. from clint.textui import colored, puts, indent
  12. from .checks import AclCheck, PolicyCheck, PublicAccessCheck, LoggingCheck, VersioningCheck, EncryptionCheck, ObjectLoggingCheck
  13. __version__ = '0.3.1'
  14. canned_acls = [
  15. {
  16. 'acl': 'private',
  17. 'grants': []
  18. },
  19. {
  20. 'acl': 'public-read',
  21. 'grants': [
  22. {'Grantee': {'Type': 'Group', 'URI': 'http://acs.amazonaws.com/groups/global/AllUsers'}, 'Permission': 'READ'}
  23. ]
  24. },
  25. {
  26. 'acl': 'public-read-write',
  27. 'grants': [
  28. {'Grantee': {'Type': 'Group', 'URI': 'http://acs.amazonaws.com/groups/global/AllUsers'}, 'Permission': 'READ'},
  29. {'Grantee': {u'Type': 'Group', u'URI': 'http://acs.amazonaws.com/groups/global/AllUsers'}, 'Permission': 'WRITE'}
  30. ]
  31. },
  32. {
  33. 'acl': 'authenticated-read',
  34. 'grants': [
  35. {'Grantee': {'Type': 'Group', 'URI': 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers'}, 'Permission': 'READ'}
  36. ]
  37. },
  38. {
  39. 'acl': 'aws-exec-read',
  40. 'grants': [
  41. {'Grantee': {'Type': 'CanonicalUser', 'DisplayName': 'za-team', 'ID': '6aa5a366c34c1cbe25dc49211496e913e0351eb0e8c37aa3477e40942ec6b97c'}, 'Permission': 'READ'}
  42. ]
  43. }
  44. ]
  45. cached_s3 = None
  46. def s3():
  47. # memoize
  48. global cached_s3
  49. if cached_s3 is None:
  50. cached_s3 = boto3.resource('s3')
  51. return cached_s3
  52. def notice(message):
  53. puts(colored.yellow(message))
  54. def abort(message):
  55. puts(colored.red(message))
  56. sys.exit(1)
  57. def unicode_key(key):
  58. if sys.version_info[0] < 3 and isinstance(key, unicode):
  59. return key.encode('utf-8')
  60. else:
  61. return key
  62. def perform(check):
  63. check.perform()
  64. with indent(2):
  65. if check.status == 'passed':
  66. puts(colored.green('✔ ' + check.name + ' ' + unicode_key(check.pass_message)))
  67. elif check.status == 'failed':
  68. puts(colored.red('✘ ' + check.name + ' ' + check.fail_message))
  69. else:
  70. puts(colored.red('✘ ' + check.name + ' access denied'))
  71. return check
  72. def fetch_buckets(buckets):
  73. if buckets:
  74. if any('*' in b for b in buckets):
  75. return [b for b in s3().buckets.all() if any(fnmatch.fnmatch(b.name, bn) for bn in buckets)]
  76. else:
  77. return [s3().Bucket(bn) for bn in buckets]
  78. else:
  79. return s3().buckets.all()
  80. def fix_check(klass, buckets, dry_run, fix_args={}):
  81. for bucket in fetch_buckets(buckets):
  82. check = klass(bucket)
  83. check.perform()
  84. if check.status == 'passed':
  85. message = colored.green('already ' + check.pass_message)
  86. elif check.status == 'denied':
  87. message = colored.red('access denied')
  88. else:
  89. if dry_run:
  90. message = colored.yellow('to be ' + check.pass_message)
  91. else:
  92. try:
  93. check.fix(fix_args)
  94. message = colored.blue('just ' + check.pass_message)
  95. except botocore.exceptions.ClientError as e:
  96. message = colored.red(str(e))
  97. puts(bucket.name + ' ' + message)
  98. def encrypt_object(bucket_name, key, dry_run, kms_key_id, customer_key):
  99. obj = s3().Object(bucket_name, key)
  100. str_key = unicode_key(key)
  101. try:
  102. if customer_key:
  103. obj.load(SSECustomerAlgorithm='AES256', SSECustomerKey=customer_key)
  104. encrypted = None
  105. if customer_key:
  106. encrypted = obj.sse_customer_algorithm is not None
  107. elif kms_key_id:
  108. encrypted = obj.server_side_encryption == 'aws:kms'
  109. else:
  110. encrypted = obj.server_side_encryption == 'AES256'
  111. if encrypted:
  112. puts(str_key + ' ' + colored.green('already encrypted'))
  113. return 'already encrypted'
  114. else:
  115. if dry_run:
  116. puts(str_key + ' ' + colored.yellow('to be encrypted'))
  117. return 'to be encrypted'
  118. else:
  119. copy_source = {'Bucket': bucket_name, 'Key': obj.key}
  120. # TODO support going from customer encryption to other forms
  121. if kms_key_id:
  122. obj.copy_from(
  123. CopySource=copy_source,
  124. ServerSideEncryption='aws:kms',
  125. SSEKMSKeyId=kms_key_id
  126. )
  127. elif customer_key:
  128. obj.copy_from(
  129. CopySource=copy_source,
  130. SSECustomerAlgorithm='AES256',
  131. SSECustomerKey=customer_key
  132. )
  133. else:
  134. obj.copy_from(
  135. CopySource=copy_source,
  136. ServerSideEncryption='AES256'
  137. )
  138. puts(str_key + ' ' + colored.blue('just encrypted'))
  139. return 'just encrypted'
  140. except (botocore.exceptions.ClientError, botocore.exceptions.NoCredentialsError) as e:
  141. puts(str_key + ' ' + colored.red(str(e)))
  142. return 'error'
  143. def determine_mode(acl):
  144. owner = acl.owner
  145. grants = acl.grants
  146. non_owner_grants = [grant for grant in grants if not (grant['Grantee'].get('ID') == owner['ID'] and grant['Permission'] == 'FULL_CONTROL')]
  147. # TODO bucket-owner-read and bucket-owner-full-control
  148. return next((ca['acl'] for ca in canned_acls if ca['grants'] == non_owner_grants), 'custom')
  149. def scan_object(bucket_name, key):
  150. obj = s3().Object(bucket_name, key)
  151. str_key = unicode_key(key)
  152. try:
  153. mode = determine_mode(obj.Acl())
  154. if mode == 'private':
  155. puts(str_key + ' ' + colored.green(mode))
  156. else:
  157. puts(str_key + ' ' + colored.yellow(mode))
  158. return mode
  159. except (botocore.exceptions.ClientError, botocore.exceptions.NoCredentialsError) as e:
  160. puts(str_key + ' ' + colored.red(str(e)))
  161. return 'error'
  162. def reset_object(bucket_name, key, dry_run, acl):
  163. obj = s3().Object(bucket_name, key)
  164. str_key = unicode_key(key)
  165. try:
  166. obj_acl = obj.Acl()
  167. mode = determine_mode(obj_acl)
  168. if mode == acl:
  169. puts(str_key + ' ' + colored.green('ACL already ' + acl))
  170. return 'ACL already ' + acl
  171. elif dry_run:
  172. puts(str_key + ' ' + colored.yellow('ACL to be updated to ' + acl))
  173. return 'ACL to be updated to ' + acl
  174. else:
  175. obj_acl.put(ACL=acl)
  176. puts(str_key + ' ' + colored.blue('ACL updated to ' + acl))
  177. return 'ACL updated to ' + acl
  178. except (botocore.exceptions.ClientError, botocore.exceptions.NoCredentialsError) as e:
  179. puts(str_key + ' ' + colored.red(str(e)))
  180. return 'error'
  181. def delete_unencrypted_version(bucket_name, key, id, dry_run):
  182. object_version = s3().ObjectVersion(bucket_name, key, id)
  183. try:
  184. obj = object_version.get()
  185. if obj.get('ServerSideEncryption') or obj.get('SSECustomerAlgorithm'):
  186. puts(key + ' ' + id + ' ' + colored.green('encrypted'))
  187. return 'encrypted'
  188. else:
  189. if dry_run:
  190. puts(key + ' ' + id + ' ' + colored.blue('to be deleted'))
  191. return 'to be deleted'
  192. else:
  193. puts(key + ' ' + id + ' ' + colored.blue('deleted'))
  194. object_version.delete()
  195. return 'deleted'
  196. except (botocore.exceptions.ClientError, botocore.exceptions.NoCredentialsError) as e:
  197. puts(key + ' ' + id + ' ' + colored.red(str(e)))
  198. return 'error'
  199. def object_matches(key, only, _except):
  200. match = True
  201. if only:
  202. match = fnmatch.fnmatch(key, only)
  203. if _except and match:
  204. match = not fnmatch.fnmatch(key, _except)
  205. return match
  206. def parallelize(bucket, only, _except, fn, args=(), versions=False):
  207. bucket = s3().Bucket(bucket)
  208. # use prefix for performance
  209. prefix = None
  210. if only:
  211. # get the first prefix before wildcard
  212. prefix = '/'.join(only.split('*')[0].split('/')[:-1])
  213. if prefix:
  214. prefix = prefix + '/'
  215. if versions:
  216. object_versions = bucket.object_versions.filter(Prefix=prefix) if prefix else bucket.object_versions.all()
  217. # delete markers have no size
  218. return Parallel(n_jobs=24)(delayed(fn)(bucket.name, ov.object_key, ov.id, *args) for ov in object_versions if object_matches(ov.object_key, only, _except) and not ov.is_latest and ov.size is not None)
  219. else:
  220. objects = bucket.objects.filter(Prefix=prefix) if prefix else bucket.objects.all()
  221. if only and not '*' in only:
  222. objects = [s3().Object(bucket, only)]
  223. return Parallel(n_jobs=24)(delayed(fn)(bucket.name, os.key, *args) for os in objects if object_matches(os.key, only, _except))
  224. def public_statement(bucket):
  225. return OrderedDict([
  226. ('Sid', 'Public'),
  227. ('Effect', 'Allow'),
  228. ('Principal', '*'),
  229. ('Action', 's3:GetObject'),
  230. ('Resource', 'arn:aws:s3:::%s/*' % bucket.name)
  231. ])
  232. def no_object_acl_statement(bucket):
  233. return OrderedDict([
  234. ('Sid', 'NoObjectAcl'),
  235. ('Effect', 'Deny'),
  236. ('Principal', '*'),
  237. ('Action', 's3:PutObjectAcl'),
  238. ('Resource', 'arn:aws:s3:::%s/*' % bucket.name)
  239. ])
  240. def public_uploads_statement(bucket):
  241. return OrderedDict([
  242. ('Sid', 'PublicUploads'),
  243. ('Effect', 'Deny'),
  244. ('Principal', '*'),
  245. ('Action', ['s3:PutObject', 's3:PutObjectAcl']),
  246. ('Resource', 'arn:aws:s3:::%s/*' % bucket.name),
  247. ('Condition', {'StringNotEquals': {'s3:x-amz-acl': 'public-read'}})
  248. ])
  249. def no_uploads_statement(bucket):
  250. return OrderedDict([
  251. ('Sid', 'NoUploads'),
  252. ('Effect', 'Deny'),
  253. ('Principal', '*'),
  254. ('Action', 's3:PutObject'),
  255. ('Resource', 'arn:aws:s3:::%s/*' % bucket.name)
  256. ])
  257. def encryption_statement(bucket):
  258. return OrderedDict([
  259. ('Sid', 'Hash'),
  260. ('Effect', 'Deny'),
  261. ('Principal', '*'),
  262. ('Action', 's3:PutObject'),
  263. ('Resource', 'arn:aws:s3:::%s/*' % bucket.name),
  264. ('Condition', {'StringNotEquals': {'s3:x-amz-server-side-encryption': 'AES256'}})
  265. ])
  266. def statement_matches(s1, s2):
  267. s1 = dict(s1)
  268. s2 = dict(s2)
  269. s1.pop('Sid', None)
  270. s2.pop('Sid', None)
  271. return s1 == s2
  272. def fetch_policy(bucket):
  273. policy = None
  274. try:
  275. policy = bucket.Policy().policy
  276. except botocore.exceptions.ClientError as e:
  277. if 'NoSuchBucket' not in str(e):
  278. raise
  279. if policy:
  280. policy = json.loads(policy, object_pairs_hook=OrderedDict)
  281. return policy
  282. def print_dns_bucket(name, buckets, found_buckets):
  283. if not name in found_buckets:
  284. puts(name)
  285. with indent(2):
  286. if name in buckets:
  287. puts(colored.green('owned'))
  288. else:
  289. puts(colored.red('not owned'))
  290. puts()
  291. found_buckets.add(name)
  292. def print_policy(policy):
  293. with indent(2):
  294. if any(policy['Statement']):
  295. puts(colored.yellow(json.dumps(policy, indent=4)))
  296. else:
  297. puts(colored.yellow("None"))
  298. def summarize(values):
  299. summary = Counter(values)
  300. puts()
  301. puts("Summary")
  302. for k, v in summary.most_common():
  303. puts(k + ': ' + str(v))
  304. def fetch_event_selectors():
  305. # TODO get trails across all regions
  306. # even regions without buckets may have multi-region trails
  307. client = boto3.client('cloudtrail')
  308. paginator = client.get_paginator('list_trails')
  309. event_selectors = {}
  310. for page in paginator.paginate():
  311. for trail in page['Trails']:
  312. name = trail['Name']
  313. region_client = boto3.client('cloudtrail', region_name=trail['HomeRegion'])
  314. response = region_client.get_event_selectors(TrailName=name)
  315. for event_selector in response['EventSelectors']:
  316. read_write_type = event_selector['ReadWriteType']
  317. for data_resource in event_selector['DataResources']:
  318. if data_resource['Type'] == 'AWS::S3::Object':
  319. for value in data_resource['Values']:
  320. if value == 'arn:aws:s3':
  321. trail_response = region_client.get_trail(Name=name)['Trail']
  322. if trail_response['IsMultiRegionTrail']:
  323. bucket = ('global')
  324. else:
  325. bucket = ('region', trail['HomeRegion'])
  326. path = ''
  327. else:
  328. parts = value.split("/", 2)
  329. bucket = ('bucket', parts[0].replace('arn:aws:s3:::', ''))
  330. path = parts[1]
  331. if bucket not in event_selectors:
  332. event_selectors[bucket] = []
  333. event_selectors[bucket].append({'trail': name, 'path': path, 'read_write_type': read_write_type})
  334. return event_selectors
  335. @click.group()
  336. @click.version_option(version=__version__)
  337. def cli():
  338. pass
  339. @cli.command()
  340. @click.argument('buckets', nargs=-1)
  341. @click.option('--log-bucket', multiple=True, help='Check log bucket(s)')
  342. @click.option('--log-prefix', help='Check log prefix')
  343. @click.option('--skip-logging', is_flag=True, help='Skip logging check')
  344. @click.option('--skip-versioning', is_flag=True, help='Skip versioning check')
  345. @click.option('--skip-default-encryption', is_flag=True, help='Skip default encryption check')
  346. @click.option('--default-encryption', is_flag=True) # no op, can't hide from help until click 7 released
  347. @click.option('--object-level-logging', is_flag=True)
  348. @click.option('--sns-topic', help='Send SNS notification for failures')
  349. def scan(buckets, log_bucket=None, log_prefix=None, skip_logging=False, skip_versioning=False, skip_default_encryption=False, default_encryption=True, object_level_logging=False, sns_topic=None):
  350. event_selectors = fetch_event_selectors() if object_level_logging else {}
  351. checks = []
  352. for bucket in fetch_buckets(buckets):
  353. puts(bucket.name)
  354. checks.append(perform(AclCheck(bucket)))
  355. checks.append(perform(PolicyCheck(bucket)))
  356. checks.append(perform(PublicAccessCheck(bucket)))
  357. if not skip_logging:
  358. checks.append(perform(LoggingCheck(bucket, log_bucket=log_bucket, log_prefix=log_prefix)))
  359. if not skip_versioning:
  360. checks.append(perform(VersioningCheck(bucket)))
  361. if not skip_default_encryption:
  362. checks.append(perform(EncryptionCheck(bucket)))
  363. if object_level_logging:
  364. checks.append(perform(ObjectLoggingCheck(bucket, event_selectors=event_selectors)))
  365. puts()
  366. failed_checks = [c for c in checks if c.status != 'passed']
  367. if any(failed_checks):
  368. if sns_topic:
  369. topic = boto3.resource('sns').Topic(sns_topic)
  370. message = ''
  371. for check in failed_checks:
  372. msg = check.fail_message if check.status == 'failed' else 'access denied'
  373. message += check.bucket.name + ': ' + check.name + ' ' + msg + '\n'
  374. topic.publish(Message=message, Subject='[s3tk] Scan Failures')
  375. sys.exit(1)
  376. @cli.command(name='scan-dns')
  377. def scan_dns():
  378. buckets = set([b.name for b in s3().buckets.all()])
  379. found_buckets = set()
  380. client = boto3.client('route53')
  381. paginator = client.get_paginator('list_hosted_zones')
  382. for page in paginator.paginate():
  383. for hosted_zone in page['HostedZones']:
  384. paginator2 = client.get_paginator('list_resource_record_sets')
  385. for page2 in paginator2.paginate(HostedZoneId=hosted_zone['Id']):
  386. for resource_set in page2['ResourceRecordSets']:
  387. if resource_set.get('AliasTarget'):
  388. value = resource_set['AliasTarget']['DNSName']
  389. if value.startswith('s3-website-') and value.endswith('.amazonaws.com.'):
  390. print_dns_bucket(resource_set['Name'][:-1], buckets, found_buckets)
  391. elif resource_set.get('ResourceRecords'):
  392. for record in resource_set['ResourceRecords']:
  393. value = record['Value']
  394. if value.endswith('.s3.amazonaws.com'):
  395. print_dns_bucket('.'.join(value.split('.')[:-3]), buckets, found_buckets)
  396. if 's3-website-' in value and value.endswith('.amazonaws.com'):
  397. print_dns_bucket(resource_set['Name'][:-1], buckets, found_buckets)
  398. @cli.command(name='block-public-access')
  399. @click.argument('buckets', nargs=-1)
  400. @click.option('--dry-run', is_flag=True, help='Dry run')
  401. def block_public_access(buckets, dry_run=False):
  402. if not buckets:
  403. abort('Must specify at least one bucket or wildcard')
  404. fix_check(PublicAccessCheck, buckets, dry_run)
  405. @cli.command(name='enable-logging')
  406. @click.argument('buckets', nargs=-1)
  407. @click.option('--dry-run', is_flag=True, help='Dry run')
  408. @click.option('--log-bucket', required=True, help='Bucket to store logs')
  409. @click.option('--log-prefix', help='Log prefix')
  410. def enable_logging(buckets, log_bucket=None, log_prefix=None, dry_run=False):
  411. fix_check(LoggingCheck, buckets, dry_run, {'log_bucket': log_bucket, 'log_prefix': log_prefix})
  412. @cli.command(name='enable-versioning')
  413. @click.argument('buckets', nargs=-1)
  414. @click.option('--dry-run', is_flag=True, help='Dry run')
  415. def enable_versioning(buckets, dry_run=False):
  416. fix_check(VersioningCheck, buckets, dry_run)
  417. @cli.command(name='enable-default-encryption')
  418. @click.argument('buckets', nargs=-1)
  419. @click.option('--dry-run', is_flag=True, help='Dry run')
  420. def enable_versioning(buckets, dry_run=False):
  421. fix_check(EncryptionCheck, buckets, dry_run)
  422. @cli.command()
  423. @click.argument('bucket')
  424. @click.option('--only', help='Only certain objects')
  425. @click.option('--except', '_except', help='Except certain objects')
  426. @click.option('--dry-run', is_flag=True, help='Dry run')
  427. @click.option('--kms-key-id', help='KMS key id')
  428. @click.option('--customer-key', help='Customer key')
  429. def encrypt(bucket, only=None, _except=None, dry_run=False, kms_key_id=None, customer_key=None):
  430. summarize(parallelize(bucket, only, _except, encrypt_object, (dry_run, kms_key_id, customer_key,)))
  431. @cli.command(name='scan-object-acl')
  432. @click.argument('bucket')
  433. @click.option('--only', help='Only certain objects')
  434. @click.option('--except', '_except', help='Except certain objects')
  435. def scan_object_acl(bucket, only=None, _except=None):
  436. summarize(parallelize(bucket, only, _except, scan_object))
  437. @cli.command(name='reset-object-acl')
  438. @click.argument('bucket')
  439. @click.option('--only', help='Only certain objects')
  440. @click.option('--except', '_except', help='Except certain objects')
  441. @click.option('--acl', default='private', help='ACL to use')
  442. @click.option('--dry-run', is_flag=True, help='Dry run')
  443. def reset_object_acl(bucket, only=None, _except=None, acl=None, dry_run=False):
  444. summarize(parallelize(bucket, only, _except, reset_object, (dry_run, acl,)))
  445. @cli.command(name='delete-unencrypted-versions')
  446. @click.argument('bucket')
  447. @click.option('--only', help='Only certain objects')
  448. @click.option('--except', '_except', help='Except certain objects')
  449. @click.option('--dry-run', is_flag=True, help='Dry run')
  450. def delete_unencrypted_versions(bucket, only=None, _except=None, dry_run=False):
  451. summarize(parallelize(bucket, only, _except, delete_unencrypted_version, (dry_run,), True))
  452. @cli.command(name='list-policy')
  453. @click.argument('buckets', nargs=-1)
  454. @click.option('--named', is_flag=True, help='Print named statements')
  455. def list_policy(buckets, named=False):
  456. for bucket in fetch_buckets(buckets):
  457. puts(bucket.name)
  458. policy = fetch_policy(bucket)
  459. with indent(2):
  460. if policy is None:
  461. puts(colored.yellow('None'))
  462. else:
  463. if named:
  464. public = public_statement(bucket)
  465. no_object_acl = no_object_acl_statement(bucket)
  466. public_uploads = public_uploads_statement(bucket)
  467. no_uploads = no_uploads_statement(bucket)
  468. encryption = encryption_statement(bucket)
  469. for statement in policy['Statement']:
  470. if statement_matches(statement, public):
  471. named_statement = 'Public'
  472. elif statement_matches(statement, no_object_acl):
  473. named_statement = 'No object ACL'
  474. elif statement_matches(statement, public_uploads):
  475. named_statement = 'Public uploads'
  476. elif statement_matches(statement, no_uploads):
  477. named_statement = 'No uploads'
  478. elif statement_matches(statement, encryption):
  479. named_statement = 'Hash'
  480. else:
  481. named_statement = 'Custom'
  482. puts(colored.yellow(named_statement))
  483. else:
  484. puts(colored.yellow(json.dumps(policy, indent=4)))
  485. puts()
  486. @cli.command(name='set-policy')
  487. @click.argument('bucket')
  488. @click.option('--public', is_flag=True, help='Make all objects public')
  489. @click.option('--no-object-acl', is_flag=True, help='Prevent object ACL')
  490. @click.option('--public-uploads', is_flag=True, help='Only public uploads')
  491. @click.option('--no-uploads', is_flag=True, help='Prevent new uploads')
  492. @click.option('--encryption', is_flag=True, help='Require encryption')
  493. @click.option('--dry-run', is_flag=True, help='Dry run')
  494. def set_policy(bucket, public=False, no_object_acl=False, public_uploads=False, no_uploads=False, encryption=False, dry_run=False):
  495. bucket = s3().Bucket(bucket)
  496. bucket_policy = bucket.Policy()
  497. statements = []
  498. if public:
  499. statements.append(public_statement(bucket))
  500. if no_object_acl:
  501. statements.append(no_object_acl_statement(bucket))
  502. if public_uploads:
  503. statements.append(public_uploads_statement(bucket))
  504. if no_uploads:
  505. statements.append(no_uploads_statement(bucket))
  506. if encryption:
  507. statements.append(encryption_statement(bucket))
  508. if any(statements):
  509. puts('New policy')
  510. policy = OrderedDict([
  511. ('Version', '2012-10-17'),
  512. ('Statement', statements)
  513. ])
  514. print_policy(policy)
  515. if not dry_run:
  516. bucket_policy.put(Policy=json.dumps(policy))
  517. else:
  518. abort('No policies specified')
  519. # experimental
  520. @cli.command(name='update-policy')
  521. @click.argument('bucket')
  522. @click.option('--encryption/--no-encryption', default=None, help='Require encryption')
  523. @click.option('--dry-run', is_flag=True, help='Dry run')
  524. def update_policy(bucket, encryption=None, dry_run=False):
  525. bucket = s3().Bucket(bucket)
  526. policy = fetch_policy(bucket)
  527. if not policy:
  528. policy = OrderedDict([
  529. ('Version', '2012-10-17'),
  530. ('Statement', [])
  531. ])
  532. es = encryption_statement(bucket)
  533. es_index = next((i for i, s in enumerate(policy['Statement']) if statement_matches(s, es)), -1)
  534. if es_index != -1:
  535. if encryption:
  536. puts("No encryption change")
  537. print_policy(policy)
  538. elif encryption is False:
  539. puts("Removing encryption")
  540. policy['Statement'].pop(es_index)
  541. print_policy(policy)
  542. if not dry_run:
  543. if any(policy['Statement']):
  544. bucket.Policy().put(Policy=json.dumps(policy))
  545. else:
  546. bucket.Policy().delete()
  547. else:
  548. if encryption:
  549. puts("Adding encryption")
  550. policy['Statement'].append(es)
  551. print_policy(policy)
  552. if not dry_run:
  553. bucket.Policy().put(Policy=json.dumps(policy))
  554. elif encryption is False:
  555. puts(colored.yellow("No encryption change"))
  556. print_policy(policy)
  557. @cli.command(name='delete-policy')
  558. @click.argument('bucket')
  559. def delete_policy(bucket):
  560. s3().Bucket(bucket).Policy().delete()
  561. puts('Policy deleted')