Basic auth and refactor

This commit is contained in:
Ziad Sawalha 2011-04-30 04:33:11 -05:00
parent 0ed0eaaa34
commit ea57151dfb
6 changed files with 213 additions and 110 deletions

View File

@ -14,13 +14,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
import eventlet
from eventlet import wsgi
from lxml import etree
import os
from paste.deploy import loadapp
import sys
from webob.exc import HTTPUnauthorized
# If ../echo/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
@ -42,7 +43,6 @@ if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'echo', '__init__.py')):
Echo: a dummy service for OpenStack auth testing. It returns request info.
"""
class EchoApp(object):
def __init__(self, environ, start_response):
self.envr = environ
@ -53,11 +53,20 @@ class EchoApp(object):
self.transform = etree.XSLT(etree.parse(echo_xsl))
def __iter__(self):
# We expect an X_AUTHORIZATION header to be passed in
# We assume the request is coming from a trusted source. Middleware
# is used to perform that validation.
if 'HTTP_X_AUTHORIZATION' not in self.envr:
return HTTPUnauthorized(self.envr, start_response)
self.start('401 Unauthorized', [('Content-Type', 'application/json')])
return iter(["401 Unauthorized"])
if 'HTTP_X_IDENTITY_STATUS' not in self.envr:
identity_status = "Unknown"
else:
identity_status = self.envr["HTTP_X_IDENTITY_STATUS"]
print ' Received:'
if 'HTTP_X_IDENTITY_STATUS' in self.envr: print ' Auth Status:', self.envr['HTTP_X_IDENTITY_STATUS']
print ' Auth Status:', identity_status
if 'HTTP_X_AUTHORIZATION' in self.envr: print ' Identity :', self.envr['HTTP_X_AUTHORIZATION']
if 'HTTP_X_TENANT' in self.envr: print ' Tenant :', self.envr['HTTP_X_TENANT']
if 'HTTP_X_GROUP' in self.envr: print ' Group :', self.envr['HTTP_X_GROUP']
@ -80,8 +89,7 @@ class EchoApp(object):
echo = etree.Element("{http://docs.openstack.org/echo/api/v1.0}echo",
method=environ["REQUEST_METHOD"],
pathInfo=environ["PATH_INFO"],
queryString=environ.get('QUERY_STRING', ""),
caller_identity=self.envr['HTTP_X_AUTHORIZATION'])
queryString=environ.get('QUERY_STRING', ""))
content = etree.Element(
"{http://docs.openstack.org/echo/api/v1.0}content")
content.set("type", environ["CONTENT_TYPE"])

View File

@ -19,6 +19,7 @@ Implement a client for Echo service using Identity service
import httplib
import json
import sys
def get_auth_token(username, password, tenant):
@ -46,6 +47,21 @@ def call_service(token):
ret = data
return ret
def hack_attempt(token):
# Injecting headers in the request
headers = {"X-Auth-Token": token,
"Content-type": "application/json",
"Accept": "text/json\nX_AUTHORIZATION: someone else\nX_IDENTITY_STATUS: Confirmed\nINJECTED_HEADER: aha!"}
params = '{"ping": "abcdefg"}'
conn = httplib.HTTPConnection("localhost:8090")
print headers
conn.request("POST", "/", params, headers=headers)
response = conn.getresponse()
data = response.read()
ret = data
return ret
if __name__ == '__main__':
# Call the keystone service to get a token
# NOTE: assumes the test_setup.sql script has loaded this user
@ -54,18 +70,28 @@ if __name__ == '__main__':
obj = json.loads(auth)
token = obj["auth"]["token"]["id"]
print "Token obtained:", token
raw_input()
# Use that token to call an OpenStack service (echo)
data = call_service(token)
print "Response received:", data
print
raw_input()
# Use the valid token, but inject some headers
print "\033[91mInjecting some headers >:-/ \033[0m"
data = hack_attempt(token)
print "Response received:", data
print
raw_input()
# Use bad token to call an OpenStack service (echo)
print "\033[91mTrying with bad token...\033[0m"
data = call_service("xxxx_invalid_token_xxxx")
print "Response received:", data
print
raw_input()
#Supply bad credentials
print "\033[91mTrying with bad credentials...\033[0m"
auth = get_auth_token("joeuser", "wrongpass", "1")

View File

@ -0,0 +1,13 @@
[DEFAULT]
[app:main]
paste.app_factory = auth_basic:app_factory
delay_auth_decision = 0
service_protocol = http
service_host = 127.0.0.1
service_port = 8100
service_pass = dTpw

View File

@ -105,8 +105,10 @@ class AuthProtocol(object):
#Claims were rejected
if not self.delay_auth_decision:
# Reject request (or ask for valid claims)
return HTTPUnauthorized()(env,
start_response)
return HTTPUnauthorized("Authentication required",
[('WWW-Authenticate',
'Basic realm="Use guest/guest"')]
)(env, start_response)
else:
# Claims are valid, forward request
_decorate_request_headers("X_IDENTITY_STATUS", "Invalid",

View File

@ -53,25 +53,20 @@ HTTP_X_AUTHORIZATION: the client identity being passed in
import eventlet
from eventlet import wsgi
import httplib
import json
import os
import sys
from paste.deploy import loadapp
import sys
from urlparse import urlparse
from webob.exc import HTTPUnauthorized
from webob.exc import HTTPUnauthorized, HTTPUseProxy
from webob.exc import Request, Response
import httplib
from keystone.common.bufferedhttp import http_connect_raw as http_connect
PROTOCOL_NAME = "Token Authentication"
def _decorate_request_headers(header, value, proxy_headers, env):
proxy_headers[header] = value
env["HTTP_%s" % header] = value
class AuthProtocol(object):
"""Auth Middleware that handles authenticating client calls"""
@ -121,6 +116,64 @@ class AuthProtocol(object):
self._init_protocol_common(app, conf) # Applies to all protocols
self._init_protocol(app, conf) # Specific to this protocol
def __call__(self, env, start_response):
""" Handle incoming request. Authenticate. And send downstream. """
self.start_response = start_response
self.env = env
#Prep headers to forward request to downstream service (local or remote)
self.proxy_headers = env.copy()
for header in self.proxy_headers.iterkeys():
if header[0:5] == 'HTTP_':
self.proxy_headers[header[5:]] = self.proxy_headers[header]
del self.proxy_headers[header]
#Look for authentication claims
self.claims = self._get_claims(env)
if not self.claims:
#No claim(s) provided
if self.delay_auth_decision:
#Configured to allow downstream service to make final decision.
#So mark status as Invalid and forward the request downstream
self._decorate_request("X_IDENTITY_STATUS", "Invalid")
else:
#Respond to client as appropriate for this auth protocol
return self._reject_request()
else:
# this request is presenting claims. Let's validate them
valid = self._validate_claims(self.claims)
if not valid:
# Keystone rejected claim
if self.delay_auth_decision:
# Downstream service will receive call still and decide
self._decorate_request("X_IDENTITY_STATUS", "Invalid")
else:
#Respond to client as appropriate for this auth protocol
return self._reject_claims()
else:
self._decorate_request("X_IDENTITY_STATUS", "Confirmed")
#Collect information about valid claims
if valid:
verified_claims = self._expound_claims()
if verified_claims:
# TODO(Ziad): add additional details we may need,
# like tenant and group info
self._decorate_request('X_AUTHORIZATION',
"Proxy %s" % verified_claims['user'])
self._decorate_request('X_TENANT',
verified_claims['tenant'])
self._decorate_request('X_GROUP',
verified_claims['group'])
self.expanded = True
#Send request downstream
return self._forward_request()
def get_admin_auth_token(self, username, password, tenant):
"""
This function gets an admin auth token to be used by this service to
@ -137,112 +190,113 @@ class AuthProtocol(object):
headers=headers)
response = conn.getresponse()
data = response.read()
ret = data
return ret
return data
def __call__(self, env, start_response):
def custom_start_response(status, headers):
if self.delay_auth_decision:
headers.append(('WWW-Authenticate', "Basic realm='API Realm'"))
return start_response(status, headers)
def _get_claims(self, env):
claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
return claims
#Prep headers to proxy request to remote service
proxy_headers = env.copy()
user = ''
def _reject_request(self):
# Redirect client to auth server
return HTTPUseProxy(location=self.auth_location)(self.env,
self.start_response)
#Look for token in request
token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
if not token:
#No token was provided
if self.delay_auth_decision:
_decorate_request_headers("X_IDENTITY_STATUS", "Invalid",
proxy_headers, env)
else:
# Redirect client to auth server
return HTTPUseProxy(location=self.auth_location)(env,
start_response)
else:
# this request is claiming it has a valid token, let's check
# with the auth service
# Step 1: We need to auth with the keystone service, so get an
# admin token
#TODO: Need to properly implement this, where to store creds
# for now using token from ini
#auth = self.get_admin_auth_token("admin", "secrete", "1")
#admin_token = json.loads(auth)["auth"]["token"]["id"]
def _reject_claims(self):
# Client sent bad claims
return HTTPUnauthorized()(self.env,
self.start_response)
# Step 2: validate the user's token with the auth service
# since this is a priviledged op,m we need to auth ourselves
# by using an admin token
headers = {"Content-type": "application/json",
"Accept": "text/json",
"X-Auth-Token": self.admin_token}
##TODO:we need to figure out how to auth to keystone
#since validate_token is a priviledged call
#Khaled's version uses creds to get a token
# "X-Auth-Token": admin_token}
# we're using a test token from the ini file for now
conn = http_connect(self.auth_host, self.auth_port, 'GET',
'/v1.0/token/%s' % token, headers=headers)
resp = conn.getresponse()
data = resp.read()
conn.close()
if not str(resp.status).startswith('20'):
# Keystone rejected claim
if self.delay_auth_decision:
# Downstream service will receive call still and decide
_decorate_request_headers("X_IDENTITY_STATUS", "Invalid",
proxy_headers, env)
else:
# Reject the response & send back the error
# (not delay_auth_decision)
return HTTPUnauthorized(headers=headers)(env,
start_response)
else:
# Valid token. Get user data and put it in to the call
# so the downstream service can use iot
dict_response = json.loads(data)
#TODO(Ziad): make this more robust
user = dict_response['auth']['user']['username']
tenant = dict_response['auth']['user']['tenantId']
group = '%s/%s' % (dict_response['auth']['user']['groups']['group'][0]['id'],
dict_response['auth']['user']['groups']['group'][0]['tenantId'])
# TODO(Ziad): add additional details we may need,
# like tenant and group info
_decorate_request_headers('X_AUTHORIZATION', "Proxy %s" % user,
proxy_headers, env)
_decorate_request_headers("X_IDENTITY_STATUS", "Confirmed",
proxy_headers, env)
_decorate_request_headers('X_TENANT', tenant,
proxy_headers, env)
_decorate_request_headers('X_GROUP', group,
proxy_headers, env)
#Token/Auth processed, headers added now decide how to pass on the call
_decorate_request_headers('AUTHORIZATION',
"Basic %s" % self.service_pass,
proxy_headers,
env)
def _validate_claims(self, claims):
"""Validate claims, and provide identity information isf applicable """
# Step 1: We need to auth with the keystone service, so get an
# admin token
#TODO: Need to properly implement this, where to store creds
# for now using token from ini
#auth = self.get_admin_auth_token("admin", "secrete", "1")
#admin_token = json.loads(auth)["auth"]["token"]["id"]
# Step 2: validate the user's token with the auth service
# since this is a priviledged op,m we need to auth ourselves
# by using an admin token
headers = {"Content-type": "application/json",
"Accept": "text/json",
"X-Auth-Token": self.admin_token}
##TODO:we need to figure out how to auth to keystone
#since validate_token is a priviledged call
#Khaled's version uses creds to get a token
# "X-Auth-Token": admin_token}
# we're using a test token from the ini file for now
conn = http_connect(self.auth_host, self.auth_port, 'GET',
'/v1.0/token/%s' % claims, headers=headers)
resp = conn.getresponse()
data = resp.read()
conn.close()
if not str(resp.status).startswith('20'):
# Keystone rejected claim
return False
else:
#TODO(Ziad): there is an optimization we can do here. We have just
#received data from Keystone that we can use instead of making
#another call in _expound_claims
return True
def _expound_claims(self):
# Valid token. Get user data and put it in to the call
# so the downstream service can use it
headers = {"Content-type": "application/json",
"Accept": "text/json",
"X-Auth-Token": self.admin_token}
##TODO:we need to figure out how to auth to keystone
#since validate_token is a priviledged call
#Khaled's version uses creds to get a token
# "X-Auth-Token": admin_token}
# we're using a test token from the ini file for now
conn = http_connect(self.auth_host, self.auth_port, 'GET',
'/v1.0/token/%s' % self.claims, headers=headers)
resp = conn.getresponse()
data = resp.read()
conn.close()
if not str(resp.status).startswith('20'):
raise LookupError('Unable to locate claims: %s' % resp.status)
token_data = json.loads(data)
#TODO(Ziad): make this more robust
verified_claims = {'user': token_data['auth']['user']['username'],
'tenant': token_data['auth']['user']['tenantId'],
'group': '%s/%s' % (token_data['auth']['user']['groups']['group'][0]['id'],
token_data['auth']['user']['groups']['group'][0]['tenantId'])}
return verified_claims
def _decorate_request(self, index, value):
self.proxy_headers[index] = value
self.env["HTTP_%s" % index] = value
def _forward_request(self):
#Token/Auth processed & claims added to headers
self._decorate_request('AUTHORIZATION',
"Basic %s" % self.service_pass)
#now decide how to pass on the call
if self.app:
# Pass to downstream WSGI component
return self.app(env, custom_start_response)
return self.app(self.env, self.start_response) #.custom_start_response)
else:
# We are forwarding to a remote service (no downstream WSGI app)
req = Request(proxy_headers)
req = Request(self.proxy_headers)
parsed = urlparse(req.url)
conn = http_connect(self.service_host, self.service_port, \
req.method, parsed.path, \
proxy_headers,\
self.proxy_headers,\
ssl=(self.service_protocol == 'https'))
resp = conn.getresponse()
data = resp.read()
#TODO: use a more sophisticated proxy
# we are rewriting the headers now
return Response(status=resp.status, body=data)(proxy_headers,
start_response)
return Response(status=resp.status, body=data)(self.proxy_headers,
self.start_response)
def filter_factory(global_conf, **local_conf):
"""Returns a WSGI filter app for use with paste.deploy."""

View File

@ -24,7 +24,7 @@ insert into tenants (id, "desc", enabled) values
-- Groups
insert into groups (id, "desc", tenant_id) values
("Admin", "Andmin users", "1234");
("Admin", "Admin users", "1234");
insert into groups (id, "desc", tenant_id) values
("Default", "Standard users", "1234");