Esempio n. 1
0
async def get_eligible_role_details(
    eligible_roles: List[str],
) -> EligibleRolesModelArray:
    account_ids_to_name = await get_account_id_to_name_mapping()
    eligible_roles_detailed = []
    for role in eligible_roles:
        arn_parsed = parse_arn(role)
        account_id = arn_parsed["account"]
        role_name = (
            arn_parsed["resource_path"]
            if arn_parsed["resource_path"]
            else arn_parsed["resource"]
        )
        account_friendly_name = account_ids_to_name.get(account_id, "Unknown")
        role_apps = await get_app_details_for_role(role)
        eligible_roles_detailed.append(
            EligibleRolesModel(
                arn=role,
                account_id=account_id,
                account_friendly_name=account_friendly_name,
                role_name=role_name,
                apps=role_apps,
            )
        )

    return EligibleRolesModelArray(roles=eligible_roles_detailed)
Esempio n. 2
0
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
Esempio n. 3
0
    async def post(self):
        """
        POST /api/v2/request

        Request example JSON: (Request Schema is RequestCreationModel in models.py)

        {
          "changes": {
            "changes": [
              {
                "principal_arn": "arn:aws:iam::123456789012:role/curtisTestRole1",
                "change_type": "inline_policy",
                "action": "attach",
                "policy": {
                  "policy_document": {
                    "Version": "2012-10-17",
                    "Statement": [
                      {
                        "Action": [
                          "s3:ListMultipartUploadParts*",
                          "s3:ListBucket"
                        ],
                        "Effect": "Allow",
                        "Resource": [
                          "arn:aws:s3:::curtis-nflx-test/*",
                          "arn:aws:s3:::curtis-nflx-test"
                        ],
                        "Sid": "cmccastrapel159494014dsd1shak"
                      },
                      {
                        "Action": [
                          "ec2:describevolumes",
                          "ec2:detachvolume",
                          "ec2:describelicenses",
                          "ec2:AssignIpv6Addresses",
                          "ec2:reportinstancestatus"
                        ],
                        "Effect": "Allow",
                        "Resource": [
                          "*"
                        ],
                        "Sid": "cmccastrapel1594940141hlvvv"
                      },
                      {
                        "Action": [
                          "sts:AssumeRole"
                        ],
                        "Effect": "Allow",
                        "Resource": [
                          "arn:aws:iam::123456789012:role/curtisTestInstanceProfile"
                        ],
                        "Sid": "cmccastrapel1596483596easdits"
                      }
                    ]
                  }
                }
              },
              {
                "principal_arn": "arn:aws:iam::123456789012:role/curtisTestRole1",
                "change_type": "assume_role_policy",
                "policy": {
                  "policy_document": {
                    "Statement": [
                      {
                        "Action": "sts:AssumeRole",
                        "Effect": "Allow",
                        "Principal": {
                          "AWS": "arn:aws:iam::123456789012:role/consolemeInstanceProfile"
                        },
                        "Sid": "AllowConsoleMeProdAssumeRolses"
                      }
                    ],
                    "Version": "2012-10-17"
                  }
                }
              },
              {
                "principal_arn": "arn:aws:iam::123456789012:role/curtisTestRole1",
                "change_type": "managed_policy",
                "policy_name": "ApiProtect",
                "action": "attach",
                "arn": "arn:aws:iam::123456789012:policy/ApiProtect"
              },
              {
                "principal_arn": "arn:aws:iam::123456789012:role/curtisTestRole1",
                "change_type": "managed_policy",
                "policy_name": "TagProtect",
                "action": "detach",
                "arn": "arn:aws:iam::123456789012:policy/TagProtect"
              },
              {
                "principal_arn": "arn:aws:iam::123456789012:role/curtisTestRole1",
                "change_type": "inline_policy",
                "policy_name": "random_policy254",
                "action": "attach",
                "policy": {
                  "policy_document": {
                    "Version": "2012-10-17",
                    "Statement": [
                      {
                        "Action": [
                          "ec2:AssignIpv6Addresses"
                        ],
                        "Effect": "Allow",
                        "Resource": [
                          "*"
                        ],
                        "Sid": "cmccastrapel1594940141shakabcd"
                      }
                    ]
                  }
                }
              }
            ]
          },
          "justification": "testing this out.",
          "admin_auto_approve": false
        }

        Response example JSON: (Response Schema is RequestCreationResponse in models.py)

        {
            "errors": 1,
            "request_created": true,
            "request_id": "0c9fb298-c8ea-4d50-917c-3212da07b3ad",
            "action_results": [
                {
                    "status": "success",
                    "message": "Success description"
                },
                {
                    "status": "error",
                    "message": "Error description"
                }
            ]
        }


        """

        if config.get("policy_editor.disallow_contractors",
                      True) and self.contractor:
            if self.user not in config.get(
                    "groups.can_bypass_contractor_restrictions", []):
                raise MustBeFte("Only FTEs are authorized to view this page.")

        tags = {"user": self.user}
        stats.count("RequestHandler.post", tags=tags)
        log_data = {
            "function":
            f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}",
            "user": self.user,
            "message": "Create request initialization",
            "user-agent": self.request.headers.get("User-Agent"),
            "request_id": self.request_uuid,
            "ip": self.ip,
            "admin_auto_approved": False,
            "probe_auto_approved": False,
        }
        log.debug(log_data)
        try:
            # Validate the model
            changes = RequestCreationModel.parse_raw(self.request.body)
            extended_request = await generate_request_from_change_model_array(
                changes, self.user)
            log_data["request"] = extended_request.dict()
            log.debug(log_data)
            admin_approved = False
            approval_probe_approved = False

            # TODO: Provide a note to the requester that admin_auto_approve will apply the requested policies only.
            # It will not automatically apply generated policies. The administrative user will need to visit the policy
            # Request page to do this manually.
            if changes.admin_auto_approve:
                # make sure user is allowed to use admin_auto_approve
                can_manage_policy_request = (can_admin_policies(
                    self.user, self.groups), )
                if can_manage_policy_request:
                    extended_request.request_status = RequestStatus.approved
                    admin_approved = True
                    extended_request.reviewer = self.user
                    self_approval_comment = CommentModel(
                        id=str(uuid.uuid4()),
                        timestamp=int(time.time()),
                        user_email=self.user,
                        user=extended_request.requester_info,
                        last_modified=int(time.time()),
                        text=f"Self-approved by admin: {self.user}",
                    )
                    extended_request.comments.append(self_approval_comment)
                    log_data["admin_auto_approved"] = True
                    log_data["request"] = extended_request.dict()
                    log.debug(log_data)
                    stats.count(
                        f"{log_data['function']}.post.admin_auto_approved",
                        tags={"user": self.user},
                    )
                else:
                    # someone is trying to use admin bypass without being an admin, don't allow request to proceed
                    stats.count(
                        f"{log_data['function']}.post.unauthorized_admin_bypass",
                        tags={"user": self.user},
                    )
                    log_data[
                        "message"] = "Unauthorized user trying to use admin bypass"
                    log.error(log_data)
                    await write_json_error("Unauthorized", obj=self)
                    return
            else:
                # If admin auto approve is false, check for auto-approve probe eligibility
                is_eligible_for_auto_approve_probe = (
                    await is_request_eligible_for_auto_approval(
                        extended_request, self.user))
                # If we have only made requests that are eligible for auto-approval probe, check against them
                if is_eligible_for_auto_approve_probe:
                    should_auto_approve_request = await should_auto_approve_policy_v2(
                        extended_request, self.user, self.groups)
                    if should_auto_approve_request["approved"]:
                        extended_request.request_status = RequestStatus.approved
                        approval_probe_approved = True
                        stats.count(
                            f"{log_data['function']}.probe_auto_approved",
                            tags={"user": self.user},
                        )
                        approving_probes = []
                        for approving_probe in should_auto_approve_request[
                                "approving_probes"]:
                            approving_probe_comment = CommentModel(
                                id=str(uuid.uuid4()),
                                timestamp=int(time.time()),
                                user_email=
                                f"Auto-Approve Probe: {approving_probe['name']}",
                                last_modified=int(time.time()),
                                text=
                                f"Policy {approving_probe['policy']} auto-approved by probe: {approving_probe['name']}",
                            )
                            extended_request.comments.append(
                                approving_probe_comment)
                            approving_probes.append(approving_probe["name"])
                        extended_request.reviewer = (
                            f"Auto-Approve Probe: {','.join(approving_probes)}"
                        )
                        log_data["probe_auto_approved"] = True
                        log_data["request"] = extended_request.dict()
                        log.debug(log_data)

            dynamo = UserDynamoHandler(self.user)
            request = await dynamo.write_policy_request_v2(extended_request)
            log_data["message"] = "New request created in Dynamo"
            log_data["request"] = extended_request.dict()
            log_data["dynamo_request"] = request
            log.debug(log_data)
        except (InvalidRequestParameter, ValidationError) as e:
            log_data["message"] = "Validation Exception"
            log.error(log_data, exc_info=True)
            stats.count(f"{log_data['function']}.validation_exception",
                        tags={"user": self.user})
            self.write_error(400, message="Error validating input: " + str(e))
            if config.get("development"):
                raise
            return
        except Exception as e:
            log_data[
                "message"] = "Unknown Exception occurred while parsing request"
            log.error(log_data, exc_info=True)
            stats.count(f"{log_data['function']}.exception",
                        tags={"user": self.user})
            sentry_sdk.capture_exception(tags={"user": self.user})
            self.write_error(500, message="Error parsing request: " + str(e))
            if config.get("development"):
                raise
            return

        # If here, request has been successfully created
        response = RequestCreationResponse(
            errors=0,
            request_created=True,
            request_id=extended_request.id,
            request_url=f"/policies/request/{extended_request.id}",
            action_results=[],
        )

        # If approved is true due to an auto-approval probe or admin auto-approval, apply the non-autogenerated changes
        if extended_request.request_status == RequestStatus.approved:
            for change in extended_request.changes.changes:
                if change.autogenerated:
                    continue
                policy_request_modification_model = (
                    PolicyRequestModificationRequestModel.parse_obj({
                        "modification_model": {
                            "command": "apply_change",
                            "change_id": change.id,
                        }
                    }))
                policy_apply_response = (
                    await parse_and_apply_policy_request_modification(
                        extended_request,
                        policy_request_modification_model,
                        self.user,
                        self.groups,
                        int(time.time()),
                        approval_probe_approved,
                    ))
                response.errors = policy_apply_response.errors
                response.action_results = policy_apply_response.action_results

            # Update in dynamo
            await dynamo.write_policy_request_v2(extended_request)
            account_id = await get_resource_account(extended_request.arn)

            # Force a refresh of the role in Redis/DDB
            arn_parsed = parse_arn(extended_request.arn)
            if arn_parsed["service"] == "iam" and arn_parsed[
                    "resource"] == "role":
                await aws.fetch_iam_role(account_id,
                                         extended_request.arn,
                                         force_refresh=True)
            log_data["request"] = extended_request.dict()
            log_data["message"] = "Applied changes based on approved request"
            log_data["response"] = response.dict()
            log.debug(log_data)

        await aws.send_communications_new_policy_request(
            extended_request, admin_approved, approval_probe_approved)
        self.write(response.json())
        await self.finish()
        await cache_all_policy_requests()
        return
Esempio n. 4
0
    async def get(self, request_id):
        """
        GET /api/v2/requests/{request_id}
        """
        tags = {"user": self.user}
        stats.count("RequestDetailHandler.get", tags=tags)
        log_data = {
            "function":
            f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}",
            "user": self.user,
            "message": "Get request details",
            "user-agent": self.request.headers.get("User-Agent"),
            "request_id": self.request_uuid,
            "policy_request_id": request_id,
        }
        log.debug(log_data)

        if config.get("policy_editor.disallow_contractors",
                      True) and self.contractor:
            if self.user not in config.get(
                    "groups.can_bypass_contractor_restrictions", []):
                self.write_error(
                    403, message="Only FTEs are authorized to view this page.")
                return

        try:
            extended_request, last_updated = await self._get_extended_request(
                request_id, log_data)
        except InvalidRequestParameter as e:
            sentry_sdk.capture_exception(tags={"user": self.user})
            self.write_error(400, message="Error validating input: " + str(e))
            return
        except NoMatchingRequest as e:
            sentry_sdk.capture_exception(tags={"user": self.user})
            self.write_error(404, message="Error getting request:" + str(e))
            return
        # Run these tasks concurrently.
        concurrent_results = await asyncio.gather(
            populate_old_policies(extended_request, self.user),
            populate_cross_account_resource_policies(extended_request,
                                                     self.user),
        )
        extended_request = concurrent_results[0]

        populate_cross_account_resource_policies_result = concurrent_results[1]

        if populate_cross_account_resource_policies_result["changed"]:
            extended_request = populate_cross_account_resource_policies_result[
                "extended_request"]
            # Update in dynamo with the latest resource policy changes
            dynamo = UserDynamoHandler(self.user)
            updated_request = await dynamo.write_policy_request_v2(
                extended_request)
            last_updated = updated_request.get("last_updated")

        can_approve_reject = (can_admin_policies(self.user, self.groups), )
        can_update_cancel = await can_update_cancel_requests_v2(
            extended_request.requester_email, self.user, self.groups)
        can_move_back_to_pending = await can_move_back_to_pending_v2(
            extended_request, last_updated, self.user, self.groups)

        # In the future request_specific_config will have specific approvers for specific changes based on ABAC
        request_specific_config = {
            "can_approve_reject": can_approve_reject,
            "can_update_cancel": can_update_cancel,
            "can_move_back_to_pending": can_move_back_to_pending,
        }

        template = None
        # Force a refresh of the role in Redis/DDB
        arn_parsed = parse_arn(extended_request.arn)
        if arn_parsed["service"] == "iam" and arn_parsed["resource"] == "role":
            iam_role = await aws.fetch_iam_role(arn_parsed["account"],
                                                extended_request.arn)
            template = iam_role.get("templated")
        response = {
            "request": extended_request.json(),
            "last_updated": last_updated,
            "request_config": request_specific_config,
            "template": template,
        }

        self.write(response)
Esempio n. 5
0
def get_resource_from_arn(arn):
    """Given an ARN, parse it according to ARN namespacing and return the resource. 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"]
Esempio n. 6
0
def get_service_from_arn(arn):
    """Given an ARN string, return the service """
    result = parse_arn(arn)
    return result["service"]
Esempio n. 7
0
 def test_parse_arn(self):
     """util.arns.parse_arn: Ensure that invalid ARNs raise a proper exception message"""
     with self.assertRaises(Exception):
         parse_arn("aaa")
Esempio n. 8
0
    def get_rendered_policy(self, minimize=None):
        """
        Get the JSON rendered policy

        Arguments:
            minimize: Reduce the character count of policies without creating overlap with other action names
        Returns:
            Dictionary: The IAM Policy JSON
        """
        statements = []
        # Only set the actions to lowercase if minimize is provided
        all_actions = get_all_actions(lowercase=True)

        # render the policy
        sids_to_be_changed = []
        for sid in self.sids:
            temp_actions = self.sids[sid]["actions"]
            if len(temp_actions) == 0:
                logger.debug(f"No actions for sid {sid}")
                continue
            actions = []
            if self.exclude_actions:
                for temp_action in temp_actions:
                    if temp_action.lower() in self.exclude_actions:
                        logger.debug(f"\tExcluded action: {temp_action}")
                    else:
                        if temp_action not in actions:
                            actions.append(temp_action)
            else:
                actions = temp_actions
            # temp_actions.clear()
            match_found = False
            if minimize is not None and isinstance(minimize, int):
                logger.debug("Minimizing statements...")
                actions = minimize_statement_actions(actions,
                                                     all_actions,
                                                     minchars=minimize)
                # searching in the existing statements
                # further minimizing the the output
                for stmt in statements:
                    if stmt["Resource"] == self.sids[sid]["arn"]:
                        stmt["Action"].extend(actions)
                        match_found = True
                        sids_to_be_changed.append(stmt["Sid"])
                        break
            logger.debug(f"Adding statement with SID {sid}")
            logger.debug(f"{sid} SID has the actions: {actions}")
            logger.debug(
                f"{sid} SID has the resources: {self.sids[sid]['arn']}")

            if not match_found:
                statements.append({
                    "Sid": sid,
                    "Effect": "Allow",
                    "Action": actions,
                    "Resource": self.sids[sid]["arn"],
                })

        if sids_to_be_changed:
            for stmt in statements:
                if stmt['Sid'] in sids_to_be_changed:
                    arn_details = parse_arn(stmt['Resource'][0])
                    resource_path = arn_details.get("resource_path")
                    resource_sid_segment = strip_special_characters(
                        f"{arn_details['resource']}{resource_path}")
                    stmt['Sid'] = create_policy_sid_namespace(
                        arn_details['service'], "Mult", resource_sid_segment)

        policy = {"Version": POLICY_LANGUAGE_VERSION, "Statement": statements}
        return policy
Esempio n. 9
0
    async def get(self):
        """
        /api/v2/get_resource_url - Endpoint used to get an URL from an ARN
        ---
        get:
            description: Get the resource URL for ConsoleMe, given an ARN
            responses:
                200:
                    description: Returns a URL generated from the ARN in JSON form
                400:
                    description: Malformed Request
                403:
                    description: Forbidden
        """
        self.user: str = self.requester["email"]
        arn: str = self.get_argument("arn", None)
        log_data = {
            "function":
            f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}",
            "user": self.user,
            "arn": arn,
            "message": "Generating URL for resource",
            "user-agent": self.request.headers.get("User-Agent"),
            "request_id": self.request_uuid,
        }
        log.debug(log_data)
        stats.count("GetResourceURL.get", tags={"user": self.user})
        if not arn:
            generic_error_message: str = "Missing required parameter"
            errors = ["arn is a required parameter"]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 404, "missing_data",
                                                log_data)
            return

        try:
            # parse_arn will raise an exception on invalid arns
            parse_arn(arn)
            resource_url = await get_url_for_resource(arn)
            if not resource_url:
                raise ValueError(
                    "This resource type is currently not supported")
        except (ResourceNotFound, ValueError) as e:
            generic_error_message: str = "Unsupported data"
            errors = [str(e)]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 404, "invalid_data",
                                                log_data)
            return
        except Exception as e:
            generic_error_message: str = "Malformed data"
            errors = [str(e)]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 404, "malformed_data",
                                                log_data)
            return

        res = WebResponse(
            status="success",
            status_code=200,
            message="Successfully generated URL for ARN",
            data={"url": resource_url},
        )

        self.write(res.json())
        await self.finish()
Esempio n. 10
0
    async def get(self):
        """
        /api/v2/get_resource_url - Endpoint used to get an URL from an ARN
        ---
        get:
            description: Get the resource URL for ConsoleMe, given an ARN
            responses:
                200:
                    description: Returns a URL generated from the ARN in JSON form
                400:
                    description: Malformed Request
                403:
                    description: Forbidden
        """
        self.user: str = self.requester["email"]
        arn: str = self.get_argument("arn", None)
        log_data = {
            "function":
            f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}",
            "user": self.user,
            "arn": arn,
            "message": "Generating URL for resource",
            "user-agent": self.request.headers.get("User-Agent"),
            "request_id": self.request_uuid,
        }
        log.debug(log_data)
        stats.count("GetResourceURL.get", tags={"user": self.user})
        if not arn:
            generic_error_message: str = "Missing required parameter"
            errors = ["arn is a required parameter"]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 404, "missing_data",
                                                log_data)
            return

        try:
            # parse_arn will raise an exception on invalid arns
            parse_arn(arn)

            resources_from_aws_config_redis_key = config.get(
                "aws_config_cache.redis_key", "AWSCONFIG_RESOURCE_CACHE")
            if not red.exists(resources_from_aws_config_redis_key):
                # This will force a refresh of our redis cache if the data exists in S3
                await retrieve_json_data_from_redis_or_s3(
                    redis_key=resources_from_aws_config_redis_key,
                    s3_bucket=config.get(
                        "aws_config_cache_combined.s3.bucket"),
                    s3_key=config.get(
                        "aws_config_cache_combined.s3.file",
                        "aws_config_cache_combined/aws_config_resource_cache_combined_v1.json.gz",
                    ),
                    redis_data_type="hash",
                )
            resource_info = await redis_hget(
                resources_from_aws_config_redis_key, arn)
            if not resource_info:
                raise ValueError("Resource not found in organization cache")
            resource_url = await get_url_for_resource(arn)
            if not resource_url:
                raise ValueError(
                    "This resource type is currently not supported")
        except (ResourceNotFound, ValueError) as e:
            generic_error_message: str = "Unsupported data"
            errors = [str(e)]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 404, "invalid_data",
                                                log_data)
            return
        except Exception as e:
            generic_error_message: str = "Malformed data"
            errors = [str(e)]
            await handle_generic_error_response(self, generic_error_message,
                                                errors, 404, "malformed_data",
                                                log_data)
            return

        res = WebResponse(
            status="success",
            status_code=200,
            message="Successfully generated URL for ARN",
            data={"url": resource_url},
        )

        self.write(res.json())
        await self.finish()