async def _get_extended_request(self, request_id, log_data): dynamo = UserDynamoHandler(self.user) requests = await dynamo.get_policy_requests(request_id=request_id) if len(requests) == 0: log_data["message"] = "Request with that ID not found" log.warn(log_data) stats.count(f"{log_data['function']}.not_found", tags={"user": self.user}) raise NoMatchingRequest(log_data["message"]) if len(requests) > 1: log_data["message"] = "Multiple requests with that ID found" log.error(log_data) stats.count( f"{log_data['function']}.multiple_requests_found", tags={"user": self.user}, ) raise InvalidRequestParameter(log_data["message"]) request = requests[0] if request.get("version") != "2": # Request format is not compatible with this endpoint version raise InvalidRequestParameter( "Request with that ID is not a v2 request") extended_request = ExtendedRequestModel.parse_obj( request.get("extended_request")) return extended_request, request.get("last_updated")
async def validate_policy_name(policy_name): p = re.compile("^[a-zA-Z0-9+=,.@\\-_]+$") match = p.match(policy_name) if not match: raise InvalidRequestParameter( "The specified value for policyName is invalid. " "It must contain only alphanumeric characters and/or the following: +=,.@_-" )
async def _get_actions_from_groups( action_groups: List[str], permissions_map: Dict[List, Dict[str, List[str]]]) -> List[str]: """Get actions based on "groups" defined in permissions_map TODO(psanders): Move this to a more sensible module :param action_groups: A list of requested CRUD operations to convert into IAM actions :param permissions_map: A mapping of actions associated with the resource type, usually from configuration :return: actions: A list of IAM policy actions """ actions: List[str] = [] for ag in action_groups: for pm in permissions_map: if pm["name"] == ag: actions += pm.get("permissions", []) if not actions: raise InvalidRequestParameter( f"One or more of the passed actions is invalid for the generator type: {action_groups}" ) return actions
async def generate_change_model_array( changes: ChangeGeneratorModelArray, ) -> ChangeModelArray: """ Compiles a ChangeModelArray which includes all of the AWS policies required to satisfy the ChangeGeneratorModelArray request. :param changes: ChangeGeneratorModelArray :return: ChangeModelArray """ change_models = [] inline_iam_policy_statements: List[Dict] = [] primary_principal_arn = None primary_user = None resources = [] for change in changes.changes: # Enforce a maximum of one user per ChangeGeneratorModelArray (aka Policy Request) if not primary_user: primary_user = change.user if primary_user != change.user: raise InvalidRequestParameter( "All changes associated with request must be associated with the same user." ) # Enforce a maximum of one principal ARN per ChangeGeneratorModelArray (aka Policy Request) if not primary_principal_arn: primary_principal_arn = change.principal_arn if primary_principal_arn != change.principal_arn: raise InvalidRequestParameter( "We only support making changes to a single principal ARN per request." ) # Generate inline policy for the change, if applicable inline_policy = ( await _generate_inline_iam_policy_statement_from_change_generator(change) ) if inline_policy and (not inline_policy.get("Action") or not inline_policy.get("Effect") or not inline_policy.get("Resource")): raise InvalidRequestParameter( f"Generated inline policy is invalid. Double-check request parameter: {inline_policy}" ) if inline_policy: # TODO(ccastrapel): Add more details to the ResourceModel when we determine we can use it for something. resource_model = await _generate_resource_model_from_arn( change.resource_arn) # If the resource arn is actually a wildcard, we might not have a valid resource model if resource_model: resources.append(resource_model) inline_iam_policy_statements.append(inline_policy) # TODO(ccastrapel): V2: Generate resource policies for the change, if applicable # Minimize the policy statements to remove redundancy inline_iam_policy_statements = await _minimize_iam_policy_statements( inline_iam_policy_statements) # Attach Sids to each of the statements that will help with identifying who made the request and when. inline_iam_policy_statements = await _attach_sids_to_policy_statements( inline_iam_policy_statements, primary_user) # TODO(ccastrapel): Check if the inline policy statements would be auto-approved and supply that context inline_iam_policy_change_model = await _generate_inline_policy_change_model( primary_principal_arn, resources, inline_iam_policy_statements, primary_user) change_models.append(inline_iam_policy_change_model) return ChangeModelArray.parse_obj({"changes": change_models})
async def handle_resource_type_ahead_request(cls): try: search_string: str = cls.request.arguments.get("search")[0].decode( "utf-8") except TypeError: cls.send_error(400, message="`search` parameter must be defined") return try: resource_type: str = cls.request.arguments.get("resource")[0].decode( "utf-8") except TypeError: cls.send_error(400, message="`resource_type` parameter must be defined") return account_id = None topic_is_hash = True account_id_optional: Optional[List[bytes]] = cls.request.arguments.get( "account_id") if account_id_optional: account_id = account_id_optional[0].decode("utf-8") limit: int = 10 limit_optional: Optional[List[bytes]] = cls.request.arguments.get("limit") if limit_optional: limit = int(limit_optional[0].decode("utf-8")) # By default, we only return the S3 bucket name of a resource and not the full ARN # unless you specifically request it show_full_arn_for_s3_buckets: Optional[bool] = cls.request.arguments.get( "show_full_arn_for_s3_buckets") role_name = False if resource_type == "s3": topic = config.get("redis.s3_bucket_key", "S3_BUCKETS") s3_bucket = config.get("account_resource_cache.s3_combined.bucket") s3_key = config.get( "account_resource_cache.s3_combined.file", "account_resource_cache/cache_s3_combined_v1.json.gz", ) elif resource_type == "sqs": topic = config.get("redis.sqs_queues_key", "SQS_QUEUES") s3_bucket = config.get("account_resource_cache.sqs_combined.bucket") s3_key = config.get( "account_resource_cache.sqs_combined.file", "account_resource_cache/cache_sqs_queues_combined_v1.json.gz", ) elif resource_type == "sns": topic = config.get("redis.sns_topics_key ", "SNS_TOPICS") s3_bucket = config.get( "account_resource_cache.sns_topics_combined.bucket") s3_key = config.get( "account_resource_cache.sns_topics_topics_combined.file", "account_resource_cache/cache_sns_topics_combined_v1.json.gz", ) elif resource_type == "iam_arn": topic = config.get("aws.iamroles_redis_key ", "IAM_ROLE_CACHE") s3_bucket = config.get( "cache_iam_resources_across_accounts.all_roles_combined.s3.bucket") s3_key = config.get( "cache_iam_resources_across_accounts.all_roles_combined.s3.file", "account_resource_cache/cache_all_roles_v1.json.gz", ) elif resource_type == "iam_role": topic = config.get("aws.iamroles_redis_key ", "IAM_ROLE_CACHE") s3_bucket = config.get( "cache_iam_resources_across_accounts.all_roles_combined.s3.bucket") s3_key = config.get( "cache_iam_resources_across_accounts.all_roles_combined.s3.file", "account_resource_cache/cache_all_roles_v1.json.gz", ) role_name = True elif resource_type == "account": topic = None s3_bucket = None s3_key = None topic_is_hash = False elif resource_type == "app": topic = config.get("celery.apps_to_roles.redis_key", "APPS_TO_ROLES") s3_bucket = None s3_key = None topic_is_hash = False else: cls.send_error(404, message=f"Invalid resource_type: {resource_type}") return if not topic and resource_type != "account": raise InvalidRequestParameter("Invalid resource_type specified") if topic and topic_is_hash and s3_key: data = await retrieve_json_data_from_redis_or_s3( redis_key=topic, redis_data_type="hash", s3_bucket=s3_bucket, s3_key=s3_key) elif topic: data = await redis_get(topic) results: List[Dict] = [] unique_roles: List[str] = [] if resource_type == "account": account_and_id_list = [] account_ids_to_names = await get_account_id_to_name_mapping() for account_id, account_name in account_ids_to_names.items(): account_and_id_list.append(f"{account_name} ({account_id})") for account in account_and_id_list: if search_string.lower() in account.lower(): results.append({"title": account}) elif resource_type == "app": results = {} all_role_arns = [] all_role_arns_j = await redis_hgetall( (config.get("aws.iamroles_redis_key", "IAM_ROLE_CACHE"))) if all_role_arns_j: all_role_arns = all_role_arns_j.keys() # ConsoleMe (Account: Test, Arn: arn) # TODO: Make this OSS compatible and configurable try: accounts = await get_account_id_to_name_mapping() except Exception as e: # noqa accounts = {} app_to_role_map = {} if data: app_to_role_map = json.loads(data) seen: Dict = {} seen_roles = {} for app_name, roles in app_to_role_map.items(): if len(results.keys()) > 9: break if search_string.lower() in app_name.lower(): results[app_name] = {"name": app_name, "results": []} for role in roles: account_id = role.split(":")[4] account = accounts.get(account_id, "") parsed_app_name = ( f"{app_name} on {account} ({account_id}) ({role})]") if seen.get(parsed_app_name): continue seen[parsed_app_name] = True seen_roles[role] = True results[app_name]["results"].append({ "title": role, "description": account }) for role in all_role_arns: if len(results.keys()) > 9: break if search_string.lower() in role.lower(): if seen_roles.get(role): continue account_id = role.split(":")[4] account = accounts.get(account_id, "") if not results.get("Unknown App"): results["Unknown App"] = { "name": "Unknown App", "results": [] } results["Unknown App"]["results"].append({ "title": role, "description": account }) else: if not data: return [] for k, v in data.items(): if account_id and k != account_id: continue if role_name: try: r = k.split("role/")[1] except IndexError: continue if search_string.lower() in r.lower(): if r not in unique_roles: unique_roles.append(r) results.append({"title": r}) elif resource_type == "iam_arn": if k.startswith("arn:") and search_string.lower() in k.lower(): results.append({"title": k}) else: list_of_items = json.loads(v) for item in list_of_items: # A Hack to get S3 to show full ARN, and maintain backwards compatibility # TODO: Fix this in V2 of resource specific typeahead endpoints if resource_type == "s3" and show_full_arn_for_s3_buckets: item = f"arn:aws:s3:::{item}" if search_string.lower() in item.lower(): results.append({"title": item, "account_id": k}) if len(results) > limit: break if len(results) > limit: break return results
async def get_formatted_policy_changes(account_id, arn, request): aws = get_plugin_by_name(config.get("plugins.aws", "default_aws"))() existing_role: dict = await aws.fetch_iam_role(account_id, arn, force_refresh=True) policy_changes: list = json.loads(request.get("policy_changes")) formatted_policy_changes = [] # Parse request json and figure out how to present to the page for policy_change in policy_changes: if not policy_change.get("inline_policies"): policy_change["inline_policies"] = [] if len(policy_change.get("inline_policies")) > 1: raise InvalidRequestParameter( "Only one inline policy change at a time is currently supported." ) for inline_policy in policy_change.get("inline_policies"): if policy_change.get("arn") != arn: raise InvalidRequestParameter( "Only one role can be changed in a request") policy_name = inline_policy.get("policy_name") await validate_policy_name(policy_name) policy_document = inline_policy.get("policy_document") old_policy = {} new_policy: bool = False existing_policy_document = {} if request.get("status") == "approved": old_policy = request.get("old_policy", {}) if old_policy: existing_policy_document = json.loads(old_policy)[0] if not old_policy: existing_inline_policies = existing_role["policy"].get( "RolePolicyList", []) existing_policy_document = {} for existing_policy in existing_inline_policies: if existing_policy["PolicyName"] == policy_name: existing_policy_document = existing_policy[ "PolicyDocument"] # Generate dictionary with old / new policy documents diff = DeepDiff(existing_policy_document, policy_document) if not existing_policy_document: new_policy = True formatted_policy_changes.append({ "name": policy_name, "old": existing_policy_document, "new": policy_document, "diff": diff, "new_policy": new_policy, }) assume_role_policy_document = policy_change.get( "assume_role_policy_document") if assume_role_policy_document: if policy_change.get("arn") != arn: raise InvalidRequestParameter( "Only one role can be changed in a request") existing_ar_policy = existing_role["policy"][ "AssumeRolePolicyDocument"] old_policy = request.get("old_policy", {}) if old_policy: existing_ar_policy = json.loads(old_policy)[0] diff = DeepDiff( existing_ar_policy, assume_role_policy_document.get("assume_role_policy_document"), ) formatted_policy_changes.append({ "name": "AssumeRolePolicyDocument", "old": existing_ar_policy, "new": assume_role_policy_document.get("assume_role_policy_document"), "new_policy": False, "diff": diff, }) resource_policy_documents = request.get("resource_policies") if resource_policy_documents: for resource in resource_policy_documents: existing_policy_document = None # TODO: make this actually fetch the resource policy # existing_policy_document = aws.fetch_resource_policy() new_policy_document = resource["policy_document"] diff = DeepDiff(existing_policy_document, new_policy_document) formatted_policy_changes.append({ "name": "ResourcePolicy", "old": existing_policy_document, "new": new_policy_document, "new_policy": not existing_policy_document, "diff": diff, }) return {"changes": formatted_policy_changes, "role": existing_role}