diff --git a/st2auth/st2auth/backends/keystone.py b/st2auth/st2auth/backends/keystone.py new file mode 100644 index 0000000000..56548f2951 --- /dev/null +++ b/st2auth/st2auth/backends/keystone.py @@ -0,0 +1,97 @@ +# 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 +import httplib + +from six.moves.urllib.parse import urlparse +from six.moves.urllib.parse import urljoin + +__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`` + """ + 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): + if self._keystone_version == 2: + creds = { + "auth": { + "passwordCredentials": { + "username": username, + "password": password + } + } + } + login = requests.post(urljoin(self._keystone_url, 'v2.0/tokens'), json=creds) + + 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 [httplib.OK, httplib.CREATED]: + 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 diff --git a/st2auth/tests/unit/test_auth_backends.py b/st2auth/tests/unit/test_auth_backends.py index 7f995d938d..556e8c38b3 100644 --- a/st2auth/tests/unit/test_auth_backends.py +++ b/st2auth/tests/unit/test_auth_backends.py @@ -16,12 +16,16 @@ import os import unittest2 +import mock +from requests.models import Response +import httplib 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 +84,35 @@ 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': httplib.OK, + 'goodv3': httplib.CREATED, + 'bad': httplib.UNAUTHORIZED + } + json = kwargs.get('json') + res = Response() + try: + # v2 + res.status_code = return_codes[json['auth']['passwordCredentials']['username']] + 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, mock_post): + 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')) + self.assertTrue(backendv3.authenticate('goodv3', 'password')) + # bad ones + self.assertFalse(backendv2.authenticate('bad', 'password')) + self.assertFalse(backendv3.authenticate('bad', 'password'))