Skip to content

Commit 5560a31

Browse files
committed
CLI improvements and fixes; v1.3.6
1 parent f421ed1 commit 5560a31

File tree

5 files changed

+47
-15
lines changed

5 files changed

+47
-15
lines changed

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ Changelog for PyUNICORE
33

44
Issue tracker: https://github.com/HumanBrainProject/pyunicore
55

6+
Version 1.3.6 (Dec 18, 2025)
7+
-----------------------------
8+
- CLI: add bearer-token authentication method
9+
- CLI: add sshkey authentication method
10+
- CLI: resolve ${...} environment values in properties from config file
11+
- fix: using non-EdDSA keys did not correctly set the JWT signing algorithm
12+
613
Version 1.3.5 (Sep 8, 2025)
714
---------------------------
815
- improve forwarder

pyunicore/cli/base.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import os
77
from base64 import b64decode
8+
from string import Template
89

910
import pyunicore.client
1011
import pyunicore.credentials
@@ -33,7 +34,8 @@ def _value(self, value: str):
3334
return True
3435
if value.lower() == "false":
3536
return False
36-
return value
37+
t = Template(value)
38+
return t.substitute(os.environ)
3739

3840
def load_user_properties(self):
3941
with open(self.config_file) as f:
@@ -43,7 +45,7 @@ def load_user_properties(self):
4345
continue
4446
try:
4547
key, value = line.split("=", 1)
46-
self.config[key] = self._value(value)
48+
self.config[key.strip()] = self._value(value.strip())
4749
except ValueError:
4850
pass
4951

@@ -82,18 +84,31 @@ def get_group(self):
8284
return "Other"
8385

8486
def create_credential(self):
85-
auth_method = self.config.get("authentication-method", "USERNAME").upper()
86-
if "OIDC-AGENT" == auth_method:
87+
auth_method = self.config.get("authentication-method", "USERNAME")
88+
_m = auth_method.upper()
89+
if "OIDC-AGENT" == _m:
8790
account_name = self.config.get("oidc-agent.account")
8891
self.credential = pyunicore.credentials.OIDCAgentToken(account_name)
89-
elif "OIDC-SERVER" == auth_method:
92+
elif "OIDC-SERVER" == _m:
9093
self.credential = pyunicore.credentials.OIDCServerToken(self.config)
91-
elif "USERNAME" == auth_method:
94+
elif "USERNAME" == _m:
9295
username = self.config["username"]
9396
password = self._get_password()
9497
self.credential = pyunicore.credentials.create_credential(username, password)
95-
elif "ANONYMOUS" == auth_method:
98+
elif "BEARER-TOKEN" == _m:
99+
token = self.config["token"]
100+
self.credential = pyunicore.credentials.BearerToken(token)
101+
elif "SSHKEY" == _m:
102+
username = self.config["username"]
103+
identity = self.config["identity"]
104+
password = self._get_password()
105+
self.credential = pyunicore.credentials.create_credential(
106+
username=username, password=password, identity=identity
107+
)
108+
elif "ANONYMOUS" == _m or "NONE" == _m:
96109
self.credential = pyunicore.credentials.Anonymous()
110+
else:
111+
raise ValueError(f"No such authentication method: {auth_method}")
97112

98113
def _get_password(self, key="password") -> str:
99114
password = self.config.get(key)

pyunicore/cli/info.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
class Info(Base):
1010
def add_command_args(self):
11-
self.parser.prog = "unicore system-info"
11+
self.parser.prog = "unicore info"
1212
self.parser.description = self.get_synopsis()
1313
self.parser.add_argument("URL", help="Endpoint URL(s)", nargs="*")
1414
self.parser.add_argument(
@@ -52,21 +52,25 @@ def run(self, args):
5252

5353
def show_endpoint_details(self, ep: Resource):
5454
print(ep.resource_url)
55-
if ep.resource_url.endswith("/rest/core"):
55+
if re.match(".*/rest/core[/]?$", ep.resource_url):
5656
self._show_details_core(ep)
5757
elif re.match(".*/rest/core/storages/.+", ep.resource_url):
5858
self._show_details_storage(ep)
59+
elif re.match(".*/rest/core/factories/.+", ep.resource_url):
60+
self._show_details_sitefactory(ep)
5961
else:
6062
print(" * no further details available.")
6163

6264
def _show_details_core(self, ep: Resource):
6365
props = ep.properties
6466
print(" * type: UNICORE/X base")
6567
print(f" * server v{props['server']['version']}")
68+
dn = {props["client"]["dn"]}
6669
xlogin = props["client"]["xlogin"]
6770
role = props["client"]["role"]["selected"]
6871
uid = xlogin.get("UID", "n/a")
69-
print(f" * authenticated as: '{props['client']['dn']}' role='{role}' uid='{uid}'")
72+
method = props["client"].get("authenticationMethod", "n/a")
73+
print(f" * authenticated (via '{method}') as: '{dn}' role='{role}' uid='{uid}'")
7074
grps = xlogin.get("availableGroups", [])
7175
uids = xlogin.get("availableUIDs", [])
7276
if len(uids) > 0:
@@ -85,3 +89,7 @@ def _show_details_storage(self, ep: Resource):
8589
print(f" * type: {t}")
8690
print(f" * mount point: {props['mountPoint']}")
8791
print(f" * free space : {int(props['freeSpace'] / 1024 / 1024)} MB")
92+
93+
def _show_details_sitefactory(self, ep: Resource):
94+
t = "compute"
95+
print(f" * type: {t}")

pyunicore/cli/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
}
2424

2525

26-
def get_command(name):
26+
def get_command(name) -> pyunicore.cli.base.Base:
2727
return _commands.get(name)()
2828

2929

@@ -37,12 +37,12 @@ def show_version():
3737

3838

3939
def help():
40+
print(_header)
4041
s = """UNICORE Commandline Client (pyUNICORE) %s, https://www.unicore.eu
4142
Usage: unicore <command> [OPTIONS] <args>
4243
The following commands are available:""" % pyunicore._version.get_versions().get(
4344
"version", "n/a"
4445
)
45-
print(_header)
4646
print(s)
4747
for cmd in sorted(_commands):
4848
print(f" {cmd:20} - {get_command(cmd).get_description()}")

pyunicore/credentials.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ def create_credential(username=None, password=None, token=None, identity=None):
332332
username + password : create a UsernamePassword credential
333333
token ; create a OIDCToken credential from the given token
334334
username + identity : create a JWTToken credential which will be signed
335-
with the given private key (ssh key or PEM)
335+
with the given private key (ssh key or X509)
336336
"""
337337

338338
if token is not None:
@@ -343,6 +343,8 @@ def create_credential(username=None, password=None, token=None, identity=None):
343343
raise AuthenticationFailedException("Not enough info to create user credential")
344344
try:
345345
from cryptography.hazmat.primitives import serialization
346+
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
347+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
346348

347349
if not isabs(identity):
348350
if identity.startswith("~"):
@@ -362,9 +364,9 @@ def create_credential(username=None, password=None, token=None, identity=None):
362364
secret = private_key
363365
sub = username
364366
algo = "EdDSA"
365-
if "BEGIN RSA" in pem:
367+
if isinstance(private_key, RSAPrivateKey):
366368
algo = "RS256"
367-
elif "BEGIN EC" in pem or "PuTTY" in pem:
369+
elif isinstance(private_key, EllipticCurvePrivateKey) or "PuTTY" in pem:
368370
algo = "ES256"
369371
return JWTToken(sub, sub, secret, algorithm=algo, etd=False)
370372
except ImportError:

0 commit comments

Comments
 (0)