azure-devops/azext_devops/dev/common/credential_store.py (135 lines of code) (raw):
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
import os
import sys
from knack.util import CLIError, ensure_dir
from knack.log import get_logger
from six.moves import configparser
from .config import AZ_DEVOPS_GLOBAL_CONFIG_DIR
from .pip_helper import install_keyring
logger = get_logger(__name__)
class CredentialStore:
def __init__(self):
self._initialize_keyring()
def set_password(self, key, token):
try:
import keyring
except ImportError:
install_keyring()
self._initialize_keyring()
import keyring
try:
# check for and delete existing credential
old_token = keyring.get_password(key, self._USERNAME)
if old_token is not None:
keyring.delete_password(key, self._USERNAME)
logger.debug('Setting credential: %s', key)
keyring.set_password(key, self._USERNAME, token)
except Exception as ex: # pylint: disable=broad-except
# store credentials in azuredevops config directory if keyring is missing or malfunctioning
if sys.platform.startswith(self._LINUX_PLATFORM):
logger.warning('Failed to store PAT using keyring; falling back to file storage.')
logger.warning('You can clear the stored credential by running az devops logout.')
logger.warning('Refer https://aka.ms/azure-devops-cli-auth to know more on sign in with PAT.')
logger.debug('Keyring failed. ERROR :%s', ex)
logger.debug('Storing credentials in the file: %s', self._PAT_FILE)
creds_list = self._get_credentials_list()
if key not in creds_list.sections():
creds_list.add_section(key)
logger.debug('Added new entry to PAT file : %s ', key)
creds_list.set(key, self._USERNAME, token)
self._commit_change(creds_list)
else:
raise CLIError(ex)
def get_password(self, key):
try:
import keyring
except ImportError:
return None
token = None
try:
token = keyring.get_password(key, self._USERNAME)
except Exception as ex: # pylint: disable=broad-except
# fetch credentials from file if keyring is missing or malfunctioning
if sys.platform.startswith(self._LINUX_PLATFORM):
token = None
else:
raise CLIError(ex)
# look for credential in file too for linux if token is None
if token is None and sys.platform.startswith(self._LINUX_PLATFORM):
token = self.get_PAT_from_file(key)
return token
def clear_password(self, key):
try:
import keyring
except ImportError:
install_keyring()
self._initialize_keyring()
import keyring
if sys.platform.startswith(self._LINUX_PLATFORM):
keyring_token = None
file_token = None
try:
keyring_token = keyring.get_password(key, self._USERNAME)
if keyring_token:
keyring.delete_password(key, self._USERNAME)
except Exception as ex: # pylint: disable=broad-except
logger.debug("%s", ex)
finally:
file_token = self.get_PAT_from_file(key)
if file_token:
self.delete_PAT_from_file(key)
if (keyring_token is None and file_token is None):
raise CLIError(self._CRDENTIAL_NOT_FOUND_MSG)
else:
try:
keyring.delete_password(key, self._USERNAME)
except keyring.errors.PasswordDeleteError:
raise CLIError(self._CRDENTIAL_NOT_FOUND_MSG)
except RuntimeError as ex: # pylint: disable=broad-except
raise CLIError(ex)
def get_PAT_from_file(self, key):
ensure_dir(AZ_DEVOPS_GLOBAL_CONFIG_DIR)
logger.debug('Keyring not configured properly or package not found.'
'Looking for credentials with key:%s in the file: %s', key, self._PAT_FILE)
creds_list = self._get_credentials_list()
try:
return creds_list.get(key, self._USERNAME)
except (configparser.NoOptionError, configparser.NoSectionError):
return None
def delete_PAT_from_file(self, key):
logger.debug('Keyring not configured properly or package not found.'
'Looking for credentials with key:%s in the file: %s', key, self._PAT_FILE)
creds_list = self._get_credentials_list()
if key not in creds_list.sections():
raise CLIError(self._CRDENTIAL_NOT_FOUND_MSG)
creds_list.remove_section(key)
self._commit_change(creds_list)
@staticmethod
def _get_config_parser():
if sys.version_info.major == 3:
return configparser.ConfigParser(interpolation=None)
return configparser.ConfigParser()
@staticmethod
def _get_credentials_list():
try:
credential_list = CredentialStore._get_config_parser()
credential_list.read(CredentialStore._PAT_FILE)
return credential_list
except BaseException: # pylint: disable=broad-except
return CredentialStore._get_config_parser()
@staticmethod
def _commit_change(credential_list):
with open(CredentialStore._PAT_FILE, 'w+') as creds_file:
credential_list.write(creds_file)
@staticmethod
def _initialize_keyring():
try:
import keyring
except ImportError:
return
def _only_builtin(backend):
return (
backend.__module__.startswith('keyring.backends.') and
'chain' not in backend.__module__
)
keyring.core.init_backend(_only_builtin)
logger.debug('Keyring backend : %s', keyring.get_keyring())
# a value is required for the python config file that gets generated on some operating systems.
_USERNAME = 'Personal Access Token'
_LINUX_PLATFORM = 'linux'
_PAT_FILE = os.path.join(AZ_DEVOPS_GLOBAL_CONFIG_DIR, 'personalAccessTokens')
_CRDENTIAL_NOT_FOUND_MSG = 'The credential was not found'