Skip to content

Commit

Permalink
Merge pull request #25 from AzureAD/release-0.1.0
Browse files Browse the repository at this point in the history
Release 0.1.0
  • Loading branch information
rayluo authored Jul 11, 2019
2 parents 55561d9 + 6809f72 commit efdfeba
Show file tree
Hide file tree
Showing 16 changed files with 942 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ paket-files/
__pycache__/
*.pyc

# Python Auxiliary Tools
*.egg-info/
.tox/

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
Expand Down
3 changes: 3 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[MESSAGES CONTROL]
disable=
useless-object-inheritance
70 changes: 70 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
language: python

matrix:
fast_finish: true
include:
- python: "2.7"
env: TOXENV=py27 PYPI=true
os: linux
- python: "3.5"
env: TOXENV=py35
os: linux
- python: "3.6"
env: TOXENV=py36
os: linux
- python: "3.7"
env: TOXENV=py37
os: linux
dist: xenial
- name: "Python 3.7 on macOS"
env: TOXENV=py37
os: osx
osx_image: xcode10.2
language: shell
- name: "Python 2.7 on Windows"
env: TOXENV=py27 PATH=/c/Python27:/c/Python27/Scripts:$PATH
os: windows
before_install: choco install python2
language: shell
- name: "Python 3.5 on Windows"
env: TOXENV=py35 PATH=/c/Python35:/c/Python35/Scripts:$PATH
os: windows
before_install: choco install python3 --version 3.5.4
language: shell
- name: "Python 3.7 on Windows"
env: TOXENV=py37 PATH=/c/Python37:/c/Python37/Scripts:$PATH
os: windows
before_install: choco install python3 --version 3.7.3
language: shell

install:
- pip install tox pylint
- pip install .

script:
- pylint msal_extensions
- tox

deploy:
- # test pypi
provider: pypi
distributions: "sdist bdist_wheel"
server: https://test.pypi.org/legacy/
user: "nugetaad"
password:
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=
on:
branch: master
tags: false
condition: $PYPI = "true"

- # production pypi
provider: pypi
distributions: "sdist bdist_wheel"
user: "nugetaad"
password:
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=
on:
branch: master
tags: true
condition: $PYPI = "true"
8 changes: 8 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resources:
- repo: self

trigger:
batch: true
branches:
include:
- '*'
11 changes: 11 additions & 0 deletions msal_extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Provides auxiliary functionality to the `msal` package."""
__version__ = "0.1.0"

import sys

if sys.platform.startswith('win'):
from .token_cache import WindowsTokenCache as TokenCache
elif sys.platform.startswith('darwin'):
from .token_cache import OSXTokenCache as TokenCache
else:
from .token_cache import UnencryptedTokenCache as TokenCache
33 changes: 33 additions & 0 deletions msal_extensions/cache_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Provides a mechanism for not competing with other processes interacting with an MSAL cache."""
import os
import sys
import errno
import portalocker


class CrossPlatLock(object):
"""Offers a mechanism for waiting until another process is finished interacting with a shared
resource. This is specifically written to interact with a class of the same name in the .NET
extensions library.
"""
def __init__(self, lockfile_path):
self._lockpath = lockfile_path
self._fh = None

def __enter__(self):
pid = os.getpid()

self._fh = open(self._lockpath, 'wb+', buffering=0)
portalocker.lock(self._fh, portalocker.LOCK_EX)
self._fh.write('{} {}'.format(pid, sys.argv[0]).encode('utf-8'))

def __exit__(self, *args):
self._fh.close()
try:
# Attempt to delete the lockfile. In either of the failure cases enumerated below, it is
# likely that another process has raced this one and ended up clearing or locking the
# file for itself.
os.remove(self._lockpath)
except OSError as ex:
if ex.errno != errno.ENOENT and ex.errno != errno.EACCES:
raise
253 changes: 253 additions & 0 deletions msal_extensions/osx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# pylint: disable=duplicate-code

"""Implements a macOS specific TokenCache, and provides auxiliary helper types."""

import os
import ctypes as _ctypes

OS_RESULT = _ctypes.c_int32


class KeychainError(OSError):
"""The RuntimeError that will be run when a function interacting with Keychain fails."""

ACCESS_DENIED = -128
NO_SUCH_KEYCHAIN = -25294
NO_DEFAULT = -25307
ITEM_NOT_FOUND = -25300

def __init__(self, exit_status):
super(KeychainError, self).__init__()
self.exit_status = exit_status
# TODO: pylint: disable=fixme
# use SecCopyErrorMessageString to fetch the appropriate message here.
self.message = \
'{} ' \
'see https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-18.1/MacErrors.h'\
.format(self.exit_status)

def _get_native_location(name):
# type: (str) -> str
"""
Fetches the location of a native MacOS library.
:param name: The name of the library to be loaded.
:return: The location of the library on a MacOS filesystem.
"""
return '/System/Library/Frameworks/{0}.framework/{0}'.format(name)


# Load native MacOS libraries
_SECURITY = _ctypes.CDLL(_get_native_location('Security'))
_CORE = _ctypes.CDLL(_get_native_location('CoreFoundation'))


# Bind CFRelease from native MacOS libraries.
_CORE_RELEASE = _CORE.CFRelease
_CORE_RELEASE.argtypes = (
_ctypes.c_void_p,
)

# Bind SecCopyErrorMessageString from native MacOS libraries.
# https://developer.apple.com/documentation/security/1394686-seccopyerrormessagestring?language=objc
_SECURITY_COPY_ERROR_MESSAGE_STRING = _SECURITY.SecCopyErrorMessageString
_SECURITY_COPY_ERROR_MESSAGE_STRING.argtypes = (
OS_RESULT,
_ctypes.c_void_p
)
_SECURITY_COPY_ERROR_MESSAGE_STRING.restype = _ctypes.c_char_p

# Bind SecKeychainOpen from native MacOS libraries.
# https://developer.apple.com/documentation/security/1396431-seckeychainopen
_SECURITY_KEYCHAIN_OPEN = _SECURITY.SecKeychainOpen
_SECURITY_KEYCHAIN_OPEN.argtypes = (
_ctypes.c_char_p,
_ctypes.POINTER(_ctypes.c_void_p)
)
_SECURITY_KEYCHAIN_OPEN.restype = OS_RESULT

# Bind SecKeychainCopyDefault from native MacOS libraries.
# https://developer.apple.com/documentation/security/1400743-seckeychaincopydefault?language=objc
_SECURITY_KEYCHAIN_COPY_DEFAULT = _SECURITY.SecKeychainCopyDefault
_SECURITY_KEYCHAIN_COPY_DEFAULT.argtypes = (
_ctypes.POINTER(_ctypes.c_void_p),
)
_SECURITY_KEYCHAIN_COPY_DEFAULT.restype = OS_RESULT


# Bind SecKeychainItemFreeContent from native MacOS libraries.
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT = _SECURITY.SecKeychainItemFreeContent
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT.argtypes = (
_ctypes.c_void_p,
_ctypes.c_void_p,
)
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT.restype = OS_RESULT

# Bind SecKeychainItemModifyAttributesAndData from native MacOS libraries.
_SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA = \
_SECURITY.SecKeychainItemModifyAttributesAndData
_SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA.argtypes = (
_ctypes.c_void_p,
_ctypes.c_void_p,
_ctypes.c_uint32,
_ctypes.c_void_p,
)
_SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA.restype = OS_RESULT

# Bind SecKeychainFindGenericPassword from native MacOS libraries.
# https://developer.apple.com/documentation/security/1397301-seckeychainfindgenericpassword?language=objc
_SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD = _SECURITY.SecKeychainFindGenericPassword
_SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD.argtypes = (
_ctypes.c_void_p,
_ctypes.c_uint32,
_ctypes.c_char_p,
_ctypes.c_uint32,
_ctypes.c_char_p,
_ctypes.POINTER(_ctypes.c_uint32),
_ctypes.POINTER(_ctypes.c_void_p),
_ctypes.POINTER(_ctypes.c_void_p),
)
_SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD.restype = OS_RESULT
# Bind SecKeychainAddGenericPassword from native MacOS
# https://developer.apple.com/documentation/security/1398366-seckeychainaddgenericpassword?language=objc
_SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD = _SECURITY.SecKeychainAddGenericPassword
_SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD.argtypes = (
_ctypes.c_void_p,
_ctypes.c_uint32,
_ctypes.c_char_p,
_ctypes.c_uint32,
_ctypes.c_char_p,
_ctypes.c_uint32,
_ctypes.c_char_p,
_ctypes.POINTER(_ctypes.c_void_p),
)
_SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD.restype = OS_RESULT


class Keychain(object):
"""Encapsulates the interactions with a particular MacOS Keychain."""
def __init__(self, filename=None):
# type: (str) -> None
self._ref = _ctypes.c_void_p()

if filename:
filename = os.path.expanduser(filename)
self._filename = filename.encode('utf-8')
else:
self._filename = None

def __enter__(self):
if self._filename:
status = _SECURITY_KEYCHAIN_OPEN(self._filename, self._ref)
else:
status = _SECURITY_KEYCHAIN_COPY_DEFAULT(self._ref)

if status:
raise OSError(status)
return self

def __exit__(self, *args):
if self._ref:
_CORE_RELEASE(self._ref)

def get_generic_password(self, service, account_name):
# type: (str, str) -> str
"""Fetch the password associated with a particular service and account.
:param service: The service that this password is associated with.
:param account_name: The account that this password is associated with.
:return: The value of the password associated with the specified service and account.
"""
service = service.encode('utf-8')
account_name = account_name.encode('utf-8')

length = _ctypes.c_uint32()
contents = _ctypes.c_void_p()
exit_status = _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD(
self._ref,
len(service),
service,
len(account_name),
account_name,
length,
contents,
None,
)

if exit_status:
raise KeychainError(exit_status=exit_status)

value = _ctypes.create_string_buffer(length.value)
_ctypes.memmove(value, contents.value, length.value)
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT(None, contents)
return value.raw.decode('utf-8')

def set_generic_password(self, service, account_name, value):
# type: (str, str, str) -> None
"""Associate a password with a given service and account.
:param service: The service to associate this password with.
:param account_name: The account to associate this password with.
:param value: The string that should be used as the password.
"""
service = service.encode('utf-8')
account_name = account_name.encode('utf-8')
value = value.encode('utf-8')

entry = _ctypes.c_void_p()
find_exit_status = _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD(
self._ref,
len(service),
service,
len(account_name),
account_name,
None,
None,
entry,
)

if not find_exit_status:
modify_exit_status = _SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA(
entry,
None,
len(value),
value,
)
if modify_exit_status:
raise KeychainError(exit_status=modify_exit_status)

elif find_exit_status == KeychainError.ITEM_NOT_FOUND:
add_exit_status = _SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD(
self._ref,
len(service),
service,
len(account_name),
account_name,
len(value),
value,
None
)

if add_exit_status:
raise KeychainError(exit_status=add_exit_status)
else:
raise KeychainError(exit_status=find_exit_status)

def get_internet_password(self, service, username):
# type: (str, str) -> str
""" Fetches a password associated with a domain and username.
NOTE: THIS IS NOT YET IMPLEMENTED
:param service: The website/service that this password is associated with.
:param username: The account that this password is associated with.
:return: The password that was associated with the given service and username.
"""
raise NotImplementedError()

def set_internet_password(self, service, username, value):
# type: (str, str, str) -> None
"""Sets a password associated with a domain and a username.
NOTE: THIS IS NOT YET IMPLEMENTED
:param service: The website/service that this password is associated with.
:param username: The account that this password is associated with.
:param value: The password that should be associated with the given service and username.
"""
raise NotImplementedError()
Loading

0 comments on commit efdfeba

Please sign in to comment.