Added authmiddleware
Change-Id: I2b45674ca70e8a9314fe2387734b3d115d9add3e
This commit is contained in:
parent
2ec8b4bf27
commit
f7316cd525
@ -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
|
||||
|
||||
|
@ -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": "@"
|
||||
}
|
0
surveil/api/authmiddleware/__init__.py
Normal file
0
surveil/api/authmiddleware/__init__.py
Normal file
170
surveil/api/authmiddleware/auth.py
Normal file
170
surveil/api/authmiddleware/auth.py
Normal 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)]
|
32
surveil/api/authmiddleware/utils.py
Normal file
32
surveil/api/authmiddleware/utils.py
Normal 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'))
|
@ -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()
|
||||
|
42
surveil/api/controllers/v2/auth/login.py
Normal file
42
surveil/api/controllers/v2/auth/login.py
Normal 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)
|
@ -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()
|
||||
|
0
surveil/tests/api/controllers/v2/auth/__init__.py
Normal file
0
surveil/tests/api/controllers/v2/auth/__init__.py
Normal file
51
surveil/tests/api/controllers/v2/auth/test_auth.py
Normal file
51
surveil/tests/api/controllers/v2/auth/test_auth.py
Normal 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)
|
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user