Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

auth.py 13KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import base64
  2. import json
  3. import logging
  4. from . import credentials
  5. from . import errors
  6. from .utils import config
  7. INDEX_NAME = 'docker.io'
  8. INDEX_URL = f'https://index.{INDEX_NAME}/v1/'
  9. TOKEN_USERNAME = '<token>'
  10. log = logging.getLogger(__name__)
  11. def resolve_repository_name(repo_name):
  12. if '://' in repo_name:
  13. raise errors.InvalidRepository(
  14. f'Repository name cannot contain a scheme ({repo_name})'
  15. )
  16. index_name, remote_name = split_repo_name(repo_name)
  17. if index_name[0] == '-' or index_name[-1] == '-':
  18. raise errors.InvalidRepository(
  19. 'Invalid index name ({}). Cannot begin or end with a'
  20. ' hyphen.'.format(index_name)
  21. )
  22. return resolve_index_name(index_name), remote_name
  23. def resolve_index_name(index_name):
  24. index_name = convert_to_hostname(index_name)
  25. if index_name == 'index.' + INDEX_NAME:
  26. index_name = INDEX_NAME
  27. return index_name
  28. def get_config_header(client, registry):
  29. log.debug('Looking for auth config')
  30. if not client._auth_configs or client._auth_configs.is_empty:
  31. log.debug(
  32. "No auth config in memory - loading from filesystem"
  33. )
  34. client._auth_configs = load_config(credstore_env=client.credstore_env)
  35. authcfg = resolve_authconfig(
  36. client._auth_configs, registry, credstore_env=client.credstore_env
  37. )
  38. # Do not fail here if no authentication exists for this
  39. # specific registry as we can have a readonly pull. Just
  40. # put the header if we can.
  41. if authcfg:
  42. log.debug('Found auth config')
  43. # auth_config needs to be a dict in the format used by
  44. # auth.py username , password, serveraddress, email
  45. return encode_header(authcfg)
  46. log.debug('No auth config found')
  47. return None
  48. def split_repo_name(repo_name):
  49. parts = repo_name.split('/', 1)
  50. if len(parts) == 1 or (
  51. '.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost'
  52. ):
  53. # This is a docker index repo (ex: username/foobar or ubuntu)
  54. return INDEX_NAME, repo_name
  55. return tuple(parts)
  56. def get_credential_store(authconfig, registry):
  57. if not isinstance(authconfig, AuthConfig):
  58. authconfig = AuthConfig(authconfig)
  59. return authconfig.get_credential_store(registry)
  60. class AuthConfig(dict):
  61. def __init__(self, dct, credstore_env=None):
  62. if 'auths' not in dct:
  63. dct['auths'] = {}
  64. self.update(dct)
  65. self._credstore_env = credstore_env
  66. self._stores = {}
  67. @classmethod
  68. def parse_auth(cls, entries, raise_on_error=False):
  69. """
  70. Parses authentication entries
  71. Args:
  72. entries: Dict of authentication entries.
  73. raise_on_error: If set to true, an invalid format will raise
  74. InvalidConfigFile
  75. Returns:
  76. Authentication registry.
  77. """
  78. conf = {}
  79. for registry, entry in entries.items():
  80. if not isinstance(entry, dict):
  81. log.debug(
  82. 'Config entry for key {} is not auth config'.format(
  83. registry
  84. )
  85. )
  86. # We sometimes fall back to parsing the whole config as if it
  87. # was the auth config by itself, for legacy purposes. In that
  88. # case, we fail silently and return an empty conf if any of the
  89. # keys is not formatted properly.
  90. if raise_on_error:
  91. raise errors.InvalidConfigFile(
  92. 'Invalid configuration for registry {}'.format(
  93. registry
  94. )
  95. )
  96. return {}
  97. if 'identitytoken' in entry:
  98. log.debug(
  99. 'Found an IdentityToken entry for registry {}'.format(
  100. registry
  101. )
  102. )
  103. conf[registry] = {
  104. 'IdentityToken': entry['identitytoken']
  105. }
  106. continue # Other values are irrelevant if we have a token
  107. if 'auth' not in entry:
  108. # Starting with engine v1.11 (API 1.23), an empty dictionary is
  109. # a valid value in the auths config.
  110. # https://github.com/docker/compose/issues/3265
  111. log.debug(
  112. 'Auth data for {} is absent. Client might be using a '
  113. 'credentials store instead.'.format(registry)
  114. )
  115. conf[registry] = {}
  116. continue
  117. username, password = decode_auth(entry['auth'])
  118. log.debug(
  119. 'Found entry (registry={}, username={})'
  120. .format(repr(registry), repr(username))
  121. )
  122. conf[registry] = {
  123. 'username': username,
  124. 'password': password,
  125. 'email': entry.get('email'),
  126. 'serveraddress': registry,
  127. }
  128. return conf
  129. @classmethod
  130. def load_config(cls, config_path, config_dict, credstore_env=None):
  131. """
  132. Loads authentication data from a Docker configuration file in the given
  133. root directory or if config_path is passed use given path.
  134. Lookup priority:
  135. explicit config_path parameter > DOCKER_CONFIG environment
  136. variable > ~/.docker/config.json > ~/.dockercfg
  137. """
  138. if not config_dict:
  139. config_file = config.find_config_file(config_path)
  140. if not config_file:
  141. return cls({}, credstore_env)
  142. try:
  143. with open(config_file) as f:
  144. config_dict = json.load(f)
  145. except (OSError, KeyError, ValueError) as e:
  146. # Likely missing new Docker config file or it's in an
  147. # unknown format, continue to attempt to read old location
  148. # and format.
  149. log.debug(e)
  150. return cls(_load_legacy_config(config_file), credstore_env)
  151. res = {}
  152. if config_dict.get('auths'):
  153. log.debug("Found 'auths' section")
  154. res.update({
  155. 'auths': cls.parse_auth(
  156. config_dict.pop('auths'), raise_on_error=True
  157. )
  158. })
  159. if config_dict.get('credsStore'):
  160. log.debug("Found 'credsStore' section")
  161. res.update({'credsStore': config_dict.pop('credsStore')})
  162. if config_dict.get('credHelpers'):
  163. log.debug("Found 'credHelpers' section")
  164. res.update({'credHelpers': config_dict.pop('credHelpers')})
  165. if res:
  166. return cls(res, credstore_env)
  167. log.debug(
  168. "Couldn't find auth-related section ; attempting to interpret "
  169. "as auth-only file"
  170. )
  171. return cls({'auths': cls.parse_auth(config_dict)}, credstore_env)
  172. @property
  173. def auths(self):
  174. return self.get('auths', {})
  175. @property
  176. def creds_store(self):
  177. return self.get('credsStore', None)
  178. @property
  179. def cred_helpers(self):
  180. return self.get('credHelpers', {})
  181. @property
  182. def is_empty(self):
  183. return (
  184. not self.auths and not self.creds_store and not self.cred_helpers
  185. )
  186. def resolve_authconfig(self, registry=None):
  187. """
  188. Returns the authentication data from the given auth configuration for a
  189. specific registry. As with the Docker client, legacy entries in the
  190. config with full URLs are stripped down to hostnames before checking
  191. for a match. Returns None if no match was found.
  192. """
  193. if self.creds_store or self.cred_helpers:
  194. store_name = self.get_credential_store(registry)
  195. if store_name is not None:
  196. log.debug(
  197. f'Using credentials store "{store_name}"'
  198. )
  199. cfg = self._resolve_authconfig_credstore(registry, store_name)
  200. if cfg is not None:
  201. return cfg
  202. log.debug('No entry in credstore - fetching from auth dict')
  203. # Default to the public index server
  204. registry = resolve_index_name(registry) if registry else INDEX_NAME
  205. log.debug(f"Looking for auth entry for {repr(registry)}")
  206. if registry in self.auths:
  207. log.debug(f"Found {repr(registry)}")
  208. return self.auths[registry]
  209. for key, conf in self.auths.items():
  210. if resolve_index_name(key) == registry:
  211. log.debug(f"Found {repr(key)}")
  212. return conf
  213. log.debug("No entry found")
  214. return None
  215. def _resolve_authconfig_credstore(self, registry, credstore_name):
  216. if not registry or registry == INDEX_NAME:
  217. # The ecosystem is a little schizophrenic with index.docker.io VS
  218. # docker.io - in that case, it seems the full URL is necessary.
  219. registry = INDEX_URL
  220. log.debug(f"Looking for auth entry for {repr(registry)}")
  221. store = self._get_store_instance(credstore_name)
  222. try:
  223. data = store.get(registry)
  224. res = {
  225. 'ServerAddress': registry,
  226. }
  227. if data['Username'] == TOKEN_USERNAME:
  228. res['IdentityToken'] = data['Secret']
  229. else:
  230. res.update({
  231. 'Username': data['Username'],
  232. 'Password': data['Secret'],
  233. })
  234. return res
  235. except credentials.CredentialsNotFound:
  236. log.debug('No entry found')
  237. return None
  238. except credentials.StoreError as e:
  239. raise errors.DockerException(
  240. f'Credentials store error: {repr(e)}'
  241. )
  242. def _get_store_instance(self, name):
  243. if name not in self._stores:
  244. self._stores[name] = credentials.Store(
  245. name, environment=self._credstore_env
  246. )
  247. return self._stores[name]
  248. def get_credential_store(self, registry):
  249. if not registry or registry == INDEX_NAME:
  250. registry = INDEX_URL
  251. return self.cred_helpers.get(registry) or self.creds_store
  252. def get_all_credentials(self):
  253. auth_data = self.auths.copy()
  254. if self.creds_store:
  255. # Retrieve all credentials from the default store
  256. store = self._get_store_instance(self.creds_store)
  257. for k in store.list().keys():
  258. auth_data[k] = self._resolve_authconfig_credstore(
  259. k, self.creds_store
  260. )
  261. auth_data[convert_to_hostname(k)] = auth_data[k]
  262. # credHelpers entries take priority over all others
  263. for reg, store_name in self.cred_helpers.items():
  264. auth_data[reg] = self._resolve_authconfig_credstore(
  265. reg, store_name
  266. )
  267. auth_data[convert_to_hostname(reg)] = auth_data[reg]
  268. return auth_data
  269. def add_auth(self, reg, data):
  270. self['auths'][reg] = data
  271. def resolve_authconfig(authconfig, registry=None, credstore_env=None):
  272. if not isinstance(authconfig, AuthConfig):
  273. authconfig = AuthConfig(authconfig, credstore_env)
  274. return authconfig.resolve_authconfig(registry)
  275. def convert_to_hostname(url):
  276. return url.replace('http://', '').replace('https://', '').split('/', 1)[0]
  277. def decode_auth(auth):
  278. if isinstance(auth, str):
  279. auth = auth.encode('ascii')
  280. s = base64.b64decode(auth)
  281. login, pwd = s.split(b':', 1)
  282. return login.decode('utf8'), pwd.decode('utf8')
  283. def encode_header(auth):
  284. auth_json = json.dumps(auth).encode('ascii')
  285. return base64.urlsafe_b64encode(auth_json)
  286. def parse_auth(entries, raise_on_error=False):
  287. """
  288. Parses authentication entries
  289. Args:
  290. entries: Dict of authentication entries.
  291. raise_on_error: If set to true, an invalid format will raise
  292. InvalidConfigFile
  293. Returns:
  294. Authentication registry.
  295. """
  296. return AuthConfig.parse_auth(entries, raise_on_error)
  297. def load_config(config_path=None, config_dict=None, credstore_env=None):
  298. return AuthConfig.load_config(config_path, config_dict, credstore_env)
  299. def _load_legacy_config(config_file):
  300. log.debug("Attempting to parse legacy auth file format")
  301. try:
  302. data = []
  303. with open(config_file) as f:
  304. for line in f.readlines():
  305. data.append(line.strip().split(' = ')[1])
  306. if len(data) < 2:
  307. # Not enough data
  308. raise errors.InvalidConfigFile(
  309. 'Invalid or empty configuration file!'
  310. )
  311. username, password = decode_auth(data[0])
  312. return {'auths': {
  313. INDEX_NAME: {
  314. 'username': username,
  315. 'password': password,
  316. 'email': data[1],
  317. 'serveraddress': INDEX_URL,
  318. }
  319. }}
  320. except Exception as e:
  321. log.debug(e)
  322. log.debug("All parsing attempts failed - returning empty config")
  323. return {}