async def get(self, identifier): 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.") log_data = { "function": "ServiceControlPolicyHandler.get", "user": self.user, "message": "Retrieving service control policies for identifier", "identifier": identifier, "user-agent": self.request.headers.get("User-Agent"), "request_id": self.request_uuid, } log.debug(log_data) try: scps = await get_scps_for_account_or_ou(identifier) except Exception as e: sentry_sdk.capture_exception() response = WebResponse(status=Status2.error, status_code=403, errors=[str(e)], data=[]) self.write(response.json()) return response = WebResponse(status=Status2.success, status_code=200, data=scps.__root__) self.write(response.json())
async def put(self, request_id): """ PUT /api/v2/requests/{request_id} """ tags = {"user": self.user} stats.count("RequestDetailHandler.put", tags=tags) log_data = { "function": "RequestDetailHandler.put", "user": self.user, "message": "Incoming request", "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", []): raise MustBeFte("Only FTEs are authorized to view this page.") try: # Validate the request body request_changes = PolicyRequestModificationRequestModel.parse_raw( self.request.body) log_data["message"] = "Parsed request body" log_data["request"] = request_changes.dict() log.debug(log_data) extended_request, last_updated = await self._get_extended_request( request_id, log_data) response = await parse_and_apply_policy_request_modification( extended_request, request_changes, self.user, self.groups, last_updated) except (NoMatchingRequest, InvalidRequestParameter, ValidationError) as e: log_data["message"] = "Validation Exception" log.error(log_data, exc_info=True) sentry_sdk.capture_exception(tags={"user": self.user}) 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 Unauthorized as e: log_data["message"] = "Unauthorized" log.error(log_data, exc_info=True) sentry_sdk.capture_exception(tags={"user": self.user}) stats.count(f"{log_data['function']}.unauthorized", tags={"user": self.user}) self.write_error(403, message=str(e)) if config.get("development"): raise return self.write(response.json()) await self.finish() return
async def get(self, account_id, role_name): 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.") log_data = { "function": "ManagedPoliciesOnRoleHandler.get", "user": self.user, "ip": self.ip, "message": "Retrieving managed policies for role", "user-agent": self.request.headers.get("User-Agent"), "request_id": self.request_uuid, "account_id": account_id, "role_name": role_name, } log.debug(log_data) managed_policy_details = await sync_to_async( get_role_managed_policy_documents)( { "RoleName": role_name }, account_number=account_id, assume_role=config.get("policies.role_name"), region=config.region, ) res = WebResponse( status="success", status_code=200, data=managed_policy_details, ) self.write(res.json())
async def get(self): 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.") results = await handle_resource_type_ahead_request(self) self.write(json.dumps(results))
async def get(self, account_id): """ Retrieve a list of managed policies for an account. """ 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.") all_account_managed_policies = await get_all_iam_managed_policies_for_account( account_id) self.write(json.dumps(all_account_managed_policies))
async def get(self, policy_arn: str): 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.") account_id = policy_arn.split(":")[4] policy_name = policy_arn.split("/")[-1] log_data = { "function": "ManagedPoliciesHandler.get", "user": self.user, "ip": self.ip, "message": "Retrieving managed policy", "user-agent": self.request.headers.get("User-Agent"), "request_id": self.request_uuid, "account_id": account_id, "policy_name": policy_name, "policy_arn": policy_arn, } log.debug(log_data) managed_policy_details = await sync_to_async( get_managed_policy_document)( policy_arn=policy_arn, account_number=account_id, assume_role=config.get("policies.role_name"), region=config.region, retry_max_attempts=2, client_kwargs=config.get("boto3.client_kwargs", {}), ) res = WebResponse( status=Status2.success, status_code=200, data=managed_policy_details, ) self.write(res.json())
async def get(self): """ /api/v1/policyuniverse/autocomplete/?prefix= --- get: description: Supplies autocompleted permissions for the ace code editor. responses: 200: description: Returns a list of the matching permissions. """ 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.") only_filter_services = False if (self.request.arguments.get("only_filter_services") and self.request.arguments.get( "only_filter_services")[0].decode("utf-8") == "true"): only_filter_services = True prefix = self.request.arguments.get("prefix")[0].decode("utf-8") + "*" results = _expand_wildcard_action(prefix) if only_filter_services: # We return known matching services in a format that the frontend expects to see them. We omit the wildcard # character returned by policyuniverse. services = sorted( list({r.split(":")[0].replace("*", "") for r in results})) results = [{"title": service} for service in services] else: results = [dict(permission=r) for r in results] self.write(json.dumps(results)) await self.finish()
async def get(self, account_id, resource_type, region=None, resource_name=None): if not self.user: return 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.") read_only = False can_save_delete = (can_admin_policies(self.user, self.groups),) account_id_for_arn: str = account_id if resource_type == "s3": account_id_for_arn = "" arn = f"arn:aws:{resource_type}:{region or ''}:{account_id_for_arn}:{resource_name}" stats.count( "ResourcePolicyEditHandler.get", tags={"user": self.user, "arn": arn} ) log_data = { "user": self.user, "ip": self.ip, "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}", "message": "Incoming request", "user-agent": self.request.headers.get("User-Agent"), "request_id": self.request_uuid, "arn": arn, } log.debug(log_data) resource_details = await fetch_resource_details( account_id, resource_type, resource_name, region ) # TODO: Get S3 errors for s3 buckets only, else CT errors yesterday = (datetime.today() - timedelta(days=1)).strftime("%Y%m%d") s3_query_url = None if resource_type == "s3": s3_query_url = config.get("s3.bucket_query_url") all_s3_errors = None if s3_query_url: s3_query_url = s3_query_url.format( yesterday=yesterday, bucket_name=f"'{resource_name}'" ) s3_error_topic = config.get("redis.s3_errors", "S3_ERRORS") all_s3_errors = self.red.get(s3_error_topic) s3_errors = [] if all_s3_errors: s3_errors = json.loads(all_s3_errors).get(arn, []) account_ids_to_name = await get_account_id_to_name_mapping() # TODO(ccastrapel/psanders): Make a Swagger spec for this self.write( dict( arn=arn, resource_details=resource_details, account_id=account_id, account_name=account_ids_to_name.get(account_id, None), read_only=read_only, can_save_delete=can_save_delete, s3_errors=s3_errors, error_url=s3_query_url, ) )
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, account_id, resource_type, region=None, resource_name=None): if not self.user: return 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.") read_only = False can_save_delete = (can_admin_policies(self.user, self.groups), ) account_id_for_arn: str = account_id if resource_type == "s3": account_id_for_arn = "" arn = f"arn:aws:{resource_type}:{region or ''}:{account_id_for_arn}:{resource_name}" path = "" if resource_type == "managed_policy": # special case for managed policies path = region or "" if path: arn = f"arn:aws:iam::{account_id}:policy/{path}/{resource_name}" else: arn = f"arn:aws:iam::{account_id}:policy/{resource_name}" stats.count("ResourcePolicyEditHandler.get", tags={ "user": self.user, "arn": arn }) log_data = { "user": self.user, "ip": self.ip, "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}", "message": "Incoming request", "user-agent": self.request.headers.get("User-Agent"), "request_id": self.request_uuid, "arn": arn, } log.debug(log_data) error = "" try: resource_details = await fetch_resource_details( account_id, resource_type, resource_name, region, path) except Exception as e: sentry_sdk.capture_exception() log.error({**log_data, "error": e}, exc_info=True) resource_details = None error = str(e) if not resource_details: self.send_error( 404, message= (f"Unable to retrieve the specified {resource_type} resource: " f"{account_id}/{resource_name}/{region}. {error}", ), ) return # TODO: Get S3 errors for s3 buckets only, else CT errors yesterday = (datetime.today() - timedelta(days=1)).strftime("%Y%m%d") s3_query_url = None if resource_type == "s3": s3_query_url = config.get("s3.bucket_query_url") all_s3_errors = None if s3_query_url: s3_query_url = s3_query_url.format( yesterday=yesterday, bucket_name=f"'{resource_name}'") s3_error_topic = config.get("redis.s3_errors", "S3_ERRORS") all_s3_errors = self.red.get(s3_error_topic) s3_errors = [] if all_s3_errors: s3_errors = json.loads(all_s3_errors).get(arn, []) account_ids_to_name = await get_account_id_to_name_mapping() # TODO(ccastrapel/psanders): Make a Swagger spec for this self.write( dict( arn=arn, resource_details=resource_details, account_id=account_id, account_name=account_ids_to_name.get(account_id, None), read_only=read_only, can_save_delete=can_save_delete, s3_errors=s3_errors, error_url=s3_query_url, config_timeline_url=resource_details.get( "config_timeline_url"), ))
async def get(self, arn): 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.") errors = [] if not arn.startswith("arn:aws:iam::"): errors.append("ARN must start with 'arn:aws:iam::'") principal_name = tornado.escape.xhtml_escape(arn.split("/")[-1]) principal_type = tornado.escape.xhtml_escape( arn.split(":")[5].split("/")[0]) account_id = tornado.escape.xhtml_escape(arn.split(":")[4]) if principal_type not in ["role", "user"]: errors.append( f"Principal type must be role or user. not {principal_type}") log_data = { "function": "ManagedPoliciesOnRoleHandler.get", "user": self.user, "ip": self.ip, "message": "Retrieving managed policies for role", "user-agent": self.request.headers.get("User-Agent"), "request_id": self.request_uuid, "account_id": account_id, "principal_name": principal_name, "principal_type": principal_type, } log.debug(log_data) if errors: log.error({ **log_data, "errors": errors, "message": "Unable to process request" }) res = WebResponse( status=Status2.error, reason="bad_request", status_code=400, errors=errors, ) self.write(res.json()) return if principal_type == "role": managed_policy_details = await sync_to_async( get_role_managed_policy_documents)( { "RoleName": principal_name }, account_number=account_id, assume_role=config.get("policies.role_name"), region=config.region, retry_max_attempts=2, client_kwargs=config.get("boto3.client_kwargs", {}), ) elif principal_type == "user": managed_policy_details = await sync_to_async( get_user_managed_policy_documents)( { "UserName": principal_name }, account_number=account_id, assume_role=config.get("policies.role_name"), region=config.region, retry_max_attempts=2, client_kwargs=config.get("boto3.client_kwargs", {}), ) else: raise Exception("Invalid principal type") res = WebResponse( status=Status2.success, status_code=200, data=managed_policy_details, ) self.write(res.json())