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 c21130eed9 Docs 202109 (#1067)
* module docstrings

* db schema graph

* dev docs first pass

* dev docs pass 2

* add generated notification to top of generated docs files

* linting

* regen docs

Co-authored-by: Stefan Pratter <stefan@20c.com>
Co-authored-by: Sunshine Buchholz <sunshine@20c.com>
2021-10-15 03:25:38 -05:00

553 lines
18 KiB
Python

"""
Django signal handlers
- org usergroup creation
- entity count updates (fac_count, net_count etc.)
- geocode when address model (org, fac) is saved
- verification queue creation on new objects
- asn rdap automation to automatically grant org / network to user
- user to org affiliation handling when targeted org has no users
- notify admin-com
- CORS enabling for GET api requests
"""
from datetime import datetime, timezone
import django.urls
import reversion
from allauth.account.signals import email_confirmed, user_signed_up
from corsheaders.signals import check_request_enabled
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.db.models.signals import post_save, pre_delete, pre_save
from django.dispatch import receiver
from django.template import loader
from django.utils.translation import override
from django.utils.translation import ugettext_lazy as _
from django_grainy.models import Group, GroupPermission
from django_peeringdb.const import REGION_MAPPING
from django_peeringdb.models.abstract import AddressModel
from grainy.const import PERM_CRUD, PERM_READ
import peeringdb_server.settings as pdb_settings
from peeringdb_server.deskpro import (
ticket_queue,
ticket_queue_asnauto_affil,
ticket_queue_asnauto_create,
ticket_queue_vqi_notify,
)
from peeringdb_server.inet import RdapException, RdapLookup
from peeringdb_server.models import (
QUEUE_ENABLED,
QUEUE_NOTIFY,
Facility,
Network,
NetworkIXLan,
Organization,
UserOrgAffiliationRequest,
VerificationQueueItem,
)
from peeringdb_server.util import disable_auto_now_and_save
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 update_counts_for_netixlan(netixlan):
"""
Whenever a netixlan is saved, update the ix_count for the related Network
and update net_count for the related InternetExchange.
"""
if getattr(netixlan, "id"):
network = netixlan.network
network.ix_count = (
network.netixlan_set_active.aggregate(
ix_count=Count("ixlan__ix_id", distinct=True)
)
)["ix_count"]
disable_auto_now_and_save(network)
ix = netixlan.ixlan.ix
ix.net_count = (
NetworkIXLan.objects.filter(ixlan__ix_id=ix.id, status="ok").aggregate(
net_count=Count("network_id", distinct=True)
)
)["net_count"]
disable_auto_now_and_save(ix)
def update_counts_for_netfac(netfac):
"""
Whenever a netfac is saved, update the fac_count for the related Network
and update net_count for the related Facility.
"""
if getattr(netfac, "id"):
network = netfac.network
network.fac_count = network.netfac_set_active.count()
disable_auto_now_and_save(network)
facility = netfac.facility
facility.net_count = facility.netfac_set_active.count()
disable_auto_now_and_save(facility)
def update_counts_for_ixfac(ixfac):
"""
Whenever a ixfac is saved, update the fac_count for the related Exchange
and update ix_count for the related Facility.
"""
if getattr(ixfac, "id"):
ix = ixfac.ix
ix.fac_count = ix.ixfac_set_active.count()
disable_auto_now_and_save(ix)
facility = ixfac.facility
facility.ix_count = facility.ixfac_set_active.count()
disable_auto_now_and_save(facility)
def connector_objects_post_revision_commit(**kwargs):
for vs in kwargs.get("versions"):
if vs.object.HandleRef.tag == "netixlan":
update_counts_for_netixlan(vs.object)
if vs.object.HandleRef.tag == "netfac":
update_counts_for_netfac(vs.object)
if vs.object.HandleRef.tag == "ixfac":
update_counts_for_ixfac(vs.object)
reversion.signals.post_revision_commit.connect(connector_objects_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):
"""
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.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.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, 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 temporarily.
If pdb_settings.AUTO_VERIFY_USERS is toggled on in the settings, users get automatically verified (Note: 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()
@receiver(email_confirmed, dispatch_uid="allauth.email_confirmed")
def recheck_ownership_requests(request, email_address, **kwargs):
if request.user.is_authenticated:
request.user.recheck_affiliation_requests()
# USER TO ORGANIZATION AFFILIATION
def uoar_creation(sender, instance, created=False, **kwargs):
"""
Notify the approporiate management entity when a user to organization affiliation request is created.
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)
except RdapException:
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
# no contact point exists
if not instance.user_id and not instance.org_key:
return
item = instance.item
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
ticket_queue_vqi_notify(instance, rdap)
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)
def auto_fill_region_continent(sender, instance, **kwargs):
if instance.country.code:
for region in REGION_MAPPING:
if instance.country.code == region["code"]:
instance.region_continent = region["continent"]
pre_save.connect(auto_fill_region_continent, sender=Facility)