module
pretalx.person.domain.verification
Functions 14
cancel_email_change(user)source
def cancel_email_change(user):
if not user.pending_email:
return
pending = user.pending_email
_clear_pending_email(user)
user.log_action("pretalx.user.email.change.cancel", data={"pending_email": pending})
check_send_cooldown(user, *, free_pass=False)source
def check_send_cooldown(user, *, free_pass=False):
# One correction is free, afterwards follow resend cooldown timer
remaining = send_cooldown_remaining(user)
if not remaining:
return
if free_pass and cache.add(
_free_correction_key(user), True, timeout=dt.timedelta(hours=24).total_seconds()
):
return
raise SendCooldownError(remaining)
confirm_verification(token)source
def confirm_verification(token):
user, kind = parse_verification_token(token)
if kind == KIND_CHANGE:
if pending_email_expired(user):
_clear_pending_email(user)
raise PendingEmailExpiredError
address = user.pending_email
try:
validate_email_unique(address, exclude_user=user)
except ValidationError as error:
_clear_pending_email(user)
user.log_action(
"pretalx.user.email.change.taken", data={"pending_email": address}
)
raise PendingEmailTakenError from error
with transaction.atomic():
old_email = user.email
user.email = address
user.pending_email = None
user.pending_email_sent = None
user.email_verification_state = EmailVerificationState.VERIFIED
user.save(
update_fields=[
"email",
"pending_email",
"pending_email_sent",
"email_verification_state",
]
)
send_system_mail(
subject=EMAIL_CHANGED_SUBJECT,
text=EMAIL_CHANGED_TEXT,
to=old_email,
locale=user.locale,
# Django's EmailField accepts RFC 5321 quoted local parts, which
# could include e.g. "<script>", so we treat them as untrusted
# and escape them with prejudice.
safe_extra_context={
"old_email": untrusted_plain_value(old_email),
"new_email": untrusted_plain_value(user.email),
},
context_kwargs={"user": user},
)
user.log_action(
"pretalx.user.email.change.confirm",
data={"old_email": old_email, "new_email": user.email},
)
else:
if user.email_verification_state == EmailVerificationState.VERIFIED:
raise AlreadyVerifiedError
with transaction.atomic():
user.email_verification_state = EmailVerificationState.VERIFIED
user.save(update_fields=["email_verification_state"])
user.log_action(
"pretalx.user.email.verification.confirm", data={"email": user.email}
)
return user, kind
correct_unverified_email(user, address, *, event=None, orga=False)source
@transaction.atomic
def correct_unverified_email(user, address, *, event=None, orga=False):
if user.email_verification_state != EmailVerificationState.UNVERIFIED:
raise VerificationError(
"Only unverified accounts may correct their address directly"
)
address = address.lower().strip()
try:
validate_email_unique(address, exclude_user=user)
except ValidationError as error:
raise PendingEmailTakenError from error
old_email = user.email
user.email = address
user.save(update_fields=["email"])
send_verification_mail(user, KIND_VERIFY, event=event, orga=orga, free_pass=True)
user.log_action(
"pretalx.user.email.verification.correct",
data={"old_email": old_email, "new_email": user.email},
)
finalize_registration(user, *, event=None, orga=False, invited_email=None)source
def finalize_registration(user, *, event=None, orga=False, invited_email=None):
if promote_on_invitation_match(user, invited_email):
return
with suppress(SendCooldownError):
send_verification_mail(user, KIND_VERIFY, event=event, orga=orga)
get_verification_url(token, *, event=None, orga=False)source
def get_verification_url(token, *, event=None, orga=False):
if event:
path = "orga:event.auth.verify" if orga else "cfp:event.verify"
kwargs = {"token": token, "event": event.slug}
else:
path = "orga:auth.verify"
kwargs = {"token": token}
return UrlString(build_absolute_uri(path, kwargs=kwargs))
make_verification_token(user, kind)source
def make_verification_token(user, kind):
if kind == KIND_CHANGE and not user.pending_email:
raise ValueError("Cannot make a change token without a pending email address")
return signing.dumps(
{"user": user.code, "kind": kind, "check": _token_check(user, kind)},
salt=VERIFICATION_SALT,
)
parse_verification_token(token)source
def parse_verification_token(token):
try:
payload = signing.loads(
token, salt=VERIFICATION_SALT, max_age=dt.timedelta(hours=24)
)
except signing.SignatureExpired as error:
raise ExpiredVerificationTokenError from error
except signing.BadSignature as error:
raise InvalidVerificationTokenError from error
user = User.objects.filter(code=payload.get("user")).first()
kind = payload.get("kind")
if not user or kind not in (KIND_VERIFY, KIND_CHANGE):
raise InvalidVerificationTokenError
if not constant_time_compare(payload.get("check", ""), _token_check(user, kind)):
raise InvalidVerificationTokenError
return user, kind
pending_email_expired(user)source
def pending_email_expired(user):
return bool(user.pending_email_sent) and (
now() - user.pending_email_sent > dt.timedelta(hours=24)
)
promote_on_invitation_match(user, invited_email)source
def promote_on_invitation_match(user, invited_email):
if not invited_email:
return False
if invited_email.strip().lower() != user.email.lower():
return False
if user.email_verification_state != EmailVerificationState.VERIFIED:
user.email_verification_state = EmailVerificationState.VERIFIED
user.save(update_fields=["email_verification_state"])
user.log_action(
"pretalx.user.email.verification.promote", data={"email": user.email}
)
return True
request_email_change(user, address, *, event=None, orga=False)source
@transaction.atomic
def request_email_change(user, address, *, event=None, orga=False):
address = address.lower().strip()
validate_email_unique(address, exclude_user=user)
user.pending_email = address
user.pending_email_sent = now()
user.save(update_fields=["pending_email", "pending_email_sent"])
send_verification_mail(user, KIND_CHANGE, event=event, orga=orga)
send_system_mail(
subject=EMAIL_CHANGE_REQUESTED_SUBJECT,
text=EMAIL_CHANGE_REQUESTED_TEXT,
to=user.email,
event=event,
locale=user.locale,
safe_extra_context={"pending_email": untrusted_plain_value(address)},
context_kwargs={"user": user},
)
user.log_action(
"pretalx.user.email.change.request", data={"pending_email": address}
)
send_cooldown_remaining(user)source
def send_cooldown_remaining(user):
sent = cache.get(_cooldown_key(user))
if not sent:
return 0
remaining = SEND_COOLDOWN.total_seconds() - (now().timestamp() - sent)
return max(0, math.ceil(remaining))
send_verification_mail(user, kind, *, event=None, orga=False, free_pass=False)source
def send_verification_mail(user, kind, *, event=None, orga=False, free_pass=False):
to = user.pending_email if kind == KIND_CHANGE else user.email
if not to:
raise ValueError("Cannot send a change link without a pending email address")
check_send_cooldown(user, free_pass=free_pass)
token = make_verification_token(user, kind)
send_system_mail(
subject=EMAIL_VERIFICATION_SUBJECT,
text=EMAIL_VERIFICATION_TEXT,
to=to,
event=event,
locale=user.locale,
safe_extra_context={"url": get_verification_url(token, event=event, orga=orga)},
context_kwargs={"user": user},
)
cache.set(
_cooldown_key(user), now().timestamp(), timeout=SEND_COOLDOWN.total_seconds()
)
user.log_action(
"pretalx.user.email.verification.send", data={"email": to, "kind": kind}
)
set_verification_state(user, state, *, actor)source
def set_verification_state(user, state, *, actor):
if state not in (
EmailVerificationState.VERIFIED,
EmailVerificationState.UNVERIFIED,
):
raise ValueError(
"Administrators may only set accounts to verified or unverified"
)
if user.email_verification_state == state:
return
user.email_verification_state = state
user.save(update_fields=["email_verification_state"])
user.log_action(
"pretalx.user.email.verification.set",
person=actor,
orga=True,
data={"state": state},
)