Files
CTFd/CTFd/schemas/users.py
Kevin Chung 22c132358e 2.3.0 (#1248)
2.3.0 / 2020-02-17
==================

**General**
* During setup, admins can register their email address with the CTFd LLC newsletter for news and updates
* Fix editting hints from the admin panel
* Allow admins to insert HTML code directly into the header and footer (end of body tag) of pages. This replaces and supercedes the custom CSS feature.
    * The `views.custom_css` route has been removed.
* Admins can now customize the content of outgoing emails and inject certain variables into email content.
* The `manage.py` script can now manipulate the CTFd Configs table via the `get_config` and `set_config` commands. (e.g. `python manage.py get_config ctf_theme` and `python manage.py set_config ctf_theme core`)

**Themes**
* Themes should now reference the `theme_header` and `theme_footer` configs instead of the `views.custom_css` endpoint to allow for user customizations. See the `base.html` file of the core theme.

**Plugins**
* Make `ezq` functions available to `CTFd.js` under `CTFd.ui.ezq`

**Miscellaneous**
* Python imports sorted with `isort` and import order enforced
* Black formatter running on a majority of Python code
2020-02-17 02:17:25 -05:00

231 lines
7.5 KiB
Python

from marshmallow import ValidationError, pre_load, validate
from marshmallow_sqlalchemy import field_for
from CTFd.models import Users, ma
from CTFd.utils import get_config, string_types
from CTFd.utils.crypto import verify_password
from CTFd.utils.email import check_email_is_whitelisted
from CTFd.utils.user import get_current_user, is_admin
from CTFd.utils.validators import validate_country_code
class UserSchema(ma.ModelSchema):
class Meta:
model = Users
include_fk = True
dump_only = ("id", "oauth_id", "created")
load_only = ("password",)
name = field_for(
Users,
"name",
required=True,
allow_none=False,
validate=[
validate.Length(min=1, max=128, error="User names must not be empty")
],
)
email = field_for(
Users,
"email",
allow_none=False,
validate=[
validate.Email("Emails must be a properly formatted email address"),
validate.Length(min=1, max=128, error="Emails must not be empty"),
],
)
website = field_for(
Users,
"website",
validate=[
# This is a dirty hack to let website accept empty strings so you can remove your website
lambda website: validate.URL(
error="Websites must be a proper URL starting with http or https",
schemes={"http", "https"},
)(website)
if website
else True
],
)
country = field_for(Users, "country", validate=[validate_country_code])
password = field_for(Users, "password")
@pre_load
def validate_name(self, data):
name = data.get("name")
if name is None:
return
name = name.strip()
existing_user = Users.query.filter_by(name=name).first()
current_user = get_current_user()
if is_admin():
user_id = data.get("id")
if user_id:
if existing_user and existing_user.id != user_id:
raise ValidationError(
"User name has already been taken", field_names=["name"]
)
else:
if existing_user:
if current_user:
if current_user.id != existing_user.id:
raise ValidationError(
"User name has already been taken", field_names=["name"]
)
else:
raise ValidationError(
"User name has already been taken", field_names=["name"]
)
else:
if name == current_user.name:
return data
else:
name_changes = get_config("name_changes", default=True)
if bool(name_changes) is False:
raise ValidationError(
"Name changes are disabled", field_names=["name"]
)
if existing_user:
raise ValidationError(
"User name has already been taken", field_names=["name"]
)
@pre_load
def validate_email(self, data):
email = data.get("email")
if email is None:
return
email = email.strip()
existing_user = Users.query.filter_by(email=email).first()
current_user = get_current_user()
if is_admin():
user_id = data.get("id")
if user_id:
if existing_user and existing_user.id != user_id:
raise ValidationError(
"Email address has already been used", field_names=["email"]
)
else:
if existing_user:
if current_user:
if current_user.id != existing_user.id:
raise ValidationError(
"Email address has already been used",
field_names=["email"],
)
else:
raise ValidationError(
"Email address has already been used", field_names=["email"]
)
else:
if email == current_user.email:
return data
else:
confirm = data.get("confirm")
if bool(confirm) is False:
raise ValidationError(
"Please confirm your current password", field_names=["confirm"]
)
test = verify_password(
plaintext=confirm, ciphertext=current_user.password
)
if test is False:
raise ValidationError(
"Your previous password is incorrect", field_names=["confirm"]
)
if existing_user:
raise ValidationError(
"Email address has already been used", field_names=["email"]
)
if check_email_is_whitelisted(email) is False:
raise ValidationError(
"Only email addresses under {domains} may register".format(
domains=get_config("domain_whitelist")
),
field_names=["email"],
)
if get_config("verify_emails"):
current_user.verified = False
@pre_load
def validate_password_confirmation(self, data):
password = data.get("password")
confirm = data.get("confirm")
target_user = get_current_user()
if is_admin():
pass
else:
if password and (bool(confirm) is False):
raise ValidationError(
"Please confirm your current password", field_names=["confirm"]
)
if password and confirm:
test = verify_password(
plaintext=confirm, ciphertext=target_user.password
)
if test is True:
return data
else:
raise ValidationError(
"Your previous password is incorrect", field_names=["confirm"]
)
else:
data.pop("password", None)
data.pop("confirm", None)
views = {
"user": [
"website",
"name",
"country",
"affiliation",
"bracket",
"id",
"oauth_id",
],
"self": [
"website",
"name",
"email",
"country",
"affiliation",
"bracket",
"id",
"oauth_id",
"password",
],
"admin": [
"website",
"name",
"created",
"country",
"banned",
"email",
"affiliation",
"secret",
"bracket",
"hidden",
"id",
"oauth_id",
"password",
"type",
"verified",
],
}
def __init__(self, view=None, *args, **kwargs):
if view:
if isinstance(view, string_types):
kwargs["only"] = self.views[view]
elif isinstance(view, list):
kwargs["only"] = view
super(UserSchema, self).__init__(*args, **kwargs)