#!/usr/bin/python # Copyright (c) 2024, Zoran Krleza (zoran.krleza@true-north.hr) # Based on code: # Copyright (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) # Copyright (c) 2018, Marcus Watkins # Copyright (c) 2013, Phillip Gentry # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations DOCUMENTATION = r""" module: gitlab_project_access_token short_description: Manages GitLab project access tokens version_added: 8.4.0 description: - Creates and revokes project access tokens. author: - Zoran Krleza (@pixslx) requirements: - python-gitlab >= 3.1.0 extends_documentation_fragment: - community.general.auth_basic - community.general.gitlab - community.general.attributes notes: - Access tokens can not be changed. If a parameter needs to be changed, an acceess token has to be recreated. Whether tokens are recreated or not is controlled by the O(recreate) option, which defaults to V(never). - Token string is contained in the result only when access token is created or recreated. It can not be fetched afterwards. - Token matching is done by comparing O(name) option. attributes: check_mode: support: full diff_mode: support: none options: project: description: - ID or full path of project in the form of group/name. required: true type: str name: description: - Access token's name. required: true type: str scopes: description: - Scope of the access token. - The values V(manage_runner) and V(self_rotate) were added in community.general 11.3.0. required: true type: list elements: str aliases: ["scope"] choices: - api - read_api - read_registry - write_registry - read_repository - write_repository - create_runner - manage_runner - ai_features - k8s_proxy - self_rotate access_level: description: - Access level of the access token. - The value V(planner) was added in community.general 11.3.0. type: str default: maintainer choices: ["guest", "planner", "reporter", "developer", "maintainer", "owner"] expires_at: description: - Expiration date of the access token in C(YYYY-MM-DD) format. - Make sure to quote this value in YAML to ensure it is kept as a string and not interpreted as a YAML date. type: str required: true recreate: description: - Whether the access token is recreated if it already exists. - When V(never) the token is never recreated. - When V(always) the token is always recreated. - When V(state_change) the token is recreated if there is a difference between desired state and actual state. type: str choices: ["never", "always", "state_change"] default: never state: description: - When V(present) the access token is added to the project if it does not exist. - When V(absent) it is removed from the project if it exists. default: present type: str choices: ["present", "absent"] """ EXAMPLES = r""" - name: "Creating a project access token" community.general.gitlab_project_access_token: api_url: https://gitlab.example.com/ api_token: "somegitlabapitoken" project: "my_group/my_project" name: "project_token" expires_at: "2024-12-31" access_level: developer scopes: - api - read_api - read_repository - write_repository state: present - name: "Revoking a project access token" community.general.gitlab_project_access_token: api_url: https://gitlab.example.com/ api_token: "somegitlabapitoken" project: "my_group/my_project" name: "project_token" expires_at: "2024-12-31" scopes: - api - read_api - read_repository - write_repository state: absent - name: "Change (recreate) existing token if its actual state is different than desired state" community.general.gitlab_project_access_token: api_url: https://gitlab.example.com/ api_token: "somegitlabapitoken" project: "my_group/my_project" name: "project_token" expires_at: "2024-12-31" scopes: - api - read_api - read_repository - write_repository recreate: state_change state: present """ RETURN = r""" access_token: description: - API object. - Only contains the value of the token if the token was created or recreated. returned: success and O(state=present) type: dict """ from datetime import datetime from ansible.module_utils.api import basic_auth_argument_spec from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.gitlab import ( auth_argument_spec, find_project, gitlab_authentication, gitlab, ) ACCESS_LEVELS = dict(guest=10, planner=15, reporter=20, developer=30, maintainer=40, owner=50) class GitLabProjectAccessToken: def __init__(self, module, gitlab_instance): self._module = module self._gitlab = gitlab_instance self.access_token_object = None """ @param project Project Object @param arguments Attributes of the access_token """ def create_access_token(self, project, arguments): changed = False if self._module.check_mode: return True try: self.access_token_object = project.access_tokens.create(arguments) changed = True except gitlab.exceptions.GitlabCreateError as e: self._module.fail_json(msg=f"Failed to create access token: {e}") return changed """ @param project Project object @param name of the access token """ def find_access_token(self, project, name): access_tokens = [x for x in project.access_tokens.list(all=True) if not getattr(x, "revoked", False)] for access_token in access_tokens: if access_token.name == name: self.access_token_object = access_token return False return False def revoke_access_token(self): if self._module.check_mode: return True changed = False try: self.access_token_object.delete() changed = True except gitlab.exceptions.GitlabCreateError as e: self._module.fail_json(msg=f"Failed to revoke access token: {e}") return changed def access_tokens_equal(self): if self.access_token_object.name != self._module.params["name"]: return False if self.access_token_object.scopes != self._module.params["scopes"]: return False if self.access_token_object.access_level != ACCESS_LEVELS[self._module.params["access_level"]]: return False if self.access_token_object.expires_at != self._module.params["expires_at"]: return False return True def main(): argument_spec = basic_auth_argument_spec() argument_spec.update(auth_argument_spec()) argument_spec.update( dict( state=dict(type="str", default="present", choices=["absent", "present"]), project=dict(type="str", required=True), name=dict(type="str", required=True), scopes=dict( type="list", required=True, aliases=["scope"], elements="str", choices=[ "api", "read_api", "read_registry", "write_registry", "read_repository", "write_repository", "create_runner", "manage_runner", "ai_features", "k8s_proxy", "self_rotate", ], ), access_level=dict( type="str", default="maintainer", choices=["guest", "planner", "reporter", "developer", "maintainer", "owner"], ), expires_at=dict(type="str", required=True), recreate=dict(type="str", default="never", choices=["never", "always", "state_change"]), ) ) module = AnsibleModule( argument_spec=argument_spec, mutually_exclusive=[ ["api_username", "api_token"], ["api_username", "api_oauth_token"], ["api_username", "api_job_token"], ["api_token", "api_oauth_token"], ["api_token", "api_job_token"], ], required_together=[["api_username", "api_password"]], required_one_of=[["api_username", "api_token", "api_oauth_token", "api_job_token"]], supports_check_mode=True, ) state = module.params["state"] project_identifier = module.params["project"] name = module.params["name"] scopes = module.params["scopes"] access_level_str = module.params["access_level"] expires_at = module.params["expires_at"] recreate = module.params["recreate"] access_level = ACCESS_LEVELS[access_level_str] try: datetime.strptime(expires_at, "%Y-%m-%d") except ValueError: module.fail_json(msg="Argument expires_at is not in required format YYYY-MM-DD") gitlab_instance = gitlab_authentication(module) gitlab_access_token = GitLabProjectAccessToken(module, gitlab_instance) project = find_project(gitlab_instance, project_identifier) if project is None: module.fail_json(msg=f"Failed to create access token: project {project_identifier} does not exists") gitlab_access_token_exists = False gitlab_access_token.find_access_token(project, name) if gitlab_access_token.access_token_object is not None: gitlab_access_token_exists = True if state == "absent": if gitlab_access_token_exists: gitlab_access_token.revoke_access_token() module.exit_json(changed=True, msg=f"Successfully deleted access token {name}") else: module.exit_json(changed=False, msg="Access token does not exists") if state == "present": if gitlab_access_token_exists: if gitlab_access_token.access_tokens_equal(): if recreate == "always": gitlab_access_token.revoke_access_token() gitlab_access_token.create_access_token( project, {"name": name, "scopes": scopes, "access_level": access_level, "expires_at": expires_at}, ) module.exit_json( changed=True, msg="Successfully recreated access token", access_token=gitlab_access_token.access_token_object._attrs, ) else: module.exit_json( changed=False, msg="Access token already exists", access_token=gitlab_access_token.access_token_object._attrs, ) else: if recreate == "never": module.fail_json( msg="Access token already exists and its state is different. It can not be updated without recreating." ) else: gitlab_access_token.revoke_access_token() gitlab_access_token.create_access_token( project, {"name": name, "scopes": scopes, "access_level": access_level, "expires_at": expires_at}, ) module.exit_json( changed=True, msg="Successfully recreated access token", access_token=gitlab_access_token.access_token_object._attrs, ) else: gitlab_access_token.create_access_token( project, {"name": name, "scopes": scopes, "access_level": access_level, "expires_at": expires_at} ) if module.check_mode: module.exit_json(changed=True, msg="Successfully created access token", access_token={}) else: module.exit_json( changed=True, msg="Successfully created access token", access_token=gitlab_access_token.access_token_object._attrs, ) if __name__ == "__main__": main()