Skip to content

Release 0.1.0 #25

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

Merged
merged 13 commits into from
Jul 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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