Skip to content

Commit c1a245d

Browse files
authored
Identity use pbyte (#11173)
* identity_vscode_cred_format
1 parent bfdc8e7 commit c1a245d

File tree

9 files changed

+156
-48
lines changed

9 files changed

+156
-48
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## 1.4.0b3 (Unreleased)
44

5+
- Now `DefaultAzureCredential` can authenticate with the identity signed in to Visual
6+
Studio Code's Azure extension. ([#10472](https://github.com/Azure/azure-sdk-for-python/issues/10472))
57

68
## 1.4.0b2 (2020-04-06)
79
- After an instance of `DefaultAzureCredential` successfully authenticates, it

sdk/identity/azure-identity/azure/identity/_constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
AZURE_VSCODE_CLIENT_ID = "aebc6443-996d-45c2-90f0-388ff96faa56"
99
VSCODE_CREDENTIALS_SECTION = "VS Code Azure"
1010

11+
1112
class KnownAuthorities:
1213
AZURE_CHINA = "login.chinacloudapi.cn"
1314
AZURE_GERMANY = "login.microsoftonline.de"

sdk/identity/azure-identity/azure/identity/_credentials/linux_vscode_adapter.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,38 @@
99

1010

1111
def _c_str(string):
12-
return ct.c_char_p(string.encode('utf-8'))
12+
return ct.c_char_p(string.encode("utf-8"))
1313

1414

1515
try:
16-
_libsecret = ct.cdll.LoadLibrary('libsecret-1.so.0')
17-
_libsecret.secret_schema_new.argtypes = \
18-
[ct.c_char_p, ct.c_uint, ct.c_char_p, ct.c_uint, ct.c_char_p, ct.c_uint, ct.c_void_p]
19-
_libsecret.secret_password_lookup_sync.argtypes = \
20-
[ct.c_void_p, ct.c_void_p, ct.c_void_p, ct.c_char_p, ct.c_char_p, ct.c_char_p, ct.c_char_p, ct.c_void_p]
16+
_libsecret = ct.cdll.LoadLibrary("libsecret-1.so.0")
17+
_libsecret.secret_schema_new.argtypes = [
18+
ct.c_char_p,
19+
ct.c_uint,
20+
ct.c_char_p,
21+
ct.c_uint,
22+
ct.c_char_p,
23+
ct.c_uint,
24+
ct.c_void_p,
25+
]
26+
_libsecret.secret_password_lookup_sync.argtypes = [
27+
ct.c_void_p,
28+
ct.c_void_p,
29+
ct.c_void_p,
30+
ct.c_char_p,
31+
ct.c_char_p,
32+
ct.c_char_p,
33+
ct.c_char_p,
34+
ct.c_void_p,
35+
]
2136
_libsecret.secret_password_lookup_sync.restype = ct.c_char_p
2237
_libsecret.secret_schema_unref.argtypes = [ct.c_void_p]
2338
except OSError:
2439
_libsecret = None
2540

2641

2742
def _get_user_settings_path():
28-
app_data_folder = os.environ['HOME']
43+
app_data_folder = os.environ["HOME"]
2944
return os.path.join(app_data_folder, ".config", "Code", "User", "settings.json")
3045

3146

@@ -47,17 +62,27 @@ def _get_refresh_token(service_name, account_name):
4762
# _libsecret.secret_password_lookup_sync raises segment fault on Python 2.7
4863
# temporarily disable it on 2.7
4964
import sys
65+
5066
if sys.version_info[0] < 3:
5167
raise NotImplementedError("Not supported on Python 2.7")
5268

5369
err = ct.c_int()
54-
schema = _libsecret.secret_schema_new(_c_str("org.freedesktop.Secret.Generic"), 2,
55-
_c_str("service"), 0, _c_str("account"), 0, None)
56-
p_str = _libsecret.secret_password_lookup_sync(schema, None, ct.byref(err), _c_str("service"), _c_str(service_name),
57-
_c_str("account"), _c_str(account_name), None)
70+
schema = _libsecret.secret_schema_new(
71+
_c_str("org.freedesktop.Secret.Generic"), 2, _c_str("service"), 0, _c_str("account"), 0, None
72+
)
73+
p_str = _libsecret.secret_password_lookup_sync(
74+
schema,
75+
None,
76+
ct.byref(err),
77+
_c_str("service"),
78+
_c_str(service_name),
79+
_c_str("account"),
80+
_c_str(account_name),
81+
None,
82+
)
5883
_libsecret.secret_schema_unref(schema)
5984
if err.value == 0:
60-
return p_str.decode('utf-8')
85+
return p_str.decode("utf-8")
6186

6287
return None
6388

@@ -69,5 +94,5 @@ def get_credentials():
6994
return credentials
7095
except NotImplementedError: # pylint:disable=try-except-raise
7196
raise
72-
except Exception: #pylint: disable=broad-except
97+
except Exception: # pylint: disable=broad-except
7398
return None

sdk/identity/azure-identity/azure/identity/_credentials/macos_vscode_adapter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
def _get_user_settings_path():
12-
app_data_folder = os.environ['USER']
12+
app_data_folder = os.environ["USER"]
1313
return os.path.join(app_data_folder, "Library", "Application Support", "Code", "User", "settings.json")
1414

1515

@@ -37,5 +37,5 @@ def get_credentials():
3737
environment_name = _get_user_settings()
3838
credentials = _get_refresh_token(VSCODE_CREDENTIALS_SECTION, environment_name)
3939
return credentials
40-
except Exception: #pylint: disable=broad-except
40+
except Exception: # pylint: disable=broad-except
4141
return None

sdk/identity/azure-identity/azure/identity/_credentials/vscode_credential.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
from .._exceptions import CredentialUnavailableError
88
from .._constants import AZURE_VSCODE_CLIENT_ID
99
from .._internal.aad_client import AadClient
10-
if sys.platform.startswith('win'):
10+
11+
if sys.platform.startswith("win"):
1112
from .win_vscode_adapter import get_credentials
12-
elif sys.platform.startswith('darwin'):
13+
elif sys.platform.startswith("darwin"):
1314
from .macos_vscode_adapter import get_credentials
1415
else:
1516
from .linux_vscode_adapter import get_credentials
@@ -24,8 +25,10 @@ class VSCodeCredential(object):
2425
"""Authenticates by redeeming a refresh token previously saved by VS Code
2526
2627
"""
28+
2729
def __init__(self, **kwargs):
2830
self._client = kwargs.pop("_client", None) or AadClient("organizations", AZURE_VSCODE_CLIENT_ID, **kwargs)
31+
self._refresh_token = None
2932

3033
def get_token(self, *scopes, **kwargs):
3134
# type: (*str, **Any) -> AccessToken
@@ -43,11 +46,15 @@ def get_token(self, *scopes, **kwargs):
4346
if not scopes:
4447
raise ValueError("'get_token' requires at least one scope")
4548

46-
refresh_token = get_credentials()
47-
if not refresh_token:
48-
raise CredentialUnavailableError(
49-
message="No Azure user is logged in to Visual Studio Code."
50-
)
51-
token = self._client.get_cached_access_token(scopes) \
52-
or self._client.obtain_token_by_refresh_token(refresh_token, scopes, **kwargs)
49+
token = self._client.get_cached_access_token(scopes)
50+
51+
if token:
52+
return token
53+
54+
if not self._refresh_token:
55+
self._refresh_token = get_credentials()
56+
if not self._refresh_token:
57+
raise CredentialUnavailableError(message="No Azure user is logged in to Visual Studio Code.")
58+
59+
token = self._client.obtain_token_by_refresh_token(self._refresh_token, scopes, **kwargs)
5360
return token

sdk/identity/azure-identity/azure/identity/_credentials/win_vscode_adapter.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66
import json
77
import ctypes as ct
88
from .._constants import VSCODE_CREDENTIALS_SECTION
9+
910
try:
1011
import ctypes.wintypes as wt
1112
except (IOError, ValueError):
1213
pass
1314

1415

15-
SUPPORTED_CREDKEYS = set((
16-
'Type', 'TargetName', 'Persist',
17-
'UserName', 'Comment', 'CredentialBlob'))
16+
SUPPORTED_CREDKEYS = set(("Type", "TargetName", "Persist", "UserName", "Comment", "CredentialBlob"))
17+
18+
_PBYTE = ct.POINTER(ct.c_byte)
1819

1920

2021
class _CREDENTIAL(ct.Structure):
@@ -25,17 +26,18 @@ class _CREDENTIAL(ct.Structure):
2526
("Comment", ct.c_wchar_p),
2627
("LastWritten", wt.FILETIME),
2728
("CredentialBlobSize", wt.DWORD),
28-
("CredentialBlob", wt.LPBYTE),
29+
("CredentialBlob", _PBYTE),
2930
("Persist", wt.DWORD),
3031
("AttributeCount", wt.DWORD),
3132
("Attributes", ct.c_void_p),
3233
("TargetAlias", ct.c_wchar_p),
33-
("UserName", ct.c_wchar_p)]
34+
("UserName", ct.c_wchar_p),
35+
]
3436

3537

3638
_PCREDENTIAL = ct.POINTER(_CREDENTIAL)
3739

38-
_advapi = ct.WinDLL('advapi32')
40+
_advapi = ct.WinDLL("advapi32")
3941
_advapi.CredReadW.argtypes = [wt.LPCWSTR, wt.DWORD, wt.DWORD, ct.POINTER(_PCREDENTIAL)]
4042
_advapi.CredReadW.restype = wt.BOOL
4143
_advapi.CredFree.argtypes = [_PCREDENTIAL]
@@ -47,16 +49,15 @@ def _read_credential(service_name, account_name):
4749
if _advapi.CredReadW(target, 1, 0, ct.byref(cred_ptr)):
4850
cred_blob = cred_ptr.contents.CredentialBlob
4951
cred_blob_size = cred_ptr.contents.CredentialBlobSize
50-
password_as_list = [int.from_bytes(cred_blob[pos:pos + 1], 'little')
51-
for pos in range(0, cred_blob_size)]
52-
cred = ''.join(map(chr, password_as_list))
52+
password_as_list = [int.from_bytes(cred_blob[pos : pos + 1], "little") for pos in range(0, cred_blob_size)]
53+
cred = "".join(map(chr, password_as_list))
5354
_advapi.CredFree(cred_ptr)
5455
return cred
5556
return None
5657

5758

5859
def _get_user_settings_path():
59-
app_data_folder = os.environ['APPDATA']
60+
app_data_folder = os.environ["APPDATA"]
6061
return os.path.join(app_data_folder, "Code", "User", "settings.json")
6162

6263

@@ -80,5 +81,5 @@ def get_credentials():
8081
environment_name = _get_user_settings()
8182
credentials = _get_refresh_token(VSCODE_CREDENTIALS_SECTION, environment_name)
8283
return credentials
83-
except Exception: #pylint: disable=broad-except
84+
except Exception: # pylint: disable=broad-except
8485
return None

sdk/identity/azure-identity/azure/identity/aio/_credentials/vscode_credential.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@
88
from ..._constants import AZURE_VSCODE_CLIENT_ID
99
from .._internal.aad_client import AadClient
1010
from ..._credentials.vscode_credential import get_credentials
11+
1112
if TYPE_CHECKING:
1213
# pylint:disable=unused-import,ungrouped-imports
1314
from typing import Any, Iterable, Optional
1415
from azure.core.credentials import AccessToken
1516

17+
1618
class VSCodeCredential(AsyncCredentialBase):
1719
"""Authenticates by redeeming a refresh token previously saved by VS Code
1820
1921
"""
22+
2023
def __init__(self, **kwargs):
2124
self._client = kwargs.pop("_client", None) or AadClient("organizations", AZURE_VSCODE_CLIENT_ID, **kwargs)
25+
self._refresh_token = None
2226

2327
async def __aenter__(self):
2428
if self._client:
@@ -47,12 +51,14 @@ async def get_token(self, *scopes, **kwargs):
4751
if not scopes:
4852
raise ValueError("'get_token' requires at least one scope")
4953

50-
refresh_token = get_credentials()
51-
if not refresh_token:
52-
raise CredentialUnavailableError(
53-
message="No Azure user is logged in to Visual Studio Code."
54-
)
5554
token = self._client.get_cached_access_token(scopes)
56-
if not token:
57-
token = await self._client.obtain_token_by_refresh_token(refresh_token, scopes, **kwargs)
55+
if token:
56+
return token
57+
58+
if not self._refresh_token:
59+
self._refresh_token = get_credentials()
60+
if not self._refresh_token:
61+
raise CredentialUnavailableError(message="No Azure user is logged in to Visual Studio Code.")
62+
63+
token = await self._client.obtain_token_by_refresh_token(self._refresh_token, scopes, **kwargs)
5864
return token

sdk/identity/azure-identity/tests/test_vscode_credential.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from azure.core.pipeline.policies import SansIOHTTPPolicy
1010
from azure.identity._internal.user_agent import USER_AGENT
1111
from helpers import build_aad_response, mock_response, Request, validating_transport
12+
1213
try:
1314
from unittest import mock
1415
except ImportError: # python < 3.3
@@ -65,26 +66,58 @@ def test_redeem_token():
6566
credential = VSCodeCredential(_client=mock_client)
6667
token = credential.get_token("scope")
6768
assert token is expected_token
68-
mock_client.obtain_token_by_refresh_token.assert_called_with('VALUE', ('scope',))
69+
mock_client.obtain_token_by_refresh_token.assert_called_with("VALUE", ("scope",))
6970
assert mock_client.obtain_token_by_refresh_token.call_count == 1
7071

7172

72-
@pytest.mark.skipif(not sys.platform.startswith('darwin'), reason="This test only runs on MacOS")
73+
def test_cache_refresh_token():
74+
expected_token = AccessToken("token", 42)
75+
76+
mock_client = mock.Mock(spec=object)
77+
mock_client.obtain_token_by_refresh_token = mock.Mock(return_value=expected_token)
78+
mock_client.get_cached_access_token = mock.Mock(return_value=None)
79+
mock_get_credentials = mock.Mock(return_value="VALUE")
80+
81+
with mock.patch(VSCodeCredential.__module__ + ".get_credentials", mock_get_credentials):
82+
credential = VSCodeCredential(_client=mock_client)
83+
token = credential.get_token("scope")
84+
assert token is expected_token
85+
assert mock_get_credentials.call_count == 1
86+
token = credential.get_token("scope")
87+
assert token is expected_token
88+
assert mock_get_credentials.call_count == 1
89+
90+
91+
def test_no_obtain_token_if_cached():
92+
expected_token = AccessToken("token", 42)
93+
94+
mock_client = mock.Mock(spec=object)
95+
mock_client.obtain_token_by_refresh_token = mock.Mock(return_value=expected_token)
96+
mock_client.get_cached_access_token = mock.Mock(return_value='VALUE')
97+
98+
with mock.patch(VSCodeCredential.__module__ + ".get_credentials", return_value="VALUE"):
99+
credential = VSCodeCredential(_client=mock_client)
100+
token = credential.get_token("scope")
101+
assert mock_client.obtain_token_by_refresh_token.call_count == 0
102+
103+
104+
@pytest.mark.skipif(not sys.platform.startswith("darwin"), reason="This test only runs on MacOS")
73105
def test_mac_keychain_valid_value():
74-
with mock.patch('Keychain.get_generic_password', return_value="VALUE"):
106+
with mock.patch("msal_extensions.osx.Keychain.get_generic_password", return_value="VALUE"):
75107
assert get_credentials() == "VALUE"
76108

77109

78-
@pytest.mark.skipif(not sys.platform.startswith('darwin'), reason="This test only runs on MacOS")
110+
@pytest.mark.skipif(not sys.platform.startswith("darwin"), reason="This test only runs on MacOS")
79111
def test_mac_keychain_error():
80112
from msal_extensions.osx import Keychain, KeychainError
81-
with mock.patch.object(Keychain, 'get_generic_password', side_effect=KeychainError()):
113+
114+
with mock.patch.object(Keychain, "get_generic_password", side_effect=KeychainError(-1)):
82115
credential = VSCodeCredential()
83116
with pytest.raises(CredentialUnavailableError):
84117
token = credential.get_token("scope")
85118

86119

87120
@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="This test only runs on Linux")
88121
def test_get_token():
89-
with mock.patch('azure.identity._credentials.linux_vscode_adapter._get_refresh_token', return_value="VALUE"):
122+
with mock.patch("azure.identity._credentials.linux_vscode_adapter._get_refresh_token", return_value="VALUE"):
90123
assert get_credentials() == "VALUE"

sdk/identity/azure-identity/tests/test_vscode_credential_async.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,36 @@ async def test_redeem_token():
6969
credential = VSCodeCredential(_client=mock_client)
7070
token = await credential.get_token("scope")
7171
assert token is expected_token
72+
token_by_refresh_token.assert_called_with("VALUE", ("scope",))
73+
74+
@pytest.mark.asyncio
75+
async def test_cache_refresh_token():
76+
expected_token = AccessToken("token", 42)
77+
78+
mock_client = mock.Mock(spec=object)
79+
token_by_refresh_token = mock.Mock(return_value=expected_token)
80+
mock_client.obtain_token_by_refresh_token = wrap_in_future(token_by_refresh_token)
81+
mock_client.get_cached_access_token = mock.Mock(return_value=None)
82+
mock_get_credentials = mock.Mock(return_value="VALUE")
83+
84+
with mock.patch(VSCodeCredential.__module__ + ".get_credentials", mock_get_credentials):
85+
credential = VSCodeCredential(_client=mock_client)
86+
token = await credential.get_token("scope")
87+
assert mock_get_credentials.call_count == 1
88+
token = await credential.get_token("scope")
89+
assert mock_get_credentials.call_count == 1
90+
91+
92+
@pytest.mark.asyncio
93+
async def test_no_obtain_token_if_cached():
94+
expected_token = AccessToken("token", 42)
95+
96+
mock_client = mock.Mock(spec=object)
97+
token_by_refresh_token = mock.Mock(return_value=expected_token)
98+
mock_client.obtain_token_by_refresh_token = wrap_in_future(token_by_refresh_token)
99+
mock_client.get_cached_access_token = mock.Mock(return_value='VALUE')
100+
101+
with mock.patch(VSCodeCredential.__module__ + ".get_credentials", return_value="VALUE"):
102+
credential = VSCodeCredential(_client=mock_client)
103+
token = await credential.get_token("scope")
104+
assert token_by_refresh_token.call_count == 0

0 commit comments

Comments
 (0)