Skip to content

Commit 7861d21

Browse files
Lots of fixes
1 parent 137323d commit 7861d21

5 files changed

Lines changed: 79 additions & 221 deletions

File tree

cogs/check_su_platform_authorisation.py

Lines changed: 6 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
"""Contains cog classes for SU platform access cookie authorisation check interactions."""
22

33
import logging
4-
from enum import Enum
54
from typing import TYPE_CHECKING, override
65

7-
import bs4
86
import discord
97
from discord.ext import tasks
108

@@ -13,10 +11,13 @@
1311
from utils.error_capture_decorators import (
1412
capture_guild_does_not_exist_error,
1513
)
16-
from utils.msl import fetch_url_content_with_session
14+
from utils.msl import (
15+
get_su_platform_access_cookie_status,
16+
get_su_platform_organisations,
17+
)
1718

1819
if TYPE_CHECKING:
19-
from collections.abc import Iterable, Sequence
20+
from collections.abc import Sequence
2021
from collections.abc import Set as AbstractSet
2122
from logging import Logger
2223
from typing import Final
@@ -38,130 +39,7 @@
3839
)
3940

4041

41-
class SUPlatformAccessCookieStatus(Enum):
42-
"""Enum class defining the status of the SU Platform Access Cookie."""
43-
44-
INVALID = (
45-
logging.WARNING,
46-
(
47-
"The SU platform access cookie is not associated with any MSL user, "
48-
"meaning it is invalid or expired."
49-
),
50-
)
51-
VALID = (
52-
logging.WARNING,
53-
(
54-
"The SU platform access cookie is associated with a valid MSL user, "
55-
"but is not an admin to any MSL organisations."
56-
),
57-
)
58-
AUTHORISED = (
59-
logging.INFO,
60-
(
61-
"The SU platform access cookie is associated with a valid MSL user and "
62-
"has access to at least one MSL organisation."
63-
),
64-
)
65-
66-
67-
class CheckSUPlatformAuthorisationBaseCog(TeXBotBaseCog):
68-
"""Cog class that defines the base functionality for cookie authorisation checks."""
69-
70-
async def get_su_platform_access_cookie_status(self) -> SUPlatformAccessCookieStatus:
71-
"""Retrieve the current validity status of the SU platform access cookie."""
72-
response_object: bs4.BeautifulSoup = bs4.BeautifulSoup(
73-
await fetch_url_content_with_session(SU_PLATFORM_PROFILE_URL), "html.parser"
74-
)
75-
page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title")
76-
if not page_title or "Login" in str(page_title):
77-
logger.warning("Token is invalid or expired.")
78-
return SUPlatformAccessCookieStatus.INVALID
79-
80-
organisation_admin_url: str = (
81-
f"{SU_PLATFORM_ORGANISATION_URL}/{settings['ORGANISATION_ID']}"
82-
)
83-
response_html: str = await fetch_url_content_with_session(organisation_admin_url)
84-
85-
if "admin tools" in response_html.lower():
86-
return SUPlatformAccessCookieStatus.AUTHORISED
87-
88-
if "You do not have any permissions for this organisation" in response_html.lower():
89-
return SUPlatformAccessCookieStatus.VALID
90-
91-
logger.warning(
92-
"Unexpected response when checking SU platform access cookie authorisation."
93-
)
94-
return SUPlatformAccessCookieStatus.INVALID
95-
96-
async def get_su_platform_organisations(self) -> "Iterable[str]":
97-
"""Retrieve the MSL organisations the current SU platform cookie has access to."""
98-
response_object: bs4.BeautifulSoup = bs4.BeautifulSoup(
99-
await fetch_url_content_with_session(SU_PLATFORM_PROFILE_URL), "html.parser"
100-
)
101-
102-
page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title")
103-
104-
if not page_title:
105-
logger.warning(
106-
"Profile page returned no content when checking "
107-
"SU platform access cookie's authorisation."
108-
)
109-
return ()
110-
111-
if "Login" in str(page_title):
112-
logger.warning(
113-
"Authentication redirected to login page. "
114-
"SU platform access cookie is invalid or expired."
115-
)
116-
return ()
117-
118-
profile_section_html: bs4.Tag | bs4.NavigableString | None = response_object.find(
119-
"div", {"id": "profile_main"}
120-
)
121-
122-
if profile_section_html is None:
123-
logger.warning(
124-
"Couldn't find the profile section of the user "
125-
"when scraping the SU platform's website HTML."
126-
)
127-
logger.debug("Retrieved HTML: %s", response_object.text)
128-
return ()
129-
130-
user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1")
131-
132-
if not isinstance(user_name, bs4.Tag):
133-
logger.warning(
134-
"Found user profile on the SU platform but couldn't find their name."
135-
)
136-
logger.debug("Retrieved HTML: %s", response_object.text)
137-
return ()
138-
139-
parsed_html: bs4.Tag | bs4.NavigableString | None = response_object.find(
140-
"ul", {"id": "ulOrgs"}
141-
)
142-
143-
if parsed_html is None or isinstance(parsed_html, bs4.NavigableString):
144-
NO_ADMIN_TABLE_MESSAGE: Final[str] = (
145-
f"Failed to retrieve the admin table for user: {user_name.string}. "
146-
"Please check you have used the correct SU platform access token!"
147-
)
148-
logger.warning(NO_ADMIN_TABLE_MESSAGE)
149-
return ()
150-
151-
organisations: Iterable[str] = [
152-
list_item.get_text(strip=True) for list_item in parsed_html.find_all("li")
153-
]
154-
155-
logger.debug(
156-
"SU platform access cookie has admin authorisation to: %s as user %s",
157-
organisations,
158-
user_name.text,
159-
)
160-
161-
return organisations
162-
163-
164-
class CheckSUPlatformAuthorisationCommandCog(CheckSUPlatformAuthorisationBaseCog):
42+
class CheckSUPlatformAuthorisationCommandCog(TeXBotBaseCog):
16543
"""Cog class that defines the "/check-su-platform-authorisation" command."""
16644

16745
@discord.slash_command(

utils/msl/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
from typing import TYPE_CHECKING
44

5-
from .authorisation import get_su_platform_access_cookie_status, get_su_platform_organisations
5+
from .authorisation import (
6+
SUPlatformAccessCookieStatus,
7+
get_su_platform_access_cookie_status,
8+
get_su_platform_organisations,
9+
)
610
from .memberships import (
711
fetch_community_group_members_count,
812
fetch_community_group_members_list,
9-
fetch_url_content_with_session,
1013
is_id_a_community_group_member,
1114
)
1215

@@ -15,8 +18,11 @@
1518

1619
__all__: "Sequence[str]" = (
1720
"GLOBAL_SSL_CONTEXT",
21+
"SUPlatformAccessCookieStatus",
1822
"fetch_community_group_members_count",
1923
"fetch_community_group_members_list",
2024
"fetch_url_content_with_session",
25+
"get_su_platform_access_cookie_status",
26+
"get_su_platform_organisations",
2127
"is_id_a_community_group_member",
2228
)

utils/msl/authorisation.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
"""Module for authorisation checks."""
22

33
import logging
4+
from enum import Enum
45
from typing import TYPE_CHECKING
56

6-
import aiohttp
77
import bs4
88

9-
from cogs.check_su_platform_authorisation import SUPlatformAccessCookieStatus
109
from config import settings
11-
from utils import GLOBAL_SSL_CONTEXT
1210

13-
from .core import BASE_COOKIES, BASE_HEADERS
11+
from .core import su_platform_client
1412

1513
if TYPE_CHECKING:
1614
from collections.abc import Iterable, Sequence
@@ -19,6 +17,7 @@
1917

2018

2119
__all__: "Sequence[str]" = (
20+
"SUPlatformAccessCookieStatus",
2221
"get_su_platform_access_cookie_status",
2322
"get_su_platform_organisations",
2423
)
@@ -33,19 +32,36 @@
3332
)
3433

3534

36-
async def _fetch_url_content_with_session(url: str) -> str:
37-
"""Fetch the HTTP content at the given URL, using a shared aiohttp session."""
38-
async with (
39-
aiohttp.ClientSession(headers=BASE_HEADERS, cookies=BASE_COOKIES) as http_session,
40-
http_session.get(url=url, ssl=GLOBAL_SSL_CONTEXT) as http_response,
41-
):
42-
return await http_response.text()
35+
class SUPlatformAccessCookieStatus(Enum):
36+
"""Enum class defining the status of the SU Platform Access Cookie."""
37+
38+
INVALID = (
39+
logging.WARNING,
40+
(
41+
"The SU platform access cookie is not associated with any MSL user, "
42+
"meaning it is invalid or expired."
43+
),
44+
)
45+
VALID = (
46+
logging.WARNING,
47+
(
48+
"The SU platform access cookie is associated with a valid MSL user, "
49+
"but is not an admin to any MSL organisations."
50+
),
51+
)
52+
AUTHORISED = (
53+
logging.INFO,
54+
(
55+
"The SU platform access cookie is associated with a valid MSL user and "
56+
"has access to at least one MSL organisation."
57+
),
58+
)
4359

4460

4561
async def get_su_platform_access_cookie_status() -> SUPlatformAccessCookieStatus:
4662
"""Retrieve the current validity status of the SU platform access cookie."""
4763
response_object: bs4.BeautifulSoup = bs4.BeautifulSoup(
48-
await _fetch_url_content_with_session(SU_PLATFORM_PROFILE_URL), "html.parser"
64+
await su_platform_client.fetch_url_content(SU_PLATFORM_PROFILE_URL), "html.parser"
4965
)
5066
page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title")
5167
if not page_title or "Login" in str(page_title):
@@ -55,7 +71,7 @@ async def get_su_platform_access_cookie_status() -> SUPlatformAccessCookieStatus
5571
organisation_admin_url: str = (
5672
f"{SU_PLATFORM_ORGANISATION_URL}/{settings['ORGANISATION_ID']}"
5773
)
58-
response_html: str = await _fetch_url_content_with_session(organisation_admin_url)
74+
response_html: str = await su_platform_client.fetch_url_content(organisation_admin_url)
5975

6076
if "admin tools" in response_html.lower():
6177
return SUPlatformAccessCookieStatus.AUTHORISED
@@ -72,7 +88,7 @@ async def get_su_platform_access_cookie_status() -> SUPlatformAccessCookieStatus
7288
async def get_su_platform_organisations() -> "Iterable[str]":
7389
"""Retrieve the MSL organisations the current SU platform cookie has access to."""
7490
response_object: bs4.BeautifulSoup = bs4.BeautifulSoup(
75-
await _fetch_url_content_with_session(SU_PLATFORM_PROFILE_URL), "html.parser"
91+
await su_platform_client.fetch_url_content(SU_PLATFORM_PROFILE_URL), "html.parser"
7692
)
7793

7894
page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title")

utils/msl/core.py

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from typing import TYPE_CHECKING
55

66
import aiohttp
7-
from bs4 import BeautifulSoup
87

98
from config import settings
109
from utils import GLOBAL_SSL_CONTEXT
@@ -20,46 +19,37 @@
2019

2120
logger: "Final[Logger]" = logging.getLogger("TeX-Bot")
2221

23-
24-
BASE_HEADERS: "Final[Mapping[str, str]]" = {
25-
"Cache-Control": "no-cache",
26-
"Pragma": "no-cache",
27-
"Expires": "0",
28-
}
29-
30-
BASE_COOKIES: "Final[Mapping[str, str]]" = {
31-
".ASPXAUTH": settings["SU_PLATFORM_ACCESS_COOKIE"],
32-
}
33-
3422
ORGANISATION_ID: "Final[str]" = settings["ORGANISATION_ID"]
3523

3624
ORGANISATION_ADMIN_URL: "Final[str]" = (
3725
f"https://www.guildofstudents.com/organisation/admin/{ORGANISATION_ID}/"
3826
)
3927

4028

41-
async def get_msl_context(url: str) -> tuple[dict[str, str], dict[str, str]]:
42-
"""Get the required context headers, data and cookies to make a request to MSL."""
43-
http_session: aiohttp.ClientSession = aiohttp.ClientSession(
44-
headers=BASE_HEADERS,
45-
cookies=BASE_COOKIES,
46-
)
47-
data_fields: dict[str, str] = {}
48-
cookies: dict[str, str] = {}
49-
async with http_session, http_session.get(url=url, ssl=GLOBAL_SSL_CONTEXT) as field_data:
50-
data_response = BeautifulSoup(
51-
markup=await field_data.text(),
52-
features="html.parser",
53-
)
54-
55-
for field in data_response.find_all(name="input"):
56-
if field.get("name") and field.get("value"):
57-
data_fields[field.get("name")] = field.get("value")
58-
59-
for cookie in field_data.cookies:
60-
cookie_morsel: Morsel[str] | None = field_data.cookies.get(cookie)
61-
if cookie_morsel is not None:
62-
cookies[cookie] = cookie_morsel.value
63-
cookies[".ASPXAUTH"] = settings["MEMBERS_LIST_AUTH_SESSION_COOKIE"]
64-
65-
return data_fields, cookies
29+
class SUPlatformClient:
30+
"""A client for making authenticated requests to the SU platform."""
31+
32+
def __init__(self) -> None:
33+
self.headers: Mapping[str, str] = settings["SU_PLATFORM_WEB_HEADERS"]
34+
self.cookies: Mapping[str, str] = settings["SU_PLATFORM_ACCESS_COOKIE"]
35+
36+
async def fetch_url_content(self, url: str) -> str:
37+
async with (
38+
aiohttp.ClientSession(headers=self.headers, cookies=self.cookies) as http_session,
39+
http_session.get(url=url, ssl=GLOBAL_SSL_CONTEXT) as http_response,
40+
):
41+
returned_asp_cookie: Morsel[str] | None = http_response.cookies.get(
42+
".AspNet.SharedCookie"
43+
)
44+
if not returned_asp_cookie:
45+
return await http_response.text()
46+
47+
if returned_asp_cookie.value != self.cookies[".AspNet.SharedCookie"]:
48+
logger.info("New SU platform access cookie given by server; updating local.")
49+
self.cookies = {
50+
".AspNet.SharedCookie": returned_asp_cookie.value,
51+
}
52+
return await http_response.text()
53+
54+
55+
su_platform_client: "Final[SUPlatformClient]" = SUPlatformClient()

0 commit comments

Comments
 (0)