change(users.validate): users can't edit their own account types

This commit also decouples testing regarding this feature
into several test functions.

Signed-off-by: Kevin Morris <kevr@0cost.org>

bump

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-12-14 14:31:46 -08:00
parent c7751d5d63
commit f357615bfb
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
3 changed files with 146 additions and 128 deletions

View file

@ -11,13 +11,16 @@ from typing import List, Optional, Tuple
from fastapi import Request from fastapi import Request
from sqlalchemy import and_ from sqlalchemy import and_
from aurweb import config, db, l10n, models, time, util from aurweb import config, db, l10n, logging, models, time, util
from aurweb.auth import creds
from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token
from aurweb.exceptions import ValidationError from aurweb.exceptions import ValidationError
from aurweb.models import account_type as at from aurweb.models import account_type as at
from aurweb.models.account_type import ACCOUNT_TYPE_NAME from aurweb.models.account_type import ACCOUNT_TYPE_NAME
from aurweb.models.ssh_pub_key import get_fingerprint from aurweb.models.ssh_pub_key import get_fingerprint
logger = logging.get_logger(__name__)
def invalid_fields(E: str = str(), U: str = str(), **kwargs) \ def invalid_fields(E: str = str(), U: str = str(), **kwargs) \
-> Optional[Tuple[bool, List[str]]]: -> Optional[Tuple[bool, List[str]]]:
@ -171,26 +174,31 @@ def invalid_account_type(T: int = None, request: Request = None,
_: l10n.Translator = None, _: l10n.Translator = None,
**kwargs) -> None: **kwargs) -> None:
if T is not None and (T := int(T)) != user.AccountTypeID: if T is not None and (T := int(T)) != user.AccountTypeID:
has_cred = request.user.has_credential(creds.ACCOUNT_CHANGE_TYPE)
if T not in ACCOUNT_TYPE_NAME: if T not in ACCOUNT_TYPE_NAME:
raise ValidationError(["Invalid account type provided."]) raise ValidationError(["Invalid account type provided."])
elif not request.user.is_elevated(): elif not has_cred:
raise ValidationError([ raise ValidationError([
"You do not have permission to change account types."]) "You do not have permission to change account types."])
credential_checks = { credential_checks = {
at.USER_ID: request.user.is_trusted_user, at.USER_ID: request.user.is_trusted_user,
at.TRUSTED_USER_ID: request.user.is_trusted_user, at.TRUSTED_USER_ID: request.user.is_trusted_user,
at.DEVELOPER_ID: lambda: request.user.is_developer(), at.DEVELOPER_ID: request.user.is_developer,
at.TRUSTED_USER_AND_DEV_ID: (lambda: request.user.is_trusted_user() at.TRUSTED_USER_AND_DEV_ID: (lambda: request.user.is_trusted_user()
and request.user.is_developer()) and request.user.is_developer())
} }
credential_check = credential_checks.get(T) credential_check = credential_checks.get(T)
if not credential_check(): name = ACCOUNT_TYPE_NAME.get(T)
name = ACCOUNT_TYPE_NAME.get(T) if not credential_check() or request.user == user:
error = _("You do not have permission to change " error = _("You do not have permission to change "
"this user's account type to %s.") % name "this user's account type to %s.") % name
raise ValidationError([error]) raise ValidationError([error])
else:
logger.debug(f"Trusted User '{request.user.Username}' has "
f"modified '{user.Username}' account's type to"
f" {name}.")
def invalid_captcha(captcha_salt: str = None, captcha: str = None, **kwargs) \ def invalid_captcha(captcha_salt: str = None, captcha: str = None, **kwargs) \

View file

@ -43,24 +43,25 @@
</p> </p>
{% if request.user.has_credential(creds.ACCOUNT_CHANGE_TYPE) %} {% if request.user.has_credential(creds.ACCOUNT_CHANGE_TYPE) %}
<p> {% if request.user != user %}
<label for="id_type"> <p>
{% trans %}Account Type{% endtrans %}: <label for="id_type">
</label> {% trans %}Account Type{% endtrans %}:
<select name="T" id="id_type"> </label>
{% for value, type in account_types %} <select name="T" id="id_type">
<option value="{{ value }}" {% for value, type in account_types %}
{% if request.user.AccountType.ID == value %} <option value="{{ value }}"
selected="selected" {% if request.user.AccountType.ID == value %}
{% endif %} selected="selected"
> {% endif %}
{{ type | tr }} >
</option> {{ type | tr }}
{% endfor %} </option>
{% endfor %}
</select>
</p>
</select>
</p>
{% endif %}
<p> <p>
<label for="id_suspended"> <label for="id_suspended">
{% trans %}Account Suspended{% endtrans %}: {% trans %}Account Suspended{% endtrans %}:

View file

@ -3,6 +3,7 @@ import tempfile
from datetime import datetime from datetime import datetime
from http import HTTPStatus from http import HTTPStatus
from logging import DEBUG
from subprocess import Popen from subprocess import Popen
import lxml.html import lxml.html
@ -14,7 +15,8 @@ from aurweb import captcha, db, logging
from aurweb.asgi import app from aurweb.asgi import app
from aurweb.db import create, query from aurweb.db import create, query
from aurweb.models.accepted_term import AcceptedTerm from aurweb.models.accepted_term import AcceptedTerm
from aurweb.models.account_type import DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID, AccountType from aurweb.models.account_type import (DEVELOPER_ID, TRUSTED_USER, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID,
AccountType)
from aurweb.models.ban import Ban from aurweb.models.ban import Ban
from aurweb.models.session import Session from aurweb.models.session import Session
from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint
@ -54,13 +56,18 @@ def client() -> TestClient:
yield TestClient(app=app) yield TestClient(app=app)
def create_user(username: str) -> User:
email = f"{username}@example.org"
user = create(User, Username=username, Email=email,
Passwd="testPassword",
AccountTypeID=USER_ID)
return user
@pytest.fixture @pytest.fixture
def user() -> User: def user() -> User:
with db.begin(): with db.begin():
user = create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, user = create_user(TEST_USERNAME)
RealName="Test UserZ", Passwd="testPassword",
IRCNick="testZ", AccountTypeID=USER_ID)
yield user yield user
@ -986,118 +993,130 @@ def test_post_account_edit_password(client: TestClient, user: User):
assert user.valid_password("newPassword") assert user.valid_password("newPassword")
def test_post_account_edit_account_types(client: TestClient, user: User): def test_post_account_edit_self_type_as_user(client: TestClient, user: User):
request = Request() cookies = {"AURSID": user.login(Request(), "testPassword")}
sid = user.login(request, "testPassword")
cookies = {"AURSID": sid}
endpoint = f"/account/{user.Username}/edit" endpoint = f"/account/{user.Username}/edit"
# As a normal user, we cannot see the "Account Type:" input.
with client as request: with client as request:
resp = request.get(endpoint, cookies=cookies) resp = request.get(endpoint, cookies=cookies)
assert resp.status_code == int(HTTPStatus.OK) assert resp.status_code == int(HTTPStatus.OK)
assert "id_type" not in resp.text assert "id_type" not in resp.text
# Invalid account types return an error. data = {
post_data = {
"U": user.Username,
"E": user.Email,
"T": 0, # Invalid type ID
"passwd": "testPassword"
}
with client as request:
resp = request.post(endpoint, data=post_data, cookies=cookies)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
errors = get_errors(resp.text)
expected = "Invalid account type provided."
assert errors[0].text.strip() == expected
# Nor can we change any account types.
post_data = {
"U": user.Username, "U": user.Username,
"E": user.Email, "E": user.Email,
"T": TRUSTED_USER_ID, "T": TRUSTED_USER_ID,
"passwd": "testPassword" "passwd": "testPassword"
} }
with client as request: with client as request:
resp = request.post(endpoint, data=post_data, cookies=cookies) resp = request.post(endpoint, data=data, cookies=cookies)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST) assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
errors = get_errors(resp.text) errors = get_errors(resp.text)
expected = "You do not have permission to change account types." expected = "You do not have permission to change account types."
assert errors[0].text.strip() == expected assert errors[0].text.strip() == expected
# Change user from USER_ID to TRUSTED_USER_ID.
def test_post_account_edit_other_user_as_user(client: TestClient, user: User):
with db.begin(): with db.begin():
user.AccountTypeID = TRUSTED_USER_ID user2 = create_user("test2")
cookies = {"AURSID": user.login(Request(), "testPassword")}
endpoint = f"/account/{user2.Username}/edit"
# As a trusted user, we can see the "Account Type:" input.
with client as request: with client as request:
resp = request.get(endpoint, cookies=cookies) resp = request.get(endpoint, cookies=cookies,
allow_redirects=False)
assert resp.status_code == int(HTTPStatus.SEE_OTHER)
assert resp.headers.get("location") == f"/account/{user2.Username}"
def test_post_account_edit_self_type_as_tu(client: TestClient, tu_user: User):
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
endpoint = f"/account/{tu_user.Username}/edit"
# We cannot see the Account Type field on our own edit page.
with client as request:
resp = request.get(endpoint, cookies=cookies, allow_redirects=False)
assert resp.status_code == int(HTTPStatus.OK) assert resp.status_code == int(HTTPStatus.OK)
assert "id_type" in resp.text assert "id_type" not in resp.text
# As a trusted user, we cannot change account type to DEVELOPER_ID. # We cannot modify our own account type.
post_data = { data = {
"U": user.Username, "U": tu_user.Username,
"E": user.Email, "E": tu_user.Email,
"T": DEVELOPER_ID,
"passwd": "testPassword"
}
with client as request:
resp = request.post(endpoint, data=post_data, cookies=cookies)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
errors = get_errors(resp.text)
expected = ("You do not have permission to change "
"this user's account type to Developer.")
assert errors[0].text.strip() == expected
# However, we can modify our account type to USER_ID.
post_data = {
"U": user.Username,
"E": user.Email,
"T": USER_ID, "T": USER_ID,
"passwd": "testPassword" "passwd": "testPassword"
} }
with client as request: with client as request:
resp = request.post(endpoint, data=post_data, cookies=cookies) resp = request.post(endpoint, data=data, cookies=cookies)
assert resp.status_code == int(HTTPStatus.OK)
# No errors should be displayed.
errors = get_errors(resp.text)
assert not errors
# Make sure it got changed to USER_ID as we intended.
assert user.AccountTypeID == USER_ID
# Change user to a TU & Dev, which can change themselves to a Developer.
with db.begin():
user.AccountTypeID = TRUSTED_USER_AND_DEV_ID
# As a TU & Dev, we can absolutely change all account types.
# For example, from TRUSTED_USER_AND_DEV_ID to DEVELOPER_ID:
post_data = {
"U": user.Username,
"E": user.Email,
"T": DEVELOPER_ID,
"passwd": "testPassword"
}
with client as request:
resp = request.post(endpoint, data=post_data, cookies=cookies)
assert resp.status_code == int(HTTPStatus.OK)
assert user.AccountTypeID == DEVELOPER_ID
# But we can't change a user to a Trusted User & Developer when
# we're just a Developer.
post_data = {
"U": user.Username,
"E": user.Email,
"T": TRUSTED_USER_AND_DEV_ID,
"passwd": "testPassword"
}
with client as request:
resp = request.post(endpoint, data=post_data, cookies=cookies)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST) assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
assert user.AccountTypeID == DEVELOPER_ID
errors = get_errors(resp.text)
expected = ("You do not have permission to change this "
"user's account type to User.")
assert errors[0].text.strip() == expected
def test_post_account_edit_other_user_type_as_tu(
client: TestClient, tu_user: User, caplog: pytest.LogCaptureFixture):
caplog.set_level(DEBUG)
with db.begin():
user2 = create_user("test2")
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
endpoint = f"/account/{user2.Username}/edit"
# As a TU, we can see the Account Type field for other users.
with client as request:
resp = request.get(endpoint, cookies=cookies, allow_redirects=False)
assert resp.status_code == int(HTTPStatus.OK)
assert "id_type" in resp.text
# As a TU, we can modify other user's account types.
data = {
"U": user2.Username,
"E": user2.Email,
"T": TRUSTED_USER_ID,
"passwd": "testPassword"
}
with client as request:
resp = request.post(endpoint, data=data, cookies=cookies)
assert resp.status_code == int(HTTPStatus.OK)
# Let's make sure the DB got updated properly.
assert user2.AccountTypeID == TRUSTED_USER_ID
# and also that this got logged out at DEBUG level.
expected = (f"Trusted User '{tu_user.Username}' has "
f"modified '{user2.Username}' account's type to"
f" {TRUSTED_USER}.")
assert expected in caplog.text
def test_post_account_edit_other_user_type_as_tu_invalid_type(
client: TestClient, tu_user: User, caplog: pytest.LogCaptureFixture):
with db.begin():
user2 = create_user("test2")
cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
endpoint = f"/account/{user2.Username}/edit"
# As a TU, we can modify other user's account types.
data = {
"U": user2.Username,
"E": user2.Email,
"T": 0,
"passwd": "testPassword"
}
with client as request:
resp = request.post(endpoint, data=data, cookies=cookies)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
errors = get_errors(resp.text)
expected = "Invalid account type provided."
assert errors[0].text.strip() == expected
def test_get_account(client: TestClient, user: User): def test_get_account(client: TestClient, user: User):
@ -1199,11 +1218,7 @@ def test_post_accounts(client: TestClient, user: User, tu_user: User):
users = [user] users = [user]
with db.begin(): with db.begin():
for i in range(10): for i in range(10):
_user = create(User, Username=f"test_{i}", _user = create_user(f"test_{i}")
Email=f"test_{i}@example.org",
RealName=f"Test #{i}",
Passwd="testPassword",
IRCNick=f"test_#{i}")
users.append(_user) users.append(_user)
sid = user.login(Request(), "testPassword") sid = user.login(Request(), "testPassword")
@ -1230,8 +1245,8 @@ def test_post_accounts(client: TestClient, user: User, tu_user: User):
assert atype.text.strip() == str(_user.AccountType) assert atype.text.strip() == str(_user.AccountType)
assert suspended.text.strip() == "Active" assert suspended.text.strip() == "Active"
assert real_name.text.strip() == _user.RealName assert real_name.text == (_user.RealName or None)
assert irc_nick.text == _user.IRCNick assert irc_nick.text == (_user.IRCNick or None)
assert pgp_key.text == (_user.PGPKey or None) assert pgp_key.text == (_user.PGPKey or None)
edit = edit.xpath("./a") edit = edit.xpath("./a")
@ -1435,15 +1450,9 @@ def test_post_accounts_irc(client: TestClient, user: User, tu_user: User):
def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User):
# Create a second user so we can compare sorts. # Create a second user so we can compare sorts.
account_type = query(AccountType,
AccountType.ID == DEVELOPER_ID).first()
with db.begin(): with db.begin():
create(User, Username="test2", user_ = create_user("test2")
Email="test2@example.org", user_.AccountTypeID = DEVELOPER_ID
RealName="Test User 2",
Passwd="testPassword",
IRCNick="test2",
AccountType=account_type)
sid = user.login(Request(), "testPassword") sid = user.login(Request(), "testPassword")
cookies = {"AURSID": sid} cookies = {"AURSID": sid}
@ -1464,8 +1473,8 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User):
assert len(rows) == 2 assert len(rows) == 2
def compare_text_values(column, lhs, rhs): def compare_text_values(column, lhs, rhs):
return [row[column].text.strip() for row in lhs] \ return [row[column].text for row in lhs] \
== [row[column].text.strip() for row in rhs] == [row[column].text for row in rhs]
# Test the username rows are ordered the same. # Test the username rows are ordered the same.
assert compare_text_values(0, first_rows, rows) is True assert compare_text_values(0, first_rows, rows) is True