summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md22
-rw-r--r--echo/echo/echo.ini42
-rw-r--r--echo/echo/echo.py14
-rw-r--r--echo/echo/echo_remote.ini13
-rw-r--r--keystone/__init__.py16
-rw-r--r--keystone/auth_protocols/__init__.py (renamed from keystone/auth_protocol/__init__.py)0
-rw-r--r--keystone/auth_protocols/auth_basic.py114
-rw-r--r--keystone/auth_protocols/auth_openid.py113
-rw-r--r--keystone/auth_protocols/auth_token.ini (renamed from keystone/auth_protocol/auth_protocol_token.ini)9
-rw-r--r--keystone/auth_protocols/auth_token.py (renamed from keystone/auth_protocol/auth_protocol_token.py)180
-rw-r--r--keystone/db/sqlalchemy/api.py13
-rw-r--r--keystone/db/sqlalchemy/session.py10
-rw-r--r--keystone/identity.py34
-rw-r--r--keystone/keystone.ini13
-rw-r--r--keystone/middleware/remoteauth.py (renamed from keystone/middleware/papiauth.py)31
-rw-r--r--management/getgroupusers.py1
-rw-r--r--management/getuser.py1
-rw-r--r--management/useradd.py1
-rw-r--r--management/userupdate.py1
-rw-r--r--test/unit/test_identity.py82
20 files changed, 533 insertions, 177 deletions
diff --git a/README.md b/README.md
index 8f6f4c07..3753b402 100644
--- a/README.md
+++ b/README.md
@@ -12,10 +12,14 @@ This initial proof of concept aims to address the current use cases in Swift and
SERVICES:
---------
-* Keystone - authentication service
-* PAPIAuth - WSGI middleware that can be used in services (like Swift, Nova, and Glance) to perform authentication
-* Auth_Protocal_Token - WSGI middleware that can be used ti handle token auth protocol (WSGI or remote proxy)
-* Echo - A sample service that responds by returning call details
+* Keystone - authentication service
+* Auth_Token - WSGI middleware that can be used to handle token auth protocol (WSGI or remote proxy)
+* Echo - A sample service that responds by returning call details
+
+Also included:
+* Auth_Basic - Stub for WSGI middleware that will be used to handle basic auth
+* Auth_OpenID - Stub for WSGI middleware that will be used to handle openid auth protocol
+* RemoteAuth - WSGI middleware that can be used in services (like Swift, Nova, and Glance) when Auth middleware is running remotely
DEPENDENCIES:
@@ -62,17 +66,17 @@ RUNNING KEYSTONE:
RUNNING TEST SERVICE:
---------------------
-Standalone stack (with PAPIAuth and Auth_Protocol_Token)
+ Standalone stack (with Auth_Token)
$ cd echo/echo
$ python echo.py
-Distributed stack (with PAPIAuth local and Auth_Protocol_Token remote)
+ Distributed stack (with RemoteAuth local and Auth_Token remote)
$ cd echo/echo
$ python echo.py --remote
in separate session
- $ cd keystone/auth_protocol
- $ python auth_protocol_token.py --remote
+ $ cd keystone/auth_protocols
+ $ python auth_token.py --remote
DEMO CLIENT:
---------------------
@@ -143,7 +147,7 @@ Once the Identity service is running, go to unit test/unit directory
For more on unit testing please refer
-python test_identity --help
+ python test_identity --help
diff --git a/echo/echo/echo.ini b/echo/echo/echo.ini
index cadd3fb5..b81174d0 100644
--- a/echo/echo/echo.ini
+++ b/echo/echo/echo.ini
@@ -1,40 +1,36 @@
[DEFAULT]
+;delegated means we still allow unauthenticated requests through so the
+;service can make the final decision on authentication
+delay_auth_decision = 0
+
+;where to find the OpenStack service (if not in local WSGI chain)
+service_protocol = http
+service_host = 127.0.0.1
+service_port = 8090
+;used to verify this component with the OpenStack service (or PAPIAuth)
+service_pass = dTpw
+
[app:echo]
paste.app_factory = echo:app_factory
[pipeline:main]
pipeline =
- auth
- papiauth
+ tokenauth
echo
-;remove last entry to use the service_settings below if echo is remote
-
-[filter:papiauth]
-paste.filter_factory = keystone:papiauth_factory
-;password which will tell us caller is trusted (otherwise we redirect them)
-auth_pass = dTpw
-;where to redirect untrusted calls to
-proxy_location = http://127.0.0.1:8080/
-
-[filter:auth]
+[filter:tokenauth]
paste.filter_factory = keystone:tokenauth_factory
;where to find the token auth service
auth_host = 127.0.0.1
auth_port = 8080
+auth_protocol = http
;how to authenticate to the auth service for priviledged operations
;like validate token
-auth_token = 999888777666
+admin_token = 999888777666
-;delegated means we still allow unauthenticated requests through so the
-;service can make the final decision on authentication
-delegated = 0
-
-;where to find the OpenStack service (if not in local WSGI chain)
-service_protocol = http
-service_host = 127.0.0.1
-service_port = 8090
-;used to verify this component with the OpenStack service (or PAPIAuth)
-service_pass = dTpw
+[filter:basicauth]
+paste.filter_factory = keystone:basicauth_factory
+[filter:openidauth]
+paste.filter_factory = keystone:openidauth_factory
diff --git a/echo/echo/echo.py b/echo/echo/echo.py
index dada3392..ee950b37 100644
--- a/echo/echo/echo.py
+++ b/echo/echo/echo.py
@@ -54,7 +54,13 @@ class EchoApp(object):
def __iter__(self):
if 'HTTP_X_AUTHORIZATION' not in self.envr:
- return HTTPUnauthorized(env, start_response)
+ return HTTPUnauthorized(self.envr, start_response)
+
+ print ' Received:'
+ if 'HTTP_X_IDENTITY_STATUS' in self.envr: print ' Auth Status:', self.envr['HTTP_X_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']
accept = self.envr.get("HTTP_ACCEPT", "application/json")
if accept == "application/xml":
@@ -74,7 +80,8 @@ 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', ""))
+ queryString=environ.get('QUERY_STRING', ""),
+ caller_identity=self.envr['HTTP_X_AUTHORIZATION'])
content = etree.Element(
"{http://docs.openstack.org/echo/api/v1.0}content")
content.set("type", environ["CONTENT_TYPE"])
@@ -105,7 +112,8 @@ if __name__ == "__main__":
wsgi.server(eventlet.listen(('', 8100)), app)
else:
- print "Running all components locally. Use --remote option to run with remote auth proxy"
+ print "Running all components locally."
+ print "Use --remote option to run with remote auth proxy"
app = loadapp("config:" + \
os.path.join(os.path.abspath(os.path.dirname(__file__)),
"echo.ini"), global_conf={"log_name": "echo.log"})
diff --git a/echo/echo/echo_remote.ini b/echo/echo/echo_remote.ini
index 8f6bc89b..c27b2365 100644
--- a/echo/echo/echo_remote.ini
+++ b/echo/echo/echo_remote.ini
@@ -5,14 +5,15 @@ paste.app_factory = echo:app_factory
[pipeline:main]
pipeline =
- papiauth
+ remoteauth
echo
-[filter:papiauth]
-paste.filter_factory = keystone:papiauth_factory
-;password which will tell us caller is trusted (otherwise we redirect them)
-auth_pass = dTpw
+[filter:remoteauth]
+paste.filter_factory = keystone:remoteauth_factory
+;password which will tell us call is coming from a trusted auth proxy
+; (otherwise we redirect call)
+remote_auth_pass = dTpw
;where to redirect untrusted calls to
-proxy_location = http://127.0.0.1:8080/
+auth_location = http://127.0.0.1:8080/
diff --git a/keystone/__init__.py b/keystone/__init__.py
index 3f30dd74..aec9d44f 100644
--- a/keystone/__init__.py
+++ b/keystone/__init__.py
@@ -1,3 +1,15 @@
-from middleware.papiauth import filter_factory as papiauth_factory
-from auth_protocol.auth_protocol_token \
+#TOKEN AUTH
+from auth_protocols.auth_token \
import filter_factory as tokenauth_factory
+
+#BASIC AUTH
+from auth_protocols.auth_basic \
+ import filter_factory as basicauth_factory
+
+#OPENID AUTH
+from auth_protocols.auth_openid \
+ import filter_factory as openidauth_factory
+
+#Remote Auth handler
+from middleware.remoteauth \
+ import filter_factory as remoteauth_factory
diff --git a/keystone/auth_protocol/__init__.py b/keystone/auth_protocols/__init__.py
index e69de29b..e69de29b 100644
--- a/keystone/auth_protocol/__init__.py
+++ b/keystone/auth_protocols/__init__.py
diff --git a/keystone/auth_protocols/auth_basic.py b/keystone/auth_protocols/auth_basic.py
new file mode 100644
index 00000000..2bc967ff
--- /dev/null
+++ b/keystone/auth_protocols/auth_basic.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright (c) 2010-2011 OpenStack, LLC.
+#
+# 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.
+# Not Yet PEP8 standardized
+
+
+"""
+BASIC AUTH MIDDLEWARE - STUB
+
+This WSGI component should perform multiple jobs:
+- validate incoming basic claims
+- perform all basic auth interactions with clients
+- collect and forward identity information from the authentication process
+ such as user name, groups, etc...
+
+This is an Auth component as per: http://wiki.openstack.org/openstack-authn
+
+"""
+
+
+PROTOCOL_NAME = "Basic Authentication"
+
+
+class AuthProtocol(object):
+ """Auth Middleware that handles authenticating client calls"""
+
+ def __init__(self, app, conf):
+ print "Starting the %s component" % PROTOCOL_NAME
+
+ self.conf = conf
+ self.app = app
+ #if app is set, then we are in a WSGI pipeline and requests get passed
+ # on to app. If it is not set, this component should forward requests
+
+ # where to find the OpenStack service (if not in local WSGI chain)
+ # these settings are only used if this component is acting as a proxy
+ # and the OpenSTack service is running remotely
+ self.service_protocol = conf.get('service_protocol', 'https')
+ self.service_host = conf.get('service_host')
+ self.service_port = int(conf.get('service_port'))
+ self.service_url = '%s://%s:%s' % (self.service_protocol,
+ self.service_host,
+ self.service_port)
+ # used to verify this component with the OpenStack service or PAPIAuth
+ self.service_pass = conf.get('service_pass')
+
+ # delay_auth_decision means we still allow unauthenticated requests
+ # through and we let the downstream service make the final decision
+ self.delay_auth_decision = int(conf.get('delay_auth_decision', 0))
+
+ 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)
+
+ #TODO(Ziad): PERFORM BASIC AUTH
+
+ #Auth processed, headers added now decide how to pass on the call
+ if self.app:
+ # Pass to downstream WSGI component
+ env['HTTP_AUTHORIZATION'] = "Basic %s" % self.service_pass
+ return self.app(env, custom_start_response)
+
+ proxy_headers['AUTHORIZATION'] = "Basic %s" % self.service_pass
+ # We are forwarding to a remote service (no downstream WSGI app)
+ req = Request(proxy_headers)
+ parsed = urlparse(req.url)
+ conn = http_connect(self.service_host, self.service_port, \
+ req.method, parsed.path, \
+ 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)(env, start_response)
+
+
+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
+
+
+def app_factory(global_conf, **local_conf):
+ conf = global_conf.copy()
+ conf.update(local_conf)
+ return AuthProtocol(None, conf)
+
+if __name__ == "__main__":
+ app = loadapp("config:" + \
+ os.path.join(os.path.abspath(os.path.dirname(__file__)),
+ "auth_basic.ini"),
+ global_conf={"log_name": "auth_basic.log"})
+ wsgi.server(eventlet.listen(('', 8090)), app)
diff --git a/keystone/auth_protocols/auth_openid.py b/keystone/auth_protocols/auth_openid.py
new file mode 100644
index 00000000..ac9121f7
--- /dev/null
+++ b/keystone/auth_protocols/auth_openid.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright (c) 2010-2011 OpenStack, LLC.
+#
+# 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.
+# Not Yet PEP8 standardized
+
+
+"""
+OPENID AUTH MIDDLEWARE - STUB
+
+This WSGI component should perform multiple jobs:
+- validate incoming openid claims
+- perform all openid interactions with clients
+- collect and forward identity information from the openid authentication
+ such as user name, groups, etc...
+
+This is an Auth component as per: http://wiki.openstack.org/openstack-authn
+"""
+
+
+PROTOCOL_NAME = "OpenID Authentication"
+
+
+class AuthProtocol(object):
+ """Auth Middleware that handles authenticating client calls"""
+
+ def __init__(self, app, conf):
+ print "Starting the %s component" % PROTOCOL_NAME
+
+ self.conf = conf
+ self.app = app
+ #if app is set, then we are in a WSGI pipeline and requests get passed
+ # on to app. If it is not set, this component should forward requests
+
+ # where to find the OpenStack service (if not in local WSGI chain)
+ # these settings are only used if this component is acting as a proxy
+ # and the OpenSTack service is running remotely
+ self.service_protocol = conf.get('service_protocol', 'http')
+ self.service_host = conf.get('service_host', '127.0.0.1')
+ self.service_port = int(conf.get('service_port', 8090))
+ self.service_url = '%s://%s:%s' % (self.service_protocol,
+ self.service_host,
+ self.service_port)
+ # used to verify this component with the OpenStack service or PAPIAuth
+ self.service_pass = conf.get('service_pass', 'dTpw')
+
+ # delay_auth_decision means we still allow unauthenticated requests
+ # through and we let the downstream service make the final decision
+ self.delay_auth_decision = int(conf.get('delay_auth_decision', 0))
+
+ 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)
+
+ #TODO(Rasib): PERFORM OPENID AUTH
+
+ #Auth processed, headers added now decide how to pass on the call
+ if self.app:
+ # Pass to downstream WSGI component
+ env['HTTP_AUTHORIZATION'] = "Basic %s" % self.service_pass
+ return self.app(env, custom_start_response)
+
+ proxy_headers['AUTHORIZATION'] = "Basic %s" % self.service_pass
+ # We are forwarding to a remote service (no downstream WSGI app)
+ req = Request(proxy_headers)
+ parsed = urlparse(req.url)
+ conn = http_connect(self.service_host, self.service_port, \
+ req.method, parsed.path, \
+ 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)(env, start_response)
+
+
+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
+
+
+def app_factory(global_conf, **local_conf):
+ conf = global_conf.copy()
+ conf.update(local_conf)
+ return AuthProtocol(None, conf)
+
+if __name__ == "__main__":
+ app = loadapp("config:" + \
+ os.path.join(os.path.abspath(os.path.dirname(__file__)),
+ "auth_openid.ini"),
+ global_conf={"log_name": "auth_openid.log"})
+ wsgi.server(eventlet.listen(('', 8090)), app)
diff --git a/keystone/auth_protocol/auth_protocol_token.ini b/keystone/auth_protocols/auth_token.ini
index 41e903d9..3154fc69 100644
--- a/keystone/auth_protocol/auth_protocol_token.ini
+++ b/keystone/auth_protocols/auth_token.ini
@@ -1,11 +1,14 @@
[DEFAULT]
[app:main]
-paste.app_factory = auth_protocol_token:app_factory
+paste.app_factory = auth_token:app_factory
+
+auth_protocol = http
auth_host = 127.0.0.1
auth_port = 8080
-auth_token = 999888777666
-delegated = 0
+admin_token = 999888777666
+
+delay_auth_decision = 0
service_protocol = http
service_host = 127.0.0.1
diff --git a/keystone/auth_protocol/auth_protocol_token.py b/keystone/auth_protocols/auth_token.py
index 92c88035..cee8e84f 100644
--- a/keystone/auth_protocol/auth_protocol_token.py
+++ b/keystone/auth_protocols/auth_token.py
@@ -24,8 +24,9 @@ TOKEN-BASED AUTH MIDDLEWARE
This WSGI component performs multiple jobs:
- it verifies that incoming client requests have valid tokens by verifying
tokens with the auth service.
-- it will reject unauthenticated requests UNLESS it is in 'delegated' mode,
- which means the final decision is delegated to the service
+- it will reject unauthenticated requests UNLESS it is in 'delay_auth_decision'
+ mode, which means the final decision is delegated to the downstream WSGI
+ component (usually the OpenStack service)
- it will collect and forward identity information from a valid token
such as user name, groups, etc...
@@ -34,12 +35,19 @@ Refer to: http://wiki.openstack.org/openstack-authn
HEADERS
-------
-www-authenticate : only used if this component is being used remotely
-HTTP_AUTHORIZATION : basic auth password used to validate the connection
-HTTP_X_AUTHORIZATION: the client identity being passed in
+Headers starting with HTTP_ is a standard http header
+Headers starting with HTTP_X is an extended http header
+
+> Coming in from initial call from client or customer
HTTP_X_AUTH_TOKEN : the client token being passed in
HTTP_X_STORAGE_TOKEN: the client token being passed in (legacy Rackspace use)
to support cloud files
+> Used for communication between components
+www-authenticate : only used if this component is being used remotely
+HTTP_AUTHORIZATION : basic auth password used to validate the connection
+
+> What we add to the request for use by the OpenStack service
+HTTP_X_AUTHORIZATION: the client identity being passed in
"""
@@ -55,39 +63,68 @@ import httplib
from keystone.common.bufferedhttp import http_connect_raw as http_connect
+PROTOCOL_NAME = "Token Authentication"
-class TokenAuth(object):
- """Auth Middleware that handles token authentication with an auth service"""
- def __init__(self, app, conf):
- print "Starting the Token Auth component"
+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"""
+
+ def _init_protocol_common(self, app, conf):
+ """ Common initialization code"""
+ print "Starting the %s component" % PROTOCOL_NAME
self.conf = conf
- self.app = app #if app is not set, this should forward requests
+ self.app = app
+ #if app is set, then we are in a WSGI pipeline and requests get passed
+ # on to app. If it is not set, this component should forward requests
# where to find the OpenStack service (if not in local WSGI chain)
- self.service_protocol = conf.get('service_protocol', 'http')
- self.service_host = conf.get('service_host', '127.0.0.1')
- self.service_port = int(conf.get('service_port', 8090))
- self.service_url = '%s://%s:%s' % (self.service_protocol, self.service_host, self.service_port)
- # used to verify this component with the OpenStack service (or PAPIAuth)
- self.service_pass = conf.get('service_pass', 'dTpw')
-
- # where to find the auth service (we use this to validate tokens)
- self.auth_host = conf.get('auth_ip', '127.0.0.1')
- self.auth_port = int(conf.get('auth_port', 8080))
- # used to verify this component with the Auth service
- self.auth_token = conf.get('auth_token', 'dTpw')
-
- # delegated means we still allow unauthenticated requests through
- self.delegated = int(conf.get('delegated', 0))
-
+ # these settings are only used if this component is acting as a proxy
+ # and the OpenSTack service is running remotely
+ self.service_protocol = conf.get('service_protocol', 'https')
+ self.service_host = conf.get('service_host')
+ self.service_port = int(conf.get('service_port'))
+ self.service_url = '%s://%s:%s' % (self.service_protocol,
+ self.service_host,
+ self.service_port)
+ # used to verify this component with the OpenStack service or PAPIAuth
+ self.service_pass = conf.get('service_pass')
+
+ # delay_auth_decision means we still allow unauthenticated requests
+ # through and we let the downstream service make the final decision
+ self.delay_auth_decision = int(conf.get('delay_auth_decision', 0))
+
+ def _init_protocol(self, app, conf):
+ """ Protocol specific initialization """
+
+ # where to find the auth service (we use this to validate tokens)
+ self.auth_host = conf.get('auth_host')
+ self.auth_port = int(conf.get('auth_port'))
+ self.auth_protocol = conf.get('auth_protocol', 'https')
+ self.auth_location = "%s://%s:%s" % (self.auth_protocol, self.auth_host,
+ self.auth_port)
+
+ # Credentials used to verify this component with the Auth service since
+ # validating tokens is a priviledged call
+ self.admin_token = conf.get('admin_token')
+
+ def __init__(self, app, conf):
+ """ Common initialization code """
+
+ #TODO: maybe we rafactor this into a superclass
+ self._init_protocol_common(app, conf) # Applies to all protocols
+ self._init_protocol(app, conf) # Specific to this protocol
def get_admin_auth_token(self, username, password, tenant):
"""
- This function gets an admin auth token to be used by this service to
- validate a user's token. Validate_token is a priviledged call so
- it needs to be authenticated by a service that is calling it
+ This function gets an admin auth token to be used by this service to
+ validate a user's token. Validate_token is a priviledged call so
+ it needs to be authenticated by a service that is calling it
"""
headers = {"Content-type": "application/json", "Accept": "text/json"}
params = {"passwordCredentials": {"username": username,
@@ -102,20 +139,28 @@ class TokenAuth(object):
ret = data
return ret
-
def __call__(self, env, start_response):
def custom_start_response(status, headers):
- if self.delegated:
+ if self.delay_auth_decision:
headers.append(('WWW-Authenticate', "Basic realm='API Realm'"))
return start_response(status, headers)
- #Prep for proxy request
+ #Prep headers to proxy request to remote service
proxy_headers = env.copy()
user = ''
#Look for token in request
token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
- if 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
@@ -125,10 +170,12 @@ class TokenAuth(object):
#auth = self.get_admin_auth_token("admin", "secrete", "1")
#admin_token = json.loads(auth)["auth"]["token"]["id"]
- # Step 2: validate the user's token using the admin token
+ # 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.auth_token}
+ "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
@@ -140,36 +187,47 @@ class TokenAuth(object):
data = resp.read()
conn.close()
-
if not str(resp.status).startswith('20'):
# Keystone rejected claim
- if self.delegated:
+ if self.delay_auth_decision:
# Downstream service will receive call still and decide
- proxy_headers['X_IDENTITY_STATUS'] = "Invalid"
- env['HTTPX_IDENTITY_STATUS'] = "Invalid"
+ _decorate_request_headers("X_IDENTITY_STATUS", "Invalid",
+ proxy_headers, env)
else:
- # Reject the response & send back the error (not delegated)
- return HTTPUnauthorized(headers=headers)(env, start_response)
+ # 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']
- proxy_headers['X_AUTHORIZATION'] = "Proxy " + user
- proxy_headers['X_IDENTITY_STATUS'] = "Confirmed"
- env['HTTP_X_AUTHORIZATION'] = "Proxy " + user
- env['HTTP_X_IDENTITY_STATUS'] = "Confirmed"
- else:
- #No token was provided
- if self.delegated:
- proxy_headers['X_IDENTITY_STATUS'] = "Invalid"
- env['HTTP_X_IDENTITY_STATUS'] = "Invalid"
- else:
- return HTTPUnauthorized()
+ 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
- if self.app is None:
- proxy_headers['AUTHORIZATION'] = "Basic dTpw"
+ _decorate_request_headers('AUTHORIZATION',
+ "Basic %s" % self.service_pass,
+ proxy_headers,
+ env)
+ if self.app:
+ # Pass to downstream WSGI component
+ return self.app(env, custom_start_response)
+ else:
# We are forwarding to a remote service (no downstream WSGI app)
req = Request(proxy_headers)
parsed = urlparse(req.url)
@@ -181,11 +239,8 @@ class TokenAuth(object):
data = resp.read()
#TODO: use a more sophisticated proxy
# we are rewriting the headers now
- return Response(status=resp.status, body=data)(env, start_response)
- else:
- # Pass to downstream WSGI component
- env['HTTP_AUTHORIZATION'] = "Basic dTpw"
- return self.app(env, custom_start_response)
+ return Response(status=resp.status, body=data)(proxy_headers,
+ start_response)
def filter_factory(global_conf, **local_conf):
@@ -194,16 +249,17 @@ def filter_factory(global_conf, **local_conf):
conf.update(local_conf)
def auth_filter(app):
- return TokenAuth(app, conf)
+ return AuthProtocol(app, conf)
return auth_filter
+
def app_factory(global_conf, **local_conf):
conf = global_conf.copy()
conf.update(local_conf)
- return TokenAuth(None, conf)
+ return AuthProtocol(None, conf)
if __name__ == "__main__":
app = loadapp("config:" + \
os.path.join(os.path.abspath(os.path.dirname(__file__)),
- "auth_protocol_token.ini"), global_conf={"log_name": "echo.log"})
+ "auth_token.ini"), global_conf={"log_name": "auth_token.log"})
wsgi.server(eventlet.listen(('', 8090)), app)
diff --git a/keystone/db/sqlalchemy/api.py b/keystone/db/sqlalchemy/api.py
index a8551b9d..a0910778 100644
--- a/keystone/db/sqlalchemy/api.py
+++ b/keystone/db/sqlalchemy/api.py
@@ -85,6 +85,7 @@ def user_get(id, session=None):
joinedload('tenants')).filter_by(id=id).first()
return result
+
def user_get_by_tenant(tenant_id, session=None):
if not session:
session = get_session()
@@ -92,6 +93,7 @@ def user_get_by_tenant(tenant_id, session=None):
tenant_id=tenant_id)
return result
+
def user_groups(id, session=None):
if not session:
session = get_session()
@@ -99,6 +101,7 @@ def user_groups(id, session=None):
user_id=id)
return result
+
def user_update(id, values, session=None):
if not session:
session = get_session()
@@ -107,19 +110,22 @@ def user_update(id, values, session=None):
user_ref.update(values)
user_ref.save(session=session)
+
def user_delete(id, session=None):
if not session:
session = get_session()
with session.begin():
user_ref = user_get(id, session)
session.delete(user_ref)
-
+
+
def group_get(id, session=None):
if not session:
session = get_session()
result = session.query(models.Group).filter_by(id=id).first()
return result
+
def group_users(id, session=None):
if not session:
session = get_session()
@@ -127,19 +133,22 @@ def group_users(id, session=None):
group_id=id)
return result
+
def group_get_all(session=None):
if not session:
session = get_session()
result = session.query(models.Group)
return result
+
def group_delete(id, session=None):
if not session:
session = get_session()
with session.begin():
group_ref = group_get(id, session)
session.delete(group_ref)
-
+
+
def token_create(values):
token_ref = models.Token()
token_ref.update(values)
diff --git a/keystone/db/sqlalchemy/session.py b/keystone/db/sqlalchemy/session.py
index 836a0e96..10e727a7 100644
--- a/keystone/db/sqlalchemy/session.py
+++ b/keystone/db/sqlalchemy/session.py
@@ -29,17 +29,19 @@ from sqlalchemy.orm import sessionmaker
_ENGINE = None
_MAKER = None
+
def get_connection_string():
path = os.path.realpath(__file__)
dbpath = os.path.normpath(os.path.join(path,
- os.pardir, #sqlalchemy
- os.pardir, #db
- os.pardir #keystone
+ os.pardir, # sqlalchemy
+ os.pardir, # db
+ os.pardir # keystone
))
connection_string = "sqlite:///%s/keystone.db" % dbpath
logging.debug('SQL ALchemy connection string: %s', connection_string)
return connection_string
+
def get_session(autocommit=True, expire_on_commit=False):
"""Helper method to grab session"""
global _ENGINE
@@ -47,7 +49,7 @@ def get_session(autocommit=True, expire_on_commit=False):
if not _MAKER:
if not _ENGINE:
kwargs = {'pool_recycle': 30, 'echo': False}
- kwargs['poolclass'] = pool.NullPool #for SQLite3
+ kwargs['poolclass'] = pool.NullPool # for SQLite3
_ENGINE = create_engine(get_connection_string(), **kwargs)
_MAKER = (sessionmaker(bind=_ENGINE,
autocommit=autocommit,
diff --git a/keystone/identity.py b/keystone/identity.py
index 21228ed0..8bb6a4f7 100644
--- a/keystone/identity.py
+++ b/keystone/identity.py
@@ -15,10 +15,33 @@
# limitations under the License.
+"""
+Service that stores identities and issues and manages tokens
+
+HEADERS
+-------
+HTTP_ is a standard http header
+HTTP_X is an extended http header
+
+> Coming in from initial call
+HTTP_X_AUTH_TOKEN : the client token being passed in
+HTTP_X_STORAGE_TOKEN: the client token being passed in (legacy Rackspace use)
+ to support cloud files
+> Used for communication between components
+www-authenticate : only used if this component is being used remotely
+HTTP_AUTHORIZATION : basic auth password used to validate the connection
+
+> What we add to the request for use by the OpenStack service
+HTTP_X_AUTHORIZATION: the client identity being passed in
+
+"""
+
import functools
import logging
import os
import sys
+import eventlet
+from eventlet import wsgi
import bottle
from bottle import request
@@ -87,7 +110,7 @@ def send_result(code, result):
return content
-def get_request(model):
+def get_normalized_request_content(model):
"""initialize a model from json/xml contents of request body"""
ctype = request.environ.get("CONTENT_TYPE")
@@ -181,7 +204,7 @@ def get_xsd_atom_contract(xsd):
@bottle.route('/v1.0/token', method='POST')
@wrap_error
def authenticate():
- creds = get_request(auth.PasswordCredentials)
+ creds = get_normalized_request_content(auth.PasswordCredentials)
return send_result(200, service.authenticate(creds))
@@ -201,7 +224,6 @@ def delete_token(token_id):
return send_result(204,
service.revoke_token(get_auth_token(), token_id))
-
##
## Tenant Operations
##
@@ -209,7 +231,7 @@ def delete_token(token_id):
@bottle.route('/v1.0/tenants', method='POST')
@wrap_error
def create_tenant():
- tenant = get_request(tenants.Tenant)
+ tenant = get_normalized_request_content(tenants.Tenant)
return send_result(201,
service.create_tenant(get_auth_token(), tenant))
@@ -237,7 +259,7 @@ def get_tenant(tenant_id):
@bottle.route('/v1.0/tenants/:tenant_id', method='PUT')
@wrap_error
def update_tenant(tenant_id):
- tenant = get_request(tenants.Tenant)
+ tenant = get_normalized_request_content(tenants.Tenant)
rval = service.update_tenant(get_auth_token(), tenant_id, tenant)
return send_result(200, rval)
@@ -277,4 +299,4 @@ def get_extension(ext_alias):
if __name__ == "__main__":
- bottle.run(host='localhost', port=8080)
+ wsgi.server(eventlet.listen(('', 8080)), bottle.default_app())
diff --git a/keystone/keystone.ini b/keystone/keystone.ini
index 3fed30cf..626ca1ed 100644
--- a/keystone/keystone.ini
+++ b/keystone/keystone.ini
@@ -1,5 +1,16 @@
+;
+; This file not used
+;
[DEFAULT]
-[app:main]
+[composite:main]
+use = egg:Paste#urlmap
+/ = home
+/tenants = admin
+
+[app:home]
+use = egg:keystone
+
+[app:admin]
use = egg:keystone
diff --git a/keystone/middleware/papiauth.py b/keystone/middleware/remoteauth.py
index 0e86523a..5919abd6 100644
--- a/keystone/middleware/papiauth.py
+++ b/keystone/middleware/remoteauth.py
@@ -23,8 +23,8 @@ Auth Middleware that handles auth for a service
This module can be installed as a filter in front of your service to validate
that requests are coming from a trusted component that has handled
authenticating the call. If a call comes from an untrusted source, it will
-redirect it back to be properly authenticated. This is done by sending our a 305
-proxy redirect response with the URL for the auth service.
+redirect it back to be properly authenticated. This is done by sending our a
+305 proxy redirect response with the URL for the auth service.
The auth service settings are specified in the INI file (keystone.ini). The ini
file is passed in as the WSGI config file when starting the service. For this
@@ -38,20 +38,27 @@ Refer to: http://wiki.openstack.org/openstack-authn
HEADERS
-------
+HTTP_ is a standard http header
+HTTP_X is an extended http header
-www-authenticate : only used if this component is being used remotely
-HTTP_AUTHORIZATION : basic auth password used to validate the connection
-HTTP_X_AUTHORIZATION: the client token being passed in
+> Coming in from initial call
HTTP_X_AUTH_TOKEN : the client token being passed in
HTTP_X_STORAGE_TOKEN: the client token being passed in (legacy Rackspace use)
to support cloud files
+> Used for communication between components
+www-authenticate : only used if this component is being used remotely
+HTTP_AUTHORIZATION : basic auth password used to validate the connection
+
+> What we add to the request for use by the OpenStack service
+HTTP_X_AUTHORIZATION: the client identity being passed in
+
"""
from webob.exc import HTTPUseProxy, HTTPUnauthorized
-class PAPIAuth(object):
+class RemoteAuth(object):
# app is the downstream WSGI component, usually the OpenStack service
#
@@ -63,11 +70,11 @@ class PAPIAuth(object):
self.app = app
self.conf = conf
# where to redirect untrusted requests to go and auth
- self.proxy_location = conf.get('proxy_location')
+ self.auth_location = conf.get('auth_location')
# secret that will tell us a request is coming from a trusted auth
# component
- self.auth_pass = conf.get('auth_pass', 'dTpw')
- print 'Starting PAPI Auth middleware'
+ self.remote_auth_pass = conf.get('remote_auth_pass')
+ print 'Starting Remote Auth middleware'
def __call__(self, env, start_response):
# Validate the request is trusted
@@ -77,12 +84,12 @@ class PAPIAuth(object):
return HTTPUnauthorized(headers=headers)(env, start_response)
else:
auth_type, encoded_creds = env['HTTP_AUTHORIZATION'].split(None, 1)
- if encoded_creds != self.auth_pass:
+ if encoded_creds != self.remote_auth_pass:
return HTTPUnauthorized(headers=headers)(env, start_response)
# Make sure that the user has been authenticated by the Auth Service
if 'HTTP_X_AUTHORIZATION' not in env:
- return HTTPUseProxy(location=self.proxy_location)(env,
+ return HTTPUseProxy(location=self.auth_location)(env,
start_response)
return self.app(env, start_response)
@@ -94,5 +101,5 @@ def filter_factory(global_conf, **local_conf):
conf.update(local_conf)
def auth_filter(app):
- return PAPIAuth(app, conf)
+ return RemoteAuth(app, conf)
return auth_filter
diff --git a/management/getgroupusers.py b/management/getgroupusers.py
index 86fc700d..0fc7d231 100644
--- a/management/getgroupusers.py
+++ b/management/getgroupusers.py
@@ -13,7 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-
import optparse
import keystone.db.sqlalchemy.api as db_api
diff --git a/management/getuser.py b/management/getuser.py
index 68ccb454..036f10f4 100644
--- a/management/getuser.py
+++ b/management/getuser.py
@@ -16,6 +16,7 @@
import optparse
import keystone.db.sqlalchemy.api as db_api
+
def main():
usage = "usage: %prog username"
parser = optparse.OptionParser(usage)
diff --git a/management/useradd.py b/management/useradd.py
index 969a8a7e..fc3fc8b6 100644
--- a/management/useradd.py
+++ b/management/useradd.py
@@ -13,7 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-
import optparse
import keystone.db.sqlalchemy.api as db_api
from keystone.db.sqlalchemy import models
diff --git a/management/userupdate.py b/management/userupdate.py
index 8af517cc..89174d04 100644
--- a/management/userupdate.py
+++ b/management/userupdate.py
@@ -16,6 +16,7 @@
import optparse
import keystone.db.sqlalchemy.api as db_api
+
def main():
usage = "usage: %prog username email"
parser = optparse.OptionParser(usage)
diff --git a/test/unit/test_identity.py b/test/unit/test_identity.py
index 2cf2901e..ed3ea3fb 100644
--- a/test/unit/test_identity.py
+++ b/test/unit/test_identity.py
@@ -8,25 +8,24 @@ import identity
import unittest
from webtest import TestApp
import httplib2
+import json
from lxml import etree
+import unittest
+from webtest import TestApp
-try:
- import simplejson as json
-except ImportError:
- import json
URL = 'http://localhost:8080/v1.0/'
-def get_token(user, pswd, type=''):
+def get_token(user, pswd, kind=''):
h = httplib2.Http(".cache")
url = '%stoken' % URL
- body = '{"passwordCredentials" : { "username" : "%s", "password" :\
- "%s"} }' % (user, pswd)
- resp, content = h.request(url, "POST", body=body,\
- headers={"Content-Type": "application/json"})
+ body = {"passwordCredentials": {"username": user,
+ "password": pswd}}
+ resp, content = h.request(url, "POST", body=json.dumps(body),
+ headers={"Content-Type": "application/json"})
content = json.loads(content)
token = str(content['auth']['token']['id'])
- if type == 'token':
+ if kind == 'token':
return token
else:
return (resp, content)
@@ -76,13 +75,12 @@ def create_tenant(tenantid, auth_token):
h = httplib2.Http(".cache")
url = '%stenants' % (URL)
- body = '{"tenant": { "id": "%s", \
- "description": "A description ...", "enabled"\
- :true } }' % tenantid
- resp, content = h.request(url, "POST", body=body,\
- headers={"Content-Type": "application/json",\
- "X-Auth-Token": auth_token})
-
+ body = {"tenant": {"id": tenantid,
+ "description": "A description ...",
+ "enabled": True}}
+ resp, content = h.request(url, "POST", body=json.dumps(body),
+ headers={"Content-Type": "application/json",
+ "X-Auth-Token": auth_token})
return (resp, content)
@@ -152,15 +150,15 @@ class identity_test(unittest.TestCase):
def test_a_get_version(self):
h = httplib2.Http(".cache")
url = URL
- resp, content = h.request(url, "GET", body="",\
- headers={"Content-Type": "application/json"})
+ resp, content = h.request(url, "GET", body="",
+ headers={"Content-Type": "application/json"})
self.assertEqual(200, int(resp['status']))
self.assertEqual('application/json', resp['content-type'])
def test_a_get_version(self):
h = httplib2.Http(".cache")
url = URL
- resp, content = h.request(url, "GET", body="",\
+ resp, content = h.request(url, "GET", body="",
headers={"Content-Type": "application/xml",
"ACCEPT": "application/xml"})
self.assertEqual(200, int(resp['status']))
@@ -194,9 +192,9 @@ class authorize_test(identity_test):
def test_a_authorize_user_disaabled(self):
h = httplib2.Http(".cache")
url = '%stoken' % URL
- body = '{"passwordCredentials" : { "username": "disabled", "password":\
- "secrete"} }'
- resp, content = h.request(url, "POST", body=body,\
+ body = {"passwordCredentials": {"username": "disabled",
+ "password": "secrete"}}
+ resp, content = h.request(url, "POST", body=json.dumps(body),
headers={"Content-Type": "application/json"})
content = json.loads(content)
if int(resp['status']) == 500:
@@ -215,8 +213,8 @@ class authorize_test(identity_test):
password="secrete" username="disabled" \
/>'
resp, content = h.request(url, "POST", body=body,\
- headers={"Content-Type": "application/xml",
- "ACCEPT": "application/xml"})
+ headers={"Content-Type": "application/xml",
+ "ACCEPT": "application/xml"})
content = etree.fromstring(content)
if int(resp['status']) == 500:
self.fail('IDM fault')
@@ -227,9 +225,9 @@ class authorize_test(identity_test):
def test_a_authorize_user_wrong(self):
h = httplib2.Http(".cache")
url = '%stoken' % URL
- body = '{"passwordCredentials" : { "username-w" : "disabled", \
- "password" : "secrete"} }'
- resp, content = h.request(url, "POST", body=body,\
+ body = {"passwordCredentials": {"username-w": "disabled",
+ "password": "secrete"}}
+ resp, content = h.request(url, "POST", body=json.dumps(body),
headers={"Content-Type": "application/json"})
content = json.loads(content)
if int(resp['status']) == 500:
@@ -429,11 +427,11 @@ class create_tenant_test(tenant_test):
self.tenant = content['tenant']['id']
url = '%stenants' % (URL)
- body = '{"tenant": { "id": "%s", \
- "description": "A description ...", "enabled"\
- :true } }' % self.tenant
- resp, content = h.request(url, "POST", body=body,\
- headers={"Content-Type": "application/json",\
+ body = {"tenant": {"id": self.tenant,
+ "description": "A description ...",
+ "enabled": True}}
+ resp, content = h.request(url, "POST", body=json.dumps(body),
+ headers={"Content-Type": "application/json",
"X-Auth-Token": self.token})
if int(resp['status']) == 500:
@@ -473,11 +471,11 @@ class create_tenant_test(tenant_test):
self.tenant = content['tenant']['id']
url = '%stenants' % (URL)
- body = '{"tenant": { "id": "%s", \
- "description": "A description ...", "enabled"\
- :true } }' % self.tenant
- resp, content = h.request(url, "POST", body=body,\
- headers={"Content-Type": "application/json",\
+ body = {"tenant": {"id": self.tenant,
+ "description": "A description ...",
+ "enabled": True}}
+ resp, content = h.request(url, "POST", body=json.dumps(body),
+ headers={"Content-Type": "application/json",
"X-Auth-Token": self.exp_auth_token})
if int(resp['status']) == 500:
@@ -518,10 +516,10 @@ class create_tenant_test(tenant_test):
self.tenant = content['tenant']['id']
url = '%stenants' % (URL)
- body = '{"tenant": { "id": "%s", \
- "description": "A description ...", "enabled"\
- :true } }' % self.tenant
- resp, content = h.request(url, "POST", body=body,\
+ body = {"tenant": {"id": self.tenant,
+ "description": "A description ...",
+ "enabled": True}}
+ resp, content = h.request(url, "POST", body=json.dumps(body),
headers={"Content-Type": "application/json"})
if int(resp['status']) == 500: