diff options
author | Gaige B Paulsen <gaige@cluetrust.com> | 2025-01-02 14:06:58 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-02 19:06:58 +0000 |
commit | 9e159990f949652ec1b22f9a9a6e72828bdd1e80 (patch) | |
tree | f3f580083415d4ea48cf81b86e02f08df8f9f26a /plugins/modules/vyos_user.py | |
parent | dbd87e3ab89b7839e41df76c2fa7712855853fd3 (diff) | |
download | vyos.vyos-9e159990f949652ec1b22f9a9a6e72828bdd1e80.tar.gz vyos.vyos-9e159990f949652ec1b22f9a9a6e72828bdd1e80.zip |
T6988: fix: remove role/level, fix tests (#371)
* T6988: fix: remove role/level, fix tests
* feature: add support for SSH keys
* tests: add integration tests for public_keys
* feat: add encrypted password support
* tests: add unit for encrypted
* tests: fix wrapping in YAML
* tests: fix smoke tests
Diffstat (limited to 'plugins/modules/vyos_user.py')
-rw-r--r-- | plugins/modules/vyos_user.py | 195 |
1 files changed, 158 insertions, 37 deletions
diff --git a/plugins/modules/vyos_user.py b/plugins/modules/vyos_user.py index 53c45c2..5aebf94 100644 --- a/plugins/modules/vyos_user.py +++ b/plugins/modules/vyos_user.py @@ -62,6 +62,12 @@ options: - The C(full_name) argument provides the full name of the user account to be created on the remote device. This argument accepts any text string value. type: str + encrypted_password: + description: + - The encrypted password of the user account on the remote device. Note that unlike + the C(configured_password) argument, this argument ignores the C(update_password) + and updates if the value is different from the one in the device running config. + type: str configured_password: description: - The password to be configured on the VyOS device. The password needs to be provided @@ -77,13 +83,6 @@ options: choices: - on_create - always - level: - description: - - The C(level) argument configures the level of the user when logged into the - system. This argument accepts string values admin or operator. - type: str - aliases: - - role state: description: - Configures the state of the username definition as it relates to the device @@ -94,6 +93,32 @@ options: choices: - present - absent + public_keys: &public_keys + description: + - Public keys for authentiction over SSH. + type: list + elements: dict + suboptions: + name: + description: Name of the key (usually in the form of user@hostname) + required: true + type: str + key: + description: Public key string (base64 encoded) + required: true + type: str + type: + description: Type of the key + required: true + type: str + choices: + - ssh-dss + - ssh-rsa + - ecdsa-sha2-nistp256 + - ecdsa-sha2-nistp384 + - ssh-ed25519 + - ecdsa-sha2-nistp521 + name: description: - The username to be configured on the VyOS device. This argument accepts a string @@ -104,6 +129,12 @@ options: - The C(full_name) argument provides the full name of the user account to be created on the remote device. This argument accepts any text string value. type: str + encrypted_password: + description: + - The encrypted password of the user account on the remote device. Note that unlike + the C(configured_password) argument, this argument ignores the C(update_password) + and updates if the value is different from the one in the device running config. + type: str configured_password: description: - The password to be configured on the VyOS device. The password needs to be provided @@ -120,13 +151,7 @@ options: choices: - on_create - always - level: - description: - - The C(level) argument configures the level of the user when logged into the - system. This argument accepts string values admin or operator. - type: str - aliases: - - role + public_keys: *public_keys purge: description: - Instructs the module to consider the resource definition absolute. It will remove @@ -161,7 +186,6 @@ EXAMPLES = """ aggregate: - name: netop - name: netend - level: operator state: present - name: Change Password for User netop vyos.vyos.vyos_user: @@ -177,7 +201,6 @@ commands: returned: always type: list sample: - - set system login user test level operator - set system login user authentication plaintext-password password """ @@ -198,11 +221,6 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ) -def validate_level(value, module): - if value not in ("admin", "operator"): - module.fail_json(msg="level must be either admin or operator, got %s" % value) - - def spec_to_commands(updates, module): commands = list() update_password = module.params["update_password"] @@ -220,11 +238,39 @@ def spec_to_commands(updates, module): commands.append("delete system login user %s" % want["name"]) continue - if needs_update(want, have, "level"): - add(commands, want, "level %s" % want["level"]) - if needs_update(want, have, "full_name"): - add(commands, want, "full-name %s" % want["full_name"]) + add(commands, want, "full-name '%s'" % want["full_name"]) + + # look both ways for public_keys to handle replacement + want_keys = want.get("public_keys") or dict() + have_keys = have.get("public_keys") or dict() + for key_name in want_keys: + key = want_keys[key_name] + if key_name not in have_keys or key != have_keys[key_name]: + add( + commands, + want, + "authentication public-keys %s key '%s'" % (key["name"], key["key"]), + ) + add( + commands, + want, + "authentication public-keys %s type '%s'" % (key["name"], key["type"]), + ) + + for key_name in have_keys: + if key_name not in want_keys: + commands.append( + "delete system login user %s authentication public-keys %s" + % (want["name"], key_name), + ) + + if needs_update(want, have, "encrypted_password"): + add( + commands, + want, + "authentication encrypted-password '%s'" % want["encrypted_password"], + ) if needs_update(want, have, "configured_password"): if update_password == "always" or not have: @@ -237,20 +283,57 @@ def spec_to_commands(updates, module): return commands -def parse_level(data): - match = re.search(r"level (\S+)", data, re.M) - if match: - level = match.group(1)[1:-1] - return level - - def parse_full_name(data): - match = re.search(r"full-name (\S+)", data, re.M) + match = re.search(r"full-name '(\S+)'", data, re.M) if match: full_name = match.group(1)[1:-1] return full_name +def parse_key(data): + match = re.search(r"key '(\S+)'", data, re.M) + if match: + key = match.group(1) + return key + + +def parse_key_type(data): + match = re.search(r"type '(\S+)'", data, re.M) + if match: + key_type = match.group(1) + return key_type + + +def parse_public_keys(data): + """ + Parse public keys from the configuration + returning dictionary of dictionaries indexed by key name + """ + match = re.findall(r"public-keys (\S+)", data, re.M) + if not match: + return dict() + + keys = dict() + for key in set(match): + regex = r" %s .+$" % key + cfg = re.findall(regex, data, re.M) + cfg = "\n".join(cfg) + obj = { + "name": key, + "key": parse_key(cfg), + "type": parse_key_type(cfg), + } + keys[key] = obj + return keys + + +def parse_encrypted_password(data): + match = re.search(r"authentication encrypted-password '(\S+)'", data, re.M) + if match: + encrypted_password = match.group(1) + return encrypted_password + + def config_to_dict(module): data = get_config(module) @@ -268,8 +351,9 @@ def config_to_dict(module): "name": user, "state": "present", "configured_password": None, - "level": parse_level(cfg), "full_name": parse_full_name(cfg), + "encrypted_password": parse_encrypted_password(cfg), + "public_keys": parse_public_keys(cfg), } instances.append(obj) @@ -289,6 +373,21 @@ def get_param_value(key, item, module): return value +def map_key_params_to_dict(keys): + """ + Map the list of keys to a dictionary of dictionaries + indexed by key name + """ + all_keys = dict() + if keys is None: + return all_keys + + for key in keys: + key_name = key["name"] + all_keys[key_name] = key + return all_keys + + def map_params_to_obj(module): aggregate = module.params["aggregate"] if not aggregate: @@ -309,9 +408,10 @@ def map_params_to_obj(module): for item in users: get_value = partial(get_param_value, item=item, module=module) item["configured_password"] = get_value("configured_password") + item["encrypted_password"] = get_value("encrypted_password") item["full_name"] = get_value("full_name") - item["level"] = get_value("level") item["state"] = get_value("state") + item["public_keys"] = map_key_params_to_dict(get_value("public_keys")) objects.append(item) return objects @@ -332,13 +432,30 @@ def update_objects(want, have): def main(): """main entry point for module execution""" + public_key_spec = dict( + name=dict(required=True, type="str"), + key=dict(required=True, type="str", no_log=False), + type=dict( + required=True, + type="str", + choices=[ + "ssh-dss", + "ssh-rsa", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ssh-ed25519", + "ecdsa-sha2-nistp521", + ], + ), + ) element_spec = dict( name=dict(), full_name=dict(), - level=dict(aliases=["role"]), configured_password=dict(no_log=True), + encrypted_password=dict(no_log=False), update_password=dict(default="always", choices=["on_create", "always"]), state=dict(default="present", choices=["present", "absent"]), + public_keys=dict(type="list", elements="dict", options=public_key_spec), ) aggregate_spec = deepcopy(element_spec) @@ -359,7 +476,11 @@ def main(): argument_spec.update(element_spec) - mutually_exclusive = [("name", "aggregate")] + mutually_exclusive = [ + ("name", "aggregate"), + ("encrypted_password", "configured_password"), + ] + module = AnsibleModule( argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, |