From 607cc93d677e59d83846d2fb6bac60273b5d8260 Mon Sep 17 00:00:00 2001 From: Amelia Cordwell Date: Wed, 28 Dec 2016 13:59:51 +1300 Subject: [PATCH] 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 --- DEVSTACK_GUIDE.md | 7 ++- .../migrations/0002_action_auto_approve.py | 19 ++++++++ stacktask/actions/models.py | 11 ++++- stacktask/actions/v1/base.py | 9 ++++ stacktask/actions/v1/users.py | 2 + stacktask/api/v1/openstack.py | 5 +-- stacktask/api/v1/tasks.py | 44 ++++++++++++++----- 7 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 stacktask/actions/migrations/0002_action_auto_approve.py diff --git a/DEVSTACK_GUIDE.md b/DEVSTACK_GUIDE.md index 493a726..7f36fe0 100644 --- a/DEVSTACK_GUIDE.md +++ b/DEVSTACK_GUIDE.md @@ -220,4 +220,9 @@ EMAIL_SETTINGS: EMAIL_HOST_PASSWORD: ``` -Once the service has reset, it should now send emails via that server rather than print them to console. \ No newline at end of file +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. diff --git a/stacktask/actions/migrations/0002_action_auto_approve.py b/stacktask/actions/migrations/0002_action_auto_approve.py new file mode 100644 index 0000000..e920ea2 --- /dev/null +++ b/stacktask/actions/migrations/0002_action_auto_approve.py @@ -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), + ), + ] diff --git a/stacktask/actions/models.py b/stacktask/actions/models.py index 2bc97e6..c22fc36 100644 --- a/stacktask/actions/models.py +++ b/stacktask/actions/models.py @@ -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): diff --git a/stacktask/actions/v1/base.py b/stacktask/actions/v1/base.py index 23c028c..3e85824 100644 --- a/stacktask/actions/v1/base.py +++ b/stacktask/actions/v1/base.py @@ -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. diff --git a/stacktask/actions/v1/users.py b/stacktask/actions/v1/users.py index cf228ee..9b755e7 100644 --- a/stacktask/actions/v1/users.py +++ b/stacktask/actions/v1/users.py @@ -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() diff --git a/stacktask/api/v1/openstack.py b/stacktask/api/v1/openstack.py index d0ce927..42c1f37 100644 --- a/stacktask/api/v1/openstack.py +++ b/stacktask/api/v1/openstack.py @@ -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']) diff --git a/stacktask/api/v1/tasks.py b/stacktask/api/v1/tasks.py index 1cd4b40..51d898b 100644 --- a/stacktask/api/v1/tasks.py +++ b/stacktask/api/v1/tasks.py @@ -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)