Integrating monasca-api with keystonemiddleware

Adds the possibility of authenticating through keystone-middleware
and creates a Keystone Context after validating the token.

Both v3 and v2 tokens are valid, once the user has a valid role
on the project/tenant he's scoped. The default_authorized_roles property
in monasca.conf must be updated if another user should have API full access.

Change-Id: Iacd66cd2868cd44a5ab1b2a3f80fa38c7e0bb6da
This commit is contained in:
henriquetruta 2015-01-29 14:27:57 -03:00 committed by Craig Bryant
parent 707b737e6f
commit ccdb3e1806
6 changed files with 219 additions and 7 deletions

View File

@ -18,7 +18,7 @@ dispatcher = v2_ref_notifications
[security]
# The roles that are allowed full access to the API.
default_authorized_roles = admin
default_authorized_roles = admin,monasca-user
# The roles that are allowed to only POST metrics to the API. This role would be used by the Monasca Agent.
agent_authorized_roles = agent
@ -113,4 +113,15 @@ database_name = mon
database_name = mon
hostname = 192.168.10.4
username = monapi
password = password
password = password
[keystone_authtoken]
identity_uri = http://192.168.10.5:35357
auth_uri = http://192.168.10.5:5000
admin_password = admin
admin_user = admin
admin_tenant_name = admin
cafile =
certfile =
keyfile =
insecure = false

View File

@ -3,7 +3,7 @@ name = monasca
[pipeline:main]
# Add validator in the pipeline so the metrics messages can be validated.
pipeline = auth api
pipeline = auth keystonecontext api
[app:api]
paste.app_factory = monasca.api.server:api_app
@ -18,11 +18,14 @@ use = egg: monasca_api#inspector
use = egg: monasca_api#metric_validator
[filter:auth]
use = egg: monasca_api#mock_auth_filter
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
[filter:keystonecontext]
paste.filter_factory = monasca.middleware.keystone_context_filter:filter_factory
[server:main]
use = egg:gunicorn#main
host = 0.0.0.0
port = 9000
workers = 1
proc_name = monasca
proc_name = monasca

View File

@ -0,0 +1,84 @@
# Copyright (c) 2015 OpenStack Foundation
#
# 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.
"""RequestContext: context for requests that persist through monasca."""
import uuid
from oslo.utils import timeutils
from monasca.openstack.common import log
LOG = log.getLogger(__name__)
class RequestContext(object):
"""Security context and request information.
Represents the user taking a given action within the system.
"""
def __init__(self, user_id, project_id, domain_id=None, domain_name=None,
roles=None, timestamp=None, request_id=None,
auth_token=None, user_name=None, project_name=None,
service_catalog=None, user_auth_plugin=None, **kwargs):
"""Creates the Keystone Context. Supports additional parameters:
:param user_auth_plugin:
The auth plugin for the current request's authentication data.
:param kwargs:
Extra arguments that might be present
"""
if kwargs:
LOG.warning(
'Arguments dropped when creating context: %s') % str(kwargs)
self._roles = roles or []
self.timestamp = timeutils.utcnow()
if not request_id:
request_id = self.generate_request_id()
self._request_id = request_id
self._auth_token = auth_token
self._service_catalog = service_catalog
self._domain_id = domain_id
self._domain_name = domain_name
self._user_id = user_id
self._user_name = user_name
self._project_id = project_id
self._project_name = project_name
self._user_auth_plugin = user_auth_plugin
def to_dict(self):
return {'user_id': self._user_id,
'project_id': self._project_id,
'domain_id': self._domain_id,
'domain_name': self._domain_name,
'roles': self._roles,
'timestamp': timeutils.strtime(self._timestamp),
'request_id': self._request_id,
'auth_token': self._auth_token,
'user_name': self._user_name,
'service_catalog': self._service_catalog,
'project_name': self._project_name,
'user': self._user}
def generate_request_id(self):
return b'req-' + str(uuid.uuid4()).encode('ascii')

View File

@ -0,0 +1,111 @@
# Copyright (c) 2015 OpenStack Foundation
#
# 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 falcon
from monasca.middleware import context
from monasca.openstack.common import log
from oslo.middleware import request_id
from oslo.serialization import jsonutils
LOG = log.getLogger(__name__)
def filter_factory(global_conf, **local_conf):
def validator_filter(app):
return KeystoneContextFilter(app, local_conf)
return validator_filter
class KeystoneContextFilter(object):
"""Make a request context from keystone headers."""
def __init__(self, app, conf):
self._app = app
self._conf = conf
def __call__(self, env, start_response):
LOG.debug("Creating Keystone Context Object.")
user_id = env.get('HTTP_X_USER_ID', env.get('HTTP_X_USER'))
if user_id is None:
msg = "Neither X_USER_ID nor X_USER found in request"
LOG.error(msg)
raise falcon.HTTPUnauthorized(title='Forbidden', description=msg)
roles = self._get_roles(env)
project_id = env.get('HTTP_X_PROJECT_ID')
project_name = env.get('HTTP_X_PROJECT_NAME')
domain_id = env.get('HTTP_X_DOMAIN_ID')
domain_name = env.get('HTTP_X_DOMAIN_NAME')
user_name = env.get('HTTP_X_USER_NAME')
req_id = env.get(request_id.ENV_REQUEST_ID)
# Get the auth token
auth_token = env.get('HTTP_X_AUTH_TOKEN',
env.get('HTTP_X_STORAGE_TOKEN'))
service_catalog = None
if env.get('HTTP_X_SERVICE_CATALOG') is not None:
try:
catalog_header = env.get('HTTP_X_SERVICE_CATALOG')
service_catalog = jsonutils.loads(catalog_header)
except ValueError:
msg = "Invalid service catalog json."
LOG.error(msg)
raise falcon.HTTPInternalServerError(msg)
# NOTE(jamielennox): This is a full auth plugin set by auth_token
# middleware in newer versions.
user_auth_plugin = env.get('keystone.token_auth')
# Build a context
ctx = context.RequestContext(user_id,
project_id,
user_name=user_name,
project_name=project_name,
domain_id=domain_id,
domain_name=domain_name,
roles=roles,
auth_token=auth_token,
service_catalog=service_catalog,
request_id=req_id,
user_auth_plugin=user_auth_plugin)
env['monasca.context'] = ctx
LOG.debug("Keystone Context succesfully created.")
return self._app(env, start_response)
def _get_roles(self, env):
"""Get the list of roles."""
if 'HTTP_X_ROLES' in env:
roles = env.get('HTTP_X_ROLES', '')
else:
# Fallback to deprecated role header:
roles = env.get('HTTP_X_ROLE', '')
if roles:
LOG.warning(
'Sourcing roles from deprecated X-Role HTTP header')
return [r.strip() for r in roles.split(',')]

View File

@ -80,14 +80,14 @@ def validate_authorization(req, authorized_roles):
str_roles = req.get_header('X-ROLES')
if str_roles is None:
raise falcon.HTTPUnauthorized('Forbidden',
'Tenant does not have any roles', '')
'Tenant does not have any roles')
roles = str_roles.lower().split(',')
for role in roles:
if role in authorized_roles:
return
raise falcon.HTTPUnauthorized('Forbidden',
'Tenant ID is missing a required role to '
'access this service', '')
'access this service')
def get_tenant_id(req):

View File

@ -22,7 +22,10 @@
falcon>=0.1.9
gunicorn>=19.1.0
keystonemiddleware
oslo.config>=1.2.1
oslo.middleware
oslo.serialization
pastedeploy>=1.3.3
pbr>=0.6,!=0.7,<1.0
python-dateutil>=1.5