Use the IPA API directly when adding the HTTP principal
[cascardo/ipsilon.git] / ipsilon / helpers / ipa.py
1 # Copyright (C) 2014  Simo Sorce <simo@redhat.com>
2 #
3 # see file 'COPYING' for use and warranty information
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 import logging
19 import pwd
20 import os
21 import socket
22 import subprocess
23 import sys
24
25 from ipsilon.helpers.common import EnvHelpersInstaller
26
27
28 IPA_CONFIG_FILE = '/etc/ipa/default.conf'
29 HTTPD_IPA_KEYTAB = '/etc/httpd/conf/ipa.keytab'
30 IPA_COMMAND = '/usr/bin/ipa'
31 IPA_GETKEYTAB = '/usr/sbin/ipa-getkeytab'
32 HTTPD_USER = 'apache'
33
34 NO_CREDS_FOR_KEYTAB = """
35 Valid IPA admin credentials are required to get a keytab.
36 Please kinit with a pivileged user like 'admin' and retry.
37 """
38
39 FAILED_TO_GET_KEYTAB = """
40 A pre-existing keytab was not found and it was not possible to
41 successfully retrieve a new keytab for the IPA server. Please
42 manually provide a keytab or resolve the error that cause this
43 failure (see logs) and retry.
44 """
45
46
47 class Installer(EnvHelpersInstaller):
48
49     def __init__(self, *pargs):
50         super(Installer, self).__init__()
51         self.name = 'ipa'
52         self.ptype = 'helper'
53         self.logger = None
54         self.realm = None
55         self.domain = None
56         self.server = None
57
58     def install_args(self, group):
59         group.add_argument('--ipa', choices=['yes', 'no', 'auto'],
60                            default='auto',
61                            help='Helper for IPA joined machines')
62
63     def conf_init(self, opts):
64         logger = self.logger
65         # Do a simple check to see if machine is ipa joined
66         if not os.path.exists(IPA_CONFIG_FILE):
67             logger.info('No IPA configuration file. Skipping ipa helper...')
68             if opts['ipa'] == 'yes':
69                 raise Exception('No IPA installation found!')
70             return
71
72         # Get config vars from ipa file
73         try:
74             from ipapython import config as ipaconfig
75
76             ipaconfig.init_config()
77             self.realm = ipaconfig.config.get_realm()
78             self.domain = ipaconfig.config.get_domain()
79             self.server = ipaconfig.config.get_server()
80
81         except Exception, e:  # pylint: disable=broad-except
82             logger.info('IPA tools installation found: [%s]', e)
83             if opts['ipa'] == 'yes':
84                 raise Exception('No IPA installation found!')
85             return
86
87     def get_keytab(self, opts):
88         logger = self.logger
89         # Check if we have need ipa tools
90         if not os.path.exists(IPA_GETKEYTAB):
91             logger.info('ipa-getkeytab missing. Will skip keytab creation.')
92             if opts['ipa'] == 'yes':
93                 raise Exception('No IPA tools found!')
94
95         # Check if we already have a keytab for HTTP
96         if 'krb_httpd_keytab' in opts:
97             msg = "Searching for keytab in: %s" % opts['krb_httpd_keytab']
98             print >> sys.stdout, msg,
99             if os.path.exists(opts['krb_httpd_keytab']):
100                 print >> sys.stdout, "... Found!"
101                 return
102             else:
103                 print >> sys.stdout, "... Not found!"
104
105         msg = "Searching for keytab in: %s" % HTTPD_IPA_KEYTAB
106         print >> sys.stdout, msg,
107         if os.path.exists(HTTPD_IPA_KEYTAB):
108             opts['krb_httpd_keytab'] = HTTPD_IPA_KEYTAB
109             print >> sys.stdout, "... Found!"
110             return
111         else:
112             print >> sys.stdout, "... Not found!"
113
114         us = socket.gethostname()
115         princ = 'HTTP/%s@%s' % (us, self.realm)
116
117         # Check we have credentials to access server (for keytab)
118         from ipalib import api
119         from ipalib import errors as ipaerrors
120
121         api.bootstrap(context='ipsilon_installer')
122         api.finalize()
123
124         try:
125             api.Backend.rpcclient.connect()
126             logger.debug('Try RPC connection')
127             api.Backend.rpcclient.forward('ping')
128             print >> sys.stdout, "... Succeeded!"
129         except ipaerrors.KerberosError as e:
130             print >> sys.stderr, NO_CREDS_FOR_KEYTAB
131             logger.error('Invalid credentials: [%s]', repr(e))
132             if api.Backend.rpcclient.isconnected():
133                 api.Backend.rpcclient.disconnect()
134             raise Exception('Invalid credentials: [%s]' % e)
135         except ipaerrors.PublicError as e:
136             print >> sys.stderr, "Can't connect to any IPA server"
137             logger.error(
138                 'Cannot connect to the server due to generic error: %s', e)
139             if api.Backend.rpcclient.isconnected():
140                 api.Backend.rpcclient.disconnect()
141             raise Exception('Unable to connect to IPA server: %s' % e)
142
143         # Specify an older version to work on nearly any master. Force is
144         # set to True so a DNS A record is not required for adding the
145         # service.
146         try:
147             api.Backend.rpcclient.forward(
148                 'service_add',
149                 unicode(princ),
150                 force=True,
151                 version=u'2.0',
152             )
153         except ipaerrors.DuplicateEntry:
154             logger.debug('Principal %s already exists' % princ)
155         except ipaerrors.NotFound as e:
156             print >> sys.stderr, "%s" % e
157             logger.error('%s' % e)
158             raise Exception('%s' % e)
159         except ipaerrors.ACIError as e:
160             print >> sys.stderr, NO_CREDS_FOR_KEYTAB
161             logger.error('Invalid credentials: [%s]', repr(e))
162             raise Exception('Invalid credentials: [%s]' % e)
163         finally:
164             server = api.Backend.rpcclient.api.env.server
165             if api.Backend.rpcclient.isconnected():
166                 api.Backend.rpcclient.disconnect()
167
168         try:
169             msg = "Trying to fetch keytab[%s] for %s" % (
170                   opts['krb_httpd_keytab'], princ)
171             print >> sys.stdout, msg,
172             subprocess.check_output([IPA_GETKEYTAB,
173                                      '-s', server, '-p', princ,
174                                      '-k', opts['krb_httpd_keytab']],
175                                     stderr=subprocess.STDOUT)
176         except subprocess.CalledProcessError, e:
177             # unfortunately this one is fatal
178             print >> sys.stderr, FAILED_TO_GET_KEYTAB
179             logger.info('Error trying to get HTTP keytab:')
180             logger.info('Cmd> %s\n%s', e.cmd, e.output)
181             raise Exception('Missing keytab: [%s]' % e)
182
183         # Fixup permissions so only the ipsilon user can read these files
184         pw = pwd.getpwnam(HTTPD_USER)
185         os.chown(opts['krb_httpd_keytab'], pw.pw_uid, pw.pw_gid)
186
187     def configure_server(self, opts):
188         if opts['ipa'] != 'yes' and opts['ipa'] != 'auto':
189             return
190         if opts['ipa'] != 'yes' and opts['krb'] == 'no':
191             return
192
193         self.logger = logging.getLogger()
194
195         self.conf_init(opts)
196
197         self.get_keytab(opts)
198
199         # Forcibly use krb then pam modules
200         if 'lm_order' not in opts:
201             opts['lm_order'] = []
202         opts['krb'] = 'yes'
203         if 'krb' not in opts['lm_order']:
204             opts['lm_order'].insert(0, 'krb')
205         opts['form'] = 'yes'
206         if not any(lm in opts['lm_order'] for lm in ('form', 'pam')):
207             opts['lm_order'].append('form')