aurweb/aurweb/models/user.py
Kevin Morris c006386079
add User.is_elevated()
This one returns true if the user is either a Trusted User
or a Developer.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-19 12:44:18 -07:00

221 lines
7.5 KiB
Python

import hashlib
from datetime import datetime
import bcrypt
from fastapi import Request
from sqlalchemy import Column, ForeignKey, Integer, String, or_, text
from sqlalchemy.orm import backref, relationship
import aurweb.config
import aurweb.models.account_type
import aurweb.schema
from aurweb import db
from aurweb.models.ban import is_banned
from aurweb.models.declarative import Base
SALT_ROUNDS_DEFAULT = 12
class User(Base):
""" An ORM model of a single Users record. """
__tablename__ = "Users"
ID = Column(Integer, primary_key=True)
AccountTypeID = Column(
Integer, ForeignKey("AccountTypes.ID", ondelete="NO ACTION"),
nullable=False, server_default=text("1"))
AccountType = relationship(
"AccountType",
backref=backref("users", lazy="dynamic"),
foreign_keys=[AccountTypeID],
uselist=False)
Passwd = Column(String(255), default=str())
__mapper_args__ = {"primary_key": [ID]}
# High-level variables used to track authentication (not in DB).
authenticated = False
nonce = None
# Make this static to the class just in case SQLAlchemy ever
# does something to bypass our constructor.
salt_rounds = aurweb.config.getint("options", "salt_rounds",
SALT_ROUNDS_DEFAULT)
def __init__(self, Passwd: str = str(), **kwargs):
super().__init__(**kwargs)
# Run this again in the constructor in case we rehashed config.
self.salt_rounds = aurweb.config.getint("options", "salt_rounds",
SALT_ROUNDS_DEFAULT)
if Passwd:
self.update_password(Passwd)
def update_password(self, password):
self.Passwd = bcrypt.hashpw(
password.encode(),
bcrypt.gensalt(rounds=self.salt_rounds)).decode()
@staticmethod
def minimum_passwd_length():
return aurweb.config.getint("options", "passwd_min_len")
def is_authenticated(self):
""" Return internal authenticated state. """
return self.authenticated
def valid_password(self, password: str):
""" Check authentication against a given password. """
if password is None:
return False
password_is_valid = False
try:
password_is_valid = bcrypt.checkpw(password.encode(),
self.Passwd.encode())
except ValueError:
pass
# If our Salt column is not empty, we're using a legacy password.
if not password_is_valid and self.Salt != str():
# Try to login with legacy method.
password_is_valid = hashlib.md5(
f"{self.Salt}{password}".encode()
).hexdigest() == self.Passwd
# We got here, we passed the legacy authentication.
# Update the password to our modern hash style.
if password_is_valid:
self.update_password(password)
return password_is_valid
def _login_approved(self, request: Request):
return not is_banned(request) and not self.Suspended
def login(self, request: Request, password: str, session_time=0):
""" Login and authenticate a request. """
from aurweb import db
from aurweb.models.session import Session, generate_unique_sid
if not self._login_approved(request):
return None
self.authenticated = self.valid_password(password)
if not self.authenticated:
return None
now_ts = datetime.utcnow().timestamp()
session_ts = now_ts + (
session_time if session_time
else aurweb.config.getint("options", "login_timeout")
)
sid = None
with db.begin():
self.LastLogin = now_ts
self.LastLoginIPAddress = request.client.host
if not self.session:
sid = generate_unique_sid()
self.session = Session(UsersID=self.ID, SessionID=sid,
LastUpdateTS=session_ts)
db.add(self.session)
else:
last_updated = self.session.LastUpdateTS
if last_updated and last_updated < now_ts:
self.session.SessionID = sid = generate_unique_sid()
else:
# Session is still valid; retrieve the current SID.
sid = self.session.SessionID
self.session.LastUpdateTS = session_ts
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 logout(self, request):
del request.cookies["AURSID"]
self.authenticated = False
if self.session:
with db.begin():
db.session.delete(self.session)
def is_trusted_user(self):
return self.AccountType.ID in {
aurweb.models.account_type.TRUSTED_USER_ID,
aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID
}
def is_developer(self):
return self.AccountType.ID in {
aurweb.models.account_type.DEVELOPER_ID,
aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID
}
def is_elevated(self):
""" A User is 'elevated' when they have either a
Trusted User or Developer AccountType. """
return self.AccountType.ID in {
aurweb.models.account_type.TRUSTED_USER_ID,
aurweb.models.account_type.DEVELOPER_ID,
aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID,
}
def can_edit_user(self, user):
""" Can this account record edit the target user? It must either
be the target user or a user with enough permissions to do so.
:param user: Target user
:return: Boolean indicating whether this instance can edit `user`
"""
return self == user or self.is_trusted_user() or self.is_developer()
def voted_for(self, package) -> bool:
""" Has this User voted for package? """
from aurweb.models.package_vote import PackageVote
return bool(package.PackageBase.package_votes.filter(
PackageVote.UsersID == self.ID
).scalar())
def notified(self, package) -> bool:
""" Is this User being notified about package? """
from aurweb.models.package_notification import PackageNotification
return bool(package.PackageBase.package_notifications.filter(
PackageNotification.UserID == self.ID
).scalar())
def packages(self):
""" Returns an ORM query to Package objects owned by this user.
This should really be replaced with an internal ORM join
configured for the User model. This has not been done yet
due to issues I've been encountering in the process, so
sticking with this function until we can properly implement it.
:return: ORM query of User-packaged or maintained Package objects
"""
from aurweb.models.package import Package
from aurweb.models.package_base import PackageBase
return db.query(Package).join(PackageBase).filter(
or_(
PackageBase.PackagerUID == self.ID,
PackageBase.MaintainerUID == self.ID
)
)
def __repr__(self):
return "<User(ID='%s', AccountType='%s', Username='%s')>" % (
self.ID, str(self.AccountType), self.Username)