From 3e7b68c71b31802d583d2bae9da4f71b1bcb448d Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 11:07:30 +0200 Subject: [PATCH 01/11] add keystone auth backend --- st2auth/st2auth/backends/keystone.py | 64 ++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 st2auth/st2auth/backends/keystone.py diff --git a/st2auth/st2auth/backends/keystone.py b/st2auth/st2auth/backends/keystone.py new file mode 100644 index 0000000000..a223334f93 --- /dev/null +++ b/st2auth/st2auth/backends/keystone.py @@ -0,0 +1,64 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + + +from st2common import log as logging +from st2auth.backends.base import BaseAuthenticationBackend +import requests + +__all__ = [ + 'KeystoneAuthenticationBackend' +] + +LOG = logging.getLogger(__name__) + + +class KeystoneAuthenticationBackend(BaseAuthenticationBackend): + """ + Backend which reads authentication information from keystone + + Note: This backend depends on the "requests" library. + """ + + def __init__(self, keystone_url, keystone_version=2): + """ + :param keystone_url: Url of the Keystone server to authenticate against. + :type keystone_url: ``str`` + :param keystone_version: Keystone version to authenticate against (default to 2). + :type keystone_version: ``int`` + """ + self._keystone_url = keystone_url + self._keystone_version = keystone_version + + def authenticate(self, username, password): + creds = { + "auth": { + "passwordCredentials": { + "username": username, + "password": password + } + } + } + login = requests.post(self._keystone_url, json=creds) + + if login.status_code == 200: + LOG.debug('Authentication for user "{}" successful'.format(username)) + return True + else: + LOG.debug('Authentication for user "{}" failed: {}'.format(username, login.content)) + return False + + def get_user(self, username): + pass From e58effe2f1210341e6c65ff0ccf07212874f60a0 Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 19:41:10 +0200 Subject: [PATCH 02/11] add support for v2/v3 keystone, add tests with mocking. Hope thye work, tests are not running locally --- st2auth/st2auth/backends/keystone.py | 50 ++++++++++++++++++++---- st2auth/tests/unit/test_auth_backends.py | 29 ++++++++++++++ 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/st2auth/st2auth/backends/keystone.py b/st2auth/st2auth/backends/keystone.py index a223334f93..0c50a51ccc 100644 --- a/st2auth/st2auth/backends/keystone.py +++ b/st2auth/st2auth/backends/keystone.py @@ -17,6 +17,12 @@ from st2common import log as logging from st2auth.backends.base import BaseAuthenticationBackend import requests +try: + from urlparse import urlparse + from urlparse import urljoin +except ImportError: + from urllib.parse import urlparse + from urllib.parse import urljoin __all__ = [ 'KeystoneAuthenticationBackend' @@ -39,21 +45,49 @@ def __init__(self, keystone_url, keystone_version=2): :param keystone_version: Keystone version to authenticate against (default to 2). :type keystone_version: ``int`` """ + url = urlparse(keystone_url) + if url.path != '' or url.query != '' or url.fragment != '': + raise Exception("The Keystone url {} does not seem to be correct.\n" + "Please only set the scheme+url+port (e.x.: http://example.com:5000)".format(keystone_url)) self._keystone_url = keystone_url self._keystone_version = keystone_version def authenticate(self, username, password): - creds = { - "auth": { - "passwordCredentials": { - "username": username, - "password": password + if self._keystone_version == 2: + creds = { + "auth": { + "passwordCredentials": { + "username": username, + "password": password + } } } - } - login = requests.post(self._keystone_url, json=creds) + login = requests.post(urljoin(self._keystone_url, 'v2.0/tokens'), json=creds) - if login.status_code == 200: + elif self._keystone_version == 3: + creds = { + "auth": { + "identity": { + "methods": [ + "password" + ], + "password": { + "domain": { + "id": "default" + }, + "user": { + "name": username, + "password": password + } + } + } + } + } + login = requests.post(urljoin(self._keystone_url, 'v3/auth/tokens'), json=creds) + else: + raise Exception("Keystone version {} not supported".format(self._keystone_version)) + + if login.status_code in [200, 201]: LOG.debug('Authentication for user "{}" successful'.format(username)) return True else: diff --git a/st2auth/tests/unit/test_auth_backends.py b/st2auth/tests/unit/test_auth_backends.py index 7f995d938d..864d60726d 100644 --- a/st2auth/tests/unit/test_auth_backends.py +++ b/st2auth/tests/unit/test_auth_backends.py @@ -16,12 +16,15 @@ import os import unittest2 +import mock +from requests.models import Response from st2tests.config import parse_args parse_args() from st2auth.backends.flat_file import FlatFileAuthenticationBackend from st2auth.backends.mongodb import MongoDBAuthenticationBackend +from st2auth.backends.keystone import KeystoneAuthenticationBackend BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -80,3 +83,29 @@ def test_authenticate(self): # Valid password self.assertTrue(self._backend.authenticate(username='test1', password='testpassword')) + + +class KeystoneAuthenticationBackendTestCase(unittest2.TestCase): + def _mock_keystone(self, *args, **kwargs): + return_codes = {'goodv2': 200, 'goodv3': 201, 'bad': 400} + json = kwargs.get('json') + res = Response() + try: + # v2 + res.status_code = return_codes[json['auth']['passwordCredentials']['user']] + except KeyError: + # v3 + res.status_code = return_codes[json['auth']['identity']['password']['user']['name']] + return res + + @mock.patch('requests.post', side_effect=_mock_keystone) + def test_authenticate(self): + backendv2 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", version=2) + backendv3 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", version=3) + + # good users + self.assertTrue(backendv2.authenticate('goodv2', 'password')) + self.assertTrue(backendv3.authenticate('goodv3', 'password')) + # bad ones + self.assertFalse(backendv2.authenticate('bad', 'password')) + self.assertFalse(backendv3.authenticate('bad', 'password')) From 6ed8e257a8f0bef37790336de03cc9d66d458a4f Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 19:46:49 +0200 Subject: [PATCH 03/11] fix messed up flake8 --- st2auth/st2auth/backends/keystone.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/st2auth/st2auth/backends/keystone.py b/st2auth/st2auth/backends/keystone.py index 0c50a51ccc..aa83e8948c 100644 --- a/st2auth/st2auth/backends/keystone.py +++ b/st2auth/st2auth/backends/keystone.py @@ -17,6 +17,7 @@ from st2common import log as logging from st2auth.backends.base import BaseAuthenticationBackend import requests + try: from urlparse import urlparse from urlparse import urljoin @@ -48,7 +49,8 @@ def __init__(self, keystone_url, keystone_version=2): url = urlparse(keystone_url) if url.path != '' or url.query != '' or url.fragment != '': raise Exception("The Keystone url {} does not seem to be correct.\n" - "Please only set the scheme+url+port (e.x.: http://example.com:5000)".format(keystone_url)) + "Please only set the scheme+url+port " + "(e.x.: http://example.com:5000)".format(keystone_url)) self._keystone_url = keystone_url self._keystone_version = keystone_version @@ -78,11 +80,11 @@ def authenticate(self, username, password): "user": { "name": username, "password": password - } } } } } + } login = requests.post(urljoin(self._keystone_url, 'v3/auth/tokens'), json=creds) else: raise Exception("Keystone version {} not supported".format(self._keystone_version)) From 5ecf6de1fd9daf629685ab5defb271a2a88ab999 Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 19:58:22 +0200 Subject: [PATCH 04/11] set pylint to ignore the importerrors as they are there for python3 --- st2auth/st2auth/backends/keystone.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/st2auth/st2auth/backends/keystone.py b/st2auth/st2auth/backends/keystone.py index aa83e8948c..af0a750e35 100644 --- a/st2auth/st2auth/backends/keystone.py +++ b/st2auth/st2auth/backends/keystone.py @@ -22,8 +22,8 @@ from urlparse import urlparse from urlparse import urljoin except ImportError: - from urllib.parse import urlparse - from urllib.parse import urljoin + from urllib.parse import urlparse # pylint: disable=E0611 + from urllib.parse import urljoin # pylint: disable=E0611 __all__ = [ 'KeystoneAuthenticationBackend' From adb44eea6981d84132091fdd64d682cb44adeb2b Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 20:05:37 +0200 Subject: [PATCH 05/11] forgot that when patching, mock is passed to the function! --- st2auth/tests/unit/test_auth_backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2auth/tests/unit/test_auth_backends.py b/st2auth/tests/unit/test_auth_backends.py index 864d60726d..ba0bed181a 100644 --- a/st2auth/tests/unit/test_auth_backends.py +++ b/st2auth/tests/unit/test_auth_backends.py @@ -99,7 +99,7 @@ def _mock_keystone(self, *args, **kwargs): return res @mock.patch('requests.post', side_effect=_mock_keystone) - def test_authenticate(self): + def test_authenticate(self, mock_post): backendv2 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", version=2) backendv3 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", version=3) From 2937e887004d5205b04b43959934de9bc622a11e Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 20:11:24 +0200 Subject: [PATCH 06/11] Im dumb and cant read variable names --- st2auth/tests/unit/test_auth_backends.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/st2auth/tests/unit/test_auth_backends.py b/st2auth/tests/unit/test_auth_backends.py index ba0bed181a..73a6653f5e 100644 --- a/st2auth/tests/unit/test_auth_backends.py +++ b/st2auth/tests/unit/test_auth_backends.py @@ -100,8 +100,8 @@ def _mock_keystone(self, *args, **kwargs): @mock.patch('requests.post', side_effect=_mock_keystone) def test_authenticate(self, mock_post): - backendv2 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", version=2) - backendv3 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", version=3) + backendv2 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", keystone_version=2) + backendv3 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", keystone_version=3) # good users self.assertTrue(backendv2.authenticate('goodv2', 'password')) From bfe23a00ba5ee90b58639f7e3c515395acab03ae Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 20:14:35 +0200 Subject: [PATCH 07/11] use httplib for the http codes, use six for easier py2/3 compatibility --- st2auth/st2auth/backends/keystone.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/st2auth/st2auth/backends/keystone.py b/st2auth/st2auth/backends/keystone.py index af0a750e35..7358802f2d 100644 --- a/st2auth/st2auth/backends/keystone.py +++ b/st2auth/st2auth/backends/keystone.py @@ -17,13 +17,10 @@ from st2common import log as logging from st2auth.backends.base import BaseAuthenticationBackend import requests +import httplib -try: - from urlparse import urlparse - from urlparse import urljoin -except ImportError: - from urllib.parse import urlparse # pylint: disable=E0611 - from urllib.parse import urljoin # pylint: disable=E0611 +from six.moves.urllib import parse as urlparse +from six.moves.urllib.parse import urljoin __all__ = [ 'KeystoneAuthenticationBackend' @@ -89,7 +86,7 @@ def authenticate(self, username, password): else: raise Exception("Keystone version {} not supported".format(self._keystone_version)) - if login.status_code in [200, 201]: + if login.status_code in [httplib.OK, httplib.CREATED]: LOG.debug('Authentication for user "{}" successful'.format(username)) return True else: From 8ea82caaa238e4ffe839b6f9cb651c8c4f89aa04 Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 20:20:22 +0200 Subject: [PATCH 08/11] wrong import --- st2auth/st2auth/backends/keystone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2auth/st2auth/backends/keystone.py b/st2auth/st2auth/backends/keystone.py index 7358802f2d..221e126072 100644 --- a/st2auth/st2auth/backends/keystone.py +++ b/st2auth/st2auth/backends/keystone.py @@ -19,7 +19,7 @@ import requests import httplib -from six.moves.urllib import parse as urlparse +from six.moves.urllib.parse import parse as urlparse from six.moves.urllib.parse import urljoin __all__ = [ From 9e8c55bb392a0830f55541b0a5a0d337f7ab24c0 Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 20:23:53 +0200 Subject: [PATCH 09/11] REALLY fix the wrong import --- st2auth/st2auth/backends/keystone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2auth/st2auth/backends/keystone.py b/st2auth/st2auth/backends/keystone.py index 221e126072..56548f2951 100644 --- a/st2auth/st2auth/backends/keystone.py +++ b/st2auth/st2auth/backends/keystone.py @@ -19,7 +19,7 @@ import requests import httplib -from six.moves.urllib.parse import parse as urlparse +from six.moves.urllib.parse import urlparse from six.moves.urllib.parse import urljoin __all__ = [ From f4f7e3de5967dc0a58cbb4f711f32e438caabcc9 Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 22:07:50 +0200 Subject: [PATCH 10/11] use httlib for status_code, use proper key for keystone v2 --- st2auth/tests/unit/test_auth_backends.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/st2auth/tests/unit/test_auth_backends.py b/st2auth/tests/unit/test_auth_backends.py index 73a6653f5e..8758c2aa3a 100644 --- a/st2auth/tests/unit/test_auth_backends.py +++ b/st2auth/tests/unit/test_auth_backends.py @@ -18,6 +18,7 @@ import unittest2 import mock from requests.models import Response +import httplib from st2tests.config import parse_args parse_args() @@ -87,12 +88,12 @@ def test_authenticate(self): class KeystoneAuthenticationBackendTestCase(unittest2.TestCase): def _mock_keystone(self, *args, **kwargs): - return_codes = {'goodv2': 200, 'goodv3': 201, 'bad': 400} + return_codes = {'goodv2': httplib.OK, 'goodv3': httplib.CREATED, 'bad': httplib.UNAUTHORIZED} json = kwargs.get('json') res = Response() try: # v2 - res.status_code = return_codes[json['auth']['passwordCredentials']['user']] + res.status_code = return_codes[json['auth']['passwordCredentials']['username']] except KeyError: # v3 res.status_code = return_codes[json['auth']['identity']['password']['user']['name']] From e31ed58860c9c6c20965f7170be051463530628d Mon Sep 17 00:00:00 2001 From: Itxaka Serrano Date: Mon, 20 Jul 2015 22:14:57 +0200 Subject: [PATCH 11/11] some linting --- st2auth/tests/unit/test_auth_backends.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/st2auth/tests/unit/test_auth_backends.py b/st2auth/tests/unit/test_auth_backends.py index 8758c2aa3a..556e8c38b3 100644 --- a/st2auth/tests/unit/test_auth_backends.py +++ b/st2auth/tests/unit/test_auth_backends.py @@ -88,7 +88,11 @@ def test_authenticate(self): class KeystoneAuthenticationBackendTestCase(unittest2.TestCase): def _mock_keystone(self, *args, **kwargs): - return_codes = {'goodv2': httplib.OK, 'goodv3': httplib.CREATED, 'bad': httplib.UNAUTHORIZED} + return_codes = { + 'goodv2': httplib.OK, + 'goodv3': httplib.CREATED, + 'bad': httplib.UNAUTHORIZED + } json = kwargs.get('json') res = Response() try: @@ -101,8 +105,10 @@ def _mock_keystone(self, *args, **kwargs): @mock.patch('requests.post', side_effect=_mock_keystone) def test_authenticate(self, mock_post): - backendv2 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", keystone_version=2) - backendv3 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", keystone_version=3) + backendv2 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", + keystone_version=2) + backendv3 = KeystoneAuthenticationBackend(keystone_url="http://fake.com:5000", + keystone_version=3) # good users self.assertTrue(backendv2.authenticate('goodv2', 'password'))