#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2024 Alexander Bakanovskii # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = r''' --- module: krb_ticket short_description: Kerberos utils for managing tickets version_added: 10.0.0 description: - Manage Kerberos tickets with C(kinit), C(klist) and C(kdestroy) base utilities. - See U(https://web.mit.edu/kerberos/krb5-1.12/doc/user/user_commands/index.html) for reference. author: "Alexander Bakanovskii (@abakanovskii)" attributes: check_mode: support: full diff_mode: support: none options: password: description: - Principal password. - It is required to specify O(password) or O(keytab_path). type: str principal: description: - The principal name. - If not set, the user running this module will be used. type: str state: description: - The state of the Kerberos ticket. - V(present) is equivalent of C(kinit) command. - V(absent) is equivalent of C(kdestroy) command. type: str default: present choices: ["present", "absent"] kdestroy_all: description: - When O(state=absent) destroys all credential caches in collection. - Equivalent of running C(kdestroy -A). type: bool cache_name: description: - Use O(cache_name) as the ticket cache name and location. - If this option is not used, the default cache name and location are used. - The default credentials cache may vary between systems. - If not set the the value of E(KRB5CCNAME) environment variable will be used instead, its value is used to name the default ticket cache. type: str lifetime: description: - Requests a ticket with the lifetime, if the O(lifetime) is not specified, the default ticket lifetime is used. - Specifying a ticket lifetime longer than the maximum ticket lifetime (configured by each site) will not override the configured maximum ticket lifetime. - "The value for O(lifetime) must be followed by one of the following suffixes: V(s) - seconds, V(m) - minutes, V(h) - hours, V(d) - days." - You cannot mix units; a value of V(3h30m) will result in an error. - See U(https://web.mit.edu/kerberos/krb5-1.12/doc/basic/date_format.html) for reference. type: str start_time: description: - Requests a postdated ticket. - Postdated tickets are issued with the invalid flag set, and need to be resubmitted to the KDC for validation before use. - O(start_time) specifies the duration of the delay before the ticket can become valid. - You can use absolute time formats, for example V(July 27, 2012 at 20:30) you would neet to set O(start_time=20120727203000). - You can also use time duration format similar to O(lifetime) or O(renewable). - See U(https://web.mit.edu/kerberos/krb5-1.12/doc/basic/date_format.html) for reference. type: str renewable: description: - Requests renewable tickets, with a total lifetime equal to O(renewable). - "The value for O(renewable) must be followed by one of the following delimiters: V(s) - seconds, V(m) - minutes, V(h) - hours, V(d) - days." - You cannot mix units; a value of V(3h30m) will result in an error. - See U(https://web.mit.edu/kerberos/krb5-1.12/doc/basic/date_format.html) for reference. type: str forwardable: description: - Request forwardable or non-forwardable tickets. type: bool proxiable: description: - Request proxiable or non-proxiable tickets. type: bool address_restricted: description: - Request tickets restricted to the host's local address or non-restricted. type: bool anonymous: description: - Requests anonymous processing. type: bool canonicalization: description: - Requests canonicalization of the principal name, and allows the KDC to reply with a different client principal from the one requested. type: bool enterprise: description: - Treats the principal name as an enterprise name (implies the O(canonicalization) option). type: bool renewal: description: - Requests renewal of the ticket-granting ticket. - Note that an expired ticket cannot be renewed, even if the ticket is still within its renewable life. type: bool validate: description: - Requests that the ticket-granting ticket in the cache (with the invalid flag set) be passed to the KDC for validation. - If the ticket is within its requested time range, the cache is replaced with the validated ticket. type: bool keytab: description: - Requests a ticket, obtained from a key in the local host's keytab. - If O(keytab_path) is not specified will try to use default client keytab path (C(-i) option). type: bool keytab_path: description: - Use when O(keytab=true) to specify path to a keytab file. - It is required to specify O(password) or O(keytab_path). type: path requirements: - krb5-user and krb5-config packages extends_documentation_fragment: - community.general.attributes ''' EXAMPLES = r''' - name: Get Kerberos ticket using default principal community.general.krb_ticket: password: some_password - name: Get Kerberos ticket using keytab community.general.krb_ticket: keytab: true keytab_path: /etc/ipa/file.keytab - name: Get Kerberos ticket with a lifetime of 7 days community.general.krb_ticket: password: some_password lifetime: 7d - name: Get Kerberos ticket with a starting time of July 2, 2024, 1:35:30 p.m. community.general.krb_ticket: password: some_password start_time: "240702133530" - name: Get Kerberos ticket using principal name community.general.krb_ticket: password: some_password principal: admin - name: Get Kerberos ticket using principal with realm community.general.krb_ticket: password: some_password principal: admin@IPA.TEST - name: Check for existence by ticket cache community.general.krb_ticket: cache_name: KEYRING:persistent:0:0 - name: Make sure default ticket is destroyed community.general.krb_ticket: state: absent - name: Make sure specific ticket destroyed by principal community.general.krb_ticket: state: absent principal: admin@IPA.TEST - name: Make sure specific ticket destroyed by cache_name community.general.krb_ticket: state: absent cache_name: KEYRING:persistent:0:0 - name: Make sure all tickets are destroyed community.general.krb_ticket: state: absent kdestroy_all: true ''' from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt class IPAKeytab(object): def __init__(self, module, **kwargs): self.module = module self.password = kwargs['password'] self.principal = kwargs['principal'] self.state = kwargs['state'] self.kdestroy_all = kwargs['kdestroy_all'] self.cache_name = kwargs['cache_name'] self.start_time = kwargs['start_time'] self.renewable = kwargs['renewable'] self.forwardable = kwargs['forwardable'] self.proxiable = kwargs['proxiable'] self.address_restricted = kwargs['address_restricted'] self.canonicalization = kwargs['canonicalization'] self.enterprise = kwargs['enterprise'] self.renewal = kwargs['renewal'] self.validate = kwargs['validate'] self.keytab = kwargs['keytab'] self.keytab_path = kwargs['keytab_path'] self.kinit = CmdRunner( module, command='kinit', arg_formats=dict( lifetime=cmd_runner_fmt.as_opt_val('-l'), start_time=cmd_runner_fmt.as_opt_val('-s'), renewable=cmd_runner_fmt.as_opt_val('-r'), forwardable=cmd_runner_fmt.as_bool('-f', '-F', ignore_none=True), proxiable=cmd_runner_fmt.as_bool('-p', '-P', ignore_none=True), address_restricted=cmd_runner_fmt.as_bool('-a', '-A', ignore_none=True), anonymous=cmd_runner_fmt.as_bool('-n'), canonicalization=cmd_runner_fmt.as_bool('-C'), enterprise=cmd_runner_fmt.as_bool('-E'), renewal=cmd_runner_fmt.as_bool('-R'), validate=cmd_runner_fmt.as_bool('-v'), keytab=cmd_runner_fmt.as_bool('-k'), keytab_path=cmd_runner_fmt.as_func(lambda v: ['-t', v] if v else ['-i']), cache_name=cmd_runner_fmt.as_opt_val('-c'), principal=cmd_runner_fmt.as_list(), ) ) self.kdestroy = CmdRunner( module, command='kdestroy', arg_formats=dict( kdestroy_all=cmd_runner_fmt.as_bool('-A'), cache_name=cmd_runner_fmt.as_opt_val('-c'), principal=cmd_runner_fmt.as_opt_val('-p'), ) ) self.klist = CmdRunner( module, command='klist', arg_formats=dict( show_list=cmd_runner_fmt.as_bool('-l'), ) ) def exec_kinit(self): params = dict(self.module.params) with self.kinit( "lifetime start_time renewable forwardable proxiable address_restricted anonymous " "canonicalization enterprise renewal validate keytab keytab_path cache_name principal", check_rc=True, data=self.password, ) as ctx: rc, out, err = ctx.run(**params) return out def exec_kdestroy(self): params = dict(self.module.params) with self.kdestroy( "kdestroy_all cache_name principal", check_rc=True ) as ctx: rc, out, err = ctx.run(**params) return out def exec_klist(self, show_list): # Use chech_rc = False because # If no tickets present, klist command will always return rc = 1 params = dict(show_list=show_list) with self.klist( "show_list", check_rc=False ) as ctx: rc, out, err = ctx.run(**params) return rc, out, err def check_ticket_present(self): ticket_present = True show_list = False if not self.principal and not self.cache_name: rc, out, err = self.exec_klist(show_list) if rc != 0: ticket_present = False else: show_list = True rc, out, err = self.exec_klist(show_list) if self.principal and self.principal not in str(out): ticket_present = False if self.cache_name and self.cache_name not in str(out): ticket_present = False return ticket_present def main(): arg_spec = dict( principal=dict(type='str'), password=dict(type='str', no_log=True), state=dict(default='present', choices=['present', 'absent']), kdestroy_all=dict(type='bool'), cache_name=dict(type='str', fallback=(env_fallback, ['KRB5CCNAME'])), lifetime=dict(type='str'), start_time=dict(type='str'), renewable=dict(type='str'), forwardable=dict(type='bool'), proxiable=dict(type='bool'), address_restricted=dict(type='bool'), anonymous=dict(type='bool'), canonicalization=dict(type='bool'), enterprise=dict(type='bool'), renewal=dict(type='bool'), validate=dict(type='bool'), keytab=dict(type='bool'), keytab_path=dict(type='path'), ) module = AnsibleModule( argument_spec=arg_spec, supports_check_mode=True, required_by={ 'keytab_path': 'keytab' }, required_if=[ ('state', 'present', ('password', 'keytab_path'), True), ], ) state = module.params['state'] kdestroy_all = module.params['kdestroy_all'] keytab = IPAKeytab(module, state=state, kdestroy_all=kdestroy_all, principal=module.params['principal'], password=module.params['password'], cache_name=module.params['cache_name'], lifetime=module.params['lifetime'], start_time=module.params['start_time'], renewable=module.params['renewable'], forwardable=module.params['forwardable'], proxiable=module.params['proxiable'], address_restricted=module.params['address_restricted'], anonymous=module.params['anonymous'], canonicalization=module.params['canonicalization'], enterprise=module.params['enterprise'], renewal=module.params['renewal'], validate=module.params['validate'], keytab=module.params['keytab'], keytab_path=module.params['keytab_path'], ) if module.params['keytab_path'] is not None and module.params['keytab'] is not True: module.fail_json(msg="If keytab_path is specified then keytab parameter must be True") changed = False if state == 'present': if not keytab.check_ticket_present(): changed = True if not module.check_mode: keytab.exec_kinit() if state == 'absent': if kdestroy_all: changed = True if not module.check_mode: keytab.exec_kdestroy() elif keytab.check_ticket_present(): changed = True if not module.check_mode: keytab.exec_kdestroy() module.exit_json(changed=changed) if __name__ == '__main__': main()