Skip to content

Commit 8ee3540

Browse files
authored
Merge pull request #93 from elixir-europe/multi-auth-provider-credentials
Multi auth provider credentials
2 parents 08cc1ba + dc51e2d commit 8ee3540

12 files changed

Lines changed: 256 additions & 59 deletions

.github/workflows/test-mars.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
matrix:
1717
os: [ubuntu-latest, windows-latest]
18-
python-version: ["3.9", "3.10", "3.11", "3.12"]
18+
python-version: ["3.9", "3.13"]
1919
runs-on: ${{ matrix.os }}
2020
env:
2121
working-directory: ./mars-cli
@@ -48,6 +48,9 @@ jobs:
4848
run: ruff check mars_lib/
4949
working-directory: ${{ env.working-directory }}
5050

51+
- name: Create mypy cache directory
52+
run: mkdir -p /tmp/mypy_cache
53+
5154
- name: Type checking
52-
run: mypy --install-types --non-interactive mars_lib/
55+
run: mypy --install-types --non-interactive --cache-dir /tmp/mypy_cache mars_lib/
5356
working-directory: ${{ env.working-directory }}

mars-cli/mars_cli.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,22 @@ def cli(ctx, development):
197197

198198
@cli.command()
199199
@click.option(
200-
"--credential-service-name", type=click.STRING, help="service name from the keyring"
200+
"--webin-username",
201+
type=click.STRING,
202+
help="Username for webin authentication",
203+
envvar="WEBIN_USERNAME",
201204
)
202205
@click.option(
203-
"--username-credentials", type=click.STRING, help="Username from the keyring"
206+
"--metabolights-username",
207+
type=click.STRING,
208+
help="Username for MetaboLights metadata submission",
209+
envvar="METABOLIGHTS_USERNAME",
210+
)
211+
@click.option(
212+
"--metabolights-ftp-username",
213+
type=click.STRING,
214+
help="Username for MetaboLights data submission",
215+
envvar="METABOLIGHTS_FTP_USERNAME",
204216
)
205217
@click.option(
206218
"--credentials-file",
@@ -218,8 +230,10 @@ def cli(ctx, development):
218230
@click.option("--submit-to-ena", type=click.BOOL, default=True, help="Submit to ENA.")
219231
@click.option(
220232
"--file-transfer",
221-
type=click.STRING,
222-
help="provide the name of a file transfer solution, like ftp or aspera",
233+
type=click.Choice(["ftp", "aspera"], case_sensitive=False),
234+
required=True,
235+
default="ftp",
236+
help="provide the name of a file transfer solution, like ftp or aspera. The default is ftp.",
223237
)
224238
@click.option(
225239
"--data-files",
@@ -247,8 +261,9 @@ def cli(ctx, development):
247261
@click.pass_context
248262
def submit(
249263
ctx,
250-
credential_service_name,
251-
username_credentials,
264+
webin_username,
265+
metabolights_username,
266+
metabolights_ftp_username,
252267
credentials_file,
253268
isa_json_file,
254269
submit_to_biosamples,
@@ -280,8 +295,9 @@ def submit(
280295
data_file_paths = [f.name for f in data_files] if file_transfer else []
281296

282297
submission(
283-
credential_service_name,
284-
username_credentials,
298+
webin_username,
299+
metabolights_username,
300+
metabolights_ftp_username,
285301
credentials_file,
286302
isa_json_file.name,
287303
target_repositories,
@@ -361,12 +377,14 @@ def validate_isa_json(isa_json_file, investigation_is_root, validation_schema):
361377

362378
@cli.command()
363379
@click.option(
364-
"--service-name",
365-
type=click.STRING,
380+
"--auth-provider",
381+
type=click.Choice(
382+
["webin", "metabolights_metadata", "metabolights_data"], case_sensitive=False
383+
),
366384
is_flag=False,
367385
flag_value="value",
368-
default=f"mars-cli_{datetime.now().strftime('%Y-%m-%dT%H:%M:%S')}",
369-
help='You are advised to include service name to match the credentials to. If empty, it defaults to "mars-cli_{DATESTAMP}"',
386+
required=True,
387+
help="",
370388
)
371389
@click.argument(
372390
"username",
@@ -380,9 +398,9 @@ def validate_isa_json(isa_json_file, investigation_is_root, validation_schema):
380398
confirmation_prompt=True,
381399
help="The password to store. Note: You are required to confirm the password.",
382400
)
383-
def set_password(service_name, username, password):
401+
def set_password(auth_provider, username, password):
384402
"""Store a password in the keyring."""
385-
CredentialManager(service_name).set_password_keyring(username, password)
403+
CredentialManager(auth_provider).set_password_keyring(username, password)
386404

387405

388406
if __name__ == "__main__":

mars-cli/mars_lib/authentication.py

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,68 @@
1-
from typing import Optional
1+
import io
2+
from typing import Optional, Union
23
import requests
34
import json
5+
from enum import Enum
6+
7+
8+
class AuthProvider(Enum):
9+
"""
10+
Holds constants, tied to the repository authentication providers.
11+
"""
12+
13+
WEBIN = "webin"
14+
METABOLIGHTS_METADATA = "metabolights_metadata"
15+
METABOLIGHTS_DATA = "metabolights_data"
16+
17+
@classmethod
18+
def available_providers(cls):
19+
return {item.value for item in cls}
20+
21+
@classmethod
22+
def is_valid_provider(cls, provider: str):
23+
return provider in cls.available_providers()
24+
25+
26+
def load_credentials(
27+
credentials_file: Union[io.TextIOWrapper, str]
28+
) -> dict[str, dict[str, str]]:
29+
"""
30+
Validate the credentials.
31+
32+
Args:
33+
credentials_file (_): The credentials in file formate.
34+
35+
Raises:
36+
ValueError: If the credentials are not valid.
37+
38+
Returns:
39+
dict: The credentials.
40+
"""
41+
if isinstance(credentials_file, str):
42+
with open(credentials_file, "r") as file:
43+
credentials = json.load(file)
44+
elif isinstance(credentials_file, io.TextIOWrapper):
45+
with open(credentials_file.name, "r") as file:
46+
credentials = json.load(file)
47+
else:
48+
raise TypeError("Credentials file must be of type str or io.TextIOWrapper.")
49+
50+
if not all(
51+
repo in AuthProvider.available_providers() for repo in credentials.keys()
52+
):
53+
raise ValueError(
54+
f"Credentials dictionary must have valid keys. Valid keys are:\n{AuthProvider.available_providers()}"
55+
)
56+
57+
if not all(
58+
key in ["username", "password"]
59+
for repo, creds in credentials.items()
60+
for key in creds.keys()
61+
):
62+
raise ValueError(
63+
"Credentials dictionary must contain 'username' and 'password' keys."
64+
)
65+
return credentials
466

567

668
def get_webin_auth_token(
@@ -45,7 +107,10 @@ def get_webin_auth_token(
45107

46108
def get_metabolights_auth_token(
47109
credentials_dict: dict[str, str],
48-
headers: dict[str, str] = {"Content-Type": "application/x-www-form-urlencoded"},
110+
headers: dict[str, str] = {
111+
"Content-Type": "application/x-www-form-urlencoded",
112+
"Accept": "application/json",
113+
},
49114
auth_url: str = "https://www-test.ebi.ac.uk/metabolights/mars/ws3/auth/token",
50115
) -> Optional[str]:
51116
"""
@@ -59,10 +124,6 @@ def get_metabolights_auth_token(
59124
Returns:
60125
str: The obtained token.
61126
"""
62-
headers = {
63-
"Content-Type": "application/x-www-form-urlencoded",
64-
"Accept": "application/json",
65-
}
66127
form_data = f'grant_type=password&username={credentials_dict["username"]}&password={credentials_dict["password"]}'
67128
try:
68129
response = requests.post(

mars-cli/mars_lib/credential.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import getpass
44
import keyring.util.platform_ as keyring_platform
55

6+
from mars_lib.authentication import AuthProvider
7+
68
"""
79
Credential Manager Module
810
=========================
@@ -52,8 +54,11 @@
5254

5355

5456
class CredentialManager:
55-
def __init__(self, service_name: str) -> None:
56-
self.service_name = service_name
57+
def __init__(self, auth_provider: str) -> None:
58+
if not AuthProvider.is_valid_provider(auth_provider):
59+
raise ValueError(f"Invalid authentication provider: {auth_provider}")
60+
61+
self.service_name = auth_provider
5762

5863
def get_credential_env(self, username: str) -> str:
5964
"""

mars-cli/mars_lib/models/isa_json.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ class MaterialAttribute(IsaBase):
255255

256256

257257
class Study(CommentedIsaBase):
258-
id: str = Field(alias="@id", default=None)
258+
id: Optional[str] = Field(alias="@id", default=None)
259259
assays: List[Assay] = []
260260
characteristicCategories: List[MaterialAttribute] = []
261261
description: Optional[str] = None
@@ -284,7 +284,7 @@ def validate_filename(cls, v: str) -> Union[str, None]:
284284

285285

286286
class Investigation(CommentedIsaBase):
287-
id: str = Field(alias="@id", default=None)
287+
id: Optional[str] = Field(alias="@id", default=None)
288288
description: Optional[str] = None
289289
filename: Optional[str] = None
290290
identifier: Optional[str] = None

mars-cli/mars_lib/submit.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
import requests
77
import json
88
from typing import Any
9-
from mars_lib.authentication import get_metabolights_auth_token, get_webin_auth_token
9+
from mars_lib.authentication import (
10+
get_metabolights_auth_token,
11+
get_webin_auth_token,
12+
load_credentials,
13+
AuthProvider,
14+
)
1015
from mars_lib.biosamples_external_references import (
1116
get_header,
1217
biosamples_endpoints,
@@ -44,8 +49,9 @@ def save_step_to_file(time_stamp: float, filename: str, isa_json: IsaJson):
4449

4550

4651
def submission(
47-
credential_service_name: str,
48-
username_credentials: str,
52+
webin_username: str,
53+
metabolights_username: str,
54+
metabolights_ftp_username: str,
4955
credentials_file: TextIOWrapper,
5056
isa_json_file: str,
5157
target_repositories: list[str],
@@ -59,17 +65,24 @@ def submission(
5965
# Get password from the credential manager
6066
# Else:
6167
# read credentials from file
62-
if not (credential_service_name is None or username_credentials is None):
63-
cm = CredentialManager(credential_service_name)
68+
if all([webin_username, metabolights_username, metabolights_ftp_username]):
6469
user_credentials = {
65-
"username": username_credentials,
66-
"password": cm.get_password_keyring(username_credentials),
70+
cred_pair[0]: {
71+
"username": cred_pair[1],
72+
"password": CredentialManager(cred_pair[0]).get_password_keyring(
73+
cred_pair[1]
74+
),
75+
}
76+
for cred_pair in zip(
77+
AuthProvider.available_providers(),
78+
[webin_username, metabolights_username, metabolights_ftp_username],
79+
)
6780
}
6881
else:
6982
if credentials_file == "":
7083
raise ValueError("No credentials found")
7184

72-
user_credentials = json.load(credentials_file)
85+
user_credentials = load_credentials(credentials_file)
7386

7487
isa_json = load_isa_json(isa_json_file, investigation_is_root)
7588

@@ -101,7 +114,7 @@ def submission(
101114
# Submit to Biosamples
102115
biosamples_result = submit_to_biosamples(
103116
isa_json=isa_json,
104-
biosamples_credentials=user_credentials,
117+
biosamples_credentials=user_credentials[AuthProvider.WEBIN.value],
105118
biosamples_url=urls["BIOSAMPLES"]["SUBMISSION"],
106119
webin_token_url=urls["WEBIN"]["TOKEN"],
107120
)
@@ -124,7 +137,7 @@ def submission(
124137
file_paths=[
125138
Path(df) for df in data_file_map[TargetRepository.ENA.value]
126139
],
127-
user_credentials=user_credentials,
140+
user_credentials=user_credentials[AuthProvider.WEBIN.value],
128141
submission_url=urls["ENA"]["DATA-SUBMISSION"],
129142
file_transfer=file_transfer,
130143
)
@@ -135,7 +148,7 @@ def submission(
135148
# Step 2 : submit isa-json to ena
136149
ena_result = submit_to_ena(
137150
isa_json=isa_json,
138-
user_credentials=user_credentials,
151+
user_credentials=user_credentials[AuthProvider.WEBIN.value],
139152
submission_url=urls["ENA"]["SUBMISSION"],
140153
)
141154
print_and_log(
@@ -159,7 +172,9 @@ def submission(
159172
file_paths=data_file_map[TargetRepository.METABOLIGHTS.value],
160173
file_transfer=file_transfer,
161174
isa_json=isa_json,
162-
metabolights_credentials=user_credentials,
175+
metabolights_credentials=user_credentials[
176+
AuthProvider.METABOLIGHTS_METADATA.value
177+
],
163178
metabolights_url=urls["METABOLIGHTS"]["SUBMISSION"],
164179
metabolights_token_url=urls["METABOLIGHTS"]["TOKEN"],
165180
)
@@ -252,9 +267,9 @@ def upload_to_metabolights(
252267
"accept": "application/json",
253268
"Authorization": f"Bearer {token}",
254269
}
255-
isa_json_str = isa_json.investigation.model_dump_json(
256-
by_alias=True, exclude_none=True
257-
)
270+
isa_json_str = reduce_isa_json_for_target_repo(
271+
isa_json, TargetRepository.METABOLIGHTS
272+
).investigation.model_dump_json(by_alias=True, exclude_none=True)
258273
json_file = io.StringIO(isa_json_str)
259274

260275
files = {"isa_json_file": ("isa_json.json", json_file)}
@@ -360,7 +375,7 @@ def submit_to_ena(
360375
else result.request.body or ""
361376
)
362377
raise requests.HTTPError(
363-
f"Request towards ENA failed!\nRequest:\nMethod:{result.request.method}\nStatus:{result.status_code}\nURL:{result.request.url}\nHeaders:{result.request.headers}\nBody:{body}"
378+
f"Request towards ENA failed!\nRequest:\nMethod:{result.request.method}\nStatus:{result.status_code}\nURL:{submission_url}\nParams: ['webinUserName': {params.get('webinUserName')}, 'webinPassword': ****]\nHeaders:{result.request.headers}\nBody:{body}"
364379
)
365380

366381
return result
@@ -372,11 +387,8 @@ def upload_to_ena(
372387
submission_url: str,
373388
file_transfer: str,
374389
):
375-
ALLOWED_FILE_TRANSFER_SOLUTIONS = {"ftp", "aspera"}
376390
file_transfer = file_transfer.lower()
377391

378-
if file_transfer not in ALLOWED_FILE_TRANSFER_SOLUTIONS:
379-
raise ValueError(f"Unsupported transfer protocol: {file_transfer}")
380392
if file_transfer == "ftp":
381393
uploader = FTPUploader(
382394
submission_url,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"webin": {
3+
"username": "put-your-username-here",
4+
"password": "put-your-password-here"
5+
},
6+
"metabolights_metadata": {
7+
"username": "put-your-username-here",
8+
"password": "put-your-password-here"
9+
},
10+
"metabolights_data": {
11+
"username": "put-your-username-here",
12+
"password": "put-your-password-here"
13+
},
14+
"blahblahblah_repo": {
15+
"username": "put-your-username",
16+
"password": "put-your-password"
17+
}
18+
}

0 commit comments

Comments
 (0)