def _do_test_basic_auth(self, creds): with temporary_dir() as tmpcookiedir: cookie_file = os.path.join(tmpcookiedir, 'pants.test.cookies') self.context(for_subsystems=[BasicAuth, Cookies], options={ BasicAuth.options_scope: { 'providers': { 'foobar': { 'url': 'http://localhost:{}'.format( self.port) } } }, Cookies.options_scope: { 'path': cookie_file } }) basic_auth = BasicAuth.global_instance() cookies = Cookies.global_instance() self.assertListEqual([], list(cookies.get_cookie_jar())) basic_auth.authenticate(provider='foobar', creds=creds, cookies=cookies) cookies_list = list(cookies.get_cookie_jar()) self.assertEqual(1, len(cookies_list)) auth_cookie = cookies_list[0] self.assertEqual('test_auth_key', auth_cookie.name) self.assertEqual('test_auth_value', auth_cookie.value)
def test_basic_auth_with_bad_creds(self): self._do_test_basic_auth(creds=BasicAuthCreds('test_user', 'test_password')) basic_auth = BasicAuth.global_instance() cookies = Cookies.global_instance() bad_creds = BasicAuthCreds('test_user', 'bad_password') self.assertRaises(BasicAuthException, lambda: basic_auth.authenticate(provider='foobar', creds=bad_creds, cookies=cookies))
def test_basic_auth_with_bad_creds(self): self._do_test_basic_auth( creds=BasicAuthCreds('test_user', 'test_password')) basic_auth = BasicAuth.global_instance() cookies = Cookies.global_instance() bad_creds = BasicAuthCreds('test_user', 'bad_password') self.assertRaises( BasicAuthException, lambda: basic_auth.authenticate( provider='foobar', creds=bad_creds, cookies=cookies))
def _do_test_basic_auth(self, creds): with self._test_options(): basic_auth = BasicAuth.global_instance() cookies = Cookies.global_instance() self.assertListEqual([], list(cookies.get_cookie_jar())) basic_auth.authenticate(provider='foobar', creds=creds, cookies=cookies) cookies_list = list(cookies.get_cookie_jar()) self.assertEqual(1, len(cookies_list)) auth_cookie = cookies_list[0] self.assertEqual('test_auth_key', auth_cookie.name) self.assertEqual('test_auth_value', auth_cookie.value)
def post_stats(cls, stats_url, stats, timeout=2, auth_provider=None): """POST stats to the given url. :return: True if upload was successful, False otherwise. """ def error(msg): # Report aleady closed, so just print error. print('WARNING: Failed to upload stats to {}. due to {}'.format( stats_url, msg), file=sys.stderr) return False # TODO(benjy): The upload protocol currently requires separate top-level params, with JSON # values. Probably better for there to be one top-level JSON value, namely json.dumps(stats). # But this will first require changing the upload receiver at every shop that uses this. params = {k: json.dumps(v) for (k, v) in stats.items()} cookies = Cookies.global_instance() auth_provider = auth_provider or '<provider>' # We can't simply let requests handle redirects, as we only allow them for specific codes: # 307 and 308 indicate that the redirected request must use the same method, POST in this case. # So they indicate a true redirect of the POST itself, and we allow them. # The other redirect codes either must, or in practice do, cause the user agent to switch the # method to GET. So when they are encountered on a POST, it indicates an auth problem (a # redirection to a login page). def do_post(url, num_redirects_allowed): if num_redirects_allowed < 0: return error('too many redirects.') r = requests.post(url, data=params, timeout=timeout, cookies=cookies.get_cookie_jar(), allow_redirects=False) if r.status_code in {307, 308}: return do_post(r.headers['location'], num_redirects_allowed - 1) elif r.status_code != 200: error('HTTP error code: {}. Reason: {}.'.format( r.status_code, r.reason)) if 300 <= r.status_code < 400 or r.status_code == 401: print( 'Use `path/to/pants login --to={}` to authenticate against the stats ' 'upload service.'.format(auth_provider), file=sys.stderr) return False return True try: return do_post(stats_url, num_redirects_allowed=6) except Exception as e: # Broad catch - we don't want to fail the build over upload errors. return error('Error: {}'.format(e))
def authenticate( self, provider: str, creds: Optional[BasicAuthCreds] = None, cookies: Optional[Cookies] = None, ) -> None: """Authenticate against the specified provider. :param provider: Authorize against this provider. :param creds: The creds to use. If unspecified, assumes that creds are set in the netrc file. :param cookies: Store the auth cookies in this instance. If unspecified, uses the global instance. :raises pants.auth.basic_auth.BasicAuthException: If auth fails due to misconfiguration or rejection by the server. """ cookies = cookies or Cookies.global_instance() if not provider: raise BasicAuthException("No basic auth provider specified.") provider_config = self.options.providers.get(provider) if not provider_config: raise BasicAuthException( f"No config found for provider {provider}.") url = provider_config.get("url") if not url: raise BasicAuthException( f"No url found in config for provider {provider}.") if not self.options.allow_insecure_urls and not url.startswith( "https://"): raise BasicAuthException( f"Auth url for provider {provider} is not secure: {url}.") auth = requests.auth.HTTPBasicAuth(creds.username, creds.password) if creds else None response = requests.get(url, auth=auth, headers={"User-Agent": f"pants/v{VERSION}"}) if response.status_code != requests.codes.ok: if response.status_code == requests.codes.unauthorized: parsed = www_authenticate.parse( response.headers.get("WWW-Authenticate", "")) if "Basic" in parsed: raise Challenged(url, response.status_code, response.reason, parsed["Basic"]["realm"]) raise BasicAuthException(url, response.status_code, response.reason) cookies.update(response.cookies)
def _do_test_basic_auth(self, creds): with self._test_options(): basic_auth = BasicAuth.global_instance() cookies = Cookies.global_instance() self.assertListEqual([], list(cookies.get_cookie_jar())) basic_auth.authenticate(provider="foobar", creds=creds, cookies=cookies) cookies_list = list(cookies.get_cookie_jar()) self.assertEqual(1, len(cookies_list)) auth_cookie = cookies_list[0] self.assertEqual("test_auth_key", auth_cookie.name) self.assertEqual("test_auth_value", auth_cookie.value)
def authenticate(self, provider, creds=None, cookies=None): """Authenticate against the specified provider. :param str provider: Authorize against this provider. :param pants.auth.basic_auth.BasicAuthCreds creds: The creds to use. If unspecified, assumes that creds are set in the netrc file. :param pants.auth.cookies.Cookies cookies: Store the auth cookies in this instance. If unspecified, uses the global instance. :raises pants.auth.basic_auth.BasicAuthException: If auth fails due to misconfiguration or rejection by the server. """ cookies = cookies or Cookies.global_instance() if not provider: raise BasicAuthException('No basic auth provider specified.') provider_config = self.get_options().providers.get(provider) if not provider_config: raise BasicAuthException( 'No config found for provider {}.'.format(provider)) url = provider_config.get('url') if not url: raise BasicAuthException( 'No url found in config for provider {}.'.format(provider)) if not self.get_options().allow_insecure_urls and not url.startswith( 'https://'): raise BasicAuthException( 'Auth url for provider {} is not secure: {}.'.format( provider, url)) if creds: auth = requests.auth.HTTPBasicAuth(creds.username, creds.password) else: auth = None # requests will use the netrc creds. response = requests.get(url, auth=auth) if response.status_code != requests.codes.ok: if response.status_code == requests.codes.unauthorized: parsed = www_authenticate.parse( response.headers.get('WWW-Authenticate', '')) if 'Basic' in parsed: raise Challenged(url, response.status_code, response.reason, parsed['Basic']['realm']) raise BasicAuthException(url, response.status_code, response.reason) cookies.update(response.cookies)
def test_cookie_file_permissions(self): with temporary_dir() as tmpcookiedir: cookie_file = os.path.join(tmpcookiedir, "pants.test.cookies") self.context( for_subsystems=[Cookies], options={Cookies.options_scope: { "path": cookie_file }}) cookies = Cookies.global_instance() self.assertFalse(os.path.exists(cookie_file)) cookies.update([]) self.assertTrue(os.path.exists(cookie_file)) file_permissions = os.stat(cookie_file).st_mode self.assertEqual(int("0100600", 8), file_permissions)
def test_cookie_file_permissions(self): with temporary_dir() as tmpcookiedir: cookie_file = os.path.join(tmpcookiedir, 'pants.test.cookies') self.context(for_subsystems=[Cookies], options={ Cookies.options_scope: { 'path': cookie_file } }) cookies = Cookies.global_instance() self.assertFalse(os.path.exists(cookie_file)) cookies.update([]) self.assertTrue(os.path.exists(cookie_file)) file_permissions = os.stat(cookie_file).st_mode self.assertEqual(int('0100600', 8), file_permissions)
def post_stats(cls, stats_url, stats, timeout=2, auth_provider=None): """POST stats to the given url. :return: True if upload was successful, False otherwise. """ def error(msg): # Report aleady closed, so just print error. print('WARNING: Failed to upload stats to {}. due to {}'.format(stats_url, msg), file=sys.stderr) return False # TODO(benjy): The upload protocol currently requires separate top-level params, with JSON # values. Probably better for there to be one top-level JSON value, namely json.dumps(stats). # But this will first require changing the upload receiver at every shop that uses this. params = {k: cls._json_dump_options(v) for (k, v) in stats.items()} cookies = Cookies.global_instance() auth_provider = auth_provider or '<provider>' # We can't simply let requests handle redirects, as we only allow them for specific codes: # 307 and 308 indicate that the redirected request must use the same method, POST in this case. # So they indicate a true redirect of the POST itself, and we allow them. # The other redirect codes either must, or in practice do, cause the user agent to switch the # method to GET. So when they are encountered on a POST, it indicates an auth problem (a # redirection to a login page). def do_post(url, num_redirects_allowed): if num_redirects_allowed < 0: return error('too many redirects.') r = requests.post(url, data=params, timeout=timeout, cookies=cookies.get_cookie_jar(), allow_redirects=False) if r.status_code in {307, 308}: return do_post(r.headers['location'], num_redirects_allowed - 1) elif r.status_code != 200: error('HTTP error code: {}. Reason: {}.'.format(r.status_code, r.reason)) if 300 <= r.status_code < 400 or r.status_code == 401: print('Use `path/to/pants login --to={}` to authenticate against the stats ' 'upload service.'.format(auth_provider), file=sys.stderr) return False return True try: return do_post(stats_url, num_redirects_allowed=6) except Exception as e: # Broad catch - we don't want to fail the build over upload errors. return error('Error: {}'.format(e))
def authenticate(self, provider, creds=None, cookies=None): """Authenticate against the specified provider. :param str provider: Authorize against this provider. :param pants.auth.basic_auth.BasicAuthCreds creds: The creds to use. If unspecified, assumes that creds are set in the netrc file. :param pants.auth.cookies.Cookies cookies: Store the auth cookies in this instance. If unspecified, uses the global instance. :raises pants.auth.basic_auth.BasicAuthException: If auth fails due to misconfiguration or rejection by the server. """ cookies = cookies or Cookies.global_instance() if not provider: raise BasicAuthException('No basic auth provider specified.') provider_config = self.get_options().providers.get(provider) if not provider_config: raise BasicAuthException( 'No config found for provider {}.'.format(provider)) url = provider_config.get('url') if not url: raise BasicAuthException( 'No url found in config for provider {}.'.format( provider_config)) # TODO: Require url to be https, except when testing. See # https://github.com/pantsbuild/pants/issues/6496. if creds: auth = requests.auth.HTTPBasicAuth(creds.username, creds.password) else: auth = None # requests will use the netrc creds. response = requests.get(url, auth=auth) if response.status_code != requests.codes.ok: raise BasicAuthException( 'Failed to auth against {}. Status code {}.'.format( response, response.status_code)) cookies.update(response.cookies)
def post_stats(cls, url, stats, timeout=2): """POST stats to the given url. :return: True if upload was successful, False otherwise. """ def error(msg): # Report aleady closed, so just print error. print('WARNING: Failed to upload stats to {} due to {}'.format(url, msg), file=sys.stderr) return False # TODO(benjy): The upload protocol currently requires separate top-level params, with JSON # values. Probably better for there to be one top-level JSON value, namely json.dumps(stats). # But this will first require changing the upload receiver at every shop that uses this # (probably only Foursquare at present). params = {k: json.dumps(v) for (k, v) in stats.items()} cookies = Cookies.global_instance() try: r = requests.post(url, data=params, timeout=timeout, cookies=cookies.get_cookie_jar()) if r.status_code != requests.codes.ok: return error("HTTP error code: {}".format(r.status_code)) except Exception as e: # Broad catch - we don't want to fail the build over upload errors. return error("Error: {}".format(e)) return True
def get_auth_for_provider(self, auth_provider: str) -> Authentication: cookies = Cookies.global_instance() return Authentication( headers={}, request_args={'cookies': cookies.get_cookie_jar()})