Ejemplo n.º 1
0
    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."
                )
            )
Ejemplo n.º 2
0
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."
                )
            )
Ejemplo n.º 3
0
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."))
Ejemplo n.º 4
0
    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."))
Ejemplo n.º 5
0
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"))
Ejemplo n.º 6
0
 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
Ejemplo n.º 7
0
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}}
Ejemplo n.º 8
0
    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")
            )
Ejemplo n.º 9
0
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
    )
Ejemplo n.º 10
0
    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")
            )
Ejemplo n.º 11
0
    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}
Ejemplo n.º 12
0
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."))
Ejemplo n.º 13
0
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))
Ejemplo n.º 14
0
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))
Ejemplo n.º 15
0
    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
Ejemplo n.º 16
0
    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."))
Ejemplo n.º 17
0
 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"))
Ejemplo n.º 18
0
    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()
Ejemplo n.º 19
0
class DeleteProviderForm(forms.Form):
    __params__ = ["provider_id"]

    provider_id = wtforms.StringField(
        validators=[
            wtforms.validators.UUID(message=_("Provider must be specified by ID"))
        ]
    )
Ejemplo n.º 20
0
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}
Ejemplo n.º 21
0
 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."
             )
         )
Ejemplo n.º 22
0
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"),
        ],
    )
Ejemplo n.º 23
0
    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."))
Ejemplo n.º 24
0
 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
Ejemplo n.º 25
0
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},
            ),
        ),
    ])
Ejemplo n.º 26
0
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
Ejemplo n.º 27
0
    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."))
Ejemplo n.º 28
0
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
Ejemplo n.º 29
0
    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."))
Ejemplo n.º 30
0
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