2021-11-23 06:09:18 +00:00
|
|
|
# vim: ts=4 et:
|
|
|
|
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
import urllib.error
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
from email.utils import parsedate
|
|
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
|
|
|
|
|
|
class IdentityBrokerClient:
|
|
|
|
"""Client for identity broker
|
|
|
|
|
|
|
|
Export IDENTITY_BROKER_ENDPOINT to override the default broker endpoint.
|
|
|
|
Export IDENTITY_BROKER_API_KEY to specify an API key for the broker.
|
|
|
|
|
|
|
|
See README_BROKER.md for more information and a spec.
|
|
|
|
"""
|
|
|
|
|
|
|
|
_DEFAULT_ENDPOINT = 'https://aws-access.crute.us/api/account'
|
|
|
|
_DEFAULT_ACCOUNT = 'alpine-amis-user'
|
|
|
|
_LOGFORMAT = '%(name)s - %(levelname)s - %(message)s'
|
|
|
|
|
|
|
|
def __init__(self, endpoint=None, key=None, account=None, debug=False):
|
|
|
|
# log to STDOUT so that it's not all red when executed by Packer
|
|
|
|
self._logger = logging.getLogger('identity-broker')
|
|
|
|
self._logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
|
|
console = logging.StreamHandler(sys.stdout)
|
|
|
|
console.setFormatter(logging.Formatter(self._LOGFORMAT))
|
|
|
|
self._logger.addHandler(console)
|
|
|
|
|
|
|
|
self._endpoint = os.environ.get('IDENTITY_BROKER_ENDPOINT') or endpoint \
|
|
|
|
or self._DEFAULT_ENDPOINT
|
|
|
|
self._key = os.environ.get('IDENTITY_BROKER_API_KEY') or key
|
|
|
|
self._account = account or self._DEFAULT_ACCOUNT
|
|
|
|
if not self._key:
|
|
|
|
raise Exception('No identity broker key found')
|
|
|
|
|
|
|
|
self._headers = {
|
|
|
|
'Accept': 'application/vnd.broker.v2+json',
|
|
|
|
'X-API-Key': self._key
|
|
|
|
}
|
|
|
|
self._cache = {}
|
|
|
|
self._expires = {}
|
|
|
|
self._default_region = {}
|
|
|
|
|
|
|
|
def _is_cache_valid(self, path):
|
|
|
|
if path not in self._cache:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# path is subject to expiry AND its time has passed
|
|
|
|
if self._expires[path] and self._expires[path] < datetime.utcnow():
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _get(self, path):
|
2021-11-28 23:04:28 +00:00
|
|
|
self._logger.debug("request: %s", path)
|
2021-11-23 06:09:18 +00:00
|
|
|
if not self._is_cache_valid(path):
|
|
|
|
while True: # to handle rate limits
|
|
|
|
try:
|
|
|
|
res = urlopen(Request(path, headers=self._headers))
|
|
|
|
except urllib.error.HTTPError as ex:
|
|
|
|
if ex.status == 401:
|
|
|
|
raise Exception('Expired or invalid identity broker token')
|
|
|
|
|
|
|
|
if ex.status == 406:
|
|
|
|
raise Exception('Invalid or malformed identity broker token')
|
|
|
|
|
|
|
|
# TODO: will this be entirely handled by the 401 above?
|
|
|
|
if ex.headers.get('Location') == '/logout':
|
|
|
|
raise Exception('Identity broker token is expired')
|
|
|
|
|
|
|
|
if ex.status == 429:
|
|
|
|
self._logger.warning(
|
|
|
|
'Rate-limited by identity broker, sleeping 30 seconds')
|
|
|
|
time.sleep(30)
|
|
|
|
continue
|
|
|
|
|
|
|
|
raise ex
|
|
|
|
|
|
|
|
if res.status not in {200, 429}:
|
|
|
|
raise Exception(res.reason)
|
|
|
|
|
|
|
|
# never expires without valid RFC 1123 Expires header
|
|
|
|
if expires := res.getheader('Expires'):
|
|
|
|
expires = parsedate(expires)
|
|
|
|
# convert RFC 1123 to datetime, if parsed successfully
|
|
|
|
expires = datetime(*expires[:6])
|
|
|
|
|
|
|
|
self._expires[path] = expires
|
|
|
|
self._cache[path] = json.load(res)
|
|
|
|
break
|
|
|
|
|
2021-11-28 23:04:28 +00:00
|
|
|
self._logger.debug("response: %s", self._cache[path])
|
2021-11-23 06:09:18 +00:00
|
|
|
return self._cache[path]
|
|
|
|
|
|
|
|
def get_credentials_url(self, vendor):
|
|
|
|
accounts = self._get(self._endpoint)
|
|
|
|
if vendor not in accounts:
|
|
|
|
raise Exception(f'No {vendor} credentials found')
|
|
|
|
|
|
|
|
for account in accounts[vendor]:
|
|
|
|
if account['short_name'] == self._account:
|
|
|
|
return account['credentials_url']
|
|
|
|
|
|
|
|
raise Exception('No account credentials found')
|
|
|
|
|
|
|
|
def get_regions(self, vendor):
|
|
|
|
out = {}
|
|
|
|
|
|
|
|
for region in self._get(self.get_credentials_url(vendor)):
|
|
|
|
if region['enabled']:
|
|
|
|
out[region['name']] = region['credentials_url']
|
|
|
|
|
|
|
|
if region['default']:
|
|
|
|
self._default_region[vendor] = region['name']
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
def get_default_region(self, vendor):
|
|
|
|
if vendor not in self._default_region:
|
|
|
|
self.get_regions(vendor)
|
|
|
|
|
|
|
|
return self._default_region.get(vendor)
|
|
|
|
|
|
|
|
def get_credentials(self, vendor, region=None):
|
2021-11-28 23:04:28 +00:00
|
|
|
if not region:
|
|
|
|
region = self.get_default_region(vendor)
|
|
|
|
|
2021-11-23 06:09:18 +00:00
|
|
|
return self._get(self.get_regions(vendor)[region])
|