Skip to content

Commit 6d23249

Browse files
committed
Add TMS credential management to dapi
Add check_credentials(), establish_credentials(), and revoke_credentials() to manage Tapis Managed Secrets (TMS) SSH keys on TACC execution systems. Includes CredentialError exception, 31 tests, docs, and example notebook. Also fixes missing comma bug in __init__.py __all__ list.
1 parent 8b55ca8 commit 6d23249

13 files changed

Lines changed: 829 additions & 7 deletions

File tree

dapi/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
FileOperationError,
6262
AppDiscoveryError,
6363
SystemInfoError,
64+
CredentialError,
6465
JobSubmissionError,
6566
JobMonitorError,
6667
)
@@ -94,6 +95,8 @@
9495
"AuthenticationError",
9596
"FileOperationError",
9697
"AppDiscoveryError",
97-
"SystemInfoError" "JobSubmissionError",
98+
"SystemInfoError",
99+
"CredentialError",
100+
"JobSubmissionError",
98101
"JobMonitorError",
99102
]

dapi/client.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,81 @@ def list_queues(self, system_id: str, verbose: bool = True) -> List[Any]:
287287
self._tapis, system_id, verbose=verbose
288288
)
289289

290+
def check_credentials(
291+
self, system_id: str, username: str = None
292+
) -> bool:
293+
"""Check whether TMS credentials exist for a user on a system.
294+
295+
Args:
296+
system_id (str): The ID of the Tapis system (e.g., 'frontera').
297+
username (str, optional): Username to check. Defaults to
298+
the authenticated user.
299+
300+
Returns:
301+
bool: True if credentials exist, False otherwise.
302+
303+
Raises:
304+
CredentialError: If the credential check fails unexpectedly.
305+
ValueError: If system_id is empty.
306+
"""
307+
return systems_module.check_credentials(
308+
self._tapis, system_id, username=username
309+
)
310+
311+
def establish_credentials(
312+
self,
313+
system_id: str,
314+
username: str = None,
315+
force: bool = False,
316+
verbose: bool = True,
317+
) -> None:
318+
"""Establish TMS credentials for a user on a Tapis system.
319+
320+
Idempotent: skips creation if credentials already exist (unless force=True).
321+
Only supported for systems using TMS_KEYS authentication.
322+
323+
Args:
324+
system_id (str): The ID of the Tapis system (e.g., 'frontera').
325+
username (str, optional): Username. Defaults to the authenticated user.
326+
force (bool, optional): Re-create even if credentials exist.
327+
Defaults to False.
328+
verbose (bool, optional): Print status messages. Defaults to True.
329+
330+
Raises:
331+
CredentialError: If the system is not TMS_KEYS or creation fails.
332+
ValueError: If system_id is empty.
333+
"""
334+
return systems_module.establish_credentials(
335+
self._tapis,
336+
system_id,
337+
username=username,
338+
force=force,
339+
verbose=verbose,
340+
)
341+
342+
def revoke_credentials(
343+
self,
344+
system_id: str,
345+
username: str = None,
346+
verbose: bool = True,
347+
) -> None:
348+
"""Remove TMS credentials for a user on a Tapis system.
349+
350+
Idempotent: succeeds silently if credentials do not exist.
351+
352+
Args:
353+
system_id (str): The ID of the Tapis system (e.g., 'frontera').
354+
username (str, optional): Username. Defaults to the authenticated user.
355+
verbose (bool, optional): Print status messages. Defaults to True.
356+
357+
Raises:
358+
CredentialError: If credential removal fails unexpectedly.
359+
ValueError: If system_id is empty.
360+
"""
361+
return systems_module.revoke_credentials(
362+
self._tapis, system_id, username=username, verbose=verbose
363+
)
364+
290365

291366
class JobMethods:
292367
"""Interface for Tapis job submission, monitoring, and management.

dapi/exceptions.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,26 @@ class SystemInfoError(DapiException):
117117
pass
118118

119119

120+
class CredentialError(DapiException):
121+
"""Exception raised when credential management operations fail.
122+
123+
This exception is raised when operations involving Tapis Managed Secrets (TMS)
124+
credentials fail, such as checking, establishing, or revoking user credentials
125+
on a Tapis execution system.
126+
127+
Args:
128+
message (str): Description of the credential operation failure.
129+
130+
Example:
131+
>>> try:
132+
... client.systems.establish_credentials("frontera")
133+
... except CredentialError as e:
134+
... print(f"Credential operation failed: {e}")
135+
"""
136+
137+
pass
138+
139+
120140
class JobSubmissionError(DapiException):
121141
"""Exception raised when job submission or validation fails.
122142

dapi/systems.py

Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# dapi/systems.py
22
from tapipy.tapis import Tapis
3-
from tapipy.errors import BaseTapyException
3+
from tapipy.errors import BaseTapyException, UnauthorizedError, NotFoundError
44
from typing import List, Any, Optional
5-
from .exceptions import SystemInfoError
5+
from .exceptions import SystemInfoError, CredentialError
66

77

88
def list_system_queues(t: Tapis, system_id: str, verbose: bool = True) -> List[Any]:
@@ -95,3 +95,200 @@ def list_system_queues(t: Tapis, system_id: str, verbose: bool = True) -> List[A
9595
raise SystemInfoError(
9696
f"An unexpected error occurred while fetching queues for system '{system_id}': {e}"
9797
) from e
98+
99+
100+
def _resolve_username(t: Tapis, username: Optional[str] = None) -> str:
101+
"""Resolve the effective username from an explicit parameter or the Tapis client.
102+
103+
Args:
104+
t: Authenticated Tapis client instance.
105+
username: Explicit username. If None, falls back to t.username.
106+
107+
Returns:
108+
The resolved username string.
109+
110+
Raises:
111+
ValueError: If username cannot be determined from either source.
112+
"""
113+
effective = username or getattr(t, "username", None)
114+
if not effective:
115+
raise ValueError(
116+
"Username must be provided or available on the Tapis client (t.username)."
117+
)
118+
return effective
119+
120+
121+
def check_credentials(
122+
t: Tapis, system_id: str, username: Optional[str] = None
123+
) -> bool:
124+
"""Check whether TMS credentials exist for a user on a Tapis system.
125+
126+
Args:
127+
t: Authenticated Tapis client instance.
128+
system_id: The ID of the Tapis system (e.g., 'frontera', 'stampede3').
129+
username: The username to check. If None, auto-detected from t.username.
130+
131+
Returns:
132+
True if credentials exist, False if they do not.
133+
134+
Raises:
135+
ValueError: If system_id is empty or username cannot be determined.
136+
CredentialError: If an unexpected API error occurs during the check.
137+
"""
138+
if not system_id:
139+
raise ValueError("system_id cannot be empty.")
140+
141+
effective_username = _resolve_username(t, username)
142+
143+
try:
144+
t.systems.checkUserCredential(
145+
systemId=system_id, userName=effective_username
146+
)
147+
return True
148+
except (UnauthorizedError, NotFoundError):
149+
return False
150+
except BaseTapyException as e:
151+
raise CredentialError(
152+
f"Failed to check credentials for user '{effective_username}' "
153+
f"on system '{system_id}': {e}"
154+
) from e
155+
except Exception as e:
156+
raise CredentialError(
157+
f"Unexpected error checking credentials for user '{effective_username}' "
158+
f"on system '{system_id}': {e}"
159+
) from e
160+
161+
162+
def establish_credentials(
163+
t: Tapis,
164+
system_id: str,
165+
username: Optional[str] = None,
166+
force: bool = False,
167+
verbose: bool = True,
168+
) -> None:
169+
"""Establish TMS credentials for a user on a Tapis system.
170+
171+
Idempotent: if credentials already exist and force is False, no action is taken.
172+
Only systems with defaultAuthnMethod 'TMS_KEYS' are supported.
173+
174+
Args:
175+
t: Authenticated Tapis client instance.
176+
system_id: The ID of the Tapis system (e.g., 'frontera', 'stampede3').
177+
username: The username. If None, auto-detected from t.username.
178+
force: If True, create credentials even if they already exist.
179+
verbose: If True, prints status messages.
180+
181+
Raises:
182+
ValueError: If system_id is empty or username cannot be determined.
183+
CredentialError: If the system does not use TMS_KEYS, if the system is
184+
not found, or if credential creation fails.
185+
"""
186+
if not system_id:
187+
raise ValueError("system_id cannot be empty.")
188+
189+
effective_username = _resolve_username(t, username)
190+
191+
# Verify system exists and uses TMS_KEYS authentication
192+
try:
193+
system_details = t.systems.getSystem(systemId=system_id)
194+
authn_method = getattr(system_details, "defaultAuthnMethod", None)
195+
except BaseTapyException as e:
196+
if hasattr(e, "response") and e.response and e.response.status_code == 404:
197+
raise CredentialError(
198+
f"System '{system_id}' not found."
199+
) from e
200+
raise CredentialError(
201+
f"Failed to retrieve system '{system_id}': {e}"
202+
) from e
203+
204+
if authn_method != "TMS_KEYS":
205+
raise CredentialError(
206+
f"System '{system_id}' uses authentication method '{authn_method}', "
207+
f"not 'TMS_KEYS'. TMS credential management is only supported "
208+
f"for TMS_KEYS systems."
209+
)
210+
211+
# Check existing credentials unless force is True
212+
if not force:
213+
if check_credentials(t, system_id, effective_username):
214+
if verbose:
215+
print(
216+
f"Credentials already exist for user '{effective_username}' "
217+
f"on system '{system_id}'. No action taken."
218+
)
219+
return
220+
221+
# Create credentials
222+
try:
223+
t.systems.createUserCredential(
224+
systemId=system_id,
225+
userName=effective_username,
226+
createTmsKeys=True,
227+
)
228+
if verbose:
229+
print(
230+
f"TMS credentials established for user '{effective_username}' "
231+
f"on system '{system_id}'."
232+
)
233+
except BaseTapyException as e:
234+
raise CredentialError(
235+
f"Failed to create credentials for user '{effective_username}' "
236+
f"on system '{system_id}': {e}"
237+
) from e
238+
except Exception as e:
239+
raise CredentialError(
240+
f"Unexpected error creating credentials for user '{effective_username}' "
241+
f"on system '{system_id}': {e}"
242+
) from e
243+
244+
245+
def revoke_credentials(
246+
t: Tapis,
247+
system_id: str,
248+
username: Optional[str] = None,
249+
verbose: bool = True,
250+
) -> None:
251+
"""Remove TMS credentials for a user on a Tapis system.
252+
253+
Idempotent: if credentials do not exist, no error is raised.
254+
255+
Args:
256+
t: Authenticated Tapis client instance.
257+
system_id: The ID of the Tapis system (e.g., 'frontera', 'stampede3').
258+
username: The username. If None, auto-detected from t.username.
259+
verbose: If True, prints status messages.
260+
261+
Raises:
262+
ValueError: If system_id is empty or username cannot be determined.
263+
CredentialError: If credential removal fails unexpectedly.
264+
"""
265+
if not system_id:
266+
raise ValueError("system_id cannot be empty.")
267+
268+
effective_username = _resolve_username(t, username)
269+
270+
try:
271+
t.systems.removeUserCredential(
272+
systemId=system_id, userName=effective_username
273+
)
274+
if verbose:
275+
print(
276+
f"Credentials revoked for user '{effective_username}' "
277+
f"on system '{system_id}'."
278+
)
279+
except (UnauthorizedError, NotFoundError):
280+
if verbose:
281+
print(
282+
f"No credentials found for user '{effective_username}' "
283+
f"on system '{system_id}'. No action taken."
284+
)
285+
except BaseTapyException as e:
286+
raise CredentialError(
287+
f"Failed to revoke credentials for user '{effective_username}' "
288+
f"on system '{system_id}': {e}"
289+
) from e
290+
except Exception as e:
291+
raise CredentialError(
292+
f"Unexpected error revoking credentials for user '{effective_username}' "
293+
f"on system '{system_id}': {e}"
294+
) from e

docs/api/exceptions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ Custom exception classes for DAPI error handling and debugging.
2222

2323
::: dapi.exceptions.SystemInfoError
2424

25+
## Credential Management Exceptions
26+
27+
::: dapi.exceptions.CredentialError
28+
2529
## Job Management Exceptions
2630

2731
::: dapi.exceptions.JobSubmissionError

docs/api/systems.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
# Systems
22

3-
System information and queue management for DesignSafe execution systems.
3+
System information, queue management, and TMS credential management for DesignSafe execution systems.
44

55
## System Queues
66

7-
::: dapi.systems.list_system_queues
7+
::: dapi.systems.list_system_queues
8+
9+
## TMS Credential Management
10+
11+
Manage Tapis Managed Secrets (TMS) credentials on execution systems. TMS credentials are SSH key pairs that allow Tapis to access TACC systems (Frontera, Stampede3, Lonestar6) on behalf of a user. They must be established once per system before submitting jobs.
12+
13+
### Check Credentials
14+
15+
::: dapi.systems.check_credentials
16+
17+
### Establish Credentials
18+
19+
::: dapi.systems.establish_credentials
20+
21+
### Revoke Credentials
22+
23+
::: dapi.systems.revoke_credentials

0 commit comments

Comments
 (0)