Skip to content
This repository has been archived by the owner on Oct 14, 2024. It is now read-only.

Commit

Permalink
Merge branch 'main' into o11y
Browse files Browse the repository at this point in the history
  • Loading branch information
samwelkanda authored Sep 12, 2023
2 parents 5e6d90c + 3774272 commit 6cd0860
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 18 deletions.
101 changes: 101 additions & 0 deletions .github/policies/resourceManagement.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
id:
name: GitOps.PullRequestIssueManagement
description: GitOps.PullRequestIssueManagement primitive
owner:
resource: repository
disabled: false
where:
configuration:
resourceManagementConfiguration:
scheduledSearches:
- description:
frequencies:
- hourly:
hour: 6
filters:
- isIssue
- isOpen
- hasLabel:
label: 'Needs: Author Feedback'
- hasLabel:
label: 'Status: No Recent Activity'
- noActivitySince:
days: 3
actions:
- closeIssue
- description:
frequencies:
- hourly:
hour: 6
filters:
- isIssue
- isOpen
- hasLabel:
label: 'Needs: Author Feedback'
- noActivitySince:
days: 4
- isNotLabeledWith:
label: 'Status: No Recent Activity'
actions:
- addLabel:
label: 'Status: No Recent Activity'
- addReply:
reply: This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **4 days**. It will be closed if no further activity occurs **within 3 days of this comment**.
- description:
frequencies:
- hourly:
hour: 6
filters:
- isIssue
- isOpen
- hasLabel:
label: 'Resolution: Duplicate'
- noActivitySince:
days: 1
actions:
- addReply:
reply: This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes.
- closeIssue
eventResponderTasks:
- if:
- payloadType: Issue_Comment
- isAction:
action: Created
- isActivitySender:
issueAuthor: True
- hasLabel:
label: 'Needs: Author Feedback'
- isOpen
then:
- addLabel:
label: 'Needs: Attention :wave:'
- removeLabel:
label: 'Needs: Author Feedback'
description:
- if:
- payloadType: Issues
- not:
isAction:
action: Closed
- hasLabel:
label: 'Status: No Recent Activity'
then:
- removeLabel:
label: 'Status: No Recent Activity'
description:
- if:
- payloadType: Issue_Comment
- hasLabel:
label: 'Status: No Recent Activity'
then:
- removeLabel:
label: 'Status: No Recent Activity'
description:
- if:
- payloadType: Pull_Request
then:
- inPrLabel:
label: WIP
description:
onFailure:
onSuccess:
4 changes: 2 additions & 2 deletions .github/workflows/build_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down Expand Up @@ -48,7 +48,7 @@ jobs:
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Python 3.8
uses: actions/setup-python@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.6.0] - 2023-09-01

### Added

- Added support for continuous access evaluation.

### Changed

## [0.5.0] - 2023-07-27

### Added
Expand Down
2 changes: 1 addition & 1 deletion kiota_http/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION: str = '0.5.0'
VERSION: str = '0.6.0'
40 changes: 38 additions & 2 deletions kiota_http/httpx_request_adapter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""HTTPX client request adapter."""

import re
from collections.abc import AsyncIterable, Iterable
from datetime import datetime
from typing import Any, Dict, Generic, List, Optional, TypeVar, Union
from urllib import parse
Expand Down Expand Up @@ -56,6 +57,10 @@

class HttpxRequestAdapter(RequestAdapter, Generic[ModelType]):

CLAIMS_KEY = "claims"
BEARER_AUTHENTICATION_SCHEME = "Bearer"
RESPONSE_AUTH_HEADER = "WWW-Authenticate"

def __init__(
self,
authentication_provider: AuthenticationProvider,
Expand Down Expand Up @@ -490,8 +495,19 @@ async def get_http_response_message(
_get_http_resp_span = self._start_local_tracing_span(
"get_http_response_message", parent_span
)

async def get_http_response_message(
self, request_info: RequestInformation, claims: str = ""
) -> httpx.Response:
self.set_base_url_for_request_information(request_info)
await self._authentication_provider.authenticate_request(request_info)

additional_authentication_context = None
if claims:
additional_authentication_context = {self.CLAIMS_KEY: claims}

await self._authentication_provider.authenticate_request(
request_info, additional_authentication_context
)

request = self.get_request_from_request_information(
request_info, _get_http_resp_span, parent_span
Expand All @@ -507,6 +523,26 @@ async def get_http_response_message(
if content_type := resp.headers.get("Content-Type", None):
parent_span.set_attribute("http.response_content_type", content_type)
_get_http_resp_span.end()
return await self.retry_cae_response_if_required(resp, request_info, claims)

async def retry_cae_response_if_required(
self, resp: httpx.Response, request_info: RequestInformation, claims: str
) -> httpx.Response:
if (
resp.status_code == 401
and not claims # previous claims exist. Means request has already been retried
and resp.headers.get(self.RESPONSE_AUTH_HEADER)
):
auth_header_value = resp.headers.get(self.RESPONSE_AUTH_HEADER)
if auth_header_value.casefold().startswith(
self.BEARER_AUTHENTICATION_SCHEME.casefold()
):
claims_match = re.search('claims="(.+)"', auth_header_value)
if not claims_match:
raise ValueError("Unable to parse claims from response")
response_claims = claims_match.group().split('="')[1]
return await self.get_http_response_message(request_info, response_claims)
return resp
return resp

def get_response_handler(self, request_info: RequestInformation) -> Any:
Expand Down
14 changes: 6 additions & 8 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

astroid==2.15.6

asyncmock==0.4.2

certifi==2023.7.22

charset-normalizer==3.2.0

colorama==0.4.6

coverage[toml]==7.3.0
coverage[toml]==7.3.1

dill==0.3.7

Expand All @@ -34,19 +32,19 @@ mccabe==0.7.0

mock==5.1.0

mypy==1.5.0
mypy==1.5.1

mypy-extensions==1.0.0

packaging==23.1

platformdirs==3.10.0

pluggy==1.2.0
pluggy==1.3.0

pylint==2.17.5

pytest==7.4.0
pytest==7.4.2

pytest-asyncio==0.21.1

Expand Down Expand Up @@ -74,7 +72,7 @@ wrapt==1.15.0

yapf==0.40.1

anyio==3.7.1
anyio==4.0.0

h11==0.14.0

Expand All @@ -88,7 +86,7 @@ httpx[http2]==0.25.0

hyperframe==6.0.1

microsoft-kiota-abstractions==0.7.1
microsoft-kiota-abstractions==0.8.1

sniffio==1.3.0

Expand Down
25 changes: 22 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import httpx
import pytest
from asyncmock import AsyncMock
from unittest.mock import AsyncMock
from kiota_abstractions.api_error import APIError
from kiota_abstractions.method import Method
from kiota_abstractions.authentication import AnonymousAuthenticationProvider
from kiota_abstractions.request_information import RequestInformation
from opentelemetry import trace
Expand All @@ -25,7 +26,11 @@ def request_info():

@pytest.fixture
def request_info_mock():
return RequestInformation()
request_info = RequestInformation()
request_info.content = b'Hello World'
request_info.http_method = Method.GET
request_info.url_template = "https://example.com"
return request_info


@pytest.fixture
Expand Down Expand Up @@ -185,10 +190,24 @@ def mock_primitive_response_bytes(mocker):
def mock_no_content_response(mocker):
return httpx.Response(204, json="Radom JSON", headers={"Content-Type": "application/json"})


tracer = trace.get_tracer(__name__)


@pytest.fixture
def mock_otel_span():
return tracer.start_span("mock")

@pytest.fixture
def mock_cae_failure_response(mocker):
auth_header = """Bearer authorization_uri="https://login.windows.net/common/oauth2/authorize",
client_id="00000003-0000-0000-c000-000000000000",
error="insufficient_claims",
claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwNDEwNjY1MSJ9fX0="
"""
return httpx.Response(
401,
headers={
"Content-Type": "application/json",
"WWW-Authenticate": auth_header
}
)
14 changes: 13 additions & 1 deletion tests/test_httpx_request_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import httpx
import pytest
from asyncmock import AsyncMock
from unittest.mock import AsyncMock, call
from kiota_abstractions.api_error import APIError
from kiota_abstractions.method import Method
from kiota_abstractions.native_response_handler import NativeResponseHandler
Expand Down Expand Up @@ -284,3 +284,15 @@ async def test_observability(request_adapter, request_info, mock_user_response,
start_tracing_span.assert_called_once_with(request_info, "send_async")
assert final_result.display_name == mock_user.display_name
assert not trace.get_current_span().is_recording()

@pytest.mark.asyncio
async def test_retries_on_cae_failure(request_adapter, request_info_mock, mock_cae_failure_response):
request_adapter._http_client.send = AsyncMock(return_value=mock_cae_failure_response)
request_adapter._authentication_provider.authenticate_request = AsyncMock()
resp = await request_adapter.get_http_response_message(request_info_mock)
assert isinstance(resp, httpx.Response)
calls = [
call(request_info_mock, None),
call(request_info_mock, {'claims': 'eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwNDEwNjY1MSJ9fX0'})
]
request_adapter._authentication_provider.authenticate_request.assert_has_awaits(calls)

0 comments on commit 6cd0860

Please sign in to comment.