diff --git a/plugins/module_utils/proxmox_api.py b/plugins/module_utils/proxmox_api.py new file mode 100644 index 0000000..089e867 --- /dev/null +++ b/plugins/module_utils/proxmox_api.py @@ -0,0 +1,76 @@ +from dataclasses import dataclass +from typing import List, Tuple + +import requests + +API_BASE_PATH: str = "/api2/json" + + +@dataclass() +class ProxmoxAuthInfo: + instance_url: str + token_id: str + secret: str + verify_cert: bool = True + + +@dataclass(frozen=True) +class ProxmoxRealm: + type: str + realm: str + comment: str = None + default: bool = False + + +def get_realms(auth_info: ProxmoxAuthInfo) -> List['ProxmoxRealm']: + realm_answer = _proxmox_request('get', '/access/domains', auth_info).json() + return list(map( + lambda r: ProxmoxRealm(r['type'], r['realm'], r.get('comment', None), r.get('default', False)), + realm_answer['data'] + )) + + +def set_realm_data(auth_info: ProxmoxAuthInfo, realm: str, config: dict[str, str], dry_run: bool, state: str = 'present') -> Tuple[dict, dict]: + existing_realm = _proxmox_request('get', f"/access/domains/{realm}", auth_info) + existing_realm_data = None + new_realm_data = {} + if state == 'present': + if existing_realm.ok: + existing_realm_data = existing_realm.json()['data'] + if not dry_run: + realm_res = _proxmox_request('put', f"/access/domains/{realm}", auth_info, data=config) + realm_res.raise_for_status() + else: + if not dry_run: + realm_res = _proxmox_request('post', '/access/domains', auth_info, data=(config | {'realm': realm})) + realm_res.raise_for_status() + if not dry_run: + if realm_res.ok: + new_realm_data = _proxmox_request('get', f"/access/domains/{realm}", auth_info).json()['data'] + else: + new_realm_data = config | {'name': realm} + else: + if existing_realm.ok: + existing_realm_data = existing_realm.json()['data'] + if not dry_run: + realm_res = _proxmox_request('delete', f"/access/domains/{realm}", auth_info) + realm_res.raise_for_status() + if realm_res.ok: + new_realm_data = None + else: + new_realm_data = None + else: + new_realm_data = None + return existing_realm_data, new_realm_data + + +def _get_token_from_auth_info(auth_info: ProxmoxAuthInfo) -> str: + return f"PVEAPIToken={auth_info.token_id}={auth_info.secret}" + + +def _proxmox_request(method: str, endpoint: str, auth_info: ProxmoxAuthInfo, **params): + return requests.request(method, + f"{auth_info.instance_url}{API_BASE_PATH}{endpoint}", + headers={'Authorization': _get_token_from_auth_info(auth_info)}, + verify=auth_info.verify_cert, + **params) diff --git a/plugins/modules/realm.py b/plugins/modules/realm.py new file mode 100644 index 0000000..47c8d9f --- /dev/null +++ b/plugins/modules/realm.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +# coding: utf-8 + +# (c) 2022, Johanna Dorothea Reichmann + +__metaclass__ = type + +import dataclasses +import json +import traceback + +from ansible.module_utils.basic import AnsibleModule + +LIB_IMP_ERR = None +try: + from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_api import * + HAS_LIB = True +except: + HAS_LIB = False + LIB_IMP_ERR = traceback.format_exc() + + +DOCUMENTATION = r''' +--- +module: realm +author: +- Johanna Dorothea Reichmann (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 +short_description: Configures an authentication realm in a proxmox node +description: + - "Configue realm in a proxmox instance" +options: + proxmox_instance: + description: Location of the proxmox API with scheme, domain name/ip and port, e.g. https://localhost:8006 + type: str + required: true + proxmox_api_token_id: + description: The token ID containing username, realm and token name (format: user@realm!name) + type: str + required: true + proxmox_api_secret: + description: The secret + type: str + required: true + proxmox_api_verify_cert: + description: If the certificate presented for `proxmox_instance_url` should be verified + type: bool + required: false + default: true + name: + description: Realm to retrieve information about. If left omitted, return all realms + type: str + required: false + config: + description: Configuration of the realm, including it's `type`. See https://pve.proxmox.com/pve-docs/api-viewer/index.html#/access/domains for a list of config options + type: dict[str, str] + required: true + state: + description: If the realm should be present or absent + type: str + choices: [present, absent] + default: present + required: false +''' + +EXAMPLES = r''' +- name: Configure LDAP realm + finallycoffee.proxmox.realm: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + realms: org_ldap + config: + type: ldap + comment: Long description of the authentication source + base_dn: dc=myorg,dc=de + bind_dn: uid=proxmox,dc=services,dc=myorg,dc=de + passwort: supersecret + mode: ldap + server1: ldap.myorg.de + port: 389 + default: 1 + user_attr: uid +- name: Configure OIDC realm + finallycoffee.proxmox.realm: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + realm: external_oidc + config: + type: openid + issuer-url: https://idp.my-org.de/issuer + client_id: myapp + client_key: secret + scopes: profile email + username-claim: sub +''' + +RETURN = r''' +realm: + description: The updated/created realm + returned: When realms were found (matching the filter) + type: list + elements: dict[str, str, str, str] + +''' + + +def main(): + _ = dict + module = AnsibleModule( + argument_spec=_( + proxmox_instance=_(required=True, type='str'), + proxmox_api_token_id=_(required=True, type='str'), + proxmox_api_secret=_(type='str', required=True, no_log=True), + proxmox_api_verify_cert=_(type='bool', required=False, default=True), + name=_(required=True, type='str'), + config=_(required=True, type='dict'), + state=_(type='str', required=False, default='present', choices=['present', 'absent']) + ), + supports_check_mode=True + ) + + result = _( + changed=False, + diff={}, + message='' + ) + + realms = [] + try: + realm_change = set_realm_data(ProxmoxAuthInfo( + module.params['proxmox_instance'], + module.params['proxmox_api_token_id'], + module.params['proxmox_api_secret'], + module.params['proxmox_api_verify_cert'], + ), + module.params['name'], + module.params['config'], + module.check_mode, + module.params['state']) + except IOError as owie: + result['msg'] = owie + module.exit_json(**result) + + diff_excluded_keys = ['name', 'password', 'digest'] + result['realm'] = realm_change[1] + result['diff'] = { + 'before_header': f"{realm_change[0].get('name', module.params['name'])}@{realm_change[0]['type']}" if realm_change[0] and 'type' in realm_change[0] else '', + 'after_header': f"{realm_change[1].get('name', module.params['name'])}@{realm_change[1]['type']}" if realm_change[1] and 'type' in realm_change[1] else '', + 'before': {k: v for k, v in realm_change[0].items() if k not in diff_excluded_keys} if realm_change[0] else realm_change[0], + 'after': {k: v for k, v in realm_change[1].items() if k not in diff_excluded_keys} if realm_change[1] else realm_change[1], + } + result['changed'] = realm_change[0] != realm_change[1] + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/realm_info.py b/plugins/modules/realm_info.py new file mode 100644 index 0000000..e426208 --- /dev/null +++ b/plugins/modules/realm_info.py @@ -0,0 +1,124 @@ +#!/usr/bin/python +# coding: utf-8 + +# (c) 2022, Johanna Dorothea Reichmann + +__metaclass__ = type + +import dataclasses +import traceback + +from ansible.module_utils.basic import AnsibleModule + +LIB_IMP_ERR = None +try: + from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_api import * + HAS_LIB = True +except: + HAS_LIB = False + LIB_IMP_ERR = traceback.format_exc() + + +DOCUMENTATION = r''' +--- +module: realm_info +author: +- Johanna Dorothea Reichmann (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 +short_description: Get configured realms of a proxmox node +description: + - "Lists all configured realms in a proxmox instance" +options: + proxmox_instance: + description: Location of the proxmox API with scheme, domain name/ip and port, e.g. https://localhost:8006 + type: str + required: true + proxmox_api_token_id: + description: The token ID containing username, realm and token name (format: user@realm!name) + type: str + required: true + proxmox_api_secret: + description: The secret + type: str + required: true + proxmox_api_verify_cert: + description: If the certificate presented for `proxmox_instance_url` should be verified + type: bool + required: false + default: true + realm: + description: Realm to retrieve information about. If left omitted, return all realms + type: str + required: false +''' + +EXAMPLES = r''' +- name: Retrieve all reams + finallycoffee.proxmox.realm_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent +- name: Retrieve realm information for 'myrealm' + finallycoffee.proxmox.realm_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + realm: myrealm +''' + +RETURN = r''' +realms: + description: The retrieved realms + returned: When realms were found (matching the filter) + type: list + elements: dict[str, str, str, str] + +''' + + +def main(): + _ = dict + module = AnsibleModule( + argument_spec=_( + proxmox_instance=_(required=True, type='str'), + proxmox_api_token_id=_(required=True, type='str'), + proxmox_api_secret=_(type='str', required=True, no_log=True), + proxmox_api_verify_cert=_(type='bool', required=False, default=True), + realm=_(required=False, type='str'), + ), + supports_check_mode=True + ) + + result = _( + changed=False, + diff={}, + message='' + ) + + realms = [] + try: + realms = get_realms(ProxmoxAuthInfo( + module.params['proxmox_instance'], + module.params['proxmox_api_token_id'], + module.params['proxmox_api_secret'], + module.params['proxmox_api_verify_cert'], + )) + except IOError as owie: + result['msg'] = owie + module.exit_json(**result) + + result['realms'] = list( + map(lambda r: dataclasses.asdict(r), + filter(lambda r: r.realm == module.params['realm'] + if module.params['realm'] is not None + else True, + realms) + ) + ) + + module.exit_json(**result) + + +if __name__ == '__main__': + main()