def HEAD(self, request): """Handle HTTP HEAD requests for the Swift Object Server.""" device, partition, account, container, obj, policy_idx = \ get_name_and_placement(request, 5, 5, True) try: disk_file = self.get_diskfile( device, partition, account, container, obj, policy_idx=policy_idx) except DiskFileDeviceUnavailable: return HTTPInsufficientStorage(drive=device, request=request) try: metadata = disk_file.read_metadata() except (DiskFileNotExist, DiskFileQuarantined) as e: headers = {} if hasattr(e, 'timestamp'): headers['X-Timestamp'] = e.timestamp return HTTPNotFound(request=request, headers=headers, conditional_response=True) response = Response(request=request, conditional_response=True) response.headers['Content-Type'] = metadata.get( 'Content-Type', 'application/octet-stream') for key, value in metadata.iteritems(): if is_user_meta('object', key) or \ key.lower() in self.allowed_headers: response.headers[key] = value response.etag = metadata['ETag'] ts = metadata['X-Timestamp'] response.last_modified = math.ceil(float(ts)) # Needed for container sync feature response.headers['X-Timestamp'] = ts response.content_length = int(metadata['Content-Length']) try: response.content_encoding = metadata['Content-Encoding'] except KeyError: pass return response
def handle_request(self, req): """ Entry point for auth requests (ones that match the self.auth_prefix). Should return a WSGI-style callable (such as swob.Response). :param req: swob.Request object """ req.start_time = time() handler = None try: version, account, user, _junk = split_path(req.path_info, 1, 4, True) except ValueError: self.logger.increment('errors') return HTTPNotFound(request=req) if version in ('v1', 'v1.0', 'auth'): if req.method == 'GET': handler = self.handle_get_token if not handler: self.logger.increment('errors') req.response = HTTPBadRequest(request=req) else: req.response = handler(req) return req.response
def HEAD(self, req): """Handle HTTP HEAD request.""" drive, part, account, container, obj = split_and_validate_path( req, 4, 5, True) out_content_type = get_listing_content_type(req) if self.mount_check and not check_mount(self.root, drive): return HTTPInsufficientStorage(drive=drive, request=req) broker = self._get_container_broker(drive, part, account, container, pending_timeout=0.1, stale_reads_ok=True) info, is_deleted = broker.get_info_is_deleted() headers = gen_resp_headers(info, is_deleted=is_deleted) if is_deleted: return HTTPNotFound(request=req, headers=headers) headers.update( (key, value) for key, (value, timestamp) in broker.metadata.items() if value != '' and (key.lower() in self.save_headers or is_sys_or_user_meta('container', key))) headers['Content-Type'] = out_content_type return HTTPNoContent(request=req, headers=headers, charset='utf-8')
def authorize(self, env_identity, req): tenant_id, tenant_name = env_identity['tenant'] user_id, user_name = env_identity['user'] referrers, roles = swift_acl.parse_acl(getattr(req, 'acl', None)) #allow OPTIONS requests to proceed as normal if req.method == 'OPTIONS': return try: part = req.split_path(1, 4, True) version, account, container, obj = part except ValueError: return HTTPNotFound(request=req) self._set_project_domain_id(req, part, env_identity) user_roles = [r.lower() for r in env_identity.get('roles', [])] # Give unconditional access to a user with the reseller_admin # role. if self.reseller_admin_role in user_roles: msg = 'User %s has reseller admin authorizing' self.logger.debug(msg, tenant_id) req.environ['swift_owner'] = True return # If we are not reseller admin and user is trying to delete its own # account then deny it. if not container and not obj and req.method == 'DELETE': # User is not allowed to issue a DELETE on its own account msg = 'User %s:%s is not allowed to delete its own account' self.logger.debug(msg, tenant_name, user_name) return self.denied_response(req) # cross-tenant authorization matched_acl = None if roles: allow_names = self._is_name_allowed_in_acl(req, part, env_identity) matched_acl = self._authorize_cross_tenant(user_id, user_name, tenant_id, tenant_name, roles, allow_names) if matched_acl is not None: log_msg = 'user %s allowed in ACL authorizing.' self.logger.debug(log_msg, matched_acl) return acl_authorized = self._authorize_unconfirmed_identity( req, obj, referrers, roles) if acl_authorized: return # Check if a user tries to access an account that does not match their # token if not self._reseller_check(account, tenant_id): log_msg = 'tenant mismatch: %s != %s' self.logger.debug(log_msg, account, tenant_id) return self.denied_response(req) # Check the roles the user is belonging to. If the user is # part of the role defined in the config variable # operator_roles (like admin) then it will be # promoted as an admin of the account/tenant. for role in self.operator_roles.split(','): role = role.strip() if role in user_roles: log_msg = 'allow user with role %s as account admin' self.logger.debug(log_msg, role) req.environ['swift_owner'] = True return # If user is of the same name of the tenant then make owner of it. if self.is_admin and user_name == tenant_name: self.logger.warning("the is_admin feature has been deprecated " "and will be removed in the future " "update your config file") req.environ['swift_owner'] = True return if acl_authorized is not None: return self.denied_response(req) # Check if we have the role in the userroles and allow it for user_role in user_roles: if user_role in (r.lower() for r in roles): log_msg = 'user %s:%s allowed in ACL: %s authorizing' self.logger.debug(log_msg, tenant_name, user_name, user_role) return return self.denied_response(req)
def handle_delete(self, req, objs_to_delete=None, user_agent='BulkDelete', swift_source='BD'): """ :params req: a swob Request :raises HTTPException: on unhandled errors :returns: a swob Response """ try: vrs, account, _junk = req.split_path(2, 3, True) except ValueError: return HTTPNotFound(request=req) incoming_format = req.headers.get('Content-Type') if incoming_format and not incoming_format.startswith('text/plain'): # For now only accept newline separated object names return HTTPNotAcceptable(request=req) out_content_type = req.accept.best_match(ACCEPTABLE_FORMATS) if not out_content_type: return HTTPNotAcceptable(request=req) if objs_to_delete is None: objs_to_delete = self.get_objs_to_delete(req) failed_files = [] success_count = not_found_count = 0 failed_file_response_type = HTTPBadRequest for obj_to_delete in objs_to_delete: obj_to_delete = obj_to_delete.strip().lstrip('/') if not obj_to_delete: continue delete_path = '/'.join(['', vrs, account, obj_to_delete]) if not check_utf8(delete_path): failed_files.append( [quote(delete_path), HTTPPreconditionFailed().status]) continue new_env = req.environ.copy() new_env['PATH_INFO'] = delete_path del (new_env['wsgi.input']) new_env['CONTENT_LENGTH'] = 0 new_env['HTTP_USER_AGENT'] = \ '%s %s' % (req.environ.get('HTTP_USER_AGENT'), user_agent) new_env['swift.source'] = swift_source delete_obj_req = Request.blank(delete_path, new_env) resp = delete_obj_req.get_response(self.app) if resp.status_int // 100 == 2: success_count += 1 elif resp.status_int == HTTP_NOT_FOUND: not_found_count += 1 elif resp.status_int == HTTP_UNAUTHORIZED: return HTTPUnauthorized(request=req) else: if resp.status_int // 100 == 5: failed_file_response_type = HTTPBadGateway failed_files.append([quote(delete_path), resp.status]) resp_body = get_response_body(out_content_type, { 'Number Deleted': success_count, 'Number Not Found': not_found_count }, failed_files) if (success_count or not_found_count) and not failed_files: return HTTPOk(resp_body, content_type=out_content_type) if failed_files: return failed_file_response_type(resp_body, content_type=out_content_type) return HTTPBadRequest('Invalid bulk delete.')
def _link_object(self, req): _, container, obj = req.headers['Oio-Copy-From'].split('/', 2) from_account = req.headers.get('X-Copy-From-Account', self.account_name) self.app.logger.info( "Creating link from %s/%s/%s to %s/%s/%s", # Existing from_account, container, obj, # New self.account_name, self.container_name, self.object_name) storage = self.app.storage if req.headers.get('Range'): raise Exception("Fast Copy with Range is unsupported") ranges = ranges_from_http_header(req.headers.get('Range')) if len(ranges) != 1: raise HTTPInternalServerError( request=req, body="mutiple ranges unsupported") ranges = ranges[0] else: ranges = None headers = self._prepare_headers(req) metadata = self.load_object_metadata(headers) oio_headers = {REQID_HEADER: self.trans_id} oio_cache = req.environ.get('oio.cache') perfdata = req.environ.get('swift.perfdata') # FIXME(FVE): use object_show, cache in req.environ version = obj_version_from_env(req.environ) props = storage.object_get_properties(from_account, container, obj, headers=oio_headers, version=version, cache=oio_cache, perfdata=perfdata) if props['properties'].get(SLO, None): raise Exception("Fast Copy with SLO is unsupported") else: if ranges: raise HTTPInternalServerError( request=req, body="no range supported with single object") try: # TODO check return code (values ?) link_meta = storage.object_link(from_account, container, obj, self.account_name, self.container_name, self.object_name, headers=oio_headers, properties=metadata, properties_directive='REPLACE', target_version=version, cache=oio_cache, perfdata=perfdata) # TODO(FVE): this exception catching block has to be refactored # TODO check which ones are ok or make non sense except exceptions.Conflict: raise HTTPConflict(request=req) except exceptions.PreconditionFailed: raise HTTPPreconditionFailed(request=req) except exceptions.SourceReadError: req.client_disconnect = True self.app.logger.warning( _('Client disconnected without sending last chunk')) self.app.logger.increment('client_disconnects') raise HTTPClientDisconnect(request=req) except exceptions.EtagMismatch: return HTTPUnprocessableEntity(request=req) except (exceptions.ServiceBusy, exceptions.OioTimeout, exceptions.DeadlineReached): raise except (exceptions.NoSuchContainer, exceptions.NotFound): raise HTTPNotFound(request=req) except exceptions.ClientException as err: # 481 = CODE_POLICY_NOT_SATISFIABLE if err.status == 481: raise exceptions.ServiceBusy() self.app.logger.exception( _('ERROR Exception transferring data %s'), {'path': req.path}) raise HTTPInternalServerError(request=req) except Exception: self.app.logger.exception( _('ERROR Exception transferring data %s'), {'path': req.path}) raise HTTPInternalServerError(request=req) resp = HTTPCreated(request=req, etag=link_meta['hash']) return resp
def handle_request(self, req): """ Entry point for proxy server. Should return a WSGI-style callable (such as swob.Response). :param req: swob.Request object """ try: self.logger.set_statsd_prefix('proxy-server') if req.content_length and req.content_length < 0: self.logger.increment('errors') return HTTPBadRequest(request=req, body='Invalid Content-Length') try: if not check_utf8(req.path_info): self.logger.increment('errors') return HTTPPreconditionFailed( request=req, body='Invalid UTF8 or contains NULL') except UnicodeError: self.logger.increment('errors') return HTTPPreconditionFailed( request=req, body='Invalid UTF8 or contains NULL') try: controller, path_parts = self.get_controller(req) p = req.path_info if isinstance(p, six.text_type): p = p.encode('utf-8') except APIVersionError: self.logger.increment('errors') return HTTPBadRequest(request=req) except ValueError: self.logger.increment('errors') return HTTPNotFound(request=req) if not controller: self.logger.increment('errors') return HTTPPreconditionFailed(request=req, body='Bad URL') if self.deny_host_headers and \ req.host.split(':')[0] in self.deny_host_headers: return HTTPForbidden(request=req, body='Invalid host header') self.logger.set_statsd_prefix('proxy-server.' + controller.server_type.lower()) controller = controller(self, **path_parts) if 'swift.trans_id' not in req.environ: # if this wasn't set by an earlier middleware, set it now trans_id_suffix = self.trans_id_suffix trans_id_extra = req.headers.get('x-trans-id-extra') if trans_id_extra: trans_id_suffix += '-' + trans_id_extra[:32] trans_id = generate_trans_id(trans_id_suffix) req.environ['swift.trans_id'] = trans_id self.logger.txn_id = trans_id req.headers['x-trans-id'] = req.environ['swift.trans_id'] controller.trans_id = req.environ['swift.trans_id'] self.logger.client_ip = get_remote_client(req) try: handler = getattr(controller, req.method) getattr(handler, 'publicly_accessible') except AttributeError: allowed_methods = getattr(controller, 'allowed_methods', set()) return HTTPMethodNotAllowed( request=req, headers={'Allow': ', '.join(allowed_methods)}) old_authorize = None if 'swift.authorize' in req.environ: # We call authorize before the handler, always. If authorized, # we remove the swift.authorize hook so isn't ever called # again. If not authorized, we return the denial unless the # controller's method indicates it'd like to gather more # information and try again later. resp = req.environ['swift.authorize'](req) if not resp and not req.headers.get('X-Copy-From-Account') \ and not req.headers.get('Destination-Account'): # No resp means authorized, no delayed recheck required. old_authorize = req.environ['swift.authorize'] else: # Response indicates denial, but we might delay the denial # and recheck later. If not delayed, return the error now. if not getattr(handler, 'delay_denial', None): return resp # Save off original request method (GET, POST, etc.) in case it # gets mutated during handling. This way logging can display the # method the client actually sent. req.environ['swift.orig_req_method'] = req.method try: if old_authorize: req.environ.pop('swift.authorize', None) return handler(req) finally: if old_authorize: req.environ['swift.authorize'] = old_authorize except HTTPException as error_response: return error_response except (Exception, Timeout): self.logger.exception(_('ERROR Unhandled exception in request')) return HTTPServerError(request=req)
def authorize(self, req): """ Returns None if the request is authorized to continue or a standard WSGI response callable if not. """ try: _junk, account, container, obj = req.split_path(1, 4, True) except ValueError: self.logger.increment('errors') return HTTPNotFound(request=req) if self._get_account_prefix(account) is None: self.logger.debug("Account name: %s doesn't start with " "reseller_prefix(s): %s." % (account, ','.join(self.reseller_prefixes))) return self.denied_response(req) # At this point, TempAuth is convinced that it is authoritative. # If you are sending an ACL header, it must be syntactically valid # according to TempAuth's rules for ACL syntax. acl_data = req.headers.get('x-account-access-control') if acl_data is not None: error = self.extract_acl_and_report_errors(req) if error: msg = 'X-Account-Access-Control invalid: %s\n\nInput: %s\n' % ( error, acl_data) headers = [('Content-Type', 'text/plain; charset=UTF-8')] return HTTPBadRequest(request=req, headers=headers, body=msg) user_groups = (req.remote_user or '').split(',') account_user = user_groups[1] if len(user_groups) > 1 else None if '.reseller_admin' in user_groups and \ account not in self.reseller_prefixes and \ not self._dot_account(account): req.environ['swift_owner'] = True self.logger.debug("User %s has reseller admin authorizing." % account_user) return None if account in user_groups and \ (req.method not in ('DELETE', 'PUT') or container): # The user is admin for the account and is not trying to do an # account DELETE or PUT account_prefix = self._get_account_prefix(account) require_group = self.account_rules.get(account_prefix).get( 'require_group') if require_group and require_group in user_groups: req.environ['swift_owner'] = True self.logger.debug("User %s has admin and %s group." " Authorizing." % (account_user, require_group)) return None elif not require_group: req.environ['swift_owner'] = True self.logger.debug("User %s has admin authorizing." % account_user) return None if (req.environ.get('swift_sync_key') and (req.environ['swift_sync_key'] == req.headers.get('x-container-sync-key', None)) and 'x-timestamp' in req.headers): self.logger.debug("Allow request with container sync-key: %s." % req.environ['swift_sync_key']) return None if req.method == 'OPTIONS': # allow OPTIONS requests to proceed as normal self.logger.debug("Allow OPTIONS request.") return None referrers, groups = parse_acl(getattr(req, 'acl', None)) if referrer_allowed(req.referer, referrers): if obj or '.rlistings' in groups: self.logger.debug("Allow authorizing %s via referer ACL." % req.referer) return None for user_group in user_groups: if user_group in groups: self.logger.debug("User %s allowed in ACL: %s authorizing." % (account_user, user_group)) return None # Check for access via X-Account-Access-Control acct_acls = self.account_acls(req) if acct_acls: # At least one account ACL is set in this account's sysmeta data, # so we should see whether this user is authorized by the ACLs. user_group_set = set(user_groups) if user_group_set.intersection(acct_acls['admin']): req.environ['swift_owner'] = True self.logger.debug('User %s allowed by X-Account-Access-Control' ' (admin)' % account_user) return None if (user_group_set.intersection(acct_acls['read-write']) and (container or req.method in ('GET', 'HEAD'))): # The RW ACL allows all operations to containers/objects, but # only GET/HEAD to accounts (and OPTIONS, above) self.logger.debug('User %s allowed by X-Account-Access-Control' ' (read-write)' % account_user) return None if (user_group_set.intersection(acct_acls['read-only']) and req.method in ('GET', 'HEAD')): self.logger.debug('User %s allowed by X-Account-Access-Control' ' (read-only)' % account_user) return None return self.denied_response(req)
def GET(self, req): """ Handle HTTP GET request. The body of the response to a successful GET request contains a listing of either objects or shard ranges. The exact content of the listing is determined by a combination of request headers and query string parameters, as follows: * The type of the listing is determined by the ``X-Backend-Record-Type`` header. If this header has value ``shard`` then the response body will be a list of shard ranges; if this header has value ``auto``, and the container state is ``sharding`` or ``sharded``, then the listing will be a list of shard ranges; otherwise the response body will be a list of objects. * Both shard range and object listings may be constrained to a name range by the ``marker`` and ``end_marker`` query string parameters. Object listings will only contain objects whose names are greater than any ``marker`` value and less than any ``end_marker`` value. Shard range listings will only contain shard ranges whose namespace is greater than or includes any ``marker`` value and is less than or includes any ``end_marker`` value. * Shard range listings may also be constrained by an ``includes`` query string parameter. If this parameter is present the listing will only contain shard ranges whose namespace includes the value of the parameter; any ``marker`` or ``end_marker`` parameters are ignored * The length of an object listing may be constrained by the ``limit`` parameter. Object listings may also be constrained by ``prefix``, ``delimiter`` and ``path`` query string parameters. * Shard range listings will include deleted shard ranges if and only if the ``X-Backend-Include-Deleted`` header value is one of :attr:`swift.common.utils.TRUE_VALUES`. Object listings never include deleted objects. * Shard range listings may be constrained to include only shard ranges whose state is specified by a query string ``states`` parameter. If present, the ``states`` parameter should be a comma separated list of either the string or integer representation of :data:`~swift.common.utils.ShardRange.STATES`. Two alias values may be used in a ``states`` parameter value: ``listing`` will cause the listing to include all shard ranges in a state suitable for contributing to an object listing; ``updating`` will cause the listing to include all shard ranges in a state suitable to accept an object update. If either of these aliases is used then the shard range listing will if necessary be extended with a synthesised 'filler' range in order to satisfy the requested name range when insufficient actual shard ranges are found. Any 'filler' shard range will cover the otherwise uncovered tail of the requested name range and will point back to the same container. * Listings are not normally returned from a deleted container. However, the ``X-Backend-Override-Deleted`` header may be used with a value in :attr:`swift.common.utils.TRUE_VALUES` to force a shard range listing to be returned from a deleted container whose DB file still exists. :param req: an instance of :class:`swift.common.swob.Request` :returns: an instance of :class:`swift.common.swob.Response` """ drive, part, account, container, obj = get_obj_name_and_placement(req) path = get_param(req, 'path') prefix = get_param(req, 'prefix') delimiter = get_param(req, 'delimiter') marker = get_param(req, 'marker', '') end_marker = get_param(req, 'end_marker') limit = constraints.CONTAINER_LISTING_LIMIT given_limit = get_param(req, 'limit') reverse = config_true_value(get_param(req, 'reverse')) if given_limit and given_limit.isdigit(): limit = int(given_limit) if limit > constraints.CONTAINER_LISTING_LIMIT: return HTTPPreconditionFailed( request=req, body='Maximum limit is %d' % constraints.CONTAINER_LISTING_LIMIT) out_content_type = listing_formats.get_listing_content_type(req) try: check_drive(self.root, drive, self.mount_check) except ValueError: return HTTPInsufficientStorage(drive=drive, request=req) broker = self._get_container_broker(drive, part, account, container, pending_timeout=0.1, stale_reads_ok=True) info, is_deleted = broker.get_info_is_deleted() record_type = req.headers.get('x-backend-record-type', '').lower() if record_type == 'auto' and info.get('db_state') in (SHARDING, SHARDED): record_type = 'shard' if record_type == 'shard': override_deleted = info and config_true_value( req.headers.get('x-backend-override-deleted', False)) resp_headers = gen_resp_headers(info, is_deleted=is_deleted and not override_deleted) if is_deleted and not override_deleted: return HTTPNotFound(request=req, headers=resp_headers) resp_headers['X-Backend-Record-Type'] = 'shard' includes = get_param(req, 'includes') states = get_param(req, 'states') fill_gaps = False if states: states = list_from_csv(states) fill_gaps = any(('listing' in states, 'updating' in states)) try: states = broker.resolve_shard_range_states(states) except ValueError: return HTTPBadRequest(request=req, body='Bad state') include_deleted = config_true_value( req.headers.get('x-backend-include-deleted', False)) container_list = broker.get_shard_ranges( marker, end_marker, includes, reverse, states=states, include_deleted=include_deleted, fill_gaps=fill_gaps) else: resp_headers = gen_resp_headers(info, is_deleted=is_deleted) if is_deleted: return HTTPNotFound(request=req, headers=resp_headers) resp_headers['X-Backend-Record-Type'] = 'object' # Use the retired db while container is in process of sharding, # otherwise use current db src_broker = broker.get_brokers()[0] container_list = src_broker.list_objects_iter( limit, marker, end_marker, prefix, delimiter, path, storage_policy_index=info['storage_policy_index'], reverse=reverse, allow_reserved=req.allow_reserved_names) return self.create_listing(req, out_content_type, info, resp_headers, broker.metadata, container_list, container)
def authorize(self, req): """ Returns None if the request is authorized to continue or a standard WSGI response callable if not. Assumes that user groups are all lower case, which is true when Red Hat Enterprise Linux Identity Management is used. """ try: version, account, container, obj = req.split_path(1, 4, True) except ValueError: self.logger.increment('errors') return HTTPNotFound(request=req) if not account or not account.startswith(self.reseller_prefix): self.logger.debug("Account name: %s doesn't start with " "reseller_prefix: %s." % (account, self.reseller_prefix)) return self.denied_response(req) user_groups = (req.remote_user or '').split(',') account_user = user_groups[1] if len(user_groups) > 1 else None # If the user is in the reseller_admin group for our prefix, he gets # full access to all accounts we manage. For the default reseller # prefix, the group name is auth_reseller_admin. admin_group = ("%sreseller_admin" % self.reseller_prefix).lower() if admin_group in user_groups and \ account != self.reseller_prefix and \ account[len(self.reseller_prefix)] != '.': req.environ['swift_owner'] = True return None # The "account" is part of the request URL, and already contains the # reseller prefix, like in "/v1/AUTH_vol1/pictures/pic1.png". if account.lower() in user_groups and \ (req.method not in ('DELETE', 'PUT') or container): # If the user is admin for the account and is not trying to do an # account DELETE or PUT... req.environ['swift_owner'] = True self.logger.debug("User %s has admin authorizing." % account_user) return None if (req.environ.get('swift_sync_key') and (req.environ['swift_sync_key'] == req.headers.get('x-container-sync-key', None)) and 'x-timestamp' in req.headers): self.logger.debug("Allow request with container sync-key: %s." % req.environ['swift_sync_key']) return None if req.method == 'OPTIONS': #allow OPTIONS requests to proceed as normal self.logger.debug("Allow OPTIONS request.") return None referrers, groups = parse_acl(getattr(req, 'acl', None)) if referrer_allowed(req.referer, referrers): if obj or '.rlistings' in groups: self.logger.debug("Allow authorizing %s via referer ACL." % req.referer) return None for user_group in user_groups: if user_group in groups: self.logger.debug("User %s allowed in ACL: %s authorizing." % (account_user, user_group)) return None return self.denied_response(req)
def handle_object(self, env, start_response): """ Handles a possible static web request for an object. This object could resolve into an index or listing request. :param env: The original WSGI environment dict. :param start_response: The original WSGI start_response hook. """ tmp_env = dict(env) tmp_env['HTTP_USER_AGENT'] = \ '%s StaticWeb' % env.get('HTTP_USER_AGENT') tmp_env['swift.source'] = 'SW' resp = self._app_call(tmp_env) status_int = self._get_status_int() if is_success(status_int) or is_redirection(status_int): start_response(self._response_status, self._response_headers, self._response_exc_info) return resp if status_int != HTTP_NOT_FOUND: return self._error_response(resp, env, start_response) self._get_container_info(env) if not self._listings and not self._index: return self.app(env, start_response) status_int = HTTP_NOT_FOUND if self._index: tmp_env = dict(env) tmp_env['HTTP_USER_AGENT'] = \ '%s StaticWeb' % env.get('HTTP_USER_AGENT') tmp_env['swift.source'] = 'SW' if tmp_env['PATH_INFO'][-1] != '/': tmp_env['PATH_INFO'] += '/' tmp_env['PATH_INFO'] += self._index resp = self._app_call(tmp_env) status_int = self._get_status_int() if is_success(status_int) or is_redirection(status_int): if env['PATH_INFO'][-1] != '/': resp = HTTPMovedPermanently(location=env['PATH_INFO'] + '/') return resp(env, start_response) start_response(self._response_status, self._response_headers, self._response_exc_info) return resp if status_int == HTTP_NOT_FOUND: if env['PATH_INFO'][-1] != '/': tmp_env = make_pre_authed_env( env, 'GET', '/%s/%s/%s' % (self.version, self.account, self.container), self.agent, swift_source='SW') tmp_env['QUERY_STRING'] = 'limit=1&format=json&delimiter' \ '=/&limit=1&prefix=%s' % quote(self.obj + '/') resp = self._app_call(tmp_env) body = ''.join(resp) if not is_success(self._get_status_int()) or not body or \ not json.loads(body): resp = HTTPNotFound()(env, self._start_response) return self._error_response(resp, env, start_response) resp = HTTPMovedPermanently(location=env['PATH_INFO'] + '/') return resp(env, start_response) return self._listing(env, start_response, self.obj)
def DELETE(self, req): """HTTP DELETE request handler.""" container_info = self.container_info( self.account_name, self.container_name, req) # pass the policy index to storage nodes via req header policy_index = req.headers.get('X-Backend-Storage-Policy-Index', container_info['storage_policy']) obj_ring = self.app.get_object_ring(policy_index) # pass the policy index to storage nodes via req header req.headers['X-Backend-Storage-Policy-Index'] = policy_index container_partition = container_info['partition'] containers = container_info['nodes'] req.acl = container_info['write_acl'] req.environ['swift_sync_key'] = container_info['sync_key'] object_versions = container_info['versions'] if object_versions: # this is a version manifest and needs to be handled differently object_versions = unquote(object_versions) lcontainer = object_versions.split('/')[0] prefix_len = '%03x' % len(self.object_name) lprefix = prefix_len + self.object_name + '/' last_item = None try: for last_item in self._listing_iter(lcontainer, lprefix, req.environ): pass except ListingIterNotFound: # no worries, last_item is None pass except ListingIterNotAuthorized as err: return err.aresp except ListingIterError: return HTTPServerError(request=req) if last_item: # there are older versions so copy the previous version to the # current object and delete the previous version orig_container = self.container_name orig_obj = self.object_name self.container_name = lcontainer self.object_name = last_item['name'].encode('utf-8') copy_path = '/v1/' + self.account_name + '/' + \ self.container_name + '/' + self.object_name copy_headers = {'X-Newest': 'True', 'Destination': orig_container + '/' + orig_obj } copy_environ = {'REQUEST_METHOD': 'COPY', 'swift_versioned_copy': True } creq = Request.blank(copy_path, headers=copy_headers, environ=copy_environ) copy_resp = self.COPY(creq) if is_client_error(copy_resp.status_int): # some user error, maybe permissions return HTTPPreconditionFailed(request=req) elif not is_success(copy_resp.status_int): # could not copy the data, bail return HTTPServiceUnavailable(request=req) # reset these because the COPY changed them self.container_name = lcontainer self.object_name = last_item['name'].encode('utf-8') new_del_req = Request.blank(copy_path, environ=req.environ) container_info = self.container_info( self.account_name, self.container_name, req) policy_idx = container_info['storage_policy'] obj_ring = self.app.get_object_ring(policy_idx) # pass the policy index to storage nodes via req header new_del_req.headers['X-Backend-Storage-Policy-Index'] = \ policy_idx container_partition = container_info['partition'] containers = container_info['nodes'] new_del_req.acl = container_info['write_acl'] new_del_req.path_info = copy_path req = new_del_req # remove 'X-If-Delete-At', since it is not for the older copy if 'X-If-Delete-At' in req.headers: del req.headers['X-If-Delete-At'] if 'swift.authorize' in req.environ: aresp = req.environ['swift.authorize'](req) if aresp: return aresp if not containers: return HTTPNotFound(request=req) partition, nodes = obj_ring.get_nodes( self.account_name, self.container_name, self.object_name) # Used by container sync feature if 'x-timestamp' in req.headers: try: req_timestamp = Timestamp(req.headers['X-Timestamp']) except ValueError: return HTTPBadRequest( request=req, content_type='text/plain', body='X-Timestamp should be a UNIX timestamp float value; ' 'was %r' % req.headers['x-timestamp']) req.headers['X-Timestamp'] = req_timestamp.internal else: req.headers['X-Timestamp'] = Timestamp(time.time()).internal headers = self._backend_requests( req, len(nodes), container_partition, containers) resp = self.make_requests(req, obj_ring, partition, 'DELETE', req.swift_entity_path, headers) return resp
def PUT(self, req): """HTTP PUT request handler.""" if req.if_none_match is not None and '*' not in req.if_none_match: # Sending an etag with if-none-match isn't currently supported return HTTPBadRequest(request=req, content_type='text/plain', body='If-None-Match only supports *') container_info = self.container_info( self.account_name, self.container_name, req) policy_index = req.headers.get('X-Backend-Storage-Policy-Index', container_info['storage_policy']) obj_ring = self.app.get_object_ring(policy_index) # pass the policy index to storage nodes via req header req.headers['X-Backend-Storage-Policy-Index'] = policy_index container_partition = container_info['partition'] containers = container_info['nodes'] req.acl = container_info['write_acl'] req.environ['swift_sync_key'] = container_info['sync_key'] object_versions = container_info['versions'] if 'swift.authorize' in req.environ: aresp = req.environ['swift.authorize'](req) if aresp: return aresp if not containers: return HTTPNotFound(request=req) try: ml = req.message_length() except ValueError as e: return HTTPBadRequest(request=req, content_type='text/plain', body=str(e)) except AttributeError as e: return HTTPNotImplemented(request=req, content_type='text/plain', body=str(e)) if ml is not None and ml > constraints.MAX_FILE_SIZE: return HTTPRequestEntityTooLarge(request=req) if 'x-delete-after' in req.headers: try: x_delete_after = int(req.headers['x-delete-after']) except ValueError: return HTTPBadRequest(request=req, content_type='text/plain', body='Non-integer X-Delete-After') req.headers['x-delete-at'] = normalize_delete_at_timestamp( time.time() + x_delete_after) partition, nodes = obj_ring.get_nodes( self.account_name, self.container_name, self.object_name) # do a HEAD request for container sync and checking object versions if 'x-timestamp' in req.headers or \ (object_versions and not req.environ.get('swift_versioned_copy')): # make sure proxy-server uses the right policy index _headers = {'X-Backend-Storage-Policy-Index': policy_index, 'X-Newest': 'True'} hreq = Request.blank(req.path_info, headers=_headers, environ={'REQUEST_METHOD': 'HEAD'}) hresp = self.GETorHEAD_base( hreq, _('Object'), obj_ring, partition, hreq.swift_entity_path) # Used by container sync feature if 'x-timestamp' in req.headers: try: req_timestamp = Timestamp(req.headers['X-Timestamp']) if hresp.environ and 'swift_x_timestamp' in hresp.environ and \ hresp.environ['swift_x_timestamp'] >= req_timestamp: return HTTPAccepted(request=req) except ValueError: return HTTPBadRequest( request=req, content_type='text/plain', body='X-Timestamp should be a UNIX timestamp float value; ' 'was %r' % req.headers['x-timestamp']) req.headers['X-Timestamp'] = req_timestamp.internal else: req.headers['X-Timestamp'] = Timestamp(time.time()).internal # Sometimes the 'content-type' header exists, but is set to None. content_type_manually_set = True detect_content_type = \ config_true_value(req.headers.get('x-detect-content-type')) if detect_content_type or not req.headers.get('content-type'): guessed_type, _junk = mimetypes.guess_type(req.path_info) req.headers['Content-Type'] = guessed_type or \ 'application/octet-stream' if detect_content_type: req.headers.pop('x-detect-content-type') else: content_type_manually_set = False error_response = check_object_creation(req, self.object_name) or \ check_content_type(req) if error_response: return error_response if object_versions and not req.environ.get('swift_versioned_copy'): if hresp.status_int != HTTP_NOT_FOUND: # This is a version manifest and needs to be handled # differently. First copy the existing data to a new object, # then write the data from this request to the version manifest # object. lcontainer = object_versions.split('/')[0] prefix_len = '%03x' % len(self.object_name) lprefix = prefix_len + self.object_name + '/' ts_source = hresp.environ.get('swift_x_timestamp') if ts_source is None: ts_source = time.mktime(time.strptime( hresp.headers['last-modified'], '%a, %d %b %Y %H:%M:%S GMT')) new_ts = Timestamp(ts_source).internal vers_obj_name = lprefix + new_ts copy_headers = { 'Destination': '%s/%s' % (lcontainer, vers_obj_name)} copy_environ = {'REQUEST_METHOD': 'COPY', 'swift_versioned_copy': True } copy_req = Request.blank(req.path_info, headers=copy_headers, environ=copy_environ) copy_resp = self.COPY(copy_req) if is_client_error(copy_resp.status_int): # missing container or bad permissions return HTTPPreconditionFailed(request=req) elif not is_success(copy_resp.status_int): # could not copy the data, bail return HTTPServiceUnavailable(request=req) reader = req.environ['wsgi.input'].read data_source = iter(lambda: reader(self.app.client_chunk_size), '') source_header = req.headers.get('X-Copy-From') source_resp = None if source_header: if req.environ.get('swift.orig_req_method', req.method) != 'POST': req.environ.setdefault('swift.log_info', []).append( 'x-copy-from:%s' % source_header) src_container_name, src_obj_name = check_copy_from_header(req) ver, acct, _rest = req.split_path(2, 3, True) if isinstance(acct, unicode): acct = acct.encode('utf-8') source_header = '/%s/%s/%s/%s' % (ver, acct, src_container_name, src_obj_name) source_req = req.copy_get() # make sure the source request uses it's container_info source_req.headers.pop('X-Backend-Storage-Policy-Index', None) source_req.path_info = source_header source_req.headers['X-Newest'] = 'true' orig_obj_name = self.object_name orig_container_name = self.container_name self.object_name = src_obj_name self.container_name = src_container_name sink_req = Request.blank(req.path_info, environ=req.environ, headers=req.headers) source_resp = self.GET(source_req) # This gives middlewares a way to change the source; for example, # this lets you COPY a SLO manifest and have the new object be the # concatenation of the segments (like what a GET request gives # the client), not a copy of the manifest file. hook = req.environ.get( 'swift.copy_hook', (lambda source_req, source_resp, sink_req: source_resp)) source_resp = hook(source_req, source_resp, sink_req) if source_resp.status_int >= HTTP_MULTIPLE_CHOICES: return source_resp self.object_name = orig_obj_name self.container_name = orig_container_name data_source = iter(source_resp.app_iter) sink_req.content_length = source_resp.content_length if sink_req.content_length is None: # This indicates a transfer-encoding: chunked source object, # which currently only happens because there are more than # CONTAINER_LISTING_LIMIT segments in a segmented object. In # this case, we're going to refuse to do the server-side copy. return HTTPRequestEntityTooLarge(request=req) if sink_req.content_length > constraints.MAX_FILE_SIZE: return HTTPRequestEntityTooLarge(request=req) sink_req.etag = source_resp.etag # we no longer need the X-Copy-From header del sink_req.headers['X-Copy-From'] if not content_type_manually_set: sink_req.headers['Content-Type'] = \ source_resp.headers['Content-Type'] if not config_true_value( sink_req.headers.get('x-fresh-metadata', 'false')): copy_headers_into(source_resp, sink_req) copy_headers_into(req, sink_req) # copy over x-static-large-object for POSTs and manifest copies if 'X-Static-Large-Object' in source_resp.headers and \ req.params.get('multipart-manifest') == 'get': sink_req.headers['X-Static-Large-Object'] = \ source_resp.headers['X-Static-Large-Object'] req = sink_req if 'x-delete-at' in req.headers: try: x_delete_at = normalize_delete_at_timestamp( int(req.headers['x-delete-at'])) if int(x_delete_at) < time.time(): return HTTPBadRequest( body='X-Delete-At in past', request=req, content_type='text/plain') except ValueError: return HTTPBadRequest(request=req, content_type='text/plain', body='Non-integer X-Delete-At') req.environ.setdefault('swift.log_info', []).append( 'x-delete-at:%s' % x_delete_at) delete_at_container = normalize_delete_at_timestamp( int(x_delete_at) / self.app.expiring_objects_container_divisor * self.app.expiring_objects_container_divisor) delete_at_part, delete_at_nodes = \ self.app.container_ring.get_nodes( self.app.expiring_objects_account, delete_at_container) else: delete_at_container = delete_at_part = delete_at_nodes = None node_iter = GreenthreadSafeIterator( self.iter_nodes_local_first(obj_ring, partition)) pile = GreenPile(len(nodes)) te = req.headers.get('transfer-encoding', '') chunked = ('chunked' in te) outgoing_headers = self._backend_requests( req, len(nodes), container_partition, containers, delete_at_container, delete_at_part, delete_at_nodes) for nheaders in outgoing_headers: # RFC2616:8.2.3 disallows 100-continue without a body if (req.content_length > 0) or chunked: nheaders['Expect'] = '100-continue' pile.spawn(self._connect_put_node, node_iter, partition, req.swift_entity_path, nheaders, self.app.logger.thread_locals) conns = [conn for conn in pile if conn] min_conns = quorum_size(len(nodes)) if req.if_none_match is not None and '*' in req.if_none_match: statuses = [conn.resp.status for conn in conns if conn.resp] if HTTP_PRECONDITION_FAILED in statuses: # If we find any copy of the file, it shouldn't be uploaded self.app.logger.debug( _('Object PUT returning 412, %(statuses)r'), {'statuses': statuses}) return HTTPPreconditionFailed(request=req) if len(conns) < min_conns: self.app.logger.error( _('Object PUT returning 503, %(conns)s/%(nodes)s ' 'required connections'), {'conns': len(conns), 'nodes': min_conns}) return HTTPServiceUnavailable(request=req) bytes_transferred = 0 try: with ContextPool(len(nodes)) as pool: for conn in conns: conn.failed = False conn.queue = Queue(self.app.put_queue_depth) pool.spawn(self._send_file, conn, req.path) while True: with ChunkReadTimeout(self.app.client_timeout): try: chunk = next(data_source) except StopIteration: if chunked: for conn in conns: conn.queue.put('0\r\n\r\n') break bytes_transferred += len(chunk) if bytes_transferred > constraints.MAX_FILE_SIZE: return HTTPRequestEntityTooLarge(request=req) for conn in list(conns): if not conn.failed: conn.queue.put( '%x\r\n%s\r\n' % (len(chunk), chunk) if chunked else chunk) else: conns.remove(conn) if len(conns) < min_conns: self.app.logger.error(_( 'Object PUT exceptions during' ' send, %(conns)s/%(nodes)s required connections'), {'conns': len(conns), 'nodes': min_conns}) return HTTPServiceUnavailable(request=req) for conn in conns: if conn.queue.unfinished_tasks: conn.queue.join() conns = [conn for conn in conns if not conn.failed] except ChunkReadTimeout as err: self.app.logger.warn( _('ERROR Client read timeout (%ss)'), err.seconds) self.app.logger.increment('client_timeouts') return HTTPRequestTimeout(request=req) except (Exception, Timeout): self.app.logger.exception( _('ERROR Exception causing client disconnect')) return HTTPClientDisconnect(request=req) if req.content_length and bytes_transferred < req.content_length: req.client_disconnect = True self.app.logger.warn( _('Client disconnected without sending enough data')) self.app.logger.increment('client_disconnects') return HTTPClientDisconnect(request=req) statuses, reasons, bodies, etags = self._get_put_responses(req, conns, nodes) if len(etags) > 1: self.app.logger.error( _('Object servers returned %s mismatched etags'), len(etags)) return HTTPServerError(request=req) etag = etags.pop() if len(etags) else None resp = self.best_response(req, statuses, reasons, bodies, _('Object PUT'), etag=etag) if source_header: resp.headers['X-Copied-From'] = quote( source_header.split('/', 3)[3]) if 'last-modified' in source_resp.headers: resp.headers['X-Copied-From-Last-Modified'] = \ source_resp.headers['last-modified'] copy_headers_into(req, resp) resp.last_modified = math.ceil( float(Timestamp(req.headers['X-Timestamp']))) return resp
def POST(self, req): """HTTP POST request handler.""" if 'x-delete-after' in req.headers: try: x_delete_after = int(req.headers['x-delete-after']) except ValueError: return HTTPBadRequest(request=req, content_type='text/plain', body='Non-integer X-Delete-After') req.headers['x-delete-at'] = normalize_delete_at_timestamp( time.time() + x_delete_after) if self.app.object_post_as_copy: req.method = 'PUT' req.path_info = '/v1/%s/%s/%s' % ( self.account_name, self.container_name, self.object_name) req.headers['Content-Length'] = 0 req.headers['X-Copy-From'] = quote('/%s/%s' % (self.container_name, self.object_name)) req.headers['X-Fresh-Metadata'] = 'true' req.environ['swift_versioned_copy'] = True if req.environ.get('QUERY_STRING'): req.environ['QUERY_STRING'] += '&multipart-manifest=get' else: req.environ['QUERY_STRING'] = 'multipart-manifest=get' resp = self.PUT(req) # Older editions returned 202 Accepted on object POSTs, so we'll # convert any 201 Created responses to that for compatibility with # picky clients. if resp.status_int != HTTP_CREATED: return resp return HTTPAccepted(request=req) else: error_response = check_metadata(req, 'object') if error_response: return error_response container_info = self.container_info( self.account_name, self.container_name, req) container_partition = container_info['partition'] containers = container_info['nodes'] req.acl = container_info['write_acl'] if 'swift.authorize' in req.environ: aresp = req.environ['swift.authorize'](req) if aresp: return aresp if not containers: return HTTPNotFound(request=req) if 'x-delete-at' in req.headers: try: x_delete_at = normalize_delete_at_timestamp( int(req.headers['x-delete-at'])) if int(x_delete_at) < time.time(): return HTTPBadRequest( body='X-Delete-At in past', request=req, content_type='text/plain') except ValueError: return HTTPBadRequest(request=req, content_type='text/plain', body='Non-integer X-Delete-At') req.environ.setdefault('swift.log_info', []).append( 'x-delete-at:%s' % x_delete_at) delete_at_container = normalize_delete_at_timestamp( int(x_delete_at) / self.app.expiring_objects_container_divisor * self.app.expiring_objects_container_divisor) delete_at_part, delete_at_nodes = \ self.app.container_ring.get_nodes( self.app.expiring_objects_account, delete_at_container) else: delete_at_container = delete_at_part = delete_at_nodes = None # pass the policy index to storage nodes via req header policy_index = req.headers.get('X-Backend-Storage-Policy-Index', container_info['storage_policy']) obj_ring = self.app.get_object_ring(policy_index) req.headers['X-Backend-Storage-Policy-Index'] = policy_index partition, nodes = obj_ring.get_nodes( self.account_name, self.container_name, self.object_name) req.headers['X-Timestamp'] = Timestamp(time.time()).internal headers = self._backend_requests( req, len(nodes), container_partition, containers, delete_at_container, delete_at_part, delete_at_nodes) resp = self.make_requests(req, obj_ring, partition, 'POST', req.swift_entity_path, headers) return resp
def GETorHEAD(self, req): """Handler for HTTP GET/HEAD requests.""" ai = self.account_info(self.account_name, req) auto_account = self.account_name.startswith( self.app.auto_create_account_prefix) if not (auto_account or ai[1]): if 'swift.authorize' in req.environ: aresp = req.environ['swift.authorize'](req) if aresp: # Don't cache this. It doesn't reflect the state of the # container, just that the user can't access it. return aresp # Don't cache this. The lack of account will be cached, and that # is sufficient. return HTTPNotFound(request=req) # The read-modify-write of params here is because the Request.params # getter dynamically generates a dict of params from the query string; # the setter must be called for new params to update the query string. params = req.params params['format'] = 'json' # x-backend-record-type may be sent via internal client e.g. from # the sharder or in probe tests record_type = req.headers.get('X-Backend-Record-Type', '').lower() if not record_type: record_type = 'auto' req.headers['X-Backend-Record-Type'] = 'auto' params['states'] = 'listing' req.params = params memcache = cache_from_env(req.environ, True) if (req.method == 'GET' and record_type != 'object' and self.app.recheck_listing_shard_ranges > 0 and memcache and get_param(req, 'states') == 'listing' and not config_true_value( req.headers.get('x-backend-include-deleted', False))): # This GET might be served from cache or might populate cache. # 'x-backend-include-deleted' is not usually expected in requests # to the proxy (it is used from sharder to container servers) but # it is included in the conditions just in case because we don't # cache deleted shard ranges. resp = self._GET_using_cache(req) else: resp = self._GETorHEAD_from_backend(req) resp_record_type = resp.headers.get('X-Backend-Record-Type', '') if all((req.method == "GET", record_type == 'auto', resp_record_type.lower() == 'shard')): resp = self._get_from_shards(req, resp) if not config_true_value(resp.headers.get('X-Backend-Cached-Results')): # Cache container metadata. We just made a request to a storage # node and got up-to-date information for the container. resp.headers['X-Backend-Recheck-Container-Existence'] = str( self.app.recheck_container_existence) set_info_cache(self.app, req.environ, self.account_name, self.container_name, resp) if 'swift.authorize' in req.environ: req.acl = resp.headers.get('x-container-read') aresp = req.environ['swift.authorize'](req) if aresp: # Don't cache this. It doesn't reflect the state of the # container, just that the user can't access it. return aresp if not req.environ.get('swift_owner', False): for key in self.app.swift_owner_headers: if key in resp.headers: del resp.headers[key] # Expose sharding state in reseller requests if req.environ.get('reseller_request', False): resp.headers['X-Container-Sharding'] = config_true_value( resp.headers.get( get_sys_meta_prefix('container') + 'Sharding', 'False')) return resp
def DELETE(self, request): """Handle HTTP DELETE requests for the Swift Object Server.""" device, partition, account, container, obj, policy_idx = \ get_name_and_placement(request, 5, 5, True) req_timestamp = valid_timestamp(request) try: disk_file = self.get_diskfile(device, partition, account, container, obj, policy_idx=policy_idx) except DiskFileDeviceUnavailable: return HTTPInsufficientStorage(drive=device, request=request) try: orig_metadata = disk_file.read_metadata() except DiskFileXattrNotSupported: return HTTPInsufficientStorage(drive=device, request=request) except DiskFileExpired as e: orig_timestamp = e.timestamp orig_metadata = e.metadata response_class = HTTPNotFound except DiskFileDeleted as e: orig_timestamp = e.timestamp orig_metadata = {} response_class = HTTPNotFound except (DiskFileNotExist, DiskFileQuarantined): orig_timestamp = 0 orig_metadata = {} response_class = HTTPNotFound else: orig_timestamp = Timestamp(orig_metadata.get('X-Timestamp', 0)) if orig_timestamp < req_timestamp: response_class = HTTPNoContent else: response_class = HTTPConflict response_timestamp = max(orig_timestamp, req_timestamp) orig_delete_at = int(orig_metadata.get('X-Delete-At') or 0) try: req_if_delete_at_val = request.headers['x-if-delete-at'] req_if_delete_at = int(req_if_delete_at_val) except KeyError: pass except ValueError: return HTTPBadRequest(request=request, body='Bad X-If-Delete-At header value') else: # request includes x-if-delete-at; we must not place a tombstone # if we can not verify the x-if-delete-at time if not orig_timestamp: # no object found at all return HTTPNotFound() if orig_delete_at != req_if_delete_at: return HTTPPreconditionFailed( request=request, body='X-If-Delete-At and X-Delete-At do not match') else: # differentiate success from no object at all response_class = HTTPNoContent if orig_delete_at: self.delete_at_update('DELETE', orig_delete_at, account, container, obj, request, device, policy_idx) if orig_timestamp < req_timestamp: disk_file.delete(req_timestamp) self.container_update( 'DELETE', account, container, obj, request, HeaderKeyDict({'x-timestamp': req_timestamp.internal}), device, policy_idx) return response_class( request=request, headers={'X-Backend-Timestamp': response_timestamp.internal})
def account_update(self, req, account, container, broker): """ Update the account server(s) with latest container info. :param req: swob.Request object :param account: account name :param container: container name :param broker: container DB broker object :returns: if all the account requests return a 404 error code, HTTPNotFound response object, if the account cannot be updated due to a malformed header, an HTTPBadRequest response object, otherwise None. """ account_hosts = [ h.strip() for h in req.headers.get('X-Account-Host', '').split(',') ] account_devices = [ d.strip() for d in req.headers.get('X-Account-Device', '').split(',') ] account_partition = req.headers.get('X-Account-Partition', '') if len(account_hosts) != len(account_devices): # This shouldn't happen unless there's a bug in the proxy, # but if there is, we want to know about it. self.logger.error( _('ERROR Account update failed: different ' 'numbers of hosts and devices in request: ' '"%(hosts)s" vs "%(devices)s"') % { 'hosts': req.headers.get('X-Account-Host', ''), 'devices': req.headers.get('X-Account-Device', '') }) return HTTPBadRequest(req=req) if account_partition: # zip is lazy on py3, but we need a list, so force evaluation. # On py2 it's an extra list copy, but the list is so small # (one element per replica in account ring, usually 3) that it # doesn't matter. updates = list(zip(account_hosts, account_devices)) else: updates = [] account_404s = 0 for account_host, account_device in updates: account_ip, account_port = account_host.rsplit(':', 1) new_path = '/' + '/'.join([account, container]) info = broker.get_info() account_headers = HeaderKeyDict({ 'x-put-timestamp': info['put_timestamp'], 'x-delete-timestamp': info['delete_timestamp'], 'x-object-count': info['object_count'], 'x-bytes-used': info['bytes_used'], 'x-trans-id': req.headers.get('x-trans-id', '-'), 'X-Backend-Storage-Policy-Index': info['storage_policy_index'], 'user-agent': 'container-server %s' % os.getpid(), 'referer': req.as_referer() }) if req.headers.get('x-account-override-deleted', 'no').lower() == \ 'yes': account_headers['x-account-override-deleted'] = 'yes' try: with ConnectionTimeout(self.conn_timeout): conn = http_connect(account_ip, account_port, account_device, account_partition, 'PUT', new_path, account_headers) with Timeout(self.node_timeout): account_response = conn.getresponse() account_response.read() if account_response.status == HTTP_NOT_FOUND: account_404s += 1 elif not is_success(account_response.status): self.logger.error( _('ERROR Account update failed ' 'with %(ip)s:%(port)s/%(device)s (will retry ' 'later): Response %(status)s %(reason)s'), { 'ip': account_ip, 'port': account_port, 'device': account_device, 'status': account_response.status, 'reason': account_response.reason }) except (Exception, Timeout): self.logger.exception( _('ERROR account update failed with ' '%(ip)s:%(port)s/%(device)s (will retry later)'), { 'ip': account_ip, 'port': account_port, 'device': account_device }) if updates and account_404s == len(updates): return HTTPNotFound(req=req) else: return None
def GET(self, request): __CBAC__ = True """Handle HTTP GET requests for the Swift Object Server.""" my_debug('testing', '@GET()') my_debug('#request headers', request.headers) #print my_debug device, partition, account, container, obj = \ split_and_validate_path(request, 5, 5, True) keep_cache = self.keep_cache_private or ( 'X-Auth-Token' not in request.headers and 'X-Storage-Token' not in request.headers) try: disk_file = self.get_diskfile( device, partition, account, container, obj) except DiskFileDeviceUnavailable: return HTTPInsufficientStorage(drive=device, request=request) my_debug("locals()",locals()) my_debug("original Diskfile", disk_file) try: with disk_file.open(): metadata = disk_file.get_metadata() my_debug("metadata", metadata) obj_size = int(metadata['Content-Length']) file_x_ts = metadata['X-Timestamp'] #json_policy = metadata['JSON-Policy'] file_x_ts_flt = float(file_x_ts) keep_cache = (self.keep_cache_private or ('X-Auth-Token' not in request.headers and 'X-Storage-Token' not in request.headers)) ''' if policy file is present & user_label is present apply JSONAC Additional thing to have: 1. Check if policy from metadata is valid json 2. Check if user_clearance is present is headers. Read keystone info. 3. Pass JSONPath as headers 4. ''' if __CBAC__: my_debug("----CBAC Starts here-----", "***********") disk_file_iter = disk_file.reader(keep_cache=keep_cache) original_data = "" for chunk in iter(disk_file_iter): original_data += chunk userRoles = request.headers['X-Roles'] json_policy = metadata['X-Object-Meta-Jsonpolicy'] \ if metadata.has_key('X-Object-Meta-Jsonpolicy') else None user_labels = metadata['X-Object-Meta-Userlabels'] \ if metadata.has_key('X-Object-Meta-Userlabels') else None object_labels = metadata['X-Object-Meta-Objectlabels'] \ if metadata.has_key('X-Object-Meta-Objectlabels') else None object_labelling = metadata['X-Object-Meta-Jsonlabelling'] \ if metadata.has_key('X-Object-Meta-Jsonlabelling') else None if json_policy and user_labels and object_labels : cbac_policy = {} cbac_policy['user_labels'] = json.loads(user_labels) cbac_policy['object_labels'] = json.loads(object_labels) cbac_policy['policy'] = json.loads(json_policy) #user_clearance = ['manager'] user_clearance = userRoles jsonpath = "/" filtered_content = "" my_debug("json_policy is", json_policy) my_debug("original data is ", original_data) #try: if json_policy : if json_policy and user_clearance and jsonpath and object_labelling: '''filtered_content = ContentFilter(content_str=original_data, labeling_policy_str=object_labelling, user_clearance=user_clearance, query=jsonpath, cbac_policy=cbac_policy).apply()''' filtered_content = "testing" my_debug("#filtered content is ", filtered_content) #else: #my_debug("#content_filter not working", True) #except Exception as e: #else: # my_debug("Exception with content filtering {}".format("e"), True) '''end of content -filter ''' tmp_file = self.tmp_disk_file( request=request, device=device, partition=partition, container=container, obj="tmp", data=filtered_content, account=account) tmp_file = self.get_diskfile( device, partition, account, container, "tmp") my_debug("tmp_file is", tmp_file) try: with tmp_file.open(): tmp_metadata = tmp_file.get_metadata() my_debug("with tmp_file.open()", tmp_file.reader(keep_cache=keep_cache)) response = Response( app_iter=tmp_file.reader(keep_cache=keep_cache), request=request, conditional_response=True) response.headers['Content-Type'] = tmp_metadata.get( 'Content-Type', 'application/octet-stream') response.content_length = len(filtered_content) for key, value in tmp_metadata.iteritems(): if is_user_meta('object', key) or \ key.lower() in self.allowed_headers: response.headers[key] = value response.etag = tmp_metadata['ETag'] #response.last_modified = math.ceil(file_x_ts_flt) #response.content_length = obj_size my_debug("get_response", "test") resp = request.get_response(response) my_debug( "response", resp ) my_debug("rsponse.__dict__", resp.__dict__) except (DiskFileNotExist, DiskFileQuarantined): my_debug("tmp file is not found", "test") resp = HTTPNotFound(request=request, conditional_response=True) my_debug("final resp object", resp) return resp disk_file.open() response = Response( app_iter=disk_file.reader(keep_cache=keep_cache), # instead of app_iter which reads from the file, content is given in body #body="Yes, you are smart!\n", request=request, conditional_response=True) response.headers['Content-Type'] = metadata.get( 'Content-Type', 'application/octet-stream') for key, value in metadata.iteritems(): if is_user_meta('object', key) or \ key.lower() in self.allowed_headers: response.headers[key] = value response.etag = metadata['ETag'] response.last_modified = math.ceil(file_x_ts_flt) response.content_length = obj_size #response.content_length = response.headers['Content-Length'] try: response.content_encoding = metadata[ 'Content-Encoding'] except KeyError: pass response.headers['X-Timestamp'] = file_x_ts resp = request.get_response(response) except (DiskFileNotExist, DiskFileQuarantined): resp = HTTPNotFound(request=request, conditional_response=True) my_debug("resp object without cbac", resp.__dict__) return resp
def authorize(self, req): """ Returns None if the request is authorized to continue or a standard WSGI response callable if not. """ try: version, account, container, obj = req.split_path(1, 4, True) except ValueError: self.logger.increment('errors') return HTTPNotFound(request=req) if not account or not account.startswith(self.reseller_prefix): self.logger.debug("Account name: %s doesn't start with " "reseller_prefix: %s." % (account, self.reseller_prefix)) return self.denied_response(req) user_groups = (req.remote_user or '').split(',') account_user = user_groups[1] if len(user_groups) > 1 else None if '.reseller_admin' in user_groups and \ account != self.reseller_prefix and \ account[len(self.reseller_prefix)] != '.': req.environ['swift_owner'] = True self.logger.debug("User %s has reseller admin authorizing." % account_user) return None if account in user_groups and \ (req.method not in ('DELETE', 'PUT') or container): # If the user is admin for the account and is not trying to do an # account DELETE or PUT... req.environ['swift_owner'] = True self.logger.debug("User %s has admin authorizing." % account_user) return None if (req.environ.get('swift_sync_key') and (req.environ['swift_sync_key'] == req.headers.get( 'x-container-sync-key', None)) and 'x-timestamp' in req.headers): self.logger.debug("Allow request with container sync-key: %s." % req.environ['swift_sync_key']) return None if req.method == 'OPTIONS': #allow OPTIONS requests to proceed as normal self.logger.debug("Allow OPTIONS request.") return None referrers, groups = parse_acl(getattr(req, 'acl', None)) if referrer_allowed(req.referer, referrers): if obj or '.rlistings' in groups: self.logger.debug("Allow authorizing %s via referer ACL." % req.referer) return None for user_group in user_groups: if user_group in groups: self.logger.debug("User %s allowed in ACL: %s authorizing." % (account_user, user_group)) return None return self.denied_response(req)
def _listing(self, env, start_response, prefix=None): """ Sends an HTML object listing to the remote client. :param env: The original WSGI environment dict. :param start_response: The original WSGI start_response hook. :param prefix: Any prefix desired for the container listing. """ if not config_true_value(self._listings): body = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 ' \ 'Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n' \ '<html>\n' \ '<head>\n' \ '<title>Listing of %s</title>\n' % cgi.escape(env['PATH_INFO']) if self._listings_css: body += ' <link rel="stylesheet" type="text/css" ' \ 'href="%s" />\n' % self._build_css_path(prefix or '') else: body += ' <style type="text/css">\n' \ ' h1 {font-size: 1em; font-weight: bold;}\n' \ ' p {font-size: 2}\n' \ ' </style>\n' body += '</head>\n<body>' \ ' <h1>Web Listing Disabled</h1>' \ ' <p>The owner of this web site has disabled web listing.' \ ' <p>If you are the owner of this web site, you can enable' \ ' web listing by setting X-Container-Meta-Web-Listings.</p>' if self._index: body += '<h1>Index File Not Found</h1>' \ ' <p>The owner of this web site has set ' \ ' <b>X-Container-Meta-Web-Index: %s</b>. ' \ ' However, this file is not found.</p>' % self._index body += ' </body>\n</html>\n' resp = HTTPNotFound(body=body)(env, self._start_response) return self._error_response(resp, env, start_response) tmp_env = make_env(env, 'GET', '/%s/%s/%s' % (self.version, self.account, self.container), self.agent, swift_source='SW') tmp_env['QUERY_STRING'] = 'delimiter=/&format=json' if prefix: tmp_env['QUERY_STRING'] += '&prefix=%s' % quote(prefix) else: prefix = '' resp = self._app_call(tmp_env) if not is_success(self._get_status_int()): return self._error_response(resp, env, start_response) listing = None body = ''.join(resp) if body: listing = json.loads(body) if not listing: resp = HTTPNotFound()(env, self._start_response) return self._error_response(resp, env, start_response) headers = {'Content-Type': 'text/html; charset=UTF-8'} body = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 ' \ 'Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n' \ '<html>\n' \ ' <head>\n' \ ' <title>Listing of %s</title>\n' % \ cgi.escape(env['PATH_INFO']) if self._listings_css: body += ' <link rel="stylesheet" type="text/css" ' \ 'href="%s" />\n' % (self._build_css_path(prefix)) else: body += ' <style type="text/css">\n' \ ' h1 {font-size: 1em; font-weight: bold;}\n' \ ' th {text-align: left; padding: 0px 1em 0px 1em;}\n' \ ' td {padding: 0px 1em 0px 1em;}\n' \ ' a {text-decoration: none;}\n' \ ' </style>\n' body += ' </head>\n' \ ' <body>\n' \ ' <h1 id="title">Listing of %s</h1>\n' \ ' <table id="listing">\n' \ ' <tr id="heading">\n' \ ' <th class="colname">Name</th>\n' \ ' <th class="colsize">Size</th>\n' \ ' <th class="coldate">Date</th>\n' \ ' </tr>\n' % \ cgi.escape(env['PATH_INFO']) if prefix: body += ' <tr id="parent" class="item">\n' \ ' <td class="colname"><a href="../">../</a></td>\n' \ ' <td class="colsize"> </td>\n' \ ' <td class="coldate"> </td>\n' \ ' </tr>\n' for item in listing: if 'subdir' in item: subdir = item['subdir'].encode("utf-8") if prefix: subdir = subdir[len(prefix):] body += ' <tr class="item subdir">\n' \ ' <td class="colname"><a href="%s">%s</a></td>\n' \ ' <td class="colsize"> </td>\n' \ ' <td class="coldate"> </td>\n' \ ' </tr>\n' % \ (quote(subdir), cgi.escape(subdir)) for item in listing: if 'name' in item: name = item['name'].encode("utf-8") if prefix: name = name[len(prefix):] content_type = item['content_type'].encode("utf-8") bytes = human_readable(item['bytes']) last_modified = (cgi.escape(item['last_modified'].encode( "utf-8")).split('.')[0].replace('T', ' ')) body += ' <tr class="item %s">\n' \ ' <td class="colname"><a href="%s">%s</a></td>\n' \ ' <td class="colsize">%s</td>\n' \ ' <td class="coldate">%s</td>\n' \ ' </tr>\n' % \ (' '.join('type-' + cgi.escape(t.lower(), quote=True) for t in content_type.split('/')), quote(name), cgi.escape(name), bytes, last_modified) body += ' </table>\n' \ ' </body>\n' \ '</html>\n' resp = Response(headers=headers, body=body) return resp(env, start_response)
def handle_get_token(self, req): """ Handles the various `request for token and service end point(s)` calls. There are various formats to support the various auth servers in the past. Examples:: GET <auth-prefix>/v1/<act>/auth X-Auth-User: <act>:<usr> or X-Storage-User: <usr> X-Auth-Key: <key> or X-Storage-Pass: <key> GET <auth-prefix>/auth X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr> X-Auth-Key: <key> or X-Storage-Pass: <key> GET <auth-prefix>/v1.0 X-Auth-User: <act>:<usr> or X-Storage-User: <act>:<usr> X-Auth-Key: <key> or X-Storage-Pass: <key> On successful authentication, the response will have X-Auth-Token and X-Storage-Token set to the token to use with Swift and X-Storage-URL set to the URL to the default Swift cluster to use. :param req: The swob.Request to process. :returns: swob.Response, 2xx on success with data set as explained above. """ # Validate the request info try: pathsegs = split_path(req.path_info, 1, 3, True) except ValueError: self.logger.increment('errors') return HTTPNotFound(request=req) if pathsegs[0] == 'v1' and pathsegs[2] == 'auth': account = pathsegs[1] user = req.headers.get('x-storage-user') if not user: user = req.headers.get('x-auth-user') if not user or ':' not in user: self.logger.increment('token_denied') auth = 'Swift realm="%s"' % account return HTTPUnauthorized(request=req, headers={'Www-Authenticate': auth}) account2, user = user.split(':', 1) if account != account2: self.logger.increment('token_denied') auth = 'Swift realm="%s"' % account return HTTPUnauthorized(request=req, headers={'Www-Authenticate': auth}) key = req.headers.get('x-storage-pass') if not key: key = req.headers.get('x-auth-key') elif pathsegs[0] in ('auth', 'v1.0'): user = req.headers.get('x-auth-user') if not user: user = req.headers.get('x-storage-user') if not user or ':' not in user: self.logger.increment('token_denied') auth = 'Swift realm="unknown"' return HTTPUnauthorized(request=req, headers={'Www-Authenticate': auth}) account, user = user.split(':', 1) key = req.headers.get('x-auth-key') if not key: key = req.headers.get('x-storage-pass') else: return HTTPBadRequest(request=req) if not all((account, user, key)): self.logger.increment('token_denied') realm = account or 'unknown' return HTTPUnauthorized(request=req, headers={'Www-Authenticate': 'Swift realm="%s"' % realm}) # Authenticate user account_user = account + ':' + user if account_user not in self.users: self.logger.increment('token_denied') auth = 'Swift realm="%s"' % account return HTTPUnauthorized(request=req, headers={'Www-Authenticate': auth}) if self.users[account_user]['key'] != key: self.logger.increment('token_denied') auth = 'Swift realm="unknown"' return HTTPUnauthorized(request=req, headers={'Www-Authenticate': auth}) account_id = self.users[account_user]['url'].rsplit('/', 1)[-1] # Get memcache client memcache_client = cache_from_env(req.environ) if not memcache_client: raise Exception('Memcache required') # See if a token already exists and hasn't expired token = None memcache_user_key = '%s/user/%s' % (self.reseller_prefix, account_user) candidate_token = memcache_client.get(memcache_user_key) if candidate_token: memcache_token_key = \ '%s/token/%s' % (self.reseller_prefix, candidate_token) cached_auth_data = memcache_client.get(memcache_token_key) if cached_auth_data: expires, old_groups = cached_auth_data old_groups = old_groups.split(',') new_groups = self._get_user_groups(account, account_user, account_id) if expires > time() and \ set(old_groups) == set(new_groups.split(',')): token = candidate_token # Create a new token if one didn't exist if not token: # Generate new token token = '%stk%s' % (self.reseller_prefix, uuid4().hex) expires = time() + self.token_life groups = self._get_user_groups(account, account_user, account_id) # Save token memcache_token_key = '%s/token/%s' % (self.reseller_prefix, token) memcache_client.set(memcache_token_key, (expires, groups), time=float(expires - time())) # Record the token with the user info for future use. memcache_user_key = \ '%s/user/%s' % (self.reseller_prefix, account_user) memcache_client.set(memcache_user_key, token, time=float(expires - time())) resp = Response(request=req, headers={ 'x-auth-token': token, 'x-storage-token': token}) url = self.users[account_user]['url'].replace('$HOST', resp.host_url) if self.storage_url_scheme != 'default': url = self.storage_url_scheme + ':' + url.split(':', 1)[1] resp.headers['x-storage-url'] = url return resp
def handle_object(self, env, start_response): """ Handles a possible static web request for an object. This object could resolve into an index or listing request. :param env: The original WSGI environment dict. :param start_response: The original WSGI start_response hook. """ tmp_env = dict(env) tmp_env['HTTP_USER_AGENT'] = \ '%s StaticWeb' % env.get('HTTP_USER_AGENT') tmp_env['swift.source'] = 'SW' resp = self._app_call(tmp_env) status_int = self._get_status_int() self._get_container_info(env) if is_success(status_int) or is_redirection(status_int): # Treat directory marker objects as not found if not self._dir_type: self._dir_type = 'application/directory' content_length = self._response_header_value('content-length') content_length = int(content_length) if content_length else 0 if self._response_header_value('content-type') == self._dir_type \ and content_length <= 1: status_int = HTTP_NOT_FOUND else: start_response(self._response_status, self._response_headers, self._response_exc_info) return resp if status_int != HTTP_NOT_FOUND: # Retaining the previous code's behavior of not using custom error # pages for non-404 errors. self._error = None return self._error_response(resp, env, start_response) if not self._listings and not self._index: start_response(self._response_status, self._response_headers, self._response_exc_info) return resp status_int = HTTP_NOT_FOUND if self._index: tmp_env = dict(env) tmp_env['HTTP_USER_AGENT'] = \ '%s StaticWeb' % env.get('HTTP_USER_AGENT') tmp_env['swift.source'] = 'SW' if not tmp_env['PATH_INFO'].endswith('/'): tmp_env['PATH_INFO'] += '/' tmp_env['PATH_INFO'] += self._index resp = self._app_call(tmp_env) status_int = self._get_status_int() if is_success(status_int) or is_redirection(status_int): if not env['PATH_INFO'].endswith('/'): resp = HTTPMovedPermanently(location=env['PATH_INFO'] + '/') return resp(env, start_response) start_response(self._response_status, self._response_headers, self._response_exc_info) return resp if status_int == HTTP_NOT_FOUND: if not env['PATH_INFO'].endswith('/'): tmp_env = make_env( env, 'GET', '/%s/%s/%s' % (self.version, self.account, self.container), self.agent, swift_source='SW') tmp_env['QUERY_STRING'] = 'limit=1&format=json&delimiter' \ '=/&limit=1&prefix=%s' % quote(self.obj + '/') resp = self._app_call(tmp_env) body = ''.join(resp) if not is_success(self._get_status_int()) or not body or \ not json.loads(body): resp = HTTPNotFound()(env, self._start_response) return self._error_response(resp, env, start_response) resp = HTTPMovedPermanently(location=env['PATH_INFO'] + '/') return resp(env, start_response) return self._listing(env, start_response, self.obj)
def get_object_head_resp(self, req): storage = self.app.storage oio_headers = {REQID_HEADER: self.trans_id} oio_cache = req.environ.get('oio.cache') perfdata = req.environ.get('swift.perfdata') version = obj_version_from_env(req.environ) force_master = False while True: try: if self.app.check_state: metadata, chunks = storage.object_locate( self.account_name, self.container_name, self.object_name, version=version, headers=oio_headers, force_master=force_master, cache=oio_cache, perfdata=perfdata) else: metadata = storage.object_get_properties( self.account_name, self.container_name, self.object_name, version=version, headers=oio_headers, force_master=force_master, cache=oio_cache, perfdata=perfdata) break except (exceptions.NoSuchObject, exceptions.NoSuchContainer): if force_master or not \ self.container_name.endswith(MULTIUPLOAD_SUFFIX): # Either the request failed with the master, # or it is not an MPU return HTTPNotFound(request=req) # This part appears in the manifest, so it should be there. # To be sure, we must go check the master # in case of desynchronization. force_master = True if self.app.check_state: storage_method = STORAGE_METHODS.load(metadata['chunk_method']) # TODO(mbo): use new property of STORAGE_METHODS min_chunks = storage_method.ec_nb_data if storage_method.ec else 1 chunks_by_pos = _sort_chunks(chunks, storage_method.ec) for idx, entries in enumerate(chunks_by_pos.items()): if idx != entries[0]: return HTTPBadRequest(request=req) nb_chunks_ok = 0 for entry in entries[1]: try: storage.blob_client.chunk_head(entry['url'], headers=oio_headers) nb_chunks_ok += 1 except exceptions.OioException: pass if nb_chunks_ok >= min_chunks: break else: return HTTPBadRequest(request=req) resp = self.make_object_response(req, metadata) return resp
def PUT(self, req): """Handle HTTP PUT request.""" drive, part, account, container = split_and_validate_path(req, 3, 4) try: check_drive(self.root, drive, self.mount_check) except ValueError: return HTTPInsufficientStorage(drive=drive, request=req) if not self.check_free_space(drive): return HTTPInsufficientStorage(drive=drive, request=req) if container: # put account container if 'x-timestamp' not in req.headers: timestamp = Timestamp.now() else: timestamp = valid_timestamp(req) pending_timeout = None container_policy_index = \ req.headers.get('X-Backend-Storage-Policy-Index', 0) if 'x-trans-id' in req.headers: pending_timeout = 3 broker = self._get_account_broker(drive, part, account, pending_timeout=pending_timeout) if account.startswith(self.auto_create_account_prefix) and \ not os.path.exists(broker.db_file): try: broker.initialize(timestamp.internal) except DatabaseAlreadyExists: pass if req.headers.get('x-account-override-deleted', 'no').lower() != \ 'yes' and broker.is_deleted(): return HTTPNotFound(request=req) broker.put_container(container, req.headers['x-put-timestamp'], req.headers['x-delete-timestamp'], req.headers['x-object-count'], req.headers['x-bytes-used'], container_policy_index) if req.headers['x-delete-timestamp'] > \ req.headers['x-put-timestamp']: return HTTPNoContent(request=req) else: return HTTPCreated(request=req) else: # put account timestamp = valid_timestamp(req) broker = self._get_account_broker(drive, part, account) if not os.path.exists(broker.db_file): try: broker.initialize(timestamp.internal) created = True except DatabaseAlreadyExists: created = False elif broker.is_status_deleted(): return self._deleted_response(broker, req, HTTPForbidden, body='Recently deleted') else: created = broker.is_deleted() broker.update_put_timestamp(timestamp.internal) if broker.is_deleted(): return HTTPConflict(request=req) self._update_metadata(req, broker, timestamp) if created: return HTTPCreated(request=req) else: return HTTPAccepted(request=req)
def _store_object(self, req, data_source, headers): kwargs = {} content_type = req.headers.get('content-type', 'octet/stream') policy = None container_info = self.container_info(self.account_name, self.container_name, req) if 'X-Oio-Storage-Policy' in req.headers: policy = req.headers.get('X-Oio-Storage-Policy') if not self.app.POLICIES.get_by_name(policy): raise HTTPBadRequest( "invalid policy '%s', must be in %s" % (policy, self.app.POLICIES.by_name.keys())) else: try: policy_index = int( req.headers.get('X-Backend-Storage-Policy-Index', container_info['storage_policy'])) except TypeError: policy_index = 0 if policy_index != 0: policy = self.app.POLICIES.get_by_index(policy_index).name else: content_length = int(req.headers.get('content-length', 0)) policy = self._get_auto_policy_from_size(content_length) ct_props = {'properties': {}, 'system': {}} metadata = self.load_object_metadata(headers) oio_headers = {REQID_HEADER: self.trans_id} oio_cache = req.environ.get('oio.cache') perfdata = req.environ.get('swift.perfdata') # only send headers if needed if SUPPORT_VERSIONING and headers.get(FORCEVERSIONING_HEADER): oio_headers[FORCEVERSIONING_HEADER] = \ headers.get(FORCEVERSIONING_HEADER) if req.environ.get('oio.force-version'): # In a case of MPU, it contains version of the UploadId # to be able to include version-id of MPU in S3 reponse kwargs['version'] = req.environ.get('oio.force-version') # In case a shard is being created, save the name of the S3 bucket # in a container property. This will be used when aggregating # container statistics to make bucket statistics. if BUCKET_NAME_HEADER in headers: bname = headers[BUCKET_NAME_HEADER] # FIXME(FVE): the segments container is not part of another bucket! # We should not have to strip this here. if bname and bname.endswith(MULTIUPLOAD_SUFFIX): bname = bname[:-len(MULTIUPLOAD_SUFFIX)] ct_props['system'][BUCKET_NAME_PROP] = bname try: _chunks, _size, checksum, _meta = self._object_create( self.account_name, self.container_name, obj_name=self.object_name, file_or_path=data_source, mime_type=content_type, policy=policy, headers=oio_headers, etag=req.headers.get('etag', '').strip('"'), properties=metadata, container_properties=ct_props, cache=oio_cache, perfdata=perfdata, **kwargs) # TODO(FVE): when oio-sds supports it, do that in a callback # passed to object_create (or whatever upload method supports it) footer_md = self.load_object_metadata(self._get_footers(req)) if footer_md: self.app.storage.object_set_properties(self.account_name, self.container_name, self.object_name, version=_meta.get( 'version', None), properties=footer_md, headers=oio_headers, cache=oio_cache, perfdata=perfdata) except exceptions.Conflict: raise HTTPConflict(request=req) except exceptions.PreconditionFailed: raise HTTPPreconditionFailed(request=req) except SourceReadTimeout as err: self.app.logger.warning(_('ERROR Client read timeout (%s)'), err) self.app.logger.increment('client_timeouts') raise HTTPRequestTimeout(request=req) except exceptions.SourceReadError: req.client_disconnect = True self.app.logger.warning( _('Client disconnected without sending last chunk')) self.app.logger.increment('client_disconnects') raise HTTPClientDisconnect(request=req) except exceptions.EtagMismatch: return HTTPUnprocessableEntity(request=req) except (exceptions.ServiceBusy, exceptions.OioTimeout, exceptions.DeadlineReached): raise except exceptions.NoSuchContainer: raise HTTPNotFound(request=req) except exceptions.ClientException as err: # 481 = CODE_POLICY_NOT_SATISFIABLE if err.status == 481: raise exceptions.ServiceBusy() self.app.logger.exception( _('ERROR Exception transferring data %s'), {'path': req.path}) raise HTTPInternalServerError(request=req) except Exception: self.app.logger.exception( _('ERROR Exception transferring data %s'), {'path': req.path}) raise HTTPInternalServerError(request=req) last_modified = int(_meta.get('mtime', math.ceil(time.time()))) # FIXME(FVE): if \x10 character in object name, decode version # number and set it in the response headers, instead of the oio # version number. version_id = _meta.get('version', 'null') resp = HTTPCreated(request=req, etag=checksum, last_modified=last_modified, headers={'x-object-sysmeta-version-id': version_id}) return resp
def GET(self, request): """Handle HTTP GET requests for the Swift Object Server.""" device, partition, account, container, obj = \ split_and_validate_path(request, 5, 5, True) try: disk_file = self._diskfile(device, partition, account, container, obj, keep_data_fp=True, iter_hook=sleep) except DiskFileDeviceUnavailable: return HTTPInsufficientStorage(drive=device, request=request) if disk_file.is_deleted() or disk_file.is_expired(): if request.headers.get('if-match') == '*': return HTTPPreconditionFailed(request=request) else: return HTTPNotFound(request=request) try: file_size = disk_file.get_data_file_size() except (DiskFileError, DiskFileNotExist): disk_file.quarantine() return HTTPNotFound(request=request) if request.headers.get('if-match') not in (None, '*') and \ disk_file.metadata['ETag'] not in request.if_match: disk_file.close() return HTTPPreconditionFailed(request=request) if request.headers.get('if-none-match') is not None: if disk_file.metadata['ETag'] in request.if_none_match: resp = HTTPNotModified(request=request) resp.etag = disk_file.metadata['ETag'] disk_file.close() return resp try: if_unmodified_since = request.if_unmodified_since except (OverflowError, ValueError): # catches timestamps before the epoch return HTTPPreconditionFailed(request=request) if if_unmodified_since and \ datetime.fromtimestamp( float(disk_file.metadata['X-Timestamp']), UTC) > \ if_unmodified_since: disk_file.close() return HTTPPreconditionFailed(request=request) try: if_modified_since = request.if_modified_since except (OverflowError, ValueError): # catches timestamps before the epoch return HTTPPreconditionFailed(request=request) if if_modified_since and \ datetime.fromtimestamp( float(disk_file.metadata['X-Timestamp']), UTC) < \ if_modified_since: disk_file.close() return HTTPNotModified(request=request) response = Response(app_iter=disk_file, request=request, conditional_response=True) response.headers['Content-Type'] = disk_file.metadata.get( 'Content-Type', 'application/octet-stream') for key, value in disk_file.metadata.iteritems(): if key.lower().startswith('x-object-meta-') or \ key.lower() in self.allowed_headers: response.headers[key] = value response.etag = disk_file.metadata['ETag'] response.last_modified = float(disk_file.metadata['X-Timestamp']) response.content_length = file_size if response.content_length < self.keep_cache_size and \ (self.keep_cache_private or ('X-Auth-Token' not in request.headers and 'X-Storage-Token' not in request.headers)): disk_file.keep_cache = True if 'Content-Encoding' in disk_file.metadata: response.content_encoding = disk_file.metadata['Content-Encoding'] response.headers['X-Timestamp'] = disk_file.metadata['X-Timestamp'] return request.get_response(response)
def handle_extract(self, req, compress_type): """ :params req: a swob Request :params compress_type: specifying the compression type of the tar. Accepts '', 'gz, or 'bz2' :raises HTTPException: on unhandled errors :returns: a swob response to request """ success_count = 0 failed_files = [] existing_containers = set() out_content_type = req.accept.best_match(ACCEPTABLE_FORMATS) if not out_content_type: return HTTPNotAcceptable(request=req) if req.content_length is None and \ req.headers.get('transfer-encoding', '').lower() != 'chunked': return HTTPLengthRequired(request=req) try: vrs, account, extract_base = req.split_path(2, 3, True) except ValueError: return HTTPNotFound(request=req) extract_base = extract_base or '' extract_base = extract_base.rstrip('/') try: tar = tarfile.open(mode='r|' + compress_type, fileobj=req.body_file) while True: tar_info = tar.next() if tar_info is None or \ len(failed_files) >= self.max_failed_extractions: break if tar_info.isfile(): obj_path = tar_info.name if obj_path.startswith('./'): obj_path = obj_path[2:] obj_path = obj_path.lstrip('/') if extract_base: obj_path = extract_base + '/' + obj_path if '/' not in obj_path: continue # ignore base level file destination = '/'.join(['', vrs, account, obj_path]) container = obj_path.split('/', 1)[0] if not check_utf8(destination): failed_files.append([ quote(destination[:MAX_PATH_LENGTH]), HTTPPreconditionFailed().status ]) continue if tar_info.size > MAX_FILE_SIZE: failed_files.append([ quote(destination[:MAX_PATH_LENGTH]), HTTPRequestEntityTooLarge().status ]) continue if container not in existing_containers: try: self.create_container( req, '/'.join(['', vrs, account, container])) existing_containers.add(container) except CreateContainerError, err: if err.status_int == HTTP_UNAUTHORIZED: return HTTPUnauthorized(request=req) failed_files.append([ quote(destination[:MAX_PATH_LENGTH]), err.status ]) continue except ValueError: failed_files.append([ quote(destination[:MAX_PATH_LENGTH]), HTTP_BAD_REQUEST ]) continue if len(existing_containers) > self.max_containers: return HTTPBadRequest( 'More than %d base level containers in tar.' % self.max_containers)
def PUT(self, req): """Handle HTTP PUT request.""" drive, part, account, container, obj = split_and_validate_path( req, 4, 5, True) if 'x-timestamp' not in req.headers or \ not check_float(req.headers['x-timestamp']): return HTTPBadRequest(body='Missing timestamp', request=req, content_type='text/plain') if 'x-container-sync-to' in req.headers: err = validate_sync_to(req.headers['x-container-sync-to'], self.allowed_sync_hosts) if err: return HTTPBadRequest(err) if self.mount_check and not check_mount(self.root, drive): return HTTPInsufficientStorage(drive=drive, request=req) timestamp = normalize_timestamp(req.headers['x-timestamp']) broker = self._get_container_broker(drive, part, account, container) if obj: # put container object if account.startswith(self.auto_create_account_prefix) and \ not os.path.exists(broker.db_file): try: broker.initialize(timestamp) except DatabaseAlreadyExists: pass if not os.path.exists(broker.db_file): return HTTPNotFound() broker.put_object(obj, timestamp, int(req.headers['x-size']), req.headers['x-content-type'], req.headers['x-etag']) return HTTPCreated(request=req) else: # put container if not os.path.exists(broker.db_file): try: broker.initialize(timestamp) created = True except DatabaseAlreadyExists: created = False else: created = broker.is_deleted() broker.update_put_timestamp(timestamp) if broker.is_deleted(): return HTTPConflict(request=req) metadata = {} metadata.update((key, (value, timestamp)) for key, value in req.headers.iteritems() if key.lower() in self.save_headers or is_sys_or_user_meta('container', key)) if metadata: if 'X-Container-Sync-To' in metadata: if 'X-Container-Sync-To' not in broker.metadata or \ metadata['X-Container-Sync-To'][0] != \ broker.metadata['X-Container-Sync-To'][0]: broker.set_x_container_sync_points(-1, -1) broker.update_metadata(metadata) resp = self.account_update(req, account, container, broker) if resp: return resp if created: return HTTPCreated(request=req) else: return HTTPAccepted(request=req)
def authorize(self, env_identity, req): # Cleanup - make sure that a previously set swift_owner setting is # cleared now. This might happen for example with COPY requests. req.environ.pop('swift_owner', None) tenant_id, tenant_name = env_identity['tenant'] user_id, user_name = env_identity['user'] referrers, roles = swift_acl.parse_acl(getattr(req, 'acl', None)) # allow OPTIONS requests to proceed as normal if req.method == 'OPTIONS': return try: part = req.split_path(1, 4, True) version, account, container, obj = part except ValueError: return HTTPNotFound(request=req) self._set_project_domain_id(req, part, env_identity) user_roles = [r.lower() for r in env_identity.get('roles', [])] user_service_roles = [ r.lower() for r in env_identity.get('service_roles', []) ] # Give unconditional access to a user with the reseller_admin # role. if self.reseller_admin_role in user_roles: msg = 'User %s has reseller admin authorizing' self.logger.debug(msg, tenant_id) req.environ['swift_owner'] = True return # The system_reader_role is almost as good as reseller_admin. if self.system_reader_roles.intersection(user_roles): # Note that if a system reader is trying to write, we're letting # the request fall on other access checks below. This way, # a compliance auditor can write a log file as a normal member. if req.method in ('GET', 'HEAD'): msg = 'User %s has system reader authorizing' self.logger.debug(msg, tenant_id) # We aren't setting 'swift_owner' nor 'reseller_request' # because they are only ever used for something that modifies # the contents of the cluster (setting ACL, deleting accounts). return # If we are not reseller admin and user is trying to delete its own # account then deny it. if not container and not obj and req.method == 'DELETE': # User is not allowed to issue a DELETE on its own account msg = 'User %s:%s is not allowed to delete its own account' self.logger.debug(msg, tenant_name, user_name) return self.denied_response(req) # cross-tenant authorization matched_acl = None if roles: allow_names = self._is_name_allowed_in_acl(req, part, env_identity) matched_acl = self._authorize_cross_tenant(user_id, user_name, tenant_id, tenant_name, roles, allow_names) if matched_acl is not None: log_msg = 'user %s allowed in ACL authorizing.' self.logger.debug(log_msg, matched_acl) return acl_authorized = self._authorize_unconfirmed_identity( req, obj, referrers, roles) if acl_authorized: return # Check if a user tries to access an account that does not match their # token if not self._account_matches_tenant(account, tenant_id): log_msg = 'tenant mismatch: %s != %s' self.logger.debug(log_msg, account, tenant_id) return self.denied_response(req) # Compare roles from tokens against the configuration options: # # X-Auth-Token role Has specified X-Service-Token role Grant # in operator_roles? service_roles? in service_roles? swift_owner? # ------------------ -------------- -------------------- ------------ # yes yes yes yes # yes yes no no # yes no don't care yes # no don't care don't care no # ------------------ -------------- -------------------- ------------ account_prefix = self._get_account_prefix(account) operator_roles = self.account_rules[account_prefix]['operator_roles'] have_operator_role = set(operator_roles).intersection(set(user_roles)) service_roles = self.account_rules[account_prefix]['service_roles'] have_service_role = set(service_roles).intersection( set(user_service_roles)) allowed = False if have_operator_role and (service_roles and have_service_role): allowed = True elif have_operator_role and not service_roles: allowed = True if allowed: log_msg = 'allow user with role(s) %s as account admin' self.logger.debug( log_msg, ','.join(have_operator_role.union(have_service_role))) req.environ['swift_owner'] = True return if acl_authorized is not None: return self.denied_response(req) # Check if we have the role in the userroles and allow it for user_role in user_roles: if user_role in (r.lower() for r in roles): log_msg = 'user %s:%s allowed in ACL: %s authorizing' self.logger.debug(log_msg, tenant_name, user_name, user_role) return # Check if this user has a read-only role: # This check needs to come after the normal role check so that # read-only users can have write capabilities added for # specific containers readonly_roles = self.account_rules[account_prefix]['readonly_roles'] have_readonly_role = set(readonly_roles).intersection(set(user_roles)) if have_readonly_role and req.method in {'GET', 'HEAD'}: log_msg = 'user %s:%s allowed with read-only role: authorizing' self.logger.debug(log_msg, tenant_name, user_name) return return self.denied_response(req)
def GET(self, req): """Handle HTTP GET request.""" drive, part, account, container, obj = split_and_validate_path( req, 4, 5, True) path = get_param(req, 'path') prefix = get_param(req, 'prefix') delimiter = get_param(req, 'delimiter') if delimiter and (len(delimiter) > 1 or ord(delimiter) > 254): # delimiters can be made more flexible later return HTTPPreconditionFailed(body='Bad delimiter') marker = get_param(req, 'marker', '') end_marker = get_param(req, 'end_marker') limit = CONTAINER_LISTING_LIMIT given_limit = get_param(req, 'limit') if given_limit and given_limit.isdigit(): limit = int(given_limit) if limit > CONTAINER_LISTING_LIMIT: return HTTPPreconditionFailed(request=req, body='Maximum limit is %d' % CONTAINER_LISTING_LIMIT) out_content_type = get_listing_content_type(req) if self.mount_check and not check_mount(self.root, drive): return HTTPInsufficientStorage(drive=drive, request=req) broker = self._get_container_broker(drive, part, account, container, pending_timeout=0.1, stale_reads_ok=True) if broker.is_deleted(): return HTTPNotFound(request=req) info = broker.get_info() resp_headers = { 'X-Container-Object-Count': info['object_count'], 'X-Container-Bytes-Used': info['bytes_used'], 'X-Timestamp': info['created_at'], 'X-PUT-Timestamp': info['put_timestamp'], } for key, (value, timestamp) in broker.metadata.iteritems(): if value and (key.lower() in self.save_headers or is_sys_or_user_meta('container', key)): resp_headers[key] = value ret = Response(request=req, headers=resp_headers, content_type=out_content_type, charset='utf-8') container_list = broker.list_objects_iter(limit, marker, end_marker, prefix, delimiter, path) if out_content_type == 'application/json': ret.body = json.dumps( [self.update_data_record(record) for record in container_list]) elif out_content_type.endswith('/xml'): doc = Element('container', name=container.decode('utf-8')) for obj in container_list: record = self.update_data_record(obj) if 'subdir' in record: name = record['subdir'].decode('utf-8') sub = SubElement(doc, 'subdir', name=name) SubElement(sub, 'name').text = name else: obj_element = SubElement(doc, 'object') for field in [ "name", "hash", "bytes", "content_type", "last_modified" ]: SubElement(obj_element, field).text = str( record.pop(field)).decode('utf-8') for field in sorted(record): SubElement(obj_element, field).text = str( record[field]).decode('utf-8') ret.body = tostring(doc, encoding='UTF-8').replace( "<?xml version='1.0' encoding='UTF-8'?>", '<?xml version="1.0" encoding="UTF-8"?>', 1) else: if not container_list: return HTTPNoContent(request=req, headers=resp_headers) ret.body = '\n'.join(rec[0] for rec in container_list) + '\n' return ret