Configure the SAML2 session database during installation
[cascardo/ipsilon.git] / ipsilon / install / ipsilon-server-install
index e088b39..5c1ef70 100755 (executable)
@@ -1,31 +1,16 @@
 #!/usr/bin/python
 #!/usr/bin/python
-#
-# Copyright (C) 2014  Simo Sorce <simo@redhat.com>
-#
-# see file 'COPYING' for use and warranty information
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
 
 from ipsilon.login.common import LoginMgrsInstall
 from ipsilon.info.common import InfoProviderInstall
 from ipsilon.providers.common import ProvidersInstall
 from ipsilon.helpers.common import EnvHelpersInstall
 
 from ipsilon.login.common import LoginMgrsInstall
 from ipsilon.info.common import InfoProviderInstall
 from ipsilon.providers.common import ProvidersInstall
 from ipsilon.helpers.common import EnvHelpersInstall
-from ipsilon.util.data import Store
+from ipsilon.util.data import UserStore
 from ipsilon.tools import files
 import ConfigParser
 import argparse
 import cherrypy
 from ipsilon.tools import files
 import ConfigParser
 import argparse
 import cherrypy
+import json
 import logging
 import os
 import pwd
 import logging
 import os
 import pwd
@@ -40,7 +25,7 @@ TEMPLATES = '/usr/share/ipsilon/templates/install'
 CONFDIR = '/etc/ipsilon'
 DATADIR = '/var/lib/ipsilon'
 HTTPDCONFD = '/etc/httpd/conf.d'
 CONFDIR = '/etc/ipsilon'
 DATADIR = '/var/lib/ipsilon'
 HTTPDCONFD = '/etc/httpd/conf.d'
-BINDIR = '/usr/sbin'
+BINDIR = '/usr/libexec'
 STATICDIR = '/usr/share/ipsilon'
 WSGI_SOCKET_PREFIX = None
 
 STATICDIR = '/usr/share/ipsilon'
 WSGI_SOCKET_PREFIX = None
 
@@ -79,7 +64,15 @@ def openlogs():
         lh = logging.StreamHandler(sys.stderr)
     formatter = logging.Formatter('[%(asctime)s] %(message)s')
     lh.setFormatter(formatter)
         lh = logging.StreamHandler(sys.stderr)
     formatter = logging.Formatter('[%(asctime)s] %(message)s')
     lh.setFormatter(formatter)
+    lh.setLevel(logging.DEBUG)
     logger.addHandler(lh)
     logger.addHandler(lh)
+    logger.propagate = False
+    ch = logging.StreamHandler(sys.stdout)
+    formatter = logging.Formatter('%(message)s')
+    ch.setFormatter(formatter)
+    ch.setLevel(logging.INFO)
+    logger.addHandler(ch)
+    cherrypy.log.error_log.setLevel(logging.DEBUG)
 
 
 def install(plugins, args):
 
 
 def install(plugins, args):
@@ -93,6 +86,9 @@ def install(plugins, args):
     args['httpd_conf'] = os.path.join(HTTPDCONFD,
                                       'ipsilon-%s.conf' % args['instance'])
     args['data_dir'] = os.path.join(DATADIR, args['instance'])
     args['httpd_conf'] = os.path.join(HTTPDCONFD,
                                       'ipsilon-%s.conf' % args['instance'])
     args['data_dir'] = os.path.join(DATADIR, args['instance'])
+    args['public_data_dir'] = os.path.join(args['data_dir'], 'public')
+    args['wellknown_dir'] = os.path.join(args['public_data_dir'],
+                                         'well-known')
     if os.path.exists(ipsilon_conf):
         shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
     if os.path.exists(idp_conf):
     if os.path.exists(ipsilon_conf):
         shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
     if os.path.exists(idp_conf):
@@ -101,11 +97,34 @@ def install(plugins, args):
         os.makedirs(instance_conf, 0700)
     confopts = {'instance': args['instance'],
                 'datadir': args['data_dir'],
         os.makedirs(instance_conf, 0700)
     confopts = {'instance': args['instance'],
                 'datadir': args['data_dir'],
+                'publicdatadir': args['public_data_dir'],
+                'wellknowndir': args['wellknown_dir'],
                 'sysuser': args['system_user'],
                 'ipsilondir': BINDIR,
                 'staticdir': STATICDIR,
                 'sysuser': args['system_user'],
                 'ipsilondir': BINDIR,
                 'staticdir': STATICDIR,
+                'admindb': args['admin_dburi'] or args['database_url'] % {
+                    'datadir': args['data_dir'], 'dbname': 'adminconfig'},
+                'usersdb': args['users_dburi'] or args['database_url'] % {
+                    'datadir': args['data_dir'], 'dbname': 'userprefs'},
+                'transdb': args['transaction_dburi'] or args['database_url'] %
+                {'datadir': args['data_dir'], 'dbname': 'transactions'},
+                'samlsessionsdb': args['samlsessions_dburi'] or args[
+                    'database_url'] % {'datadir': args['data_dir'],
+                                       'dbname': 'saml2sessions'},
                 'secure': "False" if args['secure'] == "no" else "True",
                 'debugging': "True" if args['server_debugging'] else "False"}
                 'secure': "False" if args['secure'] == "no" else "True",
                 'debugging': "True" if args['server_debugging'] else "False"}
+    # Testing database sessions
+    if 'session_type' in args:
+        confopts['sesstype'] = args['session_type']
+    else:
+        confopts['sesstype'] = 'file'
+    if 'session_dburi' in args:
+        confopts['sessopt'] = 'dburi'
+        confopts['sessval'] = args['session_dburi']
+    else:
+        confopts['sessopt'] = 'path'
+        confopts['sessval'] = os.path.join(args['data_dir'], 'sessions')
+    # Whether to disable security (for testing)
     if args['secure'] == 'no':
         confopts['secure'] = "False"
         confopts['sslrequiressl'] = ""
     if args['secure'] == 'no':
         confopts['secure'] = "False"
         confopts['sslrequiressl'] = ""
@@ -124,6 +143,10 @@ def install(plugins, args):
                               confopts)
     if not os.path.exists(args['httpd_conf']):
         os.symlink(idp_conf, args['httpd_conf'])
                               confopts)
     if not os.path.exists(args['httpd_conf']):
         os.symlink(idp_conf, args['httpd_conf'])
+    if not os.path.exists(args['public_data_dir']):
+        os.makedirs(args['public_data_dir'], 0755)
+    if not os.path.exists(args['wellknown_dir']):
+        os.makedirs(args['wellknown_dir'], 0755)
     sessdir = os.path.join(args['data_dir'], 'sessions')
     if not os.path.exists(sessdir):
         os.makedirs(sessdir, 0700)
     sessdir = os.path.join(args['data_dir'], 'sessions')
     if not os.path.exists(sessdir):
         os.makedirs(sessdir, 0700)
@@ -135,6 +158,12 @@ def install(plugins, args):
     # components
     cherrypy.config.update(ipsilon_conf)
 
     # components
     cherrypy.config.update(ipsilon_conf)
 
+    # Prepare to allow plugins to save things changed during install
+    changes = {'env_helper': {},
+               'login_manager': {},
+               'info_provider': {},
+               'auth_provider': {}}
+
     # Move pre-existing admin db away
     admin_db = cherrypy.config['admin.config.db']
     if os.path.exists(admin_db):
     # Move pre-existing admin db away
     admin_db = cherrypy.config['admin.config.db']
     if os.path.exists(admin_db):
@@ -144,28 +173,49 @@ def install(plugins, args):
     users_db = cherrypy.config['user.prefs.db']
     if os.path.exists(users_db):
         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
     users_db = cherrypy.config['user.prefs.db']
     if os.path.exists(users_db):
         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
-    db = Store()
+    db = UserStore()
     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
 
     logger.info('Configuring environment helpers')
     for plugin_name in plugins['Environment Helpers']:
         plugin = plugins['Environment Helpers'][plugin_name]
     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
 
     logger.info('Configuring environment helpers')
     for plugin_name in plugins['Environment Helpers']:
         plugin = plugins['Environment Helpers'][plugin_name]
-        plugin.configure_server(args)
+        plugin_changes = {}
+        if plugin.configure_server(args, plugin_changes) == False:
+            logger.info('Configuration of environment helper %s failed' % plugin_name)
+        changes['env_helper'][plugin_name] = plugin_changes
 
     logger.info('Configuring login managers')
     for plugin_name in args['lm_order']:
 
     logger.info('Configuring login managers')
     for plugin_name in args['lm_order']:
-        plugin = plugins['Login Managers'][plugin_name]
-        plugin.configure(args)
+        try:
+            plugin = plugins['Login Managers'][plugin_name]
+        except KeyError:
+            sys.exit('Login provider %s not installed' % plugin_name)
+        plugin_changes = {}
+        if plugin.configure(args, plugin_changes) == False:
+            logger.info('Configuration of login manager %s failed' % plugin_name)
+        changes['login_manager'][plugin_name] = plugin_changes
 
     logger.info('Configuring Info provider')
     for plugin_name in plugins['Info Provider']:
         plugin = plugins['Info Provider'][plugin_name]
 
     logger.info('Configuring Info provider')
     for plugin_name in plugins['Info Provider']:
         plugin = plugins['Info Provider'][plugin_name]
-        plugin.configure(args)
+        plugin_changes = {}
+        if plugin.configure(args, plugin_changes) == False:
+            logger.info('Configuration of info provider %s failed' % plugin_name)
+        changes['info_provider'][plugin_name] = plugin_changes
 
     logger.info('Configuring Authentication Providers')
     for plugin_name in plugins['Auth Providers']:
         plugin = plugins['Auth Providers'][plugin_name]
 
     logger.info('Configuring Authentication Providers')
     for plugin_name in plugins['Auth Providers']:
         plugin = plugins['Auth Providers'][plugin_name]
-        plugin.configure(args)
+        plugin_changes = {}
+        if plugin.configure(args, plugin_changes) == False:
+            logger.info('Configuration of auth provider %s failed' % plugin_name)
+        changes['auth_provider'][plugin_name] = plugin_changes
+
+    # Save any changes that were made
+    install_changes = os.path.join(instance_conf, 'install_changes')
+    changes = json.dumps(changes)
+    with open(install_changes, 'w+') as f:
+        f.write(changes)
 
     # Fixup permissions so only the ipsilon user can read these files
     files.fix_user_dirs(instance_conf, opts['system_user'])
 
     # Fixup permissions so only the ipsilon user can read these files
     files.fix_user_dirs(instance_conf, opts['system_user'])
@@ -175,9 +225,68 @@ def install(plugins, args):
     except Exception:  # pylint: disable=broad-except
         pass
 
     except Exception:  # pylint: disable=broad-except
         pass
 
+
 def uninstall(plugins, args):
     logger.info('Uninstallation initiated')
 def uninstall(plugins, args):
     logger.info('Uninstallation initiated')
-    raise Exception('Not Implemented')
+    instance_conf = os.path.join(CONFDIR, args['instance'])
+
+    httpd_conf = os.path.join(HTTPDCONFD,
+                              'ipsilon-%s.conf' % args['instance'])
+    data_dir = os.path.join(DATADIR, args['instance'])
+
+    if not os.path.exists(instance_conf):
+        raise Exception('Could not find instance %s configuration'
+                        % args['instance'])
+    if not os.path.exists(httpd_conf):
+        raise Exception('Could not find instance %s httpd configuration'
+                        % args['instance'])
+    if not args['yes']:
+        sure = raw_input(('Are you certain you want to erase instance %s ' +
+                          '[yes/NO]: ')
+                         % args['instance'])
+        if sure != 'yes':
+            raise Exception('Aborting')
+
+    # Get the details of what we changed during installation
+    install_changes = os.path.join(instance_conf, 'install_changes')
+    with open(install_changes, 'r') as f:
+        changes = json.loads(f.read())
+
+    logger.info('Removing environment helpers')
+    for plugin_name in plugins['Environment Helpers']:
+        plugin = plugins['Environment Helpers'][plugin_name]
+        plugin_changes = changes['env_helper'].get(plugin_name, {})
+        if plugin.unconfigure(args, plugin_changes) == False:
+            logger.info('Removal of environment helper %s failed' % plugin_name)
+
+    logger.info('Removing login managers')
+    for plugin_name in plugins['Login Managers']:
+        plugin = plugins['Login Managers'][plugin_name]
+        plugin_changes = changes['login_manager'].get(plugin_name, {})
+        if plugin.unconfigure(args, plugin_changes) == False:
+            logger.info('Removal of login manager %s failed' % plugin_name)
+
+    logger.info('Removing Info providers')
+    for plugin_name in plugins['Info Provider']:
+        plugin = plugins['Info Provider'][plugin_name]
+        plugin_changes = changes['info_provider'].get(plugin_name, {})
+        if plugin.unconfigure(args, plugin_changes) == False:
+            logger.info('Removal of info provider %s failed' % plugin_name)
+
+    logger.info('Removing Authentication Providers')
+    for plugin_name in plugins['Auth Providers']:
+        plugin = plugins['Auth Providers'][plugin_name]
+        plugin_changes = changes['auth_provider'].get(plugin_name, {})
+        if plugin.unconfigure(args, plugin_changes) == False:
+            logger.info('Removal of auth provider %s failed' % plugin_name)
+
+    logger.info('Removing httpd configuration')
+    os.remove(httpd_conf)
+    logger.info('Erasing instance configuration')
+    shutil.rmtree(instance_conf)
+    logger.info('Erasing instance data')
+    shutil.rmtree(data_dir)
+    logger.info('Uninstalled instance %s' % args['instance'])
 
 
 def find_plugins():
 
 
 def find_plugins():
@@ -191,7 +300,7 @@ def find_plugins():
 
 
 def parse_config_profile(args):
 
 
 def parse_config_profile(args):
-    config = ConfigParser.ConfigParser()
+    config = ConfigParser.RawConfigParser()
     files = config.read(args['config_profile'])
     if len(files) == 0:
         raise ConfigurationError('Config Profile file %s not found!' %
     files = config.read(args['config_profile'])
     if len(files) == 0:
         raise ConfigurationError('Config Profile file %s not found!' %
@@ -231,14 +340,28 @@ def parse_args(plugins):
                         help="User account used to run the server")
     parser.add_argument('--admin-user', default='admin',
                         help="User account that is assigned admin privileges")
                         help="User account used to run the server")
     parser.add_argument('--admin-user', default='admin',
                         help="User account that is assigned admin privileges")
+    parser.add_argument('--database-url',
+                        default='sqlite:///%(datadir)s/%(dbname)s.sqlite',
+                        help="The (templatized) database URL to use")
     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
                         help="Turn on all security checks")
     parser.add_argument('--config-profile', default=None,
     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
                         help="Turn on all security checks")
     parser.add_argument('--config-profile', default=None,
-                        help="File containing install options")
+                        help=argparse.SUPPRESS)
     parser.add_argument('--server-debugging', action='store_true',
     parser.add_argument('--server-debugging', action='store_true',
-                        help="Uninstall the server and all data")
+                        help="Enable debugging")
     parser.add_argument('--uninstall', action='store_true',
                         help="Uninstall the server and all data")
     parser.add_argument('--uninstall', action='store_true',
                         help="Uninstall the server and all data")
+    parser.add_argument('--yes', action='store_true',
+                        help="Always answer yes")
+    parser.add_argument('--admin-dburi',
+                        help='Configuration database URI (override template)')
+    parser.add_argument('--users-dburi',
+                        help='User configuration database URI (override '
+                             'template)')
+    parser.add_argument('--transaction-dburi',
+                        help='Transaction database URI (override template)')
+    parser.add_argument('--samlsessions-dburi',
+                        help='SAML 2 sessions database URI (override template)')
 
     lms = []
 
 
     lms = []
 
@@ -258,9 +381,17 @@ def parse_args(plugins):
     if not args['hostname']:
         args['hostname'] = socket.getfqdn()
 
     if not args['hostname']:
         args['hostname'] = socket.getfqdn()
 
+    if args['uninstall']:
+        return args
+
     if len(args['hostname'].split('.')) < 2:
         raise ConfigurationError('Hostname: %s is not a FQDN')
 
     if len(args['hostname'].split('.')) < 2:
         raise ConfigurationError('Hostname: %s is not a FQDN')
 
+    for plugin_group in plugins:
+        for plugin_name in plugins[plugin_group]:
+            plugin = plugins[plugin_group][plugin_name]
+            plugin.validate_args(args)
+
     try:
         pwd.getpwnam(args['system_user'])
     except KeyError:
     try:
         pwd.getpwnam(args['system_user'])
     except KeyError:
@@ -275,12 +406,7 @@ def parse_args(plugins):
         args['lm_order'] = args['lm_order'].split(',')
 
     if len(args['lm_order']) == 0:
         args['lm_order'] = args['lm_order'].split(',')
 
     if len(args['lm_order']) == 0:
-        #force the basic pam provider if nothing else is selected
-        if 'pam' not in args:
-            parser.print_help()
-            sys.exit(-1)
-        args['lm_order'] = ['pam']
-        args['pam'] = 'yes'
+        sys.exit('No login plugins are enabled.')
 
     #FIXME: check instance is only alphanums
 
 
     #FIXME: check instance is only alphanums
 
@@ -296,27 +422,33 @@ if __name__ == '__main__':
 
         logger.setLevel(logging.DEBUG)
 
 
         logger.setLevel(logging.DEBUG)
 
-        logger.info('Intallation arguments:')
+        logger.debug('Installation arguments:')
         for k in sorted(opts.iterkeys()):
         for k in sorted(opts.iterkeys()):
-            logger.info('%s: %s', k, opts[k])
+            logger.debug('%s: %s', k, opts[k])
 
         if 'uninstall' in opts and opts['uninstall'] is True:
 
         if 'uninstall' in opts and opts['uninstall'] is True:
+            if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
+                logger.info('Instance %s could not be found' % opts['instance'])
+                sys.exit(0)
             uninstall(fplugins, opts)
             uninstall(fplugins, opts)
-
-        install(fplugins, opts)
+        else:
+            install(fplugins, opts)
     except Exception, e:  # pylint: disable=broad-except
         logger.exception(e)
         if 'uninstall' in opts and opts['uninstall'] is True:
     except Exception, e:  # pylint: disable=broad-except
         logger.exception(e)
         if 'uninstall' in opts and opts['uninstall'] is True:
-            print 'Uninstallation aborted.'
+            logger.info('Uninstallation aborted.')
         else:
         else:
-            print 'Installation aborted.'
-        print 'See log file %s for details' % LOGFILE
+            logger.info('Installation aborted.')
+        logger.info('See log file %s for details' % LOGFILE)
+        out = 1
+    except SystemExit:
         out = 1
         out = 1
+        raise
     finally:
         if out == 0:
             if 'uninstall' in opts and opts['uninstall'] is True:
     finally:
         if out == 0:
             if 'uninstall' in opts and opts['uninstall'] is True:
-                print 'Uninstallation complete.'
+                logger.info('Uninstallation complete.')
             else:
             else:
-                print 'Installation complete.'
-                print 'Please restart HTTPD to enable the IdP instance.'
+                logger.info('Installation complete.')
+                logger.info('Please restart HTTPD to enable the IdP instance.')
     sys.exit(out)
     sys.exit(out)