summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimo Sorce <simo@redhat.com>2014-04-05 13:23:02 -0400
committerSimo Sorce <simo@redhat.com>2014-04-11 18:02:07 -0400
commita0374da67060c6e69ff6f1c2d25d2df357c25751 (patch)
tree23231072e26ab1a212300086daf281b8109d0938
parent2bed65a8810b7f81ec2be93275fa06f4da52e56f (diff)
downloadipsilon-a0374da67060c6e69ff6f1c2d25d2df357c25751.tar.gz
ipsilon-a0374da67060c6e69ff6f1c2d25d2df357c25751.tar.xz
ipsilon-a0374da67060c6e69ff6f1c2d25d2df357c25751.zip
Add basic installation script with saml support
Generates (self signed) certificates and a metdata.xml file. Optionally configures an Apache Httpd server. If the admin does not configure a specific application at install time a default landing page is made available to be able to test that the SP configuration works. Uninstall removes all certificates and metadata file and is irreversible.
-rwxr-xr-xipsilon/install/ipsilon-client-install259
-rwxr-xr-xsetup.py7
-rw-r--r--templates/install/saml2/sp.conf28
-rw-r--r--ui/saml2sp/index.html8
4 files changed, 301 insertions, 1 deletions
diff --git a/ipsilon/install/ipsilon-client-install b/ipsilon/install/ipsilon-client-install
new file mode 100755
index 0000000..8802ea1
--- /dev/null
+++ b/ipsilon/install/ipsilon-client-install
@@ -0,0 +1,259 @@
+#!/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/>.
+
+from ipsilon.tools.saml2metadata import Metadata
+from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP
+from ipsilon.tools.saml2metadata import SAML2_SERVICE_MAP
+from ipsilon.tools.certs import Certificate
+from string import Template
+import argparse
+import logging
+import os
+import pwd
+import requests
+import shutil
+import socket
+import sys
+
+
+HTTPDCONFD = '/etc/httpd/conf.d'
+SAML2_TEMPLATE = '/usr/share/ipsilon/templates/install/saml2/sp.conf'
+SAML2_CONFFILE = '/etc/httpd/conf.d/ipsilon-saml.conf'
+SAML2_HTTPDIR = '/etc/httpd/saml2'
+SAML2_PROTECTED = '/saml2protected'
+
+#Installation arguments
+args = dict()
+
+# Regular logging
+logger = logging.getLogger()
+
+
+def openlogs():
+ global logger # pylint: disable=W0603
+ logger = logging.getLogger()
+ lh = logging.StreamHandler(sys.stderr)
+ logger.addHandler(lh)
+
+
+def saml2():
+ logger.info('Installing SAML2 Service Provider')
+
+ if args['saml_idp_metadata'] is None:
+ #TODO: detect via SRV records ?
+ raise ValueError('An IDP metadata file/url is required.')
+
+ idpmeta = None
+
+ try:
+ if os.path.exists(args['saml_idp_metadata']):
+ with open(args['saml_idp_metadata']) as f:
+ idpmeta = f.read()
+ elif args['saml_idp_metadata'].startswith('file://'):
+ with open(args['saml_idp_metadata'][7:]) as f:
+ idpmeta = f.read()
+ else:
+ r = requests.get(args['saml_idp_metadata'])
+ r.raise_for_status()
+ idpmeta = r.content
+ except Exception, e: # pylint: disable=broad-except
+ logger.error("Failed to retrieve IDP Metadata file!\n" +
+ "Error: [%s]" % repr(e))
+ raise
+
+ path = None
+ if args['saml_httpd']:
+ path = os.path.join(SAML2_HTTPDIR, args['hostname'])
+ os.makedirs(path, 0750)
+ else:
+ path = os.getcwd()
+
+ url = 'https://' + args['hostname']
+ url_sp = url + args['saml_sp']
+ url_logout = url + args['saml_sp_logout']
+ url_post = url + args['saml_sp_post']
+
+ # Generate metadata
+ m = Metadata('sp')
+ c = Certificate(path)
+ c.generate('certificate', args['hostname'])
+ 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")
+ sp_metafile = os.path.join(path, 'metadata.xml')
+ m.output(sp_metafile)
+
+ if args['saml_httpd']:
+ idp_metafile = os.path.join(path, 'idp-metadata.xml')
+ with open(idp_metafile, 'w+') as f:
+ f.write(idpmeta)
+
+ saml_protect = 'auth'
+ saml_auth=''
+ if args['saml_base'] != args['saml_auth']:
+ saml_protect = 'info'
+ saml_auth = '<Location %s>\n' \
+ ' MellonEnable "auth"\n' \
+ '</Location>\n' % args['saml_auth']
+
+ psp = '# '
+ if args['saml_auth'] == SAML2_PROTECTED:
+ # default location, enable the default page
+ psp = ''
+
+ with open(SAML2_TEMPLATE) as f:
+ template = f.read()
+ t = Template(template)
+ hunk = t.substitute(saml_base=args['saml_base'],
+ saml_protect=saml_protect,
+ saml_sp_key=c.key,
+ saml_sp_cert=c.cert,
+ saml_sp_meta=sp_metafile,
+ saml_idp_meta=idp_metafile,
+ saml_sp=args['saml_sp'],
+ saml_auth=saml_auth, sp=psp)
+
+ with open(SAML2_CONFFILE, 'w+') as f:
+ f.write(hunk)
+
+ pw = pwd.getpwnam(args['httpd_user'])
+ for root, dirs, files in os.walk(SAML2_HTTPDIR):
+ for name in dirs:
+ target = os.path.join(root, name)
+ os.chown(target, pw.pw_uid, pw.pw_gid)
+ os.chmod(target, 0700)
+ for name in files:
+ target = os.path.join(root, name)
+ os.chown(target, pw.pw_uid, pw.pw_gid)
+ os.chmod(target, 0600)
+
+ logger.info('SAML Service Provider configured.')
+ logger.info('You should be able to restart the HTTPD server and' +
+ ' then access it at %s%s' % (url, args['saml_auth']))
+ else:
+ logger.info('SAML Service Provider configuration ready.')
+ logger.info('Use the certificate, key and metadata.xml files to' +
+ ' configure your Service Provider')
+
+
+def install():
+ if args['saml']:
+ saml2()
+
+
+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)
+
+
+def uninstall():
+ logger.info('Uninstalling Service Provider')
+ #FXIME: ask confirmation
+ saml2_uninstall()
+ logger.info('Uninstalled SAML2 data')
+
+
+def log_exception(e):
+ if 'debug' in args and args['debug']:
+ logger.exception(e)
+ else:
+ logger.error(e)
+
+
+def parse_args():
+ global args
+
+ fc = argparse.ArgumentDefaultsHelpFormatter
+ parser = argparse.ArgumentParser(description='Client Install Options',
+ formatter_class=fc)
+ parser.add_argument('--version',
+ action='version', version='%(prog)s 0.1')
+ parser.add_argument('--hostname', default=socket.getfqdn(),
+ help="Machine's fully qualified host name")
+ parser.add_argument('--admin-user', default='admin',
+ help="Account allowed to create a SP")
+ parser.add_argument('--httpd-user', default='apache',
+ help="Web server account used to read certs")
+ parser.add_argument('--saml', action='store_true', default=False,
+ help="Whether to install a saml2 SP")
+ parser.add_argument('--saml-idp-metadata', default=None,
+ help="A URL pointing at the IDP Metadata (FILE or HTTP)")
+ parser.add_argument('--saml-httpd', action='store_true', default=False,
+ help="Automatically configure httpd")
+ parser.add_argument('--saml-base', default='/',
+ help="Where saml2 authdata is available")
+ parser.add_argument('--saml-auth', default=SAML2_PROTECTED,
+ 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',
+ help="Single Logout URL")
+ parser.add_argument('--saml-sp-post', default='/saml2/postResponse',
+ help="Post response URL")
+ parser.add_argument('--debug', action='store_true', default=False,
+ help="Turn on script debugging")
+ parser.add_argument('--uninstall', action='store_true',
+ help="Uninstall the server and all data")
+
+ args = vars(parser.parse_args())
+
+ if len(args['hostname'].split('.')) < 2:
+ raise ValueError('Hostname: %s is not a FQDN.')
+
+ # At least one on this list needs to be specified or we do nothing
+ sp_list = ['saml']
+ present = False
+ for sp in sp_list:
+ if args[sp]:
+ present = True
+ if not present and not args['uninstall']:
+ raise ValueError('Nothing to install, please select a Service type.')
+
+
+if __name__ == '__main__':
+ out = 0
+ openlogs()
+ try:
+ parse_args()
+
+ if 'uninstall' in args and args['uninstall'] is True:
+ uninstall()
+
+ install()
+ except Exception, e: # pylint: disable=broad-except
+ log_exception(e)
+ if 'uninstall' in args and args['uninstall'] is True:
+ print 'Uninstallation aborted.'
+ else:
+ print 'Installation aborted.'
+ out = 1
+ finally:
+ if out == 0:
+ if 'uninstall' in args and args['uninstall'] is True:
+ print 'Uninstallation complete.'
+ else:
+ print 'Installation complete.'
+ sys.exit(out)
diff --git a/setup.py b/setup.py
index 3de7faa..8b0b042 100755
--- a/setup.py
+++ b/setup.py
@@ -36,14 +36,19 @@ setup(
(DATA+'ui/css', glob('ui/css/*.css')),
(DATA+'ui/img', glob('ui/img/*')),
(DATA+'ui/js', glob('ui/js/*.js')),
+ (DATA+'ui/saml2sp', glob('ui/saml2sp/*.html')),
(DATA+'templates', glob('templates/*.html')),
(DATA+'templates/admin', glob('templates/admin/*.html')),
(DATA+'templates/login', glob('templates/login/*.html')),
(DATA+'templates/saml2', glob('templates/saml2/*.html')),
(DATA+'templates/install', glob('templates/install/*.conf')),
+ (DATA+'templates/install/saml2',
+ glob('templates/install/saml2/*.conf')),
(DATA+'templates/admin/providers',
glob('templates/admin/providers/*.html')),
],
- scripts = ['ipsilon/ipsilon', 'ipsilon/install/ipsilon-server-install']
+ scripts = ['ipsilon/ipsilon',
+ 'ipsilon/install/ipsilon-server-install',
+ 'ipsilon/install/ipsilon-client-install']
)
diff --git a/templates/install/saml2/sp.conf b/templates/install/saml2/sp.conf
new file mode 100644
index 0000000..57abdfd
--- /dev/null
+++ b/templates/install/saml2/sp.conf
@@ -0,0 +1,28 @@
+# This is a server-wide configuration that will add information from the Mellon
+# session to all requests under this path.
+<Location ${saml_base}>
+ MellonEnable "${saml_protect}"
+ MellonSPPrivateKeyFile "${saml_sp_key}"
+ MellonSPCertFile "${saml_sp_cert}"
+ MellonSPMetadataFile "${saml_sp_meta}"
+ MellonIdPMetadataFile "${saml_idp_meta}"
+ MellonEndpointPath ${saml_sp}
+ MellonVariable "saml-sesion-cookie"
+ # Comment out the next line if you want to allow logins on bare HTTP
+ MellonsecureCookie On
+ MellonUser "NAME_ID"
+ MellonIdP "IDP"
+ MellonSessionLength 3600
+ # MellonNoCookieErrorPage "https://idp.example.com/no-cookie-error.html"
+ # MellonPostDirectory "/var/lib/ipsilon/post_cache"
+ # MellonPostReplay On
+</Location>
+
+${saml_auth}
+
+${sp}Alias /saml2protected /usr/share/ipsilon/ui/saml2sp
+${sp}
+${sp}<Directory /usr/share/ipsilon/ui/saml2sp>
+${sp} SSLRequireSSL
+${sp} Require all granted
+${sp}</Directory>
diff --git a/ui/saml2sp/index.html b/ui/saml2sp/index.html
new file mode 100644
index 0000000..a7e6b66
--- /dev/null
+++ b/ui/saml2sp/index.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Service Provider Authenticated Page</title>
+ </head>
+ <body>
+ Congratulations, your SAML2 Service Provider is working.
+ </body>
+</html>