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 handle_generic_error_response( request, message: str, errors: List[str], status_code: int, reason: str, log_data: Dict[str, Any], ) -> bool: """ Args: request: Tornado web request message: Message to be logged reason: One line reason for the response (easier for frontend to parse) errors: List of errors to be logged, and to be returned to user status_code: Status code to return to end-user log_data: Dictionary of data to log, typically containing function and information about the user. Returns: boolean """ log.error({**log_data, "message": message, "errors": errors}) res = WebResponse(status="error", status_code=status_code, errors=errors, reason=reason) request.set_status(status_code) request.write(res.json(exclude_unset=True)) await request.finish() return True
async def get(self): log_data = { "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}", "message": "Attempting to log out user", "user-agent": self.request.headers.get("User-Agent"), "ip": self.ip, } if not config.get("auth.set_auth_cookie"): await handle_generic_error_response( self, "Unable to log out", [("Configuration value `auth.set_auth_cookie` is not enabled. " "ConsoleMe isn't able to delete an auth cookie if setting auth " "cookies is not enabled.")], 400, "logout_failure", log_data, ) return cookie_name: str = config.get("auth_cookie_name", "consoleme_auth") if not cookie_name: await handle_generic_error_response( self, "Unable to log out", [("Configuration value `auth_cookie_name` is not set. " "ConsoleMe isn't able to delete an auth cookie if the auth cookie name " "is not known.")], 400, "logout_failure", log_data, ) return self.clear_cookie(cookie_name) extra_auth_cookies: list = config.get("auth.extra_auth_cookies", []) for cookie in extra_auth_cookies: self.clear_cookie(cookie) redirect_url: str = config.get("auth.logout_redirect_url", "/") res = WebResponse( status="redirect", redirect_url=redirect_url, status_code=200, reason="logout_redirect", message= "User has successfully logged out. Redirecting to landing page", ) log.debug({**log_data, "message": "Successfully logged out user."}) self.write(res.json())
async def get(self): """ GET /api/v2/audit/roles """ log_data = { "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}", "user-agent": self.request.headers.get("User-Agent"), "request_id": self.request_uuid, } page = self.get_argument("page", "0") try: page = int(page) except ValueError: log_data["message"] = f"invalid value for page: {page}" log.warning(log_data) page = 0 count = self.get_argument("count", "1000") try: count = int(count) except ValueError: log_data["message"] = f"invalid value for count: {count}" log.warning(log_data) count = 1000 if page < 0: page = 0 if count <= 0: count = 1000 app_name = self.requester.get("name") or self.requester.get("username") stats.count( "AuditRoleHandler.get", tags={ "requester": app_name, }, ) roles = await credential_mapping.all_roles( paginate=True, page=page, count=count ) total_roles = await credential_mapping.number_roles() start = page * count end = start + count if end >= total_roles: end = total_roles - 1 roles = roles[start:end] self.write( WebResponse( status=Status2.success, status_code=200, data=roles, page=page, total=total_roles, count=len(roles), last_page=_get_last_page(total_roles, count), ).json(exclude_unset=True) )
async def get(self, account_id, role_name): """ GET /api/v2/audit/roles/{accountNumber}/{roleName}/access """ log_data = { "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}", "user-agent": self.request.headers.get("User-Agent"), "request_id": self.request_uuid, "account_id": account_id, "role_name": role_name, } app_name = self.requester.get("name") or self.requester.get("username") stats.count( "RoleAccessHandler.get", tags={ "requester": app_name, "account_id": account_id, "role_name": role_name, }, ) groups = await credential_mapping.determine_role_authorized_groups( account_id, role_name ) if not groups: log_data[ "message" ] = f"No authorized groups found for {role_name} in {account_id}" log.warning(log_data) self.set_status(404) self.write( WebResponse( status=Status2.error, status_code=404, message="No groups found for requested role", ).json(exclude_unset=True) ) return self.write( WebResponse(status=Status2.success, status_code=200, data=groups).json( exclude_unset=True ) )
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 get(self): try: notification_response: GetNotificationsForUserResponse = ( await get_notifications_for_user(self.user, self.groups)) notifications: List[ ConsoleMeUserNotification] = notification_response.notifications response = WebResponse( status="success", status_code=200, data={ "unreadNotificationCount": notification_response.unread_count, "notifications": notifications, }, ) self.write(response.json()) except Exception as e: sentry_sdk.capture_exception() self.set_status(500) response = WebResponse(status=Status2.error, status_code=500, errors=[str(e)], data=[]) self.write(response.json()) return
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): """ GET /api/v2/audit/roles """ app_name = self.requester.get("name") or self.requester.get("username") stats.count( "AuditRoleHandler.get", tags={ "requester": app_name, }, ) roles = await credential_mapping.all_roles() self.write( WebResponse(status=Status2.success, status_code=200, data=roles).json(exclude_unset=True))
async def post(self): # TODO: Send verification e-mail to proposed user log_data = { "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}", "message": "Attempting to register user", "user-agent": self.request.headers.get("User-Agent"), } generic_error_message: str = "User registration failed" # Fail if getting users by password is not enabled if not config.get("auth.get_user_by_password"): errors = [ "Expected configuration `auth.get_user_by_password`, but it is not enabled." ] await handle_generic_error_response(self, generic_error_message, errors, 403, "not_configured", log_data) return # Fail if user registration not allowed if not config.get("auth.allow_user_registration"): errors = [ "Expected configuration `auth.allow_user_registration`, but it is not enabled." ] await handle_generic_error_response(self, generic_error_message, errors, 403, "not_configured", log_data) return registration_attempt = RegistrationAttemptModel.parse_raw( self.request.body) log_data["username"] = registration_attempt.username # Fail if username not valid email address try: if not validate_email(registration_attempt.username): errors = ["Username must be a valid e-mail address."] await handle_generic_error_response( self, generic_error_message, errors, 403, "invalid_request", log_data, ) return except Exception as e: sentry_sdk.capture_exception() await handle_generic_error_response(self, generic_error_message, [str(e)], 403, "invalid_request", log_data) return # Fail if user already exists if await self.ddb.get_user(registration_attempt.username): errors = ["User already exists"] await handle_generic_error_response(self, generic_error_message, errors, 403, "invalid_request", log_data) return # Fails if password is not strong enough. password_strength_errors = await check_password_strength( registration_attempt.password) if password_strength_errors: await handle_generic_error_response( self, password_strength_errors["message"], password_strength_errors["errors"], 403, "weak_password", log_data, ) return self.ddb.create_user(registration_attempt.username, registration_attempt.password) res = WebResponse( status="success", status_code=200, message= f"Successfully created user {registration_attempt.username}.", ) self.write(res.json())
async def post(self): log_data = { "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}", "user": self.user, "message": "Create/Update User", "user-agent": self.request.headers.get("User-Agent"), "request_id": self.request_uuid, "ip": self.ip, } generic_error_message = "Unable to create/update user" log.debug(log_data) # Checks authz levels of current user if not can_admin_all(self.user, self.groups): errors = ["User is not authorized to access this endpoint."] await handle_generic_error_response(self, generic_error_message, errors, 403, "unauthorized", log_data) return request = UserManagementModel.parse_raw(self.request.body) log_data["requested_user"] = request.username if request.user_management_action.value == "create": log.debug({ **log_data, "message": "Creating user", "requested_user": request.username, "requested_groups": request.groups, }) # Fails if password is not strong enough. password_strength_errors = await check_password_strength( request.password) if password_strength_errors: await handle_generic_error_response( self, password_strength_errors["message"], password_strength_errors["errors"], 403, "weak_password", log_data, ) return self.ddb.create_user( request.username, request.password, request.groups, ) res = WebResponse( status="success", status_code=200, message=f"Successfully created user {request.username}.", ) self.write(res.json()) return elif request.user_management_action.value == "update": log.debug({ **log_data, "message": "Updating user", "requested_user": request.username, "requested_groups": request.groups, }) if request.password: # Fails if password is not strong enough. password_strength_errors = await check_password_strength( request.password) if password_strength_errors: await handle_generic_error_response( self, password_strength_errors["message"], password_strength_errors["errors"], 403, "weak_password", log_data, ) return self.ddb.update_user( request.username, request.password, request.groups, ) res = WebResponse( status="success", status_code=200, message=f"Successfully updated user {request.username}.", ) self.write(res.json()) return elif request.user_management_action.value == "delete": log.debug({ **log_data, "message": "Deleting user", "requested_user": request.username, }) self.ddb.delete_user(request.username, ) res = WebResponse( status="success", status_code=200, message=f"Successfully deleted user {request.username}.", ) self.write(res.json()) return else: errors = ["Change type is not supported by this endpoint."] await handle_generic_error_response(self, generic_error_message, errors, 403, "invalid_request", log_data) return
async def post(self): log_data = { "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}", "message": "Attempting to authenticate User", "user-agent": self.request.headers.get("User-Agent"), } generic_error_message = "Authentication failed" if not config.get("auth.get_user_by_password"): errors = [ "Expected configuration `auth.get_user_by_password`, but it is not enabled." ] await handle_generic_error_response(self, generic_error_message, errors, 403, "not_configured", log_data) return # Auth cookie must be set to use password authentication. if not config.get("auth.set_auth_cookie"): errors = [ "Expected configuration `auth.set_auth_cookie`, but it is not enabled." ] await handle_generic_error_response(self, generic_error_message, errors, 403, "not_configured", log_data) return login_attempt = LoginAttemptModel.parse_raw(self.request.body) log_data["username"] = login_attempt.username log_data["after_redirect_uri"] = login_attempt.after_redirect_uri authenticated_response: AuthenticationResponse = ( await self.ddb.authenticate_user(login_attempt)) if not authenticated_response.authenticated: # Wait 1 second to protect from single-host brute-force await asyncio.sleep(1) await handle_generic_error_response( self, generic_error_message, authenticated_response.errors, 403, "authentication_failure", log_data, ) return # Make and set jwt for user expiration = datetime.utcnow().replace(tzinfo=pytz.UTC) + timedelta( minutes=config.get("jwt.expiration_minutes", 60)) encoded_cookie = await generate_jwt_token( authenticated_response.username, authenticated_response.groups, exp=expiration, ) self.set_cookie( config.get("auth_cookie_name", "consoleme_auth"), encoded_cookie, expires=expiration, secure=config.get( "auth.cookie.secure", True if "https://" in config.get("url") else False, ), httponly=config.get("auth.cookie.httponly", True), samesite=config.get("auth.cookie.samesite", True), ) res = WebResponse( status="redirect", redirect_url=login_attempt.after_redirect_uri, status_code=200, reason="authenticated_redirect", message= "User has successfully authenticated. Redirecting to their intended destination.", ) self.write(res.json())
async def put(self): """ Allows an "authorized user" (Any user the notification is intended for) to mark the notification as read/unread or hidden/unhidden for themselves or all other notification recipients :return: """ change = ConsoleMeNotificationUpdateRequest.parse_raw( self.request.body) errors = [] for untrusted_notification in change.notifications: notification = await fetch_notification( untrusted_notification.predictable_id) if not notification: errors.append("Unable to find matching notification") continue authorized = is_in_group(self.user, self.groups, notification.users_or_groups) if not authorized: errors.append( f"Unauthorized because user is not associated with notification: {notification.predictable_id}" ) continue if (change.action == ConsoleMeNotificationUpdateAction. toggle_read_for_current_user): if self.user in notification.read_by_users: # Mark as unread notification.read_by_users.remove(self.user) else: # Mark as read notification.read_by_users.append(self.user) elif (change.action == ConsoleMeNotificationUpdateAction.toggle_read_for_all_users): # Mark or unmark notification as `read_by_all`. If unmarked, # ConsoleMe will fall back to `notification.read_by_user` to determine if # a given user has read the notification notification.read_by_all = not notification.read_by_all elif (change.action == ConsoleMeNotificationUpdateAction. toggle_hidden_for_current_user): if self.user in notification.hidden_for_users: # Unmark as hidden notification.hidden_for_users.remove(self.user) else: # Mark as hidden notification.hidden_for_users.append(self.user) elif (change.action == ConsoleMeNotificationUpdateAction. toggle_hidden_for_all_users): # Mark or unmark as "Hidden for all users". If unmarked, falls back to `hidden_for_users.read_by_user` # to determine whether to show the notification to a given user notification.hidden_for_all = not notification.hidden_for_all else: raise Exception("Unknown or unsupported change action.") await write_notification(notification) try: # Retrieve and return updated notifications for user notification_response: GetNotificationsForUserResponse = ( await get_notifications_for_user(self.user, self.groups, force_refresh=True)) notifications: List[ ConsoleMeUserNotification] = notification_response.notifications response = WebResponse( status="success", status_code=200, data={ "unreadNotificationCount": notification_response.unread_count, "notifications": notifications, }, ) self.write(response.json()) except Exception as e: sentry_sdk.capture_exception() self.set_status(500) response = WebResponse(status=Status2.error, status_code=500, errors=[str(e)], data=[]) self.write(response.json()) return
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()
async def get(self): """ GET /api/v2/get_roles - Endpoint used to get details of eligible roles. Used by weep and newt. --- get: description: Returns a json-encoded list of objects of eligible roles for the user. response format: WebResponse. The "data" field within WebResponse is of format EligibleRolesModelArray Example response: { "status": "success", "status_code": 200, "data": { "roles": [ { "arn": "arn:aws:iam::123456789012:role/role_name", "account_id": "123456789012", "account_friendly_name": "prod", "role_name": "role_name", "apps": { "app_details": [ { "name": "consoleme", "owner": "*****@*****.**", "owner_url": null, "app_url": "https://example.com" } ] } }, ... ] } } """ self.user: str = self.requester["email"] include_all_roles = self.get_arguments("all") console_only = True if include_all_roles == ["true"]: console_only = False log_data = { "function": f"{__name__}.{self.__class__.__name__}.{sys._getframe().f_code.co_name}", "user": self.user, "console_only": console_only, "message": "Getting all eligible user roles", "user-agent": self.request.headers.get("User-Agent"), "request_id": self.request_uuid, } log.debug(log_data) stats.count("GetRolesMTLSHandler.get", tags={"user": self.user}) await self.authorization_flow(user=self.user, console_only=console_only) eligible_roles_details_array = await get_eligible_role_details( sorted(self.eligible_roles) ) res = WebResponse( status=Status2.success, status_code=200, data=eligible_roles_details_array.dict(), ) self.write(res.json(exclude_unset=True)) await self.finish()
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())