Modify ipahost module: the authentication is done locally on the controller

node and the credential cache is copied to the managed node

ipahost module is also using facts gathered from the server to find the
domain and realm.
This commit is contained in:
Florence Blanc-Renaud
2017-08-10 16:54:44 +02:00
parent 09f45e4acd
commit 38d7223376
10 changed files with 430 additions and 66 deletions

View File

@@ -17,61 +17,226 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import gssapi
import os
import shutil
import subprocess
import tempfile
from jinja2 import Template
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_native
from ansible.plugins.action import ActionBase
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
def run_cmd(args, stdin=None):
"""
Execute an external command.
"""
p_in = None
p_out = subprocess.PIPE
p_err = subprocess.PIPE
if stdin:
p_in = subprocess.PIPE
p = subprocess.Popen(args, stdin=p_in, stdout=p_out, stderr=p_err,
close_fds=True)
stdout, stderr = p.communicate(stdin)
return p.returncode
def kinit_password(principal, password, ccache_name, config):
"""
Perform kinit using principal/password, with the specified config file
and store the TGT in ccache_name.
"""
args = [ "/usr/bin/kinit", principal, '-c', ccache_name]
old_config = os.environ.get('KRB5_CONFIG')
os.environ['KRB5_CONFIG'] = config
try:
result = run_cmd(args, stdin=password)
return result
finally:
if old_config is not None:
os.environ['KRB5_CONFIG'] = old_config
else:
os.environ.pop('KRB5_CONFIG', None)
def kinit_keytab(principal, keytab, ccache_name, config):
"""
Perform kinit using principal/keytab, with the specified config file
and store the TGT in ccache_name.
"""
old_config = os.environ.get('KRB5_CONFIG')
os.environ['KRB5_CONFIG'] = config
try:
name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
store = {'ccache': ccache_name,
'client_keytab': keytab}
cred = gssapi.Credentials(name=name, store=store, usage='initiate')
return cred
finally:
if old_config is not None:
os.environ['KRB5_CONFIG'] = old_config
else:
os.environ.pop('KRB5_CONFIG', None)
KRB5CONF_TEMPLATE = """
[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log
[libdefaults]
default_realm = {{ ipa_realm }}
dns_lookup_realm = false
dns_lookup_kdc = true
rdns = false
ticket_lifetime = {{ ipa_lifetime }}
forwardable = true
udp_preference_limit = 0
default_ccache_name = KEYRING:persistent:%{uid}
[realms]
{{ ipa_realm }} = {
kdc = {{ ipa_server }}:88
master_kdc = {{ ipa_server }}:88
admin_server = {{ ipa_server }}:749
default_domain = {{ ipa_domain }}
}
[domain_realm]
.{{ ipa_domain }} = {{ ipa_realm }}
{{ ipa_domain }} = {{ ipa_realm}}
"""
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
"""
handler for file transfer operations
handler for credential cache transfer
ipa* commands can either provide a password or a keytab file
in order to authenticate on the managed node with Kerberos.
When a keytab is provided, it needs to be copied from the control
node to the managed node.
This Action Module performs the copy when needed.
The module is using these credentials to obtain a TGT locally on the
control node:
- need to create a krb5.conf Kerberos client configuration that is
using IPA server
- set the environment variable KRB5_CONFIG to point to this conf file
- set the environment variable KRB5CCNAME to use a specific cache
- perform kinit on the control node
This command creates the credential cache file
- copy the credential cache file on the managed node
Then the IPA commands can use this credential cache file.
"""
if task_vars is None:
task_vars = dict()
result = super(ActionModule, self).run(tmp, task_vars)
principal = self._task.args.get('principal', None)
keytab = self._task.args.get('keytab', None)
password = self._task.args.get('password', None)
lifetime = self._task.args.get('lifetime', '1h')
if (keytab is None and password is None):
if (not keytab and not password):
result['failed'] = True
result['msg'] = "keytab or password is required"
return result
# If password is supplied, just need to execute the module
if password:
result.update(self._execute_module(task_vars=task_vars))
return result
# Password not supplied, need to transfer the keytab file
# Check if the source keytab exists
try:
keytab = self._find_needle('files', keytab)
except AnsibleError as e:
if not principal:
result['failed'] = True
result['msg'] = to_native(e)
result['msg'] = "principal is required"
return result
# Create the remote tmp dir
tmp = self._make_tmp_path()
tmp_keytab = self._connection._shell.join_path(
tmp, os.path.basename(keytab))
self._transfer_file(keytab, tmp_keytab)
self._fixup_perms2((tmp, tmp_keytab))
data = self._execute_module(module_name='ipa_facts', module_args=dict(),
task_vars=None)
try:
domain = data['ansible_facts']['ipa']['domain']
realm = data['ansible_facts']['ipa']['realm']
except KeyError:
result['failed'] = True
result['msg'] = "The host is not an IPA server"
return result
new_module_args = self._task.args.copy()
new_module_args.update(dict(keytab=tmp_keytab))
items = principal.split('@')
if len(items) < 2:
principal = str('%s@%s' % (principal, realm))
# Execute module
result.update(self._execute_module(module_args=new_module_args, task_vars=task_vars))
self._remove_tmp_path(tmp)
return result
# Locally create a temp directory to store krb5.conf and ccache
local_temp_dir = tempfile.mkdtemp()
krb5conf_name = os.path.join(local_temp_dir, 'krb5.conf')
ccache_name = os.path.join(local_temp_dir, 'ccache')
# Create the krb5.conf from the template
template = Template(KRB5CONF_TEMPLATE)
content = template.render(dict(
ipa_server=task_vars['ansible_host'],
ipa_domain=domain,
ipa_realm=realm,
ipa_lifetime=lifetime))
with open(krb5conf_name, 'w') as f:
f.write(content)
if password:
# perform kinit -c ccache_name -l 1h principal
res = kinit_password(principal, password, ccache_name,
krb5conf_name)
if res:
result['failed'] = True
result['msg'] = 'kinit %s with password failed' % principal
return result
else:
# Password not supplied, need to use the keytab file
# Check if the source keytab exists
try:
keytab = self._find_needle('files', keytab)
except AnsibleError as e:
result['failed'] = True
result['msg'] = to_native(e)
return result
# perform kinit -kt keytab
try:
kinit_keytab(principal, keytab, ccache_name, krb5conf_name)
except Exception as e:
result['failed'] = True
result['msg'] = 'kinit %s with keytab %s failed' % (principal, keytab)
return result
try:
# Create the remote tmp dir
tmp = self._make_tmp_path()
tmp_ccache = self._connection._shell.join_path(
tmp, os.path.basename(ccache_name))
# Copy the ccache to the remote tmp dir
self._transfer_file(ccache_name, tmp_ccache)
self._fixup_perms2((tmp, tmp_ccache))
new_module_args = self._task.args.copy()
new_module_args.pop('password', None)
new_module_args.pop('keytab', None)
new_module_args.pop('lifetime', None)
new_module_args.update(ccache=tmp_ccache)
# Execute module
result.update(self._execute_module(module_args=new_module_args,
task_vars=task_vars))
return result
finally:
# delete the local temp directory
shutil.rmtree(local_temp_dir, ignore_errors=True)
run_cmd(['/usr/bin/kdestroy', '-c', tmp_ccache])

Binary file not shown.