def test_with_challenge(challenge, expected_scope): expected_token = "expected_token" class Requests: count = 0 def send(request): Requests.count += 1 if Requests.count == 1: # first request should be unauthorized and have no content assert not request.body assert request.headers["Content-Length"] == "0" return challenge elif Requests.count == 2: # second request should be authorized according to challenge and have the expected content assert request.headers["Content-Length"] assert request.body == expected_content assert expected_token in request.headers["Authorization"] return Mock(status_code=200) raise ValueError("unexpected request") def get_token(*scopes): assert len(scopes) == 1 assert scopes[0] == expected_scope return AccessToken(expected_token, 0) credential = Mock(get_token=Mock(wraps=get_token)) pipeline = Pipeline(policies=[ChallengeAuthPolicy(credential=credential)], transport=Mock(send=send)) request = HttpRequest("POST", get_random_url()) request.set_bytes_body(expected_content) pipeline.run(request) assert credential.get_token.call_count == 1
def test_preserves_options_and_headers(): """After a challenge, the original request should be sent with its options and headers preserved. If a policy mutates the options or headers of the challenge (unauthorized) request, the options of the service request should be present when it is sent with authorization. """ url = get_random_url() token = "**" def get_token(*_, **__): return AccessToken(token, 0) credential = Mock(get_token=Mock(wraps=get_token)) transport = validating_transport( requests=[Request()] * 2 + [Request(required_headers={"Authorization": "Bearer " + token})], responses=[ mock_response( status_code=401, headers={"WWW-Authenticate": 'Bearer authorization="{}", resource=foo'.format(url)} ) ] + [mock_response()] * 2, ) challenge_policy = ChallengeAuthPolicy(credential=credential) policies = get_policies_for_request_mutation_test(challenge_policy) pipeline = Pipeline(policies=policies, transport=transport) response = pipeline.run(HttpRequest("GET", url)) # ensure the mock sans I/O policies were called for policy in policies: if hasattr(policy, "on_request"): assert policy.on_request.called, "mock policy wasn't invoked"
def test_enforces_tls(): url = "http://not.secure" HttpChallengeCache.set_challenge_for_url(url, HttpChallenge(url, "Bearer authorization=_, resource=_")) credential = Mock() pipeline = Pipeline(transport=Mock(), policies=[ChallengeAuthPolicy(credential)]) with pytest.raises(ServiceRequestError): pipeline.run(HttpRequest("GET", url))
def test_preserves_options_and_headers(): """After a challenge, the policy should send the original request with its options and headers preserved""" url = get_random_url() token = "**" def get_token(*_, **__): return AccessToken(token, 0) credential = Mock(get_token=Mock(wraps=get_token)) transport = validating_transport( requests=[Request()] * 2 + [Request(required_headers={"Authorization": "Bearer " + token})], responses=[ mock_response( status_code=401, headers={ "WWW-Authenticate": 'Bearer authorization="{}", resource=foo'.format(url) }) ] + [mock_response()] * 2, ) key = "foo" value = "bar" def add(request): # add the expected option and header request.context.options[key] = value request.http_request.headers[key] = value adder = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock(wraps=add), on_exception=lambda _: False) def verify(request): # authorized (non-challenge) requests should have the expected option and header if request.http_request.headers.get("Authorization"): assert request.context.options.get( key ) == value, "request option wasn't preserved across challenge" assert request.http_request.headers.get( key) == value, "headers wasn't preserved across challenge" verifier = Mock(spec=SansIOHTTPPolicy, on_request=Mock(wraps=verify)) challenge_policy = ChallengeAuthPolicy(credential=credential) policies = [adder, challenge_policy, verifier] pipeline = Pipeline(policies=policies, transport=transport) pipeline.run(HttpRequest("GET", url)) # ensure the mock sans I/O policies were called assert adder.on_request.called, "mock policy wasn't invoked" assert verifier.on_request.called, "mock policy wasn't invoked"
def test_policy_updates_cache(): """ It's possible for the challenge returned for a request to change, e.g. when a vault is moved to a new tenant. When the policy receives a 401, it should update the cached challenge for the requested URL, if one exists. """ url = get_random_url() first_scope = "https://first-scope" first_token = "first-scope-token" second_scope = "https://second-scope" second_token = "second-scope-token" challenge_fmt = 'Bearer authorization="https://login.authority.net/tenant", resource={}' # mocking a tenant change: # 1. first request -> respond with challenge # 2. second request should be authorized according to the challenge # 3. third request should match the second (using a cached access token) # 4. fourth request should also match the second -> respond with a new challenge # 5. fifth request should be authorized according to the new challenge # 6. sixth request should match the fifth transport = validating_transport( requests=( Request(url), Request(url, required_headers={"Authorization": "Bearer {}".format(first_token)}), Request(url, required_headers={"Authorization": "Bearer {}".format(first_token)}), Request(url, required_headers={"Authorization": "Bearer {}".format(first_token)}), Request(url, required_headers={"Authorization": "Bearer {}".format(second_token)}), Request(url, required_headers={"Authorization": "Bearer {}".format(second_token)}), ), responses=( mock_response(status_code=401, headers={"WWW-Authenticate": challenge_fmt.format(first_scope)}), mock_response(status_code=200), mock_response(status_code=200), mock_response(status_code=401, headers={"WWW-Authenticate": challenge_fmt.format(second_scope)}), mock_response(status_code=200), mock_response(status_code=200), ), ) credential = Mock(get_token=Mock(return_value=AccessToken(first_token, time.time() + 3600))) pipeline = Pipeline(policies=[ChallengeAuthPolicy(credential=credential)], transport=transport) # policy should complete and cache the first challenge and access token for _ in range(2): pipeline.run(HttpRequest("GET", url)) assert credential.get_token.call_count == 1 # The next request will receive a new challenge. The policy should handle it and update caches. credential.get_token.return_value = AccessToken(second_token, time.time() + 3600) for _ in range(2): pipeline.run(HttpRequest("GET", url)) assert credential.get_token.call_count == 2
def test_policy_updates_cache(): """ It's possible for the challenge returned for a request to change, e.g. when a vault is moved to a new tenant. When the policy receives a 401, it should update the cached challenge for the requested URL, if one exists. """ # ensure the test starts with an empty cache HttpChallengeCache.clear() url = "https://azure.service/path" first_scope = "https://first-scope" first_token = "first-scope-token" second_scope = "https://second-scope" second_token = "second-scope-token" challenge_fmt = 'Bearer authorization="https://login.authority.net/tenant", resource={}' # mocking a tenant change: # 1. first request -> respond with challenge # 2. second request should be authorized according to the challenge -> respond with success # 3. third request should match the second -> respond with a new challenge # 4. fourth request should be authorized according to the new challenge -> respond with success # 5. fifth request should match the fourth -> respond with success transport = validating_transport( requests=( Request(url), Request(url, required_headers={"Authorization": "Bearer {}".format(first_token)}), Request(url, required_headers={"Authorization": "Bearer {}".format(first_token)}), Request(url, required_headers={"Authorization": "Bearer {}".format(second_token)}), Request(url, required_headers={"Authorization": "Bearer {}".format(second_token)}), ), responses=( mock_response(status_code=401, headers={"WWW-Authenticate": challenge_fmt.format(first_scope)}), mock_response(status_code=200), mock_response(status_code=401, headers={"WWW-Authenticate": challenge_fmt.format(second_scope)}), mock_response(status_code=200), mock_response(status_code=200), ), ) tokens = (t for t in [first_token] * 2 + [second_token] * 2) credential = Mock(get_token=lambda _: AccessToken(next(tokens), 0)) pipeline = Pipeline(policies=[ChallengeAuthPolicy(credential=credential)], transport=transport) # policy should complete and cache the first challenge pipeline.run(HttpRequest("GET", url)) # The next request will receive a challenge. The policy should handle it and update the cache entry. pipeline.run(HttpRequest("GET", url))
def test_token_expiration(): """policy should not use a cached token which has expired""" url = get_random_url() expires_on = time.time() + 3600 first_token = "*" second_token = "**" token = AccessToken(first_token, expires_on) def get_token(*_, **__): return token credential = Mock(get_token=Mock(wraps=get_token)) transport = validating_transport( requests=[ Request(), Request( required_headers={"Authorization": "Bearer " + first_token}), Request( required_headers={"Authorization": "Bearer " + first_token}), Request( required_headers={"Authorization": "Bearer " + second_token}), ], responses=[ mock_response( status_code=401, headers={ "WWW-Authenticate": 'Bearer authorization="{}", resource=foo'.format(url) }) ] + [mock_response()] * 3, ) pipeline = Pipeline(policies=[ChallengeAuthPolicy(credential=credential)], transport=transport) for _ in range(2): pipeline.run(HttpRequest("GET", url)) assert credential.get_token.call_count == 1 token = AccessToken(second_token, time.time() + 3600) with patch("time.time", lambda: expires_on): pipeline.run(HttpRequest("GET", url)) assert credential.get_token.call_count == 2
def test_policy(): # ensure the test starts with an empty cache HttpChallengeCache.clear() expected_scope = "https://challenge.resource/.default" expected_token = "expected_token" challenge = Mock( status_code=401, headers={ "WWW-Authenticate": 'Bearer authorization="https://login.authority.net/tenant", resource={}'.format( expected_scope ) }, ) success = Mock(status_code=200) data = {"spam": "eggs"} responses = (r for r in (challenge, success)) def send(request): response = next(responses) if response is challenge: # this is the first request assert not request.body assert request.headers["Content-Length"] == "0" elif response is success: # this is the second request assert request.body == data assert expected_token in request.headers["Authorization"] return response def get_token(*scopes): assert len(scopes) is 1 assert scopes[0] == expected_scope return AccessToken(expected_token, 0) credential = Mock(get_token=Mock(wraps=get_token)) pipeline = Pipeline(policies=[ChallengeAuthPolicy(credential=credential)], transport=Mock(send=send)) pipeline.run(HttpRequest("POST", "https://azure.service", data=data)) assert credential.get_token.call_count == 1