Skip to content

Commit

Permalink
Merge pull request #105 from AzureAD/release-0.3.1
Browse files Browse the repository at this point in the history
Release 0.3.1
  • Loading branch information
rayluo committed Dec 13, 2021
2 parents b90d20e + ea91eda commit 8404023
Show file tree
Hide file tree
Showing 16 changed files with 319 additions and 60 deletions.
109 changes: 109 additions & 0 deletions .github/workflows/python-package.yml
@@ -0,0 +1,109 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: CI

on:
push:
pull_request:
branches: [ dev ]
# This guards against unknown PR until a community member vet it and label it.
types: [ labeled ]

jobs:
ci:

runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, 2.7]
os: [ubuntu-latest, windows-latest, macos-latest]
include:
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-environment-variables-in-a-matrix
- python-version: 3.7
toxenv: "py37"
- python-version: 3.8
toxenv: "py38"
- python-version: 3.9
toxenv: "py39"
- python-version: 2.7
toxenv: "py27"
- python-version: 3.9
os: ubuntu-latest
lint: "true"
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Linux dependencies for Python 2
if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '2.7' }}
run: |
sudo apt update
sudo apt install python-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1 gnome-keyring
- name: Install Linux dependencies for Python 3
if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version != '2.7' }}
run: |
sudo apt update
sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1 gnome-keyring
- name: Install PyGObject on Linux
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
python -m pip install --upgrade pip
python -m pip install pygobject
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pylint tox pytest
pip install .
- name: Lint
if: ${{ matrix.lint == 'true' }}
run: |
pylint msal_extensions
# stop the build if there are Python syntax errors or undefined names
#flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
#flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test on Linux with encryption
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
# Don't know why, but the pytest and "." have to be re-installed again for them to be used
echo "echo secret_placeholder | gnome-keyring-daemon --unlock; pip install pytest .; pytest" > linux_test.sh
chmod +x linux_test.sh
sudo dbus-run-session -- ./linux_test.sh
- name: Test on other platforms without encryption
if: ${{ matrix.os != 'ubuntu-latest' }}
env:
TOXENV: ${{ matrix.toxenv }}
run: |
tox
cd:
needs: ci
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags') || github.ref == 'refs/heads/master')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Build a package for release
run: |
python -m pip install build --user
python -m build --sdist --wheel --outdir dist/ .
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@v1.4.2
if: github.ref == 'refs/heads/master'
with:
user: __token__
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@v1.4.2
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -332,3 +332,5 @@ ASALocalRun/

# MFractors (Xamarin productivity tool) working folder
.mfractor/

.eggs/
2 changes: 2 additions & 0 deletions .pylintrc
Expand Up @@ -2,5 +2,7 @@
good-names=
logger
disable=
super-with-arguments, # For Python 2.x
raise-missing-from, # For Python 2.x
trailing-newlines,
useless-object-inheritance
34 changes: 25 additions & 9 deletions .travis.yml
Expand Up @@ -9,32 +9,42 @@ matrix:
before_install:
- sudo apt update
- sudo apt install python-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
- pip install --upgrade pip
- python: "3.5"
env: TOXENV=py35
os: linux
before_install:
- sudo apt update
- sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
- python: "3.6"
env: TOXENV=py36
os: linux
before_install:
- sudo apt update
- sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
- pip install --upgrade pip

## Somehow cryptography is not able to be compiled and installed on Python 3.6
#- python: "3.6"
# env:
# - TOXENV=py36
# - CRYPTOGRAPHY_DONT_BUILD_RUST=1
# os: linux
# before_install:
# - sudo apt update
# - sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
# - pip install --upgrade pip

- python: "3.7"
env: TOXENV=py37
os: linux
dist: xenial
before_install:
- sudo apt update
- sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
- pip install --upgrade pip
- python: "3.8"
env: TOXENV=py38
os: linux
dist: xenial
before_install:
- sudo apt update
- sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1
- pip install --upgrade pip
- name: "Python 3.7 on macOS"
env: TOXENV=py37
os: osx
Expand All @@ -43,17 +53,23 @@ matrix:
- name: "Python 2.7 on Windows"
env: TOXENV=py27 PATH=/c/Python27:/c/Python27/Scripts:$PATH
os: windows
before_install: choco install python2
before_install:
- choco install python2
- pip install --upgrade --user pip
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
before_install:
- choco install python3 --version 3.5.4
- pip install --upgrade --user pip
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
before_install:
- choco install python3 --version 3.7.3
- pip install --upgrade --user pip
language: shell

install:
Expand Down
30 changes: 30 additions & 0 deletions Dockerfile
@@ -0,0 +1,30 @@
# TODO: Can this Dockerfile use multi-stage build?
# Final size 690MB. (It would be 1.16 GB if started with python:3 as base)
FROM python:3-slim

# Install Generic PyGObject (sans GTK)
#The following somehow won't work:
#RUN apt-get update && apt-get install -y python3-gi python3-gi-cairo
RUN apt-get update && apt-get install -y \
libcairo2-dev \
libgirepository1.0-dev \
python3-dev
RUN pip install "pygobject>=3,<4"

# Install MSAL Extensions dependencies
# Don't know how to get container talk to dbus on host,
# so we choose to create a self-contained image by installing gnome-keyring
RUN apt-get install -y \
gir1.2-secret-1 \
gnome-keyring

# Not strictly necessary, but we include a pytest (which is only 3MB) to facilitate testing.
RUN pip install "pytest>=6,<7"

# Install MSAL Extensions. Upgrade the pinned version number to trigger a new image build.
RUN pip install "msal-extensions==0.3"

# This setup is inspired from https://github.com/jaraco/keyring#using-keyring-on-headless-linux-systems-in-a-docker-container
ENTRYPOINT ["dbus-run-session", "--"]
# Note: gnome-keyring-daemon needs previleged mode, therefore can not be run by a RUN command.
CMD ["sh", "-c", "echo default_secret | gnome-keyring-daemon --unlock; bash"]
15 changes: 15 additions & 0 deletions docker_run.sh
@@ -0,0 +1,15 @@
#!/usr/bin/bash
IMAGE_NAME=msal-extensions:latest

docker build -t $IMAGE_NAME - < Dockerfile

echo "==== Integration Test for Persistence on Linux (libsecret) ===="
echo "After seeing the bash prompt, run the following to test encryption on Linux:"
echo " pip install -e ."
echo " pytest"
docker run --rm -it \
--privileged \
-w /home -v $PWD:/home \
$IMAGE_NAME \
$1

2 changes: 1 addition & 1 deletion msal_extensions/__init__.py
@@ -1,5 +1,5 @@
"""Provides auxiliary functionality to the `msal` package."""
__version__ = "0.3.0"
__version__ = "0.3.1"

import sys

Expand Down
38 changes: 34 additions & 4 deletions msal_extensions/cache_lock.py
Expand Up @@ -2,9 +2,15 @@
import os
import sys
import errno
import portalocker
import time
import logging
from distutils.version import LooseVersion

import portalocker


logger = logging.getLogger(__name__)


class CrossPlatLock(object):
"""Offers a mechanism for waiting until another process is finished interacting with a shared
Expand All @@ -14,7 +20,8 @@ class CrossPlatLock(object):
def __init__(self, lockfile_path):
self._lockpath = lockfile_path
# Support for passing through arguments to the open syscall was added in v1.4.0
open_kwargs = {'buffering': 0} if LooseVersion(portalocker.__version__) >= LooseVersion("1.4.0") else {}
open_kwargs = ({'buffering': 0}
if LooseVersion(portalocker.__version__) >= LooseVersion("1.4.0") else {})
self._lock = portalocker.Lock(
lockfile_path,
mode='wb+',
Expand All @@ -25,9 +32,32 @@ def __init__(self, lockfile_path):
flags=portalocker.LOCK_EX | portalocker.LOCK_NB,
**open_kwargs)

def _try_to_create_lock_file(self):
timeout = 5
check_interval = 0.25
current_time = getattr(time, "monotonic", time.time)
timeout_end = current_time() + timeout
pid = os.getpid()
while timeout_end > current_time():
try:
with open(self._lockpath, 'x'): # pylint: disable=unspecified-encoding
return True
except ValueError: # This needs to be the first clause, for Python 2 to hit it
logger.warning("Python 2 does not support atomic creation of file")
return False
except FileExistsError: # Only Python 3 will reach this clause
logger.debug(
"Process %d found existing lock file, will retry after %f second",
pid, check_interval)
time.sleep(check_interval)
return False

def __enter__(self):
pid = os.getpid()
if not self._try_to_create_lock_file():
logger.warning("Process %d failed to create lock file", pid)
file_handle = self._lock.__enter__()
file_handle.write('{} {}'.format(os.getpid(), sys.argv[0]).encode('utf-8'))
file_handle.write('{} {}'.format(pid, sys.argv[0]).encode('utf-8')) # pylint: disable=consider-using-f-string
return file_handle

def __exit__(self, *args):
Expand All @@ -38,5 +68,5 @@ def __exit__(self, *args):
# file for itself.
os.remove(self._lockpath)
except OSError as ex: # pylint: disable=invalid-name
if ex.errno != errno.ENOENT and ex.errno != errno.EACCES:
if ex.errno not in (errno.ENOENT, errno.EACCES):
raise
29 changes: 12 additions & 17 deletions msal_extensions/libsecret.py
Expand Up @@ -13,33 +13,29 @@
pip install wheel
PYGOBJECT_WITHOUT_PYCAIRO=1 pip install --no-build-isolation pygobject
"""
import logging

logger = logging.getLogger(__name__)

try:
import gi # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/wiki/Encryption-on-Linux
import gi # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/wiki/Encryption-on-Linux # pylint: disable=line-too-long
except ImportError:
logger.exception(
"""Runtime dependency of PyGObject is missing.
raise ImportError("""Unable to import module 'gi'
Runtime dependency of PyGObject is missing.
Depends on your Linux distro, you could install it system-wide by something like:
sudo apt install python3-gi python3-gi-cairo gir1.2-secret-1
If necessary, please refer to PyGObject's doc:
https://pygobject.readthedocs.io/en/latest/getting_started.html
""")
raise
""") # Message via exception rather than log

try:
# pylint: disable=no-name-in-module
gi.require_version("Secret", "1") # Would require a package gir1.2-secret-1
# pylint: disable=wrong-import-position
from gi.repository import Secret # Would require a package gir1.2-secret-1
except (ValueError, ImportError):
logger.exception(
except (ValueError, ImportError) as ex:
raise type(ex)(
"""Require a package "gir1.2-secret-1" which could be installed by:
sudo apt install gir1.2-secret-1
""")
raise
""") # Message via exception rather than log


class LibSecretAgent(object):
"""A loader/saver built on top of low-level libsecret"""
Expand All @@ -51,7 +47,7 @@ def __init__( # pylint: disable=too-many-arguments
label="", # Helpful when visualizing secrets by other viewers
attribute_types=None, # {name: SchemaAttributeType, ...}
collection=None, # None means default collection
): # pylint: disable=bad-continuation
):
"""This agent is built on top of lower level libsecret API.
Content stored via libsecret is associated with a bunch of attributes.
Expand Down Expand Up @@ -126,14 +122,13 @@ def trial_run():
agent.save(payload) # It would fail when running inside an SSH session
assert agent.load() == payload # This line is probably not reachable
agent.clear()
except (gi.repository.GLib.Error, AssertionError):
except (gi.repository.GLib.Error, AssertionError): # pylint: disable=no-member
# https://pygobject.readthedocs.io/en/latest/guide/api/error_handling.html#examples
message = """libsecret did not perform properly.
* If you encountered error "Remote error from secret service:
org.freedesktop.DBus.Error.ServiceUnknown",
you may need to install gnome-keyring package.
* Headless mode (such as in an ssh session) is not supported.
"""
logger.exception(message) # This log contains trace stack for debugging
logger.warning(message) # This is visible by default
raise
raise RuntimeError(message) # Message via exception rather than log

0 comments on commit 8404023

Please sign in to comment.