Commit 503ef1fb authored by Fredrik Wendt's avatar Fredrik Wendt

The class that spins up an authenticated HTTP client session

parents
Pipeline #291 failed with stages
import logging
import sys
import time
import uuid
from hashlib import sha256
from typing import Optional, Dict
import requests
from requests import Session, TooManyRedirects
from proagile.authentication import CredentialsManager
from proagile.aws.cloud import S3Cache, AmazonS3
from proagile.logging.rollbar_log import RollbarLogger
from proagile.scrumorg.api import PRO_AGILE_USER_AGENT
from proagile.scrumorg.new_parsers.log_error import ScrumOrgHtmlError
from proagile.scrumorg.new_parsers.login_form import LoginFormParser
from proagile.utils import TimeUtils
class SessionCreator:
"""
Creates an HTTP Request Session that is logged in as the specified username.
"""
_instance = None
def __init__(self):
self.log = logging.getLogger(__name__)
self._sessions = dict()
self._event_logger = RollbarLogger.instance()
@classmethod
def instance(cls):
if not cls._instance:
cls._instance = SessionCreator()
return cls._instance
def acquire_session(self, username: str):
"""
Always checks that we can access a secret resource, before returning the session.
If the protected resource isn't accessible, login will happen.
"""
cookie_store = CookieStore(username)
if username in self._sessions:
session = self._sessions[username]
else:
session = Session()
session.headers.update({"User-Agent": PRO_AGILE_USER_AGENT})
self._sessions[username] = session
cookies = cookie_store.fetch()
self.log.info(f"Reusing Scrum.org cookies for {username}")
if cookies:
requests.utils.add_dict_to_cookiejar(session.cookies, cookies)
protected_url = "https://www.scrum.org/admin/courses/manage"
try:
response = session.get(protected_url, allow_redirects=True)
if len(response.history) == 0:
return session
else:
html = response.text
if "Course Management" in html:
self.log.info("After being redirected, we ended up on the Course Management page re-using session")
return session
else:
self._log_response(response)
self.log.info("After being redirected we didn't end up on Course Management page, new login needed")
except TooManyRedirects as e:
self.log.info("Received too many redirects: re-using cookies is not working out, getting a fresh session")
# remove session with unusable cookies etc
del self._sessions[username]
session = self.acquire_unique_clean_session(username)
self._sessions[username] = session
cookie_store.store(dict(session.cookies))
self.log.info(f"Logging in to Scrum.org as {username} completed")
return session
def acquire_unique_clean_session(self, username: str):
self.log.info(f"Logging in on Scrum.org as {username} with a clean session")
self._event_logger.log_event("acquire_unique_clean_session", distinct_user_id=username)
session = Session()
session.headers.update({"User-Agent": PRO_AGILE_USER_AGENT})
response = session.get("https://www.scrum.org/admin/courses/manage")
password = CredentialsManager().get_password_for("scrum.org", username)
data = LoginFormParser(response.text).extract_hidden_input_fields_with_values()
data.update({
"loginId": username,
"password": password
})
try:
login_result = session.post("https://accounts.scrum.org/oauth2/authorize", data=data)
if not login_result.ok:
self._log_response(login_result)
raise Exception("Failed to login to Scrum.org - result status was not OK")
except TooManyRedirects as e:
raise Exception("Failed to login to Scrum.org - too many redirects", e)
html = login_result.text
if "Course Management" not in html:
ScrumOrgHtmlError(html).log_sdo_error("Login failure")
self.log.debug(f"Full HTML from login attempt: {html}")
raise Exception("Failed to login attempt - couldn't find marker signifying successful login")
return session
def _log_response(self, response):
file_name = f"http/response/{str(uuid.uuid4())}.json"
s3service = AmazonS3(_s3_region=AmazonS3.REGION_EU_NORTH_1)
url_on_s3 = s3service.calculate_public_url_to_s3_file("logs.proagile.se", file_name)
self.log.info(f"Logging this HTTP(s) response, recording it in {url_on_s3}")
s3service.store_public_json_item("logs.proagile.se", file_name, {
"cookies": dict(response.cookies.get_dict()),
"headers": dict(response.headers),
"text": response.text,
"url": response.url,
})
class CookieStore:
"""
Stores Cookies in an S3 file, as JSON.
"""
def __init__(self, user_identity: str):
self.user_identity = user_identity
self.s3_cache = S3Cache()
self._file_name_part = None
self._event_logger = RollbarLogger.instance()
def file_name(self) -> str:
if not self._file_name_part:
h = sha256()
h.update(self.user_identity.encode("utf-8"))
self._file_name_part = h.hexdigest()
return f"scrum-org/cookies-{self._file_name_part}.json"
def store(self, cookies: dict):
self._event_logger.log_event("cookie_stored")
self.s3_cache.store_cache(self.file_name(), {
"cookies": cookies,
"savedAtSeconds": int(time.time()),
"savedAtHuman": TimeUtils.now_in_isoformat()
})
def fetch(self) -> Optional[Dict]:
file_name_with_cookie_data = self.file_name()
if not self.s3_cache.file_exists(file_name_with_cookie_data):
return
data = self.s3_cache.fetch(file_name_with_cookie_data)
if "cookies" not in data:
return
return data["cookies"]
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
x_session = SessionCreator.instance().acquire_session("fredrik.wendt@proagile.se")
x_response = x_session.get("https://www.scrum.org/node/12093/edit")
print(x_response.text)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment