def validate_email(self, field): userid = self.user_service.find_userid_by_email(field.data) if userid and userid == self.user_id: raise wtforms.validators.ValidationError( _( "This email address is already being used by this account. " "Use a different email." ) ) if userid: raise wtforms.validators.ValidationError( _( "This email address is already being used " "by another account. Use a different email." ) ) domain = field.data.split("@")[-1] if domain in disposable_email_domains.blacklist: raise wtforms.validators.ValidationError( _( "You can't use an email address from this domain. Use a " "different email." ) )
class NewUsernameMixin: username = wtforms.StringField( validators=[ wtforms.validators.DataRequired(), wtforms.validators.Length( max=50, message=_("Choose a username with 50 characters or less.") ), # the regexp below must match the CheckConstraint # for the username field in accounts.models.User wtforms.validators.Regexp( r"^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$", message=_( "The username is invalid. Usernames " "must be composed of letters, numbers, " "dots, hyphens and underscores. And must " "also start and finish with a letter or number. " "Choose a different username." ), ), ] ) def validate_username(self, field): if self.user_service.find_userid(field.data) is not None: raise wtforms.validators.ValidationError( _( "This username is already being used by another " "account. Choose a different username." ) )
class OrganizationNameMixin: name = wtforms.StringField(validators=[ wtforms.validators.DataRequired( message="Specify organization account name"), wtforms.validators.Length( max=50, message= _("Choose an organization account name with 50 characters or less." ), ), # the regexp below must match the CheckConstraint # for the name field in organizations.model.Organization wtforms.validators.Regexp( r"^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$", message=_("The organization account name is invalid. " "Organization account names " "must be composed of letters, numbers, " "dots, hyphens and underscores. And must " "also start and finish with a letter or number. " "Choose a different organization account name."), ), ]) def validate_name(self, field): if self.organization_service.find_organizationid( field.data) is not None: raise wtforms.validators.ValidationError( _("This organization account name has already been used. " "Choose a different organization account name."))
def validate_email(self, field): # Additional checks for the validity of the address try: Address(addr_spec=field.data) except (ValueError, HeaderParseError): raise wtforms.validators.ValidationError( _("The email address isn't valid. Try again.")) # Check if the domain is valid domain = field.data.split("@")[-1] if domain in disposable_email_domains.blacklist: raise wtforms.validators.ValidationError( _("You can't use an email address from this domain. Use a " "different email.")) # Check if this email address is already in use userid = self.user_service.find_userid_by_email(field.data) if userid and userid == self.user_id: raise wtforms.validators.ValidationError( _("This email address is already being used by this account. " "Use a different email.")) if userid: raise wtforms.validators.ValidationError( _("This email address is already being used " "by another account. Use a different email."))
def verify_email(request): token_service = request.find_service(ITokenService, name="email") def _error(message): request.session.flash(message, queue="error") return HTTPSeeOther(request.route_path("manage.account")) try: token = request.params.get("token") data = token_service.loads(token) except TokenExpired: return _error(_("Expired token: request a new email verification link")) except TokenInvalid: return _error(_("Invalid token: request a new email verification link")) except TokenMissing: return _error(_("Invalid token: no token supplied")) # Check whether this token is being used correctly if data.get("action") != "email-verify": return _error(_("Invalid token: not an email verification token")) try: email = ( request.db.query(Email) .filter(Email.id == data["email.id"], Email.user == request.user) .one() ) except NoResultFound: return _error(_("Email not found")) if email.verified: return _error(_("Email already verified")) email.verified = True email.unverify_reason = None email.transient_bounces = 0 email.user.record_event( tag="account:email:verified", ip_address=request.remote_addr, additional={"email": email.email, "primary": email.primary}, ) if not email.primary: confirm_message = _("You can now set this email as your primary address") else: confirm_message = _("This is your primary address") request.user.is_active = True request.session.flash( _( "Email address ${email_address} verified. ${confirm_message}.", mapping={"email_address": email.email, "confirm_message": confirm_message}, ), queue="success", ) return HTTPSeeOther(request.route_path("manage.account"))
def validate_password(self, field): userid = self.user_service.find_userid(self.username.data) if userid is not None: try: if not self.user_service.check_password( userid, field.data, tags=self._check_password_metrics_tags): raise wtforms.validators.ValidationError( _("The password is invalid. Try again.")) except TooManyFailedLogins: raise wtforms.validators.ValidationError( _("There have been too many unsuccessful login attempts. " "Try again later.")) from None
def webauthn_authentication_validate(request): if request.authenticated_userid is not None: return {"fail": {"errors": ["Already authenticated"]}} try: two_factor_data = _get_two_factor_data(request) except TokenException: request.session.flash(_("Invalid or expired two factor login."), queue="error") return { "fail": { "errors": [_("Invalid or expired two factor login.")] } } redirect_to = two_factor_data.get("redirect_to") userid = two_factor_data.get("userid") user_service = request.find_service(IUserService, context=None) form = WebAuthnAuthenticationForm( **request.POST, user_id=userid, user_service=user_service, challenge=request.session.get_webauthn_challenge(), origin=request.host_url, rp_id=request.domain, ) request.session.clear_webauthn_challenge() if form.validate(): credential_id, sign_count = form.validated_credential webauthn = user_service.get_webauthn_by_credential_id( userid, credential_id) webauthn.sign_count = sign_count _login_user(request, userid, two_factor_method="webauthn") request.response.set_cookie( USER_ID_INSECURE_COOKIE, hashlib.blake2b(str(userid).encode("ascii"), person=b"warehouse.userid").hexdigest().lower(), ) return { "success": _("Successful WebAuthn assertion"), "redirect_to": redirect_to, } errors = [str(error) for error in form.credential.errors] return {"fail": {"errors": errors}}
def validate_workflow_filename(self, field): workflow_filename = field.data if not ( workflow_filename.endswith(".yml") or workflow_filename.endswith(".yaml") ): raise wtforms.validators.ValidationError( _("Workflow name must end with .yml or .yaml") ) if "/" in workflow_filename: raise wtforms.validators.ValidationError( _("Workflow filename must be a filename only, without directories") )
def webauthn_authentication_options(request): if request.authenticated_userid is not None: return {"fail": {"errors": [_("Already authenticated")]}} try: two_factor_data = _get_two_factor_data(request) except TokenException: request.session.flash(_("Invalid or expired two factor login."), queue="error") return {"fail": {"errors": [_("Invalid or expired two factor login.")]}} userid = two_factor_data.get("userid") user_service = request.find_service(IUserService, context=None) return user_service.get_webauthn_assertion_options( userid, challenge=request.session.get_webauthn_challenge(), rp_id=request.domain )
def validate_username(self, field): userid = self.user_service.find_userid(field.data) if userid is None: raise wtforms.validators.ValidationError( _("No user found with that username") )
def add_email(self): form = AddEmailForm( self.request.POST, user_service=self.user_service, user_id=self.request.user.id, ) if form.validate(): email = self.user_service.add_email(self.request.user.id, form.email.data) self.user_service.record_event( self.request.user.id, tag="account:email:add", ip_address=self.request.remote_addr, additional={"email": email.email}, ) send_email_verification_email(self.request, (self.request.user, email)) self.request.session.flash( _( "Email ${email_address} added - check your email for " "a verification link", mapping={"email_address": email.email}, ), queue="success", ) return self.default_response return {**self.default_response, "add_email_form": form}
class NewEmailMixin: email = wtforms.fields.html5.EmailField(validators=[ wtforms.validators.DataRequired(), wtforms.validators.Regexp( r".+@.+\..+", message=_("The email address isn't valid. Try again.")), ]) def validate_email(self, field): userid = self.user_service.find_userid_by_email(field.data) if userid and userid == self.user_id: raise wtforms.validators.ValidationError( _("This email address is already being used by this account. " "Use a different email.")) if userid: raise wtforms.validators.ValidationError( _("This email address is already being used " "by another account. Use a different email.")) domain = field.data.split("@")[-1] if domain in disposable_email_domains.blacklist: raise wtforms.validators.ValidationError( _("You can't use an email address from this domain. Use a " "different email."))
class SaveAccountForm(forms.Form): __params__ = ["name", "public_email"] name = wtforms.StringField(validators=[ wtforms.validators.Length( max=100, message=_("The name is too long. " "Choose a name with 100 characters or less."), ) ]) public_email = wtforms.SelectField(choices=[("", "Not displayed")]) def __init__(self, *args, user_service, user_id, **kwargs): super().__init__(*args, **kwargs) self.user_service = user_service self.user_id = user_id user = user_service.get_user(user_id) self.public_email.choices.extend([(e.email, e.email) for e in user.emails if e.verified]) def validate_public_email(self, field): if field.data: user = self.user_service.get_user(self.user_id) verified_emails = [e.email for e in user.emails if e.verified] if field.data not in verified_emails: raise wtforms.validators.ValidationError( "%s is not a verified email for %s" % (field.data, user.username))
class NewPasswordMixin: new_password = wtforms.PasswordField(validators=[ wtforms.validators.DataRequired(), forms.PasswordStrengthValidator( user_input_fields=["full_name", "username", "email"]), ]) password_confirm = wtforms.PasswordField(validators=[ wtforms.validators.DataRequired(), wtforms.validators.EqualTo( "new_password", message=_("Your passwords don't match. Try again.")), ]) # These fields are here to provide the various user-defined fields to the # PasswordStrengthValidator of the new_password field, to ensure that the # newly set password doesn't contain any of them full_name = wtforms.StringField() # May be empty username = wtforms.StringField( validators=[wtforms.validators.DataRequired()]) email = wtforms.StringField(validators=[wtforms.validators.DataRequired()]) def __init__(self, *args, breach_service, **kwargs): super().__init__(*args, **kwargs) self._breach_service = breach_service def validate_new_password(self, field): if self._breach_service.check_password(field.data, tags=["method:new_password"]): raise wtforms.validators.ValidationError( markupsafe.Markup(self._breach_service.failure_message))
def validate_credential(self, field): try: json.loads(field.data.encode("utf8")) except json.JSONDecodeError: raise wtforms.validators.ValidationError( _("Invalid WebAuthn assertion: Bad payload")) try: validated_credential = self.user_service.verify_webauthn_assertion( self.user_id, field.data.encode("utf8"), challenge=self.challenge, origin=self.origin, rp_id=self.rp_id, ) except webauthn.AuthenticationRejectedError as e: self.user_service.record_event( self.user_id, tag="account:login:failure", additional={"reason": "invalid_webauthn"}, ) raise wtforms.validators.ValidationError(str(e)) self.validated_credential = validated_credential
def validate_recovery_code_value(self, field): recovery_code_value = field.data.encode("utf-8") if not self.user_service.check_recovery_code(self.user_id, recovery_code_value): raise wtforms.validators.ValidationError( _("Invalid recovery code."))
def validate_username_or_email(self, field): username_or_email = self.user_service.get_user_by_username(field.data) if username_or_email is None: username_or_email = self.user_service.get_user_by_email(field.data) if username_or_email is None: raise wtforms.validators.ValidationError( _("No user found with that username or email"))
def _lookup_owner(self, owner): # To actually validate the owner, we ask GitHub's API about them. # We can't do this for the repository, since it might be private. try: response = requests.get( f"https://api.github.com/users/{owner}", headers={ "Accept": "application/vnd.github.v3+json", **self._headers_auth(), }, allow_redirects=True, ) response.raise_for_status() except requests.HTTPError: if response.status_code == 404: raise wtforms.validators.ValidationError( _("Unknown GitHub user or organization.") ) if response.status_code == 403: # GitHub's API uses 403 to signal rate limiting, and returns a JSON # blob explaining the reason. sentry_sdk.capture_message( "Exceeded GitHub rate limit for user lookups. " f"Reason: {response.json()}" ) raise wtforms.validators.ValidationError( _( "GitHub has rate-limited this action. " "Try again in a few minutes." ) ) else: sentry_sdk.capture_message( f"Unexpected error from GitHub user lookup: {response.content=}" ) raise wtforms.validators.ValidationError( _("Unexpected error from GitHub. Try again.") ) except requests.Timeout: sentry_sdk.capture_message( "Timeout from GitHub user lookup API (possibly offline)" ) raise wtforms.validators.ValidationError( _("Unexpected timeout from GitHub. Try again in a few minutes.") ) return response.json()
class DeleteProviderForm(forms.Form): __params__ = ["provider_id"] provider_id = wtforms.StringField( validators=[ wtforms.validators.UUID(message=_("Provider must be specified by ID")) ] )
def recovery_code(request, _form_class=RecoveryCodeAuthenticationForm): if request.authenticated_userid is not None: return HTTPSeeOther(request.route_path("manage.projects")) try: two_factor_data = _get_two_factor_data(request) except TokenException: request.session.flash(_("Invalid or expired two factor login."), queue="error") return HTTPSeeOther(request.route_path("accounts.login")) userid = two_factor_data.get("userid") user_service = request.find_service(IUserService, context=None) form = _form_class(request.POST, user_id=userid, user_service=user_service) if request.method == "POST": if form.validate(): _login_user(request, userid, two_factor_method="recovery-code") resp = HTTPSeeOther(request.route_path("manage.account")) resp.set_cookie( USER_ID_INSECURE_COOKIE, hashlib.blake2b( str(userid).encode("ascii"), person=b"warehouse.userid").hexdigest().lower(), ) user_service.record_event( userid, tag="account:recovery_codes:used", ip_address=request.remote_addr, ) request.session.flash( _("Recovery code accepted. The supplied code cannot be used again." ), queue="success", ) return resp else: form.recovery_code_value.data = "" return {"form": form}
def validate_username(self, field): if self.user_service.find_userid(field.data) is not None: raise wtforms.validators.ValidationError( _( "This username is already being used by another " "account. Choose a different username." ) )
class SaveOrganizationForm(forms.Form): __params__ = ["display_name", "link_url", "description", "orgtype"] display_name = wtforms.StringField(validators=[ wtforms.validators.DataRequired( message="Specify your organization name"), wtforms.validators.Length( max=100, message=_( "The organization name is too long. " "Choose a organization name with 100 characters or less."), ), ]) link_url = wtforms.URLField(validators=[ wtforms.validators.DataRequired( message="Specify your organization URL"), wtforms.validators.Length( max=400, message=_( "The organization URL is too long. " "Choose a organization URL with 400 characters or less."), ), ]) description = wtforms.TextAreaField(validators=[ wtforms.validators.DataRequired( message="Specify your organization description"), wtforms.validators.Length( max=400, message=_( "The organization description is too long. " "Choose a organization description with 400 characters or less." ), ), ]) orgtype = wtforms.SelectField( # TODO: Map additional choices to "Company" and "Community". choices=[("Company", "Company"), ("Community", "Community")], coerce=OrganizationType, validators=[ wtforms.validators.DataRequired( message="Select organization type"), ], )
def validate_totp_value(self, field): totp_value = field.data.replace(" ", "").encode("utf8") if not self.user_service.check_totp_value(self.user_id, totp_value): self.user_service.record_event( self.user_id, tag="account:login:failure", additional={"reason": "invalid_totp"}, ) raise wtforms.validators.ValidationError(_("Invalid TOTP code."))
def validate_password(self, field): userid = self.user_service.find_userid(self.username.data) if userid is not None: try: if not self.user_service.check_password( userid, field.data, tags=self._check_password_metrics_tags, ): self.user_service.record_event( userid, tag=f"account:{self.action}:failure", additional={"reason": "invalid_password"}, ) raise wtforms.validators.ValidationError( _("The password is invalid. Try again.")) except TooManyFailedLogins: raise wtforms.validators.ValidationError( _("There have been too many unsuccessful login attempts. " "Try again later.")) from None
class TOTPValueMixin: totp_value = wtforms.StringField(validators=[ wtforms.validators.DataRequired(), wtforms.validators.Regexp( rf"^ *([0-9] *){{{TOTP_LENGTH}}}$", message=_( "TOTP code must be ${totp_length} digits.", mapping={"totp_length": TOTP_LENGTH}, ), ), ])
def failed_logins(exc, request): resp = HTTPTooManyRequests( _("There have been too many unsuccessful login attempts. Try again later."), retry_after=exc.resets_in.total_seconds(), ) # TODO: This is kind of gross, but we need it for as long as the legacy # upload API exists and is supported. Once we get rid of that we can # get rid of this as well. resp.status = "{} {}".format(resp.status_code, "Too Many Failed Login Attempts") return resp
def validate_recovery_code_value(self, field): recovery_code_value = field.data.encode("utf-8") if not self.user_service.check_recovery_code( self.user_id, recovery_code_value, self.request.remote_addr): self.user_service.record_event( self.user_id, tag="account:login:failure", ip_address=self.request.remote_addr, additional={"reason": "invalid_recovery_code"}, ) raise wtforms.validators.ValidationError( _("Invalid recovery code."))
def two_factor_and_totp_validate(request, _form_class=TOTPAuthenticationForm): if request.authenticated_userid is not None: return HTTPSeeOther(request.route_path("manage.projects")) try: two_factor_data = _get_two_factor_data(request) except TokenException: request.session.flash(_("Invalid or expired two factor login."), queue="error") return HTTPSeeOther(request.route_path("accounts.login")) userid = two_factor_data.get("userid") redirect_to = two_factor_data.get("redirect_to") user_service = request.find_service(IUserService, context=None) two_factor_state = {} if user_service.has_totp(userid): two_factor_state["totp_form"] = _form_class( request.POST, user_id=userid, user_service=user_service, check_password_metrics_tags=[ "method:auth", "auth_method:login_form" ], ) if user_service.has_webauthn(userid): two_factor_state["has_webauthn"] = True if user_service.has_recovery_codes(userid): two_factor_state["has_recovery_codes"] = True if request.method == "POST": form = two_factor_state["totp_form"] if form.validate(): _login_user(request, userid, two_factor_method="totp") user_service.update_user(userid, last_totp_value=form.totp_value.data) resp = HTTPSeeOther(redirect_to) resp.set_cookie( USER_ID_INSECURE_COOKIE, hashlib.blake2b( str(userid).encode("ascii"), person=b"warehouse.userid").hexdigest().lower(), ) return resp else: form.totp_value.data = "" return two_factor_state
def validate_recovery_code_value(self, field): recovery_code_value = field.data.encode("utf-8") try: self.user_service.check_recovery_code(self.user_id, recovery_code_value) send_recovery_code_used_email( self.request, self.user_service.get_user(self.user_id)) except (InvalidRecoveryCode, NoRecoveryCodes): self.user_service.record_event( self.user_id, tag="account:login:failure", additional={"reason": "invalid_recovery_code"}, ) raise wtforms.validators.ValidationError( _("Invalid recovery code.")) except BurnedRecoveryCode: self.user_service.record_event( self.user_id, tag="account:login:failure", additional={"reason": "burned_recovery_code"}, ) raise wtforms.validators.ValidationError( _("Recovery code has been previously used."))
class RegistrationForm(NewUsernameMixin, NewEmailMixin, NewPasswordMixin, HoneypotMixin, forms.Form): full_name = wtforms.StringField(validators=[ wtforms.validators.Length( max=100, message=_("The name is too long. " "Choose a name with 100 characters or less."), ) ]) def __init__(self, *args, user_service, **kwargs): super().__init__(*args, **kwargs) self.user_service = user_service self.user_id = None