Added authmiddleware

Change-Id: I2b45674ca70e8a9314fe2387734b3d115d9add3e
This commit is contained in:
aviau 2015-04-29 17:03:17 -04:00
parent 2ec8b4bf27
commit f7316cd525
11 changed files with 331 additions and 10 deletions

View File

@ -3,11 +3,14 @@
# Remove authtoken from the pipeline if you don't want to use keystone authentication
[pipeline:main]
pipeline = api-server
pipeline = surveil-auth api-server
[app:api-server]
paste.app_factory = surveil.api.app:app_factory
[filter:surveil-auth]
paste.filter_factory = surveil.api.authmiddleware.auth:filter_factory
[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory

View File

@ -1,4 +1,6 @@
{
"surveil:break":"!",
"surveil:pass":"@"
}
"admin_required": "role:admin or is_admin:1",
"surveil:admin": "rule:admin_required",
"surveil:break": "!",
"surveil:pass": "@"
}

View File

View File

@ -0,0 +1,170 @@
# Copyright 2010-2012 OpenStack Foundation
# Copyright 2015 - Savoir-Faire Linux 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 surveil.api.authmiddleware import utils
import six
"""
Keystone-Compatible Token-based Authentication Middleware.
This Middleware is based on keystonemiddleware, it creates the same headers but
verifies token authenticity against some other service. It was created for
Surveil so that we can have more flexibility on the authentication backend.
"""
_HEADER_TEMPLATE = {
'X%s-Domain-Id': 'domain_id',
'X%s-Domain-Name': 'domain_name',
'X%s-Project-Id': 'project_id',
'X%s-Project-Name': 'project_name',
'X%s-Project-Domain-Id': 'project_domain_id',
'X%s-Project-Domain-Name': 'project_domain_name',
'X%s-User-Id': 'user_id',
'X%s-User-Name': 'username',
'X%s-User-Domain-Id': 'user_domain_id',
'X%s-User-Domain-Name': 'user_domain_name',
}
def filter_factory(global_conf, **local_conf):
"""Returns a WSGI filter app for use with paste.deploy."""
conf = global_conf.copy()
conf.update(local_conf)
def auth_filter(app):
return AuthProtocol(app, conf)
return auth_filter
class AuthProtocol(object):
"""Middleware that handles authenticating client calls."""
def __init__(self, app, conf):
self._app = app
self._init_auth_headers()
# TODO(aviau): auth_uri should be loaded in config
self._auth_uri = 'www.surveil.com'
def _init_auth_headers(self):
"""Initialize auth header list.
Both user and service token headers are generated.
"""
auth_headers = ['X-Service-Catalog',
'X-Identity-Status',
'X-Service-Identity-Status',
'X-Roles',
'X-Service-Roles']
for key in six.iterkeys(_HEADER_TEMPLATE):
auth_headers.append(key % '')
# Service headers
auth_headers.append(key % '-Service')
self._auth_headers = auth_headers
def __call__(self, env, start_response):
"""Handle incoming request.
Authenticate send downstream on success. Reject request if
we can't authenticate.
"""
self._remove_auth_headers(env)
# TODO(aviau): Get token and validate it, then build proper headers
if False:
self._reject_request(env, start_response)
user_headers = {
'X-Identity-Status': 'Confirmed',
'X-User-Id': 'surveil',
'X-Roles': 'admin',
'X-Service-Catalog': 'surveil'
}
self._add_headers(env, user_headers)
service_headers = {
'X-Service-Identity-Status': 'Confirmed',
'X-Service-Roles': 'surveil',
}
self._add_headers(env, service_headers)
return self._call_app(env, start_response)
def _remove_auth_headers(self, env):
"""Remove headers so a user can't fake authentication.
Both user and service token headers are removed.
:param env: wsgi request environment
"""
self._remove_headers(env, self._auth_headers)
def _remove_headers(self, env, keys):
"""Remove http headers from environment."""
for k in keys:
env_key = self._header_to_env_var(k)
try:
del env[env_key]
except KeyError:
pass
def _add_headers(self, env, headers):
"""Add http headers to environment."""
for (k, v) in six.iteritems(headers):
env_key = self._header_to_env_var(k)
env[env_key] = v
def _header_to_env_var(self, key):
"""Convert header to wsgi env variable.
:param key: http header name (ex. 'X-Auth-Token')
:returns: wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN')
"""
return 'HTTP_%s' % key.replace('-', '_').upper()
def _call_app(self, env, start_response):
# NOTE(jamielennox): We wrap the given start response so that if an
# application with a 'delay_auth_decision' setting fails, or otherwise
# raises Unauthorized that we include the Authentication URL headers.
def _fake_start_response(status, response_headers, exc_info=None):
if status.startswith('401'):
response_headers.extend(self._reject_auth_headers)
return start_response(status, response_headers, exc_info)
return self._app(env, _fake_start_response)
def _reject_request(self, env, start_response):
"""Redirect client to auth server.
:param env: wsgi request environment
:param start_response: wsgi response callback
:returns: HTTPUnauthorized http response
"""
resp = utils.MiniResp('Authentication required',
env, self._reject_auth_headers)
start_response('401 Unauthorized', resp.headers)
return resp.body
@property
def _reject_auth_headers(self):
header_val = 'Keystone uri=\'%s\'' % self._auth_uri
return [('WWW-Authenticate', header_val)]

View File

@ -0,0 +1,32 @@
# 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 six.moves import urllib
def safe_quote(s):
"""URL-encode strings that are not already URL-encoded."""
return urllib.parse.quote(s) if s == urllib.parse.unquote(s) else s
class MiniResp(object):
def __init__(self, error_message, env, headers=[]):
# The HEAD method is unique: it must never return a body, even if
# it reports an error (RFC-2616 clause 9.4). We relieve callers
# from varying the error responses depending on the method.
if env['REQUEST_METHOD'] == 'HEAD':
self.body = ['']
else:
self.body = [error_message.encode()]
self.headers = list(headers)
self.headers.append(('Content-type', 'text/plain'))

View File

@ -14,8 +14,8 @@
from pecan import rest
from surveil.api.controllers.v2.auth import login
class AuthController(rest.RestController):
# login = LoginController()
# logout = LogoutController()
pass
login = login.LoginController()

View File

@ -0,0 +1,42 @@
# Copyright 2014 - Savoir-Faire Linux 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 json
import pecan
from pecan import rest
class LoginController(rest.RestController):
@pecan.expose()
def post(self):
"""Retrieve an auth token."""
access = {
"access": {
"token": {
"issued_at": "2014-01-30T15:30:58.819584",
"expires": "2014-01-31T15:30:58Z",
"id": "aaaaa-bbbbb-ccccc-dddd",
"tenant": {
"description": "Hey!",
"enabled": True,
"id": "fc394f2ab2df4114bde39905f800dc57",
"name": "demo"
}
}
}
}
return json.dumps(access)

View File

@ -26,8 +26,29 @@ class HelloController(rest.RestController):
"""Says hello."""
return "Hello World!"
@pecan.expose()
def _lookup(self, *remainder):
return HelloSubController(), remainder
class AdminController(rest.RestController):
@pecan.expose()
@util.policy_enforce(['admin'])
def get(self):
"""Says hello to the admin."""
return "Hello, dear admin!"
class DeniedController(rest.RestController):
@pecan.expose()
@util.policy_enforce(['break'])
def post(self):
"""What are you trying to post dude?"""
def get(self):
"""This should be denied."""
return "Looks like policies are not working."
class HelloSubController(rest.RestController):
admin = AdminController()
denied = DeniedController()

View File

@ -0,0 +1,51 @@
# Copyright 2015 - Savoir-Faire Linux 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 json
from surveil.tests.api import functionalTest
class TestAuthController(functionalTest.FunctionalTest):
def test_auth_login(self):
auth = {
"auth": {
"tenantName": "demo",
"passwordCredentials": {
"username": "demo",
"password": "secretsecret"
}
}
}
response = self.app.post_json('/v2/auth/login', params=auth)
expected = {
"access": {
"token": {
"issued_at": "2014-01-30T15:30:58.819584",
"expires": "2014-01-31T15:30:58Z",
"id": "aaaaa-bbbbb-ccccc-dddd",
"tenant": {
"enabled": True,
"description": "Hey!",
"name": "demo",
"id": "fc394f2ab2df4114bde39905f800dc57"
}
}
}
}
self.assertEqual(json.loads(response.body.decode()), expected)

View File

@ -24,4 +24,4 @@ class TestHelloController(functionalTest.FunctionalTest):
def test_post_policy_forbidden(self):
with self.assertRaisesRegexp(Exception, '403 Forbidden'):
self.app.post('/v2/hello')
self.app.get('/v2/hello/denied')