SP Portal administrative interface
[cascardo/ipsilon.git] / ipsilon / install / ipsilon-client-install
index 02b4d5f..d72d195 100755 (executable)
@@ -1,21 +1,6 @@
 #!/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.tools.saml2metadata import Metadata
 from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP
@@ -34,6 +19,7 @@ import requests
 import shutil
 import socket
 import sys
+import base64
 
 
 HTTPDCONFD = '/etc/httpd/conf.d'
@@ -88,6 +74,8 @@ def saml2():
     path = None
     if not args['saml_no_httpd']:
         path = os.path.join(SAML2_HTTPDIR, args['hostname'])
+        if os.path.exists(path):
+            raise Exception('Service Provider is already configured')
         os.makedirs(path, 0750)
     else:
         path = os.getcwd()
@@ -104,6 +92,7 @@ def saml2():
     url_sp = url + args['saml_sp']
     url_logout = url + args['saml_sp_logout']
     url_post = url + args['saml_sp_post']
+    url_paos = url + args['saml_sp_paos']
 
     # Generate metadata
     m = Metadata('sp')
@@ -112,7 +101,12 @@ def saml2():
     m.set_entity_id(url_sp)
     m.add_certs(c)
     m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout)
-    m.add_service(SAML2_SERVICE_MAP['response-post'], url_post, index="0")
+    if not args['no_saml_soap_logout']:
+        m.add_service(SAML2_SERVICE_MAP['slo-soap'], url_logout)
+    m.add_service(SAML2_SERVICE_MAP['response-post'], url_post,
+                  index="0", isDefault="true")
+    m.add_service(SAML2_SERVICE_MAP['response-paos'], url_paos,
+                  index="1")
     m.add_allowed_name_format(SAML2_NAMEID_MAP[args['saml_nameid']])
     sp_metafile = os.path.join(path, 'metadata.xml')
     m.output(sp_metafile)
@@ -130,6 +124,9 @@ def saml2():
                     logger.error("Failed to read password file!\n" +
                                  "Error: [%s]" % e)
                     raise
+        elif ('IPSILON_ADMIN_PASSWORD' in os.environ) and \
+             (os.environ['IPSILON_ADMIN_PASSWORD']):
+            admin_password = os.environ['IPSILON_ADMIN_PASSWORD']
         else:
             admin_password = getpass.getpass('%s password: ' %
                                              args['admin_user'])
@@ -145,11 +142,23 @@ def saml2():
                          "Error: [%s]" % e)
             raise
 
+        sp_image = None
+        if args['saml_sp_image']:
+            try:
+                # FIXME: limit size
+                with open(args['saml_sp_image']) as f:
+                    sp_image = f.read()
+                sp_image = base64.b64encode(sp_image)
+            except Exception as e:  # pylint: disable=broad-except
+                logger.error("Failed to read SP Image file!\n" +
+                             "Error: [%s]" % e)
+
         # Register the SP
         try:
             saml2_register_sp(args['saml_idp_url'], args['admin_user'],
                               admin_password, args['saml_sp_name'],
-                              sp_metadata)
+                              sp_metadata, args['saml_sp_description'],
+                              args['saml_sp_visible'], sp_image)
         except Exception as e:  # pylint: disable=broad-except
             logger.error("Failed to register SP with IDP!\n" +
                          "Error: [%s]" % e)
@@ -215,7 +224,8 @@ def saml2():
                     ' configure your Service Provider')
 
 
-def saml2_register_sp(url, user, password, sp_name, sp_metadata):
+def saml2_register_sp(url, user, password, sp_name, sp_metadata,
+                      sp_description, sp_visible, sp_image):
     s = requests.Session()
 
     # Authenticate to the IdP
@@ -235,7 +245,15 @@ def saml2_register_sp(url, user, password, sp_name, sp_metadata):
     sp_url = '%s/rest/providers/saml2/SPS/%s' % (url.rstrip('/'), sp_name)
     sp_headers = {'Content-type': 'application/x-www-form-urlencoded',
                   'Referer': sp_url}
-    sp_data = urlencode({'metadata': sp_metadata})
+    sp_data = {'metadata': sp_metadata}
+    if sp_description:
+        sp_data['description'] = sp_description
+    if sp_visible:
+        sp_data['visible'] = sp_visible
+    if sp_image:
+        if sp_image:
+            sp_data['imagefile'] = sp_image
+    sp_data = urlencode(sp_data)
 
     r = s.post(sp_url, headers=sp_headers, data=sp_data)
     if r.status_code != 201:
@@ -249,14 +267,18 @@ def install():
 
 
 def saml2_uninstall():
-    try:
-        shutil.rmtree(os.path.join(SAML2_HTTPDIR, args['hostname']))
-    except Exception, e:  # pylint: disable=broad-except
-        log_exception(e)
-    try:
-        os.remove(SAML2_CONFFILE)
-    except Exception, e:  # pylint: disable=broad-except
-        log_exception(e)
+    path = os.path.join(SAML2_HTTPDIR, args['hostname'])
+    if os.path.exists(path):
+        try:
+            shutil.rmtree(path)
+        except Exception, e:  # pylint: disable=broad-except
+            log_exception(e)
+
+    if os.path.exists(SAML2_CONFFILE):
+        try:
+            os.remove(SAML2_CONFFILE)
+        except Exception, e:  # pylint: disable=broad-except
+            log_exception(e)
 
 
 def uninstall():
@@ -291,7 +313,7 @@ def parse_config_profile(args):
             if g in globals():
                 globals()[g] = val
             else:
-                for k in globals().keys():
+                for k in globals():
                     if k.lower() == g.lower():
                         globals()[k] = val
                         break
@@ -342,10 +364,15 @@ def parse_args():
                         help="Where saml2 authentication is enforced")
     parser.add_argument('--saml-sp', default='/saml2',
                         help="Where saml communication happens")
-    parser.add_argument('--saml-sp-logout', default='/saml2/logout',
+    parser.add_argument('--saml-sp-logout', default=None,
                         help="Single Logout URL")
-    parser.add_argument('--saml-sp-post', default='/saml2/postResponse',
+    parser.add_argument('--saml-sp-post', default=None,
                         help="Post response URL")
+    parser.add_argument('--saml-sp-paos', default=None,
+                        help="PAOS response URL, used for ECP")
+    parser.add_argument('--no-saml-soap-logout', action='store_true',
+                        default=False,
+                        help="Disable Single Logout over SOAP")
     parser.add_argument('--saml-secure-setup', action='store_true',
                         default=True, help="Turn on all security checks")
     parser.add_argument('--saml-nameid', default='unspecified',
@@ -353,10 +380,18 @@ def parse_args():
                         help="SAML NameID format to use")
     parser.add_argument('--saml-sp-name', default=None,
                         help="The SP name to register with the IdP")
+    parser.add_argument('--saml-sp-description', default=None,
+                        help="The description of the SP to display on the " +
+                        "portal")
+    parser.add_argument('--saml-sp-visible', action='store_false',
+                        default=True,
+                        help="The SP is visible in the portal")
+    parser.add_argument('--saml-sp-image', default=None,
+                        help="Image to display for this SP on the portal")
     parser.add_argument('--debug', action='store_true', default=False,
                         help="Turn on script debugging")
     parser.add_argument('--config-profile', default=None,
-                        help="File containing install options")
+                        help=argparse.SUPPRESS)
     parser.add_argument('--uninstall', action='store_true',
                         help="Uninstall the server and all data")
 
@@ -373,9 +408,9 @@ def parse_args():
 
     # Validate that all path options begin with '/'
     path_args = ['saml_base', 'saml_auth', 'saml_sp', 'saml_sp_logout',
-                 'saml_sp_post']
+                 'saml_sp_post', 'saml_sp_paos']
     for path_arg in path_args:
-        if not args[path_arg].startswith('/'):
+        if args[path_arg] is not None and not args[path_arg].startswith('/'):
             raise ValueError('--%s must begin with a / character.' %
                              path_arg.replace('_', '-'))
 
@@ -384,11 +419,17 @@ def parse_args():
     if not args['saml_sp'].startswith(args['saml_base']):
         raise ValueError('--saml-sp must be a subpath of --saml-base.')
 
-    # The saml_sp_logout and saml_sp_post settings must be subpaths
-    # of saml_sp (the mellon endpoint).
-    path_args = ['saml_sp_logout', 'saml_sp_post']
-    for path_arg in path_args:
-        if not args[path_arg].startswith(args['saml_sp']):
+    # The saml_sp_logout, saml_sp_post and saml_sp_paos settings must
+    # be subpaths of saml_sp (the mellon endpoint).
+    path_args = {'saml_sp_logout': 'logout',
+                 'saml_sp_post': 'postResponse',
+                 'saml_sp_paos': 'paosResponse'}
+    for path_arg, default_path in path_args.items():
+        if args[path_arg] is None:
+            args[path_arg] = '%s/%s' % (args['saml_sp'].rstrip('/'),
+                                        default_path)
+
+        elif not args[path_arg].startswith(args['saml_sp']):
             raise ValueError('--%s must be a subpath of --saml-sp' %
                              path_arg.replace('_', '-'))
 
@@ -421,14 +462,14 @@ if __name__ == '__main__':
     except Exception, e:  # pylint: disable=broad-except
         log_exception(e)
         if 'uninstall' in args and args['uninstall'] is True:
-            print 'Uninstallation aborted.'
+            logging.info('Uninstallation aborted.')
         else:
-            print 'Installation aborted.'
+            logging.info('Installation aborted.')
         out = 1
     finally:
         if out == 0:
             if 'uninstall' in args and args['uninstall'] is True:
-                print 'Uninstallation complete.'
+                logging.info('Uninstallation complete.')
             else:
-                print 'Installation complete.'
+                logging.info('Installation complete.')
     sys.exit(out)