From dc57f34f42d2e9d15c78474911965cf961c3ef1d Mon Sep 17 00:00:00 2001 From: marsteg Date: Sat, 5 Apr 2025 14:38:48 +0200 Subject: [PATCH 1/8] added listpolicy and listacl commands --- S3/S3.py | 8 ++++++++ s3cmd | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/S3/S3.py b/S3/S3.py index 26c516fd..4afd6133 100644 --- a/S3/S3.py +++ b/S3/S3.py @@ -1224,6 +1224,14 @@ def set_policy(self, uri, policy): response = self.send_request(request) return response + def list_policy(self, uri): + headers = SortedDict(ignore_case = True) + headers['content-type'] = 'application/json' + request = self.create_request("BUCKET_LIST", uri = uri, + headers=headers, uri_params = {'policy': None}) + response = self.send_request(request) + return response + def delete_policy(self, uri): request = self.create_request("BUCKET_DELETE", uri = uri, uri_params = {'policy': None}) diff --git a/s3cmd b/s3cmd index e3b526fa..210f628f 100755 --- a/s3cmd +++ b/s3cmd @@ -2171,6 +2171,19 @@ def cmd_sync(args): return cmd_sync_remote2remote(args) raise ParameterError("Invalid source/destination: '%s'" % "' '".join(args)) +def cmd_listacl(args): + cfg = Config() + s3 = S3(cfg) + if len(args) != 1: + raise ParameterError("Too few parameters! Expected: ") + if not S3Uri(args[0]).has_bucket(): + raise ParameterError("Invalid bucket name: '%s'" % args[0]) + if not S3Uri(args[0]).has_object(): + raise ParameterError("Invalid object name: '%s'" % args[0]) + uri = S3Uri(args[0]) + acl = list_acl(s3, uri) + return acl + def cmd_setacl(args): cfg = Config() s3 = S3(cfg) @@ -2346,6 +2359,18 @@ def cmd_setpolicy(args): output(u"%s: Policy updated" % uri) return EX_OK +def cmd_listpolicy(args): + cfg = Config() + s3 = S3(cfg) + uri = S3Uri(args[0]) + + response = s3.list_policy(uri) + + debug(u"response - %s" % response['status']) + if response['status'] == 200: + output(u"%s: Policy listed" % uri) + return EX_OK + def cmd_delpolicy(args): cfg = Config() s3 = S3(cfg) @@ -2980,6 +3005,7 @@ def get_commands_list(): {"cmd":"cp", "label":"Copy object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_cp, "argc":2}, {"cmd":"modify", "label":"Modify object metadata", "param":"s3://BUCKET1/OBJECT", "func":cmd_modify, "argc":1}, {"cmd":"mv", "label":"Move object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_mv, "argc":2}, + {"cmd":"listacl", "label":"List Access control list for Bucket or Files", "param":"s3://BUCKET[/OBJECT]", "func":cmd_listacl, "argc":0}, {"cmd":"setacl", "label":"Modify Access control list for Bucket or Files", "param":"s3://BUCKET[/OBJECT]", "func":cmd_setacl, "argc":1}, {"cmd":"setversioning", "label":"Modify Bucket Versioning", "param":"s3://BUCKET enable|disable", "func":cmd_setversioning, "argc":2}, {"cmd":"setownership", "label":"Modify Bucket Object Ownership", "param":"s3://BUCKET BucketOwnerPreferred|BucketOwnerEnforced|ObjectWriter", "func":cmd_setownership, "argc":2}, @@ -2989,6 +3015,7 @@ def get_commands_list(): {"cmd":"setobjectretention", "label":"Modify Object Retention", "param":"MODE RETAIN_UNTIL_DATE s3://BUCKET/OBJECT", "func":cmd_setobjectretention, "argc":3}, {"cmd":"setpolicy", "label":"Modify Bucket Policy", "param":"FILE s3://BUCKET", "func":cmd_setpolicy, "argc":2}, + {"cmd":"listpolicy", "label":"List Bucket Policy", "param":"FILE s3://BUCKET", "func":cmd_listpolicy, "argc":0}, {"cmd":"delpolicy", "label":"Delete Bucket Policy", "param":"s3://BUCKET", "func":cmd_delpolicy, "argc":1}, {"cmd":"setcors", "label":"Modify Bucket CORS", "param":"FILE s3://BUCKET", "func":cmd_setcors, "argc":2}, {"cmd":"delcors", "label":"Delete Bucket CORS", "param":"s3://BUCKET", "func":cmd_delcors, "argc":1}, @@ -3041,6 +3068,11 @@ def format_commands(progname, commands_list): help += " %s\n %s %s %s\n" % (cmd["label"], progname, cmd["cmd"], cmd["param"]) return help +def list_acl(s3, uri): + cfg = Config() + acl = s3.get_acl(uri) + output(u"%s: ACL Listed" % uri) + return acl def update_acl(s3, uri, seq_label=""): cfg = Config() From 9f36970a5f28d3954c361395ff2b4b2ebe2764af Mon Sep 17 00:00:00 2001 From: marsteg Date: Sat, 5 Apr 2025 14:49:33 +0200 Subject: [PATCH 2/8] added more checks for the list Policy function --- s3cmd | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/s3cmd b/s3cmd index 210f628f..bcd24bc2 100755 --- a/s3cmd +++ b/s3cmd @@ -2364,6 +2364,13 @@ def cmd_listpolicy(args): s3 = S3(cfg) uri = S3Uri(args[0]) + if len(args) != 1: + raise ParameterError("Too few parameters! Expected: ") + if not uri.has_bucket(): + raise ParameterError("Invalid bucket name: '%s'" % args[0]) + if uri.has_object(): + raise ParameterError("No Object Names allowed: '%s'" % args[0]) + response = s3.list_policy(uri) debug(u"response - %s" % response['status']) From c9d07a21181a73b1698e0008ec1840a1f3d46567 Mon Sep 17 00:00:00 2001 From: marsteg Date: Sat, 5 Apr 2025 15:07:49 +0200 Subject: [PATCH 3/8] removed a false check on cmd_listacl --- s3cmd | 2 -- 1 file changed, 2 deletions(-) diff --git a/s3cmd b/s3cmd index bcd24bc2..d92ada27 100755 --- a/s3cmd +++ b/s3cmd @@ -2178,8 +2178,6 @@ def cmd_listacl(args): raise ParameterError("Too few parameters! Expected: ") if not S3Uri(args[0]).has_bucket(): raise ParameterError("Invalid bucket name: '%s'" % args[0]) - if not S3Uri(args[0]).has_object(): - raise ParameterError("Invalid object name: '%s'" % args[0]) uri = S3Uri(args[0]) acl = list_acl(s3, uri) return acl From 2087c45bcae17f68a4c41bbe4b247a9145c9700d Mon Sep 17 00:00:00 2001 From: marsteg Date: Thu, 16 Oct 2025 10:31:40 +0200 Subject: [PATCH 4/8] adapted the commands to re-use existing features as advised via Feedback --- S3/S3.py | 9 +-------- s3cmd | 37 +++++++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/S3/S3.py b/S3/S3.py index 4afd6133..ac5ca125 100644 --- a/S3/S3.py +++ b/S3/S3.py @@ -20,6 +20,7 @@ import io import pprint from xml.sax import saxutils +import xml.dom.minidom from socket import timeout as SocketTimeoutException from logging import debug, info, warning, error from stat import ST_SIZE, ST_MODE, S_ISDIR, S_ISREG @@ -1224,14 +1225,6 @@ def set_policy(self, uri, policy): response = self.send_request(request) return response - def list_policy(self, uri): - headers = SortedDict(ignore_case = True) - headers['content-type'] = 'application/json' - request = self.create_request("BUCKET_LIST", uri = uri, - headers=headers, uri_params = {'policy': None}) - response = self.send_request(request) - return response - def delete_policy(self, uri): request = self.create_request("BUCKET_DELETE", uri = uri, uri_params = {'policy': None}) diff --git a/s3cmd b/s3cmd index d92ada27..2180c0f4 100755 --- a/s3cmd +++ b/s3cmd @@ -2171,7 +2171,7 @@ def cmd_sync(args): return cmd_sync_remote2remote(args) raise ParameterError("Invalid source/destination: '%s'" % "' '".join(args)) -def cmd_listacl(args): +def cmd_getacl(args): cfg = Config() s3 = S3(cfg) if len(args) != 1: @@ -2179,7 +2179,25 @@ def cmd_listacl(args): if not S3Uri(args[0]).has_bucket(): raise ParameterError("Invalid bucket name: '%s'" % args[0]) uri = S3Uri(args[0]) - acl = list_acl(s3, uri) + try: + acl = s3.get_acl(uri) + acl_grant_list = acl.getGrantList() + owner = acl.getOwner() + output(u"Bucket: %s" % uri.bucket()) + output(u"Owner: %s (ID: %s)" % (owner['nick'], owner['id'])) + for grant in acl_grant_list: + output(u" ACL: %s: %s" % (grant['grantee'], grant['permission'])) + if acl.isAnonRead(): + output(u" URL: %s" % uri.public_url()) + output(u"\nRaw ACL Output: \n %s" % acl) + except S3Error as exc: + # Ignore the exception and don't fail the info + # if the server doesn't support setting ACLs + if exc.status not in [404, 501]: + raise exc + else: + output(u" ACL: none") + return acl def cmd_setacl(args): @@ -2369,12 +2387,9 @@ def cmd_listpolicy(args): if uri.has_object(): raise ParameterError("No Object Names allowed: '%s'" % args[0]) - response = s3.list_policy(uri) + response = s3.get_policy(uri) - debug(u"response - %s" % response['status']) - if response['status'] == 200: - output(u"%s: Policy listed" % uri) - return EX_OK + return response def cmd_delpolicy(args): cfg = Config() @@ -3010,7 +3025,7 @@ def get_commands_list(): {"cmd":"cp", "label":"Copy object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_cp, "argc":2}, {"cmd":"modify", "label":"Modify object metadata", "param":"s3://BUCKET1/OBJECT", "func":cmd_modify, "argc":1}, {"cmd":"mv", "label":"Move object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_mv, "argc":2}, - {"cmd":"listacl", "label":"List Access control list for Bucket or Files", "param":"s3://BUCKET[/OBJECT]", "func":cmd_listacl, "argc":0}, + {"cmd":"getacl", "label":"List Access control list for Bucket or Files", "param":"s3://BUCKET[/OBJECT]", "func":cmd_getacl, "argc":0}, {"cmd":"setacl", "label":"Modify Access control list for Bucket or Files", "param":"s3://BUCKET[/OBJECT]", "func":cmd_setacl, "argc":1}, {"cmd":"setversioning", "label":"Modify Bucket Versioning", "param":"s3://BUCKET enable|disable", "func":cmd_setversioning, "argc":2}, {"cmd":"setownership", "label":"Modify Bucket Object Ownership", "param":"s3://BUCKET BucketOwnerPreferred|BucketOwnerEnforced|ObjectWriter", "func":cmd_setownership, "argc":2}, @@ -3073,12 +3088,6 @@ def format_commands(progname, commands_list): help += " %s\n %s %s %s\n" % (cmd["label"], progname, cmd["cmd"], cmd["param"]) return help -def list_acl(s3, uri): - cfg = Config() - acl = s3.get_acl(uri) - output(u"%s: ACL Listed" % uri) - return acl - def update_acl(s3, uri, seq_label=""): cfg = Config() something_changed = False From 33aaee7a4fbfc5a519fdf582f0cf980dc6936c30 Mon Sep 17 00:00:00 2001 From: marsteg Date: Thu, 16 Oct 2025 10:41:54 +0200 Subject: [PATCH 5/8] removing unnecessary Package --- S3/S3.py | 1 - 1 file changed, 1 deletion(-) diff --git a/S3/S3.py b/S3/S3.py index ac5ca125..26c516fd 100644 --- a/S3/S3.py +++ b/S3/S3.py @@ -20,7 +20,6 @@ import io import pprint from xml.sax import saxutils -import xml.dom.minidom from socket import timeout as SocketTimeoutException from logging import debug, info, warning, error from stat import ST_SIZE, ST_MODE, S_ISDIR, S_ISREG From 21c4bf03aece9925f92d29d08514fac6f5eda206 Mon Sep 17 00:00:00 2001 From: marsteg Date: Fri, 17 Oct 2025 09:05:47 +0200 Subject: [PATCH 6/8] updated based on review --- s3cmd | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/s3cmd b/s3cmd index 2180c0f4..84004f5d 100755 --- a/s3cmd +++ b/s3cmd @@ -2183,22 +2183,20 @@ def cmd_getacl(args): acl = s3.get_acl(uri) acl_grant_list = acl.getGrantList() owner = acl.getOwner() - output(u"Bucket: %s" % uri.bucket()) - output(u"Owner: %s (ID: %s)" % (owner['nick'], owner['id'])) + output(u"%s (bucket):" % uri.uri()) + if owner['nick'] is None or owner['nick'] == "": + output(u" Owner: %s" % (owner['id'])) + else: + output(u" Owner: %s (Nick: %s)" % (owner['id'], owner['nick'])) for grant in acl_grant_list: output(u" ACL: %s: %s" % (grant['grantee'], grant['permission'])) if acl.isAnonRead(): output(u" URL: %s" % uri.public_url()) - output(u"\nRaw ACL Output: \n %s" % acl) except S3Error as exc: # Ignore the exception and don't fail the info # if the server doesn't support setting ACLs - if exc.status not in [404, 501]: - raise exc - else: - output(u" ACL: none") - - return acl + output(u" ACL: none") + return EX_OK def cmd_setacl(args): cfg = Config() @@ -2375,21 +2373,29 @@ def cmd_setpolicy(args): output(u"%s: Policy updated" % uri) return EX_OK -def cmd_listpolicy(args): +def cmd_getpolicy(args): cfg = Config() s3 = S3(cfg) uri = S3Uri(args[0]) - if len(args) != 1: - raise ParameterError("Too few parameters! Expected: ") - if not uri.has_bucket(): - raise ParameterError("Invalid bucket name: '%s'" % args[0]) - if uri.has_object(): - raise ParameterError("No Object Names allowed: '%s'" % args[0]) + if uri.type != "s3" or not uri.has_bucket(): + raise ParameterError("Expecting S3 URI instead of '%s'" % args[0]) - response = s3.get_policy(uri) - - return response + try: + policy = s3.get_policy(uri) + output(u" Policy: %s" % policy) + except S3Error as exc: + # Ignore the exception and don't fail the info + # if the server doesn't support setting ACLs + if exc.status == 403: + output(u" Policy: Not available: GetPolicy permission is needed to read the policy") + elif exc.status == 405: + output(u" Policy: Not available: Only the bucket owner can read the policy") + elif exc.status not in [404, 501]: + raise exc + else: + output(u" Policy: none") + return EX_OK def cmd_delpolicy(args): cfg = Config() @@ -3025,7 +3031,7 @@ def get_commands_list(): {"cmd":"cp", "label":"Copy object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_cp, "argc":2}, {"cmd":"modify", "label":"Modify object metadata", "param":"s3://BUCKET1/OBJECT", "func":cmd_modify, "argc":1}, {"cmd":"mv", "label":"Move object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_mv, "argc":2}, - {"cmd":"getacl", "label":"List Access control list for Bucket or Files", "param":"s3://BUCKET[/OBJECT]", "func":cmd_getacl, "argc":0}, + {"cmd":"getacl", "label":"List Access control list for Bucket or Files", "param":"s3://BUCKET[/OBJECT]", "func":cmd_getacl, "argc":1}, {"cmd":"setacl", "label":"Modify Access control list for Bucket or Files", "param":"s3://BUCKET[/OBJECT]", "func":cmd_setacl, "argc":1}, {"cmd":"setversioning", "label":"Modify Bucket Versioning", "param":"s3://BUCKET enable|disable", "func":cmd_setversioning, "argc":2}, {"cmd":"setownership", "label":"Modify Bucket Object Ownership", "param":"s3://BUCKET BucketOwnerPreferred|BucketOwnerEnforced|ObjectWriter", "func":cmd_setownership, "argc":2}, @@ -3035,7 +3041,7 @@ def get_commands_list(): {"cmd":"setobjectretention", "label":"Modify Object Retention", "param":"MODE RETAIN_UNTIL_DATE s3://BUCKET/OBJECT", "func":cmd_setobjectretention, "argc":3}, {"cmd":"setpolicy", "label":"Modify Bucket Policy", "param":"FILE s3://BUCKET", "func":cmd_setpolicy, "argc":2}, - {"cmd":"listpolicy", "label":"List Bucket Policy", "param":"FILE s3://BUCKET", "func":cmd_listpolicy, "argc":0}, + {"cmd":"getpolicy", "label":"Get Bucket Policy", "param":"FILE s3://BUCKET", "func":cmd_getpolicy, "argc":1}, {"cmd":"delpolicy", "label":"Delete Bucket Policy", "param":"s3://BUCKET", "func":cmd_delpolicy, "argc":1}, {"cmd":"setcors", "label":"Modify Bucket CORS", "param":"FILE s3://BUCKET", "func":cmd_setcors, "argc":2}, {"cmd":"delcors", "label":"Delete Bucket CORS", "param":"s3://BUCKET", "func":cmd_delcors, "argc":1}, From dc2d01f890cf2f24e41dd728f144b3a38ecc41a4 Mon Sep 17 00:00:00 2001 From: marsteg Date: Fri, 17 Oct 2025 09:22:57 +0200 Subject: [PATCH 7/8] removed some more unnecessary checks based on the feedback --- s3cmd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/s3cmd b/s3cmd index 84004f5d..9447fc26 100755 --- a/s3cmd +++ b/s3cmd @@ -2174,11 +2174,11 @@ def cmd_sync(args): def cmd_getacl(args): cfg = Config() s3 = S3(cfg) - if len(args) != 1: - raise ParameterError("Too few parameters! Expected: ") - if not S3Uri(args[0]).has_bucket(): - raise ParameterError("Invalid bucket name: '%s'" % args[0]) uri = S3Uri(args[0]) + + if uri.type != "s3" or not uri.has_bucket(): + raise ParameterError("Expecting S3 URI instead of '%s'" % args[0]) + try: acl = s3.get_acl(uri) acl_grant_list = acl.getGrantList() From 2bfb73fbaa7b20316259d4d36b81cb17a7b4deb6 Mon Sep 17 00:00:00 2001 From: marsteg Date: Wed, 22 Oct 2025 08:49:14 +0200 Subject: [PATCH 8/8] updating based on latest feedback --- s3cmd | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/s3cmd b/s3cmd index 9447fc26..903a04e3 100755 --- a/s3cmd +++ b/s3cmd @@ -2179,23 +2179,22 @@ def cmd_getacl(args): if uri.type != "s3" or not uri.has_bucket(): raise ParameterError("Expecting S3 URI instead of '%s'" % args[0]) - try: - acl = s3.get_acl(uri) - acl_grant_list = acl.getGrantList() - owner = acl.getOwner() - output(u"%s (bucket):" % uri.uri()) - if owner['nick'] is None or owner['nick'] == "": - output(u" Owner: %s" % (owner['id'])) - else: - output(u" Owner: %s (Nick: %s)" % (owner['id'], owner['nick'])) - for grant in acl_grant_list: - output(u" ACL: %s: %s" % (grant['grantee'], grant['permission'])) - if acl.isAnonRead(): - output(u" URL: %s" % uri.public_url()) - except S3Error as exc: - # Ignore the exception and don't fail the info - # if the server doesn't support setting ACLs - output(u" ACL: none") + acl = s3.get_acl(uri) + acl_grant_list = acl.getGrantList() + owner = acl.getOwner() + if uri.has_object(): + uri_type = "(object)" + else: + uri_type = "(bucket)" + output(u"%s %s:" % (uri, uri_type)) + if not owner['nick']: + output(u" Owner: %s" % (owner['id'])) + else: + output(u" Owner: %s (Nick: %s)" % (owner['id'], owner['nick'])) + for grant in acl_grant_list: + output(u" ACL: %s: %s" % (grant['grantee'], grant['permission'])) + if acl.isAnonRead(): + output(u" URL: %s" % uri.public_url()) return EX_OK def cmd_setacl(args): @@ -2389,8 +2388,10 @@ def cmd_getpolicy(args): # if the server doesn't support setting ACLs if exc.status == 403: output(u" Policy: Not available: GetPolicy permission is needed to read the policy") + return EX_ACCESSDENIED elif exc.status == 405: output(u" Policy: Not available: Only the bucket owner can read the policy") + return EX_ACCESSDENIED elif exc.status not in [404, 501]: raise exc else: