diff options
50 files changed, 2381 insertions, 1311 deletions
@@ -1,5 +1,5 @@ -Keystone: Identity Service -========================== +Keystone: OpenStack Identity Service +==================================== Keystone is a proposed independent authentication service for [OpenStack](http://www.openstack.org). @@ -18,80 +18,47 @@ SERVICES: Also included: +* Keystone - Service and Admin API are available separately. Admin API allows management of tenants, roles, and users as well. * 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 -ENVIRONMENT & DEPENDENCIES: ---------------------------- -See pip-requires for dependency list - -Setup: -Install http://pypi.python.org/pypi/setuptools - sudo easy_install pip - sudo pip install -r pip-requires - -Configuration: -Keystone gets its configuration from command-line parameters or a .conf file. The file can be provided explicitely -on the command line otherwise the following logic applies (the conf file in use will be output to help -in troubleshooting: - -1. config.py takes the config file from <topdir>/etc/keystone.conf -2. If the keystone package is also intalled on the system, - /etc/keystone.conf or /etc/keystone/keystone.conf have higher priority than <top_dir>/etc/keystone.conf. - -If you are also doing development on a system that has keystone.conf installed in /etc you may need to disambiguate it by providing the conf file in the command-line - - $ bin/keystone-control --confg-file etc/keystone.conf --pid-file <pidfile> auth <start|stop|restart> - -Path: -keystone-control calls keystone-auth and it needs to be in the PATH - - $ export PATH=<top_dir>/bin:$PATH - - RUNNING KEYSTONE: ----------------- - $ cd bin - $ ./keystone-auth +Starting both Admin and Service API endpoints: + $ cd bin + $ ./keystone -RUNNING KEYSTONE FOR DEVELOPMENT (HACKING): ------------------------------- - -During development, you can simply run as user (root not needed) - -From the top Keystone directory (<topdir>) +Starting the auth server only (exposes the Service API): - $ bin/keystone=auth + $ cd bin + $ ./keystone-auth -It dumps stdout and stderr onto the terminal. +Starting the admin server only (exposes the Admin API): -If you want to specify additional parameters (optional): + $ cd bin + $ ./keystone-admin - $ bin/keystone-control --pid-file <pidfile> --config-file etc/keystone.conf auth <start|stop|restart> +All above files take parameters from etc/keystone.conf file under the Keystone root folder by default -RUNNING KEYSTONE AS ROOT IN PRODUCTION --------------------------------------- -In production, stdout and stderr need to be closed and all the output needs to be redirected to a log file. -Once the package is installed through setup tools, RPM, deb, or ebuild keystone-control is installed as /usr/sbin/keystone-control. Typically, it will be started a script in /etc/init.d/keystoned -keystone-control can invoke keystone-auth and start the keystone daemon with - $ /usr/sbin/keystone-control auth start +DEPENDENCIES: +------------- +See pip-requires for dependency list. The list of dependencies should not add to what already is needed to run other OpenStack services. -It writes the process id of the daemon into /var/run/keystone/keystine-auth.pid. -The daemon can be stopped with - - $ /usr/sbin/keystone-control auth stop +Setup: -keystone-control has the infrastructure to start and stop multiple servers keystone-xxx + # Install http://pypi.python.org/pypi/setuptools + sudo easy_install pip + sudo pip install -r pip-requires -RUNNING TEST SERVICE: ---------------------- +RUNNING THE TEST SERVICE (Echo.py): +---------------------------------- Standalone stack (with Auth_Token) $ cd echo/bin @@ -103,39 +70,43 @@ RUNNING TEST SERVICE: in separate session $ cd keystone/auth_protocols - $ python auth_token.py --remote + $ python auth_token.py DEMO CLIENT: ---------------------- +------------ +A sample client that gets a token from Keystone and then uses it to call Echo (and a few other example calls): + $ cd echo/echo $ python echo_client.py - Note: this requires tests data. See section TESTING for initializing data + Note: this requires test data. See section TESTING for initializing data -TESTING -------- -After starting keystone a keystone.db sqlite database should be created in the keystone folder. +TESTING: +-------- +A set of sample data can be added by running a shell script: -Add test data to the database: + $ ./bin/sampledata.sh - $ sqlite3 bin/keystone.db < test/test_setup.sql +The script calls keystone-manage to create the sample data. -To clean the test database +After starting keystone or running keystone-manage a keystone.db sqlite database should be created in the keystone folder. - $ sqlite3 bin/keystone.db < test/kill.sql To run client demo (with all auth middleware running locally on sample service): $ ./echo/bin/echod $ python echo/echo/echo_client.py +NOTE: NOT ALL TESTS CONVERTED TO NEW MODEL YET. MANY FAIL. THIS WILL BE ADDRESSED SOON. + To run unit tests: + * go to unit test/unit directory * run tests: python test_keystone -There are 8 groups of tests. They can be run individually or as an entire colection. To run the entire test suite run +There are 10 groups of tests. They can be run individually or as an entire colection. To run the entire test suite run $ python test_keystone.py @@ -150,12 +121,49 @@ For more on unit testing please refer To perform contract validation and load testing, use SoapUI (for now). + Using SOAPUI: -Download [SOAPUI](http://sourceforge.net/projects/soapui/files/): +First, download [SOAPUI](http://sourceforge.net/projects/soapui/files/): To Test Keystone Service: * File->Import Project * Select tests/IdentitySOAPUI.xml * Double click on "Keystone Tests" and press the green play (>) button + + +ADDITIONAL INFORMATION: +----------------------- + +Configuration: +Keystone gets its configuration from command-line parameters or a .conf file. The file can be provided explicitely +on the command line otherwise the following logic applies (the conf file in use will be output to help +in troubleshooting: + +1. config.py takes the config file from <topdir>/etc/keystone.conf +2. If the keystone package is also intalled on the system, + /etc/keystone.conf or /etc/keystone/keystone.conf have higher priority than <top_dir>/etc/keystone.conf. + +CURL commands: + +curl -d '{"passwordCredentials": {"username": "joeuser", "password": "secrete"}}' -H "Content-type: application/json" http://localhost:8081/v2.0/token +curl -d '{"passwordCredentials": {"username": "joeuser", "password": "secrete", "tenant": "1234"}}' -H "Content-type: application/json" http://localhost:8081/v2.0/token + +NOVA Integration: +----------------- + +Initial support for using keystone as nova's identity component has been started. + + # clone projects + bzr clone lp:nova + git clone git://github.com/khussein/keystone.git + + # copy keystone librarys into nova + cp keystone/keystone/common/bufferedhttp.py nova/nova/auth/ + cp keystone/keystone/auth_protocols/nova_auth_token.py nova/nova/auth/ + + # copy paste config to use nova_auth_token.py + cp keystone/docs/nova-api-paste.ini nova/etc/nova/api-paste.ini + +Assuming you added the test_sql, you can then use joeuser/secrete diff --git a/bin/keystone b/bin/keystone index 45693a3f..1afebe8b 100755 --- a/bin/keystone +++ b/bin/keystone @@ -1,196 +1,83 @@ #!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright (c) 2011 OpenStack, LLC. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. # -# 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 +# 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 +# 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. +# 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. """ -Helper script for starting/stopping/reloading OpenStack server programs. -Code copied from swift. Should eventually come from common. +Keystone Identity Server - Admin and Service API """ -from __future__ import with_statement - -import errno -import os import optparse -import resource -import signal +import os import sys -import time +import tools.tracer #load this first +# 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, os.pardir)) if os.path.exists(os.path.join(possible_topdir, 'keystone', '__init__.py')): sys.path.insert(0, possible_topdir) -from keystone import version +import keystone from keystone.common import config -ALL_COMMANDS = ['start', 'stop', 'restart'] - -MAX_DESCRIPTORS = 32768 -MAX_MEMORY = (1024 * 1024 * 1024) * 1 # 2 GB -USAGE = """%prog [options] keystone <COMMAND> [CONFPATH] - -And command is one of: - - start, stop, restart - -And CONFPATH is the optional configuration file to use.""" - - -def pid_files(server, options): - pid_files = [] - if options['pid_file']: - if os.path.exists(os.path.abspath(options['pid_file'])): - pid_files = [os.path.abspath(options['pid_file'])] - else: - if os.path.exists('/var/run/keystone/%s.pid' % server): - pid_files = ['/var/run/keystone/%s.pid' % server] - for pid_file in pid_files: - pid = int(open(pid_file).read().strip()) - yield pid_file, pid - - -def do_start(server, options, args): - for pid_file, pid in pid_files(server, options): - if os.path.exists('/proc/%s' % pid): - print "%s appears to already be running: %s" % (server, pid_file) - return - else: - print "Removing stale pid file %s" % pid_file - os.unlink(pid_file) - - try: - resource.setrlimit(resource.RLIMIT_NOFILE, - (MAX_DESCRIPTORS, MAX_DESCRIPTORS)) - resource.setrlimit(resource.RLIMIT_DATA, - (MAX_MEMORY, MAX_MEMORY)) - except ValueError: - print "Unable to increase file descriptor limit. Running as non-root?" - os.environ['PYTHON_EGG_CACHE'] = '/tmp' - - def write_pid_file(pid_file, pid): - dir, file = os.path.split(pid_file) - if not os.path.exists(dir): - try: - os.makedirs(dir) - except OSError, err: - if err.errno == errno.EACCES: - sys.exit('Unable to create %s. Running as non-root?' - % dir) - fp = open(pid_file, 'w') - fp.write('%d\n' % pid) - fp.close() - - def launch(ini_file, pid_file): - args = [server, ini_file] - print 'Starting %s with %s' % (server, ini_file) - - pid = os.fork() - if pid == 0: - os.setsid() - with open(os.devnull, 'r+b') as nullfile: - for desc in (0, 1, 2): # close stdio - try: - os.dup2(nullfile.fileno(), desc) - except OSError: - pass - try: - os.execlp('%s' % server, server, ini_file) - except OSError, e: - sys.exit('unable to launch %s. Got error: %s' - % (server, str(e))) - sys.exit(0) - else: - write_pid_file(pid_file, pid) - - if not options['pid_file']: - pid_file = '/var/run/keystone/%s.pid' % server - else: - pid_file = os.path.abspath(options['pid_file']) - conf_file = config.find_config_file(options, args) - if not conf_file: - sys.exit("Could not find any configuration file to use!") - launch_args = [(conf_file, pid_file)] - - # start all servers - for conf_file, pid_file in launch_args: - launch(conf_file, pid_file) - - -def do_stop(server, options, args, graceful=False): - if graceful and server in GRACEFUL_SHUTDOWN_SERVERS: - sig = signal.SIGHUP - else: - sig = signal.SIGTERM - - did_anything = False - pfiles = pid_files(server, options) - for pid_file, pid in pfiles: - did_anything = True - try: - print 'Stopping %s pid: %s signal: %s' % (server, pid, sig) - os.kill(pid, sig) - except OSError: - print "Process %d not running" % pid - try: - os.unlink(pid_file) - except OSError: - pass - for pid_file, pid in pfiles: - for _junk in xrange(150): # 15 seconds - if not os.path.exists('/proc/%s' % pid): - break - time.sleep(0.1) - else: - print 'Waited 15 seconds for pid %s (%s) to die; giving up' % \ - (pid, pid_file) - if not did_anything: - print 'No %s running' % server +from keystone.common import wsgi if __name__ == '__main__': - oparser = optparse.OptionParser(usage=USAGE, version='%%prog %s' - % version.version_string()) - oparser.add_option('--pid-file', default=None, metavar="PATH", - help="File to use as pid file. Default: " - "/var/run/keysonte/keystone.pid") - config.add_common_options(oparser) - (options, args) = config.parse_options(oparser) - - if len(args) < 1: - oparser.print_usage() - sys.exit(1) - - server = 'keystone' - command = args.pop(0).lower() - if command not in ALL_COMMANDS: - command_list = ", ".join(ALL_COMMANDS) - msg = ("Unknown command %(command)s specified. Please specify a " - "command in this list: %(command_list)s" % locals()) - sys.exit(msg) - - if command == 'start': - do_start(server, options, args) - - if command == 'stop': - do_stop(server, options, args) - - if command == 'restart': - do_stop(server, options, args) - do_start(server, options, args) + # Initialize a parser for our configuration paramaters + parser = optparse.OptionParser(version='%%prog %s' % keystone.version) + common_group = config.add_common_options(parser) + config.add_log_options(parser) + + # Handle a special argument to support starting two endpoints + common_group.add_option('-a', '--admin-port', default=8081, + dest="admin_port", metavar="PORT", + help = "specifies port for Admin API to listen" + "on (default is 8080)") + + # Parse arguments and load config + (options, args) = config.parse_options(parser) + config_file = config.find_config_file(options, args) + print "Using config file:", config_file + + # Start services + try: + # Load Service API server + conf, app = config.load_paste_app('server', options, args) + server = wsgi.Server() + server.start(app, int(conf['bind_port']), conf['bind_host']) + print "Service API listening on %s:%s" % (conf['bind_host'], + conf['bind_port']) + + # Load Admin API server + admin_conf, admin_app = config.load_paste_app('admin', options, args) + admin_server = wsgi.Server() + admin_bind = options.get('admin_port') or admin_conf.get('bind_port') + if conf['bind_port'] == admin_bind: + admin_bind += 1 + admin_server.start(admin_app, int(admin_bind), + admin_conf['bind_host']) + print "Admin API listening on %s:%s" % (admin_conf['bind_host'], + admin_bind) + + # Wait until done + server.wait() + except RuntimeError, e: + sys.exit("ERROR: %s" % e) diff --git a/bin/keystone-admin b/bin/keystone-admin new file mode 100755 index 00000000..11442673 --- /dev/null +++ b/bin/keystone-admin @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +Keystone Identity Server - Admin API +""" + +import optparse +import os +import sys +import tools.tracer #load this first + +# 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 +from keystone.common import config +from keystone.common import wsgi + + +if __name__ == '__main__': + # Initialize a parser for our configuration paramaters + parser = optparse.OptionParser(version='%%prog %s' % keystone.version) + common_group = config.add_common_options(parser) + config.add_log_options(parser) + + # Parse arguments and load config + (options, args) = config.parse_options(parser) + config_file = config.find_config_file(options, args) + print "Using config file:", config_file + + # Start services + try: + # Load Admin API server + conf, app = config.load_paste_app('admin', options, args) + server = wsgi.Server() + server.start(app, int(conf['bind_port']), conf['bind_host']) + print "Admin API listening on %s:%s" % (conf['bind_host'], + conf['bind_port']) + + server.wait() + except RuntimeError, e: + sys.exit("ERROR: %s" % e) diff --git a/bin/keystone-auth b/bin/keystone-auth index 8dc71aaa..3e59d3e8 100755 --- a/bin/keystone-auth +++ b/bin/keystone-auth @@ -19,12 +19,13 @@ # under the License. """ -Keystone API Server +Keystone Identity Server - Service API """ import optparse import os import sys +import tools.tracer #load this first # 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... @@ -34,33 +35,29 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'keystone', '__init__.py')): sys.path.insert(0, possible_topdir) -from keystone import version +import keystone from keystone.common import config from keystone.common import wsgi -def create_options(parser): - """ - Sets up the CLI and config-file options that may be - parsed and program commands. - - :param parser: The option parser - """ - config.add_common_options(parser) +if __name__ == '__main__': + # Initialize a parser for our configuration paramaters + parser = optparse.OptionParser(version='%%prog %s' % keystone.version) + common_group = config.add_common_options(parser) config.add_log_options(parser) + # Parse arguments and load config + (options, args) = config.parse_options(parser) + config_file = config.find_config_file(options, args) + print "Using config file:", config_file -if __name__ == '__main__': - oparser = optparse.OptionParser(version='%%prog %s' - % version.version_string()) - create_options(oparser) - (options, args) = config.parse_options(oparser) - + # Start services try: + # Load Service API server conf, app = config.load_paste_app('server', options, args) server = wsgi.Server() server.start(app, int(conf['bind_port']), conf['bind_host']) - print "Server listening on %s:%s" % (conf['bind_host'], + print "Service API listening on %s:%s" % (conf['bind_host'], conf['bind_port']) server.wait() except RuntimeError, e: diff --git a/bin/keystone-control b/bin/keystone-control deleted file mode 100755 index 3061f0c8..00000000 --- a/bin/keystone-control +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 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. - -""" -Helper script for starting/stopping/reloading Keystone server programs. -Thanks for some of the code, Swifties ;) -""" - -from __future__ import with_statement - -import errno -import os -import optparse -import resource -import signal -import sys -import time - -# 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) - -from keystone import version -from keystone.common import config - -ALL_COMMANDS = ['start', 'stop', 'shutdown', 'restart', - 'reload', 'force-reload'] -ALL_SERVERS = ['keystone-auth'] -GRACEFUL_SHUTDOWN_SERVERS = ['keystone-auth'] -MAX_DESCRIPTORS = 32768 -MAX_MEMORY = (1024 * 1024 * 1024) * 2 # 2 GB -USAGE = """%prog [options] <SERVER> <COMMAND> [CONFPATH] - -Where <SERVER> is one of: - - all, auth - -And command is one of: - - start, stop, shutdown, restart, reload, force-reload - -And CONFPATH is the optional configuration file to use.""" - - -def pid_files(server, options): - pid_files = [] - if options['pid_file']: - if os.path.exists(os.path.abspath(options['pid_file'])): - pid_files = [os.path.abspath(options['pid_file'])] - else: - if os.path.exists('/var/run/keystone/%s.pid' % server): - pid_files = ['/var/run/keystone/%s.pid' % server] - for pid_file in pid_files: - pid = int(open(pid_file).read().strip()) - yield pid_file, pid - - -def do_start(server, options, args): - server_type = '-'.join(server.split('-')[:-1]) - - for pid_file, pid in pid_files(server, options): - if os.path.exists('/proc/%s' % pid): - print "%s appears to already be running: %s" % (server, pid_file) - return - else: - print "Removing stale pid file %s" % pid_file - os.unlink(pid_file) - - os.environ['PYTHON_EGG_CACHE'] = '/tmp' - - def write_pid_file(pid_file, pid): - dir, file = os.path.split(pid_file) - if not os.path.exists(dir): - try: - os.makedirs(dir) - except OSError, err: - if err.errno == errno.EACCES: - sys.exit('Unable to create %s. Running as non-root?' - % dir) - fp = open(pid_file, 'w') - fp.write('%d\n' % pid) - fp.close() - - def launch(ini_file, pid_file): - args = [server, ini_file] - print 'Starting %s with %s' % (server, ini_file) - - pid = os.fork() - if pid == 0: - os.setsid() - with open(os.devnull, 'r+b') as nullfile: - for desc in (0, 1, 2): # close stdio - try: - os.dup2(nullfile.fileno(), desc) - except OSError: - pass - try: - os.execlp('%s' % server, server, ini_file) - except OSError, e: - sys.exit('unable to launch %s. Got error: %s' - % (server, str(e))) - sys.exit(0) - else: - write_pid_file(pid_file, pid) - - if not options['pid_file']: - pid_file = '/var/run/keystone/%s.pid' % server - else: - pid_file = os.path.abspath(options['pid_file']) - conf_file = config.find_config_file(options, args) - if not conf_file: - sys.exit("Could not find any configuration file to use!") - launch_args = [(conf_file, pid_file)] - - # start all servers - for conf_file, pid_file in launch_args: - launch(conf_file, pid_file) - - -def do_stop(server, options, args, graceful=False): - if graceful and server in GRACEFUL_SHUTDOWN_SERVERS: - sig = signal.SIGHUP - else: - sig = signal.SIGTERM - - did_anything = False - pfiles = pid_files(server, options) - for pid_file, pid in pfiles: - did_anything = True - try: - print 'Stopping %s pid: %s signal: %s' % (server, pid, sig) - os.kill(pid, sig) - except OSError: - print "Process %d not running" % pid - try: - os.unlink(pid_file) - except OSError: - pass - for pid_file, pid in pfiles: - for _junk in xrange(150): # 15 seconds - if not os.path.exists('/proc/%s' % pid): - break - time.sleep(0.1) - else: - print 'Waited 15 seconds for pid %s (%s) to die; giving up' % \ - (pid, pid_file) - if not did_anything: - print 'No %s running' % server - - -if __name__ == '__main__': - oparser = optparse.OptionParser(usage=USAGE, version='%%prog %s' - % version.version_string()) - oparser.add_option('--pid-file', default=None, metavar="PATH", - help="File to use as pid file. Default: " - "/var/run/keystone/$server.pid") - config.add_common_options(oparser) - (options, args) = config.parse_options(oparser) - - if len(args) < 2: - oparser.print_usage() - sys.exit(1) - - server = args.pop(0).lower() - if server == 'all': - servers = ALL_SERVERS - else: - if not server.startswith('keystone-'): - server = 'keystone-%s' % server - if server not in ALL_SERVERS: - server_list = ", ".join([s.replace('keystone-', '') - for s in ALL_SERVERS]) - msg = ("Unknown server '%(server)s' specified. Please specify " - "all, or one of the servers: %(server_list)s" % locals()) - sys.exit(msg) - servers = [server] - - command = args.pop(0).lower() - if command not in ALL_COMMANDS: - command_list = ", ".join(ALL_COMMANDS) - msg = ("Unknown command %(command)s specified. Please specify a " - "command in this list: %(command_list)s" % locals()) - sys.exit(msg) - - if command == 'start': - for server in servers: - do_start(server, options, args) - - if command == 'stop': - for server in servers: - do_stop(server, options, args) - - if command == 'shutdown': - for server in servers: - do_stop(server, options, args, graceful=True) - - if command == 'restart': - for server in servers: - do_stop(server, options, args) - for server in servers: - do_start(server, options, args) - - if command == 'reload' or command == 'force-reload': - for server in servers: - do_stop(server, options, args, graceful=True) - do_start(server, options, args) diff --git a/bin/keystone-manage b/bin/keystone-manage new file mode 100755 index 00000000..2d5aecea --- /dev/null +++ b/bin/keystone-manage @@ -0,0 +1,280 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +Keystone Identity Server - CLI Management Interface +""" + +import datetime +import optparse +import os +import sys +import tools.tracer + +# 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 +from keystone.common import config +import keystone.db.sqlalchemy.api as db_api +import keystone.db.sqlalchemy.models as db_models + + +def Main(): + """ + Usage: keystone-manage [options] type command [id [attributes]] + type : role, tenant, user, token (more to come) + command : add, list, disable (more to come) + id : name or id + attributes : depending on type... + users : password, tenant + tokens : user, tenant, expiration + + options + -c | --config-file : config file to use + -d | --debug : debug mode + + Example: keystone-manage add user Admin P@ssw0rd + """ + usage = "usage: %prog [options] type command [id [attributes]]" + + # Initialize a parser for our configuration paramaters + parser = optparse.OptionParser(usage, version='%%prog %s' + % keystone.version()) + common_group = config.add_common_options(parser) + config.add_log_options(parser) + + # Parse command-line and load config + (options, args) = config.parse_options(parser) + config_file, conf = config.load_paste_config('admin', options, args) + + # Check arguments + if len(args) == 0: + parser.error('No object type specified for first argument') + + object_type = args[0] + if object_type in ['user', 'tenant', 'role', 'token']: + pass + else: + parser.error('%s is not a supported object type' % object_type) + + if len(args) == 1: + parser.error('No command specified for second argument') + command = args[1] + if command in ['add', 'list', 'disable', 'delete', 'grant', 'revoke']: + pass + else: + parser.error('add, disable, delete, and list are the only supported"\ + " commands (right now)') + + if len(args) == 2: + if command != 'list': + parser.error('No id specified for third argument') + if len(args) > 2: + object_id = args[2] + + # Set things up to run the command + print "Config file in use: %s" % config_file + config.setup_logging(options, conf) + + db_api.configure_db(conf) + + if object_type == "user": + if command == "add": + if len(args) < 4: + parser.error('No password specified for fourth argument') + password = args[3] + + try: + object = db_models.User() + object.id = object_id + object.password = password + object.enabled = True + if len(args) > 4: + tenant = args[4] + object.tenant_id = tenant + db_api.user_create(object) + print "User created successfully. ID=%s" % object.id + except Exception as exc: + print "ERROR: Failed to create user: %s" % exc + return + elif command == "disable": + try: + object = db_api.user_get(object_id) + if object == None: + raise IndexError("User %s not found" % object_id) + object.enabled = False + db_api.user_update(object_id, object) + print "User disabled: %s" % object.id + except Exception as exc: + print "ERROR: Failed to disable user: %s" % exc + return + elif command == "list": + try: + if len(args) > 2: + tenant = args[2] + objects = db_api.user_get_by_tenant(tenant) + if objects == None: + raise IndexError("Users not found") + print 'id', 'enabled' + print '-' * 20 + for row in objects: + print row.id, row.enabled + else: + objects = db_api.user_get_all() + if objects == None: + raise IndexError("Users not found") + print 'id', 'enabled', 'tenant' + print '-' * 20 + for row in objects: + print row.id, row.enabled, row.tenant_id + except Exception, e: + print 'Error getting all users:', str(e) + return + elif object_type == "tenant": + if command == "add": + try: + object = db_models.Tenant() + object.id = object_id + object.enabled = True + db_api.tenant_create(object) + print "Tenant created successfully. ID=%s" % object.id + return + except Exception as exc: + print "ERROR: Failed to create tenant: %s" % exc + return + elif command == "list": + try: + objects = db_api.tenant_get_all() + if objects == None: + raise IndexError("Tenants not found") + print 'tenant', 'enabled' + print '-' * 20 + for row in objects: + print row.id, row.enabled + except Exception, e: + print 'Error getting all users:', str(e) + return + elif command == "disable": + try: + object = db_api.tenant_get(object_id) + if object == None: + raise IndexError("Tenant %s not found" % object_id) + object.enabled = False + db_api.tenant_update(object_id, object) + print "Tenant disabled: %s" % object.id + except Exception as exc: + print "ERROR: Failed to disable tenant: %s" % exc + return + elif object_type == "role": + if command == "add": + try: + object = db_models.Role() + object.id = object_id + db_api.role_create(object) + print "Role created successfully. ID=%s" % object.id + return + except Exception as exc: + print "ERROR: Failed to create role: %s" % exc + return + elif command == "list": + try: + objects = db_api.role_get_all() + if objects == None: + raise IndexError("Roles not found") + print 'role' + print '-' * 20 + for row in objects: + print row.id + except Exception, e: + print 'Error getting all roles:', str(e) + return + elif command == "grant": + if len(args) < 5: + parser.error("Missing arguments: role grant 'role' 'user'"\ + "'tenant'") + user = args[3] + tenant = args[4] + try: + object = db_models.UserRoleAssociation() + object.role_id = object_id + object.user_id = user + object.tenant_id = tenant + db_api.user_role_add(object) + print "Successfully granted %s the %s role on %s" %\ + (object.user_id, object.role_id, object.tenant_id) + except Exception as exc: + print "ERROR: Failed to grant role: %s" % exc + return + elif object_type == "token": + if command == "add": + if len(args) < 6: + parser.error('Creating a token requires a token id, user'\ + ', tenant, and expiration') + try: + object = db_models.Token() + object.token_id = object_id + object.user_id = args[3] + object.tenant_id = args[4] + tuple_time = datetime.datetime.strptime(args[5] + .replace("-", ""), + "%Y%m%dT%H:%M") + object.expires = tuple_time + db_api.token_create(object) + print "Token created successfully. ID=%s" % object.token_id + return + except Exception as exc: + print "ERROR: Failed to create token: %s" % exc + return + elif command == "list": + try: + objects = db_api.token_get_all() + if objects == None: + raise IndexError("Tokens not found") + print 'token', 'user', 'expiration', 'tenant' + print '-' * 20 + for row in objects: + print row.token_id, row.user_id, row.expires, row.tenant_id + except Exception, e: + print 'Error getting all tokens:', str(e) + return + elif command == "delete": + try: + object = db_api.token_get(object_id) + if object == None: + raise IndexError("Token %s not found" % object_id) + else: + db_api.token_delete(object_id) + print 'Token %s deleted' % object_id + except Exception, e: + print 'Error deleting token %s:' % object_id, str(e) + return + + # Command not handled + print ("Fail: %s %s not yet supported" % (object_type, command)) + + +if __name__ == '__main__': + Main() diff --git a/bin/sampledata.sh b/bin/sampledata.sh new file mode 100755 index 00000000..c4102332 --- /dev/null +++ b/bin/sampledata.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +# Tenants +./keystone-manage tenant add 1234 +./keystone-manage tenant add 0000 +./keystone-manage tenant disable 0000 + +# Users +./keystone-manage user add joeuser secrete 1234 +./keystone-manage user add admin secrete 1234 +./keystone-manage user add disabled secrete 1234 +./keystone-manage user disable disabled + +# Roles +./keystone-manage role add Admin +./keystone-manage role add Admin admin 1234 + + +# Groups +#./keystone-manage group add Admin 1234 +#./keystone-manage group add Default 1234 +#./keystone-manage group add Empty 0000 + +# User Group Associations +#./keystone-manage user joeuser join Default +#./keystone-manage user disabled join Default +#./keystone-manage user admin join Admin + +# Tokens +./keystone-manage token add 887665443383838 joeuser 1234 2012-02-05T00:00 +./keystone-manage token add 999888777666 admin 1234 2015-02-05T00:00 +./keystone-manage token add 000999 admin 1234 2010-02-05T00:00 +./keystone-manage token add 999888777 disabled 1234 2015-02-05T00:00 diff --git a/docs/nova-api-paste.ini b/docs/nova-api-paste.ini new file mode 100644 index 00000000..e1c26a24 --- /dev/null +++ b/docs/nova-api-paste.ini @@ -0,0 +1,114 @@ +####### +# EC2 # +####### + +[composite:ec2] +use = egg:Paste#urlmap +/: ec2versions +/services/Cloud: ec2cloud +/services/Admin: ec2admin +/latest: ec2metadata +/2007-01-19: ec2metadata +/2007-03-01: ec2metadata +/2007-08-29: ec2metadata +/2007-10-10: ec2metadata +/2007-12-15: ec2metadata +/2008-02-01: ec2metadata +/2008-09-01: ec2metadata +/2009-04-04: ec2metadata +/1.0: ec2metadata + +[pipeline:ec2cloud] +pipeline = logrequest authenticate cloudrequest authorizer ec2executor +#pipeline = logrequest ec2lockout authenticate cloudrequest authorizer ec2executor + +[pipeline:ec2admin] +pipeline = logrequest authenticate adminrequest authorizer ec2executor + +[pipeline:ec2metadata] +pipeline = logrequest ec2md + +[pipeline:ec2versions] +pipeline = logrequest ec2ver + +[filter:logrequest] +paste.filter_factory = nova.api.ec2:RequestLogging.factory + +[filter:ec2lockout] +paste.filter_factory = nova.api.ec2:Lockout.factory + +[filter:authenticate] +paste.filter_factory = nova.api.ec2:Authenticate.factory + +[filter:cloudrequest] +controller = nova.api.ec2.cloud.CloudController +paste.filter_factory = nova.api.ec2:Requestify.factory + +[filter:adminrequest] +controller = nova.api.ec2.admin.AdminController +paste.filter_factory = nova.api.ec2:Requestify.factory + +[filter:authorizer] +paste.filter_factory = nova.api.ec2:Authorizer.factory + +[app:ec2executor] +paste.app_factory = nova.api.ec2:Executor.factory + +[app:ec2ver] +paste.app_factory = nova.api.ec2:Versions.factory + +[app:ec2md] +paste.app_factory = nova.api.ec2.metadatarequesthandler:MetadataRequestHandler.factory + +############# +# Openstack # +############# + +[composite:osapi] +use = egg:Paste#urlmap +/: osversions +/v1.0: openstackapi10 +/v1.1: openstackapi11 + +[pipeline:openstackapi10] +pipeline = faultwrap tokenauth auth_shim ratelimit osapiapp10 + +[pipeline:openstackapi11] +pipeline = faultwrap tokenauth auth_shim ratelimit extensions osapiapp11 + +[filter:faultwrap] +paste.filter_factory = nova.api.openstack:FaultWrapper.factory + +[filter:auth] +paste.filter_factory = nova.api.openstack.auth:AuthMiddleware.factory + +[filter:tokenauth] +paste.filter_factory = nova.auth.nova_auth_token:filter_factory +service_protocol = http +service_host = 127.0.0.1 +service_port = 808 +auth_host = 127.0.0.1 +auth_port = 8081 +auth_protocol = http +admin_token = 999888777666 + +[filter:auth_shim] +paste.filter_factory = nova.auth.nova_auth_token:KeystoneAuthShim.factory + +[filter:ratelimit] +paste.filter_factory = nova.api.openstack.limits:RateLimitingMiddleware.factory + +[filter:extensions] +paste.filter_factory = nova.api.openstack.extensions:ExtensionMiddleware.factory + +[app:osapiapp10] +paste.app_factory = nova.api.openstack:APIRouterV10.factory + +[app:osapiapp11] +paste.app_factory = nova.api.openstack:APIRouterV11.factory + +[pipeline:osversions] +pipeline = faultwrap osversionapp + +[app:osversionapp] +paste.app_factory = nova.api.openstack.versions:Versions.factory diff --git a/echo/echo/echo.ini b/echo/echo/echo.ini index b81174d0..a7b95e9e 100644 --- a/echo/echo/echo.ini +++ b/echo/echo/echo.ini @@ -6,7 +6,7 @@ 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 +service_port = 8081 ;used to verify this component with the OpenStack service (or PAPIAuth) service_pass = dTpw @@ -20,17 +20,17 @@ pipeline = echo [filter:tokenauth] -paste.filter_factory = keystone:tokenauth_factory +paste.filter_factory = keystone.auth_protocols.auth_token:filter_factory ;where to find the token auth service auth_host = 127.0.0.1 -auth_port = 8080 +auth_port = 8081 auth_protocol = http ;how to authenticate to the auth service for priviledged operations ;like validate token admin_token = 999888777666 [filter:basicauth] -paste.filter_factory = keystone:basicauth_factory +paste.filter_factory = keystone.auth_protocols.auth_basic:filter_factory [filter:openidauth] -paste.filter_factory = keystone:openidauth_factory +paste.filter_factory = keystone.auth_protocols.auth_openid:filter_factory diff --git a/echo/echo/echo_basic.ini b/echo/echo/echo_basic.ini index 38da0c66..65c85357 100644 --- a/echo/echo/echo_basic.ini +++ b/echo/echo/echo_basic.ini @@ -6,7 +6,7 @@ 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 +service_port = 8081 ;used to verify this component with the OpenStack service (or PAPIAuth) service_pass = dTpw @@ -20,17 +20,17 @@ pipeline = echo [filter:tokenauth] -paste.filter_factory = keystone:tokenauth_factory +paste.filter_factory = keystone.auth_protocols.auth_token:filter_factory ;where to find the token auth service auth_host = 127.0.0.1 -auth_port = 8080 +auth_port = 8081 auth_protocol = http ;how to authenticate to the auth service for priviledged operations ;like validate token admin_token = 999888777666 [filter:basicauth] -paste.filter_factory = keystone:basicauth_factory +paste.filter_factory = keystone.auth_protocols.auth_basic:filter_factory [filter:openidauth] -paste.filter_factory = keystone:openidauth_factory +paste.filter_factory = keystone.auth_protocols.auth_openid:filter_factory diff --git a/echo/echo/echo_remote.ini b/echo/echo/echo_remote.ini index c27b2365..d225f756 100644 --- a/echo/echo/echo_remote.ini +++ b/echo/echo/echo_remote.ini @@ -9,11 +9,11 @@ pipeline = echo [filter:remoteauth] -paste.filter_factory = keystone:remoteauth_factory +paste.filter_factory = keystone.middleware.remoteauth:filter_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 -auth_location = http://127.0.0.1:8080/ +auth_location = http://127.0.0.1:8081/ diff --git a/echo/echo_client.py b/echo/echo_client.py index a70d2a4a..ba835417 100644..100755 --- a/echo/echo_client.py +++ b/echo/echo_client.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright (c) 2010-2011 OpenStack, LLC. # @@ -31,6 +32,7 @@ def get_auth_token(username, password, tenant): conn.request("POST", "/v2.0/token", json.dumps(params), headers=headers) response = conn.getresponse() data = response.read() + print data ret = data return ret diff --git a/etc/keystone.conf b/etc/keystone.conf index 117b3eca..e9c59046 100644 --- a/etc/keystone.conf +++ b/etc/keystone.conf @@ -1,24 +1,15 @@ [DEFAULT] # Show more verbose log output (sets INFO log level output) -verbose = True +verbose = False # Show debugging output in logs (sets DEBUG log level output) -debug = True - -[app:server] -paste.app_factory = keystone.server:app_factory +debug = False # Which backend store should Keystone use by default. # Default: 'sqlite' # Available choices are 'sqlite' [future will include LDAP, PAM, etc] default_store = sqlite -# Address to bind the API server -bind_host = 0.0.0.0 - -# Port the bind the API server to -bind_port = 8080 - # Log to this file. Make sure you do not set the same log # file for both the API and registry servers! # @@ -35,3 +26,21 @@ sql_connection = sqlite:///../keystone/keystone.db # sql_idle_timeout = 30 +[app:admin] +paste.app_factory = keystone.server:admin_app_factory + +# Address to bind the Admin API server +bind_host = 0.0.0.0 + +# Port the bind the Admin API server to +bind_port = 8081 + + +[app:server] +paste.app_factory = keystone.server:app_factory + +# Address to bind the API server +bind_host = 0.0.0.0 + +# Port the bind the API server to +bind_port = 8080 diff --git a/keystone/__init__.py b/keystone/__init__.py index aec9d44f..b70c0009 100644 --- a/keystone/__init__.py +++ b/keystone/__init__.py @@ -1,3 +1,34 @@ +# Copyright (C) 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. + +'''Main module for Keystone service. This installs the gettext function +for all sub modules and packages.''' + +import gettext + +__version__ = '0.9' + +# This installs the _(...) function as a built-in so all other modules +# don't need to. +gettext.install('keystone') + + +def version(): + return __version__ + +''' +TODO(Ziad): Commenting out so we don't load this always - remove eventually #TOKEN AUTH from auth_protocols.auth_token \ import filter_factory as tokenauth_factory @@ -13,3 +44,4 @@ from auth_protocols.auth_openid \ #Remote Auth handler from middleware.remoteauth \ import filter_factory as remoteauth_factory +'''
\ No newline at end of file diff --git a/keystone/auth_protocols/auth_token.ini b/keystone/auth_protocols/auth_token.ini index 3154fc69..25f2cbbf 100644 --- a/keystone/auth_protocols/auth_token.ini +++ b/keystone/auth_protocols/auth_token.ini @@ -5,7 +5,7 @@ paste.app_factory = auth_token:app_factory auth_protocol = http auth_host = 127.0.0.1 -auth_port = 8080 +auth_port = 8081 admin_token = 999888777666 delay_auth_decision = 0 diff --git a/keystone/auth_protocols/auth_token.py b/keystone/auth_protocols/auth_token.py index 8fe519ca..4de54a55 100644 --- a/keystone/auth_protocols/auth_token.py +++ b/keystone/auth_protocols/auth_token.py @@ -165,8 +165,8 @@ class AuthProtocol(object): "Proxy %s" % claims['user']) self._decorate_request('X_TENANT', claims['tenant']) - self._decorate_request('X_GROUP', - claims['group']) + if 'group' in claims: + self._decorate_request('X_GROUP', claims['group']) self.expanded = True #Send request downstream @@ -262,11 +262,12 @@ class AuthProtocol(object): token_info = json.loads(data) #TODO(Ziad): make this more robust - first_group = token_info['auth']['user']['groups']['group'][0] + #first_group = token_info['auth']['user']['groups']['group'][0] verified_claims = {'user': token_info['auth']['user']['username'], - 'tenant': token_info['auth']['user']['tenantId'], - 'group': '%s/%s' % (first_group['id'], - first_group['tenantId'])} + 'tenant': token_info['auth']['user']['tenantId']} + # TODO(Ziad): removed groups for now + # ,'group': '%s/%s' % (first_group['id'], + # first_group['tenantId'])} return verified_claims def _decorate_request(self, index, value): diff --git a/keystone/auth_protocols/nova_auth_token.py b/keystone/auth_protocols/nova_auth_token.py new file mode 100644 index 00000000..8cc51206 --- /dev/null +++ b/keystone/auth_protocols/nova_auth_token.py @@ -0,0 +1,363 @@ +#!/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 + + +# FIXME(ja): fix "copy & paste ware"! +# determine how to integrate nova, dash, keystone + +""" +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 '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... + +Refer to: http://wiki.openstack.org/openstack-authn + + +HEADERS +------- +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 + +""" + +from bufferedhttp import http_connect_raw as http_connect +import eventlet +import httplib +import json +from nova import auth +from nova import context +from nova import flags +from nova import utils +from nova import wsgi +import os +from paste.deploy import loadapp +import sys +from urlparse import urlparse +import webob.exc +import webob.dec +from webob.exc import HTTPUnauthorized, HTTPUseProxy +from webob.exc import Request, Response + +FLAGS = flags.FLAGS + +PROTOCOL_NAME = "Token Authentication" + + +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 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 _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(ziad): 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 __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 local or remote downstream service + 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: + claims = self._expound_claims() + if claims: + # TODO(Ziad): add additional details we may need, + # like tenant and group info + self._decorate_request('X_AUTHORIZATION', + claims['user']) + self._decorate_request('X_TENANT', + claims['tenant']) + self._decorate_request('X_GROUP', + 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 + 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, + "password": password, + "tenantId": "1"}} + conn = httplib.HTTPConnection("%s:%s" \ + % (self.auth_host, self.auth_port)) + conn.request("POST", "/v2.0/token", json.dumps(params), \ + headers=headers) + response = conn.getresponse() + data = response.read() + return data + + def _get_claims(self, env): + claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) + return claims + + def _reject_request(self): + # Redirect client to auth server + return HTTPUseProxy(location=self.auth_location)(self.env, + self.start_response) + + def _reject_claims(self): + # Client sent bad claims + return HTTPUnauthorized()(self.env, + self.start_response) + + 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(ziad): 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(ziad):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', + '/v2.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(ziad):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', + '/v2.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_info = json.loads(data) + #TODO(Ziad): make this more robust + first_group = token_info['auth']['user']['groups']['group'][0] + verified_claims = {'user': token_info['auth']['user']['username'], + 'tenant': token_info['auth']['user']['tenantId'], + 'group': '%s/%s' % (first_group['id'], + first_group['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(self.env, self.start_response) + #.custom_start_response) + else: + # We are forwarding to a remote service (no downstream WSGI app) + req = Request(self.proxy_headers) + parsed = urlparse(req.url) + conn = http_connect(self.service_host, + self.service_port, + req.method, + parsed.path, + self.proxy_headers, + ssl=(self.service_protocol == 'https')) + resp = conn.getresponse() + data = resp.read() + #TODO(ziad): use a more sophisticated proxy + # we are rewriting the headers now + return Response(status=resp.status, body=data)(self.proxy_headers, + self.start_response) + + +class KeystoneAuthShim(wsgi.Middleware): + """Lazy provisioning nova project/users from keystone tenant/user""" + + def __init__(self, application, db_driver=None): + if not db_driver: + db_driver = FLAGS.db_driver + self.db = utils.import_object(db_driver) + self.auth = auth.manager.AuthManager() + super(KeystoneAuthShim, self).__init__(application) + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + user_id = req.headers['X_AUTHORIZATION'] + try: + user_ref = self.auth.get_user(user_id) + except: + user_ref = self.auth.create_user(user_id) + project_id = req.headers['X_TENANT'] + try: + project_ref = self.auth.get_project(project_id) + except: + project_ref = self.auth.create_project(project_id, user_id) + + if not self.auth.is_project_member(user_id, project_id): + self.auth.add_to_project(user_id, project_id) + + # groups = req.headers['X_GROUP'] + + req.environ['nova.context'] = context.RequestContext(user_ref, project_ref) + return self.application + +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_token.ini"), global_conf={"log_name": "auth_token.log"}) + wsgi.server(eventlet.listen(('', 8090)), app) diff --git a/keystone/common/config.py b/keystone/common/config.py index e202bf89..708213c9 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -75,14 +75,28 @@ def add_common_options(parser): help="Print more verbose output") group.add_option('-d', '--debug', default=False, dest="debug", action="store_true", - help="Print debugging output") - group.add_option('--config-file', default=None, metavar="PATH", - help="Path to the config file to use. When not specified " - "(the default), we generally look at the first " - "argument specified to be a config file, and if " - "that is also missing, we search standard " + help="Print debugging output to console") + group.add_option('-c', '--config-file', default=None, metavar="PATH", + help="Path to the config file to use. When not specified "\ + "(the default), we generally look at the first "\ + "argument specified to be a config file, and if "\ + "that is also missing, we search standard "\ "directories for a config file.") + group.add_option('-p', '--port', '--bind-port', default=8080, + dest="bind_port", + help="specifies port to listen on (default is 8080)") + group.add_option('--host', '--bind-host', + default="0.0.0.0", dest="bind_host", + help="specifies host address to listen on "\ + "(default is all or 0.0.0.0)") + # This one is handled by tools/tracer.py (if loaded) + group.add_option('-t', '--trace-calls', default=False, + dest="trace_calls", + action="store_true", + help="Turns on call tracing for troubleshooting") + parser.add_option_group(group) + return group def add_log_options(parser): @@ -97,22 +111,51 @@ def add_log_options(parser): group = optparse.OptionGroup(parser, "Logging Options", help_text) group.add_option('--log-config', default=None, metavar="PATH", - help="If this option is specified, the logging " - "configuration file specified is used and overrides " - "any other logging options specified. Please see " - "the Python logging module documentation for " + help="If this option is specified, the logging "\ + "configuration file specified is used and overrides "\ + "any other logging options specified. Please see "\ + "the Python logging module documentation for "\ "details on logging configuration files.") group.add_option('--log-date-format', metavar="FORMAT", default=DEFAULT_LOG_DATE_FORMAT, - help="Format string for %(asctime)s in log records. " + help="Format string for %(asctime)s in log records. "\ "Default: %default") group.add_option('--log-file', default=None, metavar="PATH", - help="(Optional) Name of log file to output to. " + help="(Optional) Name of log file to output to. "\ "If not set, logging will go to stdout.") group.add_option("--log-dir", default=None, - help="(Optional) The directory to keep log files in " + help="(Optional) The directory to keep log files in "\ "(will be prepended to --logfile)") + parser.add_option_group(group) + return group + + +def add_console_handler(logger, level=logging.INFO): + """ + Add a Handler which writes log messages to sys.stderr + (which is often the console) + There is a copy of this function in wsgi.py (TODO(Ziad): Make it one copy) + """ + console = None + for console in logger.handlers: + if isinstance(console, logging.StreamHandler): + break + + if not console: + console = logging.StreamHandler() + console.setLevel(level) + # set a format which is simpler for console use + formatter = logging.Formatter("%(name)-12s: "\ + "%(levelname)-8s %(message)s") + # tell the handler to use this format + console.setFormatter(formatter) + # add the handler to the root logger + logger.addHandler(console) + else: + if console.level != level: + console.setLevel(level) + return console def setup_logging(options, conf): @@ -128,7 +171,7 @@ def setup_logging(options, conf): logging.config.fileConfig(options['log_config']) return else: - raise RuntimeError("Unable to locate specified logging " + raise RuntimeError("Unable to locate specified logging "\ "config file: %s" % options['log_config']) # If either the CLI option or the conf value @@ -163,8 +206,12 @@ def setup_logging(options, conf): logfile = os.path.join(logdir, logfile) logfile = logging.FileHandler(logfile) logfile.setFormatter(formatter) - logfile.setFormatter(formatter) root_logger.addHandler(logfile) + # Mirror to console if verbose or debug + if debug: + add_console_handler(root_logger, logging.INFO) #debug too noisy + elif verbose: + add_console_handler(root_logger, logging.INFO) else: handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) @@ -217,10 +264,7 @@ def find_config_file(options, args): # For debug only config_file = os.path.join(POSSIBLE_TOPDIR, 'etc', \ 'keystone.conf') - print "Running server from %s " % config_file - - return os.path.join(POSSIBLE_TOPDIR, 'etc', \ - 'keystone.conf') + return config_file def load_paste_config(app_name, options, args): @@ -250,7 +294,7 @@ def load_paste_config(app_name, options, args): """ conf_file = find_config_file(options, args) if not conf_file: - raise RuntimeError("Unable to locate any configuration file. " + raise RuntimeError("Unable to locate any configuration file. "\ "Cannot load application %s" % app_name) try: conf = deploy.appconfig("config:%s" % conf_file, name=app_name) @@ -274,7 +318,7 @@ def load_paste_app(app_name, options, args): * /etc/keystone * /etc - :param app_name: Name of the application to load + :param app_name: Name of the application to load (server, admin, proxy, ...) :param options: Set of typed options returned from parse_options() :param args: Command line arguments from argv[1:] @@ -298,15 +342,15 @@ def load_paste_app(app_name, options, args): # Log the options used when starting if we're in debug mode... if debug: logger = logging.getLogger(app_name) - logger.debug("*" * 80) - logger.debug("Configuration options gathered from config file:") - logger.debug(conf_file) - logger.debug("================================================") + logger.info("*" * 50) + logger.info("Configuration options gathered from config file:") + logger.info(conf_file) + logger.info("================================================") items = dict([(k, v) for k, v in conf.items() if k not in ('__file__', 'here')]) for key, value in sorted(items.items()): - logger.debug("%(key)-30s %(value)s" % locals()) - logger.debug("*" * 80) + logger.info("%(key)-20s %(value)s" % locals()) + logger.info("*" * 50) app = deploy.loadapp("config:%s" % conf_file, name=app_name) except (LookupError, ImportError), e: raise RuntimeError("Unable to load %(app_name)s from " diff --git a/keystone/common/exception.py b/keystone/common/exception.py index 1f793c7c..fb64220d 100755 --- a/keystone/common/exception.py +++ b/keystone/common/exception.py @@ -18,7 +18,7 @@ """ OpenStack base exception handling, including decorator for re-raising -OpenSTack-type exceptions. SHOULD include dedicated exception logging. +OpenStack-type exceptions. SHOULD include dedicated exception logging. """ import logging diff --git a/keystone/common/template.py b/keystone/common/template.py index 58c8bd6e..f8412d62 100644 --- a/keystone/common/template.py +++ b/keystone/common/template.py @@ -1,3 +1,22 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. +# +# Template library copied from bottle: http://bottlepy.org/ # # Copyright (c) 2011, Marcel Hellkamp. # @@ -19,7 +38,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # -# original code copied from bottle.py import cgi diff --git a/keystone/common/wsgi.py b/keystone/common/wsgi.py index 32de27c9..1f48137c 100755 --- a/keystone/common/wsgi.py +++ b/keystone/common/wsgi.py @@ -34,13 +34,41 @@ import routes.middleware import webob.dec import webob.exc +def add_console_handler(logger, level): + """ + Add a Handler which writes messages to sys.stderr which is often the + console. + There is a copy of this function in config.py (TODO(Ziad): Make it one copy) + """ + console = None + for console in logger.handlers: + if isinstance(console, logging.StreamHandler): + break + + if not console: + console = logging.StreamHandler() + console.setLevel(level) + # set a format which is simpler for console use + formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') + # tell the handler to use this format + console.setFormatter(formatter) + # add the handler to the root logger + logger.addHandler(console) + elif console.level != level: + console.setLevel(level) + return console + class WritableLogger(object): """A thin wrapper that responds to `write` and logs.""" - def __init__(self, logger, level=logging.DEBUG): + def __init__(self, logger, level=logging.INFO): self.logger = logger self.level = level + # TODO(Ziad): figure out why root logger is not set to same level as + # caller. Maybe something to do with paste? + if level == logging.DEBUG: + add_console_handler(logger, level) def write(self, msg): self.logger.log(self.level, msg.strip("\n")) @@ -73,8 +101,10 @@ class Server(object): def _run(self, application, socket): """Start a WSGI server in a new green thread.""" logger = logging.getLogger('eventlet.wsgi.server') + # TODO(Ziad): figure out why root logger is not set to same level as + # caller. Maybe something to do with paste? eventlet.wsgi.server(socket, application, custom_pool=self.pool, - log=WritableLogger(logger)) + log=WritableLogger(logger, logging.root.level)) class Middleware(object): @@ -176,7 +206,7 @@ class Router(object): # Pointing to an arbitrary WSGI app. You can specify the # {path_info:.*} parameter so the target app can be handed just that # section of the URL. - mapper.connect(None, "/v2.0/{path_info:.*}", controller=BlogApp()) + mapper.connect(None, "/v2.0/{path_info:.*}", controller=TheApp()) """ self.map = mapper self._router = routes.middleware.RoutesMiddleware(self._dispatch, diff --git a/keystone/db/sqlalchemy/api.py b/keystone/db/sqlalchemy/api.py index e479a4aa..95fbb9ee 100644 --- a/keystone/db/sqlalchemy/api.py +++ b/keystone/db/sqlalchemy/api.py @@ -28,6 +28,7 @@ _ENGINE = None _MAKER = None BASE = models.Base + def configure_db(options): """ Establish the database, create an engine if needed, and @@ -77,11 +78,33 @@ def unregister_models(): assert _ENGINE BASE.metadata.drop_all(_ENGINE) + # -# Tenant API operations +# Role API operations # +def role_create(values): + role_ref = models.Role() + role_ref.update(values) + role_ref.save() + return role_ref + + +def role_get(id, session=None): + if not session: + session = get_session() + result = session.query(models.Role).filter_by(id=id).first() + return result + + +def role_get_all(session=None): + if not session: + session = get_session() + return session.query(models.Role).all() +# +# Tenant API operations +# def tenant_create(values): tenant_ref = models.Tenant() tenant_ref.update(values) @@ -182,11 +205,11 @@ def tenant_delete(id, session=None): tenant_ref = tenant_get(id, session) session.delete(tenant_ref) + + # # Tenant Group Operations API # - - def tenant_group_create(values): group_ref = models.Group() group_ref.update(values) @@ -288,6 +311,16 @@ def tenant_group_delete(id, tenant_id, session=None): session.delete(tenantgroup_ref) +# +# User Operations +# +def user_get_all(session=None): + if not session: + session = get_session() + result = session.query(models.User) + return result + + def get_user_by_group(user_id, group_id, session=None): if not session: session = get_session() @@ -310,10 +343,6 @@ def user_tenant_group_delete(id, group_id, session=None): usertenantgroup_ref = get_user_by_group(id, group_id, session) session.delete(usertenantgroup_ref) -# -# User Operations -# - def user_create(values): user_ref = models.User() @@ -325,8 +354,10 @@ def user_create(values): def user_get(id, session=None): if not session: session = get_session() - result = session.query(models.User).options(joinedload('groups')).options(\ - joinedload('tenants')).filter_by(id=id).first() + #TODO(Ziad): finish cleaning up model + # result = session.query(models.User).options(joinedload('groups')).options(\ + # joinedload('tenants')).filter_by(id=id).first() + result = session.query(models.User).filter_by(id=id).first() return result @@ -345,6 +376,14 @@ def user_groups(id, session=None): return result +def user_roles_by_tenant(user_id, tenant_id, session=None): + if not session: + session = get_session() + result = session.query(models.UserRoleAssociation).filter_by(\ + user_id=user_id, tenant_id=tenant_id).options(joinedload('roles')) + return result + + def user_update(id, values, session=None): if not session: session = get_session() @@ -419,6 +458,64 @@ def users_tenant_group_get_page_markers(group_id, marker, limit, session=None): return (prev, next) +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 user_get_by_tenant(id, tenant_id, session=None): + if not session: + session = get_session() + user_tenant = session.query(models.UserTenantAssociation).filter_by(\ + tenant_id=tenant_id, user_id=id).first() + if user_tenant: + return user_get(id, session) + else: + return None + + +def user_get_by_group(id, session=None): + if not session: + session = get_session() + user_group = session.query(models.Group).filter_by(tenant_id=id).all() + return user_group + + +def user_delete_tenant(id, tenant_id, session=None): + if not session: + session = get_session() + with session.begin(): + user_tenant_ref = user_get_by_tenant(id, tenant_id, session) + + session.delete(user_tenant_ref) + user_group_ref = user_get_by_group(tenant_id, session) + + if user_group_ref is not None: + for user_group in user_group_ref: + group_users = session.query(models.UserGroupAssociation)\ + .filter_by(user_id=id, + group_id=user_group.id).all() + for group_user in group_users: + session.delete(group_user) + user_tenant_ref = session.query(models.UserTenantAssociation)\ + .filter_by(user_id=id).first() + if user_tenant_ref is None: + user_ref = user_get(id, session) + session.delete(user_ref) + +def user_get_by_tenant(user_id, tenant_id, session=None): + if not session: + session = get_session() + result = session.query(models.User).filter_by(id=user_id, + tenant_id=tenant_id).first() + return result + +# +# Group Operations +# def group_get(id, session=None): if not session: session = get_session() @@ -499,11 +596,10 @@ def group_delete(id, session=None): group_ref = group_get(id, session) session.delete(group_ref) + # # Token Operations # - - def token_create(values): token_ref = models.Token() token_ref.update(values) @@ -542,6 +638,22 @@ def token_for_user_tenant(user_id, tenant_id, session=None): return result +def token_get_all(session=None): + if not session: + session = get_session() + return session.query(models.Token).all() + + +# +# Unsorted operations +# +def user_role_add(values): + user_role_ref = models.UserRoleAssociation() + user_role_ref.update(values) + user_role_ref.save() + return user_role_ref + + def user_tenant_create(values): user_tenant_ref = models.UserTenantAssociation() user_tenant_ref.update(values) @@ -555,6 +667,7 @@ def user_get_update(id, session=None): result = session.query(models.User).filter_by(id=id).first() return result + def users_get_by_tenant_get_page(tenant_id, marker, limit, session=None): if not session: session = get_session() @@ -708,49 +821,3 @@ def groups_get_by_user_get_page_markers(user_id, marker, limit, session=None): else: next = next.id return (prev, next) - - -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 user_get_by_tenant(id, tenant_id, session=None): - if not session: - session = get_session() - user_tenant = session.query(models.UserTenantAssociation).filter_by(\ - tenant_id=tenant_id, user_id=id).first() - return user_tenant - - -def user_get_by_group(id, session=None): - if not session: - session = get_session() - user_group = session.query(models.Group).filter_by(tenant_id=id).all() - return user_group - - -def user_delete_tenant(id, tenant_id, session=None): - if not session: - session = get_session() - with session.begin(): - user_tenant_ref = user_get_by_tenant(id, tenant_id, session) - - session.delete(user_tenant_ref) - user_group_ref = user_get_by_group(tenant_id, session) - - if user_group_ref is not None: - for user_group in user_group_ref: - group_users = session.query(models.UserGroupAssociation)\ - .filter_by(user_id=id, - group_id=user_group.id).all() - for group_user in group_users: - session.delete(group_user) - user_tenant_ref = session.query(models.UserTenantAssociation)\ - .filter_by(user_id=id).first() - if user_tenant_ref is None: - user_ref = user_get(id, session) - session.delete(user_ref) diff --git a/keystone/db/sqlalchemy/models.py b/keystone/db/sqlalchemy/models.py index bdcd5015..71511653 100644 --- a/keystone/db/sqlalchemy/models.py +++ b/keystone/db/sqlalchemy/models.py @@ -74,18 +74,38 @@ class KeystoneBase(object): return local.iteritems() -class UserTenantAssociation(Base, KeystoneBase): - __tablename__ = 'user_tenant_association' +# Define associations firest +class UserGroupAssociation(Base, KeystoneBase): + __tablename__ = 'user_group_association' user_id = Column(String(255), ForeignKey('users.id'), primary_key=True) - tenant_id = Column(String(255), ForeignKey('tenants.id'), primary_key=True) + group_id = Column(String(255), ForeignKey('groups.id'), primary_key=True) -class UserGroupAssociation(Base, KeystoneBase): - __tablename__ = 'user_group_association' +class UserRoleAssociation(Base, KeystoneBase): + __tablename__ = 'user_roles' user_id = Column(String(255), ForeignKey('users.id'), primary_key=True) - group_id = Column(String(255), ForeignKey('groups.id'), primary_key=True) + role_id = Column(String(255), ForeignKey('roles.id'), primary_key=True) + tenant_id = Column(String(255), ForeignKey('tenants.id'), primary_key=True) + + +# Define objects +class Role(Base, KeystoneBase): + __tablename__ = 'roles' + + id = Column(String(255), primary_key=True, unique=True) + desc = Column(String(255)) + + +class Tenant(Base, KeystoneBase): + __tablename__ = 'tenants' + + id = Column(String(255), primary_key=True, unique=True) + desc = Column(String(255)) + enabled = Column(Integer) + + groups = relationship('Group', backref='tenants') class User(Base, KeystoneBase): @@ -95,17 +115,27 @@ class User(Base, KeystoneBase): password = Column(String(255)) email = Column(String(255)) enabled = Column(Integer) + tenant_id = Column(String(255), ForeignKey('tenants.id')) + groups = relationship(UserGroupAssociation, backref='users') - tenants = relationship(UserTenantAssociation, backref='user') + roles = relationship(UserRoleAssociation) +class Credentials(Base, KeystoneBase): + __tablename__ = 'credentials' -class Tenant(Base, KeystoneBase): - __tablename__ = 'tenants' + user_id = Column(String(255), ForeignKey('users.id'), primary_key=True) + type = Column(String(20)) #('Password','APIKey','EC2') + key = Column(String(255)) + secret = Column(String(255)) + + +class Group(Base, KeystoneBase): + __tablename__ = 'groups' id = Column(String(255), primary_key=True, unique=True) desc = Column(String(255)) - enabled = Column(Integer) - groups = relationship('Group', backref='tenants') + tenant_id = Column(String(255), ForeignKey('tenants.id')) + class Token(Base, KeystoneBase): __tablename__ = 'token' @@ -115,11 +145,10 @@ class Token(Base, KeystoneBase): tenant_id = Column(String(255)) expires = Column(DateTime) -class Group(Base, KeystoneBase): - __tablename__ = 'groups' + +class Endpoints(Base, KeystoneBase): + __tablename__ = 'endpoints' id = Column(String(255), primary_key=True, unique=True) + service = Column(String(255)) desc = Column(String(255)) - tenant_id = Column(String(255), ForeignKey('tenants.id')) - - diff --git a/keystone/keystone.ini b/keystone/keystone.ini deleted file mode 100644 index 626ca1ed..00000000 --- a/keystone/keystone.ini +++ /dev/null @@ -1,16 +0,0 @@ -; -; This file not used -; -[DEFAULT] - -[composite:main] -use = egg:Paste#urlmap -/ = home -/tenants = admin - -[app:home] -use = egg:keystone - -[app:admin] -use = egg:keystone - diff --git a/keystone/logic/service.py b/keystone/logic/service.py index 8c833df3..c9fc0e2d 100644 --- a/keystone/logic/service.py +++ b/keystone/logic/service.py @@ -34,12 +34,19 @@ class IdentityService(object): # def authenticate(self, credentials): + # Check credentials if not isinstance(credentials, auth.PasswordCredentials): raise fault.BadRequestFault("Expecting Password Credentials!") - duser = db_api.user_get(credentials.username) - if duser == None: - raise fault.UnauthorizedFault("Unauthorized") + if not credentials.tenant_id: + duser = db_api.user_get(credentials.username) + if duser == None: + raise fault.UnauthorizedFault("Unauthorized") + else: + duser = db_api.user_get_by_tenant(credentials.username, credentials.tenant_id) + if duser == None: + raise fault.UnauthorizedFault("Unauthorized on this tenant") + if not duser.enabled: raise fault.UserDisabledFault("Your account has been disabled") if duser.password != credentials.password: @@ -55,23 +62,12 @@ class IdentityService(object): dtoken = db_api.token_for_user_tenant(duser.id, credentials.tenant_id) if not dtoken or dtoken.expires < datetime.now(): + # Create new token dtoken = db_models.Token() dtoken.token_id = str(uuid.uuid4()) dtoken.user_id = duser.id - - if not duser.tenants: - raise fault.IdentityFault("Strange: user %s is not associated " - "with a tenant!" % duser.id) - user = db_api.user_get_by_tenant(duser.id, credentials.tenant_id) - - if not credentials.tenant_id or not user: - raise fault.ForbiddenFault("Error: user %s is " - "not associated " - "with a tenant! %s" % (duser.id, - credentials.tenant_id)) + if credentials.tenant_id: dtoken.tenant_id = credentials.tenant_id - else: - dtoken.tenant_id = duser.tenants[0].tenant_id dtoken.expires = datetime.now() + timedelta(days=1) db_api.token_create(dtoken) @@ -833,21 +829,18 @@ class IdentityService(object): token = auth.Token(dtoken.expires, dtoken.token_id) - gs = [] + """gs = [] for ug in duser.groups: dgroup = db_api.group_get(ug.group_id) - gs.append(auth.Group(dgroup.id, dgroup.tenant_id)) - groups = auth.Groups(gs, []) - if len(duser.tenants) == 0: - raise fault.IdentityFault("Strange: user %s is not associated " - "with a tenant!" % duser.id) - if not dtoken.tenant_id and \ - db_api.user_get_by_tenant(duser.id, dtoken.tenant_id): - raise fault.IdentityFault("Error: user %s is not associated " - "with a tenant! %s" % (duser.id, - dtoken.tenant_id)) - - user = auth.User(duser.id, dtoken.tenant_id, groups) + if dtoken.tenant_id: + if dgroup.tenant_id == dtoken.tenant_id: + gs.append(auth.Group(dgroup.id, dgroup.tenant_id)) + else: + if dgroup.tenant_id == None: + gs.append(auth.Group(dgroup.id)) + user = auth.User(duser.id, dtoken.tenant_id, gs) + """ + user = auth.User(duser.id, dtoken.tenant_id, None) return auth.AuthData(token, user) def __validate_token(self, token_id, admin=True): @@ -862,11 +855,12 @@ class IdentityService(object): if not user.enabled: raise fault.UserDisabledFault("The user %s has been disabled!" % user.id) + '''TODO(Ziad): return roles if admin: - for ug in user.groups: + for roles in user.roles: if ug.group_id == "Admin": return (token, user) raise fault.UnauthorizedFault("You are not authorized " "to make this call") - + ''' return (token, user) diff --git a/keystone/logic/types/auth.py b/keystone/logic/types/auth.py index c822ea8c..d80e158e 100644 --- a/keystone/logic/types/auth.py +++ b/keystone/logic/types/auth.py @@ -21,11 +21,9 @@ from lxml import etree import keystone.logic.types.fault as fault - class PasswordCredentials(object): """Credentials based on username, password, and (optional) tenant_id. - To handle multiple token for a user depending on tenants, - tenant_id is mandatory. + To handle multiple token for a user depending on tenants. """ def __init__(self, username, password, tenant_id): @@ -49,12 +47,6 @@ class PasswordCredentials(object): if password == None: raise fault.BadRequestFault("Expecting a password") tenant_id = root.get("tenantId") - - #--for multi-token handling-- - if tenant_id == None: - raise fault.BadRequestFault("Expecting tenant") - # ---- - return PasswordCredentials(username, password, tenant_id) except etree.LxmlError as e: raise fault.BadRequestFault("Cannot parse password credentials", @@ -76,10 +68,7 @@ class PasswordCredentials(object): if "tenantId" in cred: tenant_id = cred["tenantId"] else: - #--for multi-token handling-- - if tenant_id == None: - raise fault.BadRequestFault("Expecting a tenant") - # --- + tenant_id = None return PasswordCredentials(username, password, tenant_id) except (ValueError, TypeError) as e: raise fault.BadRequestFault("Cannot parse password credentials", @@ -128,20 +117,21 @@ class AuthData(object): def to_xml(self): dom = etree.Element("auth", - xmlns="http://docs.openstack.org/identity/api/v2.0") + xmlns="http://docs.openstack.org/identity/api/v2.0") token = etree.Element("token", expires=self.token.expires.isoformat()) token.set("id", self.token.token_id) user = etree.Element("user", username=self.user.username, tenantId=str(self.user.tenant_id)) - groups = etree.Element("groups") + """groups = etree.Element("groups") for group in self.user.groups.values: g = etree.Element("group", tenantId=group.tenant_id) g.set("id", group.group_id) groups.append(g) user.append(groups) + """ dom.append(token) dom.append(user) return etree.tostring(dom) @@ -153,7 +143,7 @@ class AuthData(object): user = {} user["username"] = self.user.username user["tenantId"] = self.user.tenant_id - group = [] + """group = [] for g in self.user.groups.values: grp = {} grp["tenantId"] = g.tenant_id @@ -162,6 +152,7 @@ class AuthData(object): groups = {} groups["group"] = group user["groups"] = groups + """ auth = {} auth["token"] = token auth["user"] = user diff --git a/keystone/logic/types/user.py b/keystone/logic/types/user.py index 6f6e619e..43e7e986 100644 --- a/keystone/logic/types/user.py +++ b/keystone/logic/types/user.py @@ -19,6 +19,7 @@ import string import keystone.logic.types.fault as fault + class User(object): def __init__(self, password, user_id, tenant_id, email, enabled): @@ -33,7 +34,8 @@ class User(object): try: dom = etree.Element("root") dom.append(etree.fromstring(xml_str)) - root = dom.find("{http://docs.openstack.org/identity/api/v2.0}user") + root = dom.find("{http://docs.openstack.org/identity/api/v2.0}" \ + "user") if root == None: raise fault.BadRequestFault("Expecting User") user_id = root.get("id") @@ -43,8 +45,6 @@ class User(object): enabled = root.get("enabled") if user_id == None: raise fault.BadRequestFault("Expecting User") - elif tenant_id == None: - raise fault.BadRequestFault("Expecting User tenant") elif password == None: raise fault.BadRequestFault("Expecting User password") elif email == None: @@ -75,9 +75,10 @@ class User(object): if not "password" in user: raise fault.BadRequestFault("Expecting User Password") password = user["password"] - if not "tenantId" in user: - raise fault.BadRequestFault("Expecting User Tenant") - tenant_id = user["tenantId"] + if "tenantId" in user: + tenant_id = user["tenantId"] + else: + tenant_id = None if not "email" in user: raise fault.BadRequestFault("Expecting User Email") email = user["email"] @@ -87,15 +88,13 @@ class User(object): raise fault.BadRequestFault("Bad enabled attribute!") else: set_enabled = True - if password == '': - password = user_id return User(password, user_id, tenant_id, email, set_enabled) except (ValueError, TypeError) as e: raise fault.BadRequestFault("Cannot parse Tenant", str(e)) def to_dom(self): dom = etree.Element("user", - xmlns="http://docs.openstack.org/identity/api/v2.0") + xmlns="http://docs.openstack.org/identity/api/v2.0") if self.email: dom.set("email", self.email) if self.tenant_id: @@ -116,7 +115,8 @@ class User(object): if self.user_id: user["id"] = self.user_id - user["tenantId"] = self.tenant_id + if self.tenant_id: + user["tenantId"] = self.tenant_id if self.password: user["password"] = self.password user["email"] = self.email @@ -143,7 +143,8 @@ class User_Update(object): try: dom = etree.Element("root") dom.append(etree.fromstring(xml_str)) - root = dom.find("{http://docs.openstack.org/identity/api/v2.0}user") + root = dom.find("{http://docs.openstack.org/identity/api/v2.0}" \ + "user") if root == None: raise fault.BadRequestFault("Expecting User") user_id = root.get("id") @@ -200,7 +201,7 @@ class User_Update(object): def to_dom(self): dom = etree.Element("user", - xmlns="http://docs.openstack.org/identity/api/v2.0") + xmlns="http://docs.openstack.org/identity/api/v2.0") if self.email: dom.set("email", self.email) if self.tenant_id: diff --git a/keystone/server.py b/keystone/server.py index 91526c9a..59a3f8e3 100755..100644 --- a/keystone/server.py +++ b/keystone/server.py @@ -173,6 +173,37 @@ class AuthController(wsgi.Controller): service.revoke_token(utils.get_auth_token(req), token_id)) +class LegacyAuthController(wsgi.Controller): + """ + Auth Controller for v1.x - + Controller for token related operations + """ + + def __init__(self, options): + self.options = options + self.request = None + + @utils.wrap_error + def authenticate(self, req): + self.request = req + + creds = auth.PasswordCredentials(utils.get_auth_user(req), + utils.get_auth_key(req), + None) + + """HTTP/1.1 204 No Content + Date: Mon, 12 Nov 2007 15:32:21 GMT + X-Storage-Url: https://storage.clouddrive.com/v1/CF_xer7_34 + X-CDN-Management-Url: https://cdn.clouddrive.com/v1/CF_xer7_34 + X-Auth-Token: eaaafd18-0fed-4b3a-81b4-663c99ec1cbb + Content-Length: 0 + Content-Type: text/plain; charset=UTF-8""" + + result = service.authenticate(creds) + headers = {"X-Auth-Token": result.token.token_id} + return utils.send_legacy_result(204, headers) + + class TenantController(wsgi.Controller): """ Tenant Controller - @@ -484,13 +515,94 @@ class GroupsController(wsgi.Controller): class KeystoneAPI(wsgi.Router): - """WSGI entry point for all Keystone Auth API requests.""" + """WSGI entry point for public Keystone API requests.""" def __init__(self, options): self.options = options mapper = routes.Mapper() - + db_api.configure_db(options) + + # Legacy Token Operations + auth_controller = AuthController(options) + legacy_auth_controller = LegacyAuthController(options) + mapper.connect("/v1.0", controller=legacy_auth_controller, + action="authenticate") + mapper.connect("/v1.0/", controller=legacy_auth_controller, + action="authenticate") + + mapper.connect("/v1.1/tokens", controller=auth_controller, + action="authenticate", + conditions=dict(method=["POST"])) + mapper.connect("/v1.1/tokens/", controller=auth_controller, + action="authenticate", + conditions=dict(method=["POST"])) + + # Token Operations + mapper.connect("/v2.0/token", controller=auth_controller, + action="authenticate") + mapper.connect("/v2.0/token/{token_id}", controller=auth_controller, + action="delete_token", + conditions=dict(method=["DELETE"])) + + # Tenant Operations + tenant_controller = TenantController(options) + mapper.connect("/v2.0/tenants", controller=tenant_controller, + action="get_tenants", conditions=dict(method=["GET"])) + + # Miscellaneous Operations + version_controller = VersionController(options) + mapper.connect("/v2.0/", controller=version_controller, + action="get_version_info", + conditions=dict(method=["GET"])) + mapper.connect("/v2.0", controller=version_controller, + action="get_version_info", + conditions=dict(method=["GET"])) + + # Static Files Controller + static_files_controller = StaticFilesController(options) + mapper.connect("/v2.0/identitydevguide.pdf", + controller=static_files_controller, + action="get_pdf_contract", + conditions=dict(method=["GET"])) + mapper.connect("/v2.0/identity.wadl", + controller=static_files_controller, + action="get_identity_wadl", + conditions=dict(method=["GET"])) + mapper.connect("/v2.0/xsd/{xsd}", + controller=static_files_controller, + action="get_pdf_contract", + conditions=dict(method=["GET"])) + mapper.connect("/v2.0/xsd/atom/{xsd}", + controller=static_files_controller, + action="get_pdf_contract", + conditions=dict(method=["GET"])) + + super(KeystoneAPI, self).__init__(mapper) + + +class KeystoneAdminAPI(wsgi.Router): + """WSGI entry point for admin Keystone API requests.""" + + def __init__(self, options): + self.options = options + mapper = routes.Mapper() + + db_api.configure_db(options) + + # Legacy Token Operations + legacy_auth_controller = LegacyAuthController(options) + mapper.connect("/v1.0", controller=legacy_auth_controller, + action="authenticate") + mapper.connect("/v1.0/", controller=legacy_auth_controller, + action="authenticate") + mapper.connect("/v1.1/tokens", controller=legacy_auth_controller, + action="authenticate", + conditions=dict(method=["POST"])) + mapper.connect("/v1.1/tokens/", controller=legacy_auth_controller, + action="authenticate", + conditions=dict(method=["POST"])) + # Token Operations auth_controller = AuthController(options) mapper.connect("/v2.0/token", controller=auth_controller, @@ -649,7 +761,7 @@ class KeystoneAPI(wsgi.Router): action="get_pdf_contract", conditions=dict(method=["GET"])) - super(KeystoneAPI, self).__init__(mapper) + super(KeystoneAdminAPI, self).__init__(mapper) def app_factory(global_conf, **local_conf): @@ -660,3 +772,13 @@ def app_factory(global_conf, **local_conf): except Exception as err: print err return KeystoneAPI(conf) + + +def admin_app_factory(global_conf, **local_conf): + """paste.deploy app factory for creating OpenStack API server apps""" + try: + conf = global_conf.copy() + conf.update(local_conf) + except Exception as err: + print err + return KeystoneAdminAPI(conf) diff --git a/keystone/utils.py b/keystone/utils.py index df480e31..a72d40b6 100644 --- a/keystone/utils.py +++ b/keystone/utils.py @@ -56,6 +56,20 @@ def get_auth_token(req): return auth_token +def get_auth_user(req): + auth_user = None + if "X-Auth-User" in req.headers: + auth_user = req.headers["X-Auth-User"] + return auth_user + + +def get_auth_key(req): + auth_key = None + if "X-Auth-Key" in req.headers: + auth_key = req.headers["X-Auth-Key"] + return auth_key + + def wrap_error(func): @functools.wraps(func) @@ -68,7 +82,8 @@ def wrap_error(func): else: logging.exception(err) return send_error(500, kwargs['req'], - fault.IdentityFault("Unhandled error", str(err))) + fault.IdentityFault("Unhandled error", + str(err))) return check_error @@ -80,7 +95,8 @@ def get_normalized_request_content(model, req): elif req.content_type == "application/json": ret = model.from_json(req.body) else: - raise fault.IDMFault("I don't understand the content type ", code=415) + raise fault.IdentityFault("I don't understand the content type ", + code=415) return ret @@ -129,3 +145,18 @@ def send_result(code, req, result): resp.unicode_body = content.decode('UTF-8') return resp + + +def send_legacy_result(code, headers): + resp = Response() + if 'content-type' not in headers: + headers['content-type'] = "text/plain" + + resp.headers = headers + resp.status = code + if code > 399: + return resp + + resp.content_type_params = {'charset': 'UTF-8'} + + return resp diff --git a/keystone/version.py b/keystone/version.py deleted file mode 100755 index 770259b4..00000000 --- a/keystone/version.py +++ /dev/null @@ -1,33 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 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. - -""" Version file for the python daemon script """ - -KEYSTONE_VERSION = ['1', '0'] -MAJOR, MINOR = KEYSTONE_VERSION - -FINAL = True - - -def canonical_version_string(): - return '.'.join([MAJOR, MINOR]) - - -def version_string(): - if FINAL: - return canonical_version_string() - else: - return '%s-dev' % (canonical_version_string(),) diff --git a/management/delgroup.py b/management/delgroup.py index 9ef2d733..1b48cd77 100644..100755 --- a/management/delgroup.py +++ b/management/delgroup.py @@ -1,42 +1,54 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog group_id"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 1:
- parser.error("Incorrect number of arguments")
- else:
- index = args[0]
-
- try:
- o = db_api.group_get(index)
- if o == None:
- raise IndexError("Group %s not found", index)
- else:
- db_api.group_delete(index)
- print 'Group', index, 'deleted.'
-
- except Exception, e:
- print 'Error deleting group', index, str(e)
-
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog group_id" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + index = args[0] + + try: + o = db_api.group_get(index) + if o == None: + raise IndexError("Group %s not found", index) + else: + db_api.group_delete(index) + print 'Group', index, 'deleted.' + + except Exception, e: + print 'Error deleting group', index, str(e) + + +if __name__ == '__main__': + main() diff --git a/management/getgroup.py b/management/getgroup.py index 38a6b8f2..9bb0e41f 100644..100755 --- a/management/getgroup.py +++ b/management/getgroup.py @@ -1,40 +1,52 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog group_id"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 1:
- parser.error("Incorrect number of arguments")
- else:
- index = args[0]
-
- try:
- o = db_api.group_get(index)
- if o == None:
- raise IndexError("Group %s not found", index)
-
- print o.id, o.desc
- except Exception, e:
- print 'Error getting group', index, str(e)
-
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog group_id" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + index = args[0] + + try: + o = db_api.group_get(index) + if o == None: + raise IndexError("Group %s not found", index) + + print o.id, o.desc + except Exception, e: + print 'Error getting group', index, str(e) + + +if __name__ == '__main__': + main() diff --git a/management/getgroups.py b/management/getgroups.py index 501d948f..2fc94cb9 100644..100755 --- a/management/getgroups.py +++ b/management/getgroups.py @@ -1,37 +1,49 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog "
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 0:
- parser.error("Incorrect number of arguments")
- else:
- try:
- u = db_api.group_get_all()
- if u == None:
- raise IndexError("Groups not found")
- for row in u:
- print row.id
- except Exception, e:
- print 'Error getting groups:', str(e)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog " + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 0: + parser.error("Incorrect number of arguments") + else: + try: + u = db_api.group_get_all() + if u == None: + raise IndexError("Groups not found") + for row in u: + print row.id + except Exception, e: + print 'Error getting groups:', str(e) + +if __name__ == '__main__': + main() diff --git a/management/getgroupusers.py b/management/getgroupusers.py index 0fc7d231..1d663eb4 100644..100755 --- a/management/getgroupusers.py +++ b/management/getgroupusers.py @@ -1,39 +1,51 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog group_id"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 1:
- parser.error("Incorrect number of arguments")
- else:
- group_id = args[0]
-
- try:
- g = db_api.group_users(group_id)
- if g == None:
- raise IndexError("Group users not found")
- for row in g:
- print row
- except Exception, e:
- print 'Error getting group users for group', group_id, ':', str(e)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog group_id" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + group_id = args[0] + + try: + g = db_api.group_users(group_id) + if g == None: + raise IndexError("Group users not found") + for row in g: + print row + except Exception, e: + print 'Error getting group users for group', group_id, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/getuser.py b/management/getuser.py index 036f10f4..af0ba89f 100644..100755 --- a/management/getuser.py +++ b/management/getuser.py @@ -1,37 +1,49 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog username"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 1:
- parser.error("Incorrect number of arguments")
- else:
- username = args[0]
- try:
- u = db_api.user_get(username)
- if u == None:
- raise IndexError("User not found")
- print u.id, u.email, u.enabled
- except Exception, e:
- print 'Error finding user', username, ':', str(e)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog username" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + username = args[0] + try: + u = db_api.user_get(username) + if u == None: + raise IndexError("User not found") + print u.id, u.email, u.enabled + except Exception, e: + print 'Error finding user', username, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/getusergroups.py b/management/getusergroups.py index 3fd7d3f9..33801a93 100644..100755 --- a/management/getusergroups.py +++ b/management/getusergroups.py @@ -1,38 +1,50 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog user_id"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 1:
- parser.error("Incorrect number of arguments")
- else:
- username = args[0]
- try:
- g = db_api.user_groups(username)
- if g == None:
- raise IndexError("User groups not found")
- for row in g:
- print row
- except Exception, e:
- print 'Error getting user groups for user', user_id, ':', str(e)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog user_id" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + username = args[0] + try: + g = db_api.user_groups(username) + if g == None: + raise IndexError("User groups not found") + for row in g: + print row + except Exception, e: + print 'Error getting user groups for user', user_id, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/getusers.py b/management/getusers.py deleted file mode 100644 index 18a5de56..00000000 --- a/management/getusers.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog tenant_id"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 1:
- parser.error("Incorrect number of arguments")
- else:
- tenant_id = args[0]
- try:
- u = db_api.user_get_by_tenant(tenant_id)
- if u == None:
- raise IndexError("Users not found")
- for row in u:
- print row
- except Exception, e:
- print 'Error getting users for tenant', tenant_id, ':', str(e)
-
-if __name__ == '__main__':
- main()
diff --git a/management/groupadd.py b/management/groupadd.py index adec58dc..11329e44 100644..100755 --- a/management/groupadd.py +++ b/management/groupadd.py @@ -1,40 +1,52 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-from keystone.db.sqlalchemy import models
-
-
-def main():
- usage = "usage: %prog group_id group_desc"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 2:
- parser.error("Incorrect number of arguments")
- else:
- group_id = args[0]
- group_desc = args[1]
- try:
- g = models.Group()
- g.id = group_id
- g.desc = group_desc
- db_api.group_create(g)
- print 'Group', g.id, 'created.'
- except Exception, e:
- print 'Error creating group', group_id, ':', str(e)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api +from keystone.db.sqlalchemy import models + + +def main(): + usage = "usage: %prog group_id group_desc" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + group_id = args[0] + group_desc = args[1] + try: + g = models.Group() + g.id = group_id + g.desc = group_desc + db_api.group_create(g) + print 'Group', g.id, 'created.' + except Exception, e: + print 'Error creating group', group_id, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/setuserlock.py b/management/setuserlock.py index 622d876b..7cfa8c15 100644..100755 --- a/management/setuserlock.py +++ b/management/setuserlock.py @@ -1,49 +1,61 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog username enabled"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 2:
- parser.error("Incorrect number of arguments")
- else:
- username = args[0]
- enabled = args[1].capitalize().strip()
-
- if enabled == 'True' or enabled == '1':
- enabled = 1
- elif enabled == 'False' or enabled == '0':
- enabled = 0
- else:
- parser.error("Incorrect arguments value")
-
- try:
- u = db_api.user_get(username)
- if u == None:
- raise IndexError("User not found")
- else:
- values = {'enabled': enabled}
- db_api.user_update(username, values)
- print 'User', u.id, 'updated. Enabled =', enabled
- except Exception, e:
- print 'Error updating user', username, ':', str(e)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog username enabled" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + username = args[0] + enabled = args[1].capitalize().strip() + + if enabled == 'True' or enabled == '1': + enabled = 1 + elif enabled == 'False' or enabled == '0': + enabled = 0 + else: + parser.error("Incorrect arguments value") + + try: + u = db_api.user_get(username) + if u == None: + raise IndexError("User not found") + else: + values = {'enabled': enabled} + db_api.user_update(username, values) + print 'User', u.id, 'updated. Enabled =', enabled + except Exception, e: + print 'Error updating user', username, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/setuserpswd.py b/management/setuserpswd.py index 6283bf73..abfddbda 100644..100755 --- a/management/setuserpswd.py +++ b/management/setuserpswd.py @@ -1,41 +1,53 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog username password"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 2:
- parser.error("Incorrect number of arguments")
- else:
- username = args[0]
- password = args[1]
- try:
- u = db_api.user_get(username)
- if u == None:
- raise IndexError("User not found")
- else:
- values = {'password': password}
- db_api.user_update(username, values)
- print 'User', u.id, 'updated.'
- except Exception, e:
- print 'Error updating user', username, ':', str(e)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog username password" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + username = args[0] + password = args[1] + try: + u = db_api.user_get(username) + if u == None: + raise IndexError("User not found") + else: + values = {'password': password} + db_api.user_update(username, values) + print 'User', u.id, 'updated.' + except Exception, e: + print 'Error updating user', username, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/updategroup.py b/management/updategroup.py index 598d38ef..35b4ef66 100644..100755 --- a/management/updategroup.py +++ b/management/updategroup.py @@ -1,42 +1,54 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-from keystone.db.sqlalchemy import models
-
-
-def main():
- usage = "usage: %prog group_id group_desc"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 2:
- parser.error("Incorrect number of arguments")
- else:
- group = args[0]
- desc = args[1]
- try:
- g = db_api.group_get(group)
- if g == None:
- raise IndexError("Group not found")
- else:
- values = {'desc': desc}
- db_api.group_update(group, values)
- print 'Group', g.id, 'updated.'
- except Exception, e:
- print 'Error updating user', group, ':', str(e)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api +from keystone.db.sqlalchemy import models + + +def main(): + usage = "usage: %prog group_id group_desc" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + group = args[0] + desc = args[1] + try: + g = db_api.group_get(group) + if g == None: + raise IndexError("Group not found") + else: + values = {'desc': desc} + db_api.group_update(group, values) + print 'Group', g.id, 'updated.' + except Exception, e: + print 'Error updating user', group, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/useradd.py b/management/useradd.py deleted file mode 100644 index fc3fc8b6..00000000 --- a/management/useradd.py +++ /dev/null @@ -1,42 +0,0 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-from keystone.db.sqlalchemy import models
-
-
-def main():
- usage = "usage: %prog username password"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 2:
- parser.error("Incorrect number of arguments")
- else:
- username = args[0]
- password = args[1]
- try:
- u = models.User()
- u.id = username
- u.email = username
- u.password = password
- u.enabled = True
- db_api.user_create(u)
- print 'User', u.id, 'created.'
- except Exception, e:
- print 'Error creating user', username, ':', str(e)
-
-if __name__ == '__main__':
- main()
diff --git a/management/userdel.py b/management/userdel.py index 928dfc55..f76bc632 100644..100755 --- a/management/userdel.py +++ b/management/userdel.py @@ -1,39 +1,51 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog username"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 1:
- parser.error("Incorrect number of arguments")
- else:
- username = args[0]
- try:
- u = db_api.user_get(username)
- if u == None:
- raise IndexError("User not found")
- else:
- db_api.user_delete(username)
- print 'User', username, 'deleted.'
- except Exception, e:
- print 'Error deleting user', username, ':', str(e)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog username" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 1: + parser.error("Incorrect number of arguments") + else: + username = args[0] + try: + u = db_api.user_get(username) + if u == None: + raise IndexError("User not found") + else: + db_api.user_delete(username) + print 'User', username, 'deleted.' + except Exception, e: + print 'Error deleting user', username, ':', str(e) + +if __name__ == '__main__': + main() diff --git a/management/userupdate.py b/management/userupdate.py index 89174d04..5196d681 100644..100755 --- a/management/userupdate.py +++ b/management/userupdate.py @@ -1,41 +1,53 @@ -# 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.
-
-import optparse
-import keystone.db.sqlalchemy.api as db_api
-
-
-def main():
- usage = "usage: %prog username email"
- parser = optparse.OptionParser(usage)
- options, args = parser.parse_args()
- if len(args) != 2:
- parser.error("Incorrect number of arguments")
- else:
- username = args[0]
- email = args[1]
- try:
- u = db_api.user_get(username)
- if u == None:
- raise IndexError("User not found")
- else:
- values = {'email': email}
- db_api.user_update(username, values)
- print 'User', u.id, 'updated.'
- except Exception, e:
- print 'Error updating user', username, ':', str(e)
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python +# 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. + +import os +import sys + +# 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 optparse +import keystone.db.sqlalchemy.api as db_api + + +def main(): + usage = "usage: %prog username email" + parser = optparse.OptionParser(usage) + options, args = parser.parse_args() + if len(args) != 2: + parser.error("Incorrect number of arguments") + else: + username = args[0] + email = args[1] + try: + u = db_api.user_get(username) + if u == None: + raise IndexError("User not found") + else: + values = {'email': email} + db_api.user_update(username, values) + print 'User', u.id, 'updated.' + except Exception, e: + print 'Error updating user', username, ':', str(e) + +if __name__ == '__main__': + main() @@ -30,13 +30,14 @@ setup( url='http://www.openstack.org', include_package_data=True, packages=find_packages(exclude=['test', 'bin']), - scripts=['bin/keystone'] + scripts=['bin/keystone, bin/leystone-auth, bin/keystone-admin' + ', bin/keystone-manage'], zip_safe=False, install_requires=['setuptools'], entry_points={ 'paste.app_factory': ['main=identity:app_factory'], 'paste.filter_factory': [ - 'papiauth=keystone:papiauth_factory', + 'remoteauth=keystone:remoteauth_factory', 'tokenauth=keystone:tokenauth_factory', ], }, diff --git a/test/unit/test_authentication.py b/test/unit/test_authentication.py index a6531473..2cfd8e31 100644 --- a/test/unit/test_authentication.py +++ b/test/unit/test_authentication.py @@ -55,6 +55,11 @@ class AuthenticationTest(unittest.TestCase): self.assertEqual(200, int(resp['status'])) self.assertEqual('application/xml', utils.content_type(resp)) + def test_a_authorize_legacy(self): + resp, content = utils.get_token_legacy('joeuser', 'secrete') + self.assertEqual(204, int(resp['status'])) + self.assertTrue(resp['x-auth-token']) + def test_a_authorize_user_disabled(self): header = httplib2.Http(".cache") url = '%stoken' % utils.URL @@ -153,7 +158,7 @@ class MultiToken(unittest.TestCase): utils.delete_user('test_tenant2', 'test_user1', self.auth_token) utils.delete_tenant('test_tenant1', self.auth_token) utils.delete_tenant('test_tenant2', self.auth_token) - + """ INVALID TEST - we're changing how we delegate access to second tenant def test_multi_token(self): #get token for user1 with tenant1 @@ -170,7 +175,7 @@ class MultiToken(unittest.TestCase): resp = utils.delete_token(token1, self.auth_token) resp = utils.delete_token(token2, self.auth_token) """ - + def test_unassigned_user(self): resp, content = utils.get_token('test_user2', 'secrete', \ 'test_tenant2') diff --git a/test/unit/test_common.py b/test/unit/test_common.py index 07c46f50..800f1a7c 100644 --- a/test/unit/test_common.py +++ b/test/unit/test_common.py @@ -24,15 +24,13 @@ sys.path.append(os.path.abspath(os.path.join(os.path.abspath(__file__), '..', '..', '..', '..', 'keystone'))) import unittest - - -URL = 'http://localhost:8080/v2.0/' - +URL = 'http://localhost:8081/v2.0/' +URLv1 = 'http://localhost:8081/v1.0/' def get_token(user, pswd, tenant_id, kind=''): header = httplib2.Http(".cache") url = '%stoken' % URL - # to test multi token, removing below code + if not tenant_id: body = {"passwordCredentials": {"username": user, "password": pswd}} @@ -40,7 +38,7 @@ def get_token(user, pswd, tenant_id, kind=''): body = {"passwordCredentials": {"username": user, "password": pswd, "tenantId": tenant_id}} - + resp, content = header.request(url, "POST", body=json.dumps(body), headers={"Content-Type": "application/json"}) @@ -55,6 +53,23 @@ def get_token(user, pswd, tenant_id, kind=''): return (resp, content) +def get_token_legacy(user, pswd, kind=''): + header = httplib2.Http(".cache") + url = URLv1 + resp, content = header.request(url, "GET", '', + headers={"Content-Type": "application/json", + "X-Auth-User": user, + "X-Auth-Key": pswd}) + + if int(resp['status']) == 204: + token = resp['x-auth-token'] + else: + token = None + if kind == 'token': + return token + else: + return (resp, content) + def delete_token(token, auth_token): header = httplib2.Http(".cache") @@ -180,7 +195,8 @@ def get_token_xml(user, pswd, tenant_id, type=''): "ACCEPT": "application/xml"}) if int(resp['status']) == 200: dom = etree.fromstring(content) - root = dom.find("{http://docs.openstack.org/identity/api/v2.0}token") + root = dom.find("{http://docs.openstack.org/" \ + "identity/api/v2.0}token") token_root = root.attrib token = str(token_root['id']) else: @@ -318,6 +334,7 @@ def delete_user_xml(tenantid, userid, auth_token): "ACCEPT": "application/xml"}) return resp + def add_user_json(tenantid, userid, auth_token): header = httplib2.Http(".cache") url = '%stenants/%s/users/%s/add' % (URL, tenantid, userid) @@ -676,7 +693,8 @@ def handle_user_resp(self, content, respvalue, resptype): if respvalue == 200: if resptype == 'application/json': content = json.loads(content) - self.tenant = content['user']['tenantId'] + if 'tenantId' in content['user']: + self.tenant = content['user']['tenantId'] self.userid = content['user']['id'] if resptype == 'application/xml': content = etree.fromstring(content) @@ -686,7 +704,3 @@ def handle_user_resp(self, content, respvalue, resptype): self.fail('Identity Fault') elif respvalue == 503: self.fail('Service Not Available') - - -def content_type(resp): - return resp['content-type'].split(';')[0] diff --git a/test/unit/test_exthandler.py b/test/unit/test_exthandler.py index 5d41aeac..4379e03f 100644 --- a/test/unit/test_exthandler.py +++ b/test/unit/test_exthandler.py @@ -1,3 +1,19 @@ +# 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. + import os import sys # Need to access identity module diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tools/__init__.py diff --git a/tools/tracer.py b/tools/tracer.py new file mode 100644 index 00000000..27d52eb2 --- /dev/null +++ b/tools/tracer.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. +# +# Author: Ziad Sawalha (http://launchpad.net/~ziad-sawalha) + +""" +OpenStack Call Tracing Tool + +To use this: +1. include the tools dirextory in your project (__init__.py and tracer.py) +2. import tools.tracer as early as possible into your module +3. add --trace-calls to any argument parsers you use so the argument doesn't get +flagged as invalid. + +Usage: +# Add this as early as possible in the first module called in your service +import tools.tracer #load this first + +If a '--trace-calls' parameter is found, it will trace calls to the console and +space them to show the call graph. + +""" + +import os +import sys + + +if '--trace-calls' in sys.argv: + stack_depth = 0 + + def localtrace(frame, event, arg): + global stack_depth + if event == "return": + stack_depth = stack_depth - 1 + elif event == "exception": + co = frame.f_code + func_name = co.co_name + line_no = frame.f_lineno + filename = co.co_filename + exc_type, exc_value, exc_traceback = arg + print '\033[91m%sERROR: %s %s on line %s of %s\033[0m' % \ + (' ' * stack_depth, exc_type.__name__, exc_value, line_no, + func_name) + return None + + def selectivetrace(frame, event, arg): + global stack_depth + if event == "exception": + co = frame.f_code + func_name = co.co_name + line_no = frame.f_lineno + filename = co.co_filename + exc_type, exc_value, exc_traceback = arg + print '\033[91m%sERROR: %s %s on line %s of %s\033[0m' % \ + (' ' * stack_depth, exc_type.__name__, exc_value, line_no, + func_name) + if event != 'call': + return + co = frame.f_code + func_name = co.co_name + if func_name == 'write': + # Ignore write() calls from print statements + return + func_filename = co.co_filename + if func_filename == "<string>": + return + if func_filename.startswith("/System"): + return + if func_filename.startswith("/Library"): + return + if 'macosx' in func_filename: + return + func_line_no = frame.f_lineno + # 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)) + func_filename = func_filename.replace(possible_topdir, '') + caller = frame.f_back + caller_line_no = caller.f_lineno + caller_filename = caller.f_code.co_filename.replace(possible_topdir, + '') + print '%s%s::%s:%s (from %s:%s)' % \ + (' ' * stack_depth, func_filename, func_name, func_line_no, + caller_filename, caller_line_no) + stack_depth = stack_depth + 1 + return localtrace + + sys.settrace(selectivetrace) + print 'Starting OpenStack call tracer' |
