From 083d0dbd88598b9f332799a5b57c8e86b1bd9872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Quatremain?= Date: Tue, 26 Nov 2024 07:16:42 +0100 Subject: [PATCH] quay_robot - keyless authentication --- CHANGELOG.rst | 13 ++ changelogs/changelog.yaml | 10 ++ plugins/modules/quay_repository_mirror.py | 4 +- plugins/modules/quay_robot.py | 104 +++++++++++- plugins/modules/quay_team_ldap.py | 4 +- plugins/modules/quay_team_oidc.py | 4 +- .../targets/quay_robot/tasks/main.yml | 149 ++++++++++++++++++ 7 files changed, 279 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 608b1a7..cd9a32e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,19 @@ Quay Container Registry Collection Release Notes .. contents:: Topics +v2.5.0 +====== + +Release Summary +--------------- + +Support configuring keyless authentications with robot accounts. + +Minor Changes +------------- + +- Add the ``federations`` option to the ``infra.quay_configuration.quay_robot`` module. With this option, you can configure keyless authentications with robot accounts (Quay 3.13 and later) + v2.4.0 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index adeb8d1..ee89334 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -273,3 +273,13 @@ releases: name: quay_repository_prune namespace: '' release_date: '2024-11-23' + 2.5.0: + changes: + minor_changes: + - Add the ``federations`` option to the ``infra.quay_configuration.quay_robot`` + module. With this option, you can configure keyless authentications with robot + accounts (Quay 3.13 and later) + release_summary: Support configuring keyless authentications with robot accounts. + fragments: + - 17-v2.5.0-summary.yml + release_date: '2024-11-26' diff --git a/plugins/modules/quay_repository_mirror.py b/plugins/modules/quay_repository_mirror.py index c7bfc1d..8567f18 100644 --- a/plugins/modules/quay_repository_mirror.py +++ b/plugins/modules/quay_repository_mirror.py @@ -51,7 +51,7 @@ description: - Path to the remote container repository to synchronize, such as quay.io/projectquay/quay for example. - - That parameter is required when creating the mirroring configuration. + - This parameter is required when creating the mirroring configuration. type: str external_registry_username: description: @@ -83,7 +83,7 @@ robot_username: description: - Username of the robot account that is used for synchronization. - - That parameter is required when creating the mirroring configuration. + - This parameter is required when creating the mirroring configuration. type: str image_tags: description: diff --git a/plugins/modules/quay_robot.py b/plugins/modules/quay_robot.py index bff25db..8db757c 100644 --- a/plugins/modules/quay_robot.py +++ b/plugins/modules/quay_robot.py @@ -55,6 +55,34 @@ - Description of the robot account. You cannot update the description of existing robot accounts. type: str + federations: + description: + - Federation configurations, which enable keyless authentication with + robot accounts. + - Robot account federations require Quay version 3.13 or later. + type: list + elements: dict + suboptions: + issuer: + description: + - OpenID Connect (OIDC) issuer URL. + required: true + type: str + subject: + description: + - OpenID Connect (OIDC) subject. + required: true + type: str + append: + description: + - If V(true), then add the robot account federation configurations + defined in O(federations). + - If V(false), then the module sets the federation configurations + specified in O(federations), removing all others federation + configurations. + - Robot account federations require Quay version 3.13 or later. + type: bool + default: true state: description: - If V(absent), then the module deletes the robot account. @@ -68,6 +96,8 @@ notes: - The token that you provide in O(quay_token) must have the "Administer Organization" and "Administer User" permissions. + - The O(federations) and O(append) parameters require Quay version 3.13 or + later. attributes: check_mode: support: full @@ -113,6 +143,18 @@ state: absent quay_host: https://quay.example.com quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 + +# Robot account federations require Quay version 3.13 or later +- name: Ensure the robot account production+robotprod2 exists, with federation + infra.quay_configuration.quay_robot: + name: production+robotprod2 + description: Second robot account for production + federations: + - issuer: https://keycloak-auth-realm.quayadmin.org/realms/quayrealm + subject: 449e14f8-9eb5-4d59-a63e-b7a77c75f770 + state: present + quay_host: https://quay.example.com + quay_token: vgfH9zH5q6eV16Con7SvDQYSr0KPYQimMHVehZv7 """ RETURN = r""" @@ -132,6 +174,8 @@ sample: IWG3K5EW92KZLPP42PMOKM5CJ2DEAQMSCU33A35NR7MNL21004NKVP3BECOWSQP2 """ +import json + from ..module_utils.api_module import APIModule @@ -158,6 +202,15 @@ def main(): argument_spec = dict( name=dict(required=True), description=dict(), + federations=dict( + type="list", + elements="dict", + options=dict( + issuer=dict(required=True), + subject=dict(required=True), + ), + ), + append=dict(type="bool", default=True), state=dict(choices=["present", "absent"], default="present"), ) @@ -167,6 +220,8 @@ def main(): # Extract our parameters name = module.params.get("name") description = module.params.get("description") + federations = module.params.get("federations") + append = module.params.get("append") state = module.params.get("state") namespace, robot_shortname, is_org = module.split_name("name", name, state, separator="+") @@ -179,6 +234,13 @@ def main(): else: path_url = "user/robots/{robot_shortname}".format(robot_shortname=robot_shortname) + fed_url = "{url}/federation".format(url=path_url) + fed_req_set = ( + set([(f.get("issuer"), f.get("subject")) for f in federations]) + if federations + else set() + ) + # Get the robot account details. # # For robot accounts in organizations: @@ -211,15 +273,51 @@ def main(): module.delete(robot_details, "robot account", name, path_url) if robot_details: - exit_module(module, False, robot_details) + # GET /api/v1/organization/{orgname}/robots/{robot_shortname}/federation + # [ + # { + # "issuer": "https://keycloak-realm.quayadmin.org/realms/quayrealm", + # "subject": "449e14f8-9eb5-4d59-a63e-b7a77c75f770" + # } + # ] + fed_details = module.get_object_path(fed_url) + fed_curr_set = ( + set([(f.get("issuer"), f.get("subject")) for f in fed_details]) + if fed_details + else set() + ) + fed_to_add = fed_req_set - fed_curr_set + + if federations is None or fed_req_set == fed_curr_set or (append and not fed_to_add): + exit_module(module, False, robot_details) + + if append: + new_fields = [ + {"issuer": f[0], "subject": f[1], "isExpanded": False} + for f in fed_req_set | fed_curr_set + ] + else: + new_fields = [ + {"issuer": f[0], "subject": f[1], "isExpanded": False} for f in fed_req_set + ] + data = json.dumps(new_fields).encode() + module.create("robot account federation", name, fed_url, data, auto_exit=False) + exit_module(module, True, robot_details) # Prepare the data that gets set for create new_fields = {} if description: new_fields["description"] = description - data = module.unconditional_update("robot account", name, path_url, new_fields) - exit_module(module, True, data) + robot_data = module.unconditional_update("robot account", name, path_url, new_fields) + if federations: + new_fields = [ + {"issuer": f.get("issuer"), "subject": f.get("subject"), "isExpanded": False} + for f in federations + ] + data = json.dumps(new_fields).encode() + module.create("robot account federation", name, fed_url, data, auto_exit=False) + exit_module(module, True, robot_data) if __name__ == "__main__": diff --git a/plugins/modules/quay_team_ldap.py b/plugins/modules/quay_team_ldap.py index 6f18eee..0fb031d 100644 --- a/plugins/modules/quay_team_ldap.py +++ b/plugins/modules/quay_team_ldap.py @@ -33,8 +33,8 @@ name: description: - Name of the team to synchronize or unsynchronize with an LDAP group. - That team must exist (see the M(infra.quay_configuration.quay_team) module to - create it). + This team must exist (see the M(infra.quay_configuration.quay_team) + module to create it). required: true type: str organization: diff --git a/plugins/modules/quay_team_oidc.py b/plugins/modules/quay_team_oidc.py index 68608cc..f7a5b14 100644 --- a/plugins/modules/quay_team_oidc.py +++ b/plugins/modules/quay_team_oidc.py @@ -33,8 +33,8 @@ name: description: - Name of the team to synchronize or unsynchronize with an OIDC group. - That team must exist (see the M(infra.quay_configuration.quay_team) module to - create it). + This team must exist (see the M(infra.quay_configuration.quay_team) + module to create it). required: true type: str organization: diff --git a/tests/integration/targets/quay_robot/tasks/main.yml b/tests/integration/targets/quay_robot/tasks/main.yml index d70c319..3deba12 100644 --- a/tests/integration/targets/quay_robot/tasks/main.yml +++ b/tests/integration/targets/quay_robot/tasks/main.yml @@ -7,6 +7,12 @@ quay_host: "{{ quay_url }}" quay_token: "{{ quay_token }}" validate_certs: false + register: result + +- name: Ensure that the task did change something + ansible.builtin.assert: + that: result['changed'] + fail_msg: The preceding task should have changed something - name: Ensure robot account testrobot1 exists (no change) infra.quay_configuration.quay_robot: @@ -29,6 +35,12 @@ quay_host: "{{ quay_url }}" quay_token: "{{ quay_token }}" validate_certs: false + register: result + +- name: Ensure that the task did change something + ansible.builtin.assert: + that: result['changed'] + fail_msg: The preceding task should have changed something - name: Ensure robot account testrobot3 does not exist (no change) infra.quay_configuration.quay_robot: @@ -52,6 +64,12 @@ quay_host: "{{ quay_url }}" quay_token: "{{ quay_token }}" validate_certs: false + register: result + +- name: Ensure that the task did change something + ansible.builtin.assert: + that: result['changed'] + fail_msg: The preceding task should have changed something - name: Ensure robot account testrobot4 is removed from my namespace infra.quay_configuration.quay_robot: @@ -61,6 +79,12 @@ quay_host: "{{ quay_url }}" quay_token: "{{ quay_token }}" validate_certs: false + register: result + +- name: Ensure that the task did change something + ansible.builtin.assert: + that: result['changed'] + fail_msg: The preceding task should have changed something - name: Non-existing organization and state=absent (no change) infra.quay_configuration.quay_robot: @@ -76,6 +100,130 @@ that: not result['changed'] fail_msg: The preceding task should not have changed anything +# Federation +- name: Ensure robot account testrobot3 exists with federation + infra.quay_configuration.quay_robot: + name: ansibletestorg+testrobot3 + federations: + - issuer: https://keycloak-realm.quayadmin.org/realms/quayrealm + subject: 449e14f8-9eb5-4d59-a63e-b7a77c75f770 + - issuer: https://keycloak-realm.quayadmin.org/realms/quayrealm2 + subject: 449e14f8-9eb5-4d59-a63e-b7a77c75f771 + state: present + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did change something + ansible.builtin.assert: + that: result['changed'] + fail_msg: The preceding task should have changed something + +- name: Ensure robot account testrobot3 exists (no change) 1 + infra.quay_configuration.quay_robot: + name: ansibletestorg+testrobot3 + federations: + - issuer: https://keycloak-realm.quayadmin.org/realms/quayrealm2 + subject: 449e14f8-9eb5-4d59-a63e-b7a77c75f771 + - issuer: https://keycloak-realm.quayadmin.org/realms/quayrealm + subject: 449e14f8-9eb5-4d59-a63e-b7a77c75f770 + append: false + state: present + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure robot account testrobot3 exists (no change) 2 + infra.quay_configuration.quay_robot: + name: ansibletestorg+testrobot3 + append: false + state: present + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure robot account testrobot3 exists (no change) 3 + infra.quay_configuration.quay_robot: + name: ansibletestorg+testrobot3 + federations: [] + append: true + state: present + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did not change anything + ansible.builtin.assert: + that: not result['changed'] + fail_msg: The preceding task should not have changed anything + +- name: Ensure robot account testrobot3 exists with no federation + infra.quay_configuration.quay_robot: + name: ansibletestorg+testrobot3 + federations: [] + append: false + state: present + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did change something + ansible.builtin.assert: + that: result['changed'] + fail_msg: The preceding task should have changed something + +- name: Ensure robot account testrobot3 is updated 1 + infra.quay_configuration.quay_robot: + name: ansibletestorg+testrobot3 + federations: + - issuer: https://keycloak-realm.quayadmin.org/realms/quayrealm + subject: 449e14f8-9eb5-4d59-a63e-b7a77c75f770 + append: false + state: present + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did change something + ansible.builtin.assert: + that: result['changed'] + fail_msg: The preceding task should have changed something + +- name: Ensure robot account testrobot3 is updated 2 + infra.quay_configuration.quay_robot: + name: ansibletestorg+testrobot3 + federations: + - issuer: https://keycloak-realm.quayadmin.org/realms/quayrealm4 + subject: 449e14f8-9eb5-4d59-a63e-b7a77c75f774 + append: false + state: present + quay_host: "{{ quay_url }}" + quay_token: "{{ quay_token }}" + validate_certs: false + register: result + +- name: Ensure that the task did change something + ansible.builtin.assert: + that: result['changed'] + fail_msg: The preceding task should have changed something + +# Errors - name: ERROR EXPECTED Non-existing organization infra.quay_configuration.quay_robot: name: nonexisting+testrobot5 @@ -144,4 +292,5 @@ loop: - ansibletestorg+testrobot1 - ansibletestorg+testrobot2 + - ansibletestorg+testrobot3 ...