Use keystoneclient for authentication.
- This allows us to delegate all 2.0 authentication directly to the library without reimplementing ourselves. - Support reusing a token / storage-url without re-authenticating every time via the switch os_storage_url os_auth_token. - Allow auth via tenant_id instead of just tenant_name via the switch os_tenant_id. - Refactor a bit to make it easier in the future to add new OS features (i.e: region). - Implements blueprint use-keystoneclient-for-swiftclient. - Fixes bug 1016641. Change-Id: I532f38a68af884de25326aaac05a2050f5ffa1c7
This commit is contained in:
parent
c2a3fc56fc
commit
c8163f4112
57
bin/swift
57
bin/swift
@ -38,9 +38,9 @@ def get_conn(options):
|
||||
return Connection(options.auth,
|
||||
options.user,
|
||||
options.key,
|
||||
snet=options.snet,
|
||||
tenant_name=options.os_tenant_name,
|
||||
auth_version=options.auth_version)
|
||||
auth_version=options.auth_version,
|
||||
os_options=options.os_options,
|
||||
snet=options.snet)
|
||||
|
||||
|
||||
def mkdirs(path):
|
||||
@ -991,13 +991,6 @@ def parse_args(parser, args, enforce_requires=True):
|
||||
# Use 2.0 auth if none of the old args are present
|
||||
options.auth_version = '2.0'
|
||||
|
||||
if options.auth_version in ('2.0', '2') and not \
|
||||
options.os_tenant_name and options.user and \
|
||||
':' in options.user:
|
||||
(options.os_tenant_name,
|
||||
options.os_username) = options.user.split(':')
|
||||
options.user = options.os_username
|
||||
|
||||
# Use new-style args if old ones not present
|
||||
if not options.auth and options.os_auth_url:
|
||||
options.auth = options.os_auth_url
|
||||
@ -1006,6 +999,15 @@ def parse_args(parser, args, enforce_requires=True):
|
||||
if not options.key and options.os_password:
|
||||
options.key = options.os_password
|
||||
|
||||
# Specific OpenStack options
|
||||
options.os_options = {
|
||||
'tenant_id': options.os_tenant_id,
|
||||
'tenant_name': options.os_tenant_name,
|
||||
'service_type': options.os_service_type,
|
||||
'auth_token': options.os_auth_token,
|
||||
'object_storage_url': options.os_storage_url,
|
||||
}
|
||||
|
||||
# Handle trailing '/' in URL
|
||||
if options.auth and not options.auth.endswith('/'):
|
||||
options.auth += '/'
|
||||
@ -1017,8 +1019,8 @@ Auth version 1.0 requires ST_AUTH, ST_USER, and ST_KEY environment variables
|
||||
to be set or overridden with -A, -U, or -K.
|
||||
|
||||
Auth version 2.0 requires OS_AUTH_URL, OS_USERNAME, OS_PASSWORD, and
|
||||
OS_TENANT_NAME to be set or overridden with --os_auth_url, --os_username,
|
||||
--os_password, or --os_tenant_name.'''.strip('\n'))
|
||||
OS_TENANT_NAME OS_TENANT_ID to be set or overridden with --os-auth_url,
|
||||
--os_username, --os_password, --os_tenant_name or os_tenant_id.'''.strip('\n'))
|
||||
return options, args
|
||||
|
||||
|
||||
@ -1051,26 +1053,43 @@ Example:
|
||||
default=environ.get('ST_AUTH_VERSION', '1.0'),
|
||||
type=str,
|
||||
help='Specify a version for authentication'\
|
||||
'(default: 1.0)')
|
||||
'(default: 1.0)')
|
||||
parser.add_option('-U', '--user', dest='user',
|
||||
default=environ.get('ST_USER'),
|
||||
help='User name for obtaining an auth token')
|
||||
parser.add_option('-K', '--key', dest='key',
|
||||
default=environ.get('ST_KEY'),
|
||||
help='Key for obtaining an auth token')
|
||||
parser.add_option('--os_auth_url', dest='os_auth_url',
|
||||
default=environ.get('OS_AUTH_URL'),
|
||||
help='Openstack auth URL. Defaults to env[OS_AUTH_URL].')
|
||||
parser.add_option('--os_username', dest='os_username',
|
||||
default=environ.get('OS_USERNAME'),
|
||||
help='Openstack username. Defaults to env[OS_USERNAME].')
|
||||
parser.add_option('--os_password', dest='os_password',
|
||||
default=environ.get('OS_PASSWORD'),
|
||||
help='Openstack password. Defaults to env[OS_PASSWORD].')
|
||||
parser.add_option('--os_tenant_id',
|
||||
default=environ.get('OS_TENANT_ID'),
|
||||
help='OpenStack tenant ID.' \
|
||||
'Defaults to env[OS_TENANT_ID]')
|
||||
parser.add_option('--os_tenant_name', dest='os_tenant_name',
|
||||
default=environ.get('OS_TENANT_NAME'),
|
||||
help='Openstack tenant name.' \
|
||||
'Defaults to env[OS_TENANT_NAME].')
|
||||
parser.add_option('--os_password', dest='os_password',
|
||||
default=environ.get('OS_PASSWORD'),
|
||||
help='Openstack password. Defaults to env[OS_PASSWORD].')
|
||||
parser.add_option('--os_auth_url', dest='os_auth_url',
|
||||
default=environ.get('OS_AUTH_URL'),
|
||||
help='Openstack auth URL. Defaults to env[OS_AUTH_URL].')
|
||||
parser.add_option('--os_auth_token', dest='os_auth_token',
|
||||
default=environ.get('OS_AUTH_TOKEN'),
|
||||
help='Openstack token. Defaults to env[OS_AUTH_TOKEN]')
|
||||
parser.add_option('--os_storage_url',
|
||||
dest='os_storage_url',
|
||||
default=environ.get('OS_STORAGE_URL'),
|
||||
help='Openstack storage URL.' \
|
||||
'Defaults to env[OS_STORAGE_URL]')
|
||||
parser.add_option('--os_service_type',
|
||||
dest='os_service_type',
|
||||
default=environ.get('OS_SERVICE_TYPE'),
|
||||
help='Openstack Service type.' \
|
||||
'Defaults to env[OS_SERVICE_TYPE]')
|
||||
parser.disable_interspersed_args()
|
||||
(options, args) = parse_args(parser, argv[1:], enforce_requires=False)
|
||||
parser.enable_interspersed_args()
|
||||
|
@ -20,10 +20,9 @@ Cloud Files client library used internally
|
||||
import socket
|
||||
import os
|
||||
import logging
|
||||
import httplib
|
||||
|
||||
from urllib import quote as _quote
|
||||
from urlparse import urlparse, urlunparse, urljoin
|
||||
from urlparse import urlparse, urlunparse
|
||||
|
||||
try:
|
||||
from eventlet.green.httplib import HTTPException, HTTPSConnection
|
||||
@ -202,7 +201,7 @@ def json_request(method, url, **kwargs):
|
||||
return resp, body
|
||||
|
||||
|
||||
def _get_auth_v1_0(url, user, key, snet):
|
||||
def get_auth_1_0(url, user, key, snet):
|
||||
parsed, conn = http_connection(url)
|
||||
method = 'GET'
|
||||
conn.request(method, parsed.path, '',
|
||||
@ -230,36 +229,26 @@ def _get_auth_v1_0(url, user, key, snet):
|
||||
resp.getheader('x-auth-token'))
|
||||
|
||||
|
||||
def _get_auth_v2_0(url, user, tenant_name, key, snet):
|
||||
body = {'auth':
|
||||
{'passwordCredentials': {'password': key, 'username': user},
|
||||
'tenantName': tenant_name}}
|
||||
token_url = urljoin(url, "tokens")
|
||||
resp, body = json_request("POST", token_url, body=body)
|
||||
token_id = None
|
||||
try:
|
||||
url = None
|
||||
catalogs = body['access']['serviceCatalog']
|
||||
for service in catalogs:
|
||||
if service['type'] == 'object-store':
|
||||
url = service['endpoints'][0]['publicURL']
|
||||
token_id = body['access']['token']['id']
|
||||
if not url:
|
||||
raise ClientException("There is no object-store endpoint "
|
||||
"on this auth server.")
|
||||
except(KeyError, IndexError):
|
||||
raise ClientException("Error while getting answers from auth server")
|
||||
def get_keystoneclient_2_0(auth_url, user, key, os_options):
|
||||
"""
|
||||
Authenticate against a auth 2.0 server.
|
||||
|
||||
if snet:
|
||||
parsed = list(urlparse(url))
|
||||
# Second item in the list is the netloc
|
||||
parsed[1] = 'snet-' + parsed[1]
|
||||
url = urlunparse(parsed)
|
||||
|
||||
return url, token_id
|
||||
We are using the keystoneclient library for our 2.0 authentication.
|
||||
"""
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
_ksclient = ksclient.Client(username=user,
|
||||
password=key,
|
||||
tenant_name=os_options.get('tenant_name'),
|
||||
tenant_id=os_options.get('tenant_id'),
|
||||
auth_url=auth_url)
|
||||
service_type = os_options.get('service_type') or 'object-store'
|
||||
endpoint = _ksclient.service_catalog.url_for(
|
||||
service_type=service_type,
|
||||
endpoint_type='publicURL')
|
||||
return (endpoint, _ksclient.auth_token)
|
||||
|
||||
|
||||
def get_auth(url, user, key, snet=False, tenant_name=None, auth_version="1.0"):
|
||||
def get_auth(auth_url, user, key, **kwargs):
|
||||
"""
|
||||
Get authentication/authorization credentials.
|
||||
|
||||
@ -268,28 +257,45 @@ def get_auth(url, user, key, snet=False, tenant_name=None, auth_version="1.0"):
|
||||
of the host name for the returned storage URL. With Rackspace Cloud Files,
|
||||
use of this network path causes no bandwidth charges but requires the
|
||||
client to be running on Rackspace's ServiceNet network.
|
||||
|
||||
:param url: authentication/authorization URL
|
||||
:param user: user to authenticate as
|
||||
:param key: key or password for authorization
|
||||
:param snet: use SERVICENET internal network (see above), default is False
|
||||
:param auth_version: OpenStack auth version, default is 1.0
|
||||
:param tenant_name: The tenant/account name, required when connecting
|
||||
to a auth 2.0 system.
|
||||
:returns: tuple of (storage URL, auth token)
|
||||
:raises: ClientException: HTTP GET request to auth URL failed
|
||||
"""
|
||||
if auth_version in ["1.0", "1"]:
|
||||
return _get_auth_v1_0(url, user, key, snet)
|
||||
elif auth_version in ["2.0", "2"]:
|
||||
if not tenant_name and ':' in user:
|
||||
(tenant_name, user) = user.split(':')
|
||||
if not tenant_name:
|
||||
auth_version = kwargs.get('auth_version', '1')
|
||||
|
||||
if auth_version in ['1.0', '1', 1]:
|
||||
return get_auth_1_0(auth_url,
|
||||
user,
|
||||
key,
|
||||
kwargs.get('snet'))
|
||||
|
||||
if auth_version in ['2.0', '2', 2]:
|
||||
|
||||
# We are allowing to specify a token/storage-url to re-use
|
||||
# without having to re-authenticate.
|
||||
if (kwargs['os_options'].get('object_storage_url') and
|
||||
kwargs['os_options'].get('auth_token')):
|
||||
return(kwargs['os_options'].get('object_storage_url'),
|
||||
kwargs['os_options'].get('auth_token'))
|
||||
|
||||
# We are handling a special use case here when we were
|
||||
# allowing specifying the account/tenant_name with the -U
|
||||
# argument
|
||||
if not kwargs.get('tenant_name') and ':' in user:
|
||||
(kwargs['os_options']['tenant_name'],
|
||||
user) = user.split(':')
|
||||
|
||||
# We are allowing to have an tenant_name argument in get_auth
|
||||
# directly without having os_options
|
||||
if kwargs.get('tenant_name'):
|
||||
kwargs['os_options']['tenant_name'] = kwargs['tenant_name']
|
||||
|
||||
if (not 'tenant_name' in kwargs['os_options']):
|
||||
raise ClientException('No tenant specified')
|
||||
return _get_auth_v2_0(url, user, tenant_name, key, snet)
|
||||
else:
|
||||
raise ClientException('Unknown auth_version %s specified.'
|
||||
% auth_version)
|
||||
|
||||
(auth_url, token) = get_keystoneclient_2_0(auth_url, user,
|
||||
key, kwargs['os_options'])
|
||||
return (auth_url, token)
|
||||
|
||||
raise ClientException('Unknown auth_version %s specified.'
|
||||
% auth_version)
|
||||
|
||||
|
||||
def get_account(url, token, marker=None, limit=None, prefix=None,
|
||||
@ -898,8 +904,7 @@ class Connection(object):
|
||||
|
||||
def __init__(self, authurl, user, key, retries=5, preauthurl=None,
|
||||
preauthtoken=None, snet=False, starting_backoff=1,
|
||||
tenant_name=None,
|
||||
auth_version="1"):
|
||||
tenant_name=None, os_options={}, auth_version="1"):
|
||||
"""
|
||||
:param authurl: authentication URL
|
||||
:param user: user name to authenticate as
|
||||
@ -912,6 +917,9 @@ class Connection(object):
|
||||
:param auth_version: OpenStack auth version, default is 1.0
|
||||
:param tenant_name: The tenant/account name, required when connecting
|
||||
to a auth 2.0 system.
|
||||
:param os_options: The OpenStack options which can have tenant_id,
|
||||
auth_token, service_type, tenant_name,
|
||||
object_storage_url
|
||||
"""
|
||||
self.authurl = authurl
|
||||
self.user = user
|
||||
@ -924,13 +932,17 @@ class Connection(object):
|
||||
self.snet = snet
|
||||
self.starting_backoff = starting_backoff
|
||||
self.auth_version = auth_version
|
||||
self.tenant_name = tenant_name
|
||||
if tenant_name:
|
||||
os_options['tenant_name'] = tenant_name
|
||||
self.os_options = os_options
|
||||
|
||||
def get_auth(self):
|
||||
return get_auth(self.authurl, self.user,
|
||||
self.key, snet=self.snet,
|
||||
tenant_name=self.tenant_name,
|
||||
auth_version=self.auth_version)
|
||||
return get_auth(self.authurl,
|
||||
self.user,
|
||||
self.key,
|
||||
snet=self.snet,
|
||||
auth_version=self.auth_version,
|
||||
os_options=self.os_options)
|
||||
|
||||
def http_connection(self):
|
||||
return http_connection(self.url)
|
||||
|
@ -19,7 +19,7 @@ import unittest
|
||||
from urlparse import urlparse
|
||||
|
||||
# TODO: mock http connection class with more control over headers
|
||||
from utils import fake_http_connect
|
||||
from utils import fake_http_connect, fake_get_keystoneclient_2_0
|
||||
|
||||
from swiftclient import client as c
|
||||
|
||||
@ -175,42 +175,33 @@ class TestGetAuth(MockHttpTest):
|
||||
self.assertEquals(token, None)
|
||||
|
||||
def test_auth_v2(self):
|
||||
def read(*args, **kwargs):
|
||||
acct_url = 'http://127.0.01/AUTH_FOO'
|
||||
body = {'access': {'serviceCatalog':
|
||||
[{u'endpoints': [{'publicURL': acct_url}],
|
||||
'type': 'object-store'}],
|
||||
'token': {'id': 'XXXXXXX'}}}
|
||||
return c.json_dumps(body)
|
||||
c.http_connection = self.fake_http_connection(200, return_read=read)
|
||||
c.get_keystoneclient_2_0 = fake_get_keystoneclient_2_0
|
||||
url, token = c.get_auth('http://www.test.com', 'asdf', 'asdf',
|
||||
tenant_name='asdf', auth_version="2.0")
|
||||
os_options={'tenant_name': 'asdf'},
|
||||
auth_version="2.0")
|
||||
self.assertTrue(url.startswith("http"))
|
||||
self.assertTrue(token)
|
||||
|
||||
def test_auth_v2_no_tenant_name(self):
|
||||
def read(*args, **kwargs):
|
||||
acct_url = 'http://127.0.01/AUTH_FOO'
|
||||
body = {'access': {'serviceCatalog':
|
||||
[{u'endpoints': [{'publicURL': acct_url}],
|
||||
'type': 'object-store'}],
|
||||
'token': {'id': 'XXXXXXX'}}}
|
||||
return c.json_dumps(body)
|
||||
c.http_connection = self.fake_http_connection(200, return_read=read)
|
||||
c.get_keystoneclient_2_0 = fake_get_keystoneclient_2_0
|
||||
self.assertRaises(c.ClientException, c.get_auth,
|
||||
'http://www.tests.com', 'asdf', 'asdf',
|
||||
os_options={},
|
||||
auth_version='2.0')
|
||||
|
||||
def test_auth_v2_with_tenant_user_in_user(self):
|
||||
def read(*args, **kwargs):
|
||||
acct_url = 'http://127.0.01/AUTH_FOO'
|
||||
body = {'access': {'serviceCatalog':
|
||||
[{u'endpoints': [{'publicURL': acct_url}],
|
||||
'type': 'object-store'}],
|
||||
'token': {'id': 'XXXXXXX'}}}
|
||||
return c.json_dumps(body)
|
||||
c.http_connection = self.fake_http_connection(200, return_read=read)
|
||||
c.get_keystoneclient_2_0 = fake_get_keystoneclient_2_0
|
||||
url, token = c.get_auth('http://www.test.com', 'foo:bar', 'asdf',
|
||||
os_options={},
|
||||
auth_version="2.0")
|
||||
self.assertTrue(url.startswith("http"))
|
||||
self.assertTrue(token)
|
||||
|
||||
def test_auth_v2_tenant_name_no_os_options(self):
|
||||
c.get_keystoneclient_2_0 = fake_get_keystoneclient_2_0
|
||||
url, token = c.get_auth('http://www.test.com', 'asdf', 'asdf',
|
||||
tenant_name='asdf',
|
||||
os_options={},
|
||||
auth_version="2.0")
|
||||
self.assertTrue(url.startswith("http"))
|
||||
self.assertTrue(token)
|
||||
|
@ -17,6 +17,14 @@ from httplib import HTTPException
|
||||
from eventlet import Timeout, sleep
|
||||
|
||||
|
||||
def fake_get_keystoneclient_2_0(auth_url,
|
||||
username,
|
||||
tenant_name,
|
||||
password,
|
||||
service_type='object-store'):
|
||||
return ("http://url/", "token")
|
||||
|
||||
|
||||
def fake_http_connect(*code_iter, **kwargs):
|
||||
|
||||
class FakeConn(object):
|
||||
|
@ -1 +1,2 @@
|
||||
simplejson
|
||||
python-keystoneclient
|
||||
|
Loading…
x
Reference in New Issue
Block a user