Skip to content

Commit

Permalink
feat(api): add AuthorizedKeys reset option for the OT-2 + from_local …
Browse files Browse the repository at this point in the history
…fixes (#13745)

* Add error handling so we don't stop processing all keys if one fails
* Skip hidden files when searching for public keys
* Accept ECDSA keys as well
* Don't add existing keys to the authorized_keys file
* Report that keys were added if they are valid even if they already exist on the bot, so the client gets feedback on the operation.
  • Loading branch information
vegano1 authored Oct 10, 2023
1 parent 9998948 commit 6b2b70e
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 10 deletions.
1 change: 1 addition & 0 deletions api/src/opentrons/config/reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions robot-server/tests/service/legacy/routers/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ def test_available_resets(api_client):
"bootScripts",
"tipLengthCalibrations",
"runsHistory",
"authorizedKeys",
]
) == sorted([item["id"] for item in options_list])

Expand Down Expand Up @@ -551,6 +552,7 @@ async def mock_get_persistence_resetter() -> PersistenceResetter:
"pipetteOffsetCalibrations": False,
"tipLengthCalibrations": False,
"runsHistory": False,
"authorizedKeys": False,
},
set(),
],
Expand All @@ -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.
},
Expand All @@ -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}],
Expand Down
25 changes: 15 additions & 10 deletions update-server/otupdate/common/ssh_key_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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}")
try:
with open(key, "r") as gh:
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
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}")

Check warning on line 278 in update-server/otupdate/common/ssh_key_management.py

View check run for this annotation

Codecov / codecov/patch

update-server/otupdate/common/ssh_key_management.py#L269-L278

Added lines #L269 - L278 were not covered by tests
new_keys.append(key_hash)
except Exception as e:
LOG.error(f"Could not process ssh public key: {key} {e}")
continue

Check warning on line 282 in update-server/otupdate/common/ssh_key_management.py

View check run for this annotation

Codecov / codecov/patch

update-server/otupdate/common/ssh_key_management.py#L280-L282

Added lines #L280 - L282 were not covered by tests

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},
Expand Down

0 comments on commit 6b2b70e

Please sign in to comment.