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/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py
index af45a6cb..122ea2e2 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
@@ -48,6 +49,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
@@ -167,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
@@ -323,6 +336,30 @@ 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 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
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