Skip to content

Commit

Permalink
[DRAFT]: Add support for state:query to user module
Browse files Browse the repository at this point in the history
  • Loading branch information
t-woerner committed Aug 25, 2022
1 parent e05dc41 commit 1246318
Show file tree
Hide file tree
Showing 3 changed files with 388 additions and 9 deletions.
79 changes: 79 additions & 0 deletions plugins/module_utils/ansible_freeipa_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,85 @@ def exception_handler(module, ex, exit_args, one_name):

return changed

def execute_query(self, names, prefix, name_ipa_param,
query_param, query_command, query_param_settings):
"""
Execute query state.
Parameters
----------
names: The main items to return
It names is not None and not an empty list then all items
found with "item_find" are returned, else the items in names.
prefix: The prefix for use with several main items
The prefix is "users" for the "user" module. It is used
if only the list of main items (example: users) is returned.
name_ipa_param: The IPA param name of the name parameter
This is for example "uid" that is used for the user name in
the user module.
query_param: The parameters to return
The parameters that should be returned. If query_param is
["ALL"], all parameters in ipa_pram_names will be returned.
query_param_settings: IPA base parameters, all and mapping
The dict provides all parameters the "ALL" list and the
mapping of the default module paramter name to IPA option name
if it is not the same.
Example: "uid" for user name of the user commands.
query_command: The Query function
This is a module function that returns the structure(s) from
the show or find command.
"""

def store_params(exit_args, name, prefix, name_ipa_param, result,
params):
if params is None:
exit_args.setdefault(prefix, []).append(
result[name_ipa_param])
return
for field in params:
if field not in query_param_settings["ALL"]:
self.fail_json(
msg="query_param '%s' is not supported" % field)
if "mapping" in query_param_settings and \
field in query_param_settings["mapping"]:
ipa_field = query_param_settings["mapping"][field]
else:
ipa_field = field

if ipa_field in result:
value = result[ipa_field]
if name is None:
exit_args[field] = value
else:
exit_args.setdefault(name, {})[field] = value

# Create exit_args
exit_args = {}

if query_param == ["BASE"]:
query_param = query_param_settings["BASE"]
elif query_param == ["ALL"]:
query_param = query_param_settings["ALL"]

if names and isinstance(names, list):
with_name = len(names) > 1
for name in names:
result = query_command(self, name)
if result:
store_params(exit_args, name if with_name else None,
prefix, name_ipa_param, result,
query_param)
else:
results = query_command(self, None)
if results is not None:
for result in results:
name = result[name_ipa_param]
store_params(exit_args, name, prefix, name_ipa_param,
result, query_param)

return exit_args

class FreeIPABaseModule(IPAAnsibleModule):
"""
Base class for FreeIPA Ansible modules.
Expand Down
145 changes: 136 additions & 9 deletions plugins/modules/ipauser.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@
default: "always"
choices: ["always", "on_create"]
required: false
query_param:
description: The fields to query with state=query
required: false
action:
description: Work on user or member level
default: "user"
Expand All @@ -393,7 +396,8 @@
default: present
choices: ["present", "absent",
"enabled", "disabled",
"unlocked", "undeleted"]
"unlocked", "undeleted",
"query"]
author:
- Thomas Woerner
"""
Expand Down Expand Up @@ -481,7 +485,7 @@
unicode = str


def find_user(module, name):
def user_show(module, name):
_args = {
"all": True,
}
Expand All @@ -501,6 +505,49 @@ def find_user(module, name):
return _result


def convert_result(res):
_res = {}
for key in res:
if key in ["manager", "krbprincipalname", "ipacertmapdata"]:
_res[key] = [to_text(x) for x in (res.get(key) or [])]
elif key == "usercertificate":
_res[key] = [encode_certificate(x) for x in (res.get(key) or [])]
elif isinstance(res[key], list) and len(res[key]) == 1:
# All single value parameters should not be lists
# This does not apply to manager, krbprincipalname,
# usercertificate and ipacertmapdata
_res[key] = to_text(res[key][0])
elif key in ["uidNumber", "gidNumber"]:
_res[key] = int(res[key])
else:
_res[key] = to_text(res[key])
return _res


def user_find(module, name, sizelimit=None, timelimit=None):
_args = {"all": True}

if sizelimit is not None:
_args["sizelimit"] = sizelimit
if timelimit is not None:
_args["timelimit"] = timelimit

try:
if name:
_args["uid"] = name
_result = module.ipa_command_no_name("user_find", _args).get("result")
if _result:
if name:
_result = convert_result(_result[0])
else:
_result = [convert_result(res) for res in _result]

except ipalib_errors.NotFound:
return None
else:
return _result


def gen_args(first, last, fullname, displayname, initials, homedir, shell,
email, principalexpiration, passwordexpiration, password,
random, uid, gid, city, userstate, postalcode, phone, mobile,
Expand Down Expand Up @@ -618,6 +665,15 @@ def check_parameters( # pylint: disable=unused-argument
"certificate", "certmapdata",
])

if state == "query":
invalid.append("users")

if action == "member":
module.fail_json(
msg="Query is not possible with action=member")
else:
invalid.append("query_param")

if state != "absent" and preserve is not None:
module.fail_json(
msg="Preserve is only possible for state=absent")
Expand Down Expand Up @@ -742,6 +798,59 @@ def exception_handler(module, ex, errors, exit_args, one_name):
return False


query_param_settings = {
# password, randompassword and krbprincipalkey may not be in the returned
# information even in server context.
"ALL": [
"objectclass", "ipauniqueid", "login", "first", "last", "fullname",
"displayname", "initials", "homedir", "shell", "email",
"principalexpiration", "passwordexpiration", "uid", "gid", "city",
"userstate", "postalcode", "phone", "mobile", "pager", "fax",
"orgunit", "title", "carlicense", "sshpubkey", "userauthtype",
"userclass", "radius", "radiususer", "departmentnumber",
"employeenumber", "employeetype", "preferredlanguage", "manager",
"principal", "certificate", "certmapdata", "gecos", "krblastpwdchange",
"krblastadminunlock", "krbextradata", "krbticketflags",
"krbloginfailedcount", "krblastsuccessfulauth", "has_password",
"has_keytab", "preserved", "memberof_group", "disabled"
],
"BASE": [
"login", "first", "last", "shell", "principal", "uid", "gid",
"disabled"
],
"mapping": {
"login": "uid",
"first": "givenname",
"last": "sn",
"fullname": "cn",
"homedir": "homedirectory",
"shell": "loginshell",
"email": "mail",
"principalexpiration": "krbprincipalexpiration",
"passwordexpiration": "krbpasswordexpiration",
"uid": "uidnumber",
"gid": "gidnumber",
"city": "l",
"userstate": "st",
"postalcode": "postalcode",
"phone": "telephonenumber",
"mobile": "mobile",
"pager": "pager",
"fax": "facsimiletelephonenumber",
"orgunit": "ou",
"sshpubkey": "ipasshpubkey",
"userauthtype": "ipauserauthtype",
"radius": "ipatokenradiusconfiglink",
"radiususer": "ipatokenradiususername",
"preferredlanguage": "preferredlanguage",
"principal": "krbprincipalname",
"certificate": "usercertificate",
"certmapdata": "ipacertmapdata",
"disabled": "nsaccountock"
}
}


def main():
user_spec = dict(
# present
Expand Down Expand Up @@ -833,18 +942,25 @@ def main():
update_password=dict(type='str', default=None, no_log=False,
choices=['always', 'on_create']),

# query
query_param=dict(type="list", default=None,
choices=["ALL"].extend(
query_param_settings["ALL"]),
required=False),

# general
action=dict(type="str", default="user",
choices=["member", "user"]),
state=dict(type="str", default="present",
choices=["present", "absent", "enabled", "disabled",
"unlocked", "undeleted"]),
"unlocked", "undeleted", "query"]),

# Add user specific parameters for simple use case
**user_spec
),
mutually_exclusive=[["name", "users"]],
required_one_of=[["name", "users"]],
# Required one of [["name", "users"]] has been removed as there is
# an extra test below and it is not working with state=query
supports_check_mode=True,
)

Expand Down Expand Up @@ -911,15 +1027,19 @@ def main():
preserve = ansible_module.params_get("preserve")
# mod
update_password = ansible_module.params_get("update_password")
# query
query_param = ansible_module.params_get("query_param")

# general
action = ansible_module.params_get("action")
state = ansible_module.params_get("state")

# Check parameters

if (names is None or len(names) < 1) and \
(users is None or len(users) < 1):
ansible_module.fail_json(msg="One of name and users is required")
if state != "query":
if (names is None or len(names) < 1) and \
(users is None or len(users) < 1):
ansible_module.fail_json(msg="One of name and users is required")

if state == "present":
if names is not None and len(names) != 1:
Expand Down Expand Up @@ -949,6 +1069,13 @@ def main():
# Connect to IPA API
with ansible_module.ipa_connect():

if state == "query":
exit_args = ansible_module.execute_query(
names, "users", "uid", query_param, user_find,
query_param_settings)

ansible_module.exit_json(changed=False, user=exit_args)

# Check version specific settings

server_realm = ansible_module.ipa_get_realm()
Expand Down Expand Up @@ -1076,7 +1203,7 @@ def main():
"your IPA version")

# Make sure user exists
res_find = find_user(ansible_module, name)
res_find = user_show(ansible_module, name)

# Create command
if state == "present":
Expand Down Expand Up @@ -1136,7 +1263,7 @@ def main():
principal_add, principal_del = gen_add_del_lists(
principal, res_find.get("krbprincipalname"))
# Principals are not returned as utf8 for IPA using
# python2 using user_find, therefore we need to
# python2 using user_show, therefore we need to
# convert the principals that we should remove.
principal_del = [to_text(x) for x in principal_del]

Expand Down
Loading

0 comments on commit 1246318

Please sign in to comment.