1
0
mirror of https://github.com/peeringdb/peeringdb.git synced 2024-05-11 05:55:09 +00:00
Files
peeringdb-peeringdb/peeringdb_server/deskpro.py
Stefan Pratter 3ee0d1ea5e Support 202205 (#1188)
* Add "Last login" to https://peeringdb.com/cp/peeringdb_server/user/ #879

* Bug in renumbering tool #660

* Add the IX name and id to IX-F Import Emails #963

* Use username to create deskpro person when no first-name / last-name data exists #1057

* Organization merge tool usergroup issues #930

* Exempt superusers (PeeringDB Admin Committee & Operations Committee admins) from throttling #1172

* add missing migration for #1172

* linting

* poetry relock

* adapt to django-two-factor-auth template-tag renaming changes

* increase default for DATA_QUALITY_MAX_SPEED to 5Tb

* regenerate docs

* fix api test for netixlan max speed validation

* pdb_api_cache command needs to disable CSRF_USE_SESSIONS before it runs

Co-authored-by: David Poarch <dpoarch@20c.com>
2022-06-15 07:23:26 -05:00

520 lines
14 KiB
Python

"""
DeskPro API Client used to post and retrieve support ticket information
from the deskpro API.
"""
import datetime
import re
import uuid
import django.urls
import requests
from django.conf import settings
from django.template import loader
from django.utils.translation import override
from peeringdb_server.inet import RdapNotFoundError
from peeringdb_server.models import DeskProTicket, is_suggested
from peeringdb_server.permissions import get_org_key_from_request, get_user_from_request
def ticket_queue(subject, body, user):
"""Queue a deskpro ticket for creation."""
DeskProTicket.objects.create(
subject=f"{settings.EMAIL_SUBJECT_PREFIX}{subject}",
body=body,
user=user,
)
def ticket_queue_email_only(subject, body, email):
"""Queue a deskpro ticket for creation."""
DeskProTicket.objects.create(
subject=f"{settings.EMAIL_SUBJECT_PREFIX}{subject}",
body=body,
email=email,
user=None,
)
class APIError(IOError):
def __init__(self, msg, data):
super().__init__(msg)
self.data = data
def ticket_queue_asnauto_skipvq(request, org, net, rir_data):
"""
Queue deskro ticket creation for asn automation action: skip vq.
"""
if isinstance(net, dict):
net_name = net.get("name")
else:
net_name = net.name
if isinstance(org, dict):
org_name = org.get("name")
else:
org_name = org.name
user = get_user_from_request(request)
if user:
ticket_queue(
f"[ASNAUTO] Network '{net_name}' approved for existing Org '{org_name}'",
loader.get_template("email/notify-pdb-admin-asnauto-skipvq.txt").render(
{"user": user, "org": org, "net": net, "rir_data": rir_data}
),
user,
)
return
org_key = get_org_key_from_request(request)
if org_key:
ticket_queue_email_only(
f"[ASNAUTO] Network '{net_name}' approved for existing Org '{org_name}'",
loader.get_template(
"email/notify-pdb-admin-asnauto-skipvq-org-key.txt"
).render(
{"org_key": org_key, "org": org, "net": net, "rir_data": rir_data}
),
org_key.email,
)
def ticket_queue_asnauto_affil(user, org, net, rir_data):
"""
Queue deskro ticket creation for asn automation action: affil.
"""
ticket_queue(
"[ASNAUTO] Ownership claim granted to Org '%s' for user '%s'"
% (org.name, user.username),
loader.get_template("email/notify-pdb-admin-asnauto-affil.txt").render(
{"user": user, "org": org, "net": net, "rir_data": rir_data}
),
user,
)
def ticket_queue_asnauto_create(
user, org, net, rir_data, asn, org_created=False, net_created=False
):
"""
Queue deskro ticket creation for asn automation action: create.
"""
subject = []
if org_created:
subject.append("Organization '%s'" % org.name)
if net_created:
subject.append("Network '%s'" % net.name)
if not subject:
return
subject = ", ".join(subject)
ticket_queue(
"[ASNAUTO] %s created" % subject,
loader.get_template(
"email/notify-pdb-admin-asnauto-entity-creation.txt"
).render(
{
"user": user,
"org": org,
"net": net,
"asn": asn,
"org_created": org_created,
"net_created": net_created,
"rir_data": rir_data,
}
),
user,
)
def ticket_queue_vqi_notify(instance, rdap):
item = instance.item
user = instance.user
org_key = instance.org_key
with override("en"):
entity_type_name = str(instance.content_type)
title = f"{entity_type_name} - {item}"
if is_suggested(item):
title = f"[SUGGEST] {title}"
if user:
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": f"{settings.BASE_URL}{instance.item_admin_url}",
}
),
user,
)
elif org_key:
ticket_queue_email_only(
title,
loader.get_template("email/notify-pdb-admin-vq-org-key.txt").render(
{
"entity_type_name": entity_type_name,
"suggested": is_suggested(item),
"item": item,
"org_key": org_key,
"rdap": rdap,
"edit_url": f"{settings.BASE_URL}{instance.item_admin_url}",
}
),
org_key.email,
)
def ticket_queue_rdap_error(request, asn, error):
if isinstance(error, RdapNotFoundError):
return
error_message = f"{error}"
if re.match("(.+) returned 400", error_message):
return
user = get_user_from_request(request)
if user:
subject = f"[RDAP_ERR] {user.username} - AS{asn}"
ticket_queue(
subject,
loader.get_template("email/notify-pdb-admin-rdap-error.txt").render(
{"user": user, "asn": asn, "error_details": error_message}
),
user,
)
return
org_key = get_org_key_from_request(request)
if org_key:
subject = f"[RDAP_ERR] {org_key.email} - AS{asn}"
ticket_queue_email_only(
subject,
loader.get_template("email/notify-pdb-admin-rdap-error-org-key.txt").render(
{"org_key": org_key, "asn": asn, "error_details": error_message}
),
org_key.email,
)
class APIClient:
def __init__(self, url, key):
self.key = key
self.url = url
@property
def auth_headers(self):
return {"Authorization": f"key {self.key}"}
def parse_response(self, response, many=False):
try:
r_json = response.json()
except Exception:
print(response.content)
raise
if "status" in r_json:
if r_json["status"] >= 400:
raise APIError(r_json["message"], r_json)
else:
response.raise_for_status()
data = r_json["data"]
if isinstance(data, list):
if many:
return r_json["data"]
elif data:
return data[0]
else:
return data
def get(self, endpoint, param):
response = requests.get(
f"{self.url}/{endpoint}", params=param, headers=self.auth_headers
)
return self.parse_response(response)
def create(self, endpoint, param):
response = requests.post(
f"{self.url}/{endpoint}", json=param, headers=self.auth_headers
)
return self.parse_response(response)
def update(self, endpoint, param):
response = requests.put(
f"{self.url}/{endpoint}", json=param, headers=self.auth_headers
)
if response.status_code == 204:
return {}
return self.parse_response(response)
def require_person(self, email, user=None):
"""
Get or create a deskpro person using the deskpro API.
At minimum, this needs to be passed to an email
address.
If a peeringdb user instance is also specified, it will
be used to fill in name information.
Arguments:
- email(`str`)
- user(`User`)
"""
person = self.get("people", {"primary_email": email})
if not person:
payload = {"primary_email": email}
self.update_person_payload(payload, user, email)
person = self.create("people", payload)
return person
def update_person_payload(self, payload, user=None, email=None):
if user:
# if no first name and last name are specified, use the username
if not user.first_name and not user.last_name:
payload.update(
name=user.username,
)
else:
payload.update(
first_name=user.first_name,
last_name=user.last_name,
name=user.full_name,
)
else:
payload.update(name=email)
return payload
def create_ticket(self, ticket):
"""
Create a deskpro ticket using the deskpro API.
Arguments:
- ticket (`DeskProTicket`)
"""
if ticket.user:
person = self.require_person(ticket.user.email, user=ticket.user)
elif ticket.email:
person = self.require_person(ticket.email)
else:
raise ValueError(
"Either user or email need to be specified on the DeskProTicket instance"
)
if not ticket.deskpro_id:
cc = []
for _cc in ticket.cc_set.all():
cc.append(_cc.email)
ticket_response = self.create(
"tickets",
{
"subject": ticket.subject,
"person": {"id": person["id"]},
"status": "awaiting_agent",
"cc": cc,
},
)
ticket.deskpro_ref = ticket_response["ref"]
ticket.deskpro_id = ticket_response["id"]
else:
self.reopen_ticket(ticket)
self.create(
f"tickets/{ticket.deskpro_id}/messages",
{
"message": ticket.body.replace("\n", "<br />\n"),
"person": person["id"],
"format": "html",
},
)
def reopen_ticket(self, ticket):
"""
Check the current status of existing tickets
on deskpro's side.
If the ticket has already been resolved, set it
back to awaiting_agent before posting a new message to
it (see #920).
"""
if not ticket.deskpro_id:
return
endpoint = f"tickets/{ticket.deskpro_id}"
ticket_data = self.get(endpoint, param={})
if ticket_data and ticket_data.get("ticket_status") == "resolved":
print("ticket resolved already")
self.update(endpoint, {"status": "awaiting_agent"})
print("Re-opened ticket (set to awaiting_agent)", ticket.deskpro_id)
class MockAPIClient(APIClient):
"""
A mock API client for the deskpro API.
The IX-F importer uses this when
IXF_SEND_TICKETS=False
"""
def __init__(self, *args, **kwargs):
super().__init__("", "")
self.ticket_count = 0
def get(self, endpoint, param):
if endpoint == "people":
return {"id": 1}
return {}
def create(self, endpoint, param):
if endpoint == "tickets":
self.ticket_count += 1
ref = f"{uuid.uuid4()}"
return {"ref": ref[:16], "id": self.ticket_count}
return {}
class FailingMockAPIClient(MockAPIClient):
"""
A mock API client for the deskpro API
that returns an error on post.
Use in tests, for example
with issue 856.
"""
def __init__(self, *args, **kwargs):
super().__init__("", "")
self.ticket_count = 0
def get(self, endpoint, param):
return {"error": "API error with get."}
def create(self, endpoint, param):
return {"error": "API error with create."}
def create_ticket(self, ticket=None):
raise APIError(
"API error when creating ticket.",
{"error": "API error when creating ticket."},
)
def ticket_queue_deletion_prevented(request, instance):
"""
Queue deskpro ticket to notify the prevented
deletion of an object #696.
"""
subject = (
f"[PROTECTED] Deletion prevented: "
f"{instance.HandleRef.tag}-{instance.id} "
f"{instance}"
)
# we don't want to spam DeskPRO with tickets when a user
# repeatedly clicks the delete button for an object
#
# so we check if a ticket has recently been sent for it
# and opt out if it falls with in the spam protection
# period defined in settings
period = settings.PROTECTED_OBJECT_NOTIFICATION_PERIOD
now = datetime.datetime.now(datetime.timezone.utc)
max_age = now - datetime.timedelta(hours=period)
ticket = DeskProTicket.objects.filter(
subject=f"{settings.EMAIL_SUBJECT_PREFIX}{subject}"
)
ticket = ticket.filter(created__gt=max_age)
# recent ticket for object exists, bail
if ticket.exists():
return
model_name = instance.__class__.__name__.lower()
# Create ticket if a request was made by user or UserAPIKey
user = get_user_from_request(request)
if user:
ticket_queue(
subject,
loader.get_template("email/notify-pdb-admin-deletion-prevented.txt").render(
{
"user": user,
"instance": instance,
"admin_url": settings.BASE_URL
+ django.urls.reverse(
f"admin:peeringdb_server_{model_name}_change",
args=(instance.id,),
),
}
),
user,
)
return
# Create ticket if request was made by OrgAPIKey
org_key = get_org_key_from_request(request)
if org_key:
ticket_queue_email_only(
subject,
loader.get_template(
"email/notify-pdb-admin-deletion-prevented-org-key.txt"
).render(
{
"org_key": org_key,
"instance": instance,
"admin_url": settings.BASE_URL
+ django.urls.reverse(
f"admin:peeringdb_server_{model_name}_change",
args=(instance.id,),
),
}
),
org_key.email,
)