2017-01-05 21:20:13 +03:00
|
|
|
# Copyright (c) Microsoft Corporation
|
|
|
|
#
|
|
|
|
# All rights reserved.
|
|
|
|
#
|
|
|
|
# MIT License
|
|
|
|
#
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
|
# copy of this software and associated documentation files (the "Software"),
|
|
|
|
# to deal in the Software without restriction, including without limitation
|
|
|
|
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
|
|
# and/or sell copies of the Software, and to permit persons to whom the
|
|
|
|
# Software is furnished to do so, subject to the following conditions:
|
|
|
|
#
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
|
|
# all copies or substantial portions of the Software.
|
|
|
|
#
|
|
|
|
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
|
|
# DEALINGS IN THE SOFTWARE.
|
|
|
|
|
|
|
|
# compat imports
|
|
|
|
from __future__ import (
|
|
|
|
absolute_import, division, print_function, unicode_literals
|
|
|
|
)
|
|
|
|
from builtins import ( # noqa
|
|
|
|
bytes, dict, int, list, object, range, str, ascii, chr, hex, input,
|
|
|
|
next, oct, open, pow, round, super, filter, map, zip)
|
|
|
|
# stdlib imports
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import zlib
|
|
|
|
# non-stdlib imports
|
|
|
|
import azure.common.credentials
|
|
|
|
import azure.keyvault
|
2017-09-26 21:08:37 +03:00
|
|
|
import ruamel.yaml
|
2017-01-05 21:20:13 +03:00
|
|
|
# local imports
|
|
|
|
from . import settings
|
|
|
|
from . import util
|
|
|
|
|
|
|
|
# create logger
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
util.setup_logger(logger)
|
|
|
|
# global defines
|
|
|
|
_SECRET_ENCODED_FORMAT_KEY = 'format'
|
|
|
|
_SECRET_ENCODED_FORMAT_VALUE = 'zlib+base64'
|
|
|
|
|
|
|
|
|
2017-05-11 19:21:20 +03:00
|
|
|
def _explode_secret_id(uri):
|
|
|
|
# type: (str) -> Tuple[str, str, str]
|
|
|
|
"""Explode Secret Id URI into parts
|
|
|
|
:param str uri: secret id uri
|
|
|
|
:rtype: tuple
|
|
|
|
:return: base url, secret name, version
|
|
|
|
"""
|
|
|
|
tmp = uri.split('/')
|
|
|
|
base_url = '/'.join(tmp[:3])
|
|
|
|
nparam = len(tmp[4:])
|
|
|
|
if nparam == 1:
|
|
|
|
return base_url, tmp[4], ''
|
|
|
|
elif nparam == 2:
|
|
|
|
return base_url, tmp[4], tmp[5]
|
|
|
|
else:
|
|
|
|
raise ValueError(
|
|
|
|
'cannot handle keyvault secret id uri: {}'.format(uri))
|
|
|
|
|
|
|
|
|
2017-09-26 21:08:37 +03:00
|
|
|
def fetch_credentials_conf(
|
2017-01-05 21:20:13 +03:00
|
|
|
client, keyvault_uri, keyvault_credentials_secret_id):
|
|
|
|
# type: (azure.keyvault.KeyVaultClient, str, str) -> dict
|
2017-09-26 21:08:37 +03:00
|
|
|
"""Fetch credentials conf from KeyVault
|
2017-01-05 21:20:13 +03:00
|
|
|
:param azure.keyvault.KeyVaultClient client: keyvault client
|
|
|
|
:param str keyvault_uri: keyvault uri
|
2017-09-26 21:08:37 +03:00
|
|
|
:param str keyvault_credentials_secret_id: secret id for creds conf
|
2017-01-05 21:20:13 +03:00
|
|
|
:rtype: dict
|
|
|
|
:return: credentials dict
|
|
|
|
"""
|
2017-03-03 07:17:35 +03:00
|
|
|
if client is None:
|
|
|
|
raise RuntimeError(
|
|
|
|
'KeyVault client not initialized, please ensure proper AAD '
|
|
|
|
'credentials and KeyVault parameters have been provided')
|
2017-09-26 21:08:37 +03:00
|
|
|
logger.debug('fetching credentials conf from keyvault')
|
2017-01-19 21:15:32 +03:00
|
|
|
if util.is_none_or_empty(keyvault_credentials_secret_id):
|
|
|
|
raise RuntimeError(
|
2017-09-26 21:08:37 +03:00
|
|
|
'cannot fetch credentials conf from keyvault without a valid '
|
2017-01-19 21:15:32 +03:00
|
|
|
'keyvault credentials secret id')
|
2017-05-11 19:21:20 +03:00
|
|
|
cred = client.get_secret(
|
|
|
|
*_explode_secret_id(keyvault_credentials_secret_id))
|
2017-01-05 21:20:13 +03:00
|
|
|
if util.is_none_or_empty(cred.value):
|
|
|
|
raise ValueError(
|
2017-09-26 21:08:37 +03:00
|
|
|
'credential conf from secret id {} is invalid'.format(
|
2017-01-05 21:20:13 +03:00
|
|
|
keyvault_credentials_secret_id))
|
|
|
|
# check for encoding and decode/decompress if necessary
|
|
|
|
if cred.tags is not None:
|
|
|
|
try:
|
|
|
|
if (cred.tags[_SECRET_ENCODED_FORMAT_KEY] ==
|
|
|
|
_SECRET_ENCODED_FORMAT_VALUE):
|
|
|
|
cred.value = util.decode_string(
|
|
|
|
zlib.decompress(util.base64_decode_string(cred.value)))
|
|
|
|
else:
|
|
|
|
raise RuntimeError(
|
|
|
|
'{} encoding format is invalid'.format(
|
|
|
|
cred.tags[_SECRET_ENCODED_FORMAT_KEY]))
|
|
|
|
except KeyError:
|
|
|
|
pass
|
2017-09-26 21:08:37 +03:00
|
|
|
return ruamel.yaml.load(cred.value, Loader=ruamel.yaml.RoundTripLoader)
|
2017-01-05 21:20:13 +03:00
|
|
|
|
|
|
|
|
2017-09-26 21:08:37 +03:00
|
|
|
def store_credentials_conf(client, config, keyvault_uri, secret_name):
|
2017-01-05 21:20:13 +03:00
|
|
|
# type: (azure.keyvault.KeyVaultClient, dict, str, str) -> None
|
2017-09-26 21:08:37 +03:00
|
|
|
"""Store credentials conf in KeyVault
|
2017-01-05 21:20:13 +03:00
|
|
|
:param azure.keyvault.KeyVaultClient client: keyvault client
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param str keyvault_uri: keyvault uri
|
2017-09-26 21:08:37 +03:00
|
|
|
:param str secret_name: secret name for creds conf
|
2017-01-05 21:20:13 +03:00
|
|
|
"""
|
2017-03-03 07:17:35 +03:00
|
|
|
if client is None:
|
|
|
|
raise RuntimeError(
|
|
|
|
'KeyVault client not initialized, please ensure proper AAD '
|
|
|
|
'credentials and KeyVault parameters have been provided')
|
2017-01-10 22:21:11 +03:00
|
|
|
creds = {
|
|
|
|
'credentials': settings.raw_credentials(config, True)
|
|
|
|
}
|
2017-01-05 21:20:13 +03:00
|
|
|
creds = json.dumps(creds).encode('utf8')
|
|
|
|
# first zlib compress and encode as base64
|
|
|
|
encoded = util.base64_encode_string(zlib.compress(creds))
|
|
|
|
# store secret
|
|
|
|
logger.debug('storing secret in keyvault {} with name {}'.format(
|
|
|
|
keyvault_uri, secret_name))
|
|
|
|
bundle = client.set_secret(
|
|
|
|
keyvault_uri, secret_name, encoded,
|
|
|
|
tags={_SECRET_ENCODED_FORMAT_KEY: _SECRET_ENCODED_FORMAT_VALUE}
|
|
|
|
)
|
|
|
|
logger.info('keyvault secret id for name {}: {}'.format(
|
|
|
|
secret_name,
|
2017-09-26 21:08:37 +03:00
|
|
|
azure.keyvault.KeyVaultId.parse_secret_id(bundle.id).base_id))
|
2017-01-05 21:20:13 +03:00
|
|
|
|
|
|
|
|
|
|
|
def delete_secret(client, keyvault_uri, secret_name):
|
|
|
|
# type: (azure.keyvault.KeyVaultClient, str, str) -> None
|
|
|
|
"""Delete secret from KeyVault
|
|
|
|
:param azure.keyvault.KeyVaultClient client: keyvault client
|
|
|
|
:param str keyvault_uri: keyvault uri
|
2017-09-26 21:08:37 +03:00
|
|
|
:param str secret_name: secret name for creds conf
|
2017-01-05 21:20:13 +03:00
|
|
|
"""
|
2017-03-03 07:17:35 +03:00
|
|
|
if client is None:
|
|
|
|
raise RuntimeError(
|
|
|
|
'KeyVault client not initialized, please ensure proper AAD '
|
|
|
|
'credentials and KeyVault parameters have been provided')
|
2017-01-05 21:20:13 +03:00
|
|
|
logger.info('deleting secret in keyvault {} with name {}'.format(
|
|
|
|
keyvault_uri, secret_name))
|
|
|
|
client.delete_secret(keyvault_uri, secret_name)
|
|
|
|
|
|
|
|
|
|
|
|
def list_secrets(client, keyvault_uri):
|
|
|
|
# type: (azure.keyvault.KeyVaultClient, str) -> None
|
|
|
|
"""List all secret ids and metadata from KeyVault
|
|
|
|
:param azure.keyvault.KeyVaultClient client: keyvault client
|
|
|
|
:param str keyvault_uri: keyvault uri
|
|
|
|
"""
|
2017-03-03 07:17:35 +03:00
|
|
|
if client is None:
|
|
|
|
raise RuntimeError(
|
|
|
|
'KeyVault client not initialized, please ensure proper AAD '
|
|
|
|
'credentials and KeyVault parameters have been provided')
|
2017-01-05 21:20:13 +03:00
|
|
|
logger.debug('listing secret ids in keyvault {}'.format(keyvault_uri))
|
|
|
|
secrets = client.get_secrets(keyvault_uri)
|
|
|
|
for secret in secrets:
|
|
|
|
logger.info('id={} enabled={} tags={} content_type={}'.format(
|
|
|
|
secret.id, secret.attributes.enabled, secret.tags,
|
|
|
|
secret.content_type))
|
|
|
|
|
|
|
|
|
2017-01-19 21:15:32 +03:00
|
|
|
def get_secret(client, secret_id, value_is_json=False):
|
|
|
|
# type: (azure.keyvault.KeyVaultClient, str, bool) -> str
|
|
|
|
"""Get secret from KeyVault
|
|
|
|
:param azure.keyvault.KeyVaultClient client: keyvault client
|
|
|
|
:param str secret_id: secret id to retrieve
|
2017-09-26 21:08:37 +03:00
|
|
|
:param bool value_is_json: expected value is json or yaml
|
2017-01-19 21:15:32 +03:00
|
|
|
:rtype: str
|
|
|
|
:return: secret value
|
|
|
|
"""
|
|
|
|
if client is None:
|
|
|
|
raise RuntimeError(
|
|
|
|
'cannot retrieve secret {} with invalid KeyVault client'.format(
|
|
|
|
secret_id))
|
2017-05-11 19:21:20 +03:00
|
|
|
value = client.get_secret(*_explode_secret_id(secret_id)).value
|
2017-01-19 21:15:32 +03:00
|
|
|
if value_is_json and util.is_not_empty(value):
|
2017-09-26 21:08:37 +03:00
|
|
|
return ruamel.yaml.load(value, Loader=ruamel.yaml.RoundTripLoader)
|
2017-01-19 21:15:32 +03:00
|
|
|
else:
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2017-01-05 21:20:13 +03:00
|
|
|
def parse_secret_ids(client, config):
|
|
|
|
# type: (azure.keyvault.KeyVaultClient, dict) -> None
|
|
|
|
"""Parse secret ids in credentials, fetch values from KeyVault, and add
|
|
|
|
appropriate values to config
|
|
|
|
:param azure.keyvault.KeyVaultClient client: keyvault client
|
|
|
|
:param dict config: configuration dict
|
|
|
|
"""
|
|
|
|
# batch account key
|
|
|
|
secid = settings.credentials_batch_account_key_secret_id(config)
|
|
|
|
if secid is not None:
|
|
|
|
logger.debug('fetching batch account key from keyvault')
|
2017-01-19 21:15:32 +03:00
|
|
|
bakey = get_secret(client, secid)
|
2017-01-05 21:20:13 +03:00
|
|
|
if util.is_none_or_empty(bakey):
|
|
|
|
raise ValueError(
|
|
|
|
'batch account key retrieved for secret id {} is '
|
|
|
|
'invalid'.format(secid))
|
|
|
|
settings.set_credentials_batch_account_key(config, bakey)
|
|
|
|
# storage account keys
|
|
|
|
for ssel in settings.iterate_storage_credentials(config):
|
|
|
|
secid = settings.credentials_storage_account_key_secret_id(
|
|
|
|
config, ssel)
|
|
|
|
if secid is None:
|
|
|
|
continue
|
|
|
|
logger.debug(
|
|
|
|
'fetching storage account key for link {} from keyvault'.format(
|
|
|
|
ssel))
|
2017-01-19 21:15:32 +03:00
|
|
|
sakey = get_secret(client, secid)
|
2017-01-05 21:20:13 +03:00
|
|
|
if util.is_none_or_empty(sakey):
|
|
|
|
raise ValueError(
|
|
|
|
'storage account key retrieved for secret id {} is '
|
|
|
|
'invalid'.format(secid))
|
|
|
|
settings.set_credentials_storage_account_key(config, ssel, sakey)
|
|
|
|
# docker registry passwords
|
2017-11-08 05:16:30 +03:00
|
|
|
for reg in settings.credentials_iterate_registry_servers(config, True):
|
|
|
|
secid = settings.credentials_registry_password_secret_id(
|
|
|
|
config, reg, True)
|
2017-01-05 21:20:13 +03:00
|
|
|
if secid is None:
|
|
|
|
continue
|
|
|
|
logger.debug(
|
2017-11-08 05:16:30 +03:00
|
|
|
('fetching Docker registry password for registry {} '
|
2017-01-05 21:20:13 +03:00
|
|
|
'from keyvault').format(reg))
|
2017-01-19 21:15:32 +03:00
|
|
|
password = get_secret(client, secid)
|
2017-01-05 21:20:13 +03:00
|
|
|
if util.is_none_or_empty(password):
|
|
|
|
raise ValueError(
|
2017-11-08 05:16:30 +03:00
|
|
|
'Docker registry password retrieved for secret id {} is '
|
2017-01-05 21:20:13 +03:00
|
|
|
'invalid'.format(secid))
|
2017-11-08 05:16:30 +03:00
|
|
|
settings.set_credentials_registry_password(config, reg, True, password)
|
|
|
|
# singularity registry passwords
|
|
|
|
for reg in settings.credentials_iterate_registry_servers(config, False):
|
|
|
|
secid = settings.credentials_registry_password_secret_id(
|
|
|
|
config, reg, False)
|
|
|
|
if secid is None:
|
|
|
|
continue
|
|
|
|
logger.debug(
|
|
|
|
('fetching Singularity registry password for registry {} '
|
|
|
|
'from keyvault').format(reg))
|
|
|
|
password = get_secret(client, secid)
|
|
|
|
if util.is_none_or_empty(password):
|
|
|
|
raise ValueError(
|
|
|
|
'Singularity registry password retrieved for secret id {} is '
|
|
|
|
'invalid'.format(secid))
|
|
|
|
settings.set_credentials_registry_password(
|
|
|
|
config, reg, False, password)
|