310 lines
8.5 KiB
Python
310 lines
8.5 KiB
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.
|
|
|
|
|
|
"""
|
|
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
|
|
from bottle import response
|
|
from queryext import exthandler
|
|
|
|
# If ../keystone/__init__.py exists, add ../ to Python search path, so that
|
|
# it will override what happens to be installed in /usr/(local/)lib/python...
|
|
POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
|
os.pardir,
|
|
os.pardir))
|
|
if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'keystone', '__init__.py')):
|
|
sys.path.insert(0, POSSIBLE_TOPDIR)
|
|
|
|
import keystone.logic.service as serv
|
|
import keystone.logic.types.auth as auth
|
|
import keystone.logic.types.tenant as tenants
|
|
import keystone.logic.types.fault as fault
|
|
|
|
VERSION_STATUS = "ALPHA"
|
|
VERSION_DATE = "2011-04-23T00:00:00Z"
|
|
|
|
bottle.debug(True)
|
|
|
|
service = serv.IDMService()
|
|
|
|
##
|
|
## Override error pages
|
|
##
|
|
|
|
|
|
@bottle.error(400)
|
|
@bottle.error(401)
|
|
@bottle.error(403)
|
|
@bottle.error(404)
|
|
@bottle.error(409)
|
|
@bottle.error(415)
|
|
@bottle.error(500)
|
|
@bottle.error(503)
|
|
def error_handler(err):
|
|
return err.output
|
|
|
|
|
|
def is_xml_response():
|
|
if not "Accept" in request.header:
|
|
return False
|
|
return request.header["Accept"] == "application/xml"
|
|
|
|
|
|
def get_app_root():
|
|
return os.path.abspath(os.path.dirname(__file__))
|
|
|
|
|
|
def send_result(code, result):
|
|
content = None
|
|
response.content_type = None
|
|
if result:
|
|
if is_xml_response():
|
|
content = result.to_xml()
|
|
response.content_type = "application/xml"
|
|
else:
|
|
content = result.to_json()
|
|
response.content_type = "application/json"
|
|
response.status = code
|
|
if code > 399:
|
|
return bottle.abort(code, content)
|
|
return content
|
|
|
|
|
|
def get_normalized_request_content(model):
|
|
"""initialize a model from json/xml contents of request body"""
|
|
|
|
ctype = request.environ.get("CONTENT_TYPE")
|
|
if ctype == "application/xml":
|
|
ret = model.from_xml(request.body.read())
|
|
elif ctype == "application/json":
|
|
ret = model.from_json(request.body.read())
|
|
else:
|
|
raise fault.IDMFault("I don't understand the content type ", code=415)
|
|
return ret
|
|
|
|
|
|
def get_auth_token():
|
|
auth_token = None
|
|
if "X-Auth-Token" in request.header:
|
|
auth_token = request.header["X-Auth-Token"]
|
|
return auth_token
|
|
|
|
|
|
def wrap_error(func):
|
|
@functools.wraps(func)
|
|
def check_error(*args, **kwargs):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except Exception as err:
|
|
if isinstance(err, fault.IDMFault):
|
|
send_result(err.code, err)
|
|
else:
|
|
logging.exception(err)
|
|
send_result(500, fault.IDMFault("Unhandled error", str(err)))
|
|
return check_error
|
|
|
|
|
|
@bottle.route('/v1.0', method='GET')
|
|
@bottle.route('/v1.0/', method='GET')
|
|
@wrap_error
|
|
def get_version_info():
|
|
if is_xml_response():
|
|
resp_file = "content/version.xml"
|
|
response.content_type = "application/xml"
|
|
else:
|
|
resp_file = "content/version.json"
|
|
response.content_type = "application/json"
|
|
hostname = request.environ.get("SERVER_NAME")
|
|
port = request.environ.get("SERVER_PORT")
|
|
return bottle.template(resp_file, HOST=hostname, PORT=port,
|
|
VERSION_STATUS=VERSION_STATUS,
|
|
VERSION_DATE=VERSION_DATE)
|
|
|
|
##
|
|
## Version links:
|
|
##
|
|
|
|
|
|
@bottle.route('/v1.0/idmdevguide.pdf', method='GET')
|
|
@wrap_error
|
|
def get_pdf_contract():
|
|
return bottle.static_file("content/idmdevguide.pdf",
|
|
root=get_app_root(),
|
|
mimetype="application/pdf")
|
|
|
|
|
|
@bottle.route('/v1.0/identity.wadl', method='GET')
|
|
@wrap_error
|
|
def get_wadl_contract():
|
|
return bottle.static_file("identity.wadl",
|
|
root=get_app_root(),
|
|
mimetype="application/vnd.sun.wadl+xml")
|
|
|
|
|
|
@bottle.route('/v1.0/xsd/:xsd', method='GET')
|
|
@wrap_error
|
|
def get_xsd_contract(xsd):
|
|
return bottle.static_file("/xsd/" + xsd,
|
|
root=get_app_root(),
|
|
mimetype="application/xml")
|
|
|
|
|
|
@bottle.route('/v1.0/xsd/atom/:xsd', method='GET')
|
|
@wrap_error
|
|
def get_xsd_atom_contract(xsd):
|
|
return bottle.static_file("/xsd/atom/" + xsd,
|
|
root=get_app_root(),
|
|
mimetype="application/xml")
|
|
|
|
##
|
|
## Token Operations
|
|
##
|
|
|
|
|
|
@bottle.route('/v1.0/token', method='POST')
|
|
@wrap_error
|
|
def authenticate():
|
|
creds = get_normalized_request_content(auth.PasswordCredentials)
|
|
return send_result(200, service.authenticate(creds))
|
|
|
|
|
|
@bottle.route('/v1.0/token/:token_id', method='GET')
|
|
@wrap_error
|
|
def validate_token(token_id):
|
|
belongs_to = None
|
|
if "belongsTo" in request.GET:
|
|
belongs_to = request.GET["belongsTo"]
|
|
rval = service.validate_token(get_auth_token(), token_id, belongs_to)
|
|
return send_result(200, rval)
|
|
|
|
|
|
@bottle.route('/v1.0/token/:token_id', method='DELETE')
|
|
@wrap_error
|
|
def delete_token(token_id):
|
|
return send_result(204,
|
|
service.revoke_token(get_auth_token(), token_id))
|
|
|
|
##
|
|
## Tenant Operations
|
|
##
|
|
|
|
|
|
@bottle.route('/v1.0/tenants', method='POST')
|
|
@wrap_error
|
|
def create_tenant():
|
|
tenant = get_normalized_request_content(tenants.Tenant)
|
|
return send_result(201,
|
|
service.create_tenant(get_auth_token(), tenant))
|
|
|
|
|
|
@bottle.route('/v1.0/tenants', method='GET')
|
|
@wrap_error
|
|
def get_tenants():
|
|
marker = None
|
|
if "marker" in request.GET:
|
|
marker = request.GET["marker"]
|
|
limit = None
|
|
if "limit" in request.GET:
|
|
limit = request.GET["limit"]
|
|
tenants = service.get_tenants(get_auth_token(), marker, limit)
|
|
return send_result(200, tenants)
|
|
|
|
|
|
@bottle.route('/v1.0/tenants/:tenant_id', method='GET')
|
|
@wrap_error
|
|
def get_tenant(tenant_id):
|
|
tenant = service.get_tenant(get_auth_token(), tenant_id)
|
|
return send_result(200, tenant)
|
|
|
|
|
|
@bottle.route('/v1.0/tenants/:tenant_id', method='PUT')
|
|
@wrap_error
|
|
def update_tenant(tenant_id):
|
|
tenant = get_normalized_request_content(tenants.Tenant)
|
|
rval = service.update_tenant(get_auth_token(), tenant_id, tenant)
|
|
return send_result(200, rval)
|
|
|
|
|
|
@bottle.route('/v1.0/tenants/:tenant_id', method='DELETE')
|
|
@wrap_error
|
|
def delete_tenant(tenant_id):
|
|
rval = service.delete_tenant(get_auth_token(), tenant_id)
|
|
return send_result(204, rval)
|
|
|
|
|
|
##
|
|
## Extensions
|
|
##
|
|
|
|
@bottle.route('/v1.0/extensions', method='GET')
|
|
@wrap_error
|
|
def get_extensions():
|
|
if is_xml_response():
|
|
resp_file = "content/extensions.xml"
|
|
mimetype = "application/xml"
|
|
else:
|
|
resp_file = "content/extensions.json"
|
|
mimetype = "application/json"
|
|
return bottle.static_file(resp_file,
|
|
root=get_app_root(),
|
|
mimetype=mimetype)
|
|
|
|
|
|
@bottle.route('/v1.0/extensions/:ext_alias', method='GET')
|
|
@wrap_error
|
|
def get_extension(ext_alias):
|
|
#
|
|
# Todo: Define some extensions :-)
|
|
#
|
|
raise fault.ItemNotFoundFault("The extension is not found")
|
|
|
|
def start_server(port=8080):
|
|
app = exthandler.UrlExtensionFilter(bottle.default_app(), None)
|
|
wsgi.server(eventlet.listen(('', port)), app)
|
|
|
|
if __name__ == "__main__":
|
|
start_server()
|
|
|
|
|