From 571cc210b5e48aa959ba6c16e6b325abaf0a12de Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Wed, 6 May 2020 13:22:45 +0200 Subject: [PATCH 1/4] ansible_freeipa_module: New function load_cert_from_str For certmapdata processing in ipauser it is needed to be able to load a cert from a string given in the task to be able to get the issuer and subject of the certificate. The format of the certifiacte here is lacking the markers for the begin and end of the certificate. Therefore load_pem_x509_certificate can not be used directly. Also in IPA < 4.5 it is needed to load the certificate with load_certificate instead of load_pem_x509_certificate. The function is implementing this properly. --- .../module_utils/ansible_freeipa_module.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index af45a6cb..37e1fdfd 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -48,6 +48,13 @@ try: from ipalib.x509 import Encoding except ImportError: from cryptography.hazmat.primitives.serialization import Encoding + +try: + from ipalib.x509 import load_pem_x509_certificate +except ImportError: + from ipalib.x509 import load_certificate + load_pem_x509_certificate = None + import socket import base64 import six @@ -323,6 +330,20 @@ def encode_certificate(cert): return encoded +def load_cert_from_str(cert): + cert = cert.strip() + if not cert.startswith("-----BEGIN CERTIFICATE-----"): + cert = "-----BEGIN CERTIFICATE-----\n" + cert + if not cert.endswith("-----END CERTIFICATE-----"): + cert += "\n-----END CERTIFICATE-----" + + if load_pem_x509_certificate is not None: + cert = load_pem_x509_certificate(cert.encode('utf-8')) + else: + cert = load_certificate(cert.encode('utf-8')) + return cert + + def is_valid_port(port): if not isinstance(port, int): return False From 6a69bbeafb60427c2854c24a8d2f3725861fe8f9 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Wed, 6 May 2020 13:28:04 +0200 Subject: [PATCH 2/4] ansible_freeipa_module: New function DN_x500_text This function is needed to properly convert issuer and subject from a certificate or the issuer and subject parameters in ipauser for certmapdata to the data representation where the items in DN are reversed. The function additionally provides a fallback solution for IPA < 4.5. Certmapdata is not supported for IPA < 4.5, but the conversion is done before the API version can be checked. --- plugins/module_utils/ansible_freeipa_module.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index 37e1fdfd..78cc5768 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -39,6 +39,7 @@ try: except ImportError: from ipapython.ipautil import kinit_password, kinit_keytab from ipapython.ipautil import run +from ipapython.dn import DN from ipaplatform.paths import paths from ipalib.krb_utils import get_credentials_if_valid from ansible.module_utils.basic import AnsibleModule @@ -344,6 +345,16 @@ def load_cert_from_str(cert): return cert +def DN_x500_text(text): + if hasattr(DN, "x500_text"): + return DN(text).x500_text() + else: + # Emulate x500_text + dn = DN(text) + dn.rdns = reversed(dn.rdns) + return str(dn) + + def is_valid_port(port): if not isinstance(port, int): return False From fdcdad2c7e68c58e8e7d5a9e38da5e9e4be301ad Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Wed, 6 May 2020 16:37:18 +0200 Subject: [PATCH 3/4] ansible_freeipa_module: New function api_check_command This function can be used to check if a command is available in the API. This is used in ipauser module to check if user_add_certmapdata is available in the API. --- plugins/module_utils/ansible_freeipa_module.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index 78cc5768..122ea2e2 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -175,6 +175,11 @@ def api_command_no_name(module, command, args): return api.Command[command](**args) +def api_check_command(command): + """Return if command exists in command list.""" + return command in api.Command + + def api_check_param(command, name): """Check if param exists in command param list.""" return name in api.Command[command].params From ac61f597d5e0810b730710c8588544c923934257 Mon Sep 17 00:00:00 2001 From: Thomas Woerner Date: Wed, 6 May 2020 16:14:07 +0200 Subject: [PATCH 4/4] ipauser: Fix certmapdata, add missing certmapdata data option certmapdata was not processed properly. The certificate was not loaded and therefore the `issuer` and `subject` could not be compared to the certmapdata entries in the user record. The function `load_cert_from_str` from ansible_freeipa_moduleis used for this. Additionally there was no way to use the certmapdata data format. This is now possible with the `data` option in the certmapdata dict. Example: "data: X509:dc=com,dc=example,CN=cadc=com,dc=example,CN=test" `data` may not be used together with `certificate`, `issuer` and `subject` in the same record. Given certmapdata for the ipauser module is now converted to the internal data representation using also the new function `DN_x500_text` from `ansible_freeipa_module`. New functions `convert_certmapdata` and `check_certmapdata` have been added to ipauser. tests/user/certmapdata/test_user_certmapdata.yml has been extended with additional tasks to verify more complex issuer and subjects and also using the data format. --- README-user.md | 9 +- plugins/modules/ipauser.py | 99 +++++++++++++++---- .../certmapdata/test_user_certmapdata.yml | 83 +++++++++++++++- 3 files changed, 164 insertions(+), 27 deletions(-) diff --git a/README-user.md b/README-user.md index 6958ebe5..05872d97 100644 --- a/README-user.md +++ b/README-user.md @@ -417,10 +417,11 @@ Variable | Description | Required `employeetype` | Employee Type | no `preferredlanguage` | Preferred Language | no `certificate` | List of base-64 encoded user certificates. | no -`certmapdata` | List of certificate mappings. Either `certificate` or `issuer` together with `subject` need to be specified.
Options: | no -  | `certificate` - Base-64 encoded user certificate | no -  | `issuer` - Issuer of the certificate | no -  | `subject` - Subject of the certificate | no +`certmapdata` | List of certificate mappings. Either `data` or `certificate` or `issuer` together with `subject` need to be specified. Only usable with IPA versions 4.5 and up.
Options: | no +  | `certificate` - Base-64 encoded user certificate, not usable with other certmapdata options. | no +  | `issuer` - Issuer of the certificate, only usable together with `usbject` option. | no +  | `subject` - Subject of the certificate, only usable together with `issuer` option. | no +  | `data` - Certmap data, not usable with other certmapdata options. | no `noprivate` | Do not create user private group. (bool) | no `nomembers` | Suppress processing of membership attributes. (bool) | no diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py index 791a0d4d..6c48a2ff 100644 --- a/plugins/modules/ipauser.py +++ b/plugins/modules/ipauser.py @@ -186,7 +186,9 @@ options: description: List of base-64 encoded user certificates required: false certmapdata: - description: List of certificate mappings + description: + - List of certificate mappings + - Only usable with IPA versions 4.5 and up. options: certificate: description: Base-64 encoded user certificate @@ -197,6 +199,9 @@ options: subject: description: Subject of the certificate required: false + data: + description: Certmap data + required: false required: false noprivate: description: Don't create user private group @@ -346,7 +351,9 @@ options: description: List of base-64 encoded user certificates required: false certmapdata: - description: List of certificate mappings + description: + - List of certificate mappings + - Only usable with IPA versions 4.5 and up. options: certificate: description: Base-64 encoded user certificate @@ -357,6 +364,9 @@ options: subject: description: Subject of the certificate required: false + data: + description: Certmap data + required: false required: false noprivate: description: Don't create user private group @@ -467,7 +477,8 @@ from ansible.module_utils._text import to_text from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ temp_kdestroy, valid_creds, api_connect, api_command, date_format, \ compare_args_ipa, module_params_get, api_check_param, api_get_realm, \ - api_command_no_name, gen_add_del_lists, encode_certificate + api_command_no_name, gen_add_del_lists, encode_certificate, \ + load_cert_from_str, DN_x500_text, api_check_command import six @@ -645,13 +656,21 @@ def check_parameters(module, state, action, certificate = x.get("certificate") issuer = x.get("issuer") subject = x.get("subject") + data = x.get("data") + if data is not None: + if certificate is not None or issuer is not None or \ + subject is not None: + module.fail_json( + msg="certmapdata: data can not be used with " + "certificate, issuer or subject") + check_certmapdata(data) if certificate is not None \ and (issuer is not None or subject is not None): module.fail_json( msg="certmapdata: certificate can not be used with " "issuer or subject") - if certificate is None: + if data is None and certificate is None: if issuer is None: module.fail_json(msg="certmapdata: issuer is missing") if subject is None: @@ -666,19 +685,48 @@ def extend_emails(email, default_email_domain): return email -def gen_certmapdata_args(certmapdata): - certificate = certmapdata.get("certificate") - issuer = certmapdata.get("issuer") - subject = certmapdata.get("subject") +def convert_certmapdata(certmapdata): + if certmapdata is None: + return None - _args = {} - if certificate is not None: - _args["certificate"] = certificate - if issuer is not None: - _args["issuer"] = issuer - if subject is not None: - _args["subject"] = subject - return _args + _result = [] + for x in certmapdata: + certificate = x.get("certificate") + issuer = x.get("issuer") + subject = x.get("subject") + data = x.get("data") + + if data is None: + if issuer is None and subject is None: + cert = load_cert_from_str(certificate) + issuer = cert.issuer + subject = cert.subject + + _result.append("X509:%s%s" % (DN_x500_text(issuer), + DN_x500_text(subject))) + else: + _result.append(data) + + return _result + + +def check_certmapdata(data): + if not data.startswith("X509:"): + return False + + i = data.find("", 4) + s = data.find("", i) + issuer = data[i+3:s] + subject = data[s+3:] + + if i < 0 or s < 0 or "CN" not in issuer or "CN" not in subject: + return False + + return True + + +def gen_certmapdata_args(certmapdata): + return {"ipacertmapdata": to_text(certmapdata)} def main(): @@ -740,7 +788,8 @@ def main(): # Here certificate is a simple string certificate=dict(type="str", default=None), issuer=dict(type="str", default=None), - subject=dict(type="str", default=None) + subject=dict(type="str", default=None), + data=dict(type="str", default=None) ), elements='dict', required=False), noprivate=dict(type='bool', default=None), @@ -875,6 +924,7 @@ def main(): departmentnumber, employeenumber, employeetype, preferredlanguage, certificate, certmapdata, noprivate, nomembers, preserve, update_password) + certmapdata = convert_certmapdata(certmapdata) # Use users if names is None if users is not None: @@ -972,6 +1022,7 @@ def main(): employeetype, preferredlanguage, certificate, certmapdata, noprivate, nomembers, preserve, update_password) + certmapdata = convert_certmapdata(certmapdata) # Extend email addresses @@ -1001,6 +1052,16 @@ def main(): msg="The use of passwordexpiration is not supported by " "your IPA version") + # Check certmapdata availability. + # We need the connected API for this test, therefore it can not + # be part of check_parameters as this is used also before the + # connection to the API has been established. + if certmapdata is not None and \ + not api_check_command("user_add_certmapdata"): + ansible_module.fail_json( + msg="The use of certmapdata is not supported by " + "your IPA version") + # Make sure user exists res_find = find_user(ansible_module, name) # Also search for preserved user if the user could not be found @@ -1082,7 +1143,7 @@ def main(): certificate, res_find.get("usercertificate")) certmapdata_add, certmapdata_del = gen_add_del_lists( - certmapdata, res_find.get("ipaCertMapData")) + certmapdata, res_find.get("ipacertmapdata")) else: # Use given managers and principals @@ -1169,7 +1230,7 @@ def main(): # Remove certmapdata if len(certmapdata_del) > 0: for _data in certmapdata_del: - commands.append([name, "user_add_certmapdata", + commands.append([name, "user_remove_certmapdata", gen_certmapdata_args(_data)]) elif action == "member": diff --git a/tests/user/certmapdata/test_user_certmapdata.yml b/tests/user/certmapdata/test_user_certmapdata.yml index cf5576ec..85569e0d 100644 --- a/tests/user/certmapdata/test_user_certmapdata.yml +++ b/tests/user/certmapdata/test_user_certmapdata.yml @@ -126,8 +126,6 @@ certmapdata: - issuer: CN=issuer1 subject: CN=subject1 - - issuer: CN=issuer2 - subject: CN=subject2 - issuer: CN=issuer3 subject: CN=subject3 action: member @@ -142,8 +140,6 @@ certmapdata: - issuer: CN=issuer1 subject: CN=subject1 - - issuer: CN=issuer2 - subject: CN=subject2 - issuer: CN=issuer3 subject: CN=subject3 action: member @@ -151,6 +147,85 @@ register: result failed_when: result.changed + - name: User test certmapdata members absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=issuer2 + subject: CN=subject2 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User test certmapdata members absent again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=issuer2 + subject: CN=subject2 + action: member + state: absent + register: result + failed_when: result.changed + + - name: User test certmapdata member present + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=ca,dc=example,dc=com + subject: CN=test,dc=example,dc=com + action: member + register: result + failed_when: not result.changed + + - name: User test certmapdata member present again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=ca,dc=example,dc=com + subject: CN=test,dc=example,dc=com + action: member + register: result + failed_when: result.changed + + - name: User test certmapdata member (data) present again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - data: X509:dc=com,dc=example,CN=cadc=com,dc=example,CN=test + action: member + register: result + failed_when: result.changed + + - name: User test certmapdata member absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=ca,dc=example,dc=com + subject: CN=test,dc=example,dc=com + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User test certmapdata member (data) absent again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - data: X509:dc=com,dc=example,CN=cadc=com,dc=example,CN=test + action: member + state: absent + register: result + failed_when: result.changed + - name: User test absent ipauser: ipaadmin_password: SomeADMINpassword