Add endpoint for switching project by name
Add a switch_name URL that can be used to switch project by name by migrating the switch (project ID) function based view to a class based view and then reuse the implementation for another view for the switch_name functionality. Change-Id: I2e8aecd9bf3151fb42a326d5b9aa28b98a2472d5 Signed-off-by: Tobias Urdin <tobias.urdin@binero.com>
This commit is contained in:
@@ -40,6 +40,7 @@ DEFAULT_DOMAIN = settings.OPENSTACK_KEYSTONE_DEFAULT_DOMAIN
|
||||
# figure out how to avoid this.
|
||||
class IsA(object):
|
||||
"""Class to compare param is a specified class."""
|
||||
|
||||
def __init__(self, cls):
|
||||
self.cls = cls
|
||||
|
||||
@@ -1309,7 +1310,7 @@ class OpenStackAuthTests(test.TestCase):
|
||||
response = self.client.post(url, form_data)
|
||||
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||
|
||||
url = reverse('switch_tenants', args=[project.id])
|
||||
url = reverse('switch_project_id', args=[project.id])
|
||||
|
||||
scoped._project['id'] = self.data.project_two.id
|
||||
|
||||
@@ -1339,6 +1340,70 @@ class OpenStackAuthTests(test.TestCase):
|
||||
def test_switch_with_wrong_next(self):
|
||||
self.test_switch(next='/bad_url')
|
||||
|
||||
@mock.patch.object(v3_auth.Token, 'get_access')
|
||||
@mock.patch.object(password.PasswordPlugin, 'list_projects')
|
||||
@mock.patch.object(v3_auth.Password, 'get_access')
|
||||
def test_switch_by_name(self, mock_get_access, mock_project_list,
|
||||
mock_get_access_token,
|
||||
next=None):
|
||||
def mock_redirect_return(param):
|
||||
if 'bad' not in param:
|
||||
return original_redirect(param)
|
||||
else:
|
||||
raise NoReverseMatch
|
||||
|
||||
project = self.data.project_two
|
||||
projects = [self.data.project_one, self.data.project_two]
|
||||
user = self.data.user
|
||||
scoped = self.data.scoped_access_info
|
||||
|
||||
form_data = self.get_form_data(user)
|
||||
|
||||
mock_get_access.return_value = self.data.unscoped_access_info
|
||||
mock_get_access_token.return_value = scoped
|
||||
mock_project_list.return_value = projects
|
||||
|
||||
original_redirect = shortcuts.redirect
|
||||
with mock.patch('django.shortcuts.redirect',
|
||||
side_effect=mock_redirect_return):
|
||||
url = reverse('login')
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(url, form_data)
|
||||
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||
|
||||
url = reverse('switch_project_name', args=[project.name])
|
||||
|
||||
scoped._project['id'] = self.data.project_two.id
|
||||
|
||||
if next:
|
||||
form_data.update({auth.REDIRECT_FIELD_NAME: next})
|
||||
response = self.client.get(url, form_data)
|
||||
|
||||
if next and 'bad' not in next:
|
||||
expected_url = next
|
||||
self.assertEqual(response['location'], expected_url)
|
||||
else:
|
||||
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||
|
||||
self.assertEqual(self.client.session['token'].project['id'],
|
||||
scoped.project_id)
|
||||
|
||||
mock_get_access.assert_called_once_with(IsA(session.Session))
|
||||
mock_get_access_token.assert_called_with(IsA(session.Session))
|
||||
mock_project_list.assert_called_once_with(
|
||||
IsA(session.Session),
|
||||
IsA(v3_auth.Password),
|
||||
self.data.unscoped_access_info)
|
||||
|
||||
def test_switch_by_name_with_next(self):
|
||||
self.test_switch_by_name(next='/next_url')
|
||||
|
||||
def test_switch_by_name_with_wrong_next(self):
|
||||
self.test_switch_by_name(next='/bad_url')
|
||||
|
||||
@mock.patch.object(v3_auth.Token, 'get_access')
|
||||
@mock.patch.object(password.PasswordPlugin, 'list_projects')
|
||||
@mock.patch.object(v3_auth.Password, 'get_access')
|
||||
|
||||
@@ -22,8 +22,12 @@ from openstack_auth import views
|
||||
urlpatterns = [
|
||||
re_path(r"^login/$", views.login, name='login'),
|
||||
re_path(r"^logout/$", views.logout, name='logout'),
|
||||
re_path(r'^switch/(?P<tenant_id>[^/]+)/$', views.switch,
|
||||
name='switch_tenants'),
|
||||
re_path(r'^switch/(?P<project>[^/]+)/$',
|
||||
views.ProjectSwitchByIDView.as_view(),
|
||||
name='switch_project_id'),
|
||||
re_path(r'^switch_name/(?P<project>[^/]+)/$',
|
||||
views.ProjectSwitchByNameView.as_view(),
|
||||
name='switch_project_name'),
|
||||
re_path(r'^switch_services_region/(?P<region_name>[^/]+)/$',
|
||||
views.switch_region,
|
||||
name='switch_services_region'),
|
||||
|
||||
@@ -296,7 +296,8 @@ def clean_up_auth_url(auth_url):
|
||||
|
||||
|
||||
def get_token_auth_plugin(auth_url, token, project_id=None, domain_name=None,
|
||||
system_scope=None):
|
||||
system_scope=None, project_name=None,
|
||||
project_domain_id=None):
|
||||
if system_scope:
|
||||
return v3_auth.Token(auth_url=auth_url,
|
||||
token=token,
|
||||
@@ -307,6 +308,12 @@ def get_token_auth_plugin(auth_url, token, project_id=None, domain_name=None,
|
||||
token=token,
|
||||
domain_name=domain_name,
|
||||
reauthenticate=False)
|
||||
if project_name and project_domain_id:
|
||||
return v3_auth.Token(auth_url=auth_url,
|
||||
token=token,
|
||||
project_name=project_name,
|
||||
project_domain_id=project_domain_id,
|
||||
reauthenticate=False)
|
||||
return v3_auth.Token(auth_url=auth_url,
|
||||
token=token,
|
||||
project_id=project_id,
|
||||
|
||||
+80
-52
@@ -24,6 +24,7 @@ from django.middleware import csrf
|
||||
from django import shortcuts
|
||||
from django.urls import NoReverseMatch
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils import http
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
@@ -32,6 +33,7 @@ from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.generic import edit as edit_views
|
||||
from django.views import View
|
||||
from keystoneauth1 import exceptions as keystone_exceptions
|
||||
|
||||
from openstack_auth import exceptions
|
||||
@@ -301,62 +303,88 @@ def logout(request, login_url=None, **kwargs):
|
||||
)
|
||||
|
||||
|
||||
# TODO(stephenfin): Migrate to CBV
|
||||
@login_required
|
||||
def switch(request, tenant_id, redirect_field_name=auth.REDIRECT_FIELD_NAME):
|
||||
"""Switches an authenticated user from one project to another."""
|
||||
LOG.debug('Switching to tenant %s for user "%s".',
|
||||
tenant_id, request.user.username)
|
||||
class ProjectSwitchView(View):
|
||||
def _get_token_auth_plugin_kwargs(self, request, project, domain=None):
|
||||
"""This need to be implemented by the view."""
|
||||
return {}
|
||||
|
||||
endpoint, __ = utils.fix_auth_url_version_prefix(request.user.endpoint)
|
||||
client_ip = utils.get_client_ip(request)
|
||||
session = utils.get_session(original_ip=client_ip)
|
||||
# Keystone can be configured to prevent exchanging a scoped token for
|
||||
# another token. Always use the unscoped token for requesting a
|
||||
# scoped token.
|
||||
unscoped_token = request.user.unscoped_token
|
||||
auth = utils.get_token_auth_plugin(auth_url=endpoint,
|
||||
token=unscoped_token,
|
||||
project_id=tenant_id)
|
||||
def get(self, request, project):
|
||||
LOG.debug('Switching to project %s for user "%s".',
|
||||
project, request.user.username)
|
||||
endpoint, __ = utils.fix_auth_url_version_prefix(request.user.endpoint)
|
||||
client_ip = utils.get_client_ip(request)
|
||||
session = utils.get_session(original_ip=client_ip)
|
||||
# Keystone can be configured to prevent exchanging a scoped token for
|
||||
# another token. Always use the unscoped token for requesting a
|
||||
# scoped token.
|
||||
unscoped_token = request.user.unscoped_token
|
||||
|
||||
try:
|
||||
auth_ref = auth.get_access(session)
|
||||
msg = 'Project switch successful for user "%(username)s".' % \
|
||||
{'username': request.user.username}
|
||||
LOG.info(msg)
|
||||
except keystone_exceptions.ClientException:
|
||||
msg = (
|
||||
_('Project switch failed for user "%(username)s".') %
|
||||
{'username': request.user.username})
|
||||
messages.error(request, msg)
|
||||
auth_ref = None
|
||||
LOG.exception('An error occurred while switching sessions.')
|
||||
auth_plugin_kwargs = {
|
||||
'auth_url': endpoint,
|
||||
'token': unscoped_token,
|
||||
}
|
||||
requested_domain_id = request.GET.get('domain_id')
|
||||
auth_plugin_kwargs.update(
|
||||
self._get_token_auth_plugin_kwargs(
|
||||
request, project, domain=requested_domain_id))
|
||||
auth_plugin = utils.get_token_auth_plugin(**auth_plugin_kwargs)
|
||||
|
||||
# Ensure the user-originating redirection url is safe.
|
||||
# Taken from django.contrib.auth.views.login()
|
||||
redirect_to = request.GET.get(redirect_field_name, '')
|
||||
if (not http.url_has_allowed_host_and_scheme(
|
||||
url=redirect_to,
|
||||
allowed_hosts=[request.get_host()])):
|
||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||
try:
|
||||
auth_ref = auth_plugin.get_access(session)
|
||||
msg = 'Project switch successful for user "%(username)s".' % \
|
||||
{'username': request.user.username}
|
||||
LOG.info(msg)
|
||||
except keystone_exceptions.ClientException:
|
||||
msg = (
|
||||
_('Project switch failed for user "%(username)s".') %
|
||||
{'username': request.user.username})
|
||||
messages.error(request, msg)
|
||||
auth_ref = None
|
||||
LOG.exception('An error occurred while switching sessions.')
|
||||
|
||||
if auth_ref:
|
||||
user = auth_user.create_user_from_token(
|
||||
request,
|
||||
auth_user.Token(auth_ref, unscoped_token=unscoped_token),
|
||||
endpoint)
|
||||
auth_user.set_session_from_user(request, user)
|
||||
message = (
|
||||
_('Switch to project "%(project_name)s" successful.') %
|
||||
{'project_name': request.user.project_name})
|
||||
messages.success(request, message)
|
||||
try:
|
||||
response = shortcuts.redirect(redirect_to)
|
||||
except NoReverseMatch:
|
||||
response = django_http.HttpResponseRedirect(settings.LOGIN_REDIRECT_URL)
|
||||
utils.set_response_cookie(response, 'recent_project',
|
||||
request.user.project_id)
|
||||
return response
|
||||
# Ensure the user-originating redirection url is safe.
|
||||
# Taken from django.contrib.auth.views.login()
|
||||
redirect_to = request.GET.get(auth.REDIRECT_FIELD_NAME, '')
|
||||
if (not http.url_has_allowed_host_and_scheme(
|
||||
url=redirect_to,
|
||||
allowed_hosts=[request.get_host()])):
|
||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||
|
||||
if auth_ref:
|
||||
user = auth_user.create_user_from_token(
|
||||
request,
|
||||
auth_user.Token(auth_ref, unscoped_token=unscoped_token),
|
||||
endpoint)
|
||||
auth_user.set_session_from_user(request, user)
|
||||
message = (
|
||||
_('Switch to project "%(project_name)s" successful.') %
|
||||
{'project_name': request.user.project_name})
|
||||
messages.success(request, message)
|
||||
try:
|
||||
response = shortcuts.redirect(redirect_to)
|
||||
except NoReverseMatch:
|
||||
response = django_http.HttpResponseRedirect(
|
||||
settings.LOGIN_REDIRECT_URL)
|
||||
utils.set_response_cookie(response, 'recent_project',
|
||||
request.user.project_id)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ProjectSwitchByIDView(ProjectSwitchView):
|
||||
def _get_token_auth_plugin_kwargs(self, request, project, domain=None):
|
||||
return {'project_id': project}
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ProjectSwitchByNameView(ProjectSwitchView):
|
||||
def _get_token_auth_plugin_kwargs(self, request, project, domain=None):
|
||||
current_domain = request.user.token.project['domain_id']
|
||||
return {
|
||||
'project_name': project,
|
||||
'project_domain_id': domain or current_domain,
|
||||
}
|
||||
|
||||
|
||||
# TODO(stephenfin): Migrate to CBV
|
||||
|
||||
@@ -28,7 +28,7 @@ from openstack_dashboard.usage import quotas
|
||||
class RescopeTokenToProject(tables.LinkAction):
|
||||
name = "rescope"
|
||||
verbose_name = _("Set as Active Project")
|
||||
url = "switch_tenants"
|
||||
url = "switch_project_id"
|
||||
|
||||
def allowed(self, request, project):
|
||||
# allow rescoping token to any project the user has a role on,
|
||||
@@ -39,7 +39,7 @@ class RescopeTokenToProject(tables.LinkAction):
|
||||
project.enabled), False)
|
||||
|
||||
def get_link_url(self, project):
|
||||
# redirects to the switch_tenants url which then will redirect
|
||||
# redirects to the switch_project_id url which then will redirect
|
||||
# back to this page
|
||||
dash_url = reverse("horizon:identity:projects:index")
|
||||
base_url = reverse(self.url, args=[project.id])
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% for project in projects %}
|
||||
<li>
|
||||
<a class="{% if project.enabled and project.id == project_id %} dropdown-selected{% endif %}"
|
||||
href="{% url 'switch_tenants' project.id %}{% if page_url %}?next={{ page_url }}{% endif %}"
|
||||
href="{% url 'switch_project_id' project.id %}{% if page_url %}?next={{ page_url }}{% endif %}"
|
||||
target="_self">
|
||||
<span class="fa fa-check dropdown-selected-icon"></span>
|
||||
<span class="dropdown-title">
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added a new URL endpoint ``/auth/switch_name/<project-name>`` to Horizon that can
|
||||
be used to switch to another project by name, if ``domain_id`` query parameter
|
||||
is set that domain ID is used otherwise the domain for the current project scoped
|
||||
token is used.
|
||||
|
||||
This is the same functionality as is already provided by the ``/auth/switch/<project-id>``
|
||||
endpoint that only works with an project ID (UUID).
|
||||
Reference in New Issue
Block a user