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)
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
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
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)
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"]
def get_service_from_arn(arn): """Given an ARN string, return the service """ result = parse_arn(arn) return result["service"]
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")
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
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()
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()