Ejemplo n.º 1
0
    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")
Ejemplo n.º 2
0
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: +=,.@_-"
        )
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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})
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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}