Skip to content

Commit efdfeba

Browse files
authored
Merge pull request #25 from AzureAD/release-0.1.0
Release 0.1.0
2 parents 55561d9 + 6809f72 commit efdfeba

16 files changed

+942
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,10 @@ paket-files/
298298
__pycache__/
299299
*.pyc
300300

301+
# Python Auxiliary Tools
302+
*.egg-info/
303+
.tox/
304+
301305
# Cake - Uncomment if you are using it
302306
# tools/**
303307
# !tools/packages.config

.pylintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[MESSAGES CONTROL]
2+
disable=
3+
useless-object-inheritance

.travis.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
language: python
2+
3+
matrix:
4+
fast_finish: true
5+
include:
6+
- python: "2.7"
7+
env: TOXENV=py27 PYPI=true
8+
os: linux
9+
- python: "3.5"
10+
env: TOXENV=py35
11+
os: linux
12+
- python: "3.6"
13+
env: TOXENV=py36
14+
os: linux
15+
- python: "3.7"
16+
env: TOXENV=py37
17+
os: linux
18+
dist: xenial
19+
- name: "Python 3.7 on macOS"
20+
env: TOXENV=py37
21+
os: osx
22+
osx_image: xcode10.2
23+
language: shell
24+
- name: "Python 2.7 on Windows"
25+
env: TOXENV=py27 PATH=/c/Python27:/c/Python27/Scripts:$PATH
26+
os: windows
27+
before_install: choco install python2
28+
language: shell
29+
- name: "Python 3.5 on Windows"
30+
env: TOXENV=py35 PATH=/c/Python35:/c/Python35/Scripts:$PATH
31+
os: windows
32+
before_install: choco install python3 --version 3.5.4
33+
language: shell
34+
- name: "Python 3.7 on Windows"
35+
env: TOXENV=py37 PATH=/c/Python37:/c/Python37/Scripts:$PATH
36+
os: windows
37+
before_install: choco install python3 --version 3.7.3
38+
language: shell
39+
40+
install:
41+
- pip install tox pylint
42+
- pip install .
43+
44+
script:
45+
- pylint msal_extensions
46+
- tox
47+
48+
deploy:
49+
- # test pypi
50+
provider: pypi
51+
distributions: "sdist bdist_wheel"
52+
server: https://test.pypi.org/legacy/
53+
user: "nugetaad"
54+
password:
55+
secure: dpNi6BsZyiAx/gkxJ5Mz6m2yDz2dRGWsSgS5pF+ywNzgHJ6+0e234GyLbSUY5bFeeA7WtOr4is3bxSLB/6tTWDVWdw3TL4FGlDM/54MSLWg8R5bR9PRwO+VU1kvQ03yz+B9mTpzuiwL2e+OSwcwo97jForADzmSRA5OpEq5Z7zAs7WR8J2tyhl+288NwLtKJMVy39UmPl9oifu6/5RfBn7EWLmC7MrMFhHTb2Gj7fJWw4u+5vx9bsQ7ubfiwPbRAtYXLz6wDMtwtFzwme4zZPg5HwWCn0WWlX4b6x7xXirZ7yKsy9iACLgTrLMeAkferrex7f03NFeIDobasML+fLbZufATaL3M97kNGZwulEYNp2+RWyLu/NW6FoZCbS+cSL8HuFnkIDHzEoO56ItMiD9EH47q/NeKgwrrzKjfY+KzaMQOYLlVgCa4WrIeFh5CkwJ4RHrfanMIV2vbEvMxsnHc/mZ+yvgBOFoBNXYN1HEDzEv1NxDPcyt7MBlPUVinEreQaHba7w6qH9Rf0eWgfW2ypBXe+nHaZxQgaGC6J+WGUkzalYQspmHVU4CcuwJa55kuchJs/gbyZKkyK6P8uD5IP6VZiavwZcjWcfvwbZaLeOqzSDVCDMg8M2zYZHoa+6ZR4EgDVW7RvaRvjvvhPTPj5twmLf3YYVJtHIyJSLug=
56+
on:
57+
branch: master
58+
tags: false
59+
condition: $PYPI = "true"
60+
61+
- # production pypi
62+
provider: pypi
63+
distributions: "sdist bdist_wheel"
64+
user: "nugetaad"
65+
password:
66+
secure: dpNi6BsZyiAx/gkxJ5Mz6m2yDz2dRGWsSgS5pF+ywNzgHJ6+0e234GyLbSUY5bFeeA7WtOr4is3bxSLB/6tTWDVWdw3TL4FGlDM/54MSLWg8R5bR9PRwO+VU1kvQ03yz+B9mTpzuiwL2e+OSwcwo97jForADzmSRA5OpEq5Z7zAs7WR8J2tyhl+288NwLtKJMVy39UmPl9oifu6/5RfBn7EWLmC7MrMFhHTb2Gj7fJWw4u+5vx9bsQ7ubfiwPbRAtYXLz6wDMtwtFzwme4zZPg5HwWCn0WWlX4b6x7xXirZ7yKsy9iACLgTrLMeAkferrex7f03NFeIDobasML+fLbZufATaL3M97kNGZwulEYNp2+RWyLu/NW6FoZCbS+cSL8HuFnkIDHzEoO56ItMiD9EH47q/NeKgwrrzKjfY+KzaMQOYLlVgCa4WrIeFh5CkwJ4RHrfanMIV2vbEvMxsnHc/mZ+yvgBOFoBNXYN1HEDzEv1NxDPcyt7MBlPUVinEreQaHba7w6qH9Rf0eWgfW2ypBXe+nHaZxQgaGC6J+WGUkzalYQspmHVU4CcuwJa55kuchJs/gbyZKkyK6P8uD5IP6VZiavwZcjWcfvwbZaLeOqzSDVCDMg8M2zYZHoa+6ZR4EgDVW7RvaRvjvvhPTPj5twmLf3YYVJtHIyJSLug=
67+
on:
68+
branch: master
69+
tags: true
70+
condition: $PYPI = "true"

azure-pipelines.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
resources:
2+
- repo: self
3+
4+
trigger:
5+
batch: true
6+
branches:
7+
include:
8+
- '*'

msal_extensions/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Provides auxiliary functionality to the `msal` package."""
2+
__version__ = "0.1.0"
3+
4+
import sys
5+
6+
if sys.platform.startswith('win'):
7+
from .token_cache import WindowsTokenCache as TokenCache
8+
elif sys.platform.startswith('darwin'):
9+
from .token_cache import OSXTokenCache as TokenCache
10+
else:
11+
from .token_cache import UnencryptedTokenCache as TokenCache

msal_extensions/cache_lock.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Provides a mechanism for not competing with other processes interacting with an MSAL cache."""
2+
import os
3+
import sys
4+
import errno
5+
import portalocker
6+
7+
8+
class CrossPlatLock(object):
9+
"""Offers a mechanism for waiting until another process is finished interacting with a shared
10+
resource. This is specifically written to interact with a class of the same name in the .NET
11+
extensions library.
12+
"""
13+
def __init__(self, lockfile_path):
14+
self._lockpath = lockfile_path
15+
self._fh = None
16+
17+
def __enter__(self):
18+
pid = os.getpid()
19+
20+
self._fh = open(self._lockpath, 'wb+', buffering=0)
21+
portalocker.lock(self._fh, portalocker.LOCK_EX)
22+
self._fh.write('{} {}'.format(pid, sys.argv[0]).encode('utf-8'))
23+
24+
def __exit__(self, *args):
25+
self._fh.close()
26+
try:
27+
# Attempt to delete the lockfile. In either of the failure cases enumerated below, it is
28+
# likely that another process has raced this one and ended up clearing or locking the
29+
# file for itself.
30+
os.remove(self._lockpath)
31+
except OSError as ex:
32+
if ex.errno != errno.ENOENT and ex.errno != errno.EACCES:
33+
raise

msal_extensions/osx.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
# pylint: disable=duplicate-code
2+
3+
"""Implements a macOS specific TokenCache, and provides auxiliary helper types."""
4+
5+
import os
6+
import ctypes as _ctypes
7+
8+
OS_RESULT = _ctypes.c_int32
9+
10+
11+
class KeychainError(OSError):
12+
"""The RuntimeError that will be run when a function interacting with Keychain fails."""
13+
14+
ACCESS_DENIED = -128
15+
NO_SUCH_KEYCHAIN = -25294
16+
NO_DEFAULT = -25307
17+
ITEM_NOT_FOUND = -25300
18+
19+
def __init__(self, exit_status):
20+
super(KeychainError, self).__init__()
21+
self.exit_status = exit_status
22+
# TODO: pylint: disable=fixme
23+
# use SecCopyErrorMessageString to fetch the appropriate message here.
24+
self.message = \
25+
'{} ' \
26+
'see https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-18.1/MacErrors.h'\
27+
.format(self.exit_status)
28+
29+
def _get_native_location(name):
30+
# type: (str) -> str
31+
"""
32+
Fetches the location of a native MacOS library.
33+
:param name: The name of the library to be loaded.
34+
:return: The location of the library on a MacOS filesystem.
35+
"""
36+
return '/System/Library/Frameworks/{0}.framework/{0}'.format(name)
37+
38+
39+
# Load native MacOS libraries
40+
_SECURITY = _ctypes.CDLL(_get_native_location('Security'))
41+
_CORE = _ctypes.CDLL(_get_native_location('CoreFoundation'))
42+
43+
44+
# Bind CFRelease from native MacOS libraries.
45+
_CORE_RELEASE = _CORE.CFRelease
46+
_CORE_RELEASE.argtypes = (
47+
_ctypes.c_void_p,
48+
)
49+
50+
# Bind SecCopyErrorMessageString from native MacOS libraries.
51+
# https://developer.apple.com/documentation/security/1394686-seccopyerrormessagestring?language=objc
52+
_SECURITY_COPY_ERROR_MESSAGE_STRING = _SECURITY.SecCopyErrorMessageString
53+
_SECURITY_COPY_ERROR_MESSAGE_STRING.argtypes = (
54+
OS_RESULT,
55+
_ctypes.c_void_p
56+
)
57+
_SECURITY_COPY_ERROR_MESSAGE_STRING.restype = _ctypes.c_char_p
58+
59+
# Bind SecKeychainOpen from native MacOS libraries.
60+
# https://developer.apple.com/documentation/security/1396431-seckeychainopen
61+
_SECURITY_KEYCHAIN_OPEN = _SECURITY.SecKeychainOpen
62+
_SECURITY_KEYCHAIN_OPEN.argtypes = (
63+
_ctypes.c_char_p,
64+
_ctypes.POINTER(_ctypes.c_void_p)
65+
)
66+
_SECURITY_KEYCHAIN_OPEN.restype = OS_RESULT
67+
68+
# Bind SecKeychainCopyDefault from native MacOS libraries.
69+
# https://developer.apple.com/documentation/security/1400743-seckeychaincopydefault?language=objc
70+
_SECURITY_KEYCHAIN_COPY_DEFAULT = _SECURITY.SecKeychainCopyDefault
71+
_SECURITY_KEYCHAIN_COPY_DEFAULT.argtypes = (
72+
_ctypes.POINTER(_ctypes.c_void_p),
73+
)
74+
_SECURITY_KEYCHAIN_COPY_DEFAULT.restype = OS_RESULT
75+
76+
77+
# Bind SecKeychainItemFreeContent from native MacOS libraries.
78+
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT = _SECURITY.SecKeychainItemFreeContent
79+
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT.argtypes = (
80+
_ctypes.c_void_p,
81+
_ctypes.c_void_p,
82+
)
83+
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT.restype = OS_RESULT
84+
85+
# Bind SecKeychainItemModifyAttributesAndData from native MacOS libraries.
86+
_SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA = \
87+
_SECURITY.SecKeychainItemModifyAttributesAndData
88+
_SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA.argtypes = (
89+
_ctypes.c_void_p,
90+
_ctypes.c_void_p,
91+
_ctypes.c_uint32,
92+
_ctypes.c_void_p,
93+
)
94+
_SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA.restype = OS_RESULT
95+
96+
# Bind SecKeychainFindGenericPassword from native MacOS libraries.
97+
# https://developer.apple.com/documentation/security/1397301-seckeychainfindgenericpassword?language=objc
98+
_SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD = _SECURITY.SecKeychainFindGenericPassword
99+
_SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD.argtypes = (
100+
_ctypes.c_void_p,
101+
_ctypes.c_uint32,
102+
_ctypes.c_char_p,
103+
_ctypes.c_uint32,
104+
_ctypes.c_char_p,
105+
_ctypes.POINTER(_ctypes.c_uint32),
106+
_ctypes.POINTER(_ctypes.c_void_p),
107+
_ctypes.POINTER(_ctypes.c_void_p),
108+
)
109+
_SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD.restype = OS_RESULT
110+
# Bind SecKeychainAddGenericPassword from native MacOS
111+
# https://developer.apple.com/documentation/security/1398366-seckeychainaddgenericpassword?language=objc
112+
_SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD = _SECURITY.SecKeychainAddGenericPassword
113+
_SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD.argtypes = (
114+
_ctypes.c_void_p,
115+
_ctypes.c_uint32,
116+
_ctypes.c_char_p,
117+
_ctypes.c_uint32,
118+
_ctypes.c_char_p,
119+
_ctypes.c_uint32,
120+
_ctypes.c_char_p,
121+
_ctypes.POINTER(_ctypes.c_void_p),
122+
)
123+
_SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD.restype = OS_RESULT
124+
125+
126+
class Keychain(object):
127+
"""Encapsulates the interactions with a particular MacOS Keychain."""
128+
def __init__(self, filename=None):
129+
# type: (str) -> None
130+
self._ref = _ctypes.c_void_p()
131+
132+
if filename:
133+
filename = os.path.expanduser(filename)
134+
self._filename = filename.encode('utf-8')
135+
else:
136+
self._filename = None
137+
138+
def __enter__(self):
139+
if self._filename:
140+
status = _SECURITY_KEYCHAIN_OPEN(self._filename, self._ref)
141+
else:
142+
status = _SECURITY_KEYCHAIN_COPY_DEFAULT(self._ref)
143+
144+
if status:
145+
raise OSError(status)
146+
return self
147+
148+
def __exit__(self, *args):
149+
if self._ref:
150+
_CORE_RELEASE(self._ref)
151+
152+
def get_generic_password(self, service, account_name):
153+
# type: (str, str) -> str
154+
"""Fetch the password associated with a particular service and account.
155+
156+
:param service: The service that this password is associated with.
157+
:param account_name: The account that this password is associated with.
158+
:return: The value of the password associated with the specified service and account.
159+
"""
160+
service = service.encode('utf-8')
161+
account_name = account_name.encode('utf-8')
162+
163+
length = _ctypes.c_uint32()
164+
contents = _ctypes.c_void_p()
165+
exit_status = _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD(
166+
self._ref,
167+
len(service),
168+
service,
169+
len(account_name),
170+
account_name,
171+
length,
172+
contents,
173+
None,
174+
)
175+
176+
if exit_status:
177+
raise KeychainError(exit_status=exit_status)
178+
179+
value = _ctypes.create_string_buffer(length.value)
180+
_ctypes.memmove(value, contents.value, length.value)
181+
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT(None, contents)
182+
return value.raw.decode('utf-8')
183+
184+
def set_generic_password(self, service, account_name, value):
185+
# type: (str, str, str) -> None
186+
"""Associate a password with a given service and account.
187+
188+
:param service: The service to associate this password with.
189+
:param account_name: The account to associate this password with.
190+
:param value: The string that should be used as the password.
191+
"""
192+
service = service.encode('utf-8')
193+
account_name = account_name.encode('utf-8')
194+
value = value.encode('utf-8')
195+
196+
entry = _ctypes.c_void_p()
197+
find_exit_status = _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD(
198+
self._ref,
199+
len(service),
200+
service,
201+
len(account_name),
202+
account_name,
203+
None,
204+
None,
205+
entry,
206+
)
207+
208+
if not find_exit_status:
209+
modify_exit_status = _SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA(
210+
entry,
211+
None,
212+
len(value),
213+
value,
214+
)
215+
if modify_exit_status:
216+
raise KeychainError(exit_status=modify_exit_status)
217+
218+
elif find_exit_status == KeychainError.ITEM_NOT_FOUND:
219+
add_exit_status = _SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD(
220+
self._ref,
221+
len(service),
222+
service,
223+
len(account_name),
224+
account_name,
225+
len(value),
226+
value,
227+
None
228+
)
229+
230+
if add_exit_status:
231+
raise KeychainError(exit_status=add_exit_status)
232+
else:
233+
raise KeychainError(exit_status=find_exit_status)
234+
235+
def get_internet_password(self, service, username):
236+
# type: (str, str) -> str
237+
""" Fetches a password associated with a domain and username.
238+
NOTE: THIS IS NOT YET IMPLEMENTED
239+
:param service: The website/service that this password is associated with.
240+
:param username: The account that this password is associated with.
241+
:return: The password that was associated with the given service and username.
242+
"""
243+
raise NotImplementedError()
244+
245+
def set_internet_password(self, service, username, value):
246+
# type: (str, str, str) -> None
247+
"""Sets a password associated with a domain and a username.
248+
NOTE: THIS IS NOT YET IMPLEMENTED
249+
:param service: The website/service that this password is associated with.
250+
:param username: The account that this password is associated with.
251+
:param value: The password that should be associated with the given service and username.
252+
"""
253+
raise NotImplementedError()

0 commit comments

Comments
 (0)