pam: use a pam object method instead of pam module function
[cascardo/ipsilon.git] / ipsilon / install / ipsilon-server-install
1 #!/usr/bin/python
2 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
3
4 from ipsilon.login.common import LoginMgrsInstall
5 from ipsilon.info.common import InfoProviderInstall
6 from ipsilon.providers.common import ProvidersInstall
7 from ipsilon.helpers.common import EnvHelpersInstall
8 from ipsilon.util.data import UserStore
9 from ipsilon.tools import files, dbupgrade
10 import ConfigParser
11 import argparse
12 import cherrypy
13 import json
14 import logging
15 import os
16 import pwd
17 import shutil
18 import socket
19 import subprocess
20 import sys
21 import time
22
23
24 TEMPLATES = '/usr/share/ipsilon/templates/install'
25 CONFDIR = '/etc/ipsilon'
26 DATADIR = '/var/lib/ipsilon'
27 HTTPDCONFD = '/etc/httpd/conf.d'
28 BINDIR = '/usr/libexec'
29 STATICDIR = '/usr/share/ipsilon'
30 CACHEDIR = '/var/cache/ipsilon'
31 WSGI_SOCKET_PREFIX = None
32
33
34 class ConfigurationError(StandardError):
35
36     def __init__(self, message):
37         StandardError.__init__(self, message)
38
39
40 #Silence cherrypy logging to screen
41 cherrypy.log.screen = False
42
43 # Regular logging
44 LOGFILE = '/var/log/ipsilon-install.log'
45 logger = logging.getLogger()
46
47
48 def openlogs():
49     global logger  # pylint: disable=W0603
50     if os.path.isfile(LOGFILE):
51         try:
52             created = '%s' % time.ctime(os.path.getctime(LOGFILE))
53             shutil.move(LOGFILE, '%s.%s' % (LOGFILE, created))
54         except IOError:
55             pass
56     logger = logging.getLogger()
57     try:
58         lh = logging.FileHandler(LOGFILE)
59     except IOError, e:
60         print >> sys.stderr, 'Unable to open %s (%s)' % (LOGFILE, str(e))
61         lh = logging.StreamHandler(sys.stderr)
62     formatter = logging.Formatter('[%(asctime)s] %(message)s')
63     lh.setFormatter(formatter)
64     lh.setLevel(logging.DEBUG)
65     logger.addHandler(lh)
66     logger.propagate = False
67     ch = logging.StreamHandler(sys.stdout)
68     formatter = logging.Formatter('%(message)s')
69     ch.setFormatter(formatter)
70     ch.setLevel(logging.INFO)
71     logger.addHandler(ch)
72     cherrypy.log.error_log.setLevel(logging.DEBUG)
73
74
75 def install(plugins, args):
76     logger.info('Installation initiated')
77     now = time.strftime("%Y%m%d%H%M%S", time.gmtime())
78     instance_conf = os.path.join(CONFDIR, args['instance'])
79
80     logger.info('Installing default config files')
81     ipsilon_conf = os.path.join(instance_conf, 'ipsilon.conf')
82     idp_conf = os.path.join(instance_conf, 'idp.conf')
83     args['httpd_conf'] = os.path.join(HTTPDCONFD,
84                                       'ipsilon-%s.conf' % args['instance'])
85     args['data_dir'] = os.path.join(DATADIR, args['instance'])
86     args['public_data_dir'] = os.path.join(args['data_dir'], 'public')
87     args['wellknown_dir'] = os.path.join(args['public_data_dir'],
88                                          'well-known')
89     if os.path.exists(ipsilon_conf):
90         shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
91     if os.path.exists(idp_conf):
92         shutil.move(idp_conf, '%s.backup.%s' % (idp_conf, now))
93     if not os.path.exists(instance_conf):
94         os.makedirs(instance_conf, 0700)
95     confopts = {'instance': args['instance'],
96                 'datadir': args['data_dir'],
97                 'publicdatadir': args['public_data_dir'],
98                 'wellknowndir': args['wellknown_dir'],
99                 'sysuser': args['system_user'],
100                 'cleanup_interval': args['cleanup_interval'],
101                 'ipsilondir': BINDIR,
102                 'staticdir': STATICDIR,
103                 'cachedir': CACHEDIR,
104                 'admindb': args['admin_dburi'] or args['database_url'] % {
105                     'datadir': args['data_dir'], 'dbname': 'adminconfig'},
106                 'usersdb': args['users_dburi'] or args['database_url'] % {
107                     'datadir': args['data_dir'], 'dbname': 'userprefs'},
108                 'transdb': args['transaction_dburi'] or args['database_url'] %
109                 {'datadir': args['data_dir'], 'dbname': 'transactions'},
110                 'samlsessionsdb': args['samlsessions_dburi'] or args[
111                     'database_url'] % {'datadir': args['data_dir'],
112                                        'dbname': 'saml2sessions'},
113                 'secure': "False" if args['secure'] == "no" else "True",
114                 'debugging': "True" if args['server_debugging'] else "False"}
115     # Testing database sessions
116     if 'session_type' in args:
117         confopts['sesstype'] = args['session_type']
118     else:
119         confopts['sesstype'] = 'file'
120     if 'session_dburi' in args:
121         confopts['sessopt'] = 'dburi'
122         confopts['sessval'] = args['session_dburi']
123     else:
124         confopts['sessopt'] = 'path'
125         confopts['sessval'] = os.path.join(args['data_dir'], 'sessions')
126     # Whether to disable security (for testing)
127     if args['secure'] == 'no':
128         confopts['secure'] = "False"
129         confopts['sslrequiressl'] = ""
130     else:
131         confopts['secure'] = "True"
132         confopts['sslrequiressl'] = "   SSLRequireSSL"
133     if WSGI_SOCKET_PREFIX:
134         confopts['wsgi_socket'] = 'WSGISocketPrefix %s' % WSGI_SOCKET_PREFIX
135     else:
136         confopts['wsgi_socket'] = ''
137     files.write_from_template(ipsilon_conf,
138                               os.path.join(TEMPLATES, 'ipsilon.conf'),
139                               confopts)
140     files.write_from_template(idp_conf,
141                               os.path.join(TEMPLATES, 'idp.conf'),
142                               confopts)
143     if not os.path.exists(args['httpd_conf']):
144         os.symlink(idp_conf, args['httpd_conf'])
145     if not os.path.exists(args['public_data_dir']):
146         os.makedirs(args['public_data_dir'], 0755)
147     if not os.path.exists(args['wellknown_dir']):
148         os.makedirs(args['wellknown_dir'], 0755)
149     sessdir = os.path.join(args['data_dir'], 'sessions')
150     if not os.path.exists(sessdir):
151         os.makedirs(sessdir, 0700)
152     data_conf = os.path.join(args['data_dir'], 'ipsilon.conf')
153     if not os.path.exists(data_conf):
154         os.symlink(ipsilon_conf, data_conf)
155     # Load the cherrypy config from the newly installed file so
156     # that db paths and all is properly set before configuring
157     # components
158     cherrypy.config.update(ipsilon_conf)
159
160     # Prepare to allow plugins to save things changed during install
161     changes = {'env_helper': {},
162                'login_manager': {},
163                'info_provider': {},
164                'auth_provider': {}}
165
166     # Move pre-existing dbs away
167     admin_db = cherrypy.config['admin.config.db']
168     if os.path.exists(admin_db):
169         shutil.move(admin_db, '%s.backup.%s' % (admin_db, now))
170     users_db = cherrypy.config['user.prefs.db']
171     if os.path.exists(users_db):
172         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
173
174     # Initialize initial database schemas
175     dbupgrade.execute_upgrade(ipsilon_conf)
176
177     # Store primary admin
178     db = UserStore()
179     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
180
181     logger.info('Configuring environment helpers')
182     for plugin_name in plugins['Environment Helpers']:
183         plugin = plugins['Environment Helpers'][plugin_name]
184         plugin_changes = {}
185         if plugin.configure_server(args, plugin_changes) == False:
186             msg = 'Configuration of environment helper %s failed' % plugin_name
187             raise ConfigurationError(msg)
188         changes['env_helper'][plugin_name] = plugin_changes
189
190     logger.info('Configuring login managers')
191     for plugin_name in args['lm_order']:
192         try:
193             plugin = plugins['Login Managers'][plugin_name]
194         except KeyError:
195             sys.exit('Login provider %s not installed' % plugin_name)
196         plugin_changes = {}
197         if plugin.configure(args, plugin_changes) == False:
198             msg = 'Configuration of login manager %s failed' % plugin_name
199             raise ConfigurationError(msg)
200         changes['login_manager'][plugin_name] = plugin_changes
201
202     logger.info('Configuring Info provider')
203     for plugin_name in plugins['Info Provider']:
204         plugin = plugins['Info Provider'][plugin_name]
205         plugin_changes = {}
206         if plugin.configure(args, plugin_changes) == False:
207             msg = 'Configuration of info provider %s failed' % plugin_name
208             raise ConfigurationError(msg)
209         changes['info_provider'][plugin_name] = plugin_changes
210
211     logger.info('Configuring Authentication Providers')
212     for plugin_name in plugins['Auth Providers']:
213         plugin = plugins['Auth Providers'][plugin_name]
214         plugin_changes = {}
215         if plugin.configure(args, plugin_changes) == False:
216             msg = 'Configuration of auth provider %s failed' % plugin_name
217             raise ConfigurationError(msg)
218         changes['auth_provider'][plugin_name] = plugin_changes
219
220     # Save any changes that were made
221     install_changes = os.path.join(instance_conf, 'install_changes')
222     changes = json.dumps(changes)
223     with open(install_changes, 'w+') as f:
224         f.write(changes)
225
226     # Initialize extra database schemas
227     dbupgrade.execute_upgrade(ipsilon_conf)
228
229     # Fixup permissions so only the ipsilon user can read these files
230     files.fix_user_dirs(instance_conf, opts['system_user'])
231     files.fix_user_dirs(args['data_dir'], opts['system_user'])
232     try:
233         subprocess.call(['/usr/sbin/restorecon', '-R', args['data_dir']])
234     except Exception:  # pylint: disable=broad-except
235         pass
236
237
238 def uninstall(plugins, args):
239     logger.info('Uninstallation initiated')
240     instance_conf = os.path.join(CONFDIR, args['instance'])
241
242     httpd_conf = os.path.join(HTTPDCONFD,
243                               'ipsilon-%s.conf' % args['instance'])
244     data_dir = os.path.join(DATADIR, args['instance'])
245
246     try:
247         tconf = ConfigParser.SafeConfigParser()
248         tconf.read(os.path.join(instance_conf, 'ipsilon.conf'))
249         cache_dir = tconf.get('global', 'cache_dir')
250     except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
251         cache_dir = None
252     else:
253         cache_dir = cache_dir.replace('"', '')
254
255     if not os.path.exists(instance_conf):
256         raise Exception('Could not find instance %s configuration'
257                         % args['instance'])
258     if not os.path.exists(httpd_conf):
259         raise Exception('Could not find instance %s httpd configuration'
260                         % args['instance'])
261     if not args['yes']:
262         sure = raw_input(('Are you certain you want to erase instance %s ' +
263                           '[yes/NO]: ')
264                          % args['instance'])
265         if sure != 'yes':
266             raise Exception('Aborting')
267
268     # Get the details of what we changed during installation
269     install_changes = os.path.join(instance_conf, 'install_changes')
270     with open(install_changes, 'r') as f:
271         changes = json.loads(f.read())
272
273     logger.info('Removing environment helpers')
274     for plugin_name in plugins['Environment Helpers']:
275         plugin = plugins['Environment Helpers'][plugin_name]
276         plugin_changes = changes['env_helper'].get(plugin_name, {})
277         if plugin.unconfigure(args, plugin_changes) == False:
278             logger.info('Removal of environment helper %s failed' % plugin_name)
279
280     logger.info('Removing login managers')
281     for plugin_name in plugins['Login Managers']:
282         plugin = plugins['Login Managers'][plugin_name]
283         plugin_changes = changes['login_manager'].get(plugin_name, {})
284         if plugin.unconfigure(args, plugin_changes) == False:
285             logger.info('Removal of login manager %s failed' % plugin_name)
286
287     logger.info('Removing Info providers')
288     for plugin_name in plugins['Info Provider']:
289         plugin = plugins['Info Provider'][plugin_name]
290         plugin_changes = changes['info_provider'].get(plugin_name, {})
291         if plugin.unconfigure(args, plugin_changes) == False:
292             logger.info('Removal of info provider %s failed' % plugin_name)
293
294     logger.info('Removing Authentication Providers')
295     for plugin_name in plugins['Auth Providers']:
296         plugin = plugins['Auth Providers'][plugin_name]
297         plugin_changes = changes['auth_provider'].get(plugin_name, {})
298         if plugin.unconfigure(args, plugin_changes) == False:
299             logger.info('Removal of auth provider %s failed' % plugin_name)
300
301     logger.info('Removing httpd configuration')
302     os.remove(httpd_conf)
303     logger.info('Erasing instance configuration')
304     shutil.rmtree(instance_conf)
305     logger.info('Erasing instance data')
306     shutil.rmtree(data_dir)
307     if cache_dir and os.path.exists(cache_dir):
308         for fn in os.listdir(cache_dir):
309             os.unlink(os.path.join(cache_dir, fn))
310     logger.info('Uninstalled instance %s' % args['instance'])
311
312
313 def find_plugins():
314     plugins = {
315         'Environment Helpers': EnvHelpersInstall().plugins,
316         'Login Managers': LoginMgrsInstall().plugins,
317         'Info Provider': InfoProviderInstall().plugins,
318         'Auth Providers': ProvidersInstall().plugins
319     }
320     return plugins
321
322
323 def parse_config_profile(args):
324     config = ConfigParser.RawConfigParser()
325     files = config.read(args['config_profile'])
326     if len(files) == 0:
327         raise ConfigurationError('Config Profile file %s not found!' %
328                                  args['config_profile'])
329
330     if 'globals' in config.sections():
331         G = config.options('globals')
332         for g in G:
333             val = config.get('globals', g)
334             if g in globals():
335                 globals()[g] = val
336             else:
337                 for k in globals():
338                     if k.lower() == g.lower():
339                         globals()[k] = val
340                         break
341
342     if 'arguments' in config.sections():
343         A = config.options('arguments')
344         for a in A:
345             args[a] = config.get('arguments', a)
346
347     return args
348
349
350 def parse_args(plugins):
351     parser = argparse.ArgumentParser(description='Ipsilon Install Options')
352     parser.add_argument('--version',
353                         action='version', version='%(prog)s 0.1')
354     parser.add_argument('-o', '--login-managers-order', dest='lm_order',
355                         help='Comma separated list of login managers')
356     parser.add_argument('--hostname',
357                         help="Machine's fully qualified host name")
358     parser.add_argument('--instance', default='idp',
359                         help="IdP instance name, each is a separate idp")
360     parser.add_argument('--system-user', default='ipsilon',
361                         help="User account used to run the server")
362     parser.add_argument('--admin-user', default='admin',
363                         help="User account that is assigned admin privileges")
364     parser.add_argument('--database-url',
365                         default='sqlite:///%(datadir)s/%(dbname)s.sqlite',
366                         help="The (templatized) database URL to use")
367     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
368                         help="Turn on all security checks")
369     parser.add_argument('--config-profile', default=None,
370                         help=argparse.SUPPRESS)
371     parser.add_argument('--server-debugging', action='store_true',
372                         help="Enable debugging")
373     parser.add_argument('--uninstall', action='store_true',
374                         help="Uninstall the server and all data")
375     parser.add_argument('--yes', action='store_true',
376                         help="Always answer yes")
377     parser.add_argument('--admin-dburi',
378                         help='Configuration database URI (override template)')
379     parser.add_argument('--users-dburi',
380                         help='User configuration database URI (override '
381                              'template)')
382     parser.add_argument('--transaction-dburi',
383                         help='Transaction database URI (override template)')
384     parser.add_argument('--samlsessions-dburi',
385                         help='SAML 2 sessions database URI (override ' +
386                              'template)')
387     parser.add_argument('--cleanup-interval', default=30,
388                         help='Interval between cleaning up stale database ' +
389                              'entries (in minutes, default: 30 minutes)')
390
391     lms = []
392
393     for plugin_group in plugins:
394         group = parser.add_argument_group(plugin_group)
395         for plugin_name in plugins[plugin_group]:
396             plugin = plugins[plugin_group][plugin_name]
397             if plugin.ptype == 'login':
398                 lms.append(plugin.name)
399             plugin.install_args(group)
400
401     args = vars(parser.parse_args())
402
403     if args['config_profile']:
404         args = parse_config_profile(args)
405
406     if not args['hostname']:
407         args['hostname'] = socket.getfqdn()
408
409     if args['uninstall']:
410         return args
411
412     if len(args['hostname'].split('.')) < 2:
413         raise ConfigurationError('Hostname: %s is not a FQDN' %
414                                  args['hostname'])
415
416     for plugin_group in plugins:
417         for plugin_name in plugins[plugin_group]:
418             plugin = plugins[plugin_group][plugin_name]
419             plugin.validate_args(args)
420
421     try:
422         pwd.getpwnam(args['system_user'])
423     except KeyError:
424         raise ConfigurationError('User: %s not found on the system' %
425                                  args['system_user'])
426
427     if args['lm_order'] is None:
428         args['lm_order'] = []
429         for name in lms:
430             if args[name] == 'yes':
431                 args['lm_order'].append(name)
432     else:
433         args['lm_order'] = args['lm_order'].split(',')
434
435     if len(args['lm_order']) == 0 and args['ipa'] != 'yes':
436         sys.exit('No login plugins are enabled.')
437
438     #FIXME: check instance is only alphanums
439
440     return args
441
442 if __name__ == '__main__':
443     opts = []
444     out = 0
445     openlogs()
446     logger.setLevel(logging.DEBUG)
447
448     try:
449         fplugins = find_plugins()
450         opts = parse_args(fplugins)
451
452         logger.debug('Installation arguments:')
453         for k in sorted(opts.iterkeys()):
454             logger.debug('%s: %s', k, opts[k])
455
456         if 'uninstall' in opts and opts['uninstall'] is True:
457             if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
458                 logger.info('Instance %s could not be found' % opts['instance'])
459                 sys.exit(0)
460             uninstall(fplugins, opts)
461         else:
462             install(fplugins, opts)
463     except Exception, e:  # pylint: disable=broad-except
464         logger.info(str(e))         # emit message to console
465         logger.debug(e, exc_info=1) # add backtrace information to logfile
466
467         if 'uninstall' in opts and opts['uninstall'] is True:
468             logger.info('Uninstallation aborted.')
469         else:
470             logger.info('Installation aborted.')
471         logger.info('See log file %s for details' % LOGFILE)
472         out = 1
473     except SystemExit:
474         out = 1
475         raise
476     finally:
477         if out == 0:
478             if 'uninstall' in opts and opts['uninstall'] is True:
479                 logger.info('Uninstallation complete.')
480             else:
481                 logger.info('Installation complete.')
482                 logger.info('Please restart HTTPD to enable the IdP instance.')
483     sys.exit(out)