Add fake Keystone

* enabled 'fake' authentication in nailgun by default
* add fake keystone API and middleware to nailgun
* fixed problem with not configured logging in
  keystoneclient - now fuelclient silents all logs
* removed TEST_MODE as it's not needed in fake auth

Related to blueprint access-control-master-node
Closes-Bug: 1340141
Change-Id: I76cb7d1cb19be8e0d23ecc03fdbc968b1eeaff5c
This commit is contained in:
Vitaly Kramskikh 2014-07-04 18:22:43 +04:00 committed by Sebastian Kalinowski
parent 7375949ef9
commit 37d0f23a26
11 changed files with 266 additions and 22 deletions

View File

@ -13,6 +13,7 @@
# under the License.
import json
import logging
import os
import urllib2
@ -21,6 +22,13 @@ import yaml
from keystoneclient import client as auth_client
from fuelclient.cli.error import exceptions_decorator
from fuelclient.logs import NullHandler
# configure logging to silent all logs
# and prevent issues in keystoneclient logging
logger = logging.getLogger()
logger.addHandler(NullHandler())
class Client(object):
@ -29,7 +37,6 @@ class Client(object):
def __init__(self):
self.debug = False
self.test_mod = bool(os.environ.get('TEST_MODE', ''))
path_to_config = "/etc/fuel/client/config.yaml"
defaults = {
"SERVER_ADDRESS": "127.0.0.1",
@ -73,10 +80,9 @@ class Client(object):
def auth_status(self):
self.auth_required = False
if not self.test_mod:
request = urllib2.urlopen(''.join([self.api_root, 'version']))
self.auth_required = json.loads(
request.read()).get('auth_required', False)
request = urllib2.urlopen(''.join([self.api_root, 'version']))
self.auth_required = json.loads(
request.read()).get('auth_required', False)
def update_own_password(self, new_pass):
if self.auth_token:
@ -84,7 +90,7 @@ class Client(object):
self.password, new_pass)
def initialize_keystone_client(self):
if not self.test_mod and self.auth_required:
if self.auth_required:
self.keystone_client = auth_client.Client(
username=self.user,
password=self.password,

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Mirantis, Inc.
#
# 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.
import logging
class NullHandler(logging.Handler):
"""This handler does nothing. It's intended to be used to avoid the
"No handlers could be found for logger XXX" one-off warning. This
important for library code, which may contain code to log events.
of the library does not configure logging, the one-off warning mig
produced; to avoid this, the library developer simply needs to ins
a NullHandler and add it to the top-level logger of the library mo
package.
Taken from Python 2.7
"""
def handle(self, record):
pass
def emit(self, record):
pass
def createLock(self):
self.lock = None

View File

@ -102,7 +102,6 @@ class BaseTestCase(TestCase):
def run_cli_command(self, command_line, check_errors=False):
modified_env = os.environ.copy()
modified_env['TEST_MODE'] = 'True'
command_args = [" ".join((self.fuel_path, command_line))]
process_handle = subprocess.Popen(
command_args,

View File

@ -28,7 +28,8 @@ from nailgun.logger import HTTPLoggerMiddleware
from nailgun.logger import logger
from nailgun.middleware.http_method_override import \
HTTPMethodOverrideMiddleware
from nailgun.middleware.keystone import NailgunAuthProtocol
from nailgun.middleware.keystone import NailgunFakeKeystoneAuthMiddleware
from nailgun.middleware.keystone import NailgunKeystoneAuthMiddleware
from nailgun.middleware.static import StaticMiddleware
from nailgun.settings import settings
from nailgun.urls import urls
@ -55,7 +56,9 @@ def build_middleware(app):
middleware_list.append(StaticMiddleware)
if settings.AUTH['AUTHENTICATION_METHOD'] == 'keystone':
middleware_list.append(NailgunAuthProtocol)
middleware_list.append(NailgunKeystoneAuthMiddleware)
elif settings.AUTH['AUTHENTICATION_METHOD'] == 'fake':
middleware_list.append(NailgunFakeKeystoneAuthMiddleware)
logger.debug('Initialize middleware: %s' %
(map(lambda x: x.__name__, middleware_list)))

View File

@ -0,0 +1,30 @@
# Copyright 2014 Mirantis, Inc.
#
# 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.
import time
from nailgun.settings import settings
def validate_password_credentials(username, password, **kwargs):
return (username == settings.FAKE_KEYSTONE_USERNAME and
password == settings.FAKE_KEYSTONE_PASSWORD)
def validate_token(token):
return token.startswith('token')
def generate_token():
return 'token' + str(int(time.time()))

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Mirantis, Inc.
#
# 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.
from nailgun.api.v1.handlers.base import BaseHandler
from nailgun.api.v1.handlers.base import content_json
from nailgun.fake_keystone import generate_token
from nailgun.fake_keystone import validate_password_credentials
from nailgun.fake_keystone import validate_token
from nailgun.settings import settings
class TokensHandler(BaseHandler):
@content_json
def POST(self):
data = self.checked_data()
try:
if 'passwordCredentials' in data['auth']:
if not validate_password_credentials(
**data['auth']['passwordCredentials']):
raise self.http(401)
elif 'token' in data['auth']:
if not validate_token(data['auth']['token']['id']):
raise self.http(401)
else:
raise self.http(400)
except (KeyError, TypeError):
raise self.http(400)
token = generate_token()
return {
"access": {
"token": {
"issued_at": "2012-07-10T13:37:58.708765",
"expires": "2012-07-10T14:37:58Z",
"id": token,
"tenant": {
"description": None,
"enabled": True,
"id": "12345",
"name": "admin"
}
},
"serviceCatalog": [],
"user": {
"username": "admin",
"roles_links": [],
"id": "9876",
"roles": [{"name": "admin"}],
"name": "admin"
},
"metadata": {
"is_admin": 0,
"roles": ["4567"]
}
}
}
class VersionHandler(BaseHandler):
@content_json
def GET(self):
keystone_href = 'http://{ip_addr}:{port}/keystone/v2.0/'.format(
ip_addr=settings.LISTEN_ADDRESS, port=settings.LISTEN_PORT)
return {
'version': {
'id': 'v2.0',
'status': 'stable',
'updated': '2014-04-17T00:00:00Z',
'links': [
{
'rel': 'self',
'href': keystone_href,
}, {
'rel': 'describedby',
'type': 'text/html',
'href': 'http://docs.openstack.org/',
},
],
'media-types': [
{
'base': 'application/json',
'type': 'application/vnd.openstack.identity-v2.0+json',
}, {
'base': 'application/xml',
'type': 'application/vnd.openstack.identity-v2.0+xml',
},
],
},
}

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Mirantis, Inc.
#
# 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.
import web
from nailgun.fake_keystone.handlers import TokensHandler
from nailgun.fake_keystone.handlers import VersionHandler
urls = (
r"/v2.0/?$", VersionHandler.__name__,
r"/v2.0/tokens/?$", TokensHandler.__name__,
)
_locals = locals()
def app():
return web.application(urls, _locals)

View File

@ -17,6 +17,7 @@
import re
from nailgun.api.v1 import urls as api_urls
from nailgun.fake_keystone import validate_token
from nailgun.settings import settings
from keystoneclient.middleware import auth_token
@ -29,15 +30,13 @@ def public_urls():
urls['{0}{1}'.format('/api', url)] = methods
urls["/$"] = ['GET']
urls["/static"] = ['GET']
urls["/keystone"] = ['GET', 'POST']
return urls
class NailgunAuthProtocol(auth_token.AuthProtocol):
"""A wrapper on Keystone auth_token middleware.
Does not perform verification of authentication tokens
for public routes in the API.
class SkipAuthMixin(object):
"""Mixin which skips verification of authentication tokens for public
routes in the API.
"""
def __init__(self, app):
self.public_api_routes = {}
@ -47,10 +46,9 @@ class NailgunAuthProtocol(auth_token.AuthProtocol):
except re.error as e:
msg = 'Cannot compile public API routes: {0}'.format(e)
auth_token.LOG.error(msg)
raise Exception(error_msg=msg)
super(NailgunAuthProtocol, self).__init__(app, settings.AUTH)
super(SkipAuthMixin, self).__init__(app, settings.AUTH)
def __call__(self, env, start_response):
path = env.get('PATH_INFO', '/')
@ -68,4 +66,30 @@ class NailgunAuthProtocol(auth_token.AuthProtocol):
if env['is_public_api']:
return self.app(env, start_response)
return super(NailgunAuthProtocol, self).__call__(env, start_response)
return super(SkipAuthMixin, self).__call__(env, start_response)
class FakeAuthProtocol(object):
"""Auth protocol for fake mode.
"""
def __init__(self, app, conf):
self.app = app
def __call__(self, env, start_response):
if validate_token(env.get('HTTP_X_AUTH_TOKEN', '')):
return self.app(env, start_response)
else:
start_response('401 Unauthorized', [])
return ''
class NailgunKeystoneAuthMiddleware(SkipAuthMixin, auth_token.AuthProtocol):
"""Auth middleware for keystone.
"""
pass
class NailgunFakeKeystoneAuthMiddleware(SkipAuthMixin, FakeAuthProtocol):
"""Auth middleware for fake mode.
"""
pass

View File

@ -6,7 +6,7 @@ AUTH:
# - none - authentication is disabled
# - fake - no keystone required, credentials: admin/admin
# - keystone - authentication enabled.
AUTHENTICATION_METHOD: "none"
AUTHENTICATION_METHOD: "fake"
# use only if AUTHENTICATION_METHOD is set to "keystone"
admin_token: "ADMIN"
auth_host: "127.0.0.1"
@ -533,6 +533,9 @@ DNS_SEARCH: "example.com"
FAKE_TASKS_TICK_INTERVAL: "1"
FAKE_TASKS_TICK_COUNT: "30"
FAKE_KEYSTONE_USERNAME: admin
FAKE_KEYSTONE_PASSWORD: admin
MAX_ITEMS_PER_PAGE: 500
SHOTGUN_SSH_KEY: "/root/.ssh/id_rsa"

View File

@ -44,6 +44,6 @@ class TestVersionHandler(BaseIntegrationTest):
"astute_sha": "Unknown build",
"fuellib_sha": "Unknown build",
"ostf_sha": "Unknown build",
"auth_required": False
"auth_required": True,
}
)

View File

@ -15,12 +15,17 @@
# under the License.
from nailgun.api.v1 import urls as api_urls
from nailgun.fake_keystone import urls as fake_keystone_urls
from nailgun.settings import settings
from nailgun.webui import urls as webui_urls
def urls():
return (
urls = [
"/api/v1", api_urls.app(),
"/api", api_urls.app(),
"", webui_urls.app()
)
]
if settings.AUTH['AUTHENTICATION_METHOD'] == 'fake':
urls = ["/keystone", fake_keystone_urls.app()] + urls
return urls