diff --git a/aurweb/auth.py b/aurweb/auth.py deleted file mode 100644 index 4d6dafc6..00000000 --- a/aurweb/auth.py +++ /dev/null @@ -1,350 +0,0 @@ -import functools -import re - -from datetime import datetime -from http import HTTPStatus - -import fastapi - -from fastapi.responses import RedirectResponse -from sqlalchemy import and_ -from starlette.authentication import AuthCredentials, AuthenticationBackend -from starlette.requests import HTTPConnection - -import aurweb.config - -from aurweb import db, l10n, util -from aurweb.models import Session, User -from aurweb.models.account_type import ACCOUNT_TYPE_ID -from aurweb.templates import make_variable_context, render_template - - -class StubQuery: - """ Acts as a stubbed version of an orm.Query. Typically used - to masquerade fake records for an AnonymousUser. """ - - def filter(self, *args): - return StubQuery() - - def scalar(self): - return 0 - - -class AnonymousUser: - """ A stubbed User class used when an unauthenticated User - makes a request against FastAPI. """ - # Stub attributes used to mimic a real user. - ID = 0 - - class AccountType: - """ A stubbed AccountType static class. In here, we use an ID - and AccountType which do not exist in our constant records. - All records primary keys (AccountType.ID) should be non-zero, - so using a zero here means that we'll never match against a - real AccountType. """ - ID = 0 - AccountType = "Anonymous" - - # AccountTypeID == AccountType.ID; assign a stubbed column. - AccountTypeID = AccountType.ID - - LangPreference = aurweb.config.get("options", "default_lang") - Timezone = aurweb.config.get("options", "default_timezone") - - Suspended = 0 - InactivityTS = 0 - - # A stub ssh_pub_key relationship. - ssh_pub_key = None - - # Add stubbed relationship backrefs. - notifications = StubQuery() - package_votes = StubQuery() - - # A nonce attribute, needed for all browser sessions; set in __init__. - nonce = None - - def __init__(self): - self.nonce = util.make_nonce() - - @staticmethod - def is_authenticated(): - return False - - @staticmethod - def is_trusted_user(): - return False - - @staticmethod - def is_developer(): - return False - - @staticmethod - def is_elevated(): - return False - - @staticmethod - def has_credential(credential, **kwargs): - return False - - @staticmethod - def voted_for(package): - return False - - @staticmethod - def notified(package): - return False - - -class BasicAuthBackend(AuthenticationBackend): - async def authenticate(self, conn: HTTPConnection): - sid = conn.cookies.get("AURSID") - if not sid: - return (None, AnonymousUser()) - - now_ts = datetime.utcnow().timestamp() - record = db.query(Session).filter( - and_(Session.SessionID == sid, - Session.LastUpdateTS >= now_ts)).first() - - # If no session with sid and a LastUpdateTS now or later exists. - if not record: - return (None, AnonymousUser()) - - # At this point, we cannot have an invalid user if the record - # exists, due to ForeignKey constraints in the schema upheld - # by mysqlclient. - user = db.query(User).filter(User.ID == record.UsersID).first() - user.nonce = util.make_nonce() - user.authenticated = True - - return (AuthCredentials(["authenticated"]), user) - - -def auth_required(is_required: bool = True, - login: bool = True, - redirect: str = "/", - template: tuple = None, - status_code: HTTPStatus = HTTPStatus.UNAUTHORIZED): - """ Authentication route decorator. - - If redirect is given, the user will be redirected if the auth state - does not match is_required. - - If template is given, it will be rendered with Unauthorized if - is_required does not match and take priority over redirect. - - A precondition of this function is that, if template is provided, - it **must** match the following format: - - template=("template.html", ["Some Template For", "{}"], ["username"]) - - Where `username` is a FastAPI request path parameter, fitting - a route like: `/some_route/{username}`. - - If you wish to supply a non-formatted template, just omit any Python - format strings (with the '{}' substring). The third tuple element - will not be used, and so anything can be supplied. - - template=("template.html", ["Some Page"], None) - - All title shards and format parameters will be translated before - applying any format operations. - - :param is_required: A boolean indicating whether the function requires auth - :param login: Redirect to `/login`, passing `next=` - :param redirect: Path to redirect to if is_required isn't True - :param template: A three-element template tuple: - (path, title_iterable, variable_iterable) - :param status_code: An optional status_code for template render. - Redirects are always SEE_OTHER. - """ - - def decorator(func): - @functools.wraps(func) - async def wrapper(request, *args, **kwargs): - if request.user.is_authenticated() != is_required: - url = "/" - - if redirect: - path_params_expr = re.compile(r'\{(\w+)\}') - match = re.findall(path_params_expr, redirect) - args = {k: request.path_params.get(k) for k in match} - url = redirect.format(**args) - - if login: - url = "/login?" + util.urlencode({"next": url}) - - if template: - # template=("template.html", - # ["Some Title", "someFormatted {}"], - # ["variable"]) - # => render template.html with title: - # "Some Title someFormatted variables" - path, title_parts, variables = template - _ = l10n.get_translator_for_request(request) - - # Step through title_parts; for each part which contains - # a '{}' in it, apply .format(var) where var = the current - # iteration of variables. - # - # This implies that len(variables) is equal to - # len([part for part in title_parts if '{}' in part]) - # and this must always be true. - # - sanitized = [] - _variables = iter(variables) - for part in title_parts: - if "{}" in part: # If this part is formattable. - key = next(_variables) - var = request.path_params.get(key) - sanitized.append(_(part.format(var))) - else: # Otherwise, just add the translated part. - sanitized.append(_(part)) - - # Glue all title parts together, separated by spaces. - title = " ".join(sanitized) - - context = await make_variable_context(request, title) - return render_template(request, path, context, - status_code=status_code) - return RedirectResponse(url, - status_code=int(HTTPStatus.SEE_OTHER)) - return await func(request, *args, **kwargs) - return wrapper - - return decorator - - -def account_type_required(one_of: set): - """ A decorator that can be used on FastAPI routes to dictate - that a user belongs to one of the types defined in one_of. - - This decorator should be run after an @auth_required(True) is - dictated. - - - Example code: - - @router.get('/some_route') - @auth_required(True) - @account_type_required({"Trusted User", "Trusted User & Developer"}) - async def some_route(request: fastapi.Request): - return Response() - - :param one_of: A set consisting of strings to match against AccountType. - :return: Return the FastAPI function this decorator wraps. - """ - # Convert any account type string constants to their integer IDs. - one_of = { - ACCOUNT_TYPE_ID[atype] - for atype in one_of - if isinstance(atype, str) - } - - def decorator(func): - @functools.wraps(func) - async def wrapper(request: fastapi.Request, *args, **kwargs): - if request.user.AccountType.ID not in one_of: - return RedirectResponse("/", - status_code=int(HTTPStatus.SEE_OTHER)) - return await func(request, *args, **kwargs) - return wrapper - return decorator - - -CRED_ACCOUNT_CHANGE_TYPE = 1 -CRED_ACCOUNT_EDIT = 2 -CRED_ACCOUNT_EDIT_DEV = 3 -CRED_ACCOUNT_LAST_LOGIN = 4 -CRED_ACCOUNT_SEARCH = 5 -CRED_ACCOUNT_LIST_COMMENTS = 28 -CRED_COMMENT_DELETE = 6 -CRED_COMMENT_UNDELETE = 27 -CRED_COMMENT_VIEW_DELETED = 22 -CRED_COMMENT_EDIT = 25 -CRED_COMMENT_PIN = 26 -CRED_PKGBASE_ADOPT = 7 -CRED_PKGBASE_SET_KEYWORDS = 8 -CRED_PKGBASE_DELETE = 9 -CRED_PKGBASE_DISOWN = 10 -CRED_PKGBASE_EDIT_COMAINTAINERS = 24 -CRED_PKGBASE_FLAG = 11 -CRED_PKGBASE_LIST_VOTERS = 12 -CRED_PKGBASE_NOTIFY = 13 -CRED_PKGBASE_UNFLAG = 15 -CRED_PKGBASE_VOTE = 16 -CRED_PKGREQ_FILE = 23 -CRED_PKGREQ_CLOSE = 17 -CRED_PKGREQ_LIST = 18 -CRED_TU_ADD_VOTE = 19 -CRED_TU_LIST_VOTES = 20 -CRED_TU_VOTE = 21 -CRED_PKGBASE_MERGE = 29 - - -def has_any(user, *account_types): - return str(user.AccountType) in set(account_types) - - -def user_developer_or_trusted_user(user): - return True - - -def trusted_user(user): - return has_any(user, "Trusted User", "Trusted User & Developer") - - -def developer(user): - return has_any(user, "Developer", "Trusted User & Developer") - - -def trusted_user_or_dev(user): - return has_any(user, "Trusted User", "Developer", - "Trusted User & Developer") - - -# A mapping of functions that users must pass to have credentials. -cred_filters = { - CRED_PKGBASE_FLAG: user_developer_or_trusted_user, - CRED_PKGBASE_NOTIFY: user_developer_or_trusted_user, - CRED_PKGBASE_VOTE: user_developer_or_trusted_user, - CRED_PKGREQ_FILE: user_developer_or_trusted_user, - CRED_ACCOUNT_CHANGE_TYPE: trusted_user_or_dev, - CRED_ACCOUNT_EDIT: trusted_user_or_dev, - CRED_ACCOUNT_LAST_LOGIN: trusted_user_or_dev, - CRED_ACCOUNT_LIST_COMMENTS: trusted_user_or_dev, - CRED_ACCOUNT_SEARCH: trusted_user_or_dev, - CRED_COMMENT_DELETE: trusted_user_or_dev, - CRED_COMMENT_UNDELETE: trusted_user_or_dev, - CRED_COMMENT_VIEW_DELETED: trusted_user_or_dev, - CRED_COMMENT_EDIT: trusted_user_or_dev, - CRED_COMMENT_PIN: trusted_user_or_dev, - CRED_PKGBASE_ADOPT: trusted_user_or_dev, - CRED_PKGBASE_SET_KEYWORDS: trusted_user_or_dev, - CRED_PKGBASE_DELETE: trusted_user_or_dev, - CRED_PKGBASE_EDIT_COMAINTAINERS: trusted_user_or_dev, - CRED_PKGBASE_DISOWN: trusted_user_or_dev, - CRED_PKGBASE_LIST_VOTERS: trusted_user_or_dev, - CRED_PKGBASE_UNFLAG: trusted_user_or_dev, - CRED_PKGREQ_CLOSE: trusted_user_or_dev, - CRED_PKGREQ_LIST: trusted_user_or_dev, - CRED_TU_ADD_VOTE: trusted_user, - CRED_TU_LIST_VOTES: trusted_user_or_dev, - CRED_TU_VOTE: trusted_user, - CRED_ACCOUNT_EDIT_DEV: developer, - CRED_PKGBASE_MERGE: trusted_user_or_dev, -} - - -def has_credential(user: User, - credential: int, - approved_users: list = tuple()): - - if user in approved_users: - return True - - if credential in cred_filters: - cred_filter = cred_filters.get(credential) - return cred_filter(user) - - return False diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py new file mode 100644 index 00000000..8ceb136c --- /dev/null +++ b/aurweb/auth/__init__.py @@ -0,0 +1,192 @@ +import functools + +from datetime import datetime +from http import HTTPStatus + +import fastapi + +from fastapi import HTTPException +from fastapi.responses import RedirectResponse +from sqlalchemy import and_ +from starlette.authentication import AuthCredentials, AuthenticationBackend +from starlette.requests import HTTPConnection + +import aurweb.config + +from aurweb import db, l10n, util +from aurweb.models import Session, User +from aurweb.models.account_type import ACCOUNT_TYPE_ID + + +class StubQuery: + """ Acts as a stubbed version of an orm.Query. Typically used + to masquerade fake records for an AnonymousUser. """ + + def filter(self, *args): + return StubQuery() + + def scalar(self): + return 0 + + +class AnonymousUser: + """ A stubbed User class used when an unauthenticated User + makes a request against FastAPI. """ + # Stub attributes used to mimic a real user. + ID = 0 + + class AccountType: + """ A stubbed AccountType static class. In here, we use an ID + and AccountType which do not exist in our constant records. + All records primary keys (AccountType.ID) should be non-zero, + so using a zero here means that we'll never match against a + real AccountType. """ + ID = 0 + AccountType = "Anonymous" + + # AccountTypeID == AccountType.ID; assign a stubbed column. + AccountTypeID = AccountType.ID + + LangPreference = aurweb.config.get("options", "default_lang") + Timezone = aurweb.config.get("options", "default_timezone") + + Suspended = 0 + InactivityTS = 0 + + # A stub ssh_pub_key relationship. + ssh_pub_key = None + + # Add stubbed relationship backrefs. + notifications = StubQuery() + package_votes = StubQuery() + + # A nonce attribute, needed for all browser sessions; set in __init__. + nonce = None + + def __init__(self): + self.nonce = util.make_nonce() + + @staticmethod + def is_authenticated(): + return False + + @staticmethod + def is_trusted_user(): + return False + + @staticmethod + def is_developer(): + return False + + @staticmethod + def is_elevated(): + return False + + @staticmethod + def has_credential(credential, **kwargs): + return False + + @staticmethod + def voted_for(package): + return False + + @staticmethod + def notified(package): + return False + + +class BasicAuthBackend(AuthenticationBackend): + async def authenticate(self, conn: HTTPConnection): + sid = conn.cookies.get("AURSID") + if not sid: + return (None, AnonymousUser()) + + now_ts = datetime.utcnow().timestamp() + record = db.query(Session).filter( + and_(Session.SessionID == sid, + Session.LastUpdateTS >= now_ts)).first() + + # If no session with sid and a LastUpdateTS now or later exists. + if not record: + return (None, AnonymousUser()) + + # At this point, we cannot have an invalid user if the record + # exists, due to ForeignKey constraints in the schema upheld + # by mysqlclient. + user = db.query(User).filter(User.ID == record.UsersID).first() + user.nonce = util.make_nonce() + user.authenticated = True + + return (AuthCredentials(["authenticated"]), user) + + +def auth_required(is_required: bool = True, + template: tuple = None, + status_code: HTTPStatus = HTTPStatus.UNAUTHORIZED): + """ Authentication route decorator. + + :param is_required: A boolean indicating whether the function requires auth + :param status_code: An optional status_code for template render. + Redirects are always SEE_OTHER. + """ + + def decorator(func): + @functools.wraps(func) + async def wrapper(request, *args, **kwargs): + if request.user.is_authenticated() != is_required: + url = "/" + + if is_required: + if request.method == "GET": + url = request.url.path + elif request.method == "POST" and (referer := request.headers.get("Referer")): + aur = aurweb.config.get("options", "aur_location") + "/" + if not referer.startswith(aur): + _ = l10n.get_translator_for_request(request) + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, + detail=_("Bad Referer header.")) + url = referer[len(aur) - 1:] + + url = "/login?" + util.urlencode({"next": url}) + return RedirectResponse(url, + status_code=int(HTTPStatus.SEE_OTHER)) + return await func(request, *args, **kwargs) + return wrapper + + return decorator + + +def account_type_required(one_of: set): + """ A decorator that can be used on FastAPI routes to dictate + that a user belongs to one of the types defined in one_of. + + This decorator should be run after an @auth_required(True) is + dictated. + + - Example code: + + @router.get('/some_route') + @auth_required(True) + @account_type_required({"Trusted User", "Trusted User & Developer"}) + async def some_route(request: fastapi.Request): + return Response() + + :param one_of: A set consisting of strings to match against AccountType. + :return: Return the FastAPI function this decorator wraps. + """ + # Convert any account type string constants to their integer IDs. + one_of = { + ACCOUNT_TYPE_ID[atype] + for atype in one_of + if isinstance(atype, str) + } + + def decorator(func): + @functools.wraps(func) + async def wrapper(request: fastapi.Request, *args, **kwargs): + if request.user.AccountTypeID not in one_of: + return RedirectResponse("/", + status_code=int(HTTPStatus.SEE_OTHER)) + return await func(request, *args, **kwargs) + return wrapper + return decorator diff --git a/aurweb/auth/creds.py b/aurweb/auth/creds.py new file mode 100644 index 00000000..100aad8c --- /dev/null +++ b/aurweb/auth/creds.py @@ -0,0 +1,76 @@ +from aurweb.models.account_type import DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID +from aurweb.models.user import User + +ACCOUNT_CHANGE_TYPE = 1 +ACCOUNT_EDIT = 2 +ACCOUNT_EDIT_DEV = 3 +ACCOUNT_LAST_LOGIN = 4 +ACCOUNT_SEARCH = 5 +ACCOUNT_LIST_COMMENTS = 28 +COMMENT_DELETE = 6 +COMMENT_UNDELETE = 27 +COMMENT_VIEW_DELETED = 22 +COMMENT_EDIT = 25 +COMMENT_PIN = 26 +PKGBASE_ADOPT = 7 +PKGBASE_SET_KEYWORDS = 8 +PKGBASE_DELETE = 9 +PKGBASE_DISOWN = 10 +PKGBASE_EDIT_COMAINTAINERS = 24 +PKGBASE_FLAG = 11 +PKGBASE_LIST_VOTERS = 12 +PKGBASE_NOTIFY = 13 +PKGBASE_UNFLAG = 15 +PKGBASE_VOTE = 16 +PKGREQ_FILE = 23 +PKGREQ_CLOSE = 17 +PKGREQ_LIST = 18 +TU_ADD_VOTE = 19 +TU_LIST_VOTES = 20 +TU_VOTE = 21 +PKGBASE_MERGE = 29 + +user_developer_or_trusted_user = set([USER_ID, TRUSTED_USER_ID, DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) +trusted_user_or_dev = set([TRUSTED_USER_ID, DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) +developer = set([DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) +trusted_user = set([TRUSTED_USER_ID, TRUSTED_USER_AND_DEV_ID]) + +cred_filters = { + PKGBASE_FLAG: user_developer_or_trusted_user, + PKGBASE_NOTIFY: user_developer_or_trusted_user, + PKGBASE_VOTE: user_developer_or_trusted_user, + PKGREQ_FILE: user_developer_or_trusted_user, + ACCOUNT_CHANGE_TYPE: trusted_user_or_dev, + ACCOUNT_EDIT: trusted_user_or_dev, + ACCOUNT_LAST_LOGIN: trusted_user_or_dev, + ACCOUNT_LIST_COMMENTS: trusted_user_or_dev, + ACCOUNT_SEARCH: trusted_user_or_dev, + COMMENT_DELETE: trusted_user_or_dev, + COMMENT_UNDELETE: trusted_user_or_dev, + COMMENT_VIEW_DELETED: trusted_user_or_dev, + COMMENT_EDIT: trusted_user_or_dev, + COMMENT_PIN: trusted_user_or_dev, + PKGBASE_ADOPT: trusted_user_or_dev, + PKGBASE_SET_KEYWORDS: trusted_user_or_dev, + PKGBASE_DELETE: trusted_user_or_dev, + PKGBASE_EDIT_COMAINTAINERS: trusted_user_or_dev, + PKGBASE_DISOWN: trusted_user_or_dev, + PKGBASE_LIST_VOTERS: trusted_user_or_dev, + PKGBASE_UNFLAG: trusted_user_or_dev, + PKGREQ_CLOSE: trusted_user_or_dev, + PKGREQ_LIST: trusted_user_or_dev, + TU_ADD_VOTE: trusted_user, + TU_LIST_VOTES: trusted_user_or_dev, + TU_VOTE: trusted_user, + ACCOUNT_EDIT_DEV: developer, + PKGBASE_MERGE: trusted_user_or_dev, +} + + +def has_credential(user: User, + credential: int, + approved_users: list = tuple()): + + if user in approved_users: + return True + return user.AccountTypeID in cred_filters[credential] diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 03634a36..f0724202 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -1,6 +1,7 @@ import hashlib from datetime import datetime +from typing import List, Set import bcrypt @@ -136,10 +137,10 @@ class User(Base): request.cookies["AURSID"] = self.session.SessionID return self.session.SessionID - def has_credential(self, credential: str, approved: list = tuple()): - import aurweb.auth - cred = getattr(aurweb.auth, credential) - return aurweb.auth.has_credential(self, cred, approved) + def has_credential(self, credential: Set[int], + approved: List["User"] = list()): + from aurweb.auth.creds import has_credential + return has_credential(self, credential, approved) def logout(self, request): del request.cookies["AURSID"] diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index 545811f0..388daf84 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -10,7 +10,7 @@ from sqlalchemy import and_, or_ import aurweb.config from aurweb import cookies, db, l10n, logging, models, util -from aurweb.auth import account_type_required, auth_required +from aurweb.auth import account_type_required, auth_required, creds from aurweb.captcha import get_captcha_salts from aurweb.exceptions import ValidationError from aurweb.l10n import get_translator_for_request @@ -27,14 +27,14 @@ logger = logging.get_logger(__name__) @router.get("/passreset", response_class=HTMLResponse) -@auth_required(False, login=False) +@auth_required(False) async def passreset(request: Request): context = await make_variable_context(request, "Password Reset") return render_template(request, "passreset.html", context) @router.post("/passreset", response_class=HTMLResponse) -@auth_required(False, login=False) +@auth_required(False) async def passreset_post(request: Request, user: str = Form(...), resetkey: str = Form(default=None), @@ -176,7 +176,7 @@ def make_account_form_context(context: dict, user_account_type_id = context.get("account_types")[0][0] - if request.user.has_credential("CRED_ACCOUNT_EDIT_DEV"): + if request.user.has_credential(creds.ACCOUNT_EDIT_DEV): context["account_types"].append((at.DEVELOPER_ID, at.DEVELOPER)) context["account_types"].append((at.TRUSTED_USER_AND_DEV_ID, at.TRUSTED_USER_AND_DEV)) @@ -226,7 +226,7 @@ def make_account_form_context(context: dict, @router.get("/register", response_class=HTMLResponse) -@auth_required(False, login=False) +@auth_required(False) async def account_register(request: Request, U: str = Form(default=str()), # Username E: str = Form(default=str()), # Email @@ -252,7 +252,7 @@ async def account_register(request: Request, @router.post("/register", response_class=HTMLResponse) -@auth_required(False, login=False) +@auth_required(False) async def account_register_post(request: Request, U: str = Form(default=str()), # Username E: str = Form(default=str()), # Email @@ -332,7 +332,7 @@ async def account_register_post(request: Request, def cannot_edit(request, user): """ Return a 401 HTMLResponse if the request user doesn't have authorization, otherwise None. """ - has_dev_cred = request.user.has_credential("CRED_ACCOUNT_EDIT_DEV", + has_dev_cred = request.user.has_credential(creds.ACCOUNT_EDIT_DEV, approved=[user]) if not has_dev_cred: return HTMLResponse(status_code=HTTPStatus.UNAUTHORIZED) @@ -340,7 +340,7 @@ def cannot_edit(request, user): @router.get("/account/{username}/edit", response_class=HTMLResponse) -@auth_required(True, redirect="/account/{username}") +@auth_required() async def account_edit(request: Request, username: str): user = db.query(models.User, models.User.Username == username).first() @@ -356,7 +356,7 @@ async def account_edit(request: Request, username: str): @router.post("/account/{username}/edit", response_class=HTMLResponse) -@auth_required(True, redirect="/account/{username}") +@auth_required() async def account_edit_post(request: Request, username: str, U: str = Form(default=str()), # Username @@ -424,26 +424,20 @@ async def account_edit_post(request: Request, aurtz=TZ, aurlang=L) -account_template = ( - "account/show.html", - ["Account", "{}"], - ["username"] # Query parameters to replace in the title string. -) - - @router.get("/account/{username}") -@auth_required(True, template=account_template, - status_code=HTTPStatus.UNAUTHORIZED) async def account(request: Request, username: str): _ = l10n.get_translator_for_request(request) context = await make_variable_context( request, _("Account") + " " + username) + if not request.user.is_authenticated(): + return render_template(request, "account/show.html", context, + status_code=HTTPStatus.UNAUTHORIZED) context["user"] = get_user_by_name(username) return render_template(request, "account/show.html", context) @router.get("/account/{username}/comments") -@auth_required(redirect="/account/{username}/comments") +@auth_required() async def account_comments(request: Request, username: str): user = get_user_by_name(username) context = make_context(request, "Accounts") @@ -454,7 +448,7 @@ async def account_comments(request: Request, username: str): @router.get("/accounts") -@auth_required(True, redirect="/accounts") +@auth_required() @account_type_required({at.TRUSTED_USER, at.DEVELOPER, at.TRUSTED_USER_AND_DEV}) @@ -464,7 +458,7 @@ async def accounts(request: Request): @router.post("/accounts") -@auth_required(True, redirect="/accounts") +@auth_required() @account_type_required({at.TRUSTED_USER, at.DEVELOPER, at.TRUSTED_USER_AND_DEV}) @@ -548,7 +542,7 @@ def render_terms_of_service(request: Request, @router.get("/tos") -@auth_required(True, redirect="/tos") +@auth_required() async def terms_of_service(request: Request): # Query the database for terms that were previously accepted, # but now have a bumped Revision that needs to be accepted. @@ -572,7 +566,7 @@ async def terms_of_service(request: Request): @router.post("/tos") -@auth_required(True, redirect="/tos") +@auth_required() async def terms_of_service_post(request: Request, accept: bool = Form(default=False)): # Query the database for terms that were previously accepted, diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 1e0b026a..74763667 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -29,7 +29,7 @@ async def login_get(request: Request, next: str = "/"): @router.post("/login", response_class=HTMLResponse) -@auth_required(False, login=False) +@auth_required(False) async def login_post(request: Request, next: str = Form(...), user: str = Form(default=str()), diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index b5f8478e..c06ec51f 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -10,7 +10,7 @@ import aurweb.filters import aurweb.packages.util from aurweb import db, defaults, l10n, logging, models, util -from aurweb.auth import auth_required +from aurweb.auth import auth_required, creds from aurweb.exceptions import ValidationError from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID @@ -295,7 +295,7 @@ async def package_base_voters(request: Request, name: str) -> Response: @router.post("/pkgbase/{name}/comments") -@auth_required(True, redirect="/pkgbase/{name}/comments") +@auth_required() async def pkgbase_comments_post( request: Request, name: str, comment: str = Form(default=str()), @@ -327,7 +327,7 @@ async def pkgbase_comments_post( @router.get("/pkgbase/{name}/comments/{id}/form") -@auth_required(True, login=False) +@auth_required() async def pkgbase_comment_form(request: Request, name: str, id: int, next: str = Query(default=None)): """ Produce a comment form for comment {id}. """ @@ -353,7 +353,7 @@ async def pkgbase_comment_form(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}") +@auth_required() async def pkgbase_comment_post( request: Request, name: str, id: int, comment: str = Form(default=str()), @@ -392,7 +392,7 @@ async def pkgbase_comment_post( @router.get("/pkgbase/{name}/comments/{id}/edit") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/edit") +@auth_required() async def pkgbase_comment_edit(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -407,13 +407,13 @@ async def pkgbase_comment_edit(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/delete") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/delete") +@auth_required() async def pkgbase_comment_delete(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) - authorized = request.user.has_credential("CRED_COMMENT_DELETE", + authorized = request.user.has_credential(creds.COMMENT_DELETE, [comment.User]) if not authorized: _ = l10n.get_translator_for_request(request) @@ -433,13 +433,13 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/undelete") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/undelete") +@auth_required() async def pkgbase_comment_undelete(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential("CRED_COMMENT_UNDELETE", + has_cred = request.user.has_credential(creds.COMMENT_UNDELETE, approved=[comment.User]) if not has_cred: _ = l10n.get_translator_for_request(request) @@ -458,13 +458,13 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/pin") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/pin") +@auth_required() async def pkgbase_comment_pin(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential("CRED_COMMENT_PIN", + has_cred = request.user.has_credential(creds.COMMENT_PIN, approved=[pkgbase.Maintainer]) if not has_cred: _ = l10n.get_translator_for_request(request) @@ -483,13 +483,13 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int, @router.post("/pkgbase/{name}/comments/{id}/unpin") -@auth_required(True, redirect="/pkgbase/{name}/comments/{id}/unpin") +@auth_required() async def pkgbase_comment_unpin(request: Request, name: str, id: int, next: str = Form(default=None)): pkgbase = get_pkg_or_base(name, models.PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential("CRED_COMMENT_PIN", + has_cred = request.user.has_credential(creds.COMMENT_PIN, approved=[pkgbase.Maintainer]) if not has_cred: _ = l10n.get_translator_for_request(request) @@ -507,14 +507,14 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int, @router.get("/pkgbase/{name}/comaintainers") -@auth_required(True, redirect="/pkgbase/{name}/comaintainers") +@auth_required() async def package_base_comaintainers(request: Request, name: str) -> Response: # Get the PackageBase. pkgbase = get_pkg_or_base(name, models.PackageBase) # Unauthorized users (Non-TU/Dev and not the pkgbase maintainer) # get redirected to the package base's page. - has_creds = request.user.has_credential("CRED_PKGBASE_EDIT_COMAINTAINERS", + has_creds = request.user.has_credential(creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer]) if not has_creds: return RedirectResponse(f"/pkgbase/{name}", @@ -532,7 +532,7 @@ async def package_base_comaintainers(request: Request, name: str) -> Response: @router.post("/pkgbase/{name}/comaintainers") -@auth_required(True, redirect="/pkgbase/{name}/comaintainers") +@auth_required() async def package_base_comaintainers_post( request: Request, name: str, users: str = Form(default=str())) -> Response: @@ -541,7 +541,7 @@ async def package_base_comaintainers_post( # Unauthorized users (Non-TU/Dev and not the pkgbase maintainer) # get redirected to the package base's page. - has_creds = request.user.has_credential("CRED_PKGBASE_EDIT_COMAINTAINERS", + has_creds = request.user.has_credential(creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer]) if not has_creds: return RedirectResponse(f"/pkgbase/{name}", @@ -584,7 +584,7 @@ async def package_base_comaintainers_post( @router.get("/requests") -@auth_required(True, redirect="/requests") +@auth_required() async def requests(request: Request, O: int = Query(default=defaults.O), PP: int = Query(default=defaults.PP)): @@ -618,7 +618,7 @@ async def requests(request: Request, @router.get("/pkgbase/{name}/request") -@auth_required(True, redirect="/pkgbase/{name}/request") +@auth_required() async def package_request(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) context = await make_variable_context(request, "Submit Request") @@ -627,7 +627,7 @@ async def package_request(request: Request, name: str): @router.post("/pkgbase/{name}/request") -@auth_required(True, redirect="/pkgbase/{name}/request") +@auth_required() async def pkgbase_request_post(request: Request, name: str, type: str = Form(...), merge_into: str = Form(default=None), @@ -699,7 +699,7 @@ async def pkgbase_request_post(request: Request, name: str, @router.get("/requests/{id}/close") -@auth_required(True, redirect="/requests/{id}/close") +@auth_required() async def requests_close(request: Request, id: int): pkgreq = get_pkgreq_by_id(id) if not request.user.is_elevated() and request.user != pkgreq.User: @@ -712,7 +712,7 @@ async def requests_close(request: Request, id: int): @router.post("/requests/{id}/close") -@auth_required(True, redirect="/requests/{id}/close") +@auth_required() async def requests_close_post(request: Request, id: int, reason: int = Form(default=0), comments: str = Form(default=str())): @@ -775,11 +775,11 @@ async def pkgbase_keywords(request: Request, name: str, @router.get("/pkgbase/{name}/flag") -@auth_required(True, redirect="/pkgbase/{name}/flag") +@auth_required() async def pkgbase_flag_get(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) - has_cred = request.user.has_credential("CRED_PKGBASE_FLAG") + has_cred = request.user.has_credential(creds.PKGBASE_FLAG) if not has_cred or pkgbase.Flagger is not None: return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @@ -790,7 +790,7 @@ async def pkgbase_flag_get(request: Request, name: str): @router.post("/pkgbase/{name}/flag") -@auth_required(True, redirect="/pkgbase/{name}/flag") +@auth_required() async def pkgbase_flag_post(request: Request, name: str, comments: str = Form(default=str())): pkgbase = get_pkg_or_base(name, models.PackageBase) @@ -803,7 +803,7 @@ async def pkgbase_flag_post(request: Request, name: str, return render_template(request, "packages/flag.html", context, status_code=HTTPStatus.BAD_REQUEST) - has_cred = request.user.has_credential("CRED_PKGBASE_FLAG") + has_cred = request.user.has_credential(creds.PKGBASE_FLAG) if has_cred and not pkgbase.Flagger: now = int(datetime.utcnow().timestamp()) with db.begin(): @@ -830,7 +830,7 @@ async def pkgbase_flag_comment(request: Request, name: str): def pkgbase_unflag_instance(request: Request, pkgbase: models.PackageBase): has_cred = request.user.has_credential( - "CRED_PKGBASE_UNFLAG", approved=[pkgbase.Flagger, pkgbase.Maintainer]) + creds.PKGBASE_UNFLAG, approved=[pkgbase.Flagger, pkgbase.Maintainer]) if has_cred: with db.begin(): pkgbase.OutOfDateTS = None @@ -839,7 +839,7 @@ def pkgbase_unflag_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/unflag") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required() async def pkgbase_unflag(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) pkgbase_unflag_instance(request, pkgbase) @@ -851,7 +851,7 @@ def pkgbase_notify_instance(request: Request, pkgbase: models.PackageBase): notif = db.query(pkgbase.notifications.filter( models.PackageNotification.UserID == request.user.ID ).exists()).scalar() - has_cred = request.user.has_credential("CRED_PKGBASE_NOTIFY") + has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) if has_cred and not notif: with db.begin(): db.create(models.PackageNotification, @@ -860,7 +860,7 @@ def pkgbase_notify_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/notify") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required() async def pkgbase_notify(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) pkgbase_notify_instance(request, pkgbase) @@ -872,14 +872,14 @@ def pkgbase_unnotify_instance(request: Request, pkgbase: models.PackageBase): notif = pkgbase.notifications.filter( models.PackageNotification.UserID == request.user.ID ).first() - has_cred = request.user.has_credential("CRED_PKGBASE_NOTIFY") + has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) if has_cred and notif: with db.begin(): db.delete(notif) @router.post("/pkgbase/{name}/unnotify") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required() async def pkgbase_unnotify(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) pkgbase_unnotify_instance(request, pkgbase) @@ -888,14 +888,14 @@ async def pkgbase_unnotify(request: Request, name: str): @router.post("/pkgbase/{name}/vote") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required() async def pkgbase_vote(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) vote = pkgbase.package_votes.filter( models.PackageVote.UsersID == request.user.ID ).first() - has_cred = request.user.has_credential("CRED_PKGBASE_VOTE") + has_cred = request.user.has_credential(creds.PKGBASE_VOTE) if has_cred and not vote: now = int(datetime.utcnow().timestamp()) with db.begin(): @@ -912,14 +912,14 @@ async def pkgbase_vote(request: Request, name: str): @router.post("/pkgbase/{name}/unvote") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required() async def pkgbase_unvote(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) vote = pkgbase.package_votes.filter( models.PackageVote.UsersID == request.user.ID ).first() - has_cred = request.user.has_credential("CRED_PKGBASE_VOTE") + has_cred = request.user.has_credential(creds.PKGBASE_VOTE) if has_cred and vote: with db.begin(): db.delete(vote) @@ -954,11 +954,11 @@ def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase): @router.get("/pkgbase/{name}/disown") -@auth_required(True, redirect="/pkgbase/{name}/disown") +@auth_required() async def pkgbase_disown_get(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) - has_cred = request.user.has_credential("CRED_PKGBASE_DISOWN", + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer]) if not has_cred: return RedirectResponse(f"/pkgbase/{name}", @@ -970,12 +970,12 @@ async def pkgbase_disown_get(request: Request, name: str): @router.post("/pkgbase/{name}/disown") -@auth_required(True, redirect="/pkgbase/{name}/disown") +@auth_required() async def pkgbase_disown_post(request: Request, name: str, confirm: bool = Form(default=False)): pkgbase = get_pkg_or_base(name, models.PackageBase) - has_cred = request.user.has_credential("CRED_PKGBASE_DISOWN", + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer]) if not has_cred: return RedirectResponse(f"/pkgbase/{name}", @@ -1003,11 +1003,11 @@ def pkgbase_adopt_instance(request: Request, pkgbase: models.PackageBase): @router.post("/pkgbase/{name}/adopt") -@auth_required(True, redirect="/pkgbase/{name}") +@auth_required() async def pkgbase_adopt_post(request: Request, name: str): pkgbase = get_pkg_or_base(name, models.PackageBase) - has_cred = request.user.has_credential("CRED_PKGBASE_ADOPT") + has_cred = request.user.has_credential(creds.PKGBASE_ADOPT) if has_cred or not pkgbase.Maintainer: # If the user has credentials, they'll adopt the package regardless # of maintainership. Otherwise, we'll promote the user to maintainer @@ -1019,9 +1019,9 @@ async def pkgbase_adopt_post(request: Request, name: str): @router.get("/pkgbase/{name}/delete") -@auth_required(True, redirect="/pkgbase/{name}/delete") +@auth_required() async def pkgbase_delete_get(request: Request, name: str): - if not request.user.has_credential("CRED_PKGBASE_DELETE"): + if not request.user.has_credential(creds.PKGBASE_DELETE): return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @@ -1031,12 +1031,12 @@ async def pkgbase_delete_get(request: Request, name: str): @router.post("/pkgbase/{name}/delete") -@auth_required(True, redirect="/pkgbase/{name}/delete") +@auth_required() async def pkgbase_delete_post(request: Request, name: str, confirm: bool = Form(default=False)): pkgbase = get_pkg_or_base(name, models.PackageBase) - if not request.user.has_credential("CRED_PKGBASE_DELETE"): + if not request.user.has_credential(creds.PKGBASE_DELETE): return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @@ -1070,7 +1070,7 @@ async def packages_unflag(request: Request, package_ids: List[int] = [], models.Package.ID.in_(package_ids)).all() for pkg in packages: has_cred = request.user.has_credential( - "CRED_PKGBASE_UNFLAG", approved=[pkg.PackageBase.Flagger]) + creds.PKGBASE_UNFLAG, approved=[pkg.PackageBase.Flagger]) if not has_cred: return (False, ["You did not select any packages to unflag."]) @@ -1106,7 +1106,7 @@ async def packages_notify(request: Request, package_ids: List[int] = [], notif = db.query(pkgbase.notifications.filter( models.PackageNotification.UserID == request.user.ID ).exists()).scalar() - has_cred = request.user.has_credential("CRED_PKGBASE_NOTIFY") + has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) # If the request user either does not have credentials # or the notification already exists: @@ -1178,7 +1178,7 @@ async def packages_adopt(request: Request, package_ids: List[int] = [], # Check that the user has credentials for every package they selected. for pkgbase in bases: - has_cred = request.user.has_credential("CRED_PKGBASE_ADOPT") + has_cred = request.user.has_credential(creds.PKGBASE_ADOPT) if not (has_cred or not pkgbase.Maintainer): # TODO: This error needs to be translated. return (False, ["You are not allowed to adopt one of the " @@ -1211,7 +1211,7 @@ async def packages_disown(request: Request, package_ids: List[int] = [], # Check that the user has credentials for every package they selected. for pkgbase in bases: - has_cred = request.user.has_credential("CRED_PKGBASE_DISOWN", + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer]) if not has_cred: # TODO: This error needs to be translated. @@ -1235,7 +1235,7 @@ async def packages_delete(request: Request, package_ids: List[int] = [], return (False, ["The selected packages have not been deleted, " "check the confirmation checkbox."]) - if not request.user.has_credential("CRED_PKGBASE_DELETE"): + if not request.user.has_credential(creds.PKGBASE_DELETE): return (False, ["You do not have permission to delete packages."]) # A "memo" used to store names of packages that we delete. @@ -1279,7 +1279,7 @@ PACKAGE_ACTIONS = { @router.post("/packages") -@auth_required(redirect="/packages") +@auth_required() async def packages_post(request: Request, IDs: List[int] = Form(default=[]), action: str = Form(default=str()), @@ -1311,7 +1311,7 @@ async def packages_post(request: Request, @router.get("/pkgbase/{name}/merge") -@auth_required(redirect="/pkgbase/{name}/merge") +@auth_required() async def pkgbase_merge_get(request: Request, name: str, into: str = Query(default=str()), next: str = Query(default=str())): @@ -1329,10 +1329,10 @@ async def pkgbase_merge_get(request: Request, name: str, status_code = HTTPStatus.OK # TODO: Lookup errors from credential instead of hardcoding them. - # Idea: Something like credential_errors("CRED_PKGBASE_MERGE"). - # Perhaps additionally: bad_credential_status_code("CRED_PKGBASE_MERGE"). + # Idea: Something like credential_errors(creds.PKGBASE_MERGE). + # Perhaps additionally: bad_credential_status_code(creds.PKGBASE_MERGE). # Don't take these examples verbatim. We should find good naming. - if not request.user.has_credential("CRED_PKGBASE_MERGE"): + if not request.user.has_credential(creds.PKGBASE_MERGE): context["errors"] = [ "Only Trusted Users and Developers can merge packages."] status_code = HTTPStatus.UNAUTHORIZED @@ -1423,7 +1423,7 @@ def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase, @router.post("/pkgbase/{name}/merge") -@auth_required(redirect="/pkgbase/{name}/merge") +@auth_required() async def pkgbase_merge_post(request: Request, name: str, into: str = Form(default=str()), confirm: bool = Form(default=False), @@ -1434,7 +1434,7 @@ async def pkgbase_merge_post(request: Request, name: str, context["pkgbase"] = pkgbase # TODO: Lookup errors from credential instead of hardcoding them. - if not request.user.has_credential("CRED_PKGBASE_MERGE"): + if not request.user.has_credential(creds.PKGBASE_MERGE): context["errors"] = [ "Only Trusted Users and Developers can merge packages."] return render_template(request, "pkgbase/merge.html", context, diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py index 7c0a0404..fac68f04 100644 --- a/aurweb/routers/trusted_user.py +++ b/aurweb/routers/trusted_user.py @@ -41,7 +41,7 @@ ADDVOTE_SPECIFICS = { @router.get("/tu") -@auth_required(True, redirect="/tu") +@auth_required() @account_type_required(REQUIRED_TYPES) async def trusted_user(request: Request, coff: int = 0, # current offset @@ -147,7 +147,7 @@ def render_proposal(request: Request, @router.get("/tu/{proposal}") -@auth_required(True, redirect="/tu/{proposal}") +@auth_required() @account_type_required(REQUIRED_TYPES) async def trusted_user_proposal(request: Request, proposal: int): context = await make_variable_context(request, "Trusted User") @@ -176,7 +176,7 @@ async def trusted_user_proposal(request: Request, proposal: int): @router.post("/tu/{proposal}") -@auth_required(True, redirect="/tu/{proposal}") +@auth_required() @account_type_required(REQUIRED_TYPES) async def trusted_user_proposal_post(request: Request, proposal: int, @@ -227,8 +227,8 @@ async def trusted_user_proposal_post(request: Request, @router.get("/addvote") -@auth_required(True, redirect="/addvote") -@account_type_required({"Trusted User", "Trusted User & Developer"}) +@auth_required() +@account_type_required({TRUSTED_USER, TRUSTED_USER_AND_DEV}) async def trusted_user_addvote(request: Request, user: str = str(), type: str = "add_tu", @@ -247,7 +247,7 @@ async def trusted_user_addvote(request: Request, @router.post("/addvote") -@auth_required(True, redirect="/addvote") +@auth_required() @account_type_required({TRUSTED_USER, TRUSTED_USER_AND_DEV}) async def trusted_user_addvote_post(request: Request, user: str = Form(default=str()), diff --git a/aurweb/templates.py b/aurweb/templates.py index a7102ae1..635b22b4 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -16,7 +16,7 @@ from fastapi.responses import HTMLResponse import aurweb.config -from aurweb import captcha, cookies, l10n, time, util +from aurweb import auth, captcha, cookies, l10n, time, util # Prepare jinja2 objects. _loader = jinja2.FileSystemLoader(os.path.join( @@ -107,6 +107,7 @@ def make_context(request: Request, title: str, next: str = None): "now": datetime.now(tz=zoneinfo.ZoneInfo(timezone)), "utcnow": int(datetime.utcnow().timestamp()), "config": aurweb.config, + "creds": auth.creds, "next": next if next else request.url.path } diff --git a/templates/partials/account/comment.html b/templates/partials/account/comment.html index bc167cf7..8c310738 100644 --- a/templates/partials/account/comment.html +++ b/templates/partials/account/comment.html @@ -3,7 +3,7 @@ {% set header_cls = "%s %s" | format(header_cls, "comment-deleted") %} {% endif %} -{% if not comment.Deleter or request.user.has_credential("CRED_COMMENT_VIEW_DELETED", approved=[comment.Deleter]) %} +{% if not comment.Deleter or request.user.has_credential(creds.COMMENT_VIEW_DELETED, approved=[comment.Deleter]) %} {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %}

diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html index 2e47a932..f3c293d8 100644 --- a/templates/partials/account_form.html +++ b/templates/partials/account_form.html @@ -53,7 +53,7 @@

{% endif %} - {% if request.user.has_credential("CRED_ACCOUNT_CHANGE_TYPE") %} + {% if request.user.has_credential(creds.ACCOUNT_CHANGE_TYPE) %}

  • {% trans %}Accounts{% endtrans %} @@ -37,7 +37,7 @@
  • {# Only CRED_TU_LIST_VOTES privileged users see Trusted User #} - {% if request.user.has_credential("CRED_TU_LIST_VOTES") %} + {% if request.user.has_credential(creds.TU_LIST_VOTES) %}
  • {% trans %}Trusted User{% endtrans %}
  • diff --git a/templates/partials/comment_actions.html b/templates/partials/comment_actions.html index b8ccf945..78c4cc22 100644 --- a/templates/partials/comment_actions.html +++ b/templates/partials/comment_actions.html @@ -1,7 +1,7 @@ {% set pkgbasename = comment.PackageBase.Name %} {% if not comment.Deleter %} - {% if request.user.has_credential('CRED_COMMENT_DELETE', approved=[comment.User]) %} + {% if request.user.has_credential(creds.COMMENT_DELETE, approved=[comment.User]) %}
    {% endif %} - {% if request.user.has_credential('CRED_COMMENT_EDIT', approved=[comment.User]) %} + {% if request.user.has_credential(creds.COMMENT_EDIT, approved=[comment.User]) %} {% endif %} {% endif %} -{% elif request.user.has_credential("CRED_COMMENT_UNDELETE", approved=[comment.User]) %} +{% elif request.user.has_credential(creds.COMMENT_UNDELETE, approved=[comment.User]) %} {% endif %} - {% if request.user.has_credential('CRED_PKGBASE_EDIT_COMAINTAINERS', approved=[pkgbase.Maintainer]) %} + {% if request.user.has_credential(creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer]) %}
  • {{ "Manage Co-Maintainers" | tr }} @@ -107,14 +107,14 @@ {{ "Submit Request" | tr }}
  • - {% if request.user.has_credential("CRED_PKGBASE_DELETE") %} + {% if request.user.has_credential(creds.PKGBASE_DELETE) %}
  • {{ "Delete Package" | tr }}
  • {% endif %} - {% if request.user.has_credential("CRED_PKGBASE_MERGE") %} + {% if request.user.has_credential(creds.PKGBASE_MERGE) %}
  • {{ "Merge Package" | tr }} @@ -130,7 +130,7 @@ />
  • - {% elif request.user.has_credential("CRED_PKGBASE_DISOWN", approved=[pkgbase.Maintainer]) %} + {% elif request.user.has_credential(creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer]) %}
  • {{ "Disown Package" | tr }} diff --git a/templates/partials/packages/comment.html b/templates/partials/packages/comment.html index 676a7a73..1427e0a0 100644 --- a/templates/partials/packages/comment.html +++ b/templates/partials/packages/comment.html @@ -5,7 +5,7 @@ {% set article_cls = "%s %s" | format(article_cls, "comment-deleted") %} {% endif %} -{% if not comment.Deleter or request.user.has_credential("CRED_COMMENT_VIEW_DELETED", approved=[comment.Deleter]) %} +{% if not comment.Deleter or request.user.has_credential(creds.COMMENT_VIEW_DELETED, approved=[comment.Deleter]) %}

    {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} {% set view_account_info = 'View account information for %s' | tr | format(comment.User.Username) %} diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index dbb81c19..78e0ad1c 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -33,10 +33,10 @@ {% endif %} - {% if pkgbase.keywords.count() or request.user.has_credential("CRED_PKGBASE_SET_KEYWORDS", approved=[pkgbase.Maintainer]) %} + {% if pkgbase.keywords.count() or request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer]) %} {{ "Keywords" | tr }}: - {% if request.user.has_credential("CRED_PKGBASE_SET_KEYWORDS", approved=[pkgbase.Maintainer]) %} + {% if request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer]) %}