Added auto approval as part of actions

In actions pre-approve stage the function self.set_auto_approve()
can be called, to identify the action as one that is allowed to
be pre-approved. (True, False and None can be specified). If the
function has not been called when auto_approve is accessed it will
default to None. This is saved as a new attribute in the actions
model.

At the task layer before process_actions finishes, it checks to
see the status of it's actions auto_approve. If none of these are
False and at least one of them is True it will auto approve
if all of it's actions have auto_approve set to true, if so
instead of returning it it will run (and return the values of)
the approve function.

ResetPassword is the only pre-approved action that has not been
switched to this way, due to possible security implications, as
it would return 'actions invalid' if the user did not exist.

Change-Id: I678849d212b7e91de541120e0d70ddf08cf9b488
This commit is contained in:
Amelia Cordwell 2016-12-28 13:59:51 +13:00
parent 8a2f2f2107
commit 607cc93d67
7 changed files with 80 additions and 17 deletions

View File

@ -220,4 +220,9 @@ EMAIL_SETTINGS:
EMAIL_HOST_PASSWORD: <password> EMAIL_HOST_PASSWORD: <password>
``` ```
Once the service has reset, it should now send emails via that server rather than print them to console. Once the service has reset, it should now send emails via that server rather than print them to console.
## Updating stacktask
Stacktask doesn't have a typical manage.py file, instead this functionality is installed into the virtual enviroment when stacktask is installed.
All of the expected Django functionality can be used using the 'stacktask-api' cli.

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('actions', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='action',
name='auto_approve',
field=models.NullBooleanField(default=None),
),
]

View File

@ -30,9 +30,16 @@ class Action(models.Model):
valid = models.BooleanField(default=False) valid = models.BooleanField(default=False)
need_token = models.BooleanField(default=False) need_token = models.BooleanField(default=False)
task = models.ForeignKey('api.Task') task = models.ForeignKey('api.Task')
# NOTE(amelia): Auto approve is technically a ternary operator
# If all in a task are None it will not auto approve
# However if at least one action has it set to True it
# will auto approve. If any are set to False this will
# override all of them.
# Can be thought of in terms of priority, None has the
# lowest priority, then True with False having the
# highest priority
auto_approve = models.NullBooleanField(default=None)
order = models.IntegerField() order = models.IntegerField()
created = models.DateTimeField(default=timezone.now) created = models.DateTimeField(default=timezone.now)
def get_action(self): def get_action(self):

View File

@ -114,6 +114,15 @@ class BaseAction(object):
self.action.cache["token_fields"] = token_fields self.action.cache["token_fields"] = token_fields
self.action.save() self.action.save()
@property
def auto_approve(self):
return self.action.auto_approve
def set_auto_approve(self, can_approve=True):
self.add_note("Auto approve set to %s." % can_approve)
self.action.auto_approve = can_approve
self.action.save()
def add_note(self, note): def add_note(self, note):
""" """
Logs the note, and also adds it to the task action notes. Logs the note, and also adds it to the task action notes.

View File

@ -100,6 +100,7 @@ class NewUserAction(UserNameAction, ProjectMixin, UserMixin):
def _pre_approve(self): def _pre_approve(self):
self._validate() self._validate()
self.set_auto_approve()
def _post_approve(self): def _post_approve(self):
self._validate() self._validate()
@ -291,6 +292,7 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
def _pre_approve(self): def _pre_approve(self):
self._validate() self._validate()
self.set_auto_approve()
def _post_approve(self): def _post_approve(self):
self._validate() self._validate()

View File

@ -218,10 +218,7 @@ class UserRoles(tasks.TaskView):
timezone.now()) timezone.now())
return Response(errors, status=status) return Response(errors, status=status)
task = processed['task'] response_dict = {'notes': processed.get('notes')}
self.logger.info("(%s) - AutoApproving EditUser request."
% timezone.now())
response_dict, status = self.approve(request, task)
add_task_id_for_roles(request, processed, response_dict, ['admin']) add_task_id_for_roles(request, processed, response_dict, ['admin'])

View File

@ -138,6 +138,10 @@ class TaskView(APIViewWithLogger):
a Task and the linked actions, attaching notes a Task and the linked actions, attaching notes
based on running of the the pre_approve validation based on running of the the pre_approve validation
function on all the actions. function on all the actions.
If during the pre_approve step at least one of the actions
sets auto_approve to True, and none of them set it to False
the approval steps will also be run.
""" """
class_conf = settings.TASK_SETTINGS.get( class_conf = settings.TASK_SETTINGS.get(
self.task_type, settings.DEFAULT_TASK_SETTINGS) self.task_type, settings.DEFAULT_TASK_SETTINGS)
@ -211,6 +215,29 @@ class TaskView(APIViewWithLogger):
email_conf = class_conf.get('emails', {}).get('initial', None) email_conf = class_conf.get('emails', {}).get('initial', None)
send_email(task, email_conf) send_email(task, email_conf)
action_models = task.actions
approve_list = [act.get_action().auto_approve for act in action_models]
# TODO(amelia): It would be nice to explicitly test this, however
# currently we don't have the right combinations of
# actions to allow for it.
if False in approve_list:
can_auto_approve = False
elif True in approve_list:
can_auto_approve = True
else:
can_auto_approve = False
if can_auto_approve:
task_name = self.__class__.__name__
self.logger.info("(%s) - AutoApproving %s request."
% (timezone.now(), task_name))
approval_data, status = self.approve(request, task)
# Additional information that would be otherwise expected
approval_data['task'] = task
approval_data['auto_approved'] = True
return approval_data, status
return {'task': task}, 200 return {'task': task}, 200
def _create_token(self, task): def _create_token(self, task):
@ -417,13 +444,12 @@ class InviteUser(TaskView):
if errors: if errors:
self.logger.info("(%s) - Validation errors with task." % self.logger.info("(%s) - Validation errors with task." %
timezone.now()) timezone.now())
return Response(errors, status=status)
task = processed['task'] if isinstance(errors, dict):
self.logger.info("(%s) - AutoApproving AttachUser request." return Response(errors, status=status)
% timezone.now()) return Response({'errors': errors}, status=status)
response_dict, status = self.approve(request, task) response_dict = {'notes': processed['notes']}
add_task_id_for_roles(request, processed, response_dict, ['admin']) add_task_id_for_roles(request, processed, response_dict, ['admin'])
@ -472,6 +498,8 @@ class ResetPassword(TaskView):
self.logger.info("(%s) - AutoApproving Resetuser request." self.logger.info("(%s) - AutoApproving Resetuser request."
% timezone.now()) % timezone.now())
# NOTE(amelia): Not using auto approve due to security implications
# as it will return all errors including whether the user exists
self.approve(request, task) self.approve(request, task)
response_dict = {'notes': [ response_dict = {'notes': [
"If user with email exists, reset token will be issued."]} "If user with email exists, reset token will be issued."]}
@ -548,11 +576,7 @@ class EditUser(TaskView):
timezone.now()) timezone.now())
return Response(errors, status=status) return Response(errors, status=status)
task = processed['task'] response_dict = {'notes': processed.get('notes')}
self.logger.info("(%s) - AutoApproving EditUser request."
% timezone.now())
response_dict, status = self.approve(request, task)
add_task_id_for_roles(request, processed, response_dict, ['admin']) add_task_id_for_roles(request, processed, response_dict, ['admin'])
return Response(response_dict, status=status) return Response(response_dict, status=status)