From a4498ff89e79ce782491410d9a1461afd7310599 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Mon, 9 Oct 2023 11:45:38 -0400 Subject: [PATCH 1/3] feat(api): add resetAuthorizedKeys option for the OT-2 --- api/src/opentrons/config/reset.py | 1 + .../test_settings_reset_options.tavern.yaml | 30 +++++++++++++++++++ .../service/legacy/routers/test_settings.py | 5 ++++ 3 files changed, 36 insertions(+) diff --git a/api/src/opentrons/config/reset.py b/api/src/opentrons/config/reset.py index ccba3b2fdcb..42a2b606106 100644 --- a/api/src/opentrons/config/reset.py +++ b/api/src/opentrons/config/reset.py @@ -49,6 +49,7 @@ class ResetOptionId(str, Enum): ResetOptionId.pipette_offset, ResetOptionId.tip_length_calibrations, ResetOptionId.runs_history, + ResetOptionId.authorized_keys, ] _FLEX_RESET_OPTIONS = [ ResetOptionId.boot_scripts, diff --git a/robot-server/tests/integration/test_settings_reset_options.tavern.yaml b/robot-server/tests/integration/test_settings_reset_options.tavern.yaml index 7dc49479bba..4025206afee 100644 --- a/robot-server/tests/integration/test_settings_reset_options.tavern.yaml +++ b/robot-server/tests/integration/test_settings_reset_options.tavern.yaml @@ -27,6 +27,10 @@ stages: - id: runsHistory name: Clear Runs History description: !re_search 'Erase this device''s stored history of protocols and runs.' + - id: authorizedKeys + name: SSH Authorized Keys + description: !re_search 'Clear the ssh authorized keys' + --- test_name: POST Reset bootScripts option marks: @@ -123,6 +127,32 @@ stages: message: "gripperOffsetCalibrations is not a valid reset option." errorCode: "4000" --- +test_name: POST Reset authorizedKeys option +marks: + - usefixtures: + - ot2_server_base_url +stages: + - name: POST Reset authorizedKeys true + request: + url: '{ot2_server_base_url}/settings/reset' + method: POST + json: + authorizedKeys: true + response: + status_code: 200 + json: + message: "Options 'authorized_keys' were reset" + - name: POST Reset authorizedKeys false + request: + url: '{ot2_server_base_url}/settings/reset' + method: POST + json: + authorizedKeys: false + response: + status_code: 200 + json: + message: 'Nothing to do' +--- test_name: POST Reset non existant option marks: - usefixtures: diff --git a/robot-server/tests/service/legacy/routers/test_settings.py b/robot-server/tests/service/legacy/routers/test_settings.py index 0f96946dac0..05f4a187045 100644 --- a/robot-server/tests/service/legacy/routers/test_settings.py +++ b/robot-server/tests/service/legacy/routers/test_settings.py @@ -514,6 +514,7 @@ def test_available_resets(api_client): "bootScripts", "tipLengthCalibrations", "runsHistory", + "authorizedKeys", ] ) == sorted([item["id"] for item in options_list]) @@ -551,6 +552,7 @@ async def mock_get_persistence_resetter() -> PersistenceResetter: "pipetteOffsetCalibrations": False, "tipLengthCalibrations": False, "runsHistory": False, + "authorizedKeys": False, }, set(), ], @@ -562,6 +564,7 @@ async def mock_get_persistence_resetter() -> PersistenceResetter: "tipLengthCalibrations": True, "deckCalibration": True, "runsHistory": True, + "authorizedKeys": True, # TODO(mm, 2023-08-04): Figure out how to test Flex-only options, # then add gripperOffsetCalibrations and onDeviceDisplay. }, @@ -575,8 +578,10 @@ async def mock_get_persistence_resetter() -> PersistenceResetter: # mark_directory_reset() being an async method, and api_client having # its own event loop that interferes with making this test async. ResetOptionId.runs_history, + ResetOptionId.authorized_keys, }, ], + [{"authorizedKeys": True}, {ResetOptionId.authorized_keys}], [{"bootScripts": True}, {ResetOptionId.boot_scripts}], [{"pipetteOffsetCalibrations": True}, {ResetOptionId.pipette_offset}], [{"tipLengthCalibrations": True}, {ResetOptionId.tip_length_calibrations}], From f712376f93e52db6637edd85a2ada78255a083b4 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Tue, 10 Oct 2023 09:47:19 -0400 Subject: [PATCH 2/3] add error handling so we dont stop processing all keys if one fails skip hidden files when searching for public keys accept ecdsa keys as well --- .../otupdate/common/ssh_key_management.py | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/update-server/otupdate/common/ssh_key_management.py b/update-server/otupdate/common/ssh_key_management.py index 8129322b80d..e33c37d9ada 100644 --- a/update-server/otupdate/common/ssh_key_management.py +++ b/update-server/otupdate/common/ssh_key_management.py @@ -251,7 +251,8 @@ async def add_from_local(request: web.Request) -> web.Response: Path(root, file) for root, _, files in os.walk("/media") for file in files - if file.endswith(".pub") + # skip hidden files + if not file.startswith(".") and file.endswith(".pub") ] if not pub_keys: LOG.warning("No keys found") @@ -265,16 +266,20 @@ async def add_from_local(request: web.Request) -> web.Response: new_keys = list() with open(AUTHORIZED_KEYS, "a") as fh: for key in pub_keys: - with open(key, "r") as gh: - ssh_key = gh.read() - if "ssh-rsa" not in ssh_key: - LOG.warning(f"Invalid ssh public key: {key}") - continue - key_hash = hashlib.new("md5", ssh_key.encode()).hexdigest() - if not key_present(key_hash): - fh.write(f"{ssh_key}\n") - LOG.info(f"Added new rsa key: {key}") - new_keys.append(key_hash) + try: + with open(key, "r") as gh: + ssh_key = gh.read() + if "ssh-rsa" not in ssh_key and "ecdsa" not in ssh_key: + LOG.warning(f"Invalid ssh public key: {key}") + continue + key_hash = hashlib.new("md5", ssh_key.encode()).hexdigest() + if not key_present(key_hash): + fh.write(f"{ssh_key}\n") + LOG.info(f"Added new rsa key: {key}") + new_keys.append(key_hash) + except Exception as e: + LOG.error(f"Could not process ssh public key: {key} {e}") + continue return web.json_response( # type: ignore[no-untyped-call,no-any-return] data={"message": f"Added {len(new_keys)} new keys", "key_md5": new_keys}, From f4177de868cebe26baff27e411b38ba9a108a5ce Mon Sep 17 00:00:00 2001 From: vegano1 Date: Tue, 10 Oct 2023 11:23:16 -0400 Subject: [PATCH 3/3] dont add existing keys to authorized_keys file report that keys were added if they are valid even if they already exist on the bot, so they get feedback on the operation. --- update-server/otupdate/common/ssh_key_management.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/update-server/otupdate/common/ssh_key_management.py b/update-server/otupdate/common/ssh_key_management.py index e33c37d9ada..fa327806701 100644 --- a/update-server/otupdate/common/ssh_key_management.py +++ b/update-server/otupdate/common/ssh_key_management.py @@ -268,7 +268,7 @@ async def add_from_local(request: web.Request) -> web.Response: for key in pub_keys: try: with open(key, "r") as gh: - ssh_key = gh.read() + ssh_key = gh.read().strip() if "ssh-rsa" not in ssh_key and "ecdsa" not in ssh_key: LOG.warning(f"Invalid ssh public key: {key}") continue @@ -276,7 +276,7 @@ async def add_from_local(request: web.Request) -> web.Response: if not key_present(key_hash): fh.write(f"{ssh_key}\n") LOG.info(f"Added new rsa key: {key}") - new_keys.append(key_hash) + new_keys.append(key_hash) except Exception as e: LOG.error(f"Could not process ssh public key: {key} {e}") continue