Add system scope support to context switcher
Change-Id: Idd2ec7ae6e978a358b4b3639e86cadae06c90976
This commit is contained in:
parent
f9bab3fe19
commit
34a0159d1a
@ -233,3 +233,28 @@ class BasePlugin(object, metaclass=abc.ABCMeta):
|
|||||||
unscoped_auth_ref.user_id, _name)
|
unscoped_auth_ref.user_id, _name)
|
||||||
break
|
break
|
||||||
return domain_auth, domain_auth_ref
|
return domain_auth, domain_auth_ref
|
||||||
|
|
||||||
|
def get_system_scoped_auth(self, unscoped_auth, unscoped_auth_ref,
|
||||||
|
system_scope):
|
||||||
|
"""Get the system scoped keystone auth and access info
|
||||||
|
|
||||||
|
This function returns a system scoped keystone token plugin
|
||||||
|
and AccessInfo object.
|
||||||
|
|
||||||
|
:param unscoped_auth: keystone auth plugin
|
||||||
|
:param unscoped_auth_ref: keystoneclient.access.AccessInfo` or None.
|
||||||
|
:param system_scope: system that we should try to scope to
|
||||||
|
:return: keystone token auth plugin, AccessInfo object
|
||||||
|
"""
|
||||||
|
session = utils.get_session()
|
||||||
|
auth_url = unscoped_auth.auth_url
|
||||||
|
|
||||||
|
system_auth = None
|
||||||
|
system_auth_ref = None
|
||||||
|
token = unscoped_auth_ref.auth_token
|
||||||
|
system_auth = utils.get_token_auth_plugin(
|
||||||
|
auth_url,
|
||||||
|
token,
|
||||||
|
system_scope=system_scope)
|
||||||
|
system_auth_ref = system_auth.get_access(session)
|
||||||
|
return system_auth, system_auth_ref
|
||||||
|
@ -1382,6 +1382,54 @@ class OpenStackAuthTests(test.TestCase):
|
|||||||
def test_switch_region_with_next(self, next=None):
|
def test_switch_region_with_next(self, next=None):
|
||||||
self.test_switch_region(next='/next_url')
|
self.test_switch_region(next='/next_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_system_scope(self, mock_get_access, mock_project_list,
|
||||||
|
mock_get_access_token,
|
||||||
|
next=None):
|
||||||
|
projects = []
|
||||||
|
user = self.data.user
|
||||||
|
scoped = self.data.unscoped_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
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.assertFalse(self.client.session['token'].system_scoped)
|
||||||
|
|
||||||
|
url = reverse('switch_system_scope')
|
||||||
|
|
||||||
|
if next:
|
||||||
|
form_data.update({auth.REDIRECT_FIELD_NAME: next})
|
||||||
|
|
||||||
|
response = self.client.get(url, form_data)
|
||||||
|
|
||||||
|
if next:
|
||||||
|
expected_url = next
|
||||||
|
self.assertEqual(response['location'], expected_url)
|
||||||
|
else:
|
||||||
|
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||||
|
|
||||||
|
self.assertNotEqual(False, self.client.session['token'].system_scoped)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class OpenStackAuthTestsPublicURL(OpenStackAuthTests):
|
class OpenStackAuthTestsPublicURL(OpenStackAuthTests):
|
||||||
interface = 'publicURL'
|
interface = 'publicURL'
|
||||||
|
@ -30,6 +30,9 @@ urlpatterns = [
|
|||||||
url(r'^switch_keystone_provider/(?P<keystone_provider>[^/]+)/$',
|
url(r'^switch_keystone_provider/(?P<keystone_provider>[^/]+)/$',
|
||||||
views.switch_keystone_provider,
|
views.switch_keystone_provider,
|
||||||
name='switch_keystone_provider'),
|
name='switch_keystone_provider'),
|
||||||
|
url(r'^switch_system_scope/$',
|
||||||
|
views.switch_system_scope,
|
||||||
|
name='switch_system_scope'),
|
||||||
]
|
]
|
||||||
|
|
||||||
if utils.allow_expired_passowrd_change():
|
if utils.allow_expired_passowrd_change():
|
||||||
|
@ -61,6 +61,7 @@ def create_user_from_token(request, token, endpoint, services_region=None):
|
|||||||
project_name=token.project['name'],
|
project_name=token.project['name'],
|
||||||
domain_id=token.domain['id'],
|
domain_id=token.domain['id'],
|
||||||
domain_name=token.domain['name'],
|
domain_name=token.domain['name'],
|
||||||
|
system_scoped=token.system_scoped,
|
||||||
enabled=True,
|
enabled=True,
|
||||||
service_catalog=token.serviceCatalog,
|
service_catalog=token.serviceCatalog,
|
||||||
roles=token.roles,
|
roles=token.roles,
|
||||||
@ -117,6 +118,10 @@ class Token(object):
|
|||||||
self.roles = [{'name': role} for role in auth_ref.role_names]
|
self.roles = [{'name': role} for role in auth_ref.role_names]
|
||||||
self.serviceCatalog = auth_ref.service_catalog.catalog
|
self.serviceCatalog = auth_ref.service_catalog.catalog
|
||||||
|
|
||||||
|
# System scope
|
||||||
|
# Only keystone API V3 has it.
|
||||||
|
self.system_scoped = getattr(auth_ref, 'system_scoped', False)
|
||||||
|
|
||||||
|
|
||||||
class User(models.AbstractBaseUser, models.AnonymousUser):
|
class User(models.AbstractBaseUser, models.AnonymousUser):
|
||||||
"""A User class with some extra special sauce for Keystone.
|
"""A User class with some extra special sauce for Keystone.
|
||||||
@ -200,7 +205,8 @@ class User(models.AbstractBaseUser, models.AnonymousUser):
|
|||||||
services_region=None, user_domain_id=None,
|
services_region=None, user_domain_id=None,
|
||||||
user_domain_name=None, domain_id=None, domain_name=None,
|
user_domain_name=None, domain_id=None, domain_name=None,
|
||||||
project_id=None, project_name=None, is_federated=False,
|
project_id=None, project_name=None, is_federated=False,
|
||||||
unscoped_token=None, password=None, password_expires_at=None):
|
unscoped_token=None, password=None, password_expires_at=None,
|
||||||
|
system_scoped=False):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.pk = id
|
self.pk = id
|
||||||
self.token = token
|
self.token = token
|
||||||
@ -212,6 +218,7 @@ class User(models.AbstractBaseUser, models.AnonymousUser):
|
|||||||
self.domain_name = domain_name
|
self.domain_name = domain_name
|
||||||
self.project_id = project_id or tenant_id
|
self.project_id = project_id or tenant_id
|
||||||
self.project_name = project_name or tenant_name
|
self.project_name = project_name or tenant_name
|
||||||
|
self.system_scoped = system_scoped
|
||||||
self.service_catalog = service_catalog
|
self.service_catalog = service_catalog
|
||||||
self._services_region = (
|
self._services_region = (
|
||||||
services_region or
|
services_region or
|
||||||
@ -223,6 +230,7 @@ class User(models.AbstractBaseUser, models.AnonymousUser):
|
|||||||
self._authorized_tenants = authorized_tenants
|
self._authorized_tenants = authorized_tenants
|
||||||
self.is_federated = is_federated
|
self.is_federated = is_federated
|
||||||
self.password_expires_at = password_expires_at
|
self.password_expires_at = password_expires_at
|
||||||
|
self._is_system_user = None
|
||||||
|
|
||||||
# Unscoped token is used for listing user's project that works
|
# Unscoped token is used for listing user's project that works
|
||||||
# for both federated and keystone user.
|
# for both federated and keystone user.
|
||||||
@ -330,6 +338,22 @@ class User(models.AbstractBaseUser, models.AnonymousUser):
|
|||||||
regions.append(region)
|
regions.append(region)
|
||||||
return regions
|
return regions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_system_user(self):
|
||||||
|
"""Check if the user has access to the system scope."""
|
||||||
|
if self._is_system_user is not None:
|
||||||
|
return self._is_system_user
|
||||||
|
try:
|
||||||
|
self._is_system_user = utils.get_system_access(
|
||||||
|
user_id=self.id,
|
||||||
|
auth_url=self.endpoint,
|
||||||
|
token=self.unscoped_token,
|
||||||
|
is_federated=self.is_federated)
|
||||||
|
except (keystone_exceptions.ClientException,
|
||||||
|
keystone_exceptions.AuthorizationFailure):
|
||||||
|
LOG.exception('Unable to retrieve systems list.')
|
||||||
|
return self._is_system_user
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# Presume we can't write to Keystone.
|
# Presume we can't write to Keystone.
|
||||||
pass
|
pass
|
||||||
|
@ -20,6 +20,7 @@ from django.conf import settings
|
|||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.contrib.auth import models
|
from django.contrib.auth import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from keystoneauth1 import exceptions as keystone_exceptions
|
||||||
from keystoneauth1.identity import v3 as v3_auth
|
from keystoneauth1.identity import v3 as v3_auth
|
||||||
from keystoneauth1 import session
|
from keystoneauth1 import session
|
||||||
from keystoneauth1 import token_endpoint
|
from keystoneauth1 import token_endpoint
|
||||||
@ -294,7 +295,13 @@ def clean_up_auth_url(auth_url):
|
|||||||
scheme, netloc, re.sub(r'/auth.*', '', path), '', ''))
|
scheme, netloc, re.sub(r'/auth.*', '', path), '', ''))
|
||||||
|
|
||||||
|
|
||||||
def get_token_auth_plugin(auth_url, token, project_id=None, domain_name=None):
|
def get_token_auth_plugin(auth_url, token, project_id=None, domain_name=None,
|
||||||
|
system_scope=None):
|
||||||
|
if system_scope:
|
||||||
|
return v3_auth.Token(auth_url=auth_url,
|
||||||
|
token=token,
|
||||||
|
system_scope=system_scope,
|
||||||
|
reauthenticate=False)
|
||||||
if domain_name:
|
if domain_name:
|
||||||
return v3_auth.Token(auth_url=auth_url,
|
return v3_auth.Token(auth_url=auth_url,
|
||||||
token=token,
|
token=token,
|
||||||
@ -322,6 +329,25 @@ def get_project_list(*args, **kwargs):
|
|||||||
return projects
|
return projects
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_access(user_id, auth_url, token, is_federated):
|
||||||
|
session = get_session()
|
||||||
|
auth_url, _ = fix_auth_url_version_prefix(auth_url)
|
||||||
|
auth = token_endpoint.Token(auth_url, token)
|
||||||
|
client = get_keystone_client().Client(session=session, auth=auth)
|
||||||
|
# Old versions of keystoneclient don't have auth.system endpoint yet.
|
||||||
|
auth_system = getattr(client.auth, 'system', None)
|
||||||
|
if auth_system is not None:
|
||||||
|
return 'all' in auth_system()
|
||||||
|
# Fall back to trying to get the system scope token.
|
||||||
|
try:
|
||||||
|
auth = get_token_auth_plugin(auth_url=auth_url, token=token,
|
||||||
|
system_scope='all')
|
||||||
|
auth.get_access(session)
|
||||||
|
except keystone_exceptions.ClientException:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def default_services_region(service_catalog, request=None,
|
def default_services_region(service_catalog, request=None,
|
||||||
ks_endpoint=None):
|
ks_endpoint=None):
|
||||||
"""Return the default service region.
|
"""Return the default service region.
|
||||||
|
@ -400,6 +400,55 @@ def switch_keystone_provider(request, keystone_provider=None,
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# TODO(stephenfin): Migrate to CBV
|
||||||
|
@login_required
|
||||||
|
def switch_system_scope(request, redirect_field_name=auth.REDIRECT_FIELD_NAME):
|
||||||
|
"""Switches an authenticated user from one system to another."""
|
||||||
|
LOG.debug('Switching to system scope for user "%s".', request.user.username)
|
||||||
|
|
||||||
|
endpoint, __ = utils.fix_auth_url_version_prefix(request.user.endpoint)
|
||||||
|
session = utils.get_session()
|
||||||
|
# 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,
|
||||||
|
system_scope='all')
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth_ref = auth.get_access(session)
|
||||||
|
except keystone_exceptions.ClientException:
|
||||||
|
msg = (
|
||||||
|
_('System 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.')
|
||||||
|
else:
|
||||||
|
msg = 'System switch successful for user "%(username)s".' % \
|
||||||
|
{'username': request.user.username}
|
||||||
|
LOG.info(msg)
|
||||||
|
|
||||||
|
# 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.is_safe_url(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 system scope successful.')
|
||||||
|
messages.success(request, message)
|
||||||
|
response = shortcuts.redirect(redirect_to)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class PasswordView(edit_views.FormView):
|
class PasswordView(edit_views.FormView):
|
||||||
"""Changes user's password when it's expired or otherwise inaccessible."""
|
"""Changes user's password when it's expired or otherwise inaccessible."""
|
||||||
template_name = 'auth/password.html'
|
template_name = 'auth/password.html'
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
<span class="fa fa-list-alt"></span>
|
<span class="fa fa-list-alt"></span>
|
||||||
<span class="context-overview">
|
<span class="context-overview">
|
||||||
{% if domain_supported %}
|
{% if domain_supported %}
|
||||||
<span class="context-domain">{{ domain_name }}</span>
|
<span class="context-domain">{{ domain_name }}</span>
|
||||||
<span class="fa fa-circle context-delimiter"></span>
|
<span class="fa fa-circle context-delimiter"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if project_name %}
|
||||||
<span class="context-project">{{ project_name }}</span>
|
<span class="context-project">{{ project_name }}</span>
|
||||||
|
{% endif %}
|
||||||
{% if multi_region %}
|
{% if multi_region %}
|
||||||
<span class="fa fa-circle context-delimiter"></span>
|
<span class="fa fa-circle context-delimiter"></span>
|
||||||
<span class="context-region">{{ region_name }}</span>
|
<span class="context-region">{{ region_name }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if system_scoped %}
|
||||||
|
<span class="context-system">{% trans "system scope" %}</span>
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li class="dropdown-header">{% trans "Systems:" %}</li>
|
||||||
|
<li>
|
||||||
|
<a class="{% if system_scoped %}dropdown-selected{% endif %}"
|
||||||
|
href="{% url 'switch_system_scope' %}{% if page_url %}?next={{ page_url }}{% endif %}"
|
||||||
|
target="_self">
|
||||||
|
<span class="fa fa-check dropdown-selected-icon"></span>
|
||||||
|
<span class="dropdown-title">{% trans "system scope" %}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
@ -29,5 +29,13 @@
|
|||||||
{% show_region_list %}
|
{% show_region_list %}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% is_system_user as system_user %}
|
||||||
|
{% if system_user %}
|
||||||
|
<li>
|
||||||
|
{% show_system_list %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
@ -41,6 +41,15 @@ def is_multidomain():
|
|||||||
return is_multidomain_supported()
|
return is_multidomain_supported()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def is_system_user(context):
|
||||||
|
try:
|
||||||
|
request = context['request']
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
return request.user.is_system_user
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('context_selection/_overview.html',
|
@register.inclusion_tag('context_selection/_overview.html',
|
||||||
takes_context=True)
|
takes_context=True)
|
||||||
def show_overview(context):
|
def show_overview(context):
|
||||||
@ -55,6 +64,7 @@ def show_overview(context):
|
|||||||
'project_name': project_name or request.user.project_name,
|
'project_name': project_name or request.user.project_name,
|
||||||
'multi_region': is_multi_region_configured(request),
|
'multi_region': is_multi_region_configured(request),
|
||||||
'region_name': request.user.services_region,
|
'region_name': request.user.services_region,
|
||||||
|
'system_scoped': request.user.system_scoped,
|
||||||
'request': request}
|
'request': request}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
@ -102,6 +112,20 @@ def show_region_list(context):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('context_selection/_system_list.html',
|
||||||
|
takes_context=True)
|
||||||
|
def show_system_list(context):
|
||||||
|
if 'request' not in context:
|
||||||
|
return {}
|
||||||
|
request = context['request']
|
||||||
|
panel = request.horizon.get('panel')
|
||||||
|
context = {
|
||||||
|
'system_scoped': request.user.system_scoped,
|
||||||
|
'page_url': panel.get_absolute_url() if panel else None,
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('context_selection/_anti_clickjack.html',
|
@register.inclusion_tag('context_selection/_anti_clickjack.html',
|
||||||
takes_context=True)
|
takes_context=True)
|
||||||
def iframe_embed_settings(context):
|
def iframe_embed_settings(context):
|
||||||
|
@ -261,7 +261,8 @@ class TestCase(horizon_helpers.TestCase):
|
|||||||
authorized_tenants=None, enabled=True, domain_id=None,
|
authorized_tenants=None, enabled=True, domain_id=None,
|
||||||
user_domain_name=None):
|
user_domain_name=None):
|
||||||
def get_user(request):
|
def get_user(request):
|
||||||
return user.User(id=id,
|
ret = user.User(
|
||||||
|
id=id,
|
||||||
token=token,
|
token=token,
|
||||||
user=username,
|
user=username,
|
||||||
domain_id=domain_id,
|
domain_id=domain_id,
|
||||||
@ -272,7 +273,10 @@ class TestCase(horizon_helpers.TestCase):
|
|||||||
roles=roles,
|
roles=roles,
|
||||||
enabled=enabled,
|
enabled=enabled,
|
||||||
authorized_tenants=authorized_tenants,
|
authorized_tenants=authorized_tenants,
|
||||||
endpoint=settings.OPENSTACK_KEYSTONE_URL)
|
endpoint=settings.OPENSTACK_KEYSTONE_URL,
|
||||||
|
)
|
||||||
|
ret._is_system_user = False
|
||||||
|
return ret
|
||||||
utils.get_user = get_user
|
utils.get_user = get_user
|
||||||
|
|
||||||
def assertRedirectsNoFollow(self, response, expected_url):
|
def assertRedirectsNoFollow(self, response, expected_url):
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
A new entry has been added to the context switcher menu, visible only
|
||||||
|
when the current user has access to the system scope. This entry, labeled
|
||||||
|
"system scope", allows to switch to a system-scope token, so that operations
|
||||||
|
that require this kind of token can be performed.
|
Loading…
Reference in New Issue
Block a user