Skip to content

Commit 3dc352a

Browse files
mhmdk0hasan7n
andauthored
Private model webui (#666)
* add decryption file and tooltips/placeholders to container registration * change profile -> settings page, refactor according to changes * add certificate settings * modify GetUserCertificate.run - interactive * add certificate settings in webui * fix certificate corruption * tmp * 'Get' instead of 'Generate' certificate - certificate status 'local' to 'to be uploaded' * fix database lock error after token expiration in default profile * join thread after setting the event - auto grant access * fix certificate corruption - updated * modify 'generate' into get/retrieve certificate - settings.py. submit certificate without passing name * use sanitize path for decryption key and add more checks for validation * tmp * make 'access pending' underlined in dataset details - disable model run button if access isn't granted * - add emails to grant access and auto grant access. - show keys with revoke btn in container_access page, add revoke functionality. * move get_container_type and generate_uuid to webui.utils * fix decryption_key error in submit.py * add regex in UI to filter url/code messages from get_client_certificate output * refactor notifications/add critical printing (popup) in webui * refactor modals - making print critical works. add modal queue * fix some ui bugs - contianer_access.html * update e2e tests according to modal changes add: - encrypted container registration/association - generate and submit certificate for dataset owner - grant access - encrypted model owner to dataset owner - delete keys after running the flow * fix critical print * make settings page accessible if user is not logged in. merge CA and Fingerprint settings into profile settings (platform, gpus) modify some ui bugs. * fix e2e tests * fix e2e tests - profile activation * fix certificate checks - unit tests * rename ui.regex - ui.is_parsed_output * add decryption key to compatibility test form - webui * refactor parsed_ouput * fix cert issues * fix codeql alerts for decryption key * fix set profile args logic in webui * fix redirect security alert * fix codeql error * fix e2e test - compatibility test --------- Co-authored-by: hasan7n <hasankassim7@hotmail.com>
1 parent 7de9eb0 commit 3dc352a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+3197
-1075
lines changed

cli/medperf/commands/certificate/client_certificate.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from medperf.account_management import get_medperf_user_data
22
from medperf.certificates import get_client_cert
33
from medperf.exceptions import MedperfException
4+
from medperf.ui.interface import UI
45
from medperf.utils import get_pki_assets_path, remove_path
56
from medperf import config
67
from medperf.entities.ca import CA
@@ -11,6 +12,7 @@ class GetUserCertificate:
1112
@staticmethod
1213
def run(overwrite: bool = False):
1314
"""get user cert"""
15+
ui: UI = config.ui
1416
ca_id = config.certificate_authority_id
1517
ca = CA.get(ca_id)
1618

@@ -22,4 +24,8 @@ def run(overwrite: bool = False):
2224
"Cert and key already present. Rerun the command with --overwrite"
2325
)
2426
remove_path(output_path, sensitive=True)
25-
get_client_cert(ca, email, output_path)
27+
28+
with ui.interactive():
29+
ui.text = "Getting Certificate"
30+
with ui.parsed_output():
31+
get_client_cert(ca, email, output_path)

cli/medperf/commands/certificate/utils.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
from medperf import config
22
from medperf.entities.certificate import Certificate
3-
from medperf.utils import get_pki_assets_path
3+
from medperf.utils import get_pki_assets_path, remove_path
44
from medperf.account_management import get_medperf_user_data
55
import os
66
import base64
77
import logging
88

99

10+
def _check_and_clean_certificate_corruption(local_cert_folder):
11+
private_key_path = os.path.join(local_cert_folder, config.private_key_file)
12+
certificate_path = os.path.join(local_cert_folder, config.certificate_file)
13+
14+
private_key_exists = os.path.exists(private_key_path)
15+
certificate_exists = os.path.exists(certificate_path)
16+
17+
certificate_corrupted = not private_key_exists or not certificate_exists
18+
19+
if certificate_corrupted:
20+
remove_path(local_cert_folder, sensitive=True)
21+
22+
1023
def current_user_certificate_status():
1124
"""Check the status of the current user certificate. Possible cases:
1225
- No local certificate folder and no submitted certificate
@@ -35,6 +48,9 @@ def current_user_certificate_status():
3548
email = get_medperf_user_data()["email"]
3649
local_cert_folder = get_pki_assets_path(email, config.certificate_authority_id)
3750

51+
# If certificate is corrupted, delete the certificate folder
52+
_check_and_clean_certificate_corruption(local_cert_folder)
53+
3854
# Check
3955
exists_locally = os.path.exists(local_cert_folder)
4056
submitted = user_cert_object is not None

cli/medperf/commands/compatibility_test/run.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from medperf.entities.benchmark import Benchmark
77
from medperf.entities.report import TestReport
88
from medperf.exceptions import InvalidArgumentError
9+
from medperf.utils import sanitize_path
910
from .validate_params import CompatibilityTestParamsValidator
1011
from .utils import download_demo_data, prepare_cube, get_cube, create_test_dataset
1112
import medperf.config as config
@@ -29,7 +30,7 @@ def run(
2930
skip_data_preparation_step: bool = False,
3031
use_local_model_image: bool = False,
3132
model_decryption_key: Path = None,
32-
) -> (str, dict):
33+
) -> tuple[str, dict]:
3334
"""Execute a test workflow. Components of a complete workflow should be passed.
3435
When only the benchmark is provided, it implies the following workflow will be used:
3536
- the benchmark's demo dataset is used as the raw data
@@ -143,7 +144,7 @@ def __init__(
143144
self.evaluator_cube = None
144145

145146
# Decryption key is used for compatibility test of encrypted containers
146-
self.model_decryption_key = model_decryption_key
147+
self.model_decryption_key = sanitize_path(model_decryption_key)
147148

148149
self.validator = CompatibilityTestParamsValidator(
149150
self.benchmark_uid,

cli/medperf/commands/mlcube/grant_access.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,12 @@ def validate_allowed_emails(self):
7171
self.allowed_emails = validate_and_normalize_emails(self.allowed_emails)
7272

7373
def verify_certificate_authority(self):
74-
config.ui.print("Verifying Certificate Authority")
7574
ca = CA.get(uid=config.certificate_authority_id)
76-
verify_certificate_authority(
77-
ca, expected_fingerprint=config.certificate_authority_fingerprint
78-
)
75+
with config.ui.interactive():
76+
config.ui.text = "Verifying Certificate Authority"
77+
verify_certificate_authority(
78+
ca, expected_fingerprint=config.certificate_authority_fingerprint
79+
)
7980

8081
def prepare_certificates_list(self):
8182
config.ui.print("Getting Data Owner Certificates")

cli/medperf/commands/mlcube/submit.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import medperf.config as config
44
from medperf.entities.cube import Cube
55
from medperf.exceptions import InvalidArgumentError
6-
from medperf.utils import remove_path, store_decryption_key
6+
from medperf.utils import remove_path, sanitize_path, store_decryption_key
77
import logging
88

99

@@ -33,7 +33,7 @@ def __init__(self, submit_info: dict, decryption_key: str = None):
3333
self.comms = config.comms
3434
self.ui = config.ui
3535
self.cube = Cube(**submit_info)
36-
self.decryption_key = decryption_key
36+
self.decryption_key = sanitize_path(decryption_key)
3737
config.tmp_paths.append(self.cube.path)
3838

3939
def download_config_files(self):
@@ -50,6 +50,16 @@ def validate(self):
5050
"Container is not encrypted, but a decryption key is provided"
5151
)
5252

53+
if self.decryption_key is not None:
54+
if not os.path.exists(self.decryption_key):
55+
raise InvalidArgumentError(
56+
f"Decryption key does not exist at path {self.decryption_key}"
57+
)
58+
if os.path.isdir(self.decryption_key):
59+
raise InvalidArgumentError(
60+
f"The provided decryption key path {self.decryption_key} is a directory!"
61+
)
62+
5363
def download_run_files(self):
5464
self.cube.download_run_files()
5565

cli/medperf/comms/auth/auth0.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,15 @@ def access_token(self):
174174
except sqlite3.OperationalError:
175175
msg = "Another process is using the database. Try again later"
176176
raise CommunicationError(msg)
177-
token = self._access_token
177+
178178
# Sqlite will automatically execute COMMIT and close the connection
179179
# if an exception is raised during the retrieval of the access token.
180-
db.execute("COMMIT")
181-
db.close()
180+
181+
try:
182+
token = self._access_token
183+
finally:
184+
db.execute("COMMIT")
185+
db.close()
182186

183187
return token
184188

cli/medperf/tests/commands/certificate/test_utils_certificates.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,21 @@ def test_current_user_certificate_status(
9999
mocker.patch(PATCH_STATUS.format("get_pki_assets_path"), return_value=local_path)
100100
if local_exists:
101101
fs.create_dir(local_path)
102+
fs.create_file(
103+
os.path.join(local_path, config.private_key_file), contents=b"private-key"
104+
)
105+
fs.create_file(
106+
os.path.join(local_path, config.certificate_file), contents=b"abc"
107+
)
102108
if remote_exists and local_matches:
109+
if os.path.exists(os.path.join(local_path, config.certificate_file)):
110+
fs.remove(os.path.join(local_path, config.certificate_file))
103111
fs.create_file(
104112
os.path.join(local_path, config.certificate_file), contents=b"abc"
105113
)
106114
elif remote_exists and not local_matches:
115+
if os.path.exists(os.path.join(local_path, config.certificate_file)):
116+
fs.remove(os.path.join(local_path, config.certificate_file))
107117
fs.create_file(
108118
os.path.join(local_path, config.certificate_file), contents=b"xyz"
109119
)

cli/medperf/ui/cli.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import typer
23
from getpass import getpass
34
from yaspin import yaspin
@@ -10,13 +11,56 @@ class CLI(UI):
1011
def __init__(self):
1112
self.spinner = yaspin(color="green")
1213
self.is_interactive = False
14+
self.is_parsed_output = False
15+
16+
def print_url_message(self, message: str):
17+
match = re.search(r"https?://[^\s]+", message)
18+
19+
url = match.group(0)
20+
start, end = match.span()
21+
22+
before = message[:start].strip()
23+
after = message[end:].strip()
24+
25+
if before.strip():
26+
self.print(before)
27+
28+
self.print_url(url)
29+
30+
if after.strip():
31+
self.print(after)
32+
33+
def contains_url(self, message: str):
34+
return bool(re.search(r"https?://[^\s]+", message))
35+
36+
def is_code(self, message: str):
37+
ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
38+
message = ansi_escape.sub("", message.strip())
39+
return bool(re.fullmatch(r"[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}", message))
40+
41+
def print_subprocess_logs(self, msg: str):
42+
"""Display subprocess logs on the command line
43+
44+
Args:
45+
msg (str): message to print
46+
"""
47+
if self.is_parsed_output:
48+
if self.is_code(msg):
49+
self.print_code(msg)
50+
elif self.contains_url(msg):
51+
self.print_url_message(msg)
52+
else:
53+
self.print(msg)
54+
else:
55+
self.print(msg)
1356

1457
def print(self, msg: str = ""):
1558
"""Display a message on the command line
1659
1760
Args:
1861
msg (str): message to print
1962
"""
63+
2064
self._print(msg)
2165

2266
def print_error(self, msg: str):
@@ -29,6 +73,9 @@ def print_error(self, msg: str):
2973
msg = typer.style(msg, fg=typer.colors.RED, bold=True)
3074
self._print(msg)
3175

76+
def print_critical(self, msg: str):
77+
self.print_warning(msg)
78+
3279
def print_warning(self, msg: str):
3380
"""Display a warning message on the command line
3481
@@ -78,6 +125,31 @@ def interactive(self):
78125
finally:
79126
self.stop_interactive()
80127

128+
def start_parsed_output(self):
129+
"""Start a parsed output session where messages will be displayed based on regular expressions"""
130+
self.is_parsed_output = True
131+
132+
def stop_parsed_output(self):
133+
"""Stop the parsed output session"""
134+
self.is_parsed_output = False
135+
136+
@contextmanager
137+
def parsed_output(self):
138+
"""Context managed parsed output session.
139+
140+
Yields:
141+
CLI: Yields the current CLI instance with a parsed output session initialized
142+
"""
143+
if self.is_parsed_output:
144+
# if already parsed output, do nothing
145+
yield self
146+
else:
147+
self.start_parsed_output()
148+
try:
149+
yield self
150+
finally:
151+
self.stop_parsed_output()
152+
81153
@property
82154
def text(self):
83155
return self.spinner.text

0 commit comments

Comments
 (0)