-
Notifications
You must be signed in to change notification settings - Fork 3k
[OPS Common SDK] Update Communication Common SDK for Teams Phone Extensibility GA #41219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
cemateia
wants to merge
34
commits into
main
Choose a base branch
from
py-ops-updates
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
41d2254
Update models.py
cemateia 2ff1cb7
Update models.py
cemateia 16baeaf
Update models.py
cemateia 35c1138
Update phone number identifier
cemateia fbb7f25
add tests
cemateia 94509ef
small updates
cemateia 2e38ae9
Update test_identifier_raw_id.py
cemateia f25083c
Update test_identifier_raw_id.py
cemateia b273592
Update test_identifier_raw_id.py
cemateia 8364393
Create entra_user_credential_async.py
cemateia 5486112
add entra token cred
cemateia 0c3dbd7
add async exchange
cemateia 8a3e58c
Update models.py
cemateia a2db6c1
Update test_identifier_raw_id.py
cemateia 40eaff6
Update test_identifier_raw_id.py
cemateia 295005b
update credential
cemateia 72ff99e
Update entra_user_credential_async.py
cemateia 7325a46
Update entra_user_credential_async.py
cemateia bd8de3e
Update entra_user_credential_async.py
cemateia 5c612ef
Update entra_user_credential_async.py
cemateia cc7158a
Create pipeline_utils.py
cemateia d84645b
Create entra_token_guard_policy.py
cemateia 0fc8830
Create entra_token_credential_options.py
cemateia 61f39c5
Create token_exchange.py
cemateia 5bf5d3e
Update token_exchange.py
cemateia 58c938f
updates
cemateia 5e5f0ae
Update token_exchange.py
cemateia 1b31ec0
Update token_exchange.py
cemateia 409de87
cleanup old files
cemateia 7cb15d5
Update token_exchange.py
cemateia d05f146
Update entra_token_guard_policy.py
cemateia bd6d110
updated credential classes
cemateia 158e856
updates
cemateia b8d7f73
Delete manual_test.py
cemateia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
...e-communication-identity/azure/communication/identity/_shared/entra_token_guard_policy.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# ------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for | ||
# license information. | ||
# -------------------------------------------------------------------------- | ||
|
||
import json | ||
from datetime import datetime, timezone | ||
from azure.core.pipeline.policies import HTTPPolicy, AsyncHTTPPolicy | ||
from azure.core.pipeline import PipelineRequest | ||
|
||
from dateutil import parser as dateutil_parser | ||
|
||
class EntraTokenGuardPolicy(HTTPPolicy): | ||
"""A pipeline policy that caches the response for a given Entra token and reuses it if valid.""" | ||
|
||
def __init__(self): | ||
super().__init__() | ||
self._entra_token_cache = None | ||
self._response_cache = None | ||
|
||
def send(self, request: PipelineRequest): | ||
cache_valid, token = _EntraTokenGuardUtils.is_entra_token_cache_valid(self._entra_token_cache, request) | ||
if cache_valid and _EntraTokenGuardUtils.is_acs_token_cache_valid(self._response_cache): | ||
response = self._response_cache | ||
else: | ||
self._entra_token_cache = token | ||
response = self.next.send(request) | ||
self._response_cache = response | ||
if response is None: | ||
raise RuntimeError("Failed to obtain a valid PipelineResponse in AsyncEntraTokenGuardPolicy.send") | ||
return response | ||
|
||
|
||
|
||
class AsyncEntraTokenGuardPolicy(AsyncHTTPPolicy): | ||
"""Async pipeline policy that caches the response for a given Entra token and reuses it if valid.""" | ||
|
||
def __init__(self): | ||
super().__init__() | ||
self._entra_token_cache = None | ||
self._response_cache = None | ||
|
||
async def send(self, request: PipelineRequest): | ||
cache_valid, token = _EntraTokenGuardUtils.is_entra_token_cache_valid(self._entra_token_cache, request) | ||
if cache_valid and _EntraTokenGuardUtils.is_acs_token_cache_valid(self._response_cache): | ||
response = self._response_cache | ||
else: | ||
self._entra_token_cache = token | ||
response = await self.next.send(request) | ||
self._response_cache = response | ||
if response is None: | ||
raise RuntimeError("Failed to obtain a valid PipelineResponse in AsyncEntraTokenGuardPolicy.send") | ||
return response | ||
|
||
|
||
|
||
class _EntraTokenGuardUtils: | ||
@staticmethod | ||
def is_entra_token_cache_valid(entra_token_cache, request): | ||
current_entra_token = request.http_request.headers.get("Authorization", "") | ||
cache_valid = ( | ||
entra_token_cache is not None and | ||
current_entra_token == entra_token_cache | ||
) | ||
return cache_valid, current_entra_token | ||
|
||
@staticmethod | ||
def is_acs_token_cache_valid(response_cache): | ||
if response_cache is None or response_cache.http_response.status_code != 200: | ||
return False | ||
return _EntraTokenGuardUtils.is_access_token_valid(response_cache) | ||
|
||
@staticmethod | ||
def is_access_token_valid(response_cache): | ||
try: | ||
if response_cache is None or response_cache.http_response is None: | ||
return False | ||
content = response_cache.http_response.text() | ||
data = json.loads(content) | ||
expires_on = data["accessToken"]["expiresOn"] | ||
|
||
expires_on_dt = dateutil_parser.parse(expires_on) | ||
return datetime.now(timezone.utc) < expires_on_dt | ||
except Exception: | ||
return False | ||
|
||
|
66 changes: 66 additions & 0 deletions
66
...nication/azure-communication-identity/azure/communication/identity/_shared/manual_test.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# coding: utf-8 | ||
# ------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for | ||
# license information. | ||
# -------------------------------------------------------------------------- | ||
|
||
# from azure.core.credentials import AccessToken | ||
from token_exchange import TokenExchangeClient | ||
from azure.core.credentials import AccessToken | ||
from user_credential import CommunicationTokenCredential | ||
#from user_credential_async import CommunicationTokenCredential | ||
import asyncio | ||
|
||
|
||
def main(): | ||
# TODO: Replace these values with your real endpoint and customer token | ||
endpoint = "https://acs-auth-ppe-us-ops.unitedstates.ppe.communication.azure.net" | ||
customer_token = "" | ||
|
||
# Set up the required options for TokenExchangeClient | ||
class CustomTokenCredential: | ||
def get_token(self, *scopes, **kwargs): | ||
# Provide a hardcoded token and expiry (e.g., far future timestamp) | ||
return AccessToken("entra_token", 9999999999) | ||
|
||
|
||
#token_credential=InteractiveBrowserCredential(redirect_uri="http://localhost", tenant_id="be5c2424-1562-4d62-8d98-815720d06e4a", client_id="c6ec4113-4e29-48a9-a814-e9df50ca033e") | ||
|
||
|
||
#credentialOld = CommunicationTokenCredential(token="skypetoken") | ||
# Example usage of user_credential_async.get_token (assuming user_credential_async is defined/imported) | ||
# Initialize the credential with resource_endpoint and custom token_credential | ||
credential = CommunicationTokenCredential( | ||
resource_endpoint=endpoint, | ||
token_credential=CustomTokenCredential(), | ||
scopes=["https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls"] | ||
) | ||
|
||
# Example call to get_token (update scopes as needed) | ||
token = credential.get_token("https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls") | ||
print("User Credential Async Token:", token.token) | ||
|
||
token = credential.get_token("https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls") | ||
print("User Credential Async Token:", token.token) | ||
|
||
async def get_async_token(): | ||
token = await credential.get_token("https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls") | ||
print("User Credential Async Token (async):", token.token) | ||
|
||
token = await credential.get_token("https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls") | ||
print("User Credential Async Token (async):", token.token) | ||
|
||
asyncio.run(get_async_token()) | ||
|
||
client = TokenExchangeClient(resource_endpoint=endpoint, token_credential=CustomTokenCredential(), scopes=["https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls"],) | ||
try: | ||
access_token = client.exchange_entra_token() | ||
print("Access Token:", access_token.token) | ||
print("Expires On:", access_token.expires_on) | ||
except Exception as ex: | ||
print("Token exchange failed:", ex) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DominikMe should we consider for the PhoneNumberIdentifier to parse here the raw_id and populate the asserted_id and is_anonymous based on the raw_id?
Or is it supposed to be used just to create the raw_id starting from the properties? There's also the identifier_from_raw_id that I think should generally be used for parsing from raw_id to identifier type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cemateia Let's follow what we do in the other languages, for example here in C#
=> Don't add asserted_id and is_anonymous to the init args (we can still do that in the future if we must)
=> Inside init, compute asserted_id and is_anonymous based on the raw_id (optionally do it in the properties but probably better to do it in init)
=> in _format_raw_id don't compute the raw_id based on asserted_id and is_anonymous because these two are derived from the raw_id
=> identifier_from_raw_id probably doesn't need changes if asserted_id and is_anonymous get derived within phone number identifier, right?