def _on_xpath(self, unused_env, url_config, unused_client_cert, data): """ Handles the authentication based on XPath expressions. """ if not data: return AuthResult(False, AUTH_XPATH_NO_DATA) request = etree.fromstring(data) prefix = 'xpath-' expressions = [url_config[header] for header in url_config if header.startswith(prefix)] if not expressions: # It's clearly an error. We've been requested to use XPath yet no # expressions have been defined in the config. raise SecWallException('No XPath expressions were found in the config') for expr in expressions: if not expr(request): return AuthResult(False, AUTH_XPATH_EXPR_MISMATCH) else: auth_result = AuthResult(True, '0') auth_result.auth_info = map(str, expressions) return auth_result
def on_ssl_cert(url_config, client_cert, field_prefix, needs_auth_info=True): """ Visit _RequestApp._on_ssl_cert method's docstring. """ if client_cert: config_fields = {} for field, value in url_config.items(): if field.startswith(field_prefix): config_fields[field.split(field_prefix)[1]] = value # There are no fields so the user just wants the connection be # encrypted and the client use client certificate however they're # not interested in the cert's fields - so as long as the CA is # OK (and we know it is because otherwise we wouldn't have gotten # so far), we let the client in. if not config_fields: return True else: subject = client_cert.get('subject') if not subject: return AuthResult(False, AUTH_CERT_NO_SUBJECT) cert_fields = dict((elem[0][0].encode('utf-8'), elem[0][1].encode('utf-8')) for elem in subject) for config_field, config_value in config_fields.items(): cert_value = cert_fields.get(config_field) if not cert_value: return AuthResult(False, AUTH_CERT_NO_VALUE) if cert_value != config_value: return AuthResult(False, AUTH_CERT_VALUE_MISMATCH) else: auth_result = AuthResult(True, '0') if needs_auth_info: auth_result.auth_info = dict((quote_plus(k), quote_plus(v)) for k, v in cert_fields.iteritems()) return auth_result
def _on_custom_http(self, env, url_config, *ignored): """ Handles the authentication based on custom HTTP headers. """ prefix = 'custom-http-' expected_headers = {} expected_headers_keys = (header for header in url_config if header.startswith(prefix)) for key in expected_headers_keys: # This set of operations (.split, .upper, .replace) could be done once # when the config's read, well, it's a room for improvement. expected_headers[str(key)] = str(env.get('HTTP_' + key.split(prefix)[1].upper().replace('-', '_'), '')) if not expected_headers: # It's clearly an error. We've been requested to use custom HTTP # headers but none are in the config. raise SecWallException('No custom HTTP headers were found in the config') for key, value in expected_headers.iteritems(): if not value: return AuthResult(False, AUTH_CUSTOM_HTTP_NO_HEADER) if value != url_config[key]: return AuthResult(False, AUTH_CUSTOM_HTTP_HEADER_MISMATCH) else: auth_result = AuthResult(True, '0') auth_result.auth_info = expected_headers return auth_result
def test_auth_result_nonzero(): """ Tests AuthResult in boolean contexts. """ # It's False by default. a1 = AuthResult() eq_(False, bool(a1)) a2 = AuthResult(True) eq_(True, bool(a2))
def on_wsse_pwd(wsse, url_config, data, needs_auth_info=True): """ Visit _RequestApp._on_wsse_pwd method's docstring. """ if not data: return AuthResult(False, AUTH_WSSE_NO_DATA) request = etree.fromstring(data) try: ok, wsse_username = wsse.validate(request, url_config) except SecurityException, e: return AuthResult(False, AUTH_WSSE_VALIDATION_ERROR, e.description)
def test_auth_result_properties(): """ Tests that AuthResult's properties can be read correctly. """ # Check the defaults first. a1 = AuthResult() eq_(False, a1.status) eq_('-1', a1.code) eq_('', a1.description) status, code, description = [uuid4().hex for x in range(3)] a2 = AuthResult(status, code, description) eq_(status, a2.status) eq_(code, a2.code) eq_(description, a2.description)
def on_basic_auth(env, url_config, needs_auth_info=True): """ Visit _RequestApp._on_basic_auth method's docstring. """ username = url_config['basic-auth-username'] result = _on_basic_auth(env.get('HTTP_AUTHORIZATION', ''), username, url_config['basic-auth-password']) is_success = result is True # Yes, need to check for True auth_result = AuthResult(is_success) if is_success: if needs_auth_info: auth_result.auth_info = {b'basic-auth-username': quote_plus(username).encode('utf-8')} else: auth_result.code = result return auth_result
def test_auth_result_repr(): """ Tests the AuthResult's __repr__ output. """ at_pattern = '\w*' status, code, description = [uuid4().hex for x in range(3)] auth_info = {b'abc': b'def'} a1 = AuthResult(status, code, description) a1.auth_info = auth_info r = repr(a1) pattern = '<AuthResult at {0} status={1} code={2} description={3} auth_info={{abc: def}}\n>' pattern = pattern.format(at_pattern, status, code, description) regexp = re.compile(pattern) assert_true(regexp.match(r) is not None, (pattern, r))
def test_auth_result_repr(): """ Tests the AuthResult's __repr__ output. """ at_pattern = '\w*' status, code, description = [uuid4().hex for x in range(3)] auth_info = {b'abc':b'def'} a1 = AuthResult(status, code, description) a1.auth_info = auth_info r = repr(a1) pattern = '<AuthResult at {0} status={1} code={2} description={3} auth_info={{abc: def}}\n>' pattern = pattern.format(at_pattern, status, code, description) regexp = re.compile(pattern) assert_true(regexp.match(r) is not None, (pattern, r))
def _on_digest_auth(self, env, url_config, *ignored): """ Handles HTTP Digest Authentication. """ auth = env.get('HTTP_AUTHORIZATION') if not auth: return AuthResult(False, AUTH_DIGEST_NO_AUTH) auth = self._parse_digest_auth(auth) expected_username = url_config['digest-auth-username'] expected_password = url_config['digest-auth-password'] expected_realm = url_config['digest-auth-realm'] if auth['username'] != expected_username: return AuthResult(False, AUTH_DIGEST_USERNAME_MISMATCH) if auth['realm'] != expected_realm: return AuthResult(False, AUTH_DIGEST_REALM_MISMATCH) if env.get('QUERY_STRING'): expected_uri = '{0}?{1}'.format(env['PATH_INFO'], env['QUERY_STRING']) else: expected_uri = env['PATH_INFO'] if auth['uri'] != expected_uri: return AuthResult(False, AUTH_DIGEST_URI_MISMATCH) expected_response = self._compute_digest_auth_response(expected_username, expected_realm, expected_password, expected_uri, env['REQUEST_METHOD'], auth['nonce']) if auth['response'] == expected_response: auth_result = AuthResult(True, '0') auth_result._auth_info = {b'digest-auth-username':quote_plus(expected_username), b'digest-auth-realm':quote_plus(expected_realm)} return auth_result else: return AuthResult(False, AUTH_DIGEST_RESPONSE_MISMATCH)
def _on_ssl_cert(self, env, url_config, client_cert, data): """ Validates the client SSL/TLS certificates, its very existence and the values of its fields (commonName, organizationName etc.) """ if client_cert: field_prefix = 'ssl-cert-' config_fields = {} for field, value in url_config.items(): if field.startswith(field_prefix): config_fields[field.split(field_prefix)[1]] = value # There are no fields so the user just wants the connection be # encrypted and the client use client certificate however they're # not interested in the cert's fields - so as long as the CA is # OK (and we know it is because otherwise we wouldn't have gotten # so far), we let the client in. if not config_fields: return True else: subject = client_cert.get('subject') if not subject: return AuthResult(False, AUTH_CERT_NO_SUBJECT) cert_fields = dict(elem[0] for elem in subject) for config_field, config_value in config_fields.items(): cert_value = cert_fields.get(config_field) if not cert_value: return AuthResult(False, AUTH_CERT_NO_VALUE) if cert_value != config_value: return AuthResult(False, AUTH_CERT_VALUE_MISMATCH) else: auth_result = AuthResult(True, '0') auth_result.auth_info = dict((quote_plus(k), quote_plus(v)) for k, v in cert_fields.iteritems()) return auth_result
def _on_basic_auth(self, env, url_config, *ignored): """ Handles HTTP Basic Authentication. """ auth = env.get('HTTP_AUTHORIZATION') if not auth: return AuthResult(False, AUTH_BASIC_NO_AUTH) prefix = 'Basic ' if not auth.startswith(prefix): return AuthResult(False, AUTH_BASIC_INVALID_PREFIX) _, auth = auth.split(prefix) auth = auth.strip().decode('base64') username, password = auth.split(':', 1) if username == url_config['basic-auth-username'] and \ password == url_config['basic-auth-password']: auth_result = AuthResult(True, '0') auth_result.auth_info = {b'basic-auth-username': quote_plus(username)} return auth_result else: return AuthResult(False, AUTH_BASIC_USERNAME_OR_PASSWORD_MISMATCH)
def __call__(self, env, start_response, client_cert=None, client_cert_der=None): """ Finds the configuration for the given URL and passes the control on to the main request handler. In case no config for the given URL is found, a 404 Not Found will be returned to the calling side. """ ctx = InvocationContext(self.instance_name, self.instance_unique, next(self.msg_counter), self.now()) ctx.auth_result = AuthResult() ctx.env = env ctx.client_cert = client_cert ctx.client_cert_der = client_cert_der if self.sign_invocation_id: h = hashlib.sha256() h.update('{0}:{1}'.format(self.instance_secret, ctx.invocation_id)) ctx.invocation_id_signed = h.hexdigest() path_info = env['PATH_INFO'] if self.quote_path_info: path_info = quote_plus(path_info) query_string = env.get('QUERY_STRING') if query_string: query_string = '?' + query_string if self.quote_query_string: query_string = quote_plus(query_string) ctx.path_info = path_info ctx.query_string = query_string ctx.remote_address = env.get('REMOTE_ADDR') ctx.request_method = env.get('REQUEST_METHOD') for c, url_config in self.urls: match = c.test(path_info) if match: ctx.url_config = url_config return self._on_request(ctx, start_response, env, url_config, client_cert, match) else: # No config for that URL, we can't let the client in. return self._404(ctx, start_response)
def _on_digest_auth(self, env, url_config, *ignored): """ Handles HTTP Digest Authentication. """ auth = env.get('HTTP_AUTHORIZATION') if not auth: return AuthResult(False, AUTH_DIGEST_NO_AUTH) auth = self._parse_digest_auth(auth) expected_username = url_config['digest-auth-username'] expected_password = url_config['digest-auth-password'] expected_realm = url_config['digest-auth-realm'] if auth['username'] != expected_username: return AuthResult(False, AUTH_DIGEST_USERNAME_MISMATCH) if auth['realm'] != expected_realm: return AuthResult(False, AUTH_DIGEST_REALM_MISMATCH) if env.get('QUERY_STRING'): expected_uri = '{0}?{1}'.format(env['PATH_INFO'], env['QUERY_STRING']) else: expected_uri = env['PATH_INFO'] if auth['uri'] != expected_uri: return AuthResult(False, AUTH_DIGEST_URI_MISMATCH) expected_response = self._compute_digest_auth_response( expected_username, expected_realm, expected_password, expected_uri, env['REQUEST_METHOD'], auth['nonce']) if auth['response'] == expected_response: auth_result = AuthResult(True, '0') auth_result._auth_info = { b'digest-auth-username': quote_plus(expected_username), b'digest-auth-realm': quote_plus(expected_realm) } return auth_result else: return AuthResult(False, AUTH_DIGEST_RESPONSE_MISMATCH)
def test_invocation_context_format_log_message(): """ Tests the correctness of formatting of logging messages. """ _auth1 = AuthResult(True) _auth2 = AuthResult(False, uuid4().hex) for _auth_result in _auth1, _auth2: for _needs_details in True, False: _now = datetime.now() _start_to_ext_start = timedelta(seconds=1, microseconds=129) _ext_took = timedelta(seconds=3, microseconds=9017) _ext_end_to_proc_end = timedelta(seconds=7, microseconds=3511) _proc_start = _now _proc_end = _now + _start_to_ext_start + _ext_took + _ext_end_to_proc_end _ext_start = _now + _start_to_ext_start _ext_end = _now + _start_to_ext_start + _ext_took _env = { 'HTTP_USER_AGENT': uuid4().hex, 'SERVER_SOFTWARE': uuid4().hex, 'SERVER_NAME': uuid4().hex, 'SERVER_PORT': uuid4().hex } _code = uuid4().hex (_instance_name, _instance_unique, _message_number, _url_config, _client_cert, _data, _remote_address, _config_type, _path_info, _query_string, _client_address, _request_method) = [uuid4().hex for x in range(12)] ctx = InvocationContext(_instance_name, _instance_unique, _message_number, _proc_start, _proc_end, _ext_start, _ext_end, _env, _url_config, _client_cert, _data, _remote_address, _auth_result, _config_type, _path_info, _query_string, _client_address, _request_method) msg = ctx.format_log_message(_code, _needs_details) if _needs_details: (invocation_id, code, proc_start, remote_address, req_info, secwall_overhead, ext_overhead, proc_total, auth_result, auth_code, http_user_agent, server_software, server_name, server_port, config_type, data) = msg.split(';') else: (invocation_id, code, proc_start, remote_address, req_info, secwall_overhead, ext_overhead, proc_total, auth_result, auth_code) = msg.split(';') eq_(invocation_id, ctx.invocation_id) eq_(code, _code) eq_(proc_start, str(_proc_start)) eq_(remote_address, _remote_address) eq_(req_info, _request_method + ' ' + _path_info + _query_string) _proc_total = _proc_end - _proc_start _ext_overhead = _ext_end - _ext_start _secwall_overhead = _proc_total - _ext_overhead eq_( proc_total, str(_proc_total.seconds) + '.' + str(_proc_total.microseconds).zfill(6)) eq_( ext_overhead, str(_ext_overhead.seconds) + '.' + str(_ext_overhead.microseconds).zfill(6)) eq_( secwall_overhead, str(_secwall_overhead.seconds) + '.' + str(_secwall_overhead.microseconds).zfill(6)) if _auth_result: eq_(auth_result, '0') else: eq_(auth_result, '1') eq_(auth_code, _auth_result.code) if _needs_details: eq_(http_user_agent, '"{0}"'.format(_env.get('HTTP_USER_AGENT'))) eq_(server_software, _env.get('SERVER_SOFTWARE')) eq_(server_name, _env.get('SERVER_NAME')) eq_(server_port, _env.get('SERVER_PORT')) eq_(config_type, _config_type) eq_(data, _data)
return auth_result def on_wsse_pwd(wsse, url_config, data, needs_auth_info=True): """ Visit _RequestApp._on_wsse_pwd method's docstring. """ if not data: return AuthResult(False, AUTH_WSSE_NO_DATA) request = etree.fromstring(data) try: ok, wsse_username = wsse.validate(request, url_config) except SecurityException, e: return AuthResult(False, AUTH_WSSE_VALIDATION_ERROR, e.description) else: auth_result = AuthResult(True, '0') if needs_auth_info: auth_result.auth_info = {b'wsse-pwd-username': str(wsse_username)} return auth_result def _on_basic_auth(auth, expected_username, expected_password): """ A low-level call for checking the HTTP Basic Auth credentials. """ if not auth: return AUTH_BASIC_NO_AUTH prefix = 'Basic ' if not auth.startswith(prefix): return AUTH_BASIC_INVALID_PREFIX
def _on_request(self, ctx, start_response, env, url_config, client_cert, match=None): """ Checks security, invokes the backend server, returns the response. """ # If True, we know we're being accessed using SSL. has_ssl = False # Some quick SSL-related checks first. if url_config.get('ssl'): # Has the URL been accessed through SSL/TLS? if env.get('wsgi.url_scheme') != 'https': return self._403(ctx, start_response) # OK, we're now sure we're being invoked through SSL. has_ssl = True # Is the client cert required? if url_config.get('ssl-cert') and not client_cert: return self._401(ctx, start_response, self._get_www_auth(url_config, 'ssl-cert')) data = env['wsgi.input'].read() data = data if data else None ctx.data = data # ssl-wrap-only implies 'ssl':True but everyone's free to forget about # setting it so we may wind up ostensibly using SSL yet in reality # we'd be using plain HTTP. That's why there's an additional check # for 'has_ssl' below. if url_config.get('ssl-wrap-only'): if not has_ssl: return self._403(ctx, start_response) else: # There will be no authentication performed and we need to fill # in the information ourselves here so that what goes to logs # doesn't use the ctx's defaults. ctx.auth_result = AuthResult(True, '0') else: for config_type in self.config.validation_precedence: if config_type in url_config: handler = getattr(self, '_on_' + config_type.replace('-', '_')) auth_result = handler(env, url_config, client_cert, data) ctx.auth_result = auth_result ctx.config_type = config_type if not auth_result: return self._401(ctx, start_response, self._get_www_auth(url_config, config_type)) break else: return self._500(ctx, start_response) rewrite = url_config.get('rewrite') if rewrite: path_info = rewrite.format(**match[1]) else: path_info = env['PATH_INFO'] req = urllib2.Request(url_config['host'] + path_info, data) from_client_ignore = url_config['from-client-ignore'] to_backend_add = url_config['to-backend-add'] # Pass the headers to the backend server unless they're to be ignored. for name in (name for name in env if name.startswith('HTTP_')): value = env[name] name = name.split('HTTP_')[1].replace('_', '-') if not name in from_client_ignore: req.add_header(name, value) # Custom headers to be sent to the backend server. for name, value in to_backend_add.iteritems(): req.add_header(name, value) # Sign and return the invocation ID. if self.add_invocation_id: req.add_header('X-sec-wall-invocation-id', ctx.invocation_id) if self.sign_invocation_id: req.add_header('X-sec-wall-invocation-id-signed', ctx.invocation_id_signed) if url_config.get('add-auth-info', True): req.add_header('X-sec-wall-auth-info', ctx.auth_result.auth_info.strip()) if url_config.get('sign-auth-info', True): h = hashlib.sha256() h.update('{0}:{1}:{2}'.format(ctx.invocation_id, self.instance_secret, ctx.auth_result.auth_info.strip())) req.add_header('X-sec-wall-auth-info-signed', h.hexdigest()) try: opener = urllib2.build_opener() ctx.ext_start = self.now() resp = opener.open(req) except urllib2.HTTPError, e: resp = e