"""Classes to deal with IAM Policies""" import json import os import re import six from toolz import pipe from toolz.curried import groupby as groupbyz from toolz.curried import map as mapz BASE_ACTION_PREFIXES = ["Describe", "Create", "Delete", "Update", "Detach", "Attach", "List", "Put", "Get", ] class BaseElement: """Base Class for all IAM Policy classes""" def json_repr(self): """JSON representation of the class""" raise NotImplementedError def __eq__(self, other): if isinstance(other, self.__class__): return self.json_repr() == other.json_repr() return False def __ne__(self, other): return not self == other def __hash__(self): return hash(self.json_repr()) def __repr__(self): return str(self.json_repr()) class Action(BaseElement): """Action in an IAM Policy.""" def __init__(self, prefix, action): self.action = action self.prefix = prefix def json_repr(self): return ':'.join([self.prefix, self.action]) def _base_action(self): without_prefix = self.action for prefix in BASE_ACTION_PREFIXES: without_prefix = re.sub(prefix, "", without_prefix) without_plural = re.sub(r"s$", "", without_prefix) return without_plural def matching_actions(self, allowed_prefixes): """Return a matching create action for this Action""" if not allowed_prefixes: allowed_prefixes = BASE_ACTION_PREFIXES potential_matches = [Action(prefix=self.prefix, action=action_prefix + self._base_action()) for action_prefix in allowed_prefixes] potential_matches += [Action(prefix=self.prefix, action=action_prefix + self._base_action() + "s") for action_prefix in allowed_prefixes] return [potential_match for potential_match in potential_matches if potential_match in known_iam_actions(self.prefix) and potential_match != self] class Statement(BaseElement): """Statement in an IAM Policy.""" def __init__(self, Action, Effect, Resource): # pylint: disable=redefined-outer-name self.Action = Action # pylint: disable=invalid-name self.Effect = Effect # pylint: disable=invalid-name self.Resource = Resource # pylint: disable=invalid-name def json_repr(self): return { 'Action': self.Action, 'Effect': self.Effect, 'Resource': self.Resource, } def merge(self, other): """Merge two statements into one.""" if self.Effect != other.Effect: raise ValueError("Trying to combine two statements with differing effects: {} {}".format(self.Effect, other.Effect)) effect = self.Effect actions = list(sorted(set(self.Action + other.Action), key=lambda action: action.json_repr())) resources = list(sorted(set(self.Resource + other.Resource))) return Statement( Effect=effect, Action=actions, Resource=resources, ) def __action_list_strings(self): return "-".join([a.json_repr() for a in self.Action]) def __lt__(self, other): if self.Effect != other.Effect: return self.Effect < other.Effect if self.Action != other.Action: # pylint: disable=W0212 return self.__action_list_strings() < other.__action_list_strings() return "".join(self.Resource) < "".join(other.Resource) class PolicyDocument(BaseElement): """IAM Policy Doument.""" def __init__(self, Statement, Version="2012-10-17"): # pylint: disable=redefined-outer-name self.Version = Version # pylint: disable=invalid-name self.Statement = Statement # pylint: disable=invalid-name def json_repr(self): return { 'Version': self.Version, 'Statement': self.Statement } def to_json(self): """Render object into IAM Policy JSON""" return json.dumps(self.json_repr(), cls=IAMJSONEncoder, indent=4, sort_keys=True) class IAMJSONEncoder(json.JSONEncoder): """JSON Encoder using the json_repr functions""" def default(self, o): # pylint: disable=method-hidden if hasattr(o, 'json_repr'): return o.json_repr() return json.JSONEncoder.default(self, o) def _parse_action(action): parts = action.split(":") return Action(parts[0], parts[1]) def _parse_statement(statement): return Statement(Action=[_parse_action(action) for action in statement['Action']], Effect=statement['Effect'], Resource=statement['Resource']) def _parse_statements(json_data): # TODO: jsonData could also be dict, aka one statement; similar things happen in the rest of the policy pylint: disable=fixme # https://github.com/flosell/iam-policy-json-to-terraform/blob/fafc231/converter/decode.go#L12-L22 return [_parse_statement(statement) for statement in json_data] def parse_policy_document(stream): """Parse a stream of JSON data to a PolicyDocument object""" if isinstance(stream, six.string_types): json_dict = json.loads(stream) else: json_dict = json.load(stream) return PolicyDocument(_parse_statements(json_dict['Statement']), Version=json_dict['Version']) def all_known_iam_permissions(): "Return a list of all known IAM actions" with open(os.path.join(os.path.dirname(__file__), 'known-iam-actions.txt')) as iam_file: return {line.rstrip('\n') for line in iam_file.readlines()} def known_iam_actions(prefix): """Return known IAM actions for a prefix, e.g. all ec2 actions""" # This could be memoized for performance improvements knowledge = pipe(all_known_iam_permissions(), mapz(_parse_action), groupbyz(lambda x: x.prefix)) return knowledge.get(prefix, [])