Repository: Netflix-Skunkworks/policyuniverse

Issue:
Perform Principal extraction with any Mapping type
Previous work in #94 did not include a expansion to all `Mapping` types for `Principal` extraction in `Statement`:

https://github.com/Netflix-Skunkworks/policyuniverse/blob/37eccbc5be0552c85d58fcf2b6c5425ff7fe0984/policyuniverse/statement.py#L118


The following error is raised if a custom `Mapping` (sub-class of `collections.Mapping`) is used:

```python
 def _add_or_extend(self, value, structure):
        if is_array(value):
            structure.update(set(value))
        else:
>           structure.add(value)
E           TypeError: unhashable type: 'CustomMapping'
```

Discussion:

Patch:
diff --git a/policyuniverse/statement.py b/policyuniverse/statement.py
index a571b1d..4ad4d4d 100644
--- a/policyuniverse/statement.py
+++ b/policyuniverse/statement.py
@@ -28,6 +28,11 @@ from policyuniverse.common import ensure_array, is_array
 import re
 from collections import namedtuple
 
+try:
+    from collections.abc import Mapping
+except ImportError:
+    # Python 2.7 compatibility
+    from collections import Mapping
 
 PrincipalTuple = namedtuple("Principal", "category value")
 ConditionTuple = namedtuple("Condition", "category value")
@@ -115,7 +120,7 @@ class Statement(object):
             # It is possible not to define a principal, AWS ignores these statements.
             return principals
 
-        if isinstance(principal, dict):
+        if isinstance(principal, Mapping):
 
             if "AWS" in principal:
                 self._add_or_extend(principal["AWS"], principals)


Test patch:
diff --git a/policyuniverse/tests/helpers.py b/policyuniverse/tests/helpers.py
new file mode 100644
index 0000000..cdaf15c
--- /dev/null
+++ b/policyuniverse/tests/helpers.py
@@ -0,0 +1,27 @@
+from policyuniverse.statement import Mapping
+from policyuniverse.common import Sequence
+
+
+class CustomSequence(Sequence):
+    def __init__(self, data):
+        self._sequence = data
+
+    def __getitem__(self, item):
+        return self._sequence[item]
+
+    def __len__(self):
+        return len(self._sequence)
+
+
+class CustomMapping(Mapping):
+    def __init__(self, data):
+        self._mapping = data
+
+    def __getitem__(self, item):
+        return self._mapping[item]
+
+    def __len__(self):
+        return len(self._mapping)
+
+    def __iter__(self):
+        return iter(self._mapping)
diff --git a/policyuniverse/tests/test_policy.py b/policyuniverse/tests/test_policy.py
index 2f3238e..6197611 100644
--- a/policyuniverse/tests/test_policy.py
+++ b/policyuniverse/tests/test_policy.py
@@ -24,6 +24,8 @@ from policyuniverse import logger
 import unittest
 import json
 
+from .helpers import CustomMapping, CustomSequence
+
 
 policy01 = dict(
     Version="2012-10-08",
@@ -122,6 +124,26 @@ policy06 = dict(
     ],
 )
 
+# Custom types
+policy07 = CustomMapping(
+    dict(
+        Statement=CustomSequence(
+            [
+                CustomMapping(
+                    dict(
+                        Action="s3:GetBucketAcl",
+                        Effect="Allow",
+                        Principal=CustomMapping({"AWS": "*"}),
+                        Resource="arn:aws:s3:::example-bucket",
+                        Sid="Public Access",
+                    )
+                )
+            ]
+        ),
+        Version="2012-10-17",
+    )
+)
+
 
 class PolicyTestCase(unittest.TestCase):
     def test_internet_accessible(self):
@@ -258,3 +280,8 @@ class PolicyTestCase(unittest.TestCase):
             list(s.statement for s in policy.statements),
             [policy_document["Statement"][0]],
         )
+
+    def test_mapping_and_sequence_policy_document(self):
+        policy = Policy(policy07)
+        self.assertSetEqual(policy.principals, set("*"))
+        self.assertIs(policy.is_internet_accessible(), True)
diff --git a/policyuniverse/tests/test_statement.py b/policyuniverse/tests/test_statement.py
index a685fe0..01f704f 100644
--- a/policyuniverse/tests/test_statement.py
+++ b/policyuniverse/tests/test_statement.py
@@ -22,6 +22,8 @@
 from policyuniverse.statement import Statement
 import unittest
 
+from .helpers import CustomMapping, CustomSequence
+
 # NotPrincipal
 statement01 = dict(
     Effect="Allow",
@@ -327,6 +329,17 @@ statement30 = dict(
     Condition={"StringLike": {"AWS:PrincipalOrgID": "o-*"}},
 )
 
+# Custom Mapping / Sequence types
+statement31 = CustomMapping(
+    dict(
+        Action="s3:GetBucketAcl",
+        Effect="Allow",
+        Principal=CustomMapping({"AWS": "*"}),
+        Resource="arn:aws:s3:::example-bucket",
+        Sid="Public Access",
+    )
+)
+
 
 class StatementTestCase(unittest.TestCase):
     def test_statement_effect(self):
@@ -373,6 +386,10 @@ class StatementTestCase(unittest.TestCase):
         statement = Statement(statement_wo_principal)
         self.assertEqual(statement.principals, set([]))
 
+        # Custom types
+        statement = Statement(statement31)
+        self.assertSetEqual(statement.principals, set(["*"]))
+
     def test_statement_conditions(self):
         statement = Statement(statement07)
         self.assertEqual(