Skip to content

Commit 2a421e2

Browse files
authored
Add support for Docker credsStore and credHelpers (#206)
* Add support for Docker credsStore and credHelpers * Bump version and add version to CHANGELOG.md Signed-off-by: Rasmus Faber-Espensen <rfaber@gmail.com> --------- Signed-off-by: Rasmus Faber-Espensen <rfaber@gmail.com>
1 parent b63ede0 commit 2a421e2

File tree

5 files changed

+69
-32
lines changed

5 files changed

+69
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and **Merged pull requests**. Critical items to know are:
1414
The versions coincide with releases on pip. Only major versions will be released as tags on Github.
1515

1616
## [0.0.x](https://github.com/oras-project/oras-py/tree/main) (0.0.x)
17+
- Add support for Docker credsStore and credHelpers (0.2.34)
1718
- fix 'get_manifest()' method with adding 'load_configs()' calling (0.2.33)
1819
- fix 'Provider' method signature to allow custom CA-Bundles (0.2.32)
1920
- initialize headers variable in do_request (0.2.31)

docs/getting_started/user-guide.md

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -678,16 +678,8 @@ More verbose output should appear.
678678

679679
> I get unauthorized when trying to login to an Amazon ECR Registry
680680
681-
Note that for [Amazon ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html)
682-
you might need to login per the instructions at the link provided. If you look at your `~/.docker/config.json` and see
683-
that there is a "credsStore" section that is populated, you might also need to comment this out
684-
while using oras-py. Oras-py currently doesn't support reading external credential stores, so you will
685-
need to comment it out, login again, and then try your request. To sanity check that you've done
686-
this correctly, you should see an "auth" key under a hostname under "auths" in this file with a base64
687-
encoded string (this is actually your username and password!) An empty dictionary there indicates that you
688-
are using a helper. Finally, it would be cool if we supported these external stores! If you
689-
want to contribute this or help think about how to do it, @vsoch would be happy to help.
690-
681+
If you have configured a credsStore or a credHelper in your Docker config, you should remember
682+
to use the basic auth backend.
691683

692684

693685
## Custom Clients

oras/auth/base.py

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
__copyright__ = "Copyright The ORAS Authors."
33
__license__ = "Apache-2.0"
44

5-
5+
import json
6+
import subprocess
67
from typing import Optional
78

89
import requests
@@ -23,7 +24,7 @@ class AuthBackend:
2324
_tls_verify: bool
2425

2526
def __init__(self, *args, **kwargs):
26-
self._auths: dict = {}
27+
self._auth_config: dict = {}
2728
self.prefix: str = "https"
2829

2930
def get_auth_header(self):
@@ -48,20 +49,41 @@ def logout(self, hostname: str):
4849
:type hostname: str
4950
"""
5051
self._logout()
51-
if not self._auths:
52+
if not self._auth_config or not self._auth_config.get("auths"):
5253
logger.info(f"You are not logged in to {hostname}")
5354
return
5455

5556
for host in oras.utils.iter_localhosts(hostname):
56-
if host in self._auths:
57-
del self._auths[host]
57+
auths = self._auth_config.get("auths", {})
58+
if host in auths:
59+
del auths[host]
5860
logger.info(f"You have successfully logged out of {hostname}")
5961
return
6062
logger.info(f"You are not logged in to {hostname}")
6163

6264
def _logout(self):
6365
pass
6466

67+
def _get_auth_from_creds_store(self, binary: str, hostname: str) -> str:
68+
try:
69+
proc = subprocess.run(
70+
[binary, "get"],
71+
input=hostname.encode(),
72+
stdout=subprocess.PIPE,
73+
stderr=subprocess.PIPE,
74+
check=True,
75+
)
76+
except FileNotFoundError as exc:
77+
raise RuntimeError(
78+
f"Credential helper '{binary}' not found in PATH"
79+
) from exc
80+
except subprocess.CalledProcessError as exc:
81+
raise RuntimeError(
82+
f"Credential helper '{binary}' failed: {exc.stderr.decode().strip()}"
83+
) from exc
84+
payload = json.loads(proc.stdout)
85+
return auth_utils.get_basic_auth(payload["Username"], payload["Secret"])
86+
6587
def _load_auth(self, hostname: str) -> bool:
6688
"""
6789
Look for and load a named authentication token.
@@ -70,21 +92,35 @@ def _load_auth(self, hostname: str) -> bool:
7092
:type hostname: str
7193
"""
7294
# Note that the hostname can be defined without a token
73-
if hostname in self._auths:
74-
auth = self._auths[hostname].get("auth")
75-
76-
# Case 1: they use a credsStore we don't know how to read
77-
if not auth and "credsStore" in self._auths[hostname]:
78-
logger.warning(
79-
'"credsStore" found in your ~/.docker/config.json, which is not supported by oras-py. Remove it, docker login, and try again.'
80-
)
81-
return False
95+
auths = self._auth_config.get("auths", {})
96+
auth = auths.get(hostname)
97+
if auth is not None:
98+
auth = auths[hostname].get("auth")
8299

83-
# Case 2: no auth there (wonky file)
84-
elif not auth:
100+
if not auth:
101+
# no auth there (wonky file)
85102
return False
86103
self._basic_auth = auth
87104
return True
105+
# Check for credsStore:
106+
if self._auth_config.get("credsStore"):
107+
auth = self._get_auth_from_creds_store(
108+
self._auth_config["credsStore"], hostname
109+
)
110+
if auth is not None:
111+
self._basic_auth = auth
112+
auths[hostname] = {"auth": auth}
113+
return True
114+
# Check for credHelper
115+
if self._auth_config.get("credHelpers", {}).get(hostname):
116+
auth = self._get_auth_from_creds_store(
117+
self._auth_config["credHelpers"][hostname], hostname
118+
)
119+
if auth is not None:
120+
self._basic_auth = auth
121+
auths[hostname] = {"auth": auth}
122+
return True
123+
88124
return False
89125

90126
@decorator.ensure_container
@@ -100,8 +136,8 @@ def load_configs(self, container: container_type, configs: Optional[list] = None
100136
:param configs: list of configs to read (optional)
101137
:type configs: list
102138
"""
103-
if not self._auths:
104-
self._auths = auth_utils.load_configs(configs)
139+
if not self._auth_config:
140+
self._auth_config = auth_utils.load_configs(configs)
105141
for registry in oras.utils.iter_localhosts(container.registry): # type: ignore
106142
if self._load_auth(registry):
107143
return

oras/auth/utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,24 @@ def load_configs(configs: Optional[List[str]] = None):
2626
configs.append(default_config)
2727
configs = set(configs) # type: ignore
2828

29-
# Load configs until we find our registry hostname
29+
# Load configs
3030
auths = {}
31+
creds_store = None
32+
cred_helpers = {}
3133
for config in configs:
3234
if not os.path.exists(config):
3335
logger.warning(f"{config} does not exist.")
3436
continue
3537
cfg = oras.utils.read_json(config)
3638
auths.update(cfg.get("auths", {}))
37-
38-
return auths
39+
creds_store = cfg.get("credsStore")
40+
cred_helpers.update(cfg.get("credHelpers", {}))
41+
42+
return {
43+
"auths": auths,
44+
"credsStore": creds_store,
45+
"credHelpers": cred_helpers,
46+
}
3947

4048

4149
def get_basic_auth(username: str, password: str):

oras/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
__copyright__ = "Copyright The ORAS Authors."
33
__license__ = "Apache-2.0"
44

5-
__version__ = "0.2.33"
5+
__version__ = "0.2.34"
66
AUTHOR = "Vanessa Sochat"
77
EMAIL = "vsoch@users.noreply.github.com"
88
NAME = "oras"

0 commit comments

Comments
 (0)