Files
adjutant/api_v1/views.py
adriant 69c8435a5b basic email templating
Change-Id: I6be5ac18ee9760a39b026a71106970c31f4e0ac5
2015-02-25 16:34:14 +13:00

883 lines
30 KiB
Python

# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from decorator import decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from models import Registration, Token, Notification
from django.utils import timezone
from datetime import timedelta
from uuid import uuid4
from logging import getLogger
from django.core.mail import send_mail
from smtplib import SMTPException
from django.conf import settings
from django.template import loader, Context
@decorator
def admin_or_owner(func, *args, **kwargs):
"""
endpoints setup with this decorator require the defined roles.
"""
req_roles = {'admin', 'project_owner'}
request = args[1]
if not request.keystone_user.get('authenticated', False):
return Response({'notes': ["Credentials incorrect or none given."]},
401)
roles = set(request.keystone_user.get('roles', []))
if roles & req_roles:
return func(*args, **kwargs)
return Response({'notes': ["Must have one of the following roles: %s" %
list(req_roles)]},
403)
@decorator
def admin(func, *args, **kwargs):
"""
endpoints setup with this decorator require the admin role.
"""
request = args[1]
if not request.keystone_user.get('authenticated', False):
return Response({'notes': ["Credentials incorrect or none given."]},
401)
roles = request.keystone_user.get('roles', [])
if "admin" in roles:
return func(*args, **kwargs)
return Response({'notes': ["Must be admin."]}, 403)
def create_token(registration):
# expire needs to be made configurable.
expire = timezone.now() + timedelta(hours=24)
# is this a good way to create tokens?
uuid = uuid4().hex
token = Token.objects.create(
registration=registration,
token=uuid,
expires=expire
)
token.save()
return token
def email_token(registration, token):
emails = set()
actions = []
for action in registration.actions:
act = action.get_action()
if act.need_token:
emails.add(act.token_email())
actions.append(unicode(act))
if len(emails) > 1:
notes = {
'notes':
(("Error: Unable to send token, More than one email for" +
" registration: %s") % registration.uuid)
}
create_notification(registration, notes)
# TODO(adriant): raise some error?
# and surround calls to this function with try/except
context = {'actions': actions, 'token': token.token}
email_template = loader.get_template("token.txt")
try:
message = email_template.render(Context(context))
send_mail(
'Your token', message, 'no-reply@example.com',
[emails.pop()], fail_silently=False)
except SMTPException as e:
notes = {
'notes':
("Error: '%s' while emailing token for registration: %s" %
(e, registration.uuid))
}
create_notification(registration, notes)
# TODO(adriant): raise some error?
# and surround calls to this function with try/except
def create_notification(registration, notes):
notification = Notification.objects.create(
registration=registration,
notes=notes
)
notification.save()
class APIViewWithLogger(APIView):
"""
APIView with a logger.
"""
def __init__(self, *args, **kwargs):
super(APIViewWithLogger, self).__init__(*args, **kwargs)
self.logger = getLogger('django.request')
class NotificationList(APIViewWithLogger):
@admin
def get(self, request, format=None):
"""
A list of unacknowledged Notification objects as dicts.
"""
notifications = Notification.objects.filter(acknowledged__exact=False)
note_list = []
for notification in notifications:
note_list.append(notification.to_dict())
return Response(note_list, status=200)
@admin
def post(self, request, format=None):
"""
Acknowledge notifications.
"""
note_list = request.data.get('notifications', None)
if note_list and isinstance(note_list, list):
notifications = Notification.objects.filter(pk__in=note_list)
for notification in notifications:
notification.acknowledged = True
notification.save()
return Response({'notes': ['Notifications acknowledged.']},
status=200)
else:
return Response({'notifications': ["this field is required" +
"needs to be a list."]},
status=400)
class NotificationDetail(APIViewWithLogger):
@admin
def get(self, request, pk, format=None):
"""
Dict notification of a Notification object
and its related actions.
"""
try:
notification = Notification.objects.get(pk=pk)
except Notification.DoesNotExist:
return Response(
{'notes': ['No notification with this id.']},
status=404)
return Response(notification.to_dict())
@admin
def post(self, request, pk, format=None):
"""
Acknowledge notification.
"""
try:
notification = Notification.objects.get(pk=pk)
except Notification.DoesNotExist:
return Response(
{'notes': ['No notification with this id.']},
status=404)
if request.data.get('acknowledged', False) is True:
notification.acknowledged = True
notification.save()
return Response({'notes': ['Notification acknowledged.']},
status=200)
else:
return Response({'acknowledged': ["this field is required."]},
status=400)
class RegistrationList(APIViewWithLogger):
@admin
def get(self, request, format=None):
"""
A list of dict representations of Registration objects
and their related actions.
"""
registrations = Registration.objects.all()
reg_list = []
for registration in registrations:
reg_list.append(registration.to_dict())
return Response(reg_list, status=200)
class RegistrationDetail(APIViewWithLogger):
@admin
def get(self, request, uuid, format=None):
"""
Dict representation of a Registration object
and its related actions.
"""
try:
registration = Registration.objects.get(uuid=uuid)
except Registration.DoesNotExist:
return Response(
{'notes': ['No registration with this id.']},
status=404)
return Response(registration.to_dict())
@admin
def put(self, request, uuid, format=None):
"""
Allows the updating of action data and retriggering
of the pre_approve step.
"""
try:
registration = Registration.objects.get(uuid=uuid)
except Registration.DoesNotExist:
return Response(
{'notes': ['No registration with this id.']},
status=404)
if registration.completed:
return Response(
{'notes':
['This registration has already been completed.']},
status=400)
act_list = []
valid = True
for action in registration.actions:
action_serializer = settings.ACTION_CLASSES[action.action_name][1]
if action_serializer is not None:
serializer = action_serializer(data=request.data)
else:
serializer = None
act_list.append({
'name': action.action_name,
'action': action,
'serializer': serializer})
if serializer is not None and not serializer.is_valid():
valid = False
if valid:
for act in act_list:
if act['serializer'] is not None:
data = act['serializer'].validated_data
else:
data = {}
act['action'].action_data = data
act['action'].save()
try:
act['action'].get_action().pre_approve()
except Exception as e:
notes = {
'errors':
[("Error: '%s' while updating registration. " +
"See registration itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the server. " +
"It will be looked into shortly."]
}
return Response(response_dict, status=500)
return Response(
{'notes': ["Registration successfully updated."]},
status=200)
else:
errors = {}
for act in act_list:
if act['serializer'] is not None:
errors.update(act['serializer'].errors)
return Response({'errors': errors}, status=400)
@admin
def post(self, request, uuid, format=None):
"""
Will approve the Registration specified,
followed by running the post_approve actions
and if valid will setup and create a related token.
"""
try:
registration = Registration.objects.get(uuid=uuid)
except Registration.DoesNotExist:
return Response(
{'notes': ['No registration with this id.']},
status=404)
if request.data.get('approved', False) is True:
if registration.completed:
return Response(
{'notes':
['This registration has already been completed.']},
status=400)
need_token = False
valid = True
actions = []
for action in registration.actions:
act_model = action.get_action()
actions.append(act_model)
try:
act_model.post_approve()
except Exception as e:
notes = {
'errors':
[("Error: '%s' while approving registration. " +
"See registration itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
return Response(notes, status=500)
if not action.valid:
valid = False
if action.need_token:
need_token = True
if valid:
registration.approved = True
registration.approved_on = timezone.now()
registration.save()
if need_token:
token = create_token(registration)
email_token(registration, token)
return Response({'notes': ['created token']}, status=200)
else:
for action in actions:
try:
action.submit({})
except Exception as e:
notes = {
'errors':
[("Error: '%s' while submitting " +
"registration. See registration " +
"itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped!" +
" %s\n Trace: \n%s") %
(timezone.now(), e, trace))
return Response(notes, status=500)
registration.completed = True
registration.completed_on = timezone.now()
registration.save()
return Response(
{'notes': "Registration completed successfully."},
status=200)
return Response({'notes': ['actions invalid']}, status=400)
else:
return Response({'approved': ["this field is required."]},
status=400)
class TokenList(APIViewWithLogger):
"""
Admin functionality for managing/monitoring tokens.
"""
@admin
def get(self, request, format=None):
"""
A list of dict representations of Token objects.
"""
tokens = Token.objects.all()
token_list = []
for token in tokens:
token_list.append(token.to_dict())
return Response(token_list)
@admin
def post(self, request, format=None):
"""
Reissue a token for an approved registration.
Clears other tokens for it.
"""
uuid = request.data.get('registration', None)
if uuid is None:
return Response(
{'registration': ["This field is required.", ]},
status=400)
try:
registration = Registration.objects.get(uuid=uuid)
except Registration.DoesNotExist:
return Response(
{'notes': ['No registration with this id.']},
status=404)
if not registration.approved:
return Response(
{'notes': ['This registration has not been approved.']},
status=400)
for token in registration.tokens:
token.delete()
token = create_token(registration)
email_token(registration, token)
return Response(
{'notes': ['Token reissued.']}, status=200)
@admin
def delete(self, request, format=None):
"""
Delete all expired tokens.
"""
now = timezone.now()
Token.objects.filter(expires__lt=now).delete()
return Response(
{'notes': ['Deleted all expired tokens.']}, status=200)
class TokenDetail(APIViewWithLogger):
def get(self, request, id, format=None):
"""
Returns a response with the list of required fields
and what actions those go towards.
"""
try:
token = Token.objects.get(token=id)
except Token.DoesNotExist:
return Response(
{'notes': ['This token does not exist.']}, status=404)
if token.registration.completed:
return Response(
{'notes':
['This registration has already been completed.']},
status=400)
if token.expires < timezone.now():
token.delete()
return Response({'notes': ['This token has expired.']}, status=400)
required_fields = []
actions = []
for action in token.registration.actions:
action = action.get_action()
actions.append(action)
for field in action.token_fields:
if field not in required_fields:
required_fields.append(field)
return Response({'actions': [unicode(act) for act in actions],
'required_fields': required_fields})
def post(self, request, id, format=None):
"""
Ensures the required fields are present,
will then pass those to the actions via the submit
function.
"""
try:
token = Token.objects.get(token=id)
except Token.DoesNotExist:
return Response(
{'notes': ['This token does not exist.']}, status=404)
if token.registration.completed:
return Response(
{'notes':
['This registration has already been completed.']},
status=400)
if token.expires < timezone.now():
token.delete()
return Response({'notes': ['This token has expired.']}, status=400)
required_fields = set()
actions = []
for action in token.registration.actions:
action = action.get_action()
actions.append(action)
for field in action.token_fields:
required_fields.add(field)
errors = {}
data = {}
for field in required_fields:
try:
data[field] = request.data[field]
except KeyError:
errors[field] = ["This field is required.", ]
if errors:
return Response(errors, status=400)
for action in actions:
try:
action.submit(data)
except Exception as e:
notes = {
'errors':
[("Error: '%s' while submitting registration. " +
"See registration itself for details.") % e],
'registration': token.registration.uuid
}
create_notification(token.registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the server. " +
"It will be looked into shortly."]
}
return Response(response_dict, status=500)
token.registration.completed = True
token.registration.completed_on = timezone.now()
token.registration.save()
token.delete()
return Response(
{'notes': "Token submitted successfully."},
status=200)
class ActionView(APIViewWithLogger):
"""
Base class for api calls that start a Registration.
Until it is moved to settings, 'default_action' is a
required hardcoded field.
The default_action is considered the primary action and
will always run first. Addtional actions are defined in
the settings file and will run in the order supplied, but
after the default_action.
"""
def get(self, request):
"""
The get method will return a json listing the actions this
view will run, and the data fields that those actons require.
"""
actions = [self.default_action, ]
actions += settings.API_ACTIONS.get(self.__class__.__name__, [])
required_fields = []
for action in actions:
action_class, action_serializer = settings.ACTION_CLASSES[action]
for field in action_class.required:
if field not in required_fields:
required_fields.append(field)
return Response({'actions': actions,
'required_fields': required_fields})
def process_actions(self, request):
"""
Will ensure the request data contains the required data
based on the action serializer, and if present will create
a Registration and the linked actions, attaching notes
based on running of the the pre_approve validation
function on all the actions.
"""
actions = [self.default_action, ]
actions += settings.API_ACTIONS.get(self.__class__.__name__, [])
act_list = []
valid = True
for action in actions:
action_class, action_serializer = settings.ACTION_CLASSES[action]
if action_serializer is not None:
serializer = action_serializer(data=request.data)
else:
serializer = None
act_list.append({
'name': action,
'action': action_class,
'serializer': serializer})
if serializer is not None and not serializer.is_valid():
valid = False
if valid:
ip_addr = request.META['REMOTE_ADDR']
keystone_user = request.keystone_user
registration = Registration.objects.create(
reg_ip=ip_addr, keystone_user=keystone_user)
registration.save()
for i, act in enumerate(act_list):
if act['serializer'] is not None:
data = act['serializer'].validated_data
else:
data = {}
action = act['action'](
data=data, registration=registration,
order=i
)
try:
action.pre_approve()
except Exception as e:
notes = {
'errors':
[("Error: '%s' while setting up registration. " +
"See registration itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the server. " +
"It will be looked into shortly."]
}
return response_dict
return {'registration': registration}
else:
errors = {}
for act in act_list:
if act['serializer'] is not None:
errors.update(act['serializer'].errors)
return {'errors': errors}
def approve(self, registration):
"""
Approves the registration and runs the post_approve steps.
Will create a token if required, otherwise will run the
submit steps.
"""
registration.approved = True
registration.approved_on = timezone.now()
registration.save()
action_models = registration.actions
actions = []
valid = True
need_token = False
for action in action_models:
act = action.get_action()
actions.append(act)
if not act.valid:
valid = False
if valid:
for action in actions:
try:
action.post_approve()
except Exception as e:
notes = {
'errors':
[("Error: '%s' while approving registration. " +
"See registration itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the server. " +
"It will be looked into shortly."]
}
return Response(response_dict, status=500)
if not action.valid:
valid = False
if action.need_token:
need_token = True
if valid:
if need_token:
token = create_token(registration)
email_token(registration, token)
return Response({'notes': ['created token']}, status=200)
else:
for action in actions:
try:
action.submit({})
except Exception as e:
notes = {
'errors':
[("Error: '%s' while submitting " +
"registration. See registration " +
"itself for details.") % e],
'registration': registration.uuid
}
create_notification(registration, notes)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped!" +
" %s\n Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the " +
"server. It will be looked into shortly."]
}
return Response(response_dict, status=500)
registration.completed = True
registration.completed_on = timezone.now()
registration.save()
return Response(
{'notes': "Registration completed successfully."},
status=200)
return Response({'notes': ['actions invalid']}, status=400)
return Response({'notes': ['actions invalid']}, status=400)
class CreateProject(ActionView):
default_action = "NewProject"
def post(self, request, format=None):
"""
Unauthenticated endpoint bound primarily to NewProject.
This process requires approval, so this will validate
incoming data and create a registration to be approved
later.
"""
self.logger.info("(%s) - Starting new project registration." %
timezone.now())
processed = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with registration." %
timezone.now())
return Response(errors, status=400)
notes = {
'notes':
['New registration for CreateProject.']
}
create_notification(processed['registration'], notes)
self.logger.info("(%s) - Registration created." % timezone.now())
return Response({'notes': ['registration created']}, status=200)
class AttachUser(ActionView):
default_action = 'NewUser'
@admin_or_owner
def get(self, request):
return super(AttachUser, self).get(request)
@admin_or_owner
def post(self, request, format=None):
"""
This endpoint requires either Admin access or the
request to come from a project_owner.
As such this Registration is considered pre-approved.
Runs process_actions, then does the approve step and
post_approve validation, and creates a Token if valid.
"""
self.logger.info("(%s) - New AttachUser request." % timezone.now())
processed = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with registration." %
timezone.now())
return Response(errors, status=400)
registration = processed['registration']
self.logger.info("(%s) - AutoApproving AttachUser request."
% timezone.now())
return self.approve(registration)
class ResetPassword(ActionView):
default_action = 'ResetUser'
def post(self, request, format=None):
"""
Unauthenticated endpoint bound to the password reset action.
"""
self.logger.info("(%s) - New ResetUser request." % timezone.now())
processed = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with registration." %
timezone.now())
return Response(errors, status=400)
registration = processed['registration']
self.logger.info("(%s) - AutoApproving Resetuser request."
% timezone.now())
return self.approve(registration)