def _validate_dates(self): """ Validate Date/X-Amz-Date headers for signature v2 :raises: AccessDenied :raises: RequestTimeTooSkewed """ if self._is_query_auth: self._validate_expire_param() # TODO: make sure the case if timestamp param in query return date_header = self.headers.get('Date') amz_date_header = self.headers.get('X-Amz-Date') if not date_header and not amz_date_header: raise AccessDenied('AWS authentication requires a valid Date ' 'or x-amz-date header') # Anyways, request timestamp should be validated epoch = S3Timestamp(0) if self.timestamp < epoch: raise AccessDenied() # If the standard date is too far ahead or behind, it is an # error delta = 60 * 5 if abs(int(self.timestamp) - int(S3Timestamp.now())) > delta: raise RequestTimeTooSkewed()
def timestamp(self): """ S3Timestamp from Date header. If X-Amz-Date header specified, it will be prior to Date header. :return : S3Timestamp instance """ if not self._timestamp: try: if self._is_query_auth and 'Timestamp' in self.params: # If Timestamp specified in query, it should be prior # to any Date header (is this right?) timestamp = mktime( self.params['Timestamp'], SIGV2_TIMESTAMP_FORMAT) else: timestamp = mktime( self.headers.get('X-Amz-Date', self.headers.get('Date'))) except ValueError: raise AccessDenied('AWS authentication requires a valid Date ' 'or x-amz-date header') if timestamp < 0: raise AccessDenied('AWS authentication requires a valid Date ' 'or x-amz-date header') try: self._timestamp = S3Timestamp(timestamp) except ValueError: # Must be far-future; blame clock skew raise RequestTimeTooSkewed() return self._timestamp
def _parse_query_authentication(self): """ Parse v4 query authentication - version 4: 'X-Amz-Credential' and 'X-Amz-Signature' should be in param :raises: AccessDenied :raises: AuthorizationHeaderMalformed """ if self.params.get('X-Amz-Algorithm') != 'AWS4-HMAC-SHA256': raise InvalidArgument('X-Amz-Algorithm', self.params.get('X-Amz-Algorithm')) try: cred_param = self.params['X-Amz-Credential'].split("/") access = cred_param[0] sig = self.params['X-Amz-Signature'] expires = self.params['X-Amz-Expires'] except KeyError: raise AccessDenied() try: signed_headers = self.params['X-Amz-SignedHeaders'] except KeyError: # TODO: make sure if is it malformed request? raise AuthorizationHeaderMalformed() self._signed_headers = set(signed_headers.split(';')) # credential must be in following format: # <access-key-id>/<date>/<AWS-region>/<AWS-service>/aws4_request if not all([access, sig, len(cred_param) == 5, expires]): raise AccessDenied() return access, sig
def timestamp(self): """ Return timestamp string according to the auth type The difference from v2 is v4 have to see 'X-Amz-Date' even though it's query auth type. """ if not self._timestamp: try: if self._is_query_auth and 'X-Amz-Date' in self.params: # NOTE(andrey-mp): Date in Signature V4 has different # format timestamp = mktime( self.params['X-Amz-Date'], SIGV4_X_AMZ_DATE_FORMAT) else: if self.headers.get('X-Amz-Date'): timestamp = mktime( self.headers.get('X-Amz-Date'), SIGV4_X_AMZ_DATE_FORMAT) else: timestamp = mktime(self.headers.get('Date')) except (ValueError, TypeError): raise AccessDenied('AWS authentication requires a valid Date ' 'or x-amz-date header') if timestamp < 0: raise AccessDenied('AWS authentication requires a valid Date ' 'or x-amz-date header') try: self._timestamp = S3Timestamp(timestamp) except ValueError: # Must be far-future; blame clock skew raise RequestTimeTooSkewed() return self._timestamp
def _parse_authorization(self): if 'AWSAccessKeyId' in self.params: try: self.headers['Date'] = self.params['Expires'] self.headers['Authorization'] = \ 'AWS %(AWSAccessKeyId)s:%(Signature)s' % self.params except KeyError: raise AccessDenied() if 'Authorization' not in self.headers: raise NotS3Request() try: keyword, info = self.headers['Authorization'].split(' ', 1) except Exception: raise AccessDenied() if keyword != 'AWS': raise NotS3Request() try: access_key, signature = info.rsplit(':', 1) except Exception: err_msg = 'AWS authorization header is invalid. ' \ 'Expected AwsAccessKeyId:signature' raise InvalidArgument('Authorization', self.headers['Authorization'], err_msg) return access_key, signature
def check_owner(self, user_id): """ Check that the user is an owner. """ if not CONF.s3_acl or CONF.s3_acl_openbar: # Ignore Swift3 ACL. return if not self.owner.id: if CONF.allow_no_owner: # No owner means public. return raise AccessDenied() if user_id != self.owner.id: raise AccessDenied()
def _parse_header_authentication(self): """ Parse v4 header authentication - version 4: 'X-Amz-Credential' and 'X-Amz-Signature' should be in param :raises: AccessDenied :raises: AuthorizationHeaderMalformed """ auth_str = self.headers['Authorization'] cred_param = auth_str.partition( "Credential=")[2].split(',')[0].split("/") access = cred_param[0] sig = auth_str.partition("Signature=")[2].split(',')[0] signed_headers = auth_str.partition( "SignedHeaders=")[2].split(',', 1)[0] # credential must be in following format: # <access-key-id>/<date>/<AWS-region>/<AWS-service>/aws4_request if not all([access, sig, len(cred_param) == 5]): raise AccessDenied() if not signed_headers: # TODO: make sure if is it Malformed? raise AuthorizationHeaderMalformed() self._signed_headers = set(signed_headers.split(';')) return access, sig
def _validate_expire_param(self): """ Validate X-Amz-Expires in query parameter :raises: AccessDenied :raises: AuthorizationQueryParametersError :raises: AccessDenined """ err = None try: expires = int(self.params['X-Amz-Expires']) except ValueError: err = 'X-Amz-Expires should be a number' else: if expires < 0: err = 'X-Amz-Expires must be non-negative' elif expires >= 2 ** 63: err = 'X-Amz-Expires should be a number' elif expires > 604800: err = ('X-Amz-Expires must be less than a week (in seconds); ' 'that is, the given X-Amz-Expires must be less than ' '604800 seconds') if err: raise AuthorizationQueryParametersError(err) if int(self.timestamp) + expires < S3Timestamp.now(): raise AccessDenied('Request has expired')
def _validate_expire_param(self): """ Validate Expires in query parameters :raises: AccessDenied """ # Expires header is a float since epoch try: ex = S3Timestamp(float(self.params['Expires'])) except ValueError: raise AccessDenied() if S3Timestamp.now() > ex: raise AccessDenied('Request has expired') if ex >= 2 ** 31: raise AccessDenied( 'Invalid date (should be seconds since epoch): %s' % self.params['Expires'])
def _get_response(self, app, method, container, obj, headers=None, body=None, query=None): """ Calls the application with this request's environment. Returns a Response object that wraps up the application's result. """ sw_req = self.to_swift_req(method, container, obj, headers=headers, body=body, query=query) if CONF.s3_acl: sw_req.environ['swift_owner'] = True # needed to set ACL sw_req.environ['swift.authorize_override'] = True sw_req.environ['swift.authorize'] = lambda req: None sw_resp = sw_req.get_response(app) resp = Response.from_swift_resp(sw_resp) status = resp.status_int # pylint: disable-msg=E1101 if CONF.s3_acl: resp.bucket_acl = decode_acl('container', resp.sysmeta_headers) resp.object_acl = decode_acl('object', resp.sysmeta_headers) if not self.user_id: if 'HTTP_X_USER_NAME' in sw_resp.environ: # keystone self.user_id = \ utf8encode("%s:%s" % (sw_resp.environ['HTTP_X_TENANT_NAME'], sw_resp.environ['HTTP_X_USER_NAME'])) else: # tempauth self.user_id = self.access_key success_codes = self._swift_success_codes(method, container, obj) error_codes = self._swift_error_codes(method, container, obj) if status in success_codes: return resp err_msg = resp.body if status in error_codes: err_resp = \ error_codes[sw_resp.status_int] # pylint: disable-msg=E1101 if isinstance(err_resp, tuple): raise err_resp[0](*err_resp[1:]) else: raise err_resp() if status == HTTP_BAD_REQUEST: raise BadSwiftRequest(err_msg) if status == HTTP_UNAUTHORIZED: raise SignatureDoesNotMatch() if status == HTTP_FORBIDDEN: raise AccessDenied() raise InternalError('unexpected status code %d' % status)
def _parse_query_authentication(self): """ Parse v2 authentication query args TODO: make sure if 0, 1, 3 is supported? - version 0, 1, 2, 3: 'AWSAccessKeyId' and 'Signature' should be in param :return: a tuple of access_key and signature :raises: AccessDenied """ try: access = self.params['AWSAccessKeyId'] expires = self.params['Expires'] sig = self.params['Signature'] except KeyError: raise AccessDenied() if not all([access, sig, expires]): raise AccessDenied() return access, sig
def _parse_header_authentication(self): """ Parse v2 header authentication info :returns: a tuple of access_key and signature :raises: AccessDenied """ auth_str = self.headers['Authorization'] if not auth_str.startswith('AWS ') or ':' not in auth_str: raise AccessDenied() # This means signature format V2 access, sig = auth_str.split(' ', 1)[1].rsplit(':', 1) return access, sig
def wrapper(*args, **kwargs): req = args[1] # If there is no callback, IAM is disabled, # thus we let everything pass through. rules_cb = req.environ.get(IAM_RULES_CALLBACK) if rules_cb is None: return func(*args, **kwargs) if bucket_action and not req.is_object_request: action = bucket_action else: action = object_action # If there is no rule for this user, # don't let anything pass through. # FIXME(IAM): refine the callback parameters matcher = rules_cb(req) if not matcher: raise AccessDenied() # FIXME(IAM): a * must be used as object name, # not as wildcard in Resource below if req.object_name: rsc = IamResource(req.container_name + '/' + req.object_name) elif req.container_name: rsc = IamResource(req.container_name) else: rsc = None effect, sid = matcher(rsc, action, req) if effect != EXPLICIT_ALLOW: matcher.logger.info("Request denied by IAM (sid=%s)", sid) raise AccessDenied() return func(*args, **kwargs)
def _string_to_sign(self): """ Create 'StringToSign' value in Amazon terminology for v2. """ amz_headers = {} buf = [ self.method, _header_strip(self.headers.get('Content-MD5')) or '', _header_strip(self.headers.get('Content-Type')) or '' ] for amz_header in sorted((key.lower() for key in self.headers if key.lower().startswith('x-amz-'))): amz_headers[amz_header] = self.headers[amz_header] if self._is_header_auth: if 'x-amz-date' in amz_headers: buf.append('') elif 'Date' in self.headers: buf.append(self.headers['Date']) elif self._is_query_auth: buf.append(self.params['Expires']) else: # Should have already raised NotS3Request in _parse_auth_info, # but as a sanity check... raise AccessDenied() for k in sorted(key.lower() for key in amz_headers): buf.append("%s:%s" % (k, amz_headers[k])) path = self._canonical_uri() if self.query_string: path += '?' + self.query_string params = [] if '?' in path: path, args = path.split('?', 1) for key, value in sorted(self.params.items()): if key in ALLOWED_SUB_RESOURCES: params.append('%s=%s' % (key, value) if value else key) if params: buf.append('%s?%s' % (path, '&'.join(params))) else: buf.append(path) return '\n'.join(buf)
def check_permission(self, user_id, permission): """ Check that the user has a permission. """ if not CONF.s3_acl or CONF.s3_acl_openbar: # Ignore Swift3 ACL. return try: # owners have full control permission self.check_owner(user_id) return except AccessDenied: pass if permission in PERMISSIONS: for g in self.grants: if g.allow(user_id, 'FULL_CONTROL') or \ g.allow(user_id, permission): return raise AccessDenied()
def _get_response(self, app, method, container, obj, headers=None, body=None, query=None): """ Calls the application with this request's environment. Returns a Response object that wraps up the application's result. """ method = method or self.environ['REQUEST_METHOD'] if container is None: container = self.container_name if obj is None: obj = self.object_name sw_req = self.to_swift_req(method, container, obj, headers=headers, body=body, query=query) sw_resp = sw_req.get_response(app) resp = Response.from_swift_resp(sw_resp) status = resp.status_int # pylint: disable-msg=E1101 if not self.user_id: if 'HTTP_X_USER_NAME' in sw_resp.environ: # keystone self.user_id = \ utf8encode("%s:%s" % (sw_resp.environ['HTTP_X_TENANT_NAME'], sw_resp.environ['HTTP_X_USER_NAME'])) else: # tempauth self.user_id = self.access_key success_codes = self._swift_success_codes(method, container, obj) error_codes = self._swift_error_codes(method, container, obj) if status in success_codes: return resp err_msg = resp.body if status in error_codes: err_resp = \ error_codes[sw_resp.status_int] # pylint: disable-msg=E1101 if isinstance(err_resp, tuple): raise err_resp[0](*err_resp[1:]) else: raise err_resp() if status == HTTP_BAD_REQUEST: raise BadSwiftRequest(err_msg) if status == HTTP_UNAUTHORIZED: raise SignatureDoesNotMatch() if status == HTTP_FORBIDDEN: raise AccessDenied() raise InternalError('unexpected status code %d' % status)
def _validate_headers(self): if 'CONTENT_LENGTH' in self.environ: try: if self.content_length < 0: raise InvalidArgument('Content-Length', self.content_length) except (ValueError, TypeError): raise InvalidArgument('Content-Length', self.environ['CONTENT_LENGTH']) if 'Date' in self.headers: now = datetime.datetime.utcnow() date = email.utils.parsedate(self.headers['Date']) if 'Expires' in self.params: try: d = email.utils.formatdate(float(self.params['Expires'])) except ValueError: raise AccessDenied() # check expiration expdate = email.utils.parsedate(d) ex = datetime.datetime(*expdate[0:6]) if now > ex: raise AccessDenied('Request has expired') elif date is not None: epoch = datetime.datetime(1970, 1, 1, 0, 0, 0, 0) d1 = datetime.datetime(*date[0:6]) if d1 < epoch: raise AccessDenied() # If the standard date is too far ahead or behind, it is an # error delta = datetime.timedelta(seconds=60 * 5) if abs(d1 - now) > delta: raise RequestTimeTooSkewed() else: raise AccessDenied() if 'Content-MD5' in self.headers: value = self.headers['Content-MD5'] if not re.match('^[A-Za-z0-9+/]+={0,2}$', value): # Non-base64-alphabet characters in value. raise InvalidDigest(content_md5=value) try: self.headers['ETag'] = value.decode('base64').encode('hex') except Exception: raise InvalidDigest(content_md5=value) if 'X-Amz-Copy-Source' in self.headers: try: check_path_header(self, 'X-Amz-Copy-Source', 2, '') except swob.HTTPException: msg = 'Copy Source must mention the source bucket and key: ' \ 'sourcebucket/sourcekey' raise InvalidArgument('x-amz-copy-source', self.headers['X-Amz-Copy-Source'], msg) if 'x-amz-metadata-directive' in self.headers: value = self.headers['x-amz-metadata-directive'] if value not in ('COPY', 'REPLACE'): err_msg = 'Unknown metadata directive.' raise InvalidArgument('x-amz-metadata-directive', value, err_msg) if 'x-amz-storage-class' in self.headers: # Only STANDARD is supported now. if self.headers['x-amz-storage-class'] != 'STANDARD': raise InvalidStorageClass() if 'x-amz-mfa' in self.headers: raise S3NotImplemented('MFA Delete is not supported.') if 'x-amz-server-side-encryption' in self.headers: raise S3NotImplemented('Server-side encryption is not supported.') if 'x-amz-website-redirect-location' in self.headers: raise S3NotImplemented('Website redirection is not supported.')
def POST(self, req): raise AccessDenied()