Added Update Email task and action

The task only allows the current user to update their own email
address, a confirmation email is sent to the new address before
the switch.

Change-Id: I62b169d262c6455ffec96bdb29e254279e973851
This commit is contained in:
Amelia Cordwell 2016-12-20 10:50:17 +13:00 committed by adrian-turjak
parent 58ac750bcc
commit 829d6ac30e
18 changed files with 627 additions and 9 deletions

View File

@ -125,6 +125,14 @@ Basic default endpoints for the TaskViews.
* unauthenticated endpoint
* auto-approved
* issue a uri+token to user email to reset password
* ../v1/actions/UpdateEmail - GET
* return a json describing the actions and required fields for the endpoint.
* ../v1/actions/UpdateEmail - POST
* Authenticated but open to any user
* auto-approved
* takes an email address
* issue a uri+token to new email to update to that email
#### OpenStack Style TaskView Endpoints:

View File

@ -70,6 +70,7 @@ ACTIVE_TASKVIEWS:
- UserList
- RoleList
- SignUp
- UserUpdateEmail
DEFAULT_TASK_SETTINGS:
emails:
@ -297,6 +298,24 @@ TASK_SETTINGS:
token: null
role_blacklist:
- admin
update_email:
duplicate_policy: cancel
additional_actions:
- SendAdditionalEmailAction
emails:
initial: null
token:
subject: Confirm OpenStack Email Update
template: email_update_token.txt
completed:
subject: OpenStack Email Updated
template: email_update_completed.txt
action_settings:
SendAdditionalEmailAction:
initial:
subject: OpenStack Email Update Requested
template: email_update_started.txt
email_current_user: True
# mapping between roles and managable roles
ROLES_MAPPING:

View File

@ -113,6 +113,12 @@ class IdentityManager(object):
def update_user_password(self, user, password):
self.ks_client.users.update(user, password=password)
def update_user_email(self, user, email):
self.ks_client.users.update(user, email=email)
def update_user_name(self, user, name):
self.ks_client.users.update(user, name=name)
def find_role(self, name):
try:
role = self.ks_client.roles.find(name=name)

View File

@ -331,6 +331,30 @@ class UserMixin(ResourceMixin):
(e, self.username))
raise
def update_email(self, email, user=None):
id_manager = user_store.IdentityManager()
try:
if not user:
user = self.find_user()
id_manager.update_user_email(user, email)
except Exception as e:
self.add_note(
"Error: '%s' while changing email for user: %s" %
(e, self.username))
raise
def update_user_name(self, username, user=None):
id_manager = user_store.IdentityManager()
try:
if not user:
user = self.find_user()
id_manager.update_user_name(user, username)
except Exception as e:
self.add_note(
"Error: '%s' while changing username for user: %s" %
(e, self.username))
raise
class ProjectMixin(ResourceMixin):
"""Mixin with functions for projects."""

View File

@ -18,7 +18,8 @@ from stacktask.actions.v1 import serializers
from stacktask.actions.v1.projects import (
NewProjectWithUserAction, AddDefaultUsersToProjectAction)
from stacktask.actions.v1.users import (
EditUserRolesAction, NewUserAction, ResetUserPasswordAction)
EditUserRolesAction, NewUserAction, ResetUserPasswordAction,
UpdateUserEmailAction)
from stacktask.actions.v1.resources import (
NewDefaultNetworkAction, NewProjectDefaultNetworkAction,
SetProjectQuotaAction)
@ -44,6 +45,8 @@ register_action_class(
register_action_class(NewUserAction, serializers.NewUserSerializer)
register_action_class(ResetUserPasswordAction, serializers.ResetUserSerializer)
register_action_class(EditUserRolesAction, serializers.EditUserRolesSerializer)
register_action_class(
UpdateUserEmailAction, serializers.UpdateUserEmailSerializer)
# Register Resource actions:
register_action_class(

View File

@ -91,3 +91,7 @@ class SetProjectQuotaSerializer(serializers.Serializer):
class SendAdditionalEmailSerializer(serializers.Serializer):
pass
class UpdateUserEmailSerializer(BaseUserIdSerializer):
new_email = serializers.EmailField()

View File

@ -17,7 +17,8 @@ import mock
from django.test.utils import override_settings
from stacktask.actions.v1.users import (
EditUserRolesAction, NewUserAction, ResetUserPasswordAction)
EditUserRolesAction, NewUserAction, ResetUserPasswordAction,
UpdateUserEmailAction)
from stacktask.api.models import Task
from stacktask.api.v1 import tests
from stacktask.api.v1.tests import (FakeManager, setup_temp_cache,
@ -754,7 +755,7 @@ class UserActionTests(StacktaskTestCase):
action.pre_approve()
self.assertEquals(action.valid, True)
# Remove role from ROLES_MAPPING
# Change settings
with self.modify_dict_settings(ROLES_MAPPING={
'key_list': ['project_mod'],
'operation': "remove",
@ -785,7 +786,6 @@ class UserActionTests(StacktaskTestCase):
Tests that the role mappings do come from settings and a new role
added there will be allowed.
"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
@ -800,7 +800,6 @@ class UserActionTests(StacktaskTestCase):
setup_temp_cache({'test_project': project}, {user.id: user})
# Add a new role to the temp cache
tests.temp_cache['roles']['new_role'] = 'new_role'
task = Task.objects.create(
@ -911,7 +910,7 @@ class UserActionTests(StacktaskTestCase):
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['admin', 'project_mod'],
'roles': ['project_mod'],
'project_id': 'test_project_id',
'project_domain_id': 'default',
})
@ -944,3 +943,202 @@ class UserActionTests(StacktaskTestCase):
self.assertEquals(
tests.temp_cache['users'][user.id].email,
'test@example.com')
@override_settings(USERNAME_IS_EMAIL=True)
def test_update_email(self):
"""
Base test case for user updating email address.
"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id_1'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
setup_temp_cache({'test_project': project}, {user.id: user})
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': 'test_project_id',
'project_domain_id': 'default',
})
data = {
'new_email': 'new_test@example.com',
'user_id': user.id,
}
action = UpdateUserEmailAction(data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
action.post_approve()
self.assertEquals(action.valid, True)
token_data = {'confirm': True}
action.submit(token_data)
self.assertEquals(action.valid, True)
self.assertEquals(
tests.temp_cache['users']["user_id_1"].email,
'new_test@example.com')
self.assertEquals(
tests.temp_cache['users']["user_id_1"].name,
'new_test@example.com')
@override_settings(USERNAME_IS_EMAIL=True)
def test_update_email_invalid_user(self):
"""
Test case for an invalid user being updated.
"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id_1'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
setup_temp_cache({'test_project': project}, {})
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': 'test_project_id',
'project_domain_id': 'default',
})
data = {
'new_email': 'new_test@example.com',
'user_id': "non_user_id",
}
action = UpdateUserEmailAction(data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, False)
action.post_approve()
self.assertEquals(action.valid, False)
token_data = {'confirm': True}
action.submit(token_data)
self.assertEquals(action.valid, False)
@override_settings(USERNAME_IS_EMAIL=True)
def test_update_email_invalid_email(self):
"""
Test case for a user attempting to update with an invalid email.
"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id_1'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
setup_temp_cache({'test_project': project}, {user.id: user})
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': 'test_project_id',
'project_domain_id': 'default',
})
data = {
'new_email': 'new_testexample.com',
'user_id': "non_user_id",
}
action = UpdateUserEmailAction(data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, False)
action.post_approve()
self.assertEquals(action.valid, False)
action.submit({'confirm': True})
self.assertEquals(action.valid, False)
self.assertEquals(
tests.temp_cache['users']["user_id_1"].email,
'test@example.com')
self.assertEquals(
tests.temp_cache['users']["user_id_1"].name,
'test@example.com')
@override_settings(USERNAME_IS_EMAIL=False)
def test_update_email_username_not_email(self):
"""
Test case for a user attempting to update with an invalid email.
"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id_1'
user.name = "test_user"
user.email = "test@example.com"
user.domain = 'default'
setup_temp_cache({'test_project': project}, {user.id: user})
task = Task.objects.create(
ip_address="0.0.0.0",
keystone_user={
'roles': ['project_mod'],
'project_id': 'test_project_id',
'project_domain_id': 'default',
})
data = {
'new_email': 'new_testexample.com',
'user_id': "user_id_1",
}
action = UpdateUserEmailAction(data, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
action.post_approve()
self.assertEquals(action.valid, True)
action.submit({'confirm': True})
self.assertEquals(action.valid, True)
self.assertEquals(
tests.temp_cache['users']["user_id_1"].email,
'new_testexample.com')
self.assertEquals(
tests.temp_cache['users']["user_id_1"].name,
'test_user')

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import settings
from django.db import models
from stacktask.actions import user_store
@ -325,3 +326,67 @@ class EditUserRolesAction(UserIdAction, ProjectMixin, UserMixin):
self.add_note(
"User %s didn't have roles %s in project %s."
% (self.user_id, self.roles, self.project_id))
class UpdateUserEmailAction(UserIdAction, UserMixin):
"""
Simple action to update a users email address for a given user.
"""
required = [
'user_id',
'new_email',
]
def _get_email(self):
# Sending to new email address
return self.new_email
def _validate(self):
self.action.valid = (self._validate_user() and
self._validate_email_not_in_use())
self.action.save()
def _validate_user(self):
self.user = self._get_target_user()
if self.user:
return True
return False
def _validate_email_not_in_use(self):
if settings.USERNAME_IS_EMAIL:
self.domain_id = self.action.task.keystone_user[
'project_domain_id']
id_manager = user_store.IdentityManager()
if id_manager.find_user(self.new_email, self.domain_id):
self.add_note("User with same username already exists")
return False
self.add_note("No user with same username")
return True
def _pre_approve(self):
self._validate()
self.set_auto_approve(True)
def _post_approve(self):
self._validate()
self.action.need_token = True
self.set_token_fields(["confirm"])
def _submit(self, token_data):
self._validate()
if not self.valid:
return
if token_data["confirm"]:
self.old_username = str(self.user.name)
self.update_email(self.new_email, user=self.user)
if settings.USERNAME_IS_EMAIL:
self.update_user_name(self.new_email, user=self.user)
self.add_note('The email for user %s has been changed to %s.'
% (self.old_username, self.new_email))

View File

@ -53,3 +53,16 @@ def admin(func, *args, **kwargs):
"""
return require_roles(
{'admin'}, func, *args, **kwargs)
@decorator
def authenticated(func, *args, **kwargs):
"""
endpoints setup with this decorator require the user to be signed in
"""
request = args[1]
if not request.keystone_user.get('authenticated', False):
return Response({'errors': ["Credentials incorrect or none given."]},
401)
return func(*args, **kwargs)

View File

@ -30,6 +30,8 @@ register_taskview_class(r'^actions/CreateProject/?$', tasks.CreateProject)
register_taskview_class(r'^actions/InviteUser/?$', tasks.InviteUser)
register_taskview_class(r'^actions/ResetPassword/?$', tasks.ResetPassword)
register_taskview_class(r'^actions/EditUser/?$', tasks.EditUser)
register_taskview_class(r'^actions/UpdateEmail/?$', tasks.UpdateEmail)
register_taskview_class(
r'^openstack/users/?$', openstack.UserList)
@ -43,5 +45,7 @@ register_taskview_class(
r'^openstack/users/password-reset/?$', openstack.UserResetPassword)
register_taskview_class(
r'^openstack/users/password-set/?$', openstack.UserSetPassword)
register_taskview_class(
r'^openstack/users/email-update/?$', openstack.UserUpdateEmail)
register_taskview_class(
r'^openstack/sign-up/?$', openstack.SignUp)

View File

@ -286,6 +286,20 @@ class UserSetPassword(tasks.ResetPassword):
return super(UserSetPassword, self).post(request)
class UserUpdateEmail(tasks.UpdateEmail):
"""
The openstack endpoint for a user to update their own email.
---
"""
def get(self, request):
"""
The EmailUpdate endpoint does not support GET.
This returns a 404.
"""
return Response(status=404)
class SignUp(tasks.CreateProject):
"""
The openstack endpoint for signups.

View File

@ -580,3 +580,29 @@ class EditUser(TaskView):
add_task_id_for_roles(request, processed, response_dict, ['admin'])
return Response(response_dict, status=status)
class UpdateEmail(TaskView):
task_type = "update_email"
default_actions = ["UpdateUserEmailAction", ]
@utils.authenticated
def post(self, request, format=None):
"""
Endpoint bound to the update email action.
This will submit and approve an update email action.
"""
request.data['user_id'] = request.keystone_user['user_id']
processed, status = self.process_actions(request)
errors = processed.get('errors', None)
if errors:
self.logger.info("(%s) - Validation errors with task." %
timezone.now())
return Response(errors, status=status)
response_dict = {'notes': processed['notes']}
return Response(response_dict, status=status)

View File

@ -0,0 +1,4 @@
This email is to confirm that your Openstack account email has now been changed.
Kind regards,
The Openstack team

View File

@ -1,6 +1,6 @@
Hello,
We have had an email address change request from you. We have sent a confirmation email to your new email: {{ actions.UpdateUserEmailAction.new_email }}
We have had an email address change request from you. A confirmation email will be sent to '{{ actions.UpdateUserEmailAction.new_email }}'.
If this was not you please get in touch with an administrator immediately.

View File

@ -0,0 +1,13 @@
Hello,
We have received a request to update your Openstack email to this account.
Please click the link below to update your email:
{{ tokenurl }}{{ token }}
This link will expire automatically after 24 hours. If expired, you will need to request another email update.
If you did not request this email update, please get in touch with your systems administrator to report suspicious activity and secure your account.
Kind regards,
The Openstack team

View File

@ -143,6 +143,14 @@ class FakeManager(object):
user = self._user_from_id(user)
user.password = password
def update_user_name(self, user, username):
user = self._user_from_id(user)
user.name = username
def update_user_email(self, user, email):
user = self._user_from_id(user)
user.email = email
def enable_user(self, user):
user = self._user_from_id(user)
user.enabled = True

View File

@ -654,7 +654,210 @@ class TaskViewTests(StacktaskAPITestCase):
self.assertFalse(response.data.get('task'))
# Positive tests for when USERNAME_IS_EMAIL=False
def test_update_email_task(self):
"""
Ensure the update email workflow goes as expected.
Create task, create token, submit token.
"""
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']})
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.assertEquals(user.name, 'new_test@example.com')
def test_update_email_task_invalid_email(self):
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@examplecom"}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data,
{'new_email': [u'Enter a valid email address.']})
@override_settings(USERNAME_IS_EMAIL=True)
def test_update_email_pre_existing_user_with_email(self):
user = mock.Mock()
user.id = 'test_user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user2 = mock.Mock()
user2.id = 'new_user_id'
user2.name = "new_test@example.com"
user2.email = "new_test@example.com"
user2.domain = 'default'
setup_temp_cache({}, {user.id: user, user2.id: user2})
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,
'project_domain_id': 'default',
}
data = {'new_email': "new_test@example.com"}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, ['actions invalid'])
self.assertEqual(len(Token.objects.all()), 0)
self.assertEqual(len(mail.outbox), 0)
@override_settings(USERNAME_IS_EMAIL=False)
def test_update_email_user_with_email_username_not_email(self):
user = mock.Mock()
user.id = 'test_user_id'
user.name = "test"
user.email = "test@example.com"
user.domain = 'Default'
user2 = mock.Mock()
user2.id = 'new_user_id'
user2.name = "new_test"
user2.email = "new_test@example.com"
user2.domain = 'Default'
setup_temp_cache({}, {user.id: user, user2.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), 1)
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.assertEquals(user.email, 'new_test@example.com')
def test_update_email_task_not_authenticated(self):
"""
Ensure that an unauthenticated user cant access the endpoint.
"""
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 = {
}
data = {'new_email': "new_test@examplecom"}
response = self.client.post(url, data, format='json', headers=headers)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
@override_settings(USERNAME_IS_EMAIL=False)
def test_update_email_task_username_not_email(self):
user = mock.Mock()
user.id = 'test_user_id'
user.name = "test_user"
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_user",
'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']})
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.assertEquals(user.name, "test_user")
self.assertEquals(user.email, 'new_test@example.com')
# Tests for USERNAME_IS_EMAIL=False
@override_settings(USERNAME_IS_EMAIL=False)
def test_invite_user_email_not_username(self):
"""

View File

@ -86,6 +86,7 @@ ACTIVE_TASKVIEWS = [
'InviteUser',
'ResetPassword',
'EditUser',
'UpdateEmail'
]
DEFAULT_TASK_SETTINGS = {
@ -230,7 +231,12 @@ TASK_SETTINGS = {
'subject': 'Setup Your OpenStack Password'
}
}
}
},
'update_email': {
'emails': {
'initial': None,
},
},
}
ROLES_MAPPING = {