Skip to content

util.arns

util.arns

See the examples under https://github.com/salesforce/policy_sentry/tree/master/examples/library-usage/util.

Functions to use for parsing ARNs, matching ARN types, and getting the right fragment/component from an ARN string,

ARN

Class that helps to match ARN resource type formats neatly

same_resource_type(self, arn_in_database)

Given an arn, see if it has the same resource type

Source code in policy_sentry/util/arns.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def same_resource_type(self, arn_in_database):
    """Given an arn, see if it has the same resource type"""

    # 1. If the RAW ARN in the database is *, then it doesn't have a resource type
    if arn_in_database == "*":
        return False

    # 2. ARNs should have the same service
    elements = arn_in_database.split(":", 5)
    if self.service_prefix != elements[2]:
        return False

    # 3. Add support for resource type wildcards, per #204
    # When specifying an ARN and setting a * on the resource type (e.g. instead of db:instance it is *:*),
    #   multiple ARNs can match.
    #   Previously, this would fail and return empty results.
    #   Now it correctly returns the full list of matching ARNs and corresponding actions.
    resource_type_arn_to_test = parse_arn_for_resource_type(self.arn)
    if resource_type_arn_to_test == '*':
        return True

    # 4. Match patterns for complicated resource strings, leveraging the standardized format of the Raw ARN format
    # table/${TableName} should not match `table/${TableName}/backup/${BackupName}`
    resource_string_arn_in_database = get_resource_string(arn_in_database)

    split_resource_string_in_database = re.split(':|/', resource_string_arn_in_database)
    # logger.debug(str(split_resource_string_in_database))
    arn_format_list = []
    for elem in split_resource_string_in_database:
        if "${" not in elem:
            arn_format_list.append(elem)
        else:
            # If an element says something like ${TableName}, normalize it to an empty string
            arn_format_list.append("")

    split_resource_string_to_test = re.split(':|/', self.resource_string)
    # 4b: If we have a confusing resource string, the length of the split resource string list
    #  should at least be the same
    # Again, table/${TableName} (len of 2) should not match `table/${TableName}/backup/${BackupName}` (len of 4)
    if len(split_resource_string_to_test) != len(arn_format_list):
        return False

    # 4c: See if the non-normalized fields match
    for i in range(len(arn_format_list)):
        # If the field is not normalized to empty string, then make sure the resource type segments match
        # So, using table/${TableName}/backup/${BackupName} as an example:
        # table should match, backup should match,
        # and length of the arn_format_list should be the same as split_resource_string_to_test
        # If all conditions match, then the ARN format is the same.
        if arn_format_list[i] != "":
            if arn_format_list[i] == split_resource_string_to_test[i]:
                pass
            elif split_resource_string_to_test[i] == "*":
                pass
            else:
                return False

    # 4. Special type for S3 bucket objects and CodeCommit repos
    # Note: Each service can only have one of these, so these are definitely exceptions
    exclusion_list = ["${ObjectName}", "${RepositoryName}", "${BucketName}"]
    resource_path_arn_in_database = elements[5]
    if resource_path_arn_in_database in exclusion_list:
        logger.debug("Special type: %s", resource_path_arn_in_database)
        # If we've made it this far, then it is a special type
        # return True
        # Presence of / would mean it's an object in both so it matches
        if "/" in self.resource_string and "/" in elements[5]:
            return True
        # / not being present in either means it's a bucket in both so it matches
        elif "/" not in self.resource_string and "/" not in elements[5]:
            return True
        # If there is a / in one but not in the other, it does not match
        else:
            return False

    # 5. If we've made it this far, then it should pass
    return True

does_arn_match(arn_to_test, arn_in_database)

Given two ARNs, determine if they have the same resource type.

Parameters:

Name Type Description Default
arn_to_test

ARN provided by user

required
arn_in_database

Raw ARN that exists in the policy sentry database

required

Returns:

Type Description
Boolean

result of whether or not the ARNs match

Source code in policy_sentry/util/arns.py
230
231
232
233
234
235
236
237
238
239
240
241
242
def does_arn_match(arn_to_test, arn_in_database):
    """
    Given two ARNs, determine if they have the same resource type.

    Arguments:
        arn_to_test: ARN provided by user
        arn_in_database: Raw ARN that exists in the policy sentry database

    Returns:
        Boolean: result of whether or not the ARNs match
    """
    this_arn = ARN(arn_to_test)
    return this_arn.same_resource_type(arn_in_database)

get_account_from_arn(arn)

Given an ARN, return the account ID in the ARN, if it is available. In certain cases like S3 it is not

Source code in policy_sentry/util/arns.py
177
178
179
180
181
182
183
184
185
def get_account_from_arn(arn):
    """Given an ARN, return the account ID in the ARN, if it is available. In certain cases like S3 it is not"""
    result = parse_arn(arn)
    # Support S3 buckets with no values under account
    if result["account"] is None:
        result = ""
    else:
        result = result["account"]
    return result

get_region_from_arn(arn)

Given an ARN, return the region in the ARN, if it is available. In certain cases like S3 it is not

Source code in policy_sentry/util/arns.py
166
167
168
169
170
171
172
173
174
def get_region_from_arn(arn):
    """Given an ARN, return the region in the ARN, if it is available. In certain cases like S3 it is not"""
    result = parse_arn(arn)
    # Support S3 buckets with no values under region
    if result["region"] is None:
        result = ""
    else:
        result = result["region"]
    return result

get_resource_path_from_arn(arn)

Given an ARN, parse it according to ARN namespacing and return the resource path. See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html for more details on ARN namespacing.

Source code in policy_sentry/util/arns.py
188
189
190
191
192
def get_resource_path_from_arn(arn):
    """Given an ARN, parse it according to ARN namespacing and return the resource path. See
    http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html for more details on ARN namespacing."""
    result = parse_arn(arn)
    return result["resource_path"]

get_resource_string(arn)

Given an ARN, return the string after the account ID, no matter the ARN format.

Parameters:

Name Type Description Default
arn

An ARN, like arn:partition:service:region:account-id:resourcetype/resource

required

Returns:

Type Description
String

The resource string, like resourcetype/resource

Source code in policy_sentry/util/arns.py
195
196
197
198
199
200
201
202
203
204
205
206
def get_resource_string(arn):
    """
    Given an ARN, return the string after the account ID, no matter the ARN format.

    Arguments:
        arn: An ARN, like `arn:partition:service:region:account-id:resourcetype/resource`
    Return:
        String: The resource string, like `resourcetype/resource`
    """
    split_arn = arn.split(":")
    resource_string = ":".join(split_arn[5:])
    return resource_string

get_service_from_arn(arn)

Given an ARN string, return the service

Source code in policy_sentry/util/arns.py
160
161
162
163
def get_service_from_arn(arn):
    """Given an ARN string, return the service """
    result = parse_arn(arn)
    return result["service"]

parse_arn(arn)

Given an ARN, split up the ARN into the ARN namespacing schema dictated by the AWS docs.

Source code in policy_sentry/util/arns.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def parse_arn(arn):
    """
    Given an ARN, split up the ARN into the ARN namespacing schema dictated by the AWS docs.
    """
    elements = arn.split(":", 5)
    result = {
        "arn": elements[0],
        "partition": elements[1],
        "service": elements[2],
        "region": elements[3],
        "account": elements[4],
        "resource": elements[5],
        "resource_path": None,
    }
    if "/" in result["resource"]:
        result["resource"], result["resource_path"] = result["resource"].split("/", 1)
    elif ":" in result["resource"]:
        result["resource"], result["resource_path"] = result["resource"].split(":", 1)
    return result

parse_arn_for_resource_type(arn)

Parses the resource string (resourcetype/resource and other variants) and grab the resource type.

Parameters:

Name Type Description Default
arn

The resource string to parse, like resourcetype/resource

required

Returns:

Type Description
String

The resource type, like bucket or object

Source code in policy_sentry/util/arns.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def parse_arn_for_resource_type(arn):
    """
    Parses the resource string (resourcetype/resource and other variants) and grab the resource type.

    Arguments:
        arn: The resource string to parse, like `resourcetype/resource`
    Return:
        String: The resource type, like `bucket` or `object`
    """
    split_arn = arn.split(":")
    resource_string = ":".join(split_arn[5:])
    split_resource = re.split("/|:", resource_string)
    if len(split_resource) == 1:
        # logger.debug(f"split_resource length is 1: {str(split_resource)}")
        pass
    elif len(split_resource) > 1:
        return split_resource[0]