Repository: minio/minio-py
Pull request #991 (2020-10-05 05:58:57)

Refactor XML handling of {set,get,delete}_bucket_encryption APIs


Patch:
diff --git a/docs/API.md b/docs/API.md
index 5780a7d..7ff06f1 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")
 ```
 
-<a name="put_bucket_encryption"></a>
+<a name="set_bucket_encryption"></a>
 
-### 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()),
+)
 ```
 
 <a name="delete_bucket_encryption"></a>
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 b618602..97429b8 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 78377a5..ff502c3 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 daad2a9..58eb1be 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 bf8692c..89ebcea 100644
--- a/minio/api.py
+++ b/minio/api.py
@@ -70,11 +70,11 @@ from .selectrequest import SelectRequest
 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 @@ class Minio:  # pylint: disable=too-many-public-methods
         """
         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 <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 @@ class Minio:  # pylint: disable=too-many-public-methods
         Get encryption configuration of a bucket.
 
         :param bucket_name: Name of the bucket.
-        :return: Encryption configuration.
+        :return: :class:`SSEConfig <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 @@ class Minio:  # pylint: disable=too-many-public-methods
             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 0000000..b06fffd
--- /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 2b1dfd3..0000000
--- 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)


Test patch:
diff --git a/tests/unit/sseconfig_test.py b/tests/unit/sseconfig_test.py
new file mode 100644
index 0000000..512c821
--- /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,
+            """<ServerSideEncryptionConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+  <Rule>
+    <ApplyServerSideEncryptionByDefault>
+        <SSEAlgorithm>aws:kms</SSEAlgorithm>
+        <KMSMasterKeyID>arn:aws:kms:us-east-1:1234/5678example</KMSMasterKeyID>
+    </ApplyServerSideEncryptionByDefault>
+</Rule>
+</ServerSideEncryptionConfiguration>
+        """,
+        )
+        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",
+        )