PageRenderTime 64ms CodeModel.GetById 38ms RepoModel.GetById 0ms app.codeStats 0ms

/collections/ansible_collections/community/general/plugins/modules/identity/ipa/ipa_user.py

https://gitlab.com/cfernand/acm-aap-demo
Python | 397 lines | 379 code | 7 blank | 11 comment | 8 complexity | ff8cfe9850a24447149274c354654a59 MD5 | raw file
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. # Copyright: (c) 2017, Ansible Project
  4. # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
  5. from __future__ import absolute_import, division, print_function
  6. __metaclass__ = type
  7. DOCUMENTATION = r'''
  8. ---
  9. module: ipa_user
  10. author: Thomas Krahn (@Nosmoht)
  11. short_description: Manage FreeIPA users
  12. description:
  13. - Add, modify and delete user within IPA server.
  14. options:
  15. displayname:
  16. description: Display name.
  17. type: str
  18. update_password:
  19. description:
  20. - Set password for a user.
  21. type: str
  22. default: 'always'
  23. choices: [ always, on_create ]
  24. givenname:
  25. description: First name.
  26. type: str
  27. krbpasswordexpiration:
  28. description:
  29. - Date at which the user password will expire.
  30. - In the format YYYYMMddHHmmss.
  31. - e.g. 20180121182022 will expire on 21 January 2018 at 18:20:22.
  32. type: str
  33. loginshell:
  34. description: Login shell.
  35. type: str
  36. mail:
  37. description:
  38. - List of mail addresses assigned to the user.
  39. - If an empty list is passed all assigned email addresses will be deleted.
  40. - If None is passed email addresses will not be checked or changed.
  41. type: list
  42. elements: str
  43. password:
  44. description:
  45. - Password for a user.
  46. - Will not be set for an existing user unless I(update_password=always), which is the default.
  47. type: str
  48. sn:
  49. description: Surname.
  50. type: str
  51. sshpubkey:
  52. description:
  53. - List of public SSH key.
  54. - If an empty list is passed all assigned public keys will be deleted.
  55. - If None is passed SSH public keys will not be checked or changed.
  56. type: list
  57. elements: str
  58. state:
  59. description: State to ensure.
  60. default: "present"
  61. choices: ["absent", "disabled", "enabled", "present"]
  62. type: str
  63. telephonenumber:
  64. description:
  65. - List of telephone numbers assigned to the user.
  66. - If an empty list is passed all assigned telephone numbers will be deleted.
  67. - If None is passed telephone numbers will not be checked or changed.
  68. type: list
  69. elements: str
  70. title:
  71. description: Title.
  72. type: str
  73. uid:
  74. description: uid of the user.
  75. required: true
  76. aliases: ["name"]
  77. type: str
  78. uidnumber:
  79. description:
  80. - Account Settings UID/Posix User ID number.
  81. type: str
  82. gidnumber:
  83. description:
  84. - Posix Group ID.
  85. type: str
  86. homedirectory:
  87. description:
  88. - Default home directory of the user.
  89. type: str
  90. version_added: '0.2.0'
  91. userauthtype:
  92. description:
  93. - The authentication type to use for the user.
  94. choices: ["password", "radius", "otp", "pkinit", "hardened"]
  95. type: list
  96. elements: str
  97. version_added: '1.2.0'
  98. extends_documentation_fragment:
  99. - community.general.ipa.documentation
  100. requirements:
  101. - base64
  102. - hashlib
  103. '''
  104. EXAMPLES = r'''
  105. - name: Ensure pinky is present and always reset password
  106. community.general.ipa_user:
  107. name: pinky
  108. state: present
  109. krbpasswordexpiration: 20200119235959
  110. givenname: Pinky
  111. sn: Acme
  112. mail:
  113. - pinky@acme.com
  114. telephonenumber:
  115. - '+555123456'
  116. sshpubkey:
  117. - ssh-rsa ....
  118. - ssh-dsa ....
  119. uidnumber: '1001'
  120. gidnumber: '100'
  121. homedirectory: /home/pinky
  122. ipa_host: ipa.example.com
  123. ipa_user: admin
  124. ipa_pass: topsecret
  125. - name: Ensure brain is absent
  126. community.general.ipa_user:
  127. name: brain
  128. state: absent
  129. ipa_host: ipa.example.com
  130. ipa_user: admin
  131. ipa_pass: topsecret
  132. - name: Ensure pinky is present but don't reset password if already exists
  133. community.general.ipa_user:
  134. name: pinky
  135. state: present
  136. givenname: Pinky
  137. sn: Acme
  138. password: zounds
  139. ipa_host: ipa.example.com
  140. ipa_user: admin
  141. ipa_pass: topsecret
  142. update_password: on_create
  143. - name: Ensure pinky is present and using one time password and RADIUS authentication
  144. community.general.ipa_user:
  145. name: pinky
  146. state: present
  147. userauthtype:
  148. - otp
  149. - radius
  150. ipa_host: ipa.example.com
  151. ipa_user: admin
  152. ipa_pass: topsecret
  153. '''
  154. RETURN = r'''
  155. user:
  156. description: User as returned by IPA API
  157. returned: always
  158. type: dict
  159. '''
  160. import base64
  161. import hashlib
  162. import traceback
  163. from ansible.module_utils.basic import AnsibleModule
  164. from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
  165. from ansible.module_utils.common.text.converters import to_native
  166. class UserIPAClient(IPAClient):
  167. def __init__(self, module, host, port, protocol):
  168. super(UserIPAClient, self).__init__(module, host, port, protocol)
  169. def user_find(self, name):
  170. return self._post_json(method='user_find', name=None, item={'all': True, 'uid': name})
  171. def user_add(self, name, item):
  172. return self._post_json(method='user_add', name=name, item=item)
  173. def user_mod(self, name, item):
  174. return self._post_json(method='user_mod', name=name, item=item)
  175. def user_del(self, name):
  176. return self._post_json(method='user_del', name=name)
  177. def user_disable(self, name):
  178. return self._post_json(method='user_disable', name=name)
  179. def user_enable(self, name):
  180. return self._post_json(method='user_enable', name=name)
  181. def get_user_dict(displayname=None, givenname=None, krbpasswordexpiration=None, loginshell=None,
  182. mail=None, nsaccountlock=False, sn=None, sshpubkey=None, telephonenumber=None,
  183. title=None, userpassword=None, gidnumber=None, uidnumber=None, homedirectory=None,
  184. userauthtype=None):
  185. user = {}
  186. if displayname is not None:
  187. user['displayname'] = displayname
  188. if krbpasswordexpiration is not None:
  189. user['krbpasswordexpiration'] = krbpasswordexpiration + "Z"
  190. if givenname is not None:
  191. user['givenname'] = givenname
  192. if loginshell is not None:
  193. user['loginshell'] = loginshell
  194. if mail is not None:
  195. user['mail'] = mail
  196. user['nsaccountlock'] = nsaccountlock
  197. if sn is not None:
  198. user['sn'] = sn
  199. if sshpubkey is not None:
  200. user['ipasshpubkey'] = sshpubkey
  201. if telephonenumber is not None:
  202. user['telephonenumber'] = telephonenumber
  203. if title is not None:
  204. user['title'] = title
  205. if userpassword is not None:
  206. user['userpassword'] = userpassword
  207. if gidnumber is not None:
  208. user['gidnumber'] = gidnumber
  209. if uidnumber is not None:
  210. user['uidnumber'] = uidnumber
  211. if homedirectory is not None:
  212. user['homedirectory'] = homedirectory
  213. if userauthtype is not None:
  214. user['ipauserauthtype'] = userauthtype
  215. return user
  216. def get_user_diff(client, ipa_user, module_user):
  217. """
  218. Return the keys of each dict whereas values are different. Unfortunately the IPA
  219. API returns everything as a list even if only a single value is possible.
  220. Therefore some more complexity is needed.
  221. The method will check if the value type of module_user.attr is not a list and
  222. create a list with that element if the same attribute in ipa_user is list. In this way I hope that the method
  223. must not be changed if the returned API dict is changed.
  224. :param ipa_user:
  225. :param module_user:
  226. :return:
  227. """
  228. # sshpubkeyfp is the list of ssh key fingerprints. IPA doesn't return the keys itself but instead the fingerprints.
  229. # These are used for comparison.
  230. sshpubkey = None
  231. if 'ipasshpubkey' in module_user:
  232. hash_algo = 'md5'
  233. if 'sshpubkeyfp' in ipa_user and ipa_user['sshpubkeyfp'][0][:7].upper() == 'SHA256:':
  234. hash_algo = 'sha256'
  235. module_user['sshpubkeyfp'] = [get_ssh_key_fingerprint(pubkey, hash_algo) for pubkey in module_user['ipasshpubkey']]
  236. # Remove the ipasshpubkey element as it is not returned from IPA but save it's value to be used later on
  237. sshpubkey = module_user['ipasshpubkey']
  238. del module_user['ipasshpubkey']
  239. result = client.get_diff(ipa_data=ipa_user, module_data=module_user)
  240. # If there are public keys, remove the fingerprints and add them back to the dict
  241. if sshpubkey is not None:
  242. del module_user['sshpubkeyfp']
  243. module_user['ipasshpubkey'] = sshpubkey
  244. return result
  245. def get_ssh_key_fingerprint(ssh_key, hash_algo='sha256'):
  246. """
  247. Return the public key fingerprint of a given public SSH key
  248. in format "[fp] [comment] (ssh-rsa)" where fp is of the format:
  249. FB:0C:AC:0A:07:94:5B:CE:75:6E:63:32:13:AD:AD:D7
  250. for md5 or
  251. SHA256:[base64]
  252. for sha256
  253. Comments are assumed to be all characters past the second
  254. whitespace character in the sshpubkey string.
  255. :param ssh_key:
  256. :param hash_algo:
  257. :return:
  258. """
  259. parts = ssh_key.strip().split(None, 2)
  260. if len(parts) == 0:
  261. return None
  262. key_type = parts[0]
  263. key = base64.b64decode(parts[1].encode('ascii'))
  264. if hash_algo == 'md5':
  265. fp_plain = hashlib.md5(key).hexdigest()
  266. key_fp = ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2])).upper()
  267. elif hash_algo == 'sha256':
  268. fp_plain = base64.b64encode(hashlib.sha256(key).digest()).decode('ascii').rstrip('=')
  269. key_fp = 'SHA256:{fp}'.format(fp=fp_plain)
  270. if len(parts) < 3:
  271. return "%s (%s)" % (key_fp, key_type)
  272. else:
  273. comment = parts[2]
  274. return "%s %s (%s)" % (key_fp, comment, key_type)
  275. def ensure(module, client):
  276. state = module.params['state']
  277. name = module.params['uid']
  278. nsaccountlock = state == 'disabled'
  279. module_user = get_user_dict(displayname=module.params.get('displayname'),
  280. krbpasswordexpiration=module.params.get('krbpasswordexpiration'),
  281. givenname=module.params.get('givenname'),
  282. loginshell=module.params['loginshell'],
  283. mail=module.params['mail'], sn=module.params['sn'],
  284. sshpubkey=module.params['sshpubkey'], nsaccountlock=nsaccountlock,
  285. telephonenumber=module.params['telephonenumber'], title=module.params['title'],
  286. userpassword=module.params['password'],
  287. gidnumber=module.params.get('gidnumber'), uidnumber=module.params.get('uidnumber'),
  288. homedirectory=module.params.get('homedirectory'),
  289. userauthtype=module.params.get('userauthtype'))
  290. update_password = module.params.get('update_password')
  291. ipa_user = client.user_find(name=name)
  292. changed = False
  293. if state in ['present', 'enabled', 'disabled']:
  294. if not ipa_user:
  295. changed = True
  296. if not module.check_mode:
  297. ipa_user = client.user_add(name=name, item=module_user)
  298. else:
  299. if update_password == 'on_create':
  300. module_user.pop('userpassword', None)
  301. diff = get_user_diff(client, ipa_user, module_user)
  302. if len(diff) > 0:
  303. changed = True
  304. if not module.check_mode:
  305. ipa_user = client.user_mod(name=name, item=module_user)
  306. else:
  307. if ipa_user:
  308. changed = True
  309. if not module.check_mode:
  310. client.user_del(name)
  311. return changed, ipa_user
  312. def main():
  313. argument_spec = ipa_argument_spec()
  314. argument_spec.update(displayname=dict(type='str'),
  315. givenname=dict(type='str'),
  316. update_password=dict(type='str', default="always",
  317. choices=['always', 'on_create'],
  318. no_log=False),
  319. krbpasswordexpiration=dict(type='str', no_log=False),
  320. loginshell=dict(type='str'),
  321. mail=dict(type='list', elements='str'),
  322. sn=dict(type='str'),
  323. uid=dict(type='str', required=True, aliases=['name']),
  324. gidnumber=dict(type='str'),
  325. uidnumber=dict(type='str'),
  326. password=dict(type='str', no_log=True),
  327. sshpubkey=dict(type='list', elements='str'),
  328. state=dict(type='str', default='present',
  329. choices=['present', 'absent', 'enabled', 'disabled']),
  330. telephonenumber=dict(type='list', elements='str'),
  331. title=dict(type='str'),
  332. homedirectory=dict(type='str'),
  333. userauthtype=dict(type='list', elements='str',
  334. choices=['password', 'radius', 'otp', 'pkinit', 'hardened']))
  335. module = AnsibleModule(argument_spec=argument_spec,
  336. supports_check_mode=True)
  337. client = UserIPAClient(module=module,
  338. host=module.params['ipa_host'],
  339. port=module.params['ipa_port'],
  340. protocol=module.params['ipa_prot'])
  341. # If sshpubkey is defined as None than module.params['sshpubkey'] is [None]. IPA itself returns None (not a list).
  342. # Therefore a small check here to replace list(None) by None. Otherwise get_user_diff() would return sshpubkey
  343. # as different which should be avoided.
  344. if module.params['sshpubkey'] is not None:
  345. if len(module.params['sshpubkey']) == 1 and module.params['sshpubkey'][0] == "":
  346. module.params['sshpubkey'] = None
  347. try:
  348. client.login(username=module.params['ipa_user'],
  349. password=module.params['ipa_pass'])
  350. changed, user = ensure(module, client)
  351. module.exit_json(changed=changed, user=user)
  352. except Exception as e:
  353. module.fail_json(msg=to_native(e), exception=traceback.format_exc())
  354. if __name__ == '__main__':
  355. main()