1
0
mirror of https://github.com/peeringdb/peeringdb.git synced 2024-05-11 05:55:09 +00:00
Files
peeringdb-peeringdb/peeringdb_server/signals.py
Matt Griswold b07baf3092 Support 202011 (#917)
* install django-grainy

* nsp to grainy first iteration

* Fix validation error message overflow

* Add migration, update views.py and template to add help_text to UI

* nsp to grainy second iteration

* grainy and django-grainy pinned to latest releases

* deskpro ticket cc (#875)

* black formatting

* move ac link to bottom for ticket body

* Fix typo

* Update djangorestframework, peeringdb, django-ratelimit

* Rewrite login view ratelimit decorator

* Relock pipfile

* add list() to make copy of dictionaries before iterating

* respect ix-f url visibilty in ix-f conflict emails

* Add type coercion to settings taken from environment variables

* Add bool handling

* relock pipfile with python3.9
change docker to use python3.9

* Check bool via isinstance

* add ordering to admin search queryset for deskproticket and email

* update settings with envvar_type option

* Add tooltips to add ix and add exchange views (in org)

* Add tooltip to suggest fac view

* get phone information in view

* add missing migration

* add migration and make org a geo model

* Wire normalization to put/create requests for Facility

* Update admin with new address fields

* Refactor serializer using mixin

* Add floor and suite to address API

* Write command to geonormalize existing entries

* Remove unnecessary method from model

* Add floor and suite to views

* Add ignore geo status

* Force refresh for fac and org updates

* adjust frontend typo

* add checking if update needs geosync

* redo error handling for geosync

* remove save keyword from geonormalize command script

* change raw_id_fields

* alternate autocomplete lookup field depending on where inline is called

* remove unnecessary error handling

* Add  csv option

* Fix bug
 with None vs empty string

* add regex parsing for suite and floor conversion

* Add migration that removes geo error as a field

* add geostatus update to command

* Ignore suite floor and address2 changes for api normalization

* update geomodel by removing geo_error

* Black models.py

* Black serializers.py

* remove geocode error from admin

* Add function for reversing pretty speed

* add conversion to export method

* fix typo

* fix speed value feedback after submit

* remove conditional

* Add error handling to create endpoint

* Refine floor and suite parsing regex

* Add geocoding tests

* Add json for tests

* IX-F Importer: Bogus output of "Preview" tool #896

* remove cruft

* black formatting

* IX-F Importer: history of changes per ixlan & netixlan #893

* 6 add geocode to org view

* 4 update geocode without refresh

* Update error display

* Fix bug with formatting translated string

* Add DateTimeFields to model

* Add update signals

* add last updated fields to views and serializers

* Add last updated model migration

* Add the data migration for last updated fields

* add test that tests a normal org user with create org permissions

* grainy to 1.7
django grainy to 1.9.1

* Fix formatting issues

* Adjust var names

* Refactor signals

* Temporary: save override from network model

* Empty vlan lists no longer cause error

* typo in ixf.py

* typo in admin

* Typos in model verbose names

* Add serializer IXLAN validation for ixf_ixp_import_enabled

* Add model validation to IXLan

* relock pipfile

* relock pipfile

* begin signal test file

* Remove full clean from save in ixlan

* use post_reversion_commit signal instead

* remove redundant save override

* remove cruft / debug code

* Add signal tests

* exclude organizations with city missing from commandline geosync

* Skip geosync if the only address information we have is a country

* initial commit for vlan matcher in importer

* Add more tests and remove unused imports

* update tests

* Actually add vlan matching to importer

* Add type checking for speed list and state

* Change how we register connection.state

* add bootstrap options

* add rdap cache command

* remove outdated perm docs

* rdap from master and relock

* propagate rdap settings to peeringdb.settings

* add loaddata for initial fixtures

* user friendly error message on RdapNotFound errors (#497)

* update rdap errors

* django-peeringdb to 2.5.0 and relock

* rdap to 1.2.0 and relock

* fix migration hierarchy

* add ignore_recurse_errors option

* add missing fields to mock
remove cruft missed during merge

* rdap to 1.2.1

* dont geo validate during api tests

* fix tests

* Add test file

* fix merge

* RDAP_SELF_BOOTSTRAP to False while running tests

* black formatted

* run black

* add github actions

* add runs on

Co-authored-by: Stefan Pratter <stefan@20c.com>
Co-authored-by: Elliot Frank <elliot@20c.com>
2021-01-13 14:35:07 -06:00

490 lines
16 KiB
Python

from grainy.const import *
from datetime import datetime, timezone
import django.urls
from django.db.models.signals import post_save, pre_delete, pre_save
from django.contrib.contenttypes.models import ContentType
from django_grainy.models import Group, GroupPermission
from django.template import loader
from django.conf import settings
from django.dispatch import receiver
import reversion
from allauth.account.signals import user_signed_up
from corsheaders.signals import check_request_enabled
from django_peeringdb.models.abstract import AddressModel
from peeringdb_server.inet import RdapLookup, RdapException
from peeringdb_server.deskpro import (
ticket_queue,
ticket_queue_asnauto_affil,
ticket_queue_asnauto_create,
)
from peeringdb_server.models import (
QUEUE_ENABLED,
QUEUE_NOTIFY,
UserOrgAffiliationRequest,
is_suggested,
VerificationQueueItem,
Organization,
InternetExchange,
Facility,
Network,
NetworkContact,
NetworkIXLan,
NetworkFacility,
)
from peeringdb_server.util import PERM_CRUD
import peeringdb_server.settings as pdb_settings
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import override
def disable_auto_now_and_save(entity):
updated_field = entity._meta.get_field("updated")
updated_field.auto_now = False
entity.save()
updated_field.auto_now = True
def update_network_attribute(instance, attribute):
"""Updates 'attribute' field in Network whenever it's called."""
if getattr(instance, "id"):
network = instance.network
setattr(network, attribute, datetime.now(timezone.utc))
disable_auto_now_and_save(network)
def network_post_revision_commit(**kwargs):
for vs in kwargs.get("versions"):
if vs.object.HandleRef.tag in ["netixlan", "poc", "netfac"]:
update_network_attribute(vs.object, f"{vs.object.HandleRef.tag}_updated")
reversion.signals.post_revision_commit.connect(network_post_revision_commit)
def addressmodel_save(sender, instance=None, **kwargs):
"""
Mark address model objects for geocode sync if one of the address
fields is updated
"""
if instance.id:
# instance is being updated
old = sender.objects.get(id=instance.id)
for field in AddressModel._meta.get_fields():
if field.name in ["latitude", "longitude"]:
continue
a = getattr(instance, field.name)
b = getattr(old, field.name)
if a != b:
# print("Change in field '%s' - '%s'(%s) to '%s'(%s) - marking %s for geocode sync" % (field.name, a, type(a), b, type(b), instance))
# address model field has changed, mark for geocode sync
instance.geocode_status = False
pre_save.connect(addressmodel_save, sender=Facility)
def org_save(sender, **kwargs):
"""
we want to create a user group for an organization when that
organization is created
"""
inst = kwargs.get("instance")
# make the general member group for the org
try:
group = Group.objects.get(name=inst.group_name)
except Group.DoesNotExist:
group = Group(name=inst.group_name)
group.save()
perm = GroupPermission(
group=group, namespace=inst.grainy_namespace, permission=PERM_READ
)
perm.save()
GroupPermission(
group=group,
namespace=f"{inst.grainy_namespace}.network.*.poc_set.private",
permission=PERM_READ,
).save()
GroupPermission(
group=group,
namespace=f"{inst.grainy_namespace}.internetexchange.*.ixf_ixp_member_list_url.private",
permission=PERM_READ,
).save()
# make the admin group for the org
try:
group = Group.objects.get(name=inst.admin_group_name)
except Group.DoesNotExist:
group = Group(name=inst.admin_group_name)
group.save()
perm = GroupPermission(
group=group, namespace=inst.grainy_namespace, permission=PERM_CRUD
)
perm.save()
GroupPermission(
group=group, namespace=inst.grainy_namespace_manage, permission=PERM_CRUD
).save()
GroupPermission(
group=group,
namespace=f"{inst.grainy_namespace}.network.*.poc_set.private",
permission=PERM_CRUD,
).save()
GroupPermission(
group=group,
namespace=f"{inst.grainy_namespace}.internetexchange.*.ixf_ixp_member_list_url.private",
permission=PERM_CRUD,
).save()
if inst.status == "deleted":
for ar in inst.affiliation_requests.all():
ar.delete()
post_save.connect(org_save, sender=Organization)
def org_delete(sender, instance, **kwargs):
"""
When an organization is HARD deleted we want to also remove any
usergroups tied to the organization
"""
try:
instance.usergroup.delete()
except Group.DoesNotExist:
pass
try:
instance.admin_usergroup.delete()
except Group.DoesNotExist:
pass
for ar in instance.affiliation_requests.all():
ar.delete()
pre_delete.connect(org_delete, sender=Organization)
@receiver(user_signed_up, dispatch_uid="allauth.user_signed_up")
def new_user_to_guests(request, user, sociallogin=None, **kwargs):
"""
When a user is created via oauth login put them in the guest
group for now.
Unless pdb_settings.AUTO_VERIFY_USERS is toggled on in settings, in which
case users get automatically verified (note that this does
not include email verification, they will still need to do that)
"""
if pdb_settings.AUTO_VERIFY_USERS:
user.set_verified()
else:
user.set_unverified()
# USER TO ORGANIZATION AFFILIATION
def uoar_creation(sender, instance, created=False, **kwargs):
"""
When a user to organization affiliation request is created
we want to notify the approporiate management entity
We also want to attempt to derive the targeted organization
from the ASN the user provided
"""
if created:
if instance.asn and not instance.org_id:
network = Network.objects.filter(asn=instance.asn).first()
if network:
# network with targeted asn found, set org
instance.org = network.org
instance.status = "pending"
instance.save()
if instance.org_id and instance.org.admin_usergroup.user_set.count() > 0:
# check that user is not already a member of that org
if instance.user.groups.filter(name=instance.org.usergroup.name).exists():
instance.approve()
return
# organization exists already and has admins, notify organization
# admins
for user in instance.org.admin_usergroup.user_set.all():
with override(user.locale):
user.email_user(
_(
"User %(u_name)s wishes to be affiliated to your Organization"
)
% {"u_name": instance.user.full_name},
loader.get_template(
"email/notify-org-admin-user-affil.txt"
).render(
{
"user": instance.user,
"org": instance.org,
"org_management_url": "%s/org/%d#users"
% (settings.BASE_URL, instance.org.id),
}
),
)
else:
request_type = "be affiliated to"
rdap_data = {"emails": []}
org_created = False
net_created = False
rdap_lookup = None
if instance.asn and not instance.org_id:
# ASN specified in request, but no network found
# Lookup RDAP information
try:
rdap_lookup = rdap = RdapLookup().get_asn(instance.asn)
ok = rdap_lookup.emails
except RdapException as inst:
instance.deny()
raise
# create organization
instance.org, org_created = Organization.create_from_rdap(
rdap, instance.asn, instance.org_name
)
instance.save()
# create network
net, net_created = Network.create_from_rdap(
rdap, instance.asn, instance.org
)
# if affiliate auto appove is on, auto approve at this point
if pdb_settings.AUTO_APPROVE_AFFILIATION:
instance.approve()
return
ticket_queue_asnauto_create(
instance.user,
instance.org,
net,
rdap,
net.asn,
org_created=org_created,
net_created=net_created,
)
# if user's relationship to network can be validated now
# we can approve the ownership request right away
if instance.user.validate_rdap_relationship(rdap):
instance.approve()
ticket_queue_asnauto_affil(instance.user, instance.org, net, rdap)
return
if instance.org:
# organization has been set on affiliation request
entity_name = instance.org.name
if not instance.org.owned:
# organization is currently not owned
request_type = "request ownership of"
# if affiliate auto appove is on, auto approve at this point
if pdb_settings.AUTO_APPROVE_AFFILIATION:
instance.approve()
return
# if user's relationship to the org can be validated by
# checking the rdap information of the org's networks
# we can approve the affiliation (ownership) request right away
for asn, rdap in list(instance.org.rdap_collect.items()):
rdap_data["emails"].extend(rdap.emails)
if instance.user.validate_rdap_relationship(rdap):
ticket_queue_asnauto_affil(
instance.user,
instance.org,
Network.objects.get(asn=asn),
rdap,
)
instance.approve()
return
else:
entity_name = instance.org_name
if pdb_settings.AUTO_APPROVE_AFFILIATION:
org = Organization.objects.create(
name=instance.org_name, status="ok"
)
instance.org = org
instance.approve()
return
# organization has no owners and RDAP information could not verify the user's relationship to the organization, notify pdb staff for review
ticket_queue(
"User %s wishes to %s %s"
% (instance.user.username, request_type, entity_name),
loader.get_template("email/notify-pdb-admin-user-affil.txt").render(
{
"user": instance.user,
"instance": instance,
"base_url": settings.BASE_URL,
"org_add_url": "%s%s"
% (
settings.BASE_URL,
django.urls.reverse(
"admin:peeringdb_server_organization_add"
),
),
"net_add_url": "%s%s"
% (
settings.BASE_URL,
django.urls.reverse("admin:peeringdb_server_network_add"),
),
"review_url": "%s%s"
% (
settings.BASE_URL,
django.urls.reverse(
"admin:peeringdb_server_user_change",
args=(instance.user.id,),
),
),
"approve_url": "%s%s"
% (
settings.BASE_URL,
django.urls.reverse(
"admin:peeringdb_server_userorgaffiliationrequest_actions",
args=(instance.id, "approve_and_notify"),
),
),
"emails": list(set(rdap_data["emails"])),
"rdap_lookup": rdap_lookup,
}
),
instance.user,
)
elif instance.status == "approved" and instance.org_id:
# uoar was not created, and status is now approved, call approve
# to finalize
instance.approve()
post_save.connect(uoar_creation, sender=UserOrgAffiliationRequest)
# VERIFICATION QUEUE
if getattr(settings, "DISABLE_VERIFICATION_QUEUE", False) is False:
def verification_queue_update(sender, instance, **kwargs):
if instance.status == "pending":
try:
VerificationQueueItem.objects.get(
content_type=ContentType.objects.get_for_model(sender),
object_id=instance.id,
)
except VerificationQueueItem.DoesNotExist:
q = VerificationQueueItem(item=instance)
q.save()
else:
try:
q = VerificationQueueItem.objects.get(
content_type=ContentType.objects.get_for_model(sender),
object_id=instance.id,
)
q.delete()
except VerificationQueueItem.DoesNotExist:
pass
def verification_queue_delete(sender, instance, **kwargs):
try:
q = VerificationQueueItem.objects.get(
content_type=ContentType.objects.get_for_model(sender),
object_id=instance.id,
)
q.delete()
except VerificationQueueItem.DoesNotExist:
pass
def verification_queue_notify(sender, instance, **kwargs):
# notification was already sent
if instance.notified:
return
# we don't sent notifications unless requesting user has been identified
if not instance.user_id:
return
item = instance.item
user = instance.user
if type(item) in QUEUE_NOTIFY and not getattr(
settings, "DISABLE_VERIFICATION_QUEUE_EMAILS", False
):
if type(item) == Network:
rdap = RdapLookup().get_asn(item.asn)
else:
rdap = None
with override("en"):
entity_type_name = str(instance.content_type)
title = f"{entity_type_name} - {item}"
if is_suggested(item):
title = f"[SUGGEST] {title}"
ticket_queue(
title,
loader.get_template("email/notify-pdb-admin-vq.txt").render(
{
"entity_type_name": entity_type_name,
"suggested": is_suggested(item),
"item": item,
"user": user,
"rdap": rdap,
"edit_url": "%s%s"
% (settings.BASE_URL, instance.item_admin_url),
}
),
instance.user,
)
instance.notified = True
instance.save()
post_save.connect(verification_queue_notify, sender=VerificationQueueItem)
for model in QUEUE_ENABLED:
post_save.connect(verification_queue_update, sender=model)
pre_delete.connect(verification_queue_delete, sender=model)
def cors_allow_api_get_to_everyone(sender, request, **kwargs):
# FIXME: path name to look for should come from config
return (
request.path == "/api" or request.path.startswith("/api/")
) and request.method in ["GET", "OPTIONS"]
check_request_enabled.connect(cors_allow_api_get_to_everyone)