Support for keystone auth plugins

This patch allows the user to choose which authentication plugin
to use with the CLI. The arguments needed by the auth plugins are
automatically added to the argument parser. Some examples with
the currently available authentication plugins::

  OS_USERNAME=admin OS_PROJECT_NAME=admin OS_AUTH_URL=http://keystone:5000/v2.0 \
  OS_PASSWORD=admin openstack user list

  OS_USERNAME=admin OS_PROJECT_DOMAIN_NAME=default OS_USER_DOMAIN_NAME=default \
  OS_PROJECT_NAME=admin OS_AUTH_URL=http://keystone:5000/v3 OS_PASSWORD=admin \
  OS_IDENTITY_API_VERSION=3 OS_AUTH_PLUGIN=v3password openstack project list

  OS_TOKEN=1234 OS_URL=http://service_url:35357/v2.0 \
  OS_IDENTITY_API_VERSION=2.0 openstack user list

The --os-auth-plugin option can be omitted; if so the CLI will attempt to
guess which plugin to use from the other options.

Change-Id: I330c20ddb8d96b3a4287c68b57c36c4a0f869669
Co-Authored-By: Florent Flament <florent.flament-ext@cloudwatt.com>
This commit is contained in:
Matthieu Huin 2014-07-18 19:18:25 +02:00
parent 866965f011
commit 0c77a9fe8b
9 changed files with 613 additions and 304 deletions

View File

@ -21,6 +21,10 @@ DESCRIPTION
equivalent to the CLIs provided by the OpenStack project client libraries, but with equivalent to the CLIs provided by the OpenStack project client libraries, but with
a distinct and consistent command structure. a distinct and consistent command structure.
AUTHENTICATION METHODS
======================
:program:`openstack` uses a similar authentication scheme as the OpenStack project CLIs, with :program:`openstack` uses a similar authentication scheme as the OpenStack project CLIs, with
the credential information supplied either as environment variables or as options on the the credential information supplied either as environment variables or as options on the
command line. The primary difference is the use of 'project' in the name of the options command line. The primary difference is the use of 'project' in the name of the options
@ -33,6 +37,15 @@ command line. The primary difference is the use of 'project' in the name of the
export OS_USERNAME=<user-name> export OS_USERNAME=<user-name>
export OS_PASSWORD=<password> # (optional) export OS_PASSWORD=<password> # (optional)
:program:`openstack` can use different types of authentication plugins provided by the keystoneclient library. The following default plugins are available:
* ``token``: Authentication with a token
* ``password``: Authentication with a username and a password
Refer to the keystoneclient library documentation for more details about these plugins and their options, and for a complete list of available plugins.
Please bear in mind that some plugins might not support all of the functionalities of :program:`openstack`; for example the v3unscopedsaml plugin can deliver only unscoped tokens, some commands might not be available through this authentication method.
Additionally, it is possible to use Keystone's service token to authenticate, by setting the options :option:`--os-token` and :option:`--os-url` (or the environment variables :envvar:`OS_TOKEN` and :envvar:`OS_URL` respectively). This method takes precedence over authentication plugins.
OPTIONS OPTIONS
======= =======
@ -41,9 +54,16 @@ OPTIONS
:program:`openstack` recognizes the following global topions: :program:`openstack` recognizes the following global topions:
:option:`--os-auth-plugin` <auth-plugin>
The authentication plugin to use when connecting to the Identity service. If this option is not set, :program:`openstack` will attempt to guess the authentication method to use based on the other options.
If this option is set, its version must match :option:`--os-identity-api-version`
:option:`--os-auth-url` <auth-url> :option:`--os-auth-url` <auth-url>
Authentication URL Authentication URL
:option:`--os-url` <service-url>
Service URL, when using a service token for authentication
:option:`--os-domain-name` <auth-domain-name> | :option:`--os-domain-id` <auth-domain-id> :option:`--os-domain-name` <auth-domain-name> | :option:`--os-domain-id` <auth-domain-id>
Domain-level authorization scope (name or ID) Domain-level authorization scope (name or ID)
@ -59,6 +79,9 @@ OPTIONS
:option:`--os-password` <auth-password> :option:`--os-password` <auth-password>
Authentication password Authentication password
:option:`--os-token` <token>
Authenticated token or service token
:option:`--os-user-domain-name` <auth-user-domain-name> | :option:`--os-user-domain-id` <auth-user-domain-id> :option:`--os-user-domain-name` <auth-user-domain-name> | :option:`--os-user-domain-id` <auth-user-domain-id>
Domain name or id containing user Domain name or id containing user
@ -86,6 +109,7 @@ OPTIONS
:option:`--os-XXXX-api-version` <XXXX-api-version> :option:`--os-XXXX-api-version` <XXXX-api-version>
Additional API version options will be available depending on the installed API libraries. Additional API version options will be available depending on the installed API libraries.
COMMANDS COMMANDS
======== ========
@ -174,9 +198,15 @@ ENVIRONMENT VARIABLES
The following environment variables can be set to alter the behaviour of :program:`openstack`. Most of them have corresponding command-line options that take precedence if set. The following environment variables can be set to alter the behaviour of :program:`openstack`. Most of them have corresponding command-line options that take precedence if set.
:envvar:`OS_AUTH_PLUGIN`
The authentication plugin to use when connecting to the Identity service, its version must match the Identity API version
:envvar:`OS_AUTH_URL` :envvar:`OS_AUTH_URL`
Authentication URL Authentication URL
:envvar:`OS_URL`
Service URL (when using the service token)
:envvar:`OS_DOMAIN_NAME` :envvar:`OS_DOMAIN_NAME`
Domain-level authorization scope (name or ID) Domain-level authorization scope (name or ID)
@ -189,6 +219,9 @@ The following environment variables can be set to alter the behaviour of :progra
:envvar:`OS_USERNAME` :envvar:`OS_USERNAME`
Authentication username Authentication username
:envvar:`OS_TOKEN`
Authenticated or service token
:envvar:`OS_PASSWORD` :envvar:`OS_PASSWORD`
Authentication password Authentication password
@ -213,6 +246,7 @@ The following environment variables can be set to alter the behaviour of :progra
:envvar:`OS_XXXX_API_VERSION` :envvar:`OS_XXXX_API_VERSION`
Additional API version options will be available depending on the installed API libraries. Additional API version options will be available depending on the installed API libraries.
BUGS BUGS
==== ====

180
openstackclient/api/auth.py Normal file
View File

@ -0,0 +1,180 @@
# 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.
#
"""Authentication Library"""
import argparse
import logging
import stevedore
from keystoneclient.auth import base
from openstackclient.common import exceptions as exc
from openstackclient.common import utils
LOG = logging.getLogger(__name__)
# Initialize the list of Authentication plugins early in order
# to get the command-line options
PLUGIN_LIST = stevedore.ExtensionManager(
base.PLUGIN_NAMESPACE,
invoke_on_load=False,
propagate_map_exceptions=True,
)
# TODO(dtroyer): add some method to list the plugins for the
# --os_auth_plugin option
# Get the command line options so the help action has them available
OPTIONS_LIST = {}
for plugin in PLUGIN_LIST:
for o in plugin.plugin.get_options():
os_name = o.dest.lower().replace('_', '-')
os_env_name = 'OS_' + os_name.upper().replace('-', '_')
OPTIONS_LIST.setdefault(os_name, {'env': os_env_name, 'help': ''})
# TODO(mhu) simplistic approach, would be better to only add
# help texts if they vary from one auth plugin to another
# also the text rendering is ugly in the CLI ...
OPTIONS_LIST[os_name]['help'] += 'With %s: %s\n' % (
plugin.name,
o.help,
)
def _guess_authentication_method(options):
"""If no auth plugin was specified, pick one based on other options"""
if options.os_url:
# service token authentication, do nothing
return
auth_plugin = None
if options.os_password:
if options.os_identity_api_version == '3':
auth_plugin = 'v3password'
elif options.os_identity_api_version == '2.0':
auth_plugin = 'v2password'
else:
# let keystoneclient figure it out itself
auth_plugin = 'password'
elif options.os_token:
if options.os_identity_api_version == '3':
auth_plugin = 'v3token'
elif options.os_identity_api_version == '2.0':
auth_plugin = 'v2token'
else:
# let keystoneclient figure it out itself
auth_plugin = 'token'
else:
raise exc.CommandError(
"Could not figure out which authentication method "
"to use, please set --os-auth-plugin"
)
LOG.debug("No auth plugin selected, picking %s from other "
"options" % auth_plugin)
options.os_auth_plugin = auth_plugin
def build_auth_params(cmd_options):
auth_params = {}
if cmd_options.os_url:
return {'token': cmd_options.os_token}
if cmd_options.os_auth_plugin:
auth_plugin = base.get_plugin_class(cmd_options.os_auth_plugin)
plugin_options = auth_plugin.get_options()
for option in plugin_options:
option_name = 'os_' + option.dest
LOG.debug('fetching option %s' % option_name)
auth_params[option.dest] = getattr(cmd_options, option_name, None)
# grab tenant from project for v2.0 API compatibility
if cmd_options.os_auth_plugin.startswith("v2"):
auth_params['tenant_id'] = getattr(
cmd_options,
'os_project_id',
None,
)
auth_params['tenant_name'] = getattr(
cmd_options,
'os_project_name',
None,
)
else:
# delay the plugin choice, grab every option
plugin_options = set([o.replace('-', '_') for o in OPTIONS_LIST])
for option in plugin_options:
option_name = 'os_' + option
LOG.debug('fetching option %s' % option_name)
auth_params[option] = getattr(cmd_options, option_name, None)
return auth_params
def build_auth_plugins_option_parser(parser):
"""Auth plugins options builder
Builds dynamically the list of options expected by each available
authentication plugin.
"""
available_plugins = [plugin.name for plugin in PLUGIN_LIST]
parser.add_argument(
'--os-auth-plugin',
metavar='<OS_AUTH_PLUGIN>',
default=utils.env('OS_AUTH_PLUGIN'),
help='The authentication method to use. If this option is not set, '
'openstackclient will attempt to guess the authentication method '
'to use based on the other options. If this option is set, '
'the --os-identity-api-version argument must be consistent '
'with the version of the method.\nAvailable methods are ' +
', '.join(available_plugins),
choices=available_plugins
)
# make sur we catch old v2.0 env values
envs = {
'OS_PROJECT_NAME': utils.env(
'OS_PROJECT_NAME',
default=utils.env('OS_TENANT_NAME')
),
'OS_PROJECT_ID': utils.env(
'OS_PROJECT_ID',
default=utils.env('OS_TENANT_ID')
),
}
for o in OPTIONS_LIST:
# remove allusion to tenants from v2.0 API
if 'tenant' not in o:
parser.add_argument(
'--os-' + o,
metavar='<auth-%s>' % o,
default=envs.get(OPTIONS_LIST[o]['env'],
utils.env(OPTIONS_LIST[o]['env'])),
help='%s\n(Env: %s)' % (OPTIONS_LIST[o]['help'],
OPTIONS_LIST[o]['env']),
)
# add tenant-related options for compatibility
# this is deprecated but still used in some tempest tests...
parser.add_argument(
'--os-tenant-name',
metavar='<auth-tenant-name>',
dest='os_project_name',
default=utils.env('OS_TENANT_NAME'),
help=argparse.SUPPRESS,
)
parser.add_argument(
'--os-tenant-id',
metavar='<auth-tenant-id>',
dest='os_project_id',
default=utils.env('OS_TENANT_ID'),
help=argparse.SUPPRESS,
)
return parser

View File

@ -19,9 +19,11 @@ import logging
import pkg_resources import pkg_resources
import sys import sys
from keystoneclient.auth.identity import v2 as v2_auth from keystoneclient.auth import base
from keystoneclient.auth.identity import v3 as v3_auth
from keystoneclient import session from keystoneclient import session
import requests
from openstackclient.api import auth
from openstackclient.identity import client as identity_client from openstackclient.identity import client as identity_client
@ -45,105 +47,66 @@ class ClientManager(object):
"""Manages access to API clients, including authentication.""" """Manages access to API clients, including authentication."""
identity = ClientCache(identity_client.make_client) identity = ClientCache(identity_client.make_client)
def __init__(self, token=None, url=None, auth_url=None, def __getattr__(self, name):
domain_id=None, domain_name=None, # this is for the auth-related parameters.
project_name=None, project_id=None, if name in ['_' + o.replace('-', '_')
username=None, password=None, for o in auth.OPTIONS_LIST]:
user_domain_id=None, user_domain_name=None, return self._auth_params[name[1:]]
project_domain_id=None, project_domain_name=None,
region_name=None, api_version=None, verify=True, def __init__(self, auth_options, api_version=None, verify=True):
trust_id=None, timing=None):
self._token = token if not auth_options.os_auth_plugin:
self._url = url auth._guess_authentication_method(auth_options)
self._auth_url = auth_url
self._domain_id = domain_id self._auth_plugin = auth_options.os_auth_plugin
self._domain_name = domain_name self._url = auth_options.os_url
self._project_name = project_name self._auth_params = auth.build_auth_params(auth_options)
self._project_id = project_id self._region_name = auth_options.os_region_name
self._username = username
self._password = password
self._user_domain_id = user_domain_id
self._user_domain_name = user_domain_name
self._project_domain_id = project_domain_id
self._project_domain_name = project_domain_name
self._region_name = region_name
self._api_version = api_version self._api_version = api_version
self._trust_id = trust_id
self._service_catalog = None self._service_catalog = None
self.timing = timing self.timing = auth_options.timing
# For compatability until all clients can be updated
if 'project_name' in self._auth_params:
self._project_name = self._auth_params['project_name']
elif 'tenant_name' in self._auth_params:
self._project_name = self._auth_params['tenant_name']
# verify is the Requests-compatible form # verify is the Requests-compatible form
self._verify = verify self._verify = verify
# also store in the form used by the legacy client libs # also store in the form used by the legacy client libs
self._cacert = None self._cacert = None
if verify is True or verify is False: if isinstance(verify, bool):
self._insecure = not verify self._insecure = not verify
else: else:
self._cacert = verify self._cacert = verify
self._insecure = False self._insecure = False
ver_prefix = identity_client.AUTH_VERSIONS[
self._api_version[identity_client.API_NAME]
]
# Get logging from root logger # Get logging from root logger
root_logger = logging.getLogger('') root_logger = logging.getLogger('')
LOG.setLevel(root_logger.getEffectiveLevel()) LOG.setLevel(root_logger.getEffectiveLevel())
# NOTE(dtroyer): These plugins are hard-coded for the first step self.session = None
# in using the new Keystone auth plugins. if not self._url:
LOG.debug('Using auth plugin: %s' % self._auth_plugin)
if self._url: auth_plugin = base.get_plugin_class(self._auth_plugin)
LOG.debug('Using token auth %s', ver_prefix) self.auth = auth_plugin.load_from_options(**self._auth_params)
if ver_prefix == 'v2': # needed by SAML authentication
self.auth = v2_auth.Token( request_session = requests.session()
auth_url=url, self.session = session.Session(
token=token, auth=self.auth,
) session=request_session,
else: verify=verify,
self.auth = v3_auth.Token( )
auth_url=url,
token=token,
)
else:
LOG.debug('Using password auth %s', ver_prefix)
if ver_prefix == 'v2':
self.auth = v2_auth.Password(
auth_url=auth_url,
username=username,
password=password,
trust_id=trust_id,
tenant_id=project_id,
tenant_name=project_name,
)
else:
self.auth = v3_auth.Password(
auth_url=auth_url,
username=username,
password=password,
trust_id=trust_id,
user_domain_id=user_domain_id,
user_domain_name=user_domain_name,
domain_id=domain_id,
domain_name=domain_name,
project_id=project_id,
project_name=project_name,
project_domain_id=project_domain_id,
project_domain_name=project_domain_name,
)
self.session = session.Session(
auth=self.auth,
verify=verify,
)
self.auth_ref = None self.auth_ref = None
if not self._url: if not self._auth_plugin.endswith("token") and not self._url:
# Trigger the auth call LOG.debug("Populate other password flow attributes")
self.auth_ref = self.session.auth.get_auth_ref(self.session) self.auth_ref = self.session.auth.get_auth_ref(self.session)
# Populate other password flow attributes
self._token = self.session.auth.get_token(self.session) self._token = self.session.auth.get_token(self.session)
self._service_catalog = self.auth_ref.service_catalog self._service_catalog = self.auth_ref.service_catalog
else:
self._token = self._auth_params.get('token')
return return
@ -156,7 +119,7 @@ class ClientManager(object):
service_type=service_type) service_type=service_type)
else: else:
# Hope we were given the correct URL. # Hope we were given the correct URL.
endpoint = self._url endpoint = self._auth_url or self._url
return endpoint return endpoint

View File

@ -16,9 +16,9 @@
import logging import logging
from keystoneclient.v2_0 import client as identity_client_v2_0 from keystoneclient.v2_0 import client as identity_client_v2_0
from openstackclient.api import auth
from openstackclient.common import utils from openstackclient.common import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
DEFAULT_IDENTITY_API_VERSION = '2.0' DEFAULT_IDENTITY_API_VERSION = '2.0'
@ -47,16 +47,15 @@ def make_client(instance):
# TODO(dtroyer): Something doesn't like the session.auth when using # TODO(dtroyer): Something doesn't like the session.auth when using
# token auth, chase that down. # token auth, chase that down.
if instance._url: if instance._url:
LOG.debug('Using token auth') LOG.debug('Using service token auth')
client = identity_client( client = identity_client(
endpoint=instance._url, endpoint=instance._url,
token=instance._token, token=instance._auth_params['token'],
cacert=instance._cacert, cacert=instance._cacert,
insecure=instance._insecure, insecure=instance._insecure
trust_id=instance._trust_id,
) )
else: else:
LOG.debug('Using password auth') LOG.debug('Using auth plugin: %s' % instance._auth_plugin)
client = identity_client( client = identity_client(
session=instance.session, session=instance.session,
cacert=instance._cacert, cacert=instance._cacert,
@ -66,7 +65,6 @@ def make_client(instance):
# so we can remove it # so we can remove it
if not instance._url: if not instance._url:
instance.auth_ref = instance.auth.get_auth_ref(instance.session) instance.auth_ref = instance.auth.get_auth_ref(instance.session)
return client return client
@ -81,14 +79,7 @@ def build_option_parser(parser):
help='Identity API version, default=' + help='Identity API version, default=' +
DEFAULT_IDENTITY_API_VERSION + DEFAULT_IDENTITY_API_VERSION +
' (Env: OS_IDENTITY_API_VERSION)') ' (Env: OS_IDENTITY_API_VERSION)')
parser.add_argument( return auth.build_auth_plugins_option_parser(parser)
'--os-trust-id',
metavar='<trust-id>',
default=utils.env('OS_TRUST_ID'),
help='Trust ID to use when authenticating. '
'This can only be used with Keystone v3 API '
'(Env: OS_TRUST_ID)')
return parser
class IdentityClientv2_0(identity_client_v2_0.Client): class IdentityClientv2_0(identity_client_v2_0.Client):

View File

@ -15,7 +15,6 @@
"""Command-line interface to the OpenStack APIs""" """Command-line interface to the OpenStack APIs"""
import argparse
import getpass import getpass
import logging import logging
import sys import sys
@ -171,89 +170,13 @@ class OpenStackShell(app.App):
parser = super(OpenStackShell, self).build_option_parser( parser = super(OpenStackShell, self).build_option_parser(
description, description,
version) version)
# service token auth argument
parser.add_argument(
'--os-url',
metavar='<url>',
default=utils.env('OS_URL'),
help='Defaults to env[OS_URL]')
# Global arguments # Global arguments
parser.add_argument(
'--os-auth-url',
metavar='<auth-url>',
default=utils.env('OS_AUTH_URL'),
help='Authentication URL (Env: OS_AUTH_URL)')
parser.add_argument(
'--os-domain-name',
metavar='<auth-domain-name>',
default=utils.env('OS_DOMAIN_NAME'),
help='Domain name of the requested domain-level '
'authorization scope (Env: OS_DOMAIN_NAME)',
)
parser.add_argument(
'--os-domain-id',
metavar='<auth-domain-id>',
default=utils.env('OS_DOMAIN_ID'),
help='Domain ID of the requested domain-level '
'authorization scope (Env: OS_DOMAIN_ID)',
)
parser.add_argument(
'--os-project-name',
metavar='<auth-project-name>',
default=utils.env('OS_PROJECT_NAME',
default=utils.env('OS_TENANT_NAME')),
help='Project name of the requested project-level '
'authorization scope (Env: OS_PROJECT_NAME)',
)
parser.add_argument(
'--os-tenant-name',
metavar='<auth-tenant-name>',
dest='os_project_name',
help=argparse.SUPPRESS,
)
parser.add_argument(
'--os-project-id',
metavar='<auth-project-id>',
default=utils.env('OS_PROJECT_ID',
default=utils.env('OS_TENANT_ID')),
help='Project ID of the requested project-level '
'authorization scope (Env: OS_PROJECT_ID)',
)
parser.add_argument(
'--os-tenant-id',
metavar='<auth-tenant-id>',
dest='os_project_id',
help=argparse.SUPPRESS,
)
parser.add_argument(
'--os-username',
metavar='<auth-username>',
default=utils.env('OS_USERNAME'),
help='Authentication username (Env: OS_USERNAME)')
parser.add_argument(
'--os-password',
metavar='<auth-password>',
default=utils.env('OS_PASSWORD'),
help='Authentication password (Env: OS_PASSWORD)')
parser.add_argument(
'--os-user-domain-name',
metavar='<auth-user-domain-name>',
default=utils.env('OS_USER_DOMAIN_NAME'),
help='Domain name of the user (Env: OS_USER_DOMAIN_NAME)')
parser.add_argument(
'--os-user-domain-id',
metavar='<auth-user-domain-id>',
default=utils.env('OS_USER_DOMAIN_ID'),
help='Domain ID of the user (Env: OS_USER_DOMAIN_ID)')
parser.add_argument(
'--os-project-domain-name',
metavar='<auth-project-domain-name>',
default=utils.env('OS_PROJECT_DOMAIN_NAME'),
help='Domain name of the project which is the requested '
'project-level authorization scope '
'(Env: OS_PROJECT_DOMAIN_NAME)')
parser.add_argument(
'--os-project-domain-id',
metavar='<auth-project-domain-id>',
default=utils.env('OS_PROJECT_DOMAIN_ID'),
help='Domain ID of the project which is the requested '
'project-level authorization scope '
'(Env: OS_PROJECT_DOMAIN_ID)')
parser.add_argument( parser.add_argument(
'--os-region-name', '--os-region-name',
metavar='<auth-region-name>', metavar='<auth-region-name>',
@ -284,16 +207,6 @@ class OpenStackShell(app.App):
help='Default domain ID, default=' + help='Default domain ID, default=' +
DEFAULT_DOMAIN + DEFAULT_DOMAIN +
' (Env: OS_DEFAULT_DOMAIN)') ' (Env: OS_DEFAULT_DOMAIN)')
parser.add_argument(
'--os-token',
metavar='<token>',
default=utils.env('OS_TOKEN'),
help='Defaults to env[OS_TOKEN]')
parser.add_argument(
'--os-url',
metavar='<url>',
default=utils.env('OS_URL'),
help='Defaults to env[OS_URL]')
parser.add_argument( parser.add_argument(
'--timing', '--timing',
default=False, default=False,
@ -306,20 +219,42 @@ class OpenStackShell(app.App):
def authenticate_user(self): def authenticate_user(self):
"""Verify the required authentication credentials are present""" """Verify the required authentication credentials are present"""
self.log.debug('validating authentication options') self.log.debug("validating authentication options")
if self.options.os_token or self.options.os_url:
# Assuming all auth plugins will be named in the same fashion,
# ie vXpluginName
if (not self.options.os_url and
self.options.os_auth_plugin.startswith('v') and
self.options.os_auth_plugin[1] !=
self.options.os_identity_api_version[0]):
raise exc.CommandError(
"Auth plugin %s not compatible"
" with requested API version" % self.options.os_auth_plugin
)
# TODO(mhu) All these checks should be exposed at the plugin level
# or just dropped altogether, as the client instantiation will fail
# anyway
if self.options.os_url and not self.options.os_token:
# service token needed
raise exc.CommandError(
"You must provide a service token via"
" either --os-token or env[OS_TOKEN]")
if (self.options.os_auth_plugin.endswith('token') and
(self.options.os_token or self.options.os_auth_url)):
# Token flow auth takes priority # Token flow auth takes priority
if not self.options.os_token: if not self.options.os_token:
raise exc.CommandError( raise exc.CommandError(
"You must provide a token via" "You must provide a token via"
" either --os-token or env[OS_TOKEN]") " either --os-token or env[OS_TOKEN]")
if not self.options.os_url: if not self.options.os_auth_url:
raise exc.CommandError( raise exc.CommandError(
"You must provide a service URL via" "You must provide a service URL via"
" either --os-url or env[OS_URL]") " either --os-auth-url or env[OS_AUTH_URL]")
else: if (not self.options.os_url and
not self.options.os_auth_plugin.endswith('token')):
# Validate password flow auth # Validate password flow auth
if not self.options.os_username: if not self.options.os_username:
raise exc.CommandError( raise exc.CommandError(
@ -347,13 +282,15 @@ class OpenStackShell(app.App):
(self.options.os_domain_id (self.options.os_domain_id
or self.options.os_domain_name) or or self.options.os_domain_name) or
self.options.os_trust_id): self.options.os_trust_id):
raise exc.CommandError( if self.options.os_auth_plugin.endswith('password'):
"You must provide authentication scope as a project " raise exc.CommandError(
"or a domain via --os-project-id or env[OS_PROJECT_ID], " "You must provide authentication scope as a project "
"--os-project-name or env[OS_PROJECT_NAME], " "or a domain via --os-project-id "
"--os-domain-id or env[OS_DOMAIN_ID], or" "or env[OS_PROJECT_ID], "
"--os-domain-name or env[OS_DOMAIN_NAME], or " "--os-project-name or env[OS_PROJECT_NAME], "
"--os-trust-id or env[OS_TRUST_ID].") "--os-domain-id or env[OS_DOMAIN_ID], or"
"--os-domain-name or env[OS_DOMAIN_NAME], or "
"--os-trust-id or env[OS_TRUST_ID].")
if not self.options.os_auth_url: if not self.options.os_auth_url:
raise exc.CommandError( raise exc.CommandError(
@ -375,24 +312,9 @@ class OpenStackShell(app.App):
"Pick one of project, domain or trust.") "Pick one of project, domain or trust.")
self.client_manager = clientmanager.ClientManager( self.client_manager = clientmanager.ClientManager(
token=self.options.os_token, auth_options=self.options,
url=self.options.os_url,
auth_url=self.options.os_auth_url,
domain_id=self.options.os_domain_id,
domain_name=self.options.os_domain_name,
project_name=self.options.os_project_name,
project_id=self.options.os_project_id,
user_domain_id=self.options.os_user_domain_id,
user_domain_name=self.options.os_user_domain_name,
project_domain_id=self.options.os_project_domain_id,
project_domain_name=self.options.os_project_domain_name,
username=self.options.os_username,
password=self.options.os_password,
region_name=self.options.os_region_name,
verify=self.verify, verify=self.verify,
timing=self.options.timing,
api_version=self.api_version, api_version=self.api_version,
trust_id=self.options.os_trust_id,
) )
return return

View File

@ -12,34 +12,25 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
# #
import mock import mock
from requests_mock.contrib import fixture
from keystoneclient.auth.identity import v2 as auth_v2 from keystoneclient.auth.identity import v2 as auth_v2
from keystoneclient.openstack.common import jsonutils
from keystoneclient import service_catalog
from openstackclient.api import auth
from openstackclient.common import clientmanager from openstackclient.common import clientmanager
from openstackclient.common import exceptions as exc
from openstackclient.tests import fakes
from openstackclient.tests import utils from openstackclient.tests import utils
AUTH_REF = {'a': 1} API_VERSION = {"identity": "2.0"}
AUTH_TOKEN = "foobar"
AUTH_URL = "http://0.0.0.0"
USERNAME = "itchy"
PASSWORD = "scratchy"
SERVICE_CATALOG = {'sc': '123'}
API_VERSION = { AUTH_REF = {'version': 'v2.0'}
'identity': '2.0', AUTH_REF.update(fakes.TEST_RESPONSE_DICT['access'])
} SERVICE_CATALOG = service_catalog.ServiceCatalogV2(AUTH_REF)
def FakeMakeClient(instance):
return FakeClient()
class FakeClient(object):
auth_ref = AUTH_REF
auth_token = AUTH_TOKEN
service_catalog = SERVICE_CATALOG
class Container(object): class Container(object):
@ -49,6 +40,18 @@ class Container(object):
pass pass
class FakeOptions(object):
def __init__(self, **kwargs):
for option in auth.OPTIONS_LIST:
setattr(self, 'os_' + option.replace('-', '_'), None)
self.os_auth_plugin = None
self.os_identity_api_version = '2.0'
self.timing = None
self.os_region_name = None
self.os_url = None
self.__dict__.update(kwargs)
class TestClientCache(utils.TestCase): class TestClientCache(utils.TestCase):
def test_singleton(self): def test_singleton(self):
@ -58,30 +61,38 @@ class TestClientCache(utils.TestCase):
self.assertEqual(c.attr, c.attr) self.assertEqual(c.attr, c.attr)
@mock.patch('keystoneclient.session.Session')
class TestClientManager(utils.TestCase): class TestClientManager(utils.TestCase):
def setUp(self): def setUp(self):
super(TestClientManager, self).setUp() super(TestClientManager, self).setUp()
self.mock = mock.Mock()
self.requests = self.useFixture(fixture.Fixture())
# fake v2password token retrieval
self.stub_auth(json=fakes.TEST_RESPONSE_DICT)
# fake v3password token retrieval
self.stub_auth(json=fakes.TEST_RESPONSE_DICT_V3,
url='/'.join([fakes.AUTH_URL, 'auth/tokens']))
# fake password version endpoint discovery
self.stub_auth(json=fakes.TEST_VERSIONS,
url=fakes.AUTH_URL,
verb='GET')
clientmanager.ClientManager.identity = \ def test_client_manager_token(self):
clientmanager.ClientCache(FakeMakeClient)
def test_client_manager_token(self, mock):
client_manager = clientmanager.ClientManager( client_manager = clientmanager.ClientManager(
token=AUTH_TOKEN, auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN,
url=AUTH_URL, os_auth_url=fakes.AUTH_URL,
verify=True, os_auth_plugin='v2token'),
api_version=API_VERSION, api_version=API_VERSION,
verify=True
) )
self.assertEqual( self.assertEqual(
AUTH_TOKEN, fakes.AUTH_TOKEN,
client_manager._token, client_manager._token,
) )
self.assertEqual( self.assertEqual(
AUTH_URL, fakes.AUTH_URL,
client_manager._url, client_manager._auth_url,
) )
self.assertIsInstance( self.assertIsInstance(
client_manager.auth, client_manager.auth,
@ -90,26 +101,26 @@ class TestClientManager(utils.TestCase):
self.assertFalse(client_manager._insecure) self.assertFalse(client_manager._insecure)
self.assertTrue(client_manager._verify) self.assertTrue(client_manager._verify)
def test_client_manager_password(self, mock): def test_client_manager_password(self):
client_manager = clientmanager.ClientManager( client_manager = clientmanager.ClientManager(
auth_url=AUTH_URL, auth_options=FakeOptions(os_auth_url=fakes.AUTH_URL,
username=USERNAME, os_username=fakes.USERNAME,
password=PASSWORD, os_password=fakes.PASSWORD),
verify=False,
api_version=API_VERSION, api_version=API_VERSION,
verify=False,
) )
self.assertEqual( self.assertEqual(
AUTH_URL, fakes.AUTH_URL,
client_manager._auth_url, client_manager._auth_url,
) )
self.assertEqual( self.assertEqual(
USERNAME, fakes.USERNAME,
client_manager._username, client_manager._username,
) )
self.assertEqual( self.assertEqual(
PASSWORD, fakes.PASSWORD,
client_manager._password, client_manager._password,
) )
self.assertIsInstance( self.assertIsInstance(
@ -119,16 +130,87 @@ class TestClientManager(utils.TestCase):
self.assertTrue(client_manager._insecure) self.assertTrue(client_manager._insecure)
self.assertFalse(client_manager._verify) self.assertFalse(client_manager._verify)
def test_client_manager_password_verify_ca(self, mock): # These need to stick around until the old-style clients are gone
self.assertEqual(
AUTH_REF,
client_manager.auth_ref,
)
self.assertEqual(
fakes.AUTH_TOKEN,
client_manager._token,
)
self.assertEqual(
dir(SERVICE_CATALOG),
dir(client_manager._service_catalog),
)
def stub_auth(self, json=None, url=None, verb=None, **kwargs):
subject_token = fakes.AUTH_TOKEN
base_url = fakes.AUTH_URL
if json:
text = jsonutils.dumps(json)
headers = {'X-Subject-Token': subject_token,
'Content-Type': 'application/json'}
if not url:
url = '/'.join([base_url, 'tokens'])
url = url.replace("/?", "?")
if not verb:
verb = 'POST'
self.requests.register_uri(verb,
url,
headers=headers,
text=text)
def test_client_manager_password_verify_ca(self):
client_manager = clientmanager.ClientManager( client_manager = clientmanager.ClientManager(
auth_url=AUTH_URL, auth_options=FakeOptions(os_auth_url=fakes.AUTH_URL,
username=USERNAME, os_username=fakes.USERNAME,
password=PASSWORD, os_password=fakes.PASSWORD,
verify='cafile', os_auth_plugin='v2password'),
api_version=API_VERSION, api_version=API_VERSION,
verify='cafile',
) )
self.assertFalse(client_manager._insecure) self.assertFalse(client_manager._insecure)
self.assertTrue(client_manager._verify) self.assertTrue(client_manager._verify)
self.assertEqual('cafile', client_manager._cacert) self.assertEqual('cafile', client_manager._cacert)
def _client_manager_guess_auth_plugin(self, auth_params,
api_version, auth_plugin):
auth_params['os_auth_plugin'] = auth_plugin
auth_params['os_identity_api_version'] = api_version
client_manager = clientmanager.ClientManager(
auth_options=FakeOptions(**auth_params),
api_version=API_VERSION,
verify=True
)
self.assertEqual(
auth_plugin,
client_manager._auth_plugin,
)
def test_client_manager_guess_auth_plugin(self):
# test token auth
params = dict(os_token=fakes.AUTH_TOKEN,
os_auth_url=fakes.AUTH_URL)
self._client_manager_guess_auth_plugin(params, '2.0', 'v2token')
self._client_manager_guess_auth_plugin(params, '3', 'v3token')
self._client_manager_guess_auth_plugin(params, 'XXX', 'token')
# test service auth
params = dict(os_token=fakes.AUTH_TOKEN, os_url='test')
self._client_manager_guess_auth_plugin(params, 'XXX', '')
# test password auth
params = dict(os_auth_url=fakes.AUTH_URL,
os_username=fakes.USERNAME,
os_password=fakes.PASSWORD)
self._client_manager_guess_auth_plugin(params, '2.0', 'v2password')
self._client_manager_guess_auth_plugin(params, '3', 'v3password')
self._client_manager_guess_auth_plugin(params, 'XXX', 'password')
def test_client_manager_guess_auth_plugin_failure(self):
self.assertRaises(exc.CommandError,
clientmanager.ClientManager,
auth_options=FakeOptions(os_auth_plugin=''),
api_version=API_VERSION,
verify=True)

View File

@ -22,6 +22,142 @@ import requests
AUTH_TOKEN = "foobar" AUTH_TOKEN = "foobar"
AUTH_URL = "http://0.0.0.0" AUTH_URL = "http://0.0.0.0"
USERNAME = "itchy"
PASSWORD = "scratchy"
TEST_RESPONSE_DICT = {
"access": {
"metadata": {
"is_admin": 0,
"roles": [
"1234",
]
},
"serviceCatalog": [
{
"endpoints": [
{
"adminURL": AUTH_URL + "/v2.0",
"id": "1234",
"internalURL": AUTH_URL + "/v2.0",
"publicURL": AUTH_URL + "/v2.0",
"region": "RegionOne"
}
],
"endpoints_links": [],
"name": "keystone",
"type": "identity"
}
],
"token": {
"expires": "2035-01-01T00:00:01Z",
"id": AUTH_TOKEN,
"issued_at": "2013-01-01T00:00:01.692048",
"tenant": {
"description": None,
"enabled": True,
"id": "1234",
"name": "testtenant"
}
},
"user": {
"id": "5678",
"name": USERNAME,
"roles": [
{
"name": "testrole"
},
],
"roles_links": [],
"username": USERNAME
}
}
}
TEST_RESPONSE_DICT_V3 = {
"token": {
"audit_ids": [
"a"
],
"catalog": [
],
"expires_at": "2034-09-29T18:27:15.978064Z",
"extras": {},
"issued_at": "2014-09-29T17:27:15.978097Z",
"methods": [
"password"
],
"project": {
"domain": {
"id": "default",
"name": "Default"
},
"id": "bbb",
"name": "project"
},
"roles": [
],
"user": {
"domain": {
"id": "default",
"name": "Default"
},
"id": "aaa",
"name": USERNAME
}
}
}
TEST_VERSIONS = {
"versions": {
"values": [
{
"id": "v3.0",
"links": [
{
"href": AUTH_URL,
"rel": "self"
}
],
"media-types": [
{
"base": "application/json",
"type": "application/vnd.openstack.identity-v3+json"
},
{
"base": "application/xml",
"type": "application/vnd.openstack.identity-v3+xml"
}
],
"status": "stable",
"updated": "2013-03-06T00:00:00Z"
},
{
"id": "v2.0",
"links": [
{
"href": AUTH_URL,
"rel": "self"
},
{
"href": "http://docs.openstack.org/",
"rel": "describedby",
"type": "text/html"
}
],
"media-types": [
{
"base": "application/json",
"type": "application/vnd.openstack.identity-v2.0+json"
},
{
"base": "application/xml",
"type": "application/vnd.openstack.identity-v2.0+xml"
}
],
"status": "stable",
"updated": "2014-04-17T00:00:00Z"
}
]
}
}
class FakeStdout: class FakeStdout:

View File

@ -34,6 +34,8 @@ DEFAULT_PASSWORD = "password"
DEFAULT_REGION_NAME = "ZZ9_Plural_Z_Alpha" DEFAULT_REGION_NAME = "ZZ9_Plural_Z_Alpha"
DEFAULT_TOKEN = "token" DEFAULT_TOKEN = "token"
DEFAULT_SERVICE_URL = "http://127.0.0.1:8771/v3.0/" DEFAULT_SERVICE_URL = "http://127.0.0.1:8771/v3.0/"
DEFAULT_AUTH_PLUGIN = "v2password"
DEFAULT_COMPUTE_API_VERSION = "2" DEFAULT_COMPUTE_API_VERSION = "2"
DEFAULT_IDENTITY_API_VERSION = "2.0" DEFAULT_IDENTITY_API_VERSION = "2.0"
@ -106,6 +108,8 @@ class TestShell(utils.TestCase):
default_args["region_name"]) default_args["region_name"])
self.assertEqual(_shell.options.os_trust_id, self.assertEqual(_shell.options.os_trust_id,
default_args["trust_id"]) default_args["trust_id"])
self.assertEqual(_shell.options.os_auth_plugin,
default_args['auth_plugin'])
def _assert_token_auth(self, cmd_options, default_args): def _assert_token_auth(self, cmd_options, default_args):
with mock.patch("openstackclient.shell.OpenStackShell.initialize_app", with mock.patch("openstackclient.shell.OpenStackShell.initialize_app",
@ -115,7 +119,8 @@ class TestShell(utils.TestCase):
self.app.assert_called_with(["list", "role"]) self.app.assert_called_with(["list", "role"])
self.assertEqual(_shell.options.os_token, default_args["os_token"]) self.assertEqual(_shell.options.os_token, default_args["os_token"])
self.assertEqual(_shell.options.os_url, default_args["os_url"]) self.assertEqual(_shell.options.os_auth_url,
default_args["os_auth_url"])
def _assert_cli(self, cmd_options, default_args): def _assert_cli(self, cmd_options, default_args):
with mock.patch("openstackclient.shell.OpenStackShell.initialize_app", with mock.patch("openstackclient.shell.OpenStackShell.initialize_app",
@ -175,9 +180,9 @@ class TestShellPasswordAuth(TestShell):
"auth_url": DEFAULT_AUTH_URL, "auth_url": DEFAULT_AUTH_URL,
"project_id": "", "project_id": "",
"project_name": "", "project_name": "",
"user_domain_id": "",
"domain_id": "", "domain_id": "",
"domain_name": "", "domain_name": "",
"user_domain_id": "",
"user_domain_name": "", "user_domain_name": "",
"project_domain_id": "", "project_domain_id": "",
"project_domain_name": "", "project_domain_name": "",
@ -185,6 +190,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -204,6 +210,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -223,44 +230,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
} "auth_plugin": "",
self._assert_password_auth(flag, kwargs)
def test_only_tenant_id_flow(self):
flag = "--os-tenant-id " + DEFAULT_PROJECT_ID
kwargs = {
"auth_url": "",
"project_id": DEFAULT_PROJECT_ID,
"project_name": "",
"domain_id": "",
"domain_name": "",
"user_domain_id": "",
"user_domain_name": "",
"project_domain_id": "",
"project_domain_name": "",
"username": "",
"password": "",
"region_name": "",
"trust_id": "",
}
self._assert_password_auth(flag, kwargs)
def test_only_tenant_name_flow(self):
flag = "--os-tenant-name " + DEFAULT_PROJECT_NAME
kwargs = {
"auth_url": "",
"project_id": "",
"project_name": DEFAULT_PROJECT_NAME,
"domain_id": "",
"domain_name": "",
"user_domain_id": "",
"user_domain_name": "",
"project_domain_id": "",
"project_domain_name": "",
"username": "",
"password": "",
"region_name": "",
"trust_id": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -280,6 +250,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -299,6 +270,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -318,6 +290,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -337,6 +310,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -356,6 +330,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -375,6 +350,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -394,6 +370,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -413,6 +390,7 @@ class TestShellPasswordAuth(TestShell):
"password": DEFAULT_PASSWORD, "password": DEFAULT_PASSWORD,
"region_name": "", "region_name": "",
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -432,6 +410,7 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": DEFAULT_REGION_NAME, "region_name": DEFAULT_REGION_NAME,
"trust_id": "", "trust_id": "",
"auth_plugin": "",
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -451,6 +430,27 @@ class TestShellPasswordAuth(TestShell):
"password": "", "password": "",
"region_name": "", "region_name": "",
"trust_id": "1234", "trust_id": "1234",
"auth_plugin": "",
}
self._assert_password_auth(flag, kwargs)
def test_only_auth_plugin_flow(self):
flag = "--os-auth-plugin " + "v2password"
kwargs = {
"auth_url": "",
"project_id": "",
"project_name": "",
"domain_id": "",
"domain_name": "",
"user_domain_id": "",
"user_domain_name": "",
"project_domain_id": "",
"project_domain_name": "",
"username": "",
"password": "",
"region_name": "",
"trust_id": "",
"auth_plugin": DEFAULT_AUTH_PLUGIN
} }
self._assert_password_auth(flag, kwargs) self._assert_password_auth(flag, kwargs)
@ -460,7 +460,7 @@ class TestShellTokenAuth(TestShell):
super(TestShellTokenAuth, self).setUp() super(TestShellTokenAuth, self).setUp()
env = { env = {
"OS_TOKEN": DEFAULT_TOKEN, "OS_TOKEN": DEFAULT_TOKEN,
"OS_URL": DEFAULT_SERVICE_URL, "OS_AUTH_URL": DEFAULT_SERVICE_URL,
} }
self.orig_env, os.environ = os.environ, env.copy() self.orig_env, os.environ = os.environ, env.copy()
@ -472,7 +472,7 @@ class TestShellTokenAuth(TestShell):
flag = "" flag = ""
kwargs = { kwargs = {
"os_token": DEFAULT_TOKEN, "os_token": DEFAULT_TOKEN,
"os_url": DEFAULT_SERVICE_URL "os_auth_url": DEFAULT_SERVICE_URL
} }
self._assert_token_auth(flag, kwargs) self._assert_token_auth(flag, kwargs)
@ -481,7 +481,7 @@ class TestShellTokenAuth(TestShell):
flag = "" flag = ""
kwargs = { kwargs = {
"os_token": "", "os_token": "",
"os_url": "" "os_auth_url": ""
} }
self._assert_token_auth(flag, kwargs) self._assert_token_auth(flag, kwargs)

View File

@ -12,3 +12,4 @@ python-cinderclient>=1.1.0
python-neutronclient>=2.3.6,<3 python-neutronclient>=2.3.6,<3
requests>=1.2.1,!=2.4.0 requests>=1.2.1,!=2.4.0
six>=1.7.0 six>=1.7.0
stevedore>=1.0.0