From 36c021405083a80a2d2514b65fd682fb4bbc4394 Mon Sep 17 00:00:00 2001 From: Bala FA Date: Fri, 23 Oct 2020 02:55:00 +0000 Subject: [PATCH] Refactor XML handling of {set,get,delete}_bucket_encryption APIs (#991) --- docs/API.md | 33 ++---- ...ryption.py => delete_bucket_encryption.py} | 16 +-- examples/get_bucket_encryption.py | 16 +-- examples/set_bucket_encryption.py | 31 ++--- minio/api.py | 51 ++++---- minio/sseconfig.py | 98 ++++++++++++++++ minio/xml_marshal.py | 111 ------------------ tests/unit/sseconfig_test.py | 47 ++++++++ 8 files changed, 209 insertions(+), 194 deletions(-) rename examples/{remove_bucket_encryption.py => delete_bucket_encryption.py} (68%) create mode 100644 minio/sseconfig.py delete mode 100644 minio/xml_marshal.py create mode 100644 tests/unit/sseconfig_test.py diff --git a/docs/API.md b/docs/API.md index 5780a7d0b..7ff06f198 100644 --- a/docs/API.md +++ b/docs/API.md @@ -57,7 +57,7 @@ s3Client = Minio( | [`listen_bucket_notification`](#listen_bucket_notification) | | | | [`delete_bucket_encryption`](#delete_bucket_encryption) | | | | [`get_bucket_encryption`](#get_bucket_encryption) | | | -| [`put_bucket_encryption`](#put_bucket_encryption) | | | +| [`set_bucket_encryption`](#set_bucket_encryption) | | | | [`delete_object_lock_config`](#delete_object_lock_config) | | | | [`get_object_lock_config`](#get_object_lock_config) | | | | [`set_object_lock_config`](#set_object_lock_config) | | | @@ -453,9 +453,9 @@ __Parameters__ __Return Value__ -| Param | -|:------------------------------------| -| Encryption configuration as _dict_. | +| Param | +|:--------------------| +| _SSEConfig_ object. | __Example__ @@ -463,32 +463,25 @@ __Example__ config = minio.get_bucket_encryption("my-bucketname") ``` - + -### put_bucket_encryption(bucket_name, encryption_configuration) +### set_bucket_encryption(bucket_name, config) Set encryption configuration of a bucket. __Parameters__ -| Param | Type | Description | -|:----------------|:-------|:--------------------------------------------------| -| ``bucket_name`` | _str_ | Name of the bucket. | -| ``enc_config`` | _dict_ | Encryption configuration as dictionary to be set. | +| Param | Type | Description | +|:----------------|:------------|:--------------------------------------| +| ``bucket_name`` | _str_ | Name of the bucket. | +| ``config`` | _SSEConfig_ | Server-side encryption configuration. | __Example__ ```py -# Sample default encryption configuration -config = { - 'ServerSideEncryptionConfiguration':{ - 'Rule': [ - {'ApplyServerSideEncryptionByDefault': {'SSEAlgorithm': 'AES256'}} - ] - } -} - -minio.put_bucket_encryption("my-bucketname", config) +minio.set_bucket_encryption( + "my-bucketname", SSEConfig(Rule.new_sse_s3_rule()), +) ``` diff --git a/examples/remove_bucket_encryption.py b/examples/delete_bucket_encryption.py similarity index 68% rename from examples/remove_bucket_encryption.py rename to examples/delete_bucket_encryption.py index b61860229..97429b87a 100644 --- a/examples/remove_bucket_encryption.py +++ b/examples/delete_bucket_encryption.py @@ -18,15 +18,11 @@ # dummy values, please replace them with original values. from minio import Minio -from minio.error import ResponseError -client = Minio('s3.amazonaws.com', - access_key='YOUR-ACCESSKEYID', - secret_key='YOUR-SECRETACCESSKEY', - secure=True) +client = Minio( + "play.min.io", + access_key="Q3AM3UQ867SPQQA43P2F", + secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", +) -try: - # Delete default encryption configuration on bucket 'my-bucketname'. - client.delete_bucket_encryption('my-bucketname') -except ResponseError as err: - print(err) +client.delete_bucket_encryption("my-bucketname") diff --git a/examples/get_bucket_encryption.py b/examples/get_bucket_encryption.py index 78377a5f4..ff502c354 100644 --- a/examples/get_bucket_encryption.py +++ b/examples/get_bucket_encryption.py @@ -18,15 +18,11 @@ # dummy values, please replace them with original values. from minio import Minio -from minio.error import ResponseError -client = Minio('s3.amazonaws.com', - access_key='YOUR-ACCESSKEYID', - secret_key='YOUR-SECRETACCESSKEY', - secure=True) +client = Minio( + "play.min.io", + access_key="Q3AM3UQ867SPQQA43P2F", + secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", +) -try: - # Get current policy of bucket 'my-bucketname'. - print(client.get_bucket_encryption('my-bucketname')) -except ResponseError as err: - print(err) +config = client.get_bucket_encryption("my-bucketname") diff --git a/examples/set_bucket_encryption.py b/examples/set_bucket_encryption.py index daad2a9ab..58eb1bedf 100644 --- a/examples/set_bucket_encryption.py +++ b/examples/set_bucket_encryption.py @@ -18,27 +18,14 @@ # dummy values, please replace them with original values. from minio import Minio -from minio.error import ResponseError +from minio.sseconfig import Rule, SSEConfig -client = Minio('s3.amazonaws.com', - access_key='YOUR-ACCESSKEYID', - secret_key='YOUR-SECRETACCESSKEY', - secure=True) +client = Minio( + "play.min.io", + access_key="Q3AM3UQ867SPQQA43P2F", + secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", +) -try: - # Set default encryption configuration for bucket 'my-bucketname' - ENC_CONFIG = { - 'ServerSideEncryptionConfiguration': { - 'Rule': [ - { - 'ApplyServerSideEncryptionByDefault': { - 'SSEAlgorithm': 'AES256' - } - } - ] - } - } - - client.put_bucket_encryption('my-bucketname', ENC_CONFIG) -except ResponseError as err: - print(err) +client.set_bucket_encryption( + "my-bucketname", SSEConfig(Rule.new_sse_s3_rule()), +) diff --git a/minio/api.py b/minio/api.py index bf8692cd5..89ebceab8 100644 --- a/minio/api.py +++ b/minio/api.py @@ -70,11 +70,11 @@ from .signer import (AMZ_DATE_FORMAT, SIGN_V4_ALGORITHM, get_credential_string, post_presign_v4, presign_v4, sign_v4_s3) from .sse import SseCustomerKey +from .sseconfig import SSEConfig from .tagging import Tagging from .thread_pool import ThreadPool from .versioningconfig import VersioningConfig from .xml import Element, SubElement, findtext, getbytes, marshal, unmarshal -from .xml_marshal import xml_marshal_bucket_encryption, xml_to_dict try: from json.decoder import JSONDecodeError @@ -772,22 +772,22 @@ def delete_bucket_notification(self, bucket_name): """ self.set_bucket_notification(bucket_name, NotificationConfig()) - def put_bucket_encryption(self, bucket_name, enc_config): + def set_bucket_encryption(self, bucket_name, config): """ Set encryption configuration of a bucket. :param bucket_name: Name of the bucket. - :param enc_config: Encryption configuration as dictionary to be set. + :param config: :class:`SSEConfig ` object. Example:: - minio.put_bucket_encryption("my-bucketname", config) + minio.set_bucket_encryption( + "my-bucketname", SSEConfig(Rule.new_sse_s3_rule()), + ) """ check_bucket_name(bucket_name) - - # 'Rule' is a list, so we need to go through each one of - # its key/value pair and collect the encryption values. - rules = enc_config['ServerSideEncryptionConfiguration']['Rule'] - body = xml_marshal_bucket_encryption(rules) + if not isinstance(config, SSEConfig): + raise ValueError("config must be SSEConfig type") + body = marshal(config) self._execute( "PUT", bucket_name, @@ -801,18 +801,23 @@ def get_bucket_encryption(self, bucket_name): Get encryption configuration of a bucket. :param bucket_name: Name of the bucket. - :return: Encryption configuration. + :return: :class:`SSEConfig ` object. Example:: config = minio.get_bucket_encryption("my-bucketname") """ check_bucket_name(bucket_name) - response = self._execute( - "GET", - bucket_name, - query_params={"encryption": ""}, - ) - return xml_to_dict(response.data.decode()) + try: + response = self._execute( + "GET", + bucket_name, + query_params={"encryption": ""}, + ) + return unmarshal(SSEConfig, response.data.decode()) + except S3Error as exc: + if exc.code != "ServerSideEncryptionConfigurationNotFoundError": + raise + return None def delete_bucket_encryption(self, bucket_name): """ @@ -824,11 +829,15 @@ def delete_bucket_encryption(self, bucket_name): minio.delete_bucket_encryption("my-bucketname") """ check_bucket_name(bucket_name) - self._execute( - "DELETE", - bucket_name, - query_params={"encryption": ""}, - ) + try: + self._execute( + "DELETE", + bucket_name, + query_params={"encryption": ""}, + ) + except S3Error as exc: + if exc.code != "ServerSideEncryptionConfigurationNotFoundError": + raise def listen_bucket_notification(self, bucket_name, prefix='', suffix='', events=('s3:ObjectCreated:*', diff --git a/minio/sseconfig.py b/minio/sseconfig.py new file mode 100644 index 000000000..b06fffd59 --- /dev/null +++ b/minio/sseconfig.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# MinIO Python Library for Amazon S3 Compatible Cloud Storage, (C) +# 2020 MinIO, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Request/response of PutBucketEncryption and GetBucketEncryption APIs.""" + +from __future__ import absolute_import + +from abc import ABCMeta + +from .xml import Element, SubElement, find, findtext + +AES256 = "AES256" +AWS_KMS = "aws:kms" + + +class Rule: + """Server-side encryption rule. """ + __metaclass__ = ABCMeta + + def __init__(self, sse_algorithm, kms_master_key_id=None): + self._sse_algorithm = sse_algorithm + self._kms_master_key_id = kms_master_key_id + + @property + def sse_algorithm(self): + """Get SSE algorithm.""" + return self._sse_algorithm + + @property + def kms_master_key_id(self): + """Get KMS master key ID.""" + return self._kms_master_key_id + + @classmethod + def new_sse_s3_rule(cls): + """Create SSE-S3 rule.""" + return cls(AES256) + + @classmethod + def new_sse_kms_rule(cls, kms_master_key_id=None): + """Create new SSE-KMS rule.""" + return cls(AWS_KMS, kms_master_key_id) + + @classmethod + def fromxml(cls, element): + """Create new object with values from XML element.""" + element = find(element, "ApplyServerSideEncryptionByDefault") + sse_algorithm = findtext(element, "SSEAlgorithm", True) + kms_master_key_id = findtext(element, "KMSMasterKeyID") + return cls(sse_algorithm, kms_master_key_id) + + def toxml(self, element): + """Convert to XML.""" + element = SubElement(element, "Rule") + tag = SubElement(element, "ApplyServerSideEncryptionByDefault") + SubElement(tag, "SSEAlgorithm", self._sse_algorithm) + if self._kms_master_key_id is not None: + SubElement(tag, "KMSMasterKeyID", self._kms_master_key_id) + return element + + +class SSEConfig: + """server-side encyption configuration.""" + + def __init__(self, rule): + if not rule: + raise ValueError("rule must be provided") + self._rule = rule + + @property + def rule(self): + """Get rule.""" + return self._rule + + @classmethod + def fromxml(cls, element): + """Create new object with values from XML element.""" + element = find(element, "Rule") + return cls(Rule.fromxml(element)) + + def toxml(self, element): + """Convert to XML.""" + element = Element("ServerSideEncryptionConfiguration") + self._rule.toxml(element) + return element diff --git a/minio/xml_marshal.py b/minio/xml_marshal.py deleted file mode 100644 index 2b1dfd3b0..000000000 --- a/minio/xml_marshal.py +++ /dev/null @@ -1,111 +0,0 @@ -# -*- coding: utf-8 -*- -# MinIO Python Library for Amazon S3 Compatible Cloud Storage, (C) -# 2015, 2016, 2017, 2018, 2019 MinIO, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -minio.xml_marshal -~~~~~~~~~~~~~~~ - -This module contains the simple wrappers for XML marshaller's. - -:copyright: (c) 2015 by MinIO, Inc. -:license: Apache 2.0, see LICENSE for more details. - -""" - -from __future__ import absolute_import - -import io -from collections import defaultdict -from xml.etree import ElementTree as ET - -_S3_NAMESPACE = 'http://s3.amazonaws.com/doc/2006-03-01/' - - -def Element(tag, with_namespace=False): # pylint: disable=invalid-name - """Create ElementTree.Element with tag and namespace.""" - if with_namespace: - return ET.Element(tag, {'xmlns': _S3_NAMESPACE}) - return ET.Element(tag) - - -def SubElement(parent, tag, text=None): # pylint: disable=invalid-name - """Create ElementTree.SubElement on parent with tag and text.""" - element = ET.SubElement(parent, tag) - if text is not None: - element.text = text - return element - - -def _get_xml_data(element): - """Get XML data of ElementTree.Element.""" - data = io.BytesIO() - ET.ElementTree(element).write(data, encoding=None, xml_declaration=False) - return data.getvalue() - - -def _etree_to_dict(elem): - """Converts ElementTree object to dict.""" - ns = '{' + _S3_NAMESPACE + '}' # pylint: disable=invalid-name - elem.tag = elem.tag.replace(ns, '') - - d = {elem.tag: {} if elem.attrib else None} # pylint: disable=invalid-name - children = list(elem) - if children: - dd = defaultdict(list) # pylint: disable=invalid-name - is_rule = children[0].tag.replace(ns, "") == "Rule" - # pylint: disable=invalid-name - for dc in map(_etree_to_dict, children): - for k, v in dc.items(): # pylint: disable=invalid-name - dd[k].append([v] if is_rule else v) - # pylint: disable=invalid-name - d = {elem.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} - if elem.attrib: - d[elem.tag].update(('@' + k, v) for k, v in elem.attrib.items()) - if elem.text: - text = elem.text.strip() - if children or elem.attrib: - if text: - d[elem.tag]['#text'] = text - else: - d[elem.tag] = text - return d - - -def xml_to_dict(in_xml): - """Convert XML to dict.""" - elem = ET.XML(in_xml) - return _etree_to_dict(elem) - - -def xml_marshal_bucket_encryption(rules): - """Encode bucket encryption to XML.""" - - root = Element('ServerSideEncryptionConfiguration') - - if rules: - # As server supports only one rule, the first rule is taken due to - # no validation is done at server side. - apply_element = SubElement(SubElement(root, 'Rule'), - 'ApplyServerSideEncryptionByDefault') - SubElement(apply_element, 'SSEAlgorithm', - rules[0]['ApplyServerSideEncryptionByDefault'].get( - 'SSEAlgorithm', 'AES256')) - kms_text = rules[0]['ApplyServerSideEncryptionByDefault'].get( - 'KMSMasterKeyID') - if kms_text: - SubElement(apply_element, 'KMSMasterKeyID', kms_text) - - return _get_xml_data(root) diff --git a/tests/unit/sseconfig_test.py b/tests/unit/sseconfig_test.py new file mode 100644 index 000000000..512c8211c --- /dev/null +++ b/tests/unit/sseconfig_test.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# MinIO Python Library for Amazon S3 Compatible Cloud Storage, +# (C) 2020 MinIO, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from nose.tools import eq_ + +from minio import xml +from minio.sseconfig import AWS_KMS, Rule, SSEConfig + + +class ReplicationConfigTest(TestCase): + def test_config(self): + config = SSEConfig(Rule.new_sse_s3_rule()) + xml.marshal(config) + + config = xml.unmarshal( + SSEConfig, + """ + + + aws:kms + arn:aws:kms:us-east-1:1234/5678example + + + + """, + ) + xml.marshal(config) + eq_(config.rule.sse_algorithm, AWS_KMS) + eq_( + config.rule.kms_master_key_id, + "arn:aws:kms:us-east-1:1234/5678example", + )