1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-05-12 20:54:12 +00:00

gitlab_user: update SSH keys when key material changes (#11996)

* gitlab_user: update SSH keys when key material changes

Compare SSH keys by key type and key material so comment-only differences remain idempotent while changed keys are replaced. Add unit and integration coverage for SSH key updates.

Fixes #6516

* gitlab_user: add SSH key update modes

Restore backward-compatible same-name SSH key handling by default and
add explicit update and deduplicate modes for controlled replacement
behavior.

Refs: #6516

* Apply suggestions from code review

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>

---------

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
This commit is contained in:
Fulvius 2026-05-12 11:19:15 +02:00 committed by GitHub
parent 00060263a5
commit 2cb4a5d4e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 294 additions and 17 deletions

View file

@ -75,9 +75,20 @@ options:
sshkey_expires_at:
description:
- The expiration date of the SSH public key in ISO 8601 format C(YYYY-MM-DDTHH:MM:SSZ).
- This is only used when adding new SSH public keys.
- This is only used when creating or replacing SSH public keys.
type: str
version_added: 3.1.0
sshkey_update_mode:
description:
- Controls how existing SSH keys with the same O(sshkey_name) are handled.
- Use V(create) to preserve the historical behavior and leave existing keys with the same name unchanged.
- Use V(update) to replace an existing key only when exactly one key with the same name exists and its key material differs.
- Use V(deduplicate) to delete all keys with the same name before creating the requested key.
- SSH key comments are ignored when comparing key material.
type: str
choices: ["create", "update", "deduplicate"]
default: create
version_added: 13.0.0
group:
description:
- ID or Full path of parent group in the form of group/name.
@ -161,6 +172,18 @@ EXAMPLES = r"""
group: super_group/mon_group
access_level: owner
- name: "Replace a GitLab SSH key when one matching name exists"
community.general.gitlab_user:
api_url: https://gitlab.example.com/
api_token: "{{ access_token }}"
name: My Name
username: myusername
email: me@example.com
sshkey_name: MySSH
sshkey_file: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
sshkey_update_mode: update
state: present
- name: "Create GitLab User using external identity provider"
community.general.gitlab_user:
api_url: https://gitlab.example.com/
@ -309,6 +332,7 @@ class GitLabUser:
"name": options["sshkey_name"],
"file": options["sshkey_file"],
"expires_at": options["sshkey_expires_at"],
"update_mode": options["sshkey_update_mode"],
},
)
changed = changed or key_changed
@ -346,8 +370,15 @@ class GitLabUser:
@param sshkey_name Name of the ssh key
"""
@staticmethod
def normalize_ssh_key(ssh_key):
return " ".join(ssh_key.split(None, 2)[:2])
def find_ssh_keys(self, user, sshkey_name):
return [key for key in user.keys.list(**list_all_kwargs) if key.title == sshkey_name]
def ssh_key_exists(self, user, sshkey_name):
return any(k.title == sshkey_name for k in user.keys.list(**list_all_kwargs))
return bool(self.find_ssh_keys(user, sshkey_name))
"""
@param user User object
@ -355,22 +386,44 @@ class GitLabUser:
"""
def add_ssh_key_to_user(self, user, sshkey):
if not self.ssh_key_exists(user, sshkey["name"]):
if self._module.check_mode:
return True
user_keys = self.find_ssh_keys(user, sshkey["name"])
desired_key = self.normalize_ssh_key(sshkey["file"])
update_mode = sshkey["update_mode"]
matching_keys = [key for key in user_keys if self.normalize_ssh_key(key.key) == desired_key]
try:
parameter = {
"title": sshkey["name"],
"key": sshkey["file"],
}
if sshkey["expires_at"] is not None:
parameter["expires_at"] = sshkey["expires_at"]
user.keys.create(parameter)
except gitlab.exceptions.GitlabCreateError as e:
self._module.fail_json(msg=f"Failed to assign sshkey to user: {e}")
if update_mode == "create":
if user_keys:
return False
elif update_mode == "update":
if len(user_keys) > 1:
self._module.warn(
f"Found multiple SSH keys named '{sshkey['name']}'. Skipping update because sshkey_update_mode=update requires a single matching key."
)
return False
if matching_keys:
return False
elif len(user_keys) == 1 and matching_keys:
return False
if self._module.check_mode:
return True
return False
try:
for key in user_keys:
key.delete()
parameter = {
"title": sshkey["name"],
"key": sshkey["file"],
}
if sshkey["expires_at"] is not None:
parameter["expires_at"] = sshkey["expires_at"]
user.keys.create(parameter)
except gitlab.exceptions.GitlabDeleteError as e:
self._module.fail_json(msg=f"Failed to update sshkey for user: {e}")
except gitlab.exceptions.GitlabCreateError as e:
self._module.fail_json(msg=f"Failed to assign sshkey to user: {e}")
return True
"""
@param group Group object
@ -596,6 +649,7 @@ def main():
sshkey_name=dict(type="str"),
sshkey_file=dict(type="str", no_log=False),
sshkey_expires_at=dict(type="str", no_log=False),
sshkey_update_mode=dict(type="str", default="create", choices=["create", "update", "deduplicate"]),
group=dict(type="str"),
access_level=dict(
type="str", default="guest", choices=["developer", "guest", "maintainer", "master", "owner", "reporter"]
@ -637,6 +691,7 @@ def main():
user_sshkey_name = module.params["sshkey_name"]
user_sshkey_file = module.params["sshkey_file"]
user_sshkey_expires_at = module.params["sshkey_expires_at"]
user_sshkey_update_mode = module.params["sshkey_update_mode"]
group_path = module.params["group"]
access_level = module.params["access_level"]
confirm = module.params["confirm"]
@ -684,6 +739,7 @@ def main():
"sshkey_name": user_sshkey_name,
"sshkey_file": user_sshkey_file,
"sshkey_expires_at": user_sshkey_expires_at,
"sshkey_update_mode": user_sshkey_update_mode,
"group_path": group_path,
"access_level": access_level,
"confirm": confirm,