Skip to content
Merged
Show file tree
Hide file tree
Changes from 80 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
000ea0d
Add auto auth session setting setup
MattyTheHacker May 3, 2025
734a4ef
refactor and implement task
MattyTheHacker May 3, 2025
2a70103
fix quotes
MattyTheHacker May 3, 2025
79ed652
fix ruff
MattyTheHacker May 3, 2025
e6b5762
fix settings names
MattyTheHacker May 3, 2025
6230e26
fix
MattyTheHacker May 3, 2025
8d76c8b
ruff
MattyTheHacker May 3, 2025
42e0f44
actually call the method
MattyTheHacker May 3, 2025
c0debd4
fix again
MattyTheHacker May 3, 2025
be9036b
change
MattyTheHacker May 3, 2025
9010469
add logging
MattyTheHacker May 3, 2025
35eb61c
suck my ass
MattyTheHacker May 3, 2025
3f01780
Implement fix
MattyTheHacker May 4, 2025
507d42e
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 4, 2025
e6cff0a
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 4, 2025
b9c9964
Improve debug messages
MattyTheHacker May 4, 2025
b8bfc9e
refactor to make it nicer
MattyTheHacker May 4, 2025
6f29bef
minor refactor
MattyTheHacker May 4, 2025
5751f18
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 5, 2025
8c6273f
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 6, 2025
af878ef
Fix some stuff
MattyTheHacker May 9, 2025
f36602d
Refactor the method to only return the list
MattyTheHacker May 9, 2025
9d41e52
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 11, 2025
564b6e6
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 12, 2025
6bb88f1
Improve token status check
MattyTheHacker May 12, 2025
67bceb1
fix debug messages
MattyTheHacker May 12, 2025
5d7d952
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 13, 2025
b117a20
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 18, 2025
52cb2a4
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 20, 2025
3b93dc3
add missing check
MattyTheHacker May 21, 2025
61d64a4
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 21, 2025
8ef7e05
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 21, 2025
a9244dc
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 25, 2025
6288b28
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 27, 2025
88f57b0
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker May 27, 2025
a7acdb5
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker Jun 3, 2025
a5ce2d8
Merge main into 481-auto-auth-check
cssbhamdev Jun 12, 2025
e28b8f7
Merge main into 481-auto-auth-check
cssbhamdev Jun 13, 2025
34d10af
Merge main into 481-auto-auth-check
cssbhamdev Jun 13, 2025
e0eb7b5
Merge main into 481-auto-auth-check
cssbhamdev Jun 14, 2025
1a6ff75
Allow committee-elect to update actions (and appear in auto-complete)…
Thatsmusic99 Jun 15, 2025
2551458
Merge main into 481-auto-auth-check
cssbhamdev Jun 15, 2025
0d24293
Merge main into 481-auto-auth-check
cssbhamdev Jun 15, 2025
c290039
Merge main into 481-auto-auth-check
cssbhamdev Jun 15, 2025
f29438f
Merge main into 481-auto-auth-check
cssbhamdev Jun 15, 2025
3f9c902
Merge main into 481-auto-auth-check
cssbhamdev Jun 16, 2025
ded1461
Merge main into 481-auto-auth-check
cssbhamdev Jun 16, 2025
13c9194
Merge main into 481-auto-auth-check
cssbhamdev Jun 17, 2025
d676377
Merge main into 481-auto-auth-check
cssbhamdev Jun 19, 2025
aabce00
Merge main into 481-auto-auth-check
cssbhamdev Jun 19, 2025
7066be0
Merge main into 481-auto-auth-check
cssbhamdev Jun 22, 2025
f38cfe3
Merge main into 481-auto-auth-check
cssbhamdev Jun 24, 2025
b2fed62
Merge main into 481-auto-auth-check
cssbhamdev Jun 24, 2025
6ceb054
Merge main into 481-auto-auth-check
cssbhamdev Jun 24, 2025
49888a3
Merge main into 481-auto-auth-check
cssbhamdev Jun 25, 2025
d6756f9
Merge main into 481-auto-auth-check
cssbhamdev Jun 30, 2025
01d20dc
Merge main into 481-auto-auth-check
automatic-pr-updater[bot] Jun 30, 2025
f505586
Merge main into 481-auto-auth-check
automatic-pr-updater[bot] Jul 2, 2025
4b90b2c
Merge main into 481-auto-auth-check
automatic-pr-updater[bot] Jul 2, 2025
310f988
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker Jul 3, 2025
fe57278
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker Jul 4, 2025
383fe39
Merge main
MattyTheHacker Jul 4, 2025
cc54a4b
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker Jul 4, 2025
ef045f2
Merge main into 481-auto-auth-check
automatic-pr-updater[bot] Jul 4, 2025
837d69b
Merge main into 481-auto-auth-check
automatic-pr-updater[bot] Jul 4, 2025
a2473f0
Change names
MattyTheHacker Jul 4, 2025
11d401a
add file
MattyTheHacker Jul 4, 2025
55a5e94
more changes
MattyTheHacker Jul 4, 2025
f3d62f0
more
MattyTheHacker Jul 4, 2025
56749c9
More changes
MattyTheHacker Jul 4, 2025
d945acc
Fix
MattyTheHacker Jul 4, 2025
7db7115
More renaming
MattyTheHacker Jul 4, 2025
ab35042
Fix method call
MattyTheHacker Jul 4, 2025
879a741
Merge branch 'main' into 481-auto-auth-check
MattyTheHacker Jul 4, 2025
bd93d40
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jul 4, 2025
948c35d
Merge main into 481-auto-auth-check
automatic-pr-updater[bot] Jul 4, 2025
37917ae
Merge main into 481-auto-auth-check
automatic-pr-updater[bot] Jul 5, 2025
e238701
Merge main into 481-auto-auth-check
automatic-pr-updater[bot] Jul 5, 2025
eac5872
Merge main into 481-auto-auth-check
automatic-pr-updater[bot] Jul 5, 2025
1e8ce4e
Merge main into 481-auto-auth-check
automatic-pr-updater[bot] Jul 5, 2025
7b56cf7
Fix some stuff
MattyTheHacker Jul 5, 2025
9a26cbf
minor fixes
MattyTheHacker Jul 5, 2025
a0ece95
Apply suggestions from code review
MattyTheHacker Jul 5, 2025
255eae7
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jul 5, 2025
699642c
Fix shit
MattyTheHacker Jul 5, 2025
cb0a68a
Fixes
MattyTheHacker Jul 5, 2025
95ae35f
Apply stuff
MattyTheHacker Jul 5, 2025
bc661f2
variable
MattyTheHacker Jul 5, 2025
742439a
Apply suggestions from code review
MattyTheHacker Jul 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cogs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
CommitteeHandoverCommandCog,
)
from .archive import ArchiveCommandCog
from .check_su_platform_authorisation import CheckSUPlatformAuthorisationCommandCog
from .check_su_platform_authorisation import (
CheckSUPlatformAuthorisationCommandCog,
CheckSUPlatformAuthorisationTaskCog,
)
from .command_error import CommandErrorCog
from .committee_actions_tracking import (
CommitteeActionsTrackingContextCommandsCog,
Expand Down Expand Up @@ -54,6 +57,7 @@
"AnnualYearChannelsIncrementCommandCog",
"ArchiveCommandCog",
"CheckSUPlatformAuthorisationCommandCog",
"CheckSUPlatformAuthorisationTaskCog",
"ClearRemindersBacklogTaskCog",
"CommandErrorCog",
"CommitteeActionsTrackingContextCommandsCog",
Expand Down Expand Up @@ -123,6 +127,7 @@ def setup(bot: "TeXBot") -> None:
StatsCommandsCog,
StrikeCommandCog,
StrikeContextCommandsCog,
CheckSUPlatformAuthorisationTaskCog,
WriteRolesCommandCog,
)
Cog: type[TeXBotBaseCog]
Expand Down
265 changes: 193 additions & 72 deletions cogs/check_su_platform_authorisation.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
"""Contains cog classes for SU platform access cookie authorisation check interactions."""

import logging
from typing import TYPE_CHECKING
from enum import Enum
from typing import TYPE_CHECKING, override

import aiohttp
import bs4
import discord
from bs4 import BeautifulSoup
from discord.ext import tasks

from config import settings
from utils import CommandChecks, TeXBotBaseCog
from utils.error_capture_decorators import (
capture_guild_does_not_exist_error,
)

if TYPE_CHECKING:
from collections.abc import Collection, Mapping, Sequence
from collections.abc import Iterable, Mapping, Sequence
from logging import Logger
from typing import Final

from utils import TeXBotApplicationContext
from utils import TeXBot, TeXBotApplicationContext

__all__: "Sequence[str]" = ("CheckSUPlatformAuthorisationCommandCog",)
__all__: "Sequence[str]" = (
"CheckSUPlatformAuthorisationCommandCog",
"CheckSUPlatformAuthorisationTaskCog",
)

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

Expand All @@ -29,119 +36,233 @@
}

REQUEST_COOKIES: "Final[Mapping[str, str]]" = {
".ASPXAUTH": settings["SU_PLATFORM_ACCESS_COOKIE"],
".ASPXAUTH": settings["SU_PLATFORM_ACCESS_COOKIE"]
}

REQUEST_URL: "Final[str]" = "https://guildofstudents.com/profile"
PROFILE_URL: "Final[str]" = "https://guildofstudents.com/profile"
ORGANISATION_URL: "Final[str]" = "https://www.guildofstudents.com/organisation/admin"


class CheckSUPlatformAuthorisationCommandCog(TeXBotBaseCog):
"""Cog class that defines the "/check-su-platform-authorisation-cookie" command."""
class CheckSUPlatformAuthorisationBaseCog(TeXBotBaseCog):
"""Cog class that defines the base for token authorisation functions."""

@discord.slash_command( # type: ignore[no-untyped-call, misc]
name="check-su-platform-authorisation",
description="Checks the authorisations held by the SU access token.",
)
@CommandChecks.check_interaction_user_has_committee_role
@CommandChecks.check_interaction_user_in_main_guild
async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") -> None: # type: ignore[misc]
class TokenStatus(Enum):
"""
Definition of the "check_su_platform_authorisation" command.
Enum class that defines the status of the token.

The "check_su_platform_authorisation" command will retrieve the profile for the user.
The profile page will contain the user's name and a list of the MSL organisations
the user has administrative access to.
INVALID: The token does not have access to a user, meaning it is invalid or expired.
VALID: The token is a valid user, but not neccessarily admin to an organisation.
AUTHORISED: The token is a valid user and has access to an organisation.
"""

INVALID = (
logging.WARNING,
"The auth session cookie is not associated with any MSL user, "
"meaning it is invalid or expired.",
)
VALID = (
logging.WARNING,
"The auth session cookie is associated with a valid MSL user, "
"but is not an admin to any MSL organisations.",
)
AUTHORISED = (
logging.INFO,
"The auth session cookie is associated with a valid MSL user and "
"has access to at least one MSL organisation.",
)

async def _fetch_url_content_with_session(self, url: str) -> str:
"""Fetch the HTTP content at the given URL, using a shared aiohttp session."""
async with (
aiohttp.ClientSession(
headers=REQUEST_HEADERS,
cookies=REQUEST_COOKIES,
) as http_session,
http_session.get(url) as http_response,
):
return await http_response.text()

async def get_token_status(self) -> TokenStatus:
"""
Definition of method to get the status of the token.

This is done by checking if the token is valid and if it is,
checking if the token has access to the organisation.
"""
http_session: aiohttp.ClientSession = aiohttp.ClientSession(
headers=REQUEST_HEADERS, cookies=REQUEST_COOKIES
response_object: bs4.BeautifulSoup = bs4.BeautifulSoup(
await self._fetch_url_content_with_session(PROFILE_URL), "html.parser"
)
page_title: bs4.Tag | bs4.NavigableString | None = response_object.find("title")
if not page_title or "Login" in str(page_title):
logger.debug("Token is invalid or expired.")
return self.TokenStatus.INVALID

organisation_admin_url: str = f"{ORGANISATION_URL}/{settings['ORGANISATION_ID']}"
response_html: str = await self._fetch_url_content_with_session(organisation_admin_url)

async with http_session, http_session.get(REQUEST_URL) as http_response:
response_html: str = await http_response.text()
if "admin tools" in response_html.lower():
return self.TokenStatus.AUTHORISED

if "You do not have any permissions for this organisation" in response_html.lower():
return self.TokenStatus.VALID

logger.warning("Unexpected response when checking token authorisation.")
return self.TokenStatus.INVALID

async def get_token_groups(self) -> "Iterable[str]":
"""
Definition of method to get the groups the token has access to.

response_object: bs4.BeautifulSoup = BeautifulSoup(response_html, "html.parser")
This is done by requesting the user profile page and
scraping the HTML for the list of groups.
"""
response_object: bs4.BeautifulSoup = bs4.BeautifulSoup(
await self._fetch_url_content_with_session(PROFILE_URL), "html.parser"
)

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

if not page_title:
await self.command_send_error(
ctx=ctx,
message="Profile page returned no content when checking token authorisation!",
PROFILE_PAGE_INVALID: Final[str] = (
"Profile page returned no content when checking token authorisation."
)
return
logger.warning(PROFILE_PAGE_INVALID)
return ()

if "Login" in str(page_title):
INVALID_COOKIE_MESSAGE: Final[str] = (
"Unable to fetch profile page because "
"the SU platform access cookie was not valid."
EXPIRED_AUTH_MESSAGE: Final[str] = (
"Authentication redirected to login page. Token is invalid or expired."
)
logger.warning(INVALID_COOKIE_MESSAGE)
await ctx.respond(content=INVALID_COOKIE_MESSAGE)
return
logger.warning(EXPIRED_AUTH_MESSAGE)
return ()

profile_section_html: bs4.Tag | bs4.NavigableString | None = response_object.find(
"div", {"id": "profile_main"}
"div",
{"id": "profile_main"},
)

if profile_section_html is None:
logger.warning(
"Couldn't find the profile section of the user "
"when scraping the website's HTML!"
)
logger.debug("Retrieved HTML: %s", response_html)
await ctx.respond(
"Couldn't find the profile of the user! "
"This should never happen, please check the logs!"
"when scraping the website's HTML."
)
return
logger.debug("Retrieved HTML: %s", response_object.text)
return ()

user_name: bs4.Tag | bs4.NavigableString | int | None = profile_section_html.find("h1")

if not isinstance(user_name, bs4.Tag):
NO_PROFILE_DEBUG_MESSAGE: Final[str] = (
"Found user profile but couldn't find their name!"
)
logger.debug(NO_PROFILE_DEBUG_MESSAGE)
await ctx.respond(NO_PROFILE_DEBUG_MESSAGE)
return
logger.warning("Found user profile but couldn't find their name.")
logger.debug("Retrieved HTML: %s", response_object.text)
return ()

parsed_html: bs4.Tag | bs4.NavigableString | None = response_object.find(
"ul", {"id": "ulOrgs"}
"ul",
{"id": "ulOrgs"},
)

if parsed_html is None or isinstance(parsed_html, bs4.NavigableString):
NO_ADMIN_TABLE_MESSAGE: Final[str] = (
f"Failed to retrieve the admin table for user: {user_name.string}. "
"Please check you have used the correct SU platform access cookie!"
f"Failed to retrieve the admin table for user: {user_name.string}."
"Please check you have used the correct token!"
)
logger.warning(NO_ADMIN_TABLE_MESSAGE)
await ctx.respond(content=NO_ADMIN_TABLE_MESSAGE)
return
return ()

organisations: Collection[str] = [
organisations: Iterable[str] = [
list_item.get_text(strip=True) for list_item in parsed_html.find_all("li")
]

if not organisations:
logger.warning(
(
"Organisations list was unexpectedly empty "
"for the SU platform access cookie associated with %s."
),
user_name.text,
)
await ctx.respond(content="Unexpectedly empty organisations error.")
return

logger.debug(
"The SU platform access cookie has administrator access to: %s as user %s",
"Admin Token has admin access to: %s as user %s",
organisations,
user_name.text,
)

await ctx.respond(
"The SU platform access cookie has administrator access to "
"the following MSL Organisations as "
f"{user_name.text}:\n{',\n'.join(organisation for organisation in organisations)}",
ephemeral=True,
return organisations


class CheckSUPlatformAuthorisationCommandCog(CheckSUPlatformAuthorisationBaseCog):
"""Cog class that defines the "/check-su-platform-authorisation-cookie" command."""

@discord.slash_command( # type: ignore[no-untyped-call, misc]
name="check-su-platform-authorisation",
description="Checks the authorisations held by the SU access token.",
)
@CommandChecks.check_interaction_user_has_committee_role
@CommandChecks.check_interaction_user_in_main_guild
async def check_su_platform_authorisation(self, ctx: "TeXBotApplicationContext") -> None: # type: ignore[misc]
"""
Definition of the "check_su_platform_authorisation" command.

The "check_su_platform_authorisation" command will retrieve the profile for the user.
The profile page will contain the user's name and a list of the MSL organisations
the user has administrative access to.
"""
await ctx.defer(ephemeral=True)
async with ctx.typing():
await ctx.followup.send(
content=(
f"SU Platform Access Cookie has access to the following MSL Organisations:"
f"\n{
',\n'.join(
organisation for organisation in await self.get_token_groups()
)
}"
),
ephemeral=True,
)


class CheckSUPlatformAuthorisationTaskCog(CheckSUPlatformAuthorisationBaseCog):
"""Cog class that defines a repeated background task for checking SU Platform Access Cookie.""" # noqa: E501, W505

@override
def __init__(self, bot: "TeXBot") -> None:
"""Start all task managers when this cog is initialised."""
if settings["AUTO_AUTH_SESSION_COOKIE_CHECKING"]:
_ = self.token_authorisation_check_task.start()

super().__init__(bot)

@override
def cog_unload(self) -> None:
"""
Unload-hook that ends all running tasks whenever the tasks cog is unloaded.

This may be run dynamically or when the bot closes.
"""
self.token_authorisation_check_task.cancel()

@tasks.loop(**settings["AUTO_AUTH_SESSION_COOKIE_CHECKING_INTERVAL"])
@capture_guild_does_not_exist_error
async def token_authorisation_check_task(self) -> None:
"""
Definition of the background task that checks the token authorisation.

The task will check if the token is valid and if it is, it will retrieve the
groups the token has access to.
"""
logger.debug("Running token authorisation check task...")

token_status: CheckSUPlatformAuthorisationBaseCog.TokenStatus = (
await self.get_token_status()
)

match token_status:
case self.TokenStatus.AUTHORISED:
logger.info("Token is valid and has access to the organisation.")
return

case self.TokenStatus.VALID:
logger.warning("Token is valid but does not have access to the organisation.")
return

case self.TokenStatus.INVALID:
logger.warning("Token is invalid or expired.")
return

@token_authorisation_check_task.before_loop
async def before_tasks(self) -> None:
"""Pre-execution hook, preventing any tasks from executing before the bot is ready."""
await self.bot.wait_until_ready()
Loading