diff --git a/.coveragerc b/.coveragerc index dd99454..6edb8f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,5 +6,7 @@ exclude_lines = if settings.DEBUG: raise AssertionError raise NotImplementedError + self.fail omit = adjutant/wsgi.py + setup.py diff --git a/adjutant/actions/utils.py b/adjutant/actions/utils.py index cf55389..1b28471 100644 --- a/adjutant/actions/utils.py +++ b/adjutant/actions/utils.py @@ -39,7 +39,9 @@ def send_email(to_addresses, context, conf, task): # message headers from_email = conf.get('from') if not from_email: - from_email = conf['reply'] + from_email = conf.get('reply') + if not from_email: + return elif "%(task_uuid)s" in from_email: from_email = from_email % {'task_uuid': task.uuid} diff --git a/adjutant/actions/v1/misc.py b/adjutant/actions/v1/misc.py index a44211d..95d47c2 100644 --- a/adjutant/actions/v1/misc.py +++ b/adjutant/actions/v1/misc.py @@ -31,8 +31,11 @@ class SendAdditionalEmailAction(BaseAction): self.emails.add(self.action.task.keystone_user['username']) else: try: - self.emails.add(self.action.task.keystone_user['email']) - except KeyError: + id_manager = user_store.IdentityManager() + email = id_manager.get_user( + self.action.task.keystone_user['user_id']).email + self.emails.add(email) + except AttributeError: self.add_note("Could not add current user email address") if conf.get('email_roles'): @@ -52,7 +55,7 @@ class SendAdditionalEmailAction(BaseAction): self.emails.add(user.email) if conf.get('email_task_cache'): - task_emails = self.task.cache.get('additional_emails', []) + task_emails = self.action.task.cache.get('additional_emails', []) if isinstance(task_emails, six.string_types): task_emails = [task_emails] for email in task_emails: diff --git a/adjutant/actions/v1/tests/test_misc_actions.py b/adjutant/actions/v1/tests/test_misc_actions.py new file mode 100644 index 0000000..2bf9e11 --- /dev/null +++ b/adjutant/actions/v1/tests/test_misc_actions.py @@ -0,0 +1,237 @@ +# 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. + +import mock + +from django.core import mail + +from adjutant.actions.v1.misc import SendAdditionalEmailAction +from adjutant.actions.utils import send_email +from adjutant.api.models import Task +from adjutant.api.v1.tests import (FakeManager, + modify_dict_settings, AdjutantTestCase) +from smtplib import SMTPException + +default_email_conf = { + 'from': "adjutant@example.com", + 'reply': 'adjutant@example.com', + 'template': 'initial.txt', + 'html_template': 'completed.txt', + 'subject': 'additional email' +} + + +class FailEmail(mock.MagicMock): + def send(self, *args, **kwargs): + raise SMTPException + + +@mock.patch('adjutant.actions.user_store.IdentityManager', + FakeManager) +class MiscActionTests(AdjutantTestCase): + + def test_send_email(self): + # include html template + to_address = "test@example.com" + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + context = { + 'task': task, + 'actions': ["action_1", "action_2"] + } + + result = send_email(to_address, context, default_email_conf, task) + + # check the email itself + self.assertNotEqual(result, None) + self.assertEqual(len(mail.outbox), 1) + self.assertTrue("action_1" in mail.outbox[0].body) + + def test_send_email_no_addresses(self): + to_address = [] + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + context = { + 'task': task, + 'actions': ["action_1", "action_2"] + } + + result = send_email(to_address, context, default_email_conf, task) + self.assertEqual(result, None) + self.assertEqual(len(mail.outbox), 0) + + @mock.patch('adjutant.actions.utils.EmailMultiAlternatives', + FailEmail) + @modify_dict_settings(DEFAULT_ACTION_SETTINGS={ + 'operation': 'update', + 'key_list': ['SendAdditionalEmailAction', 'token'], + 'value': { + 'email_task_cache': True, + 'subject': 'Email Subject', + 'template': 'token.txt' + } + }, DEFAULT_TASK_SETTINGS={ + 'operation': 'delete', + 'key_list': ['notifications'], + }) + def test_send_additional_email_fail(self): + """ + Tests that a failure to send an additional email doesn't cause + it to become invalid or break. + """ + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={}, + task_type='edit_roles', + ) + + # setup settings + action = SendAdditionalEmailAction({}, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + task.cache["additional_emails"] = ["thisguy@righthere.com", + "nope@example.com"] + + action.post_approve() + self.assertEquals(action.valid, True) + + self.assertEqual(len(mail.outbox), 0) + self.assertTrue( + "Unable to send additional email. Stage: token" in + action.action.task.action_notes['SendAdditionalEmailAction'][1]) + + action.submit({}) + self.assertEquals(action.valid, True) + + @modify_dict_settings(DEFAULT_ACTION_SETTINGS={ + 'operation': 'update', + 'key_list': ['SendAdditionalEmailAction', 'token'], + 'value': { + 'email_task_cache': True, + 'subject': 'Email Subject', + 'template': 'token.txt' + } + }) + def test_send_additional_email_task_cache(self): + """ + Tests sending an additional email with the address placed in the + task cache. + """ + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + # setup settings + action = SendAdditionalEmailAction({}, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + task.cache["additional_emails"] = ["thisguy@righthere.com", + "nope@example.com"] + + action.post_approve() + self.assertEquals(action.valid, True) + + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(set(mail.outbox[0].to), + set(["thisguy@righthere.com", "nope@example.com"])) + + action.submit({}) + self.assertEquals(action.valid, True) + self.assertEqual(len(mail.outbox), 1) + + @modify_dict_settings(DEFAULT_ACTION_SETTINGS={ + 'operation': 'update', + 'key_list': ['SendAdditionalEmailAction', 'token'], + 'value': { + 'email_task_cache': True, + 'subject': 'Email Subject', + 'template': 'token.txt' + } + }) + def test_send_additional_email_task_cache_none_set(self): + """ + Tests sending an additional email with 'email_task_cache' set but + no address placed in the task cache. + """ + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + # setup settings + action = SendAdditionalEmailAction({}, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + action.post_approve() + self.assertEquals(action.valid, True) + + self.assertEqual(len(mail.outbox), 0) + + action.submit({}) + self.assertEquals(action.valid, True) + + @modify_dict_settings(DEFAULT_ACTION_SETTINGS={ + 'operation': 'update', + 'key_list': ['SendAdditionalEmailAction', 'token'], + 'value': { + 'email_additional_addresses': ['anadminwhocares@example.com'], + 'subject': 'Email Subject', + 'template': 'token.txt' + } + }) + def test_send_additional_email_email_in_settings(self): + """ + Tests sending an additional email with the address placed in the + task cache. + """ + + task = Task.objects.create( + ip_address="0.0.0.0", + keystone_user={} + ) + + # setup settings + action = SendAdditionalEmailAction({}, task=task, order=1) + + action.pre_approve() + self.assertEquals(action.valid, True) + + action.post_approve() + self.assertEquals(action.valid, True) + + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, + ["anadminwhocares@example.com"]) + + action.submit({}) + self.assertEquals(action.valid, True) + self.assertEqual(len(mail.outbox), 1) diff --git a/adjutant/api/v1/tests/test_api_admin.py b/adjutant/api/v1/tests/test_api_admin.py index c177611..9dc1507 100644 --- a/adjutant/api/v1/tests/test_api_admin.py +++ b/adjutant/api/v1/tests/test_api_admin.py @@ -19,13 +19,14 @@ from datetime import timedelta from unittest import skip from django.utils import timezone +from django.core import mail import mock from rest_framework import status from rest_framework.test import APITestCase -from adjutant.api.models import Task, Token +from adjutant.api.models import Task, Token, Notification from adjutant.api.v1.tests import (FakeManager, setup_temp_cache, modify_dict_settings) @@ -189,8 +190,7 @@ class AdminAPITests(APITestCase): def test_token_get(self): """ - Token should contian task uuid, task_type, required fields, and it's - own value + Token should contain actions, task_type, required fields. """ user = mock.Mock() @@ -221,6 +221,46 @@ class AdminAPITests(APITestCase): u'task_type': 'reset_password'}) self.assertEqual(1, Token.objects.count()) + def test_token_list_get(self): + user = mock.Mock() + user.id = 'user_id' + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = 'default' + user.password = "test_password" + + setup_temp_cache({}, {user.id: user}) + + url = "/v1/actions/ResetPassword" + data = {'email': "test@example.com"} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data['notes'], + ['If user with email exists, reset token will be issued.']) + + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + first_task_id = Task.objects.all()[0].uuid + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + url = "/v1/tokens/" + + response = self.client.get(url, headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + len(response.json()['tokens']), 2) + self.assertEqual(response.json()['tokens'][1]['task'], + first_task_id) + def test_task_complete(self): """ Can't approve a completed task. @@ -251,6 +291,66 @@ class AdminAPITests(APITestCase): response.json(), {'errors': ['This task has already been completed.']}) + def test_status_page(self): + """ + Status page gives details of last_created_task, last_completed_task + and error notifcations + """ + + setup_temp_cache({}, {}) + + url = "/v1/actions/CreateProject" + data = {'project_name': "test_project", 'email': "test@example.com"} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + url = "/v1/status/" + response = self.client.get(url, headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()['last_created_task'][ + 'actions'][0]['data']['email'], 'test@example.com') + self.assertEqual(response.json()['last_completed_task'], None) + + self.assertEqual(response.json()['error_notifications'], []) + + # Create a second task and ensure it is the new last_created_task + url = "/v1/actions/CreateProject" + data = {'project_name': "test_project_2", + 'email': "test_2@example.com"} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + url = "/v1/status/" + response = self.client.get(url, headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()['last_created_task'][ + 'actions'][0]['data']['email'], 'test_2@example.com') + self.assertEqual(response.json()['last_completed_task'], None) + + self.assertEqual(response.json()['error_notifications'], []) + + new_task = Task.objects.all()[0] + new_task.completed = True + new_task.save() + + url = "/v1/status/" + response = self.client.get(url, headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()['last_completed_task'][ + 'actions'][0]['data']['email'], 'test@example.com') + self.assertEqual(response.json()['last_created_task'][ + 'actions'][0]['data']['email'], 'test_2@example.com') + + self.assertEqual(response.json()['error_notifications'], []) + def test_task_update(self): """ Creates a invalid task. @@ -305,6 +405,63 @@ class AdminAPITests(APITestCase): response.json(), {'notes': ['created token']}) + def test_notification_get(self): + """ + Test that you can get details of an induvidual notfication. + """ + setup_temp_cache({}, {}) + + url = "/v1/actions/CreateProject" + data = {'project_name': "test_project", 'email': "test@example.com"} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + new_task = Task.objects.all()[0] + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + note = Notification.objects.first().uuid + + url = "/v1/notifications/%s" % note + response = self.client.get(url, headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.json()['task'], + new_task.uuid) + self.assertEqual( + response.json()['notes'], + {u'notes': [u'New task for CreateProject.']}) + self.assertEqual( + response.json()['error'], False) + + def test_notification_doesnt_exist(self): + """ + Test that you get a 404 trying to access a non-existent notification. + """ + setup_temp_cache({}, {}) + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + note = "notarealnotifiactionuuid" + + url = "/v1/notifications/%s/" % note + response = self.client.get(url, headers=headers, format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.json(), + {"errors": ["No notification with this id."]}) + def test_notification_acknowledge(self): """ Test that you can acknowledge a notification. @@ -352,6 +509,89 @@ class AdminAPITests(APITestCase): ) self.assertEqual(response.json(), {'notifications': []}) + def test_notification_acknowledge_doesnt_exist(self): + """ + Test that you cant acknowledge a non-existent notification. + """ + setup_temp_cache({}, {}) + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + + url = "/v1/notifications/dasdaaaiooiiobksd/" + response = self.client.post(url, headers=headers) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.json(), + {'errors': + ['No notification with this id.']}) + + def test_notification_re_acknowledge(self): + """ + Test that you cant reacknowledge a notification. + """ + setup_temp_cache({}, {}) + + url = "/v1/actions/CreateProject" + data = {'project_name': "test_project", 'email': "test@example.com"} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + + note_id = Notification.objects.first().uuid + url = "/v1/notifications/%s/" % note_id + data = {'acknowledged': True} + response = self.client.post(url, data, format='json', headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), + {'notes': ['Notification acknowledged.']}) + + response = self.client.post(url, data, format='json', headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), + {'notes': ['Notification already acknowledged.']}) + + def test_notification_acknowledge_no_data(self): + """ + Test that you have to include 'acknowledged': True to the request. + """ + setup_temp_cache({}, {}) + + url = "/v1/actions/CreateProject" + data = {'project_name': "test_project", 'email': "test@example.com"} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + + note_id = Notification.objects.first().uuid + url = "/v1/notifications/%s/" % note_id + data = {} + response = self.client.post(url, data, format='json', headers=headers) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json(), + {u'acknowledged': [u'this field is required.']}) + def test_notification_acknowledge_list(self): """ Test that you can acknowledge a list of notifications. @@ -397,6 +637,91 @@ class AdminAPITests(APITestCase): ) self.assertEqual(response.json(), {'notifications': []}) + def test_notification_acknowledge_list_empty_list(self): + """ + Test that you cannot acknowledge an empty list of notifications. + """ + setup_temp_cache({}, {}) + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + + url = "/v1/notifications" + response = self.client.get(url, headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = {'notifications': []} + response = self.client.post(url, data, format='json', headers=headers) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json(), + {u'notifications': + [u'this field is required and needs to be a list.']}) + + @modify_dict_settings(DEFAULT_TASK_SETTINGS={ + 'key_list': ['notifications'], + 'operation': 'override', + 'value': { + 'EmailNotification': { + 'standard': { + 'emails': ['example@example.com'], + 'reply': 'no-reply@example.com', + 'template': 'notification.txt' + }, + 'error': { + 'emails': ['example@example.com'], + 'reply': 'no-reply@example.com', + 'template': 'notification.txt' + } + } + } + }, TASK_SETTINGS={ + 'key_list': ['create_project', 'emails'], + 'operation': 'override', + 'value': { + 'initial': None, + 'token': None, + 'completed': None + } + }) + def test_notification_email(self): + """ + Tests the email notification engine + """ + setup_temp_cache({}, {}) + + url = "/v1/actions/CreateProject" + data = {'project_name': "test_project", 'email': "test@example.com"} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + new_task = Task.objects.all()[0] + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + + url = "/v1/notifications" + response = self.client.get(url, headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["notifications"][0]['task'], + new_task.uuid) + + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, 'create_project notification') + self.assertTrue("New task for CreateProject" in mail.outbox[0].body) + def test_token_expired_delete(self): """ test deleting of expired tokens. @@ -555,6 +880,124 @@ class AdminAPITests(APITestCase): self.assertEqual(response.json(), {'errors': ['No task with this id.']}) + def test_token_reissue_task_cancelled(self): + """ + Tests that a cancelled task cannot have a token reissued + """ + + user = mock.Mock() + user.id = 'user_id' + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = 'default' + user.password = "test_password" + + setup_temp_cache({}, {user.id: user}) + + url = "/v1/actions/ResetPassword" + data = {'email': "test@example.com"} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.json()['notes'], + ['If user with email exists, reset token will be issued.']) + + task = Task.objects.all()[0] + task.cancelled = True + task.save() + self.assertEqual(Token.objects.count(), 1) + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + url = "/v1/tokens/" + data = {"task": task.uuid} + response = self.client.post(url, data, format='json', + headers=headers) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json(), + {'errors': ['This task has been cancelled.']}) + + def test_token_reissue_task_completed(self): + """ + Tests that a completed task cannot have a token reissued + """ + + user = mock.Mock() + user.id = 'user_id' + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = 'default' + user.password = "test_password" + + setup_temp_cache({}, {user.id: user}) + + url = "/v1/actions/ResetPassword" + data = {'email': "test@example.com"} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.json()['notes'], + ['If user with email exists, reset token will be issued.']) + + task = Task.objects.all()[0] + task.completed = True + task.save() + self.assertEqual(Token.objects.count(), 1) + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + url = "/v1/tokens/" + data = {"task": task.uuid} + response = self.client.post(url, data, format='json', + headers=headers) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json(), + {'errors': ['This task has already been completed.']}) + + def test_token_reissue_task_not_approve(self): + """ + Tests that an unapproved task cannot have a token reissued + """ + + setup_temp_cache({}, {}) + + url = "/v1/actions/CreateProject" + data = {'email': "test@example.com", "project_name": "test_project"} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.json()['notes'], [u'task created']) + + task = Task.objects.all()[0] + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "admin,_member_", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + url = "/v1/tokens/" + data = {"task": task.uuid} + response = self.client.post(url, data, format='json', + headers=headers) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json(), + {'errors': ['This task has not been approved.']}) + def test_cancel_task(self): """ Ensure the ability to cancel a task. diff --git a/adjutant/api/v1/tests/test_api_openstack.py b/adjutant/api/v1/tests/test_api_openstack.py index d91c422..bb3992d 100644 --- a/adjutant/api/v1/tests/test_api_openstack.py +++ b/adjutant/api/v1/tests/test_api_openstack.py @@ -110,6 +110,40 @@ class OpenstackAPITests(APITestCase): response = self.client.get(url, headers=headers) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.json()['users']), 2) + self.assertTrue(b'test2@example.com' in response.content) + + def test_user_detail(self): + """ + Confirm that the user detail view functions as expected + """ + + user = mock.Mock() + user.id = 'test_user_id' + user.name = 'test@example.com' + user.email = 'test@example.com' + + project = mock.Mock() + project.id = 'test_project_id' + project.name = 'test_project' + project.domain = 'default' + project.roles = {user.id: ['_member_']} + + setup_temp_cache({'test_project': project}, {user.id: user}) + + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "project_admin,_member_,project_mod", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + + url = "/v1/openstack/users/%s" % user.id + response = self.client.get(url, headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()['username'], 'test@example.com') + self.assertEqual(response.json()['roles'], ["_member_"]) def test_user_list_managable(self): """ diff --git a/adjutant/api/v1/tests/test_api_taskview.py b/adjutant/api/v1/tests/test_api_taskview.py index 8cbef41..50fff9c 100644 --- a/adjutant/api/v1/tests/test_api_taskview.py +++ b/adjutant/api/v1/tests/test_api_taskview.py @@ -731,6 +731,131 @@ class TaskViewTests(AdjutantAPITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEquals(user.name, 'new_test@example.com') + @modify_dict_settings(TASK_SETTINGS=[ + {'key_list': ['update_email', 'additional_actions'], + 'operation': 'append', + 'value': ['SendAdditionalEmailAction']}, + {'key_list': ['update_email', 'action_settings', + 'SendAdditionalEmailAction', 'initial'], + 'operation': 'update', + 'value': { + 'subject': 'email_update_additional', + 'template': 'email_update_started.txt', + 'email_roles': [], + 'email_current_user': True, + } + } + ]) + def test_update_email_task_send_email_to_current_user(self): + """ + Tests the email update workflow, and ensures that when setup + to send a confirmation email to the old email address it does. + """ + + user = mock.Mock() + user.id = 'test_user_id' + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = 'default' + + setup_temp_cache({}, {user.id: user}) + + url = "/v1/actions/UpdateEmail" + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "project_admin,_member_,project_mod", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + + data = {'new_email': "new_test@example.com"} + response = self.client.post(url, data, format='json', headers=headers) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'notes': ['created token']}) + + self.assertEqual(len(mail.outbox), 2) + self.assertEqual(mail.outbox[0].to, ['test@example.com']) + self.assertEqual(mail.outbox[0].subject, 'email_update_additional') + + self.assertEqual(mail.outbox[1].to, ['new_test@example.com']) + self.assertEqual(mail.outbox[1].subject, 'Your Token') + + new_token = Token.objects.all()[0] + url = "/v1/tokens/" + new_token.token + + data = {'confirm': True} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(user.name, 'new_test@example.com') + + self.assertEqual(len(mail.outbox), 3) + + @modify_dict_settings(TASK_SETTINGS=[ + {'key_list': ['update_email', 'additional_actions'], + 'operation': 'append', + 'value': ['SendAdditionalEmailAction']}, + {'key_list': ['update_email', 'action_settings', + 'SendAdditionalEmailAction', 'initial'], + 'operation': 'update', + 'value': { + 'subject': 'email_update_additional', + 'template': 'email_update_started.txt', + 'email_roles': [], + 'email_current_user': True} + } + ]) + @override_settings(USERNAME_IS_EMAIL=False) + def test_update_email_task_send_email_current_name_not_email(self): + """ + Tests the email update workflow when USERNAME_IS_EMAIL=False, and + ensures that when setup to send a confirmation email to the old + email address it does. + """ + + user = mock.Mock() + user.id = 'test_user_id' + user.name = "nkdfslnkls" + user.email = "test@example.com" + user.domain = 'default' + + setup_temp_cache({}, {user.id: user}) + + url = "/v1/actions/UpdateEmail" + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "project_admin,_member_,project_mod", + 'username': "nkdfslnkls", + 'user_id': "test_user_id", + 'authenticated': True, + 'email': 'test@example.com', + } + + data = {'new_email': "new_test@example.com"} + response = self.client.post(url, data, format='json', headers=headers) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'notes': ['created token']}) + + self.assertEqual(len(mail.outbox), 2) + self.assertEqual(mail.outbox[0].to, ['test@example.com']) + self.assertEqual(mail.outbox[0].subject, 'email_update_additional') + + self.assertEqual(mail.outbox[1].to, ['new_test@example.com']) + self.assertEqual(mail.outbox[1].subject, 'Your Token') + + new_token = Token.objects.all()[0] + url = "/v1/tokens/" + new_token.token + + data = {'confirm': True} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertEqual(len(mail.outbox), 3) + def test_update_email_task_invalid_email(self): user = mock.Mock() @@ -1112,6 +1237,71 @@ class TaskViewTests(AdjutantAPITestCase): response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + @modify_dict_settings( + TASK_SETTINGS=[ + {'key_list': ['invite_user', 'additional_actions'], + 'operation': 'append', + 'value': ['SendAdditionalEmailAction']}, + {'key_list': ['invite_user', 'action_settings', + 'SendAdditionalEmailAction', 'initial'], + 'operation': 'update', + 'value': { + 'subject': 'email_update_additional', + 'template': 'email_update_started.txt', + 'email_roles': ['project_admin'], + 'email_current_user': False, + } + } + ]) + def test_additional_emails_role_no_email(self): + """ + Tests that setting email roles to something that has no people to + send to that the update action doesn't fall over + """ + + project = mock.Mock() + project.id = 'test_project_id' + project.name = 'test_project' + project.domain = 'default' + + user = mock.Mock() + user.id = 'test_user_id' + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = 'default' + + project.roles = {user.id: ['_member_']} + + setup_temp_cache({'test_project': project}, {user.id: user}) + + url = "/v1/actions/InviteUser" + headers = { + 'project_name': "test_project", + 'project_id': "test_project_id", + 'roles': "project_admin,_member_,project_mod", + 'username': "test@example.com", + 'user_id': "test_user_id", + 'authenticated': True + } + + data = {'email': "new_test@example.com", + 'roles': ['_member_']} + response = self.client.post(url, data, format='json', headers=headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'notes': ['created token']}) + + self.assertEqual(len(mail.outbox), 1) + + # Test that the token email gets sent to the other addresses + self.assertEqual(mail.outbox[0].to[0], 'new_test@example.com') + + new_token = Token.objects.all()[0] + url = "/v1/tokens/" + new_token.token + + data = {'confirm': True, 'password': '1234'} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + @modify_dict_settings( TASK_SETTINGS=[ {'key_list': ['invite_user', 'additional_actions'], diff --git a/adjutant/api/v1/views.py b/adjutant/api/v1/views.py index e000472..e8b4ea7 100644 --- a/adjutant/api/v1/views.py +++ b/adjutant/api/v1/views.py @@ -106,9 +106,11 @@ class NotificationList(APIViewWithLogger): return Response({'notes': ['Notifications acknowledged.']}, status=200) else: - return Response({'notifications': ["this field is required" + - "needs to be a list."]}, - status=400) + return Response( + {'notifications': [ + "this field is required and needs to be a list." + ]}, + status=400) class NotificationDetail(APIViewWithLogger): @@ -680,11 +682,6 @@ class TokenDetail(APIViewWithLogger): ['This task has been cancelled.']}, status=400) - if token.expires < timezone.now(): - token.delete() - return Response({'errors': ['This token has expired.']}, - status=400) - required_fields = [] actions = [] @@ -727,11 +724,6 @@ class TokenDetail(APIViewWithLogger): ['This task has been cancelled.']}, status=400) - if token.expires < timezone.now(): - token.delete() - return Response({'errors': ['This token has expired.']}, - status=400) - required_fields = set() actions = [] for action in token.task.actions: diff --git a/adjutant/settings.py b/adjutant/settings.py index b0aaddd..f134164 100644 --- a/adjutant/settings.py +++ b/adjutant/settings.py @@ -136,17 +136,10 @@ LOGGING = CONFIG['LOGGING'] EMAIL_BACKEND = CONFIG['EMAIL_SETTINGS']['EMAIL_BACKEND'] EMAIL_TIMEOUT = 60 -if CONFIG['EMAIL_SETTINGS'].get('EMAIL_HOST'): - EMAIL_HOST = CONFIG['EMAIL_SETTINGS']['EMAIL_HOST'] - -if CONFIG['EMAIL_SETTINGS'].get('EMAIL_PORT'): - EMAIL_PORT = CONFIG['EMAIL_SETTINGS']['EMAIL_PORT'] - -if CONFIG['EMAIL_SETTINGS'].get('EMAIL_HOST_USER'): - EMAIL_HOST_USER = CONFIG['EMAIL_SETTINGS']['EMAIL_HOST_USER'] - -if CONFIG['EMAIL_SETTINGS'].get('EMAIL_HOST_PASSWORD'): - EMAIL_HOST_PASSWORD = CONFIG['EMAIL_SETTINGS']['EMAIL_HOST_PASSWORD'] +EMAIL_HOST = CONFIG['EMAIL_SETTINGS'].get('EMAIL_HOST') +EMAIL_PORT = CONFIG['EMAIL_SETTINGS'].get('EMAIL_PORT') +EMAIL_HOST_USER = CONFIG['EMAIL_SETTINGS'].get('EMAIL_HOST_USER') +EMAIL_HOST_PASSWORD = CONFIG['EMAIL_SETTINGS'].get('EMAIL_HOST_PASSWORD') # setting to control if user name and email are allowed # to have different values. diff --git a/adjutant/test_settings.py b/adjutant/test_settings.py index a3642c2..93f935f 100644 --- a/adjutant/test_settings.py +++ b/adjutant/test_settings.py @@ -106,7 +106,21 @@ DEFAULT_TASK_SETTINGS = { 'template': 'completed.txt', 'subject': 'Task completed' } - } + }, + 'notifications': { + 'EmailNotification': { + 'standard': { + 'emails': ['example@example.com'], + 'reply': 'no-reply@example.com', + 'template': 'notification.txt' + }, + 'error': { + 'emails': ['example@example.com'], + 'reply': 'no-reply@example.com', + 'template': 'notification.txt' + } + } + }, } DEFAULT_ACTION_SETTINGS = { @@ -240,6 +254,9 @@ TASK_SETTINGS = { 'initial': None, }, }, + 'edit_user': { + 'role_blacklist': ['admin'] + } } ROLES_MAPPING = { diff --git a/conf/conf.yaml b/conf/conf.yaml index a5921a4..462a786 100644 --- a/conf/conf.yaml +++ b/conf/conf.yaml @@ -197,7 +197,7 @@ DEFAULT_ACTION_SETTINGS: # email_roles: # - project_admin # Or sends to an email set in the task cache - # email_in_task_cache: true + # email_task_cache: true # Or sends to an arbitrary admin email # email_additional_addresses: # - admin@example.org