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

@ -221,3 +221,8 @@ EMAIL_SETTINGS:
```
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)
need_token = models.BooleanField(default=False)
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()
created = models.DateTimeField(default=timezone.now)
def get_action(self):

View File

@ -114,6 +114,15 @@ class BaseAction(object):
self.action.cache["token_fields"] = token_fields
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):
"""
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):
self._validate()
self.set_auto_approve()
def _post_approve(self):
self._validate()
@ -291,6 +292,7 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
def _pre_approve(self):
self._validate()
self.set_auto_approve()
def _post_approve(self):
self._validate()

View File

@ -218,10 +218,7 @@ class UserRoles(tasks.TaskView):
timezone.now())
return Response(errors, status=status)
task = processed['task']
self.logger.info("(%s) - AutoApproving EditUser request."
% timezone.now())
response_dict, status = self.approve(request, task)
response_dict = {'notes': processed.get('notes')}
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
based on running of the the pre_approve validation
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(
self.task_type, settings.DEFAULT_TASK_SETTINGS)
@ -211,6 +215,29 @@ class TaskView(APIViewWithLogger):
email_conf = class_conf.get('emails', {}).get('initial', None)
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
def _create_token(self, task):
@ -417,13 +444,12 @@ class InviteUser(TaskView):
if errors:
self.logger.info("(%s) - Validation errors with task." %
timezone.now())
return Response(errors, status=status)
task = processed['task']
self.logger.info("(%s) - AutoApproving AttachUser request."
% timezone.now())
if isinstance(errors, dict):
return Response(errors, status=status)
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'])
@ -472,6 +498,8 @@ class ResetPassword(TaskView):
self.logger.info("(%s) - AutoApproving Resetuser request."
% 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)
response_dict = {'notes': [
"If user with email exists, reset token will be issued."]}
@ -548,11 +576,7 @@ class EditUser(TaskView):
timezone.now())
return Response(errors, status=status)
task = processed['task']
self.logger.info("(%s) - AutoApproving EditUser request."
% timezone.now())
response_dict, status = self.approve(request, task)
response_dict = {'notes': processed.get('notes')}
add_task_id_for_roles(request, processed, response_dict, ['admin'])
return Response(response_dict, status=status)