def remove_blacklist(request): blacklist_id = request.POST.get("blacklist_id") if blacklist_id is None: raise HTTPBadRequest("Have a blacklist_id to remove.") try: blacklist = ( request.db.query(BlacklistedProject) .filter(BlacklistedProject.id == blacklist_id) .one() ) except NoResultFound: raise HTTPNotFound from None request.db.delete(blacklist) request.session.flash(f"{blacklist.name!r} unblacklisted", queue="success") redirect_to = request.POST.get("next") # If the user-originating redirection url is not safe, then redirect to # the index instead. if not redirect_to or not is_safe_url(url=redirect_to, host=request.host): redirect_to = request.route_path("admin.blacklist.list") return HTTPSeeOther(redirect_to)
def login(request, redirect_field_name=REDIRECT_FIELD_NAME, _form_class=LoginForm): # TODO: Logging in should reset request.user # TODO: Configure the login view as the default view for not having # permission to view something. if request.authenticated_userid is not None: return HTTPSeeOther(request.route_path("manage.projects")) user_service = request.find_service(IUserService, context=None) breach_service = request.find_service(IPasswordBreachedService, context=None) redirect_to = request.POST.get( redirect_field_name, request.GET.get(redirect_field_name) ) form = _form_class( request.POST, request=request, user_service=user_service, breach_service=breach_service, check_password_metrics_tags=["method:auth", "auth_method:login_form"], ) if request.method == "POST": if form.validate(): # Get the user id for the given username. username = form.username.data userid = user_service.find_userid(username) # If the user-originating redirection url is not safe, then # redirect to the index instead. if not redirect_to or not is_safe_url(url=redirect_to, host=request.host): redirect_to = request.route_path("manage.projects") # Actually perform the login routine for our user. headers = _login_user(request, userid) # Now that we're logged in we'll want to redirect the user to # either where they were trying to go originally, or to the default # view. resp = HTTPSeeOther(redirect_to, headers=dict(headers)) # We'll use this cookie so that client side javascript can # Determine the actual user ID (not username, user ID). This is # *not* a security sensitive context and it *MUST* not be used # where security matters. # # We'll also hash this value just to avoid leaking the actual User # IDs here, even though it really shouldn't matter. resp.set_cookie( USER_ID_INSECURE_COOKIE, hashlib.blake2b(str(userid).encode("ascii"), person=b"warehouse.userid") .hexdigest() .lower(), ) return resp return { "form": form, "redirect": {"field": REDIRECT_FIELD_NAME, "data": redirect_to}, }
def remove_prohibited_project_names(request): prohibited_project_name_id = request.POST.get("prohibited_project_name_id") if prohibited_project_name_id is None: raise HTTPBadRequest("Have a prohibited_project_name_id to remove.") try: prohibited_project_names = ( request.db.query(ProhibitedProjectName) .filter(ProhibitedProjectName.id == prohibited_project_name_id) .one() ) except NoResultFound: raise HTTPNotFound from None request.db.delete(prohibited_project_names) request.session.flash( f"{prohibited_project_names.name!r} unprohibited", queue="success" ) redirect_to = request.POST.get("next") # If the user-originating redirection url is not safe, then redirect to # the index instead. if not redirect_to or not is_safe_url(url=redirect_to, host=request.host): redirect_to = request.route_path("admin.prohibited_project_names.list") return HTTPSeeOther(redirect_to)
def logout(request, redirect_field_name=REDIRECT_FIELD_NAME): # TODO: If already logged out just redirect to ?next= # TODO: Logging out should reset request.user redirect_to = request.POST.get(redirect_field_name, request.GET.get(redirect_field_name)) if request.method == "POST": # A POST to the logout view tells us to logout. There's no form to # validate here becuse there's no data. We should be protected against # CSRF attacks still because of the CSRF framework, so users will still # need a post body that contains the CSRF token. headers = forget(request) # When crossing an authentication boundry we want to create a new # session identifier. We don't want to keep any information in the # session when going from authenticated to unauthenticated because # user's generally expect that logging out is a desctructive action # that erases all of their private data. However if we don't clear the # session then another user can use the computer after them, log in to # their account, and then gain access to anything sensitive stored in # the session for the original user. request.session.invalidate() # If the user-originating redirection url is not safe, then redirect to # the index instead. if (not redirect_to or not is_safe_url(url=redirect_to, host=request.host)): redirect_to = "/" # Now that we're logged out we'll want to redirect the user to either # where they were originally, or to the default view. return HTTPSeeOther(redirect_to, headers=dict(headers)) return {"redirect": {"field": REDIRECT_FIELD_NAME, "data": redirect_to}}
def login(request, redirect_field_name=REDIRECT_FIELD_NAME, _form_class=LoginForm): # TODO: Logging in should reset request.user # TODO: Configure the login view as the default view for not having # permission to view something. login_service = request.find_service(IUserService, context=None) redirect_to = request.POST.get(redirect_field_name, request.GET.get(redirect_field_name)) form = _form_class(request.POST, login_service=login_service) if request.method == "POST" and form.validate(): # Get the user id for the given username. username = form.username.data userid = login_service.find_userid(username) # We have a session factory associated with this request, so in order # to protect against session fixation attacks we're going to make sure # that we create a new session (which for sessions with an identifier # will cause it to get a new session identifier). # We need to protect against session fixation attacks, so make sure # that we create a new session (which will cause it to get a new # session identifier). if request.unauthenticated_userid is not None and request.unauthenticated_userid != userid: # There is already a userid associated with this request and it is # a different userid than the one we're trying to remember now. In # this case we want to drop the existing session completely because # we don't want to leak any data between authenticated userids. request.session.invalidate() else: # We either do not have an associated userid with this request # already, or the userid is the same one we're trying to remember # now. In either case we want to keep all of the data but we want # to make sure that we create a new session since we're crossing # a privilege boundary. data = dict(request.session.items()) request.session.invalidate() request.session.update(data) # Remember the userid using the authentication policy. headers = remember(request, userid) # Cycle the CSRF token since we've crossed an authentication boundary # and we don't want to continue using the old one. request.session.new_csrf_token() # If the user-originating redirection url is not safe, then redirect to # the index instead. if not redirect_to or not is_safe_url(url=redirect_to, host=request.host): redirect_to = "/" # Now that we're logged in we'll want to redirect the user to either # where they were trying to go originally, or to the default view. return HTTPSeeOther(redirect_to, headers=dict(headers)) return {"form": form, "redirect": {"field": REDIRECT_FIELD_NAME, "data": redirect_to}}
def login(request, redirect_field_name=REDIRECT_FIELD_NAME, _form_class=forms.LoginForm): # TODO: Logging in should reset request.user # TODO: Configure the login view as the default view for not having # permission to view something. user_service = request.find_service(IUserService, context=None) redirect_to = request.POST.get(redirect_field_name, request.GET.get(redirect_field_name)) form = _form_class(request.POST, user_service=user_service) if request.method == "POST" and form.validate(): # Get the user id for the given username. username = form.username.data userid = user_service.find_userid(username) # If the user-originating redirection url is not safe, then redirect to # the index instead. if (not redirect_to or not is_safe_url(url=redirect_to, host=request.host)): redirect_to = "/" # Actually perform the login routine for our user. headers = _login_user(request, userid) # Now that we're logged in we'll want to redirect the user to either # where they were trying to go originally, or to the default view. resp = HTTPSeeOther(redirect_to, headers=dict(headers)) # We'll use this cookie so that client side javascript can Determine # the actual user ID (not username, user ID). This is *not* a security # sensitive context and it *MUST* not be used where security matters. # # We'll also hash this value just to avoid leaking the actual User IDs # here, even though it really shouldn't matter. resp.set_cookie( USER_ID_INSECURE_COOKIE, blake2b( str(userid).encode("ascii"), person=b"warehouse.userid", ).hexdigest().lower(), ) return resp return { "form": form, "redirect": { "field": REDIRECT_FIELD_NAME, "data": redirect_to, }, }
def locale(request): form = SetLocaleForm(**request.GET) redirect_to = request.referer if not is_safe_url(redirect_to, host=request.host): redirect_to = request.route_path("index") resp = HTTPSeeOther(redirect_to) if form.validate(): request.session.flash("Locale updated", queue="success") resp.set_cookie(LOCALE_ATTR, form.locale_id.data) return resp
def two_factor(request, _form_class=TwoFactorForm): if request.authenticated_userid is not None: return HTTPSeeOther(request.route_path("manage.projects")) token_service = request.find_service(ITokenService, name="two_factor") try: two_factor_data = token_service.loads(request.query_string) 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") if not userid: return HTTPSeeOther(request.route_path("accounts.login")) redirect_to = two_factor_data.get("redirect_to") user_service = request.find_service(IUserService, context=None) form = _form_class( request.POST, user_id=userid, user_service=user_service, check_password_metrics_tags=["method:auth", "auth_method:login_form"], ) if request.method == "POST": if form.validate(): # If the user-originating redirection url is not safe, then # redirect to the index instead. if not redirect_to or not is_safe_url(url=redirect_to, host=request.host): redirect_to = request.route_path("manage.projects") _login_user(request, userid) 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 {"form": form}
def _get_two_factor_data(request, _redirect_to="/"): token_service = request.find_service(ITokenService, name="two_factor") two_factor_data = token_service.loads(request.query_string) if two_factor_data.get("userid") is None: raise TokenInvalid # If the user-originating redirection url is not safe, then # redirect to the index instead. redirect_to = two_factor_data.get("redirect_to") if redirect_to is None or not is_safe_url(url=redirect_to, host=request.host): two_factor_data["redirect_to"] = _redirect_to return two_factor_data
def delete_macaroon(self): form = DeleteMacaroonForm(**self.request.POST, macaroon_service=self.macaroon_service) if form.validate(): description = self.macaroon_service.find_macaroon( form.macaroon_id.data).description self.macaroon_service.delete_macaroon(form.macaroon_id.data) self.request.session.flash(f"Deleted API token '{description}'.", queue="success") redirect_to = self.request.referer if not is_safe_url(redirect_to, host=self.request.host): redirect_to = self.request.route_path("manage.account") return HTTPSeeOther(redirect_to)
def two_factor(request, _form_class=TwoFactorForm): if request.authenticated_userid is not None: return HTTPSeeOther(request.route_path("manage.projects")) token_service = request.find_service(ITokenService, name="two_factor") try: two_factor_data = token_service.loads(request.query_string) 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") if not userid: return HTTPSeeOther(request.route_path("accounts.login")) redirect_to = two_factor_data.get("redirect_to") user_service = request.find_service(IUserService, context=None) form = _form_class( request.POST, user_id=userid, user_service=user_service, check_password_metrics_tags=["method:auth", "auth_method:login_form"], ) if request.method == "POST": if form.validate(): # If the user-originating redirection url is not safe, then # redirect to the index instead. if not redirect_to or not is_safe_url(url=redirect_to, host=request.host): redirect_to = request.route_path("manage.projects") _login_user(request, userid) 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 return {"form": form}
def logout(request, redirect_field_name=REDIRECT_FIELD_NAME): # TODO: Logging out should reset request.user redirect_to = request.POST.get( redirect_field_name, request.GET.get(redirect_field_name) ) # If the user-originating redirection url is not safe, then redirect to # the index instead. if not redirect_to or not is_safe_url(url=redirect_to, host=request.host): redirect_to = "/" # If we're already logged out, then we'll go ahead and issue our redirect right # away instead of trying to log a non-existent user out. if request.user is None: return HTTPSeeOther(redirect_to) if request.method == "POST": # A POST to the logout view tells us to logout. There's no form to # validate here because there's no data. We should be protected against # CSRF attacks still because of the CSRF framework, so users will still # need a post body that contains the CSRF token. headers = forget(request) # When crossing an authentication boundary we want to create a new # session identifier. We don't want to keep any information in the # session when going from authenticated to unauthenticated because # user's generally expect that logging out is a destructive action # that erases all of their private data. However, if we don't clear the # session then another user can use the computer after them, log in to # their account, and then gain access to anything sensitive stored in # the session for the original user. request.session.invalidate() # Now that we're logged out we'll want to redirect the user to either # where they were originally, or to the default view. resp = HTTPSeeOther(redirect_to, headers=dict(headers)) # Ensure that we delete our user_id__insecure cookie, since the user is # no longer logged in. resp.delete_cookie(USER_ID_INSECURE_COOKIE) return resp return {"redirect": {"field": REDIRECT_FIELD_NAME, "data": redirect_to}}
def delete_macaroon(self): form = DeleteMacaroonForm( password=self.request.POST["confirm_password"], macaroon_id=self.request.POST["macaroon_id"], macaroon_service=self.macaroon_service, username=self.request.user.username, user_service=self.user_service, ) if form.validate(): macaroon = self.macaroon_service.find_macaroon( form.macaroon_id.data) self.macaroon_service.delete_macaroon(form.macaroon_id.data) self.user_service.record_event( self.request.user.id, tag="account:api_token:removed", ip_address=self.request.remote_addr, additional={"macaroon_id": form.macaroon_id.data}, ) if "projects" in macaroon.caveats["permissions"]: projects = [ project for project in self.request.user.projects if project.normalized_name in macaroon.caveats["permissions"]["projects"] ] for project in projects: project.record_event( tag="project:api_token:removed", ip_address=self.request.remote_addr, additional={ "description": macaroon.description, "user": self.request.user.username, }, ) self.request.session.flash( f"Deleted API token '{macaroon.description}'.", queue="success") else: self.request.session.flash("Invalid credentials. Try again", queue="error") redirect_to = self.request.referer if not is_safe_url(redirect_to, host=self.request.host): redirect_to = self.request.route_path("manage.account") return HTTPSeeOther(redirect_to)
def locale(request): form = SetLocaleForm(**request.GET) redirect_to = request.referer if not is_safe_url(redirect_to, host=request.host): redirect_to = request.route_path("index") resp = HTTPSeeOther(redirect_to) if form.validate(): # Build a localizer for the locale we're about to switch to. This will # happen automatically once the cookie is set, but if we want the flash # message indicating success to be in the new language as well, we need # to do it here. tdirs = request.registry.queryUtility(ITranslationDirectories) _ = make_localizer(form.locale_id.data, tdirs).translate request.session.flash(_("Locale updated"), queue="success") resp.set_cookie(LOCALE_ATTR, form.locale_id.data) return resp
def _get_two_factor_data(request, _redirect_to="/"): token_service = request.find_service(ITokenService, name="two_factor") two_factor_data, timestamp = token_service.loads( request.query_string, return_timestamp=True ) if two_factor_data.get("userid") is None: raise TokenInvalid user_service = request.find_service(IUserService, context=None) user = user_service.get_user(two_factor_data.get("userid")) if timestamp < user.last_login: raise TokenInvalid # If the user-originating redirection url is not safe, then # redirect to the index instead. redirect_to = two_factor_data.get("redirect_to") if redirect_to is None or not is_safe_url(url=redirect_to, host=request.host): two_factor_data["redirect_to"] = _redirect_to return two_factor_data
def login(request, redirect_field_name=REDIRECT_FIELD_NAME, _form_class=forms.LoginForm): # TODO: Logging in should reset request.user # TODO: Configure the login view as the default view for not having # permission to view something. user_service = request.find_service(IUserService, context=None) redirect_to = request.POST.get(redirect_field_name, request.GET.get(redirect_field_name)) form = _form_class(request.POST, user_service=user_service) if request.method == "POST" and form.validate(): # Get the user id for the given username. username = form.username.data userid = user_service.find_userid(username) # If the user-originating redirection url is not safe, then redirect to # the index instead. if (not redirect_to or not is_safe_url(url=redirect_to, host=request.host)): redirect_to = "/" # Actually perform the login routine for our user. headers = _login_user(request, userid) # Now that we're logged in we'll want to redirect the user to either # where they were trying to go originally, or to the default view. return HTTPSeeOther(redirect_to, headers=dict(headers)) return { "form": form, "redirect": { "field": REDIRECT_FIELD_NAME, "data": redirect_to, }, }
def login(request, redirect_field_name=REDIRECT_FIELD_NAME, _form_class=LoginForm): # TODO: Logging in should reset request.user # TODO: Configure the login view as the default view for not having # permission to view something. login_service = request.find_service(IUserService, context=None) redirect_to = request.POST.get(redirect_field_name, request.GET.get(redirect_field_name)) form = _form_class(request.POST, login_service=login_service) if request.method == "POST" and form.validate(): # Get the user id for the given username. username = form.username.data userid = login_service.find_userid(username) # We have a session factory associated with this request, so in order # to protect against session fixation attacks we're going to make sure # that we create a new session (which for sessions with an identifier # will cause it to get a new session identifier). # We need to protect against session fixation attacks, so make sure # that we create a new session (which will cause it to get a new # session identifier). if (request.unauthenticated_userid is not None and request.unauthenticated_userid != userid): # There is already a userid associated with this request and it is # a different userid than the one we're trying to remember now. In # this case we want to drop the existing session completely because # we don't want to leak any data between authenticated userids. request.session.invalidate() else: # We either do not have an associated userid with this request # already, or the userid is the same one we're trying to remember # now. In either case we want to keep all of the data but we want # to make sure that we create a new session since we're crossing # a privilege boundary. data = dict(request.session.items()) request.session.invalidate() request.session.update(data) # Remember the userid using the authentication policy. headers = remember(request, userid) # Cycle the CSRF token since we've crossed an authentication boundary # and we don't want to continue using the old one. request.session.new_csrf_token() # If the user-originating redirection url is not safe, then redirect to # the index instead. if (not redirect_to or not is_safe_url(url=redirect_to, host=request.host)): redirect_to = "/" # Now that we're logged in we'll want to redirect the user to either # where they were trying to go originally, or to the default view. return HTTPSeeOther(redirect_to, headers=dict(headers)) return { "form": form, "redirect": { "field": REDIRECT_FIELD_NAME, "data": redirect_to, }, }
def test_rejects_bad_url(self, url): assert not is_safe_url(url, host="testserver")
def test_accepts_good_url(self, url): assert is_safe_url(url, host="testserver")