From b56bf3a0f3f5f80d3c2b50684d866ef79aa312d9 Mon Sep 17 00:00:00 2001 From: gholt Date: Fri, 10 Sep 2010 13:40:43 -0700 Subject: [PATCH] DevAuth support for reseller admins and an initial super admin. DevAuth server no longer needs the account ring or direct account server access. Proxy server supports account PUTs. --- bin/swift-auth-add-user | 16 ++- bin/swift-auth-recreate-accounts | 28 +++-- doc/source/development_saio.rst | 12 +- etc/auth-server.conf-sample | 2 + swift/auth/server.py | 197 ++++++++++++++++++------------- swift/common/constraints.py | 2 + swift/common/middleware/auth.py | 11 +- swift/proxy/server.py | 60 +++++++++- test/functional/swift.py | 2 +- test/functional/tests.py | 2 +- test/unit/auth/test_server.py | 120 +++++++++---------- 11 files changed, 284 insertions(+), 168 deletions(-) diff --git a/bin/swift-auth-add-user b/bin/swift-auth-add-user index 0f7eb3c3a2..d502dc83a8 100755 --- a/bin/swift-auth-add-user +++ b/bin/swift-auth-add-user @@ -33,6 +33,16 @@ if __name__ == '__main__': default=False, help='Give the user administrator access; otherwise ' 'the user will only have access to containers specifically allowed ' 'with ACLs.') + parser.add_option('-r', '--reseller-admin', dest='reseller_admin', + action='store_true', default=False, help='Give the user full reseller ' + 'administrator access, giving them full access to all accounts within ' + 'the reseller, including the ability to create new accounts. Creating ' + 'a new reseller admin requires super_admin rights.') + parser.add_option('-U', '--admin-user', dest='admin_user', + default='.super_admin', help='The user with admin rights to add users ' + '(default: .super_admin).') + parser.add_option('-K', '--admin-key', dest='admin_key', + help='The key for the user with admin rights to add users.') args = argv[1:] if not args: args.append('-h') @@ -48,9 +58,13 @@ if __name__ == '__main__': port = int(conf.get('bind_port', 11000)) ssl = conf.get('cert_file') is not None path = '/account/%s/%s' % (account, user) - headers = {'X-Auth-User-Key': password} + headers = {'X-Auth-Admin-User': options.admin_user, + 'X-Auth-Admin-Key': options.admin_key, + 'X-Auth-User-Key': password} if options.admin: headers['X-Auth-User-Admin'] = 'true' + if options.reseller_admin: + headers['X-Auth-User-Reseller-Admin'] = 'true' conn = http_connect(host, port, 'PUT', path, headers, ssl=ssl) resp = conn.getresponse() if resp.status == 204: diff --git a/bin/swift-auth-recreate-accounts b/bin/swift-auth-recreate-accounts index 8a14948c6e..e17bf2da3b 100755 --- a/bin/swift-auth-recreate-accounts +++ b/bin/swift-auth-recreate-accounts @@ -15,25 +15,37 @@ # limitations under the License. from ConfigParser import ConfigParser +from optparse import OptionParser from sys import argv, exit from swift.common.bufferedhttp import http_connect_raw as http_connect if __name__ == '__main__': - f = '/etc/swift/auth-server.conf' - if len(argv) == 2: - f = argv[1] - elif len(argv) != 1: - exit('Syntax: %s [conf_file]' % argv[0]) + default_conf = '/etc/swift/auth-server.conf' + parser = OptionParser(usage='Usage: %prog [options]') + parser.add_option('-c', '--conf', dest='conf', default=default_conf, + help='Configuration file to determine how to connect to the local ' + 'auth server (default: %s).' % default_conf) + parser.add_option('-U', '--admin-user', dest='admin_user', + default='.super_admin', help='The user with admin rights to recreate ' + 'accounts (default: .super_admin).') + parser.add_option('-K', '--admin-key', dest='admin_key', + help='The key for the user with admin rights to recreate accounts.') + args = argv[1:] + if not args: + args.append('-h') + (options, args) = parser.parse_args(args) c = ConfigParser() - if not c.read(f): - exit('Unable to read conf file: %s' % f) + if not c.read(options.conf): + exit('Unable to read conf file: %s' % options.conf) conf = dict(c.items('app:auth-server')) host = conf.get('bind_ip', '127.0.0.1') port = int(conf.get('bind_port', 11000)) ssl = conf.get('cert_file') is not None path = '/recreate_accounts' - conn = http_connect(host, port, 'POST', path, ssl=ssl) + conn = http_connect(host, port, 'POST', path, ssl=ssl, + headers={'X-Auth-Admin-User': options.admin_user, + 'X-Auth-Admin-Key': options.admin_key}) resp = conn.getresponse() if resp.status == 200: print resp.read() diff --git a/doc/source/development_saio.rst b/doc/source/development_saio.rst index b583641ab5..9365665cb9 100644 --- a/doc/source/development_saio.rst +++ b/doc/source/development_saio.rst @@ -177,6 +177,8 @@ good idea what to do on other environments. [app:auth-server] use = egg:swift#auth default_cluster_url = http://127.0.0.1:8080/v1 + # Highly recommended to change this. + super_admin_key = devauth #. Create `/etc/swift/proxy-server.conf`:: @@ -511,7 +513,9 @@ good idea what to do on other environments. #!/bin/bash - swift-auth-recreate-accounts + # Replace devauth with whatever your super_admin key is (recorded in + # /etc/swift/auth-server.conf). + swift-auth-recreate-accounts -K devauth swift-init object-updater start swift-init container-updater start swift-init object-replicator start @@ -526,12 +530,12 @@ good idea what to do on other environments. #. `remakerings` #. `cd ~/swift/trunk; ./.unittests` #. `startmain` (The ``Unable to increase file descriptor limit. Running as non-root?`` warnings are expected and ok.) - #. `swift-auth-add-user --admin test tester testing` + #. `swift-auth-add-user -K devauth -a test tester testing` # Replace ``devauth`` with whatever your super_admin key is (recorded in /etc/swift/auth-server.conf). #. Get an `X-Storage-Url` and `X-Auth-Token`: ``curl -v -H 'X-Storage-User: test:tester' -H 'X-Storage-Pass: testing' http://127.0.0.1:11000/v1.0`` #. Check that you can GET account: ``curl -v -H 'X-Auth-Token: ' `` #. Check that `st` works: `st -A http://127.0.0.1:11000/v1.0 -U test:tester -K testing stat` - #. `swift-auth-add-user --admin test2 tester2 testing2` - #. `swift-auth-add-user test tester3 testing3` + #. `swift-auth-add-user -K devauth -a test2 tester2 testing2` # Replace ``devauth`` with whatever your super_admin key is (recorded in /etc/swift/auth-server.conf). + #. `swift-auth-add-user -K devauth test tester3 testing3` # Replace ``devauth`` with whatever your super_admin key is (recorded in /etc/swift/auth-server.conf). #. `cp ~/swift/trunk/test/functional/sample.conf /etc/swift/func_test.conf` #. `cd ~/swift/trunk; ./.functests` (Note: functional tests will first delete everything in the configured accounts.) diff --git a/etc/auth-server.conf-sample b/etc/auth-server.conf-sample index 0f818d4231..1309726985 100644 --- a/etc/auth-server.conf-sample +++ b/etc/auth-server.conf-sample @@ -12,6 +12,8 @@ pipeline = auth-server [app:auth-server] use = egg:swift#auth +# Highly recommended to change this. +super_admin_key = devauth # log_name = auth-server # log_facility = LOG_LOCAL0 # log_level = INFO diff --git a/swift/auth/server.py b/swift/auth/server.py index 38c72114e5..8b42f81b94 100644 --- a/swift/auth/server.py +++ b/swift/auth/server.py @@ -14,23 +14,21 @@ # limitations under the License. from __future__ import with_statement -import errno import os -import socket from contextlib import contextmanager from time import gmtime, strftime, time from urllib import unquote, quote from uuid import uuid4 +from urlparse import urlparse import sqlite3 from webob import Request, Response -from webob.exc import HTTPBadRequest, HTTPNoContent, HTTPUnauthorized, \ - HTTPServiceUnavailable, HTTPNotFound +from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPNoContent, \ + HTTPUnauthorized, HTTPServiceUnavailable, HTTPNotFound -from swift.common.bufferedhttp import http_connect +from swift.common.bufferedhttp import http_connect_raw as http_connect from swift.common.db import get_db_connection -from swift.common.ring import Ring -from swift.common.utils import get_logger, normalize_timestamp, split_path +from swift.common.utils import get_logger, split_path class AuthController(object): @@ -69,8 +67,7 @@ class AuthController(object): * The developer makes a ReST call to create a new user. * If the account for the user does not yet exist, the auth server makes - ReST calls to the Swift cluster's account servers to create a new account - on its end. + a ReST call to the Swift cluster to create a new account on its end. * The auth server records the information in its database. A last use case is recreating existing accounts; this is really only useful @@ -78,34 +75,34 @@ class AuthController(object): the auth server's database is retained: * A developer makes an ReST call to have the existing accounts recreated. - * For each account in its database, the auth server makes ReST calls to - the Swift cluster's account servers to create a specific account on its - end. + * For each account in its database, the auth server makes a ReST call to + the Swift cluster to create the specific account on its end. :param conf: The [auth-server] dictionary of the auth server configuration file - :param ring: Overrides loading the account ring from a file; useful for - testing. See the etc/auth-server.conf-sample for information on the possible configuration parameters. """ - def __init__(self, conf, ring=None): + def __init__(self, conf): self.logger = get_logger(conf) + self.super_admin_key = conf.get('super_admin_key') + if not self.super_admin_key: + msg = 'No super_admin_key set in conf file! Exiting.' + try: + self.logger.critical(msg) + except: + pass + raise ValueError(msg) self.swift_dir = conf.get('swift_dir', '/etc/swift') self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip() if self.reseller_prefix and self.reseller_prefix[-1] != '_': self.reseller_prefix += '_' - self.default_cluster_url = \ - conf.get('default_cluster_url', 'http://127.0.0.1:8080/v1') + self.default_cluster_url = conf.get('default_cluster_url', + 'http://127.0.0.1:8080/v1').rstrip('/') self.token_life = int(conf.get('token_life', 86400)) self.log_headers = conf.get('log_headers') == 'True' - if ring: - self.account_ring = ring - else: - self.account_ring = \ - Ring(os.path.join(self.swift_dir, 'account.ring.gz')) self.db_file = os.path.join(self.swift_dir, 'auth.db') self.conn = get_db_connection(self.db_file, okay_to_create=True) try: @@ -114,9 +111,16 @@ class AuthController(object): if str(err) == 'no such column: admin': self.conn.execute("ALTER TABLE account ADD COLUMN admin TEXT") self.conn.execute("UPDATE account SET admin = 't'") + try: + self.conn.execute('SELECT reseller_admin FROM account LIMIT 1') + except sqlite3.OperationalError, err: + if str(err) == 'no such column: reseller_admin': + self.conn.execute( + "ALTER TABLE account ADD COLUMN reseller_admin TEXT") self.conn.execute('''CREATE TABLE IF NOT EXISTS account ( account TEXT, url TEXT, cfaccount TEXT, - user TEXT, password TEXT, admin TEXT)''') + user TEXT, password TEXT, admin TEXT, + reseller_admin TEXT)''') self.conn.execute('''CREATE INDEX IF NOT EXISTS ix_account_account ON account (account)''') try: @@ -139,51 +143,36 @@ class AuthController(object): def add_storage_account(self, account_name=''): """ - Creates an account within the Swift cluster by making a ReST call to - each of the responsible account servers. + Creates an account within the Swift cluster by making a ReST call. :param account_name: The desired name for the account; if omitted a UUID4 will be used. :returns: False upon failure, otherwise the name of the account within the Swift cluster. """ - begin = time() orig_account_name = account_name if not account_name: account_name = '%s%s' % (self.reseller_prefix, uuid4().hex) - partition, nodes = self.account_ring.get_nodes(account_name) - headers = {'X-Timestamp': normalize_timestamp(time()), - 'x-cf-trans-id': 'tx' + str(uuid4())} - statuses = [] - for node in nodes: - try: - conn = None - conn = http_connect(node['ip'], node['port'], node['device'], - partition, 'PUT', '/' + account_name, headers) - source = conn.getresponse() - statuses.append(source.status) - if source.status >= 500: - self.logger.error('ERROR With account server %s:%s/%s: ' - 'Response %s %s: %s' % - (node['ip'], node['port'], node['device'], - source.status, source.reason, source.read(1024))) - conn = None - except BaseException, err: - log_call = self.logger.exception - msg = 'ERROR With account server ' \ - '%(ip)s:%(port)s/%(device)s (will retry later): ' % node - if isinstance(err, socket.error): - if err[0] == errno.ECONNREFUSED: - log_call = self.logger.error - msg += 'Connection refused' - elif err[0] == errno.EHOSTUNREACH: - log_call = self.logger.error - msg += 'Host unreachable' - log_call(msg) - rv = False - if len([s for s in statuses if (200 <= s < 300)]) > len(nodes) / 2: - rv = account_name - return rv + url = '%s/%s' % (self.default_cluster_url, account_name) + parsed = urlparse(url) + # Create a single use token. + token = '%stk%s' % (self.reseller_prefix, uuid4().hex) + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO token + (token, created, account, user, cfaccount) VALUES + (?, ?, '.super_admin', '.single_use', '.reseller_admin')''', + (token, time())) + conn.commit() + conn = http_connect(parsed.hostname, parsed.port, 'PUT', parsed.path, + {'X-Auth-Token': token}, ssl=(parsed.scheme == 'https')) + resp = conn.getresponse() + resp.read() + if resp.status // 100 != 2: + self.logger.error('ERROR attempting to create account %s: %s %s' % + (url, resp.status, resp.reason)) + return False + return account_name @contextmanager def get_conn(self): @@ -229,7 +218,9 @@ class AuthController(object): :param token: The token to validate :returns: (TTL, account, user, cfaccount) if valid, False otherwise. - cfaccount will be None for users without admin access. + cfaccount will be None for users without admin access for the + account. cfaccount will be .reseller_admin for users with + full reseller admin rights. """ begin = time() self.purge_old_tokens() @@ -241,18 +232,20 @@ class AuthController(object): (token,)).fetchone() if row is not None: created = row[0] - if time() - created >= self.token_life: + if time() - created < self.token_life: + rv = (self.token_life - (time() - created), row[1], row[2], + row[3]) + # Remove the token if it was expired or single use. + if not rv or rv[2] == '.single_use': conn.execute(''' DELETE FROM token WHERE token = ?''', (token,)) conn.commit() - else: - rv = (self.token_life - (time() - created), row[1], row[2], - row[3]) self.logger.info('validate_token(%s, _, _) = %s [%.02f]' % (repr(token), repr(rv), time() - begin)) return rv - def create_user(self, account, user, password, admin=False): + def create_user(self, account, user, password, admin=False, + reseller_admin=False): """ Handles the create_user call for developers, used to request a user be added in the auth server database. If the account does not yet exist, @@ -274,6 +267,9 @@ class AuthController(object): :param admin: If true, the user will be granted full access to the account; otherwise, another user will have to add the user to the ACLs for containers to grant access. + :param reseller_admin: If true, the user will be granted full access to + all accounts within this reseller, including the + ability to create additional accounts. :returns: False if the create fails, 'already exists' if the user already exists, or storage url if successful @@ -287,9 +283,9 @@ class AuthController(object): (account, user)).fetchone() if row: self.logger.info( - 'ALREADY EXISTS create_user(%s, %s, _, %s) [%.02f]' % + 'ALREADY EXISTS create_user(%s, %s, _, %s, %s) [%.02f]' % (repr(account), repr(user), repr(admin), - time() - begin)) + repr(reseller_admin), time() - begin)) return 'already exists' row = conn.execute( 'SELECT url, cfaccount FROM account WHERE account = ?', @@ -301,21 +297,22 @@ class AuthController(object): account_hash = self.add_storage_account() if not account_hash: self.logger.info( - 'FAILED create_user(%s, %s, _, %s) [%.02f]' % + 'FAILED create_user(%s, %s, _, %s, %s) [%.02f]' % (repr(account), repr(user), repr(admin), - time() - begin)) + repr(reseller_admin), time() - begin)) return False url = self.default_cluster_url.rstrip('/') + '/' + account_hash conn.execute('''INSERT INTO account - (account, url, cfaccount, user, password, admin) - VALUES (?, ?, ?, ?, ?, ?)''', + (account, url, cfaccount, user, password, admin, + reseller_admin) + VALUES (?, ?, ?, ?, ?, ?, ?)''', (account, url, account_hash, user, password, - admin and 't' or '')) + admin and 't' or '', reseller_admin and 't' or '')) conn.commit() self.logger.info( - 'SUCCESS create_user(%s, %s, _, %s) = %s [%.02f]' % - (repr(account), repr(user), repr(admin), repr(url), - time() - begin)) + 'SUCCESS create_user(%s, %s, _, %s, %s) = %s [%.02f]' % + (repr(account), repr(user), repr(admin), repr(reseller_admin), + repr(url), time() - begin)) return url def recreate_accounts(self): @@ -339,6 +336,26 @@ class AuthController(object): (rv, time() - begin)) return rv + def authorize_reseller_admin(self, request): + if request.headers.get('X-Auth-Admin-User') == '.super_admin' and \ + request.headers.get('X-Auth-Admin-Key') == self.super_admin_key: + return None + try: + account, user = \ + request.headers.get('X-Auth-Admin-User').split(':', 1) + except ValueError: + return HTTPForbidden(request=request) + with self.get_conn() as conn: + row = conn.execute(''' + SELECT user FROM account + WHERE account = ? AND user = ? AND password = ? AND + reseller_admin = 't' ''', + (account, user, + request.headers.get('X-Auth-Admin-Key'))).fetchone() + if row: + return None + return HTTPForbidden(request=request) + def handle_token(self, request): """ Handles ReST requests from Swift to validate tokens @@ -362,7 +379,9 @@ class AuthController(object): if not validation: return HTTPNotFound() groups = ['%s:%s' % (validation[1], validation[2]), validation[1]] - if validation[3]: # admin access to a cfaccount + if validation[3]: + # admin access to a cfaccount or ".reseller_admin" to access to all + # accounts, including creating new ones. groups.append(validation[3]) return HTTPNoContent(headers={'X-Auth-TTL': validation[0], 'X-Auth-Groups': ','.join(groups)}) @@ -380,6 +399,7 @@ class AuthController(object): Valid headers: * X-Auth-User-Key: * X-Auth-User-Admin: + * X-Auth-User-Reseller-Admin: If the HTTP request returns with a 204, then the user was added, and the storage url will be available in the X-Storage-Url header. @@ -390,11 +410,21 @@ class AuthController(object): _, account_name, user_name = split_path(request.path, minsegs=3) except ValueError: return HTTPBadRequest() + create_reseller_admin = \ + request.headers.get('x-auth-user-reseller-admin') == 'true' + if create_reseller_admin and ( + request.headers.get('X-Auth-Admin-User') != '.super_admin' or + request.headers.get('X-Auth-Admin-Key') != self.super_admin_key): + return HTTPForbidden(request=request) + resp = self.authorize_reseller_admin(request) + if resp: + return resp if 'X-Auth-User-Key' not in request.headers: return HTTPBadRequest('X-Auth-User-Key is required') password = request.headers['x-auth-user-key'] storage_url = self.create_user(account_name, user_name, password, - request.headers.get('x-auth-user-admin') == 'true') + request.headers.get('x-auth-user-admin') == 'true', + create_reseller_admin) if storage_url == 'already exists': return HTTPBadRequest(storage_url) if not storage_url: @@ -412,6 +442,9 @@ class AuthController(object): :param request: webob.Request object """ + if request.headers.get('X-Auth-Admin-User') != '.super_admin' or \ + request.headers.get('X-Auth-Admin-Key') != self.super_admin_key: + return HTTPForbidden(request=request) result = self.recreate_accounts() return Response(result, 200, request=request) @@ -471,7 +504,7 @@ class AuthController(object): self.purge_old_tokens() with self.get_conn() as conn: row = conn.execute(''' - SELECT cfaccount, url, admin FROM account + SELECT cfaccount, url, admin, reseller_admin FROM account WHERE account = ? AND user = ? AND password = ?''', (account, user, password)).fetchone() if row is None: @@ -479,6 +512,7 @@ class AuthController(object): cfaccount = row[0] url = row[1] admin = row[2] == 't' + reseller_admin = row[3] == 't' row = conn.execute(''' SELECT token FROM token WHERE account = ? AND user = ?''', (account, user)).fetchone() @@ -486,11 +520,16 @@ class AuthController(object): token = row[0] else: token = '%stk%s' % (self.reseller_prefix, uuid4().hex) + token_cfaccount = '' + if admin: + token_cfaccount = cfaccount + if reseller_admin: + token_cfaccount = '.reseller_admin' conn.execute(''' INSERT INTO token (token, created, account, user, cfaccount) VALUES (?, ?, ?, ?, ?)''', - (token, time(), account, user, admin and cfaccount or '')) + (token, time(), account, user, token_cfaccount)) conn.commit() return HTTPNoContent(headers={'x-auth-token': token, 'x-storage-token': token, diff --git a/swift/common/constraints.py b/swift/common/constraints.py index cc8cb4868b..9c5e4bd5b2 100644 --- a/swift/common/constraints.py +++ b/swift/common/constraints.py @@ -36,6 +36,8 @@ MAX_OBJECT_NAME_LENGTH = 1024 CONTAINER_LISTING_LIMIT = 10000 #: Max container list length of a get request for an account ACCOUNT_LISTING_LIMIT = 10000 +MAX_ACCOUNT_NAME_LENGTH = 256 +MAX_CONTAINER_NAME_LENGTH = 256 def check_metadata(req, target_type): diff --git a/swift/common/middleware/auth.py b/swift/common/middleware/auth.py index 03770175fd..70162ac856 100644 --- a/swift/common/middleware/auth.py +++ b/swift/common/middleware/auth.py @@ -49,7 +49,7 @@ class DevAuth(object): token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) if token and token.startswith(self.reseller_prefix): memcache_client = cache_from_env(env) - key = 'devauth/%s' % token + key = '%s/token/%s' % (self.reseller_prefix, token) cached_auth_data = memcache_client.get(key) if cached_auth_data: start, expiration, groups = cached_auth_data @@ -85,14 +85,19 @@ class DevAuth(object): version, account, container, obj = split_path(req.path, 1, 4, True) if not account or not account.startswith(self.reseller_prefix): return self.denied_response(req) - if req.remote_user and account in req.remote_user.split(','): + user_groups = (req.remote_user or '').split(',') + if '.reseller_admin' in user_groups: + return None + if account in user_groups and (req.method != 'PUT' or container): + # If the user is admin for the account and is not trying to do an + # account PUT... return None referrers, groups = parse_acl(getattr(req, 'acl', None)) if referrer_allowed(req.referer, referrers): return None if not req.remote_user: return self.denied_response(req) - for user_group in req.remote_user.split(','): + for user_group in user_groups: if user_group in groups: return None return self.denied_response(req) diff --git a/swift/proxy/server.py b/swift/proxy/server.py index 892c4f4769..5f6a92c8fb 100644 --- a/swift/proxy/server.py +++ b/swift/proxy/server.py @@ -35,13 +35,12 @@ from swift.common.ring import Ring from swift.common.utils import get_logger, normalize_timestamp, split_path, \ cache_from_env from swift.common.bufferedhttp import http_connect -from swift.common.constraints import check_object_creation, check_metadata, \ - MAX_FILE_SIZE, check_xml_encodable +from swift.common.constraints import check_metadata, check_object_creation, \ + check_xml_encodable, MAX_ACCOUNT_NAME_LENGTH, MAX_CONTAINER_NAME_LENGTH, \ + MAX_FILE_SIZE from swift.common.exceptions import ChunkReadTimeout, \ ChunkWriteTimeout, ConnectionTimeout -MAX_CONTAINER_NAME_LENGTH = 256 - def update_headers(response, headers): """ @@ -1079,6 +1078,59 @@ class AccountController(Controller): return self.GETorHEAD_base(req, 'Account', partition, nodes, req.path_info.rstrip('/'), self.app.account_ring.replica_count) + @public + def PUT(self, req): + """HTTP PUT request handler.""" + error_response = check_metadata(req, 'account') + if error_response: + return error_response + if len(self.account_name) > MAX_ACCOUNT_NAME_LENGTH: + resp = HTTPBadRequest(request=req) + resp.body = 'Account name length of %d longer than %d' % \ + (len(self.account_name), MAX_ACCOUNT_NAME_LENGTH) + return resp + account_partition, accounts = \ + self.app.account_ring.get_nodes(self.account_name) + headers = {'X-Timestamp': normalize_timestamp(time.time()), + 'x-cf-trans-id': self.trans_id} + headers.update(value for value in req.headers.iteritems() + if value[0].lower().startswith('x-account-meta-')) + statuses = [] + reasons = [] + bodies = [] + for node in self.iter_nodes(account_partition, accounts, + self.app.account_ring): + if self.error_limited(node): + continue + try: + with ConnectionTimeout(self.app.conn_timeout): + conn = http_connect(node['ip'], node['port'], + node['device'], account_partition, 'PUT', + req.path_info, headers) + with Timeout(self.app.node_timeout): + source = conn.getresponse() + body = source.read() + if 200 <= source.status < 300 \ + or 400 <= source.status < 500: + statuses.append(source.status) + reasons.append(source.reason) + bodies.append(body) + else: + if source.status == 507: + self.error_limit(node) + except: + self.exception_occurred(node, 'Account', + 'Trying to PUT to %s' % req.path) + if len(statuses) >= len(accounts): + break + while len(statuses) < len(accounts): + statuses.append(503) + reasons.append('') + bodies.append('') + self.app.memcache.delete('account%s' % req.path_info.rstrip('/')) + return self.best_response(req, statuses, reasons, bodies, + 'Account PUT') + @public def POST(self, req): """HTTP POST request handler.""" diff --git a/test/functional/swift.py b/test/functional/swift.py index 872a09085b..e134de502f 100644 --- a/test/functional/swift.py +++ b/test/functional/swift.py @@ -124,7 +124,7 @@ class Connection(object): if response.status == 401: raise AuthenticationFailed() - if response.status != 204: + if response.status not in (200, 204): raise ResponseError(response) for hdr in response.getheaders(): diff --git a/test/functional/tests.py b/test/functional/tests.py index ae396f3fb0..6a28d9bb3e 100644 --- a/test/functional/tests.py +++ b/test/functional/tests.py @@ -172,7 +172,7 @@ class TestAccount(Base): def testPUT(self): self.env.account.conn.make_request('PUT') - self.assert_status(405) + self.assert_status([403, 405]) def testAccountHead(self): try_count = 0 diff --git a/test/unit/auth/test_server.py b/test/unit/auth/test_server.py index 791fc3853f..23c67578c5 100644 --- a/test/unit/auth/test_server.py +++ b/test/unit/auth/test_server.py @@ -63,12 +63,6 @@ def fake_http_connect(*code_iter, **kwargs): return connect -class FakeRing(object): - def get_nodes(self, path): - return 1, [{'ip': '10.0.0.%s' % x, 'port': 1000+x, 'device': 'sda'} - for x in xrange(3)] - - class TestAuthServer(unittest.TestCase): def setUp(self): @@ -76,8 +70,9 @@ class TestAuthServer(unittest.TestCase): 'auth_server') rmtree(self.testdir, ignore_errors=1) os.mkdir(self.testdir) - self.conf = {'swift_dir': self.testdir, 'log_name': 'auth'} - self.controller = auth_server.AuthController(self.conf, FakeRing()) + self.conf = {'swift_dir': self.testdir, 'log_name': 'auth', + 'super_admin_key': 'testkey'} + self.controller = auth_server.AuthController(self.conf) def tearDown(self): rmtree(self.testdir, ignore_errors=1) @@ -106,7 +101,7 @@ class TestAuthServer(unittest.TestCase): self.assert_(conn is not None) def test_validate_token_non_existant_token(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing',).split('/')[-1] res = self.controller.handle_auth(Request.blank('/v1/test/auth', @@ -117,7 +112,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(self.controller.validate_token(token + 'bad'), False) def test_validate_token_good(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing',).split('/')[-1] res = self.controller.handle_auth(Request.blank('/v1/test/auth', @@ -132,7 +127,7 @@ class TestAuthServer(unittest.TestCase): orig_time = auth_server.time try: auth_server.time = lambda: 1 - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user('test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/v1/test/auth', @@ -148,107 +143,98 @@ class TestAuthServer(unittest.TestCase): auth_server.time = orig_time def test_create_user_no_new_account(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) result = self.controller.create_user('', 'tester', 'testing') self.assertFalse(result) def test_create_user_no_new_user(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) result = self.controller.create_user('test', '', 'testing') self.assertFalse(result) def test_create_user_no_new_password(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) result = self.controller.create_user('test', 'tester', '') self.assertFalse(result) def test_create_user_good(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test', 'tester', 'testing') self.assert_(url) self.assertEquals('/'.join(url.split('/')[:-1]), self.controller.default_cluster_url.rstrip('/'), repr(url)) def test_recreate_accounts_none(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) rv = self.controller.recreate_accounts() self.assertEquals(rv.split()[0], '0', repr(rv)) self.assertEquals(rv.split()[-1], '[]', repr(rv)) def test_recreate_accounts_one(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) self.controller.create_user('test', 'tester', 'testing') - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) rv = self.controller.recreate_accounts() self.assertEquals(rv.split()[0], '1', repr(rv)) self.assertEquals(rv.split()[-1], '[]', repr(rv)) def test_recreate_accounts_several(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) self.controller.create_user('test1', 'tester', 'testing') - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) self.controller.create_user('test2', 'tester', 'testing') - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) self.controller.create_user('test3', 'tester', 'testing') - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) self.controller.create_user('test4', 'tester', 'testing') - auth_server.http_connect = fake_http_connect(201, 201, 201, - 201, 201, 201, - 201, 201, 201, - 201, 201, 201) + auth_server.http_connect = fake_http_connect(201, 201, 201, 201) rv = self.controller.recreate_accounts() self.assertEquals(rv.split()[0], '4', repr(rv)) self.assertEquals(rv.split()[-1], '[]', repr(rv)) def test_recreate_accounts_one_fail(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test', 'tester', 'testing') cfaccount = url.split('/')[-1] - auth_server.http_connect = fake_http_connect(500, 500, 500) + auth_server.http_connect = fake_http_connect(500) rv = self.controller.recreate_accounts() self.assertEquals(rv.split()[0], '1', repr(rv)) self.assertEquals(rv.split()[-1], '[%s]' % repr(cfaccount), repr(rv)) def test_recreate_accounts_several_fail(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test1', 'tester', 'testing') cfaccounts = [url.split('/')[-1]] - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test2', 'tester', 'testing') cfaccounts.append(url.split('/')[-1]) - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test3', 'tester', 'testing') cfaccounts.append(url.split('/')[-1]) - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test4', 'tester', 'testing') cfaccounts.append(url.split('/')[-1]) - auth_server.http_connect = fake_http_connect(500, 500, 500, - 500, 500, 500, - 500, 500, 500, - 500, 500, 500) + auth_server.http_connect = fake_http_connect(500, 500, 500, 500) rv = self.controller.recreate_accounts() self.assertEquals(rv.split()[0], '4', repr(rv)) failed = rv.split('[', 1)[-1][:-1].split(', ') self.assertEquals(set(failed), set(repr(a) for a in cfaccounts)) def test_recreate_accounts_several_fail_some(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test1', 'tester', 'testing') cfaccounts = [url.split('/')[-1]] - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test2', 'tester', 'testing') cfaccounts.append(url.split('/')[-1]) - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test3', 'tester', 'testing') cfaccounts.append(url.split('/')[-1]) - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test4', 'tester', 'testing') cfaccounts.append(url.split('/')[-1]) - auth_server.http_connect = fake_http_connect(500, 500, 500, - 201, 201, 201, - 500, 500, 500, - 201, 201, 201) + auth_server.http_connect = fake_http_connect(500, 201, 500, 201) rv = self.controller.recreate_accounts() self.assertEquals(rv.split()[0], '4', repr(rv)) failed = rv.split('[', 1)[-1][:-1].split(', ') @@ -263,7 +249,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 400) def test_auth_SOSO_missing_headers(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/v1/test/auth', @@ -279,7 +265,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 401) def test_auth_SOSO_bad_account(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/v1/testbad/auth', @@ -294,7 +280,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 401) def test_auth_SOSO_bad_user(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/v1/test/auth', @@ -309,7 +295,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 401) def test_auth_SOSO_bad_password(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/v1/test/auth', @@ -324,7 +310,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 401) def test_auth_SOSO_good(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/v1/test/auth', @@ -336,7 +322,7 @@ class TestAuthServer(unittest.TestCase): self.assert_(ttl > 0, repr(ttl)) def test_auth_SOSO_good_Mosso_headers(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/v1/test/auth', @@ -348,7 +334,7 @@ class TestAuthServer(unittest.TestCase): self.assert_(ttl > 0, repr(ttl)) def test_auth_SOSO_bad_Mosso_headers(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing',).split('/')[-1] res = self.controller.handle_auth(Request.blank('/v1/test/auth', @@ -368,7 +354,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 401) def test_auth_Mosso_missing_headers(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/auth', @@ -384,7 +370,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 401) def test_auth_Mosso_bad_header_format(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/auth', @@ -399,7 +385,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 401) def test_auth_Mosso_bad_account(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/auth', @@ -414,7 +400,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 401) def test_auth_Mosso_bad_user(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/auth', @@ -429,7 +415,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 401) def test_auth_Mosso_bad_password(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/auth', @@ -444,7 +430,7 @@ class TestAuthServer(unittest.TestCase): self.assertEquals(res.status_int, 401) def test_auth_Mosso_good(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/auth', @@ -456,7 +442,7 @@ class TestAuthServer(unittest.TestCase): self.assert_(ttl > 0, repr(ttl)) def test_auth_Mosso_good_SOSO_header_names(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) cfaccount = self.controller.create_user( 'test', 'tester', 'testing').split('/')[-1] res = self.controller.handle_auth(Request.blank('/auth', @@ -473,11 +459,11 @@ class TestAuthServer(unittest.TestCase): logger = get_logger(self.conf, 'auth') logger.logger.addHandler(log_handler) try: - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test', 'tester', 'testing') self.assertEquals(log.getvalue().rsplit(' ', 1)[0], - "auth SUCCESS create_user('test', 'tester', _, False) = %s" - % repr(url)) + "auth SUCCESS create_user('test', 'tester', _, False, False) " + "= %s" % repr(url)) log.truncate(0) def start_response(*args): pass @@ -603,8 +589,8 @@ class TestAuthServer(unittest.TestCase): conn.commit() conn.close() # Upgrade to current db - conf = {'swift_dir': swift_dir} - controller = auth_server.AuthController(conf, FakeRing()) + conf = {'swift_dir': swift_dir, 'super_admin_key': 'testkey'} + controller = auth_server.AuthController(conf) # Check new items exist and are correct conn = get_db_connection(db_file) row = conn.execute('SELECT admin FROM account').fetchone() @@ -615,17 +601,17 @@ class TestAuthServer(unittest.TestCase): rmtree(swift_dir) def test_create_user_twice(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) self.controller.create_user('test', 'tester', 'testing') - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) self.assertEquals( self.controller.create_user('test', 'tester', 'testing'), 'already exists') def test_create_2users_1account(self): - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url = self.controller.create_user('test', 'tester', 'testing') - auth_server.http_connect = fake_http_connect(201, 201, 201) + auth_server.http_connect = fake_http_connect(201) url2 = self.controller.create_user('test', 'tester2', 'testing2') self.assertEquals(url, url2)