mirror of
https://github.com/peeringdb/peeringdb.git
synced 2024-05-11 05:55:09 +00:00
June updates (#751)
* Add pointer from API docs to tutorial #650 * Sorting by clicking table headers should use local-compare #356 * Mark IXP peering LAN as bogon #352 * Add help text to "Add (Facility, Network, Exchange)" tab #669 * Add Looking Glass field to the IX object #672 * Add read-only Superuser #679 * Make spelling of traffic levels consistent #519 (#723) * Offer 2FA (#290) * Show "Last Updated" fields on fac, ix, org records (#526) * Enable sort and reverse sort of IP column in IX display (#72) * IRR validation not handling unexpected characters gracefully (#712) * Support alternative direction of writing, e.g. Arabic (#618) * Undeleting an ixlan with an emtpy IPv4 XOR IPv6 field throws a silly error (#644) * Changing org while adding net results in 500 #654 * missing delete button for organisations (#121) * When changing owner of an ix admin GUI borks because of "Ixlan for exchange already exists" #666 * Selection should only present undeleted objects (#664) * change default encoding of API calls to 'utf-8' #663 * Posting https://www.peeringdb.com onto social media doesn't select a good preview image #537 * Revert "Add Looking Glass field to the IX object #672" This reverts commit 4daf2520043c241fabe9a521757efa86a274e28a. Conflicts: peeringdb_server/migrations/0037_ix_looking_glass.py peeringdb_server/views.py * 500 Internal Error when creating IX where prefix already exists elsewhere #718 * Fix graceful restore of soft-deleted objects with translation active (#580) * Don't return any POC data with status=deleted #569 Hard delete soft-deleted pocs after grace period #566 * django-peeringdb from github@2.0.0.2-beta Co-authored-by: Stefan Pratter <stefan@20c.com>
This commit is contained in:
@@ -22,6 +22,8 @@ from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.urls import resolve, reverse, Resolver404
|
||||
from django.template import loader
|
||||
from django.utils import translation
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.crypto import constant_time_compare
|
||||
from django_namespace_perms.util import (
|
||||
get_perms,
|
||||
@@ -39,6 +41,9 @@ import requests
|
||||
from oauth2_provider.decorators import protected_resource
|
||||
from oauth2_provider.oauth2_backends import get_oauthlib_core
|
||||
|
||||
from django_otp.plugins.otp_email.models import EmailDevice
|
||||
import two_factor.views
|
||||
|
||||
from peeringdb_server import settings
|
||||
from peeringdb_server.search import search
|
||||
from peeringdb_server.stats import stats as global_stats
|
||||
@@ -85,10 +90,6 @@ from ratelimit.decorators import ratelimit, is_ratelimited
|
||||
|
||||
RATELIMITS = dj_settings.RATELIMITS
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# lazy init for translations
|
||||
# _ = lambda s: s
|
||||
|
||||
BASE_ENV = {
|
||||
"RECAPTCHA_PUBLIC_KEY": dj_settings.RECAPTCHA_PUBLIC_KEY,
|
||||
@@ -333,6 +334,7 @@ def cancel_affiliation_request(request, uoar_id):
|
||||
|
||||
@csrf_protect
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
@ratelimit(key="ip", method="POST", rate=RATELIMITS["view_affiliate_to_org_POST"])
|
||||
def view_affiliate_to_org(request):
|
||||
"""
|
||||
@@ -340,9 +342,6 @@ def view_affiliate_to_org(request):
|
||||
an ASN they provide
|
||||
"""
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return view_login(request)
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
# check if request was blocked by rate limiting
|
||||
@@ -413,6 +412,7 @@ def view_affiliate_to_org(request):
|
||||
|
||||
@csrf_protect
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
@ratelimit(key="ip", rate=RATELIMITS["resend_confirmation_mail"])
|
||||
def resend_confirmation_mail(request):
|
||||
was_limited = getattr(request, "limited", False)
|
||||
@@ -426,9 +426,6 @@ def resend_confirmation_mail(request):
|
||||
],
|
||||
)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return view_login(request)
|
||||
|
||||
request.user.send_email_confirmation(request=request)
|
||||
return view_index(request, errors=[_("We have resent your confirmation email")])
|
||||
|
||||
@@ -441,11 +438,9 @@ def view_profile(request):
|
||||
|
||||
@csrf_protect
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def view_set_user_locale(request):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return view_login(request)
|
||||
|
||||
if request.method in ["GET", "HEAD"]:
|
||||
return view_verify(request)
|
||||
elif request.method == "POST":
|
||||
@@ -457,8 +452,6 @@ def view_set_user_locale(request):
|
||||
loc = form.cleaned_data.get("locale")
|
||||
request.user.set_locale(loc)
|
||||
|
||||
from django.utils import translation
|
||||
|
||||
translation.activate(loc)
|
||||
request.session[translation.LANGUAGE_SESSION_KEY] = loc
|
||||
|
||||
@@ -467,8 +460,6 @@ def view_set_user_locale(request):
|
||||
|
||||
@protected_resource(scopes=["profile"])
|
||||
def view_profile_v1(request):
|
||||
# if not request.user.is_authenticated:
|
||||
# return view_login(request)
|
||||
oauth = get_oauthlib_core()
|
||||
scope_email, _request = oauth.verify_request(request, scopes=["email"])
|
||||
scope_networks, _request = oauth.verify_request(request, scopes=["networks"])
|
||||
@@ -483,7 +474,7 @@ def view_profile_v1(request):
|
||||
given_name=request.user.first_name,
|
||||
family_name=request.user.last_name,
|
||||
name=request.user.full_name,
|
||||
verified_user=user.is_verified,
|
||||
verified_user=user.is_verified_user,
|
||||
)
|
||||
|
||||
# only add email fields if email scope is present
|
||||
@@ -507,12 +498,10 @@ def view_profile_v1(request):
|
||||
|
||||
@csrf_protect
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
@ratelimit(key="ip", rate=RATELIMITS["view_verify_POST"], method="POST")
|
||||
def view_verify(request):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return view_login(request)
|
||||
|
||||
if request.method in ["GET", "HEAD"]:
|
||||
template = loader.get_template("site/verify.html")
|
||||
env = BASE_ENV.copy()
|
||||
@@ -568,11 +557,9 @@ def view_verify(request):
|
||||
|
||||
@csrf_protect
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def view_password_change(request):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return view_login(request)
|
||||
|
||||
if request.method in ["GET", "HEAD"]:
|
||||
return view_verify(request)
|
||||
elif request.method == "POST":
|
||||
@@ -826,27 +813,6 @@ def view_registration(request):
|
||||
return JsonResponse({"status": "ok"})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def view_login(request, errors=None):
|
||||
"""
|
||||
login page view
|
||||
"""
|
||||
if not errors:
|
||||
errors = []
|
||||
|
||||
if request.user.is_authenticated:
|
||||
return view_index(request, errors=[_("Already logged in")])
|
||||
|
||||
template = loader.get_template("site/login.html")
|
||||
|
||||
redir = request.GET.get("next", request.POST.get("next"))
|
||||
|
||||
env = BASE_ENV.copy()
|
||||
env.update({"errors": errors, "next": redir})
|
||||
update_env_beta_sync_dt(env)
|
||||
return HttpResponse(template.render(env, request))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def view_index(request, errors=None):
|
||||
"""
|
||||
@@ -1006,6 +972,12 @@ def view_organization(request, id):
|
||||
"notify_incomplete": True,
|
||||
"value": data.get("country", dismiss),
|
||||
},
|
||||
{
|
||||
"readonly": True,
|
||||
"name": "updated",
|
||||
"label": _("Last Updated"),
|
||||
"value": data.get("updated", dismiss),
|
||||
},
|
||||
{
|
||||
"name": "notes",
|
||||
"label": _("Notes"),
|
||||
@@ -1150,6 +1122,12 @@ def view_facility(request, id):
|
||||
"label": _("NPA-NXX"),
|
||||
"value": data.get("npanxx", dismiss),
|
||||
},
|
||||
{
|
||||
"readonly": True,
|
||||
"name": "updated",
|
||||
"label": _("Last Updated"),
|
||||
"value": data.get("updated", dismiss),
|
||||
},
|
||||
{
|
||||
"name": "notes",
|
||||
"label": _("Notes"),
|
||||
@@ -1181,7 +1159,6 @@ def view_facility(request, id):
|
||||
"label": _("Sales Phone"),
|
||||
"value": data.get("sales_phone", dismiss),
|
||||
},
|
||||
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1287,6 +1264,12 @@ def view_exchange(request, id):
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"readonly": True,
|
||||
"name": "updated",
|
||||
"label": _("Last Updated"),
|
||||
"value": data.get("updated", dismiss),
|
||||
},
|
||||
{
|
||||
"name": "notes",
|
||||
"label": _("Notes"),
|
||||
@@ -1669,7 +1652,7 @@ def view_network(request, id):
|
||||
# Add POC data to dataset
|
||||
data["poc_set"] = network_d.get("poc_set")
|
||||
|
||||
if not request.user.is_authenticated or not request.user.is_verified:
|
||||
if not request.user.is_authenticated or not request.user.is_verified_user:
|
||||
cnt = network.poc_set.filter(status="ok", visible="Users").count()
|
||||
data["poc_hidden"] = cnt > 0
|
||||
else:
|
||||
@@ -1856,52 +1839,198 @@ def request_search(request):
|
||||
|
||||
def request_logout(request):
|
||||
logout(request)
|
||||
return view_index(request)
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@csrf_protect
|
||||
@ensure_csrf_cookie
|
||||
@ratelimit(key="ip", rate=RATELIMITS["request_login_POST"], method="POST")
|
||||
def request_login(request):
|
||||
|
||||
if request.user.is_authenticated:
|
||||
return view_index(request)
|
||||
# We are using django-otp's EmailDevice model
|
||||
# to handle email as a recovery option for one
|
||||
# time passwords.
|
||||
#
|
||||
# Unlike all the other devices supported by
|
||||
# django-two-factor-auth it's token field is
|
||||
# not an integer field. So the token to be verified
|
||||
# needs to be turned into a string
|
||||
#
|
||||
# So we monkey patch it's verify_token function
|
||||
# to do just that
|
||||
|
||||
if request.method in ["GET", "HEAD"]:
|
||||
return view_login(request)
|
||||
EmailDevice._verify_token = EmailDevice.verify_token
|
||||
def verify_token(self, token):
|
||||
return self._verify_token(str(token))
|
||||
EmailDevice.verify_token = verify_token
|
||||
|
||||
was_limited = getattr(request, "limited", False)
|
||||
if was_limited:
|
||||
return view_login(
|
||||
request, errors=[_("Please wait a bit before trying to login again.")]
|
||||
)
|
||||
|
||||
username = request.POST["username"]
|
||||
password = request.POST["password"]
|
||||
redir = request.POST.get("next") or "/"
|
||||
if redir == "/logout":
|
||||
redir = "/"
|
||||
class LoginView(two_factor.views.LoginView):
|
||||
|
||||
try:
|
||||
resolve(redir)
|
||||
except Resolver404:
|
||||
if not is_oauth_authorize(redir):
|
||||
"""
|
||||
We extend the `LoginView` class provided
|
||||
by `two_factor` because we need to add some
|
||||
pdb specific functionality and checks
|
||||
"""
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
|
||||
"""
|
||||
If a user is already authenticated, don't show the
|
||||
login process, instead redirect to /
|
||||
"""
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
return redirect("/")
|
||||
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
@ratelimit(key="ip", rate=RATELIMITS["request_login_POST"], method="POST")
|
||||
def post(self, *args, **kwargs):
|
||||
|
||||
"""
|
||||
Posts to the `auth` step of the authentication
|
||||
process need to be rate limited
|
||||
"""
|
||||
|
||||
was_limited = getattr(self.request, "limited", False)
|
||||
if self.get_step_index() == 0 and was_limited:
|
||||
self.rate_limit_message = _("Please wait a bit before trying to login again.")
|
||||
return self.render_goto_step("auth")
|
||||
|
||||
return super().post(*args, **kwargs)
|
||||
|
||||
def get_context_data(self, form, **kwargs):
|
||||
|
||||
"""
|
||||
If post request was rate limited the rate limit message
|
||||
needs to be communicated via the template context
|
||||
"""
|
||||
|
||||
context = super().get_context_data(form, **kwargs)
|
||||
context.update(rate_limit_message=getattr(self, "rate_limit_message", None))
|
||||
|
||||
if "other_devices" in context:
|
||||
context["other_devices"] += [
|
||||
self.get_email_device()
|
||||
]
|
||||
|
||||
return context
|
||||
|
||||
def get_email_device(self):
|
||||
|
||||
"""
|
||||
Returns an EmailDevice instance for the requesting user
|
||||
which can be used for one time passwords.
|
||||
"""
|
||||
|
||||
user = self.get_user()
|
||||
|
||||
if user.email_confirmed:
|
||||
|
||||
# only users with confirmed emails should have
|
||||
# the option to request otp to their email address
|
||||
|
||||
try:
|
||||
|
||||
# check if user already has an EmailDevice instance
|
||||
|
||||
device = EmailDevice.objects.get(user=user)
|
||||
|
||||
if not device.confirmed:
|
||||
|
||||
# sync confirmed status
|
||||
|
||||
device.confirmed = True
|
||||
device.save()
|
||||
except EmailDevice.DoesNotExist:
|
||||
|
||||
# create EmaiLDevice object for user if it does
|
||||
# not exist
|
||||
|
||||
device = EmailDevice.objects.create(user=user, confirmed=True)
|
||||
|
||||
# django-two-factor-auth needs this property set to something
|
||||
device.method = "email"
|
||||
|
||||
return device
|
||||
else:
|
||||
|
||||
# if user does NOT have a confirmed email address but
|
||||
# somehow has an EmailDevice object, delete it.
|
||||
|
||||
try:
|
||||
device = EmailDevice.objects.get(user=user)
|
||||
device.delete()
|
||||
except EmailDevice.DoesNotExist:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_device(self, step=None):
|
||||
|
||||
"""
|
||||
We override this so we can enable EmailDevice as a
|
||||
challenge device for one time passwords
|
||||
"""
|
||||
|
||||
if not self.device_cache:
|
||||
challenge_device_id = self.request.POST.get('challenge_device', None)
|
||||
if challenge_device_id:
|
||||
device = self.get_email_device()
|
||||
if device.persistent_id == challenge_device_id:
|
||||
self.device_cache = device
|
||||
return self.device_cache
|
||||
|
||||
return super().get_device(step=step)
|
||||
|
||||
def get_success_url(self):
|
||||
return self.get_redirect_url()
|
||||
|
||||
def get_redirect_url(self):
|
||||
|
||||
"""
|
||||
Specifies which redirect urls are valid
|
||||
"""
|
||||
|
||||
redir = self.request.POST.get("next") or "/"
|
||||
|
||||
# if the redirect url is to logout that makes little
|
||||
# sense as the user would get logged out immediately
|
||||
# after logging in, substitute with a redirect to `/` instead
|
||||
|
||||
if redir == "/logout":
|
||||
redir = "/"
|
||||
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is not None:
|
||||
if user.is_active:
|
||||
login(request, user)
|
||||
# check if the redirect url can be resolved to a view
|
||||
# if yes, it's a valid redirect
|
||||
|
||||
from django.utils import translation
|
||||
try:
|
||||
resolve(redir)
|
||||
except Resolver404:
|
||||
|
||||
user_language = user.get_locale()
|
||||
translation.activate(user_language)
|
||||
request.session[translation.LANGUAGE_SESSION_KEY] = user_language
|
||||
# url could not be resolved to a view, so it's likely
|
||||
# invalid or pointing somewhere externally, the only
|
||||
# external urls we want to allow are the redirect urls
|
||||
# of oauth applications set up in peeringdb
|
||||
|
||||
if not is_oauth_authorize(redir):
|
||||
redir = "/"
|
||||
|
||||
return redir
|
||||
|
||||
def done(self, form_list, **kwargs):
|
||||
|
||||
"""
|
||||
User authenticated succesfully, set language options
|
||||
"""
|
||||
|
||||
response = super().done(form_list, **kwargs)
|
||||
|
||||
# TODO: do this via signal instead?
|
||||
|
||||
user_language = self.get_user().get_locale()
|
||||
translation.activate(user_language)
|
||||
self.request.session[translation.LANGUAGE_SESSION_KEY] = user_language
|
||||
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
return HttpResponseRedirect(redir)
|
||||
return view_login(request, errors=[_("Account disabled.")])
|
||||
return view_login(request, errors=[_("Invalid username/password.")])
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
|
Reference in New Issue
Block a user