From 3f64fc9788f28f4876709fa094b34241dcf6b154 Mon Sep 17 00:00:00 2001 From: Sepehr-A Date: Tue, 9 Apr 2024 11:02:21 +0200 Subject: [PATCH 1/3] add interactive username prompt - in credential_loader/static Signed-off-by: Sepehr-A --- suzieq/poller/controller/credential_loader/static.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/suzieq/poller/controller/credential_loader/static.py b/suzieq/poller/controller/credential_loader/static.py index 3cbd3fa2c9..98d7284fb1 100644 --- a/suzieq/poller/controller/credential_loader/static.py +++ b/suzieq/poller/controller/credential_loader/static.py @@ -5,8 +5,8 @@ from suzieq.poller.controller.credential_loader.base_credential_loader import \ CredentialLoader, CredentialLoaderModel, check_credentials -from suzieq.shared.utils import get_sensitive_data from suzieq.shared.exceptions import SensitiveLoadError, InventorySourceError +from suzieq.shared.utils import get_sensitive_data class StaticModel(CredentialLoaderModel): @@ -18,7 +18,7 @@ class StaticModel(CredentialLoaderModel): keyfile: Optional[str] enable_password: Optional[str] = Field(alias='enable-password') - @validator('password', 'key_passphrase', 'enable_password') + @validator('username', 'password', 'key_passphrase', 'enable_password') def validate_sens_field(cls, field): """Validate if the sensitive var was passed correctly """ @@ -57,6 +57,14 @@ def init(self, init_data: dict): init_data['key_passphrase'] = init_data.pop('key-passphrase', None) super().init(init_data) + if self._data.username == 'ask': + try: + self._data.username = get_sensitive_data( + self._data.username, + f'{self.name} Username to login to device: ') + except SensitiveLoadError as e: + raise InventorySourceError(f'{self.name} {e}') + if self._data.password == 'ask': try: self._data.password = get_sensitive_data( From 0657146931bd7c125b9aba2cf7ee7dba8851caa5 Mon Sep 17 00:00:00 2001 From: Sepehr-A Date: Tue, 9 Apr 2024 11:03:10 +0200 Subject: [PATCH 2/3] Enhance username input simulation - test_static_loader Signed-off-by: Sepehr-A --- .../static/test_static_loader.py | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/unit/poller/controller/credential_loader/static/test_static_loader.py b/tests/unit/poller/controller/credential_loader/static/test_static_loader.py index b4849b952a..56adebda3c 100644 --- a/tests/unit/poller/controller/credential_loader/static/test_static_loader.py +++ b/tests/unit/poller/controller/credential_loader/static/test_static_loader.py @@ -1,7 +1,8 @@ from typing import Dict -from pydantic import ValidationError import pytest +from pydantic import ValidationError + from suzieq.poller.controller.credential_loader.static import (StaticLoader, StaticModel) from tests.unit.poller.shared.utils import read_yaml_file @@ -84,30 +85,38 @@ def test_variables_init(monkeypatch): """ ask_password = 'ask_password' env_password = 'env_password' + ask_username = 'ask_username' + env_username = 'env_username' plain_passphrase = 'my-pass' # 'env' and 'plain' values init_data = { 'name': 'n', + 'username': 'env:SUZIEQ_ENV_USERNAME', 'key-passphrase': f'plain:{plain_passphrase}', 'password': 'env:SUZIEQ_ENV_PASSWORD' } monkeypatch.setenv('SUZIEQ_ENV_PASSWORD', env_password) + monkeypatch.setenv('SUZIEQ_ENV_USERNAME', env_username) sl = StaticModel(**init_data) assert sl.key_passphrase == plain_passphrase assert sl.password == env_password + assert sl.username == env_username # 'ask' values init_data = { 'name': 'n', + 'username': 'ask', 'password': 'ask' } valid_data = StaticModel(**init_data).dict(by_alias=True) - monkeypatch.setattr('getpass.getpass', lambda x: ask_password) + mock_get_pass = MockGetPass([ask_username, ask_password]) + monkeypatch.setattr('getpass.getpass', mock_get_pass) sl = StaticLoader(valid_data) assert sl._data.password == ask_password + assert sl._data.username == ask_username # unknown parameter init_data = { @@ -117,3 +126,40 @@ def test_variables_init(monkeypatch): } with pytest.raises(ValidationError): StaticModel(**init_data) + + +class MockGetPass: + """ + Mocks `getpass.getpass` for testing, cycling through a list of predefined + responses. + + Attributes: + responses (list): A list of responses to simulate sequential user inputs. + call_count (int): Tracks calls to provide the next response in the list. + """ + + def __init__(self, responses: list): + """ + Initializes the mock with responses and resets call count. + + Args: + responses: Simulated user inputs. + """ + self.responses = responses + self.call_count = 0 + + def __call__(self, prompt=''): + """ + Returns the next response from the list, mimicking user input. + + Args: + prompt: Unused, present for compatibility. + + Returns: + The next simulated user input. + """ + response = self.responses[self.call_count] + self.call_count += 1 + if self.call_count >= len(self.responses): + self.call_count = 0 + return response From 22adb0aed7c43ee09683cb1ba07c18c0d66aea54 Mon Sep 17 00:00:00 2001 From: Sepehr-A Date: Wed, 10 Apr 2024 11:11:31 +0200 Subject: [PATCH 3/3] Document environment variable setup - inventory.md Signed-off-by: Sepehr-A --- docs/inventory.md | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/inventory.md b/docs/inventory.md index 3ad962d0f0..c6c896ed73 100644 --- a/docs/inventory.md +++ b/docs/inventory.md @@ -69,6 +69,22 @@ auths: - name: suzieq-user-04 key-passphrase: ask keyfile: path/to/key + +- name: suzieq-user-05 + username: ask + password: ask + +- name: suzieq-user-06 + username: env:USERNAME_ENV_VAR + password: ask + +- name: suzieq-user-07 + username: env:USERNAME_ENV_VAR + password: env:PASSWORD_ENV_VAR + +- name: suzieq-user-08 + username: ask + password: env:PASSWORD_ENV_VAR namespaces: - name: testing @@ -80,7 +96,7 @@ namespaces: !!! warning Some observations on the YAML file above: - - **This is just an example** that covers all the possible combinations, **not an real life inventory** + - **This is just an example** that covers most of the possible combinations, **not an real life inventory** - **Do not specify device type unless you're using REST**. SuzieQ automatically determines device type with SSH - Most environments require setting the `ignore-known-hosts` option in the device section - The auths section shows all the different authorization methods supported by SuzieQ @@ -95,7 +111,8 @@ For this reason, SuzieQ inventory now supports three different options to store - `env:`: the sensitive information is stored in an environment variable - `ask`: the user can write the sensitive information on the stdin -Currently this method is used to specify passwords, passphrases and tokens. +This method is currently utilized for specifying usernames, passwords, +passphrases, and tokens. ## Sources @@ -323,8 +340,10 @@ In case a private key is used to authenticate: Where `key-passphrase` is the passphrase of the private key. -Both `passoword` and `key-passphrase` are considered [sensitive data](#sensitive-data). -For this reason they can be set as plaintext, env variable or asked to the user via stdin. +`Password`, `key-passphrase` and `username` are considered [sensitive +data](#sensitive-data). +For this reason they can be set as plaintext, env variable or +asked to the user via stdin. ### Credential file