Add basic installation script with saml support
[cascardo/ipsilon.git] / ipsilon / install / ipsilon-client-install
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2014  Simo Sorce <simo@redhat.com>
4 #
5 # see file 'COPYING' for use and warranty information
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 from ipsilon.tools.saml2metadata import Metadata
21 from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP
22 from ipsilon.tools.saml2metadata import SAML2_SERVICE_MAP
23 from ipsilon.tools.certs import Certificate
24 from string import Template
25 import argparse
26 import logging
27 import os
28 import pwd
29 import requests
30 import shutil
31 import socket
32 import sys
33
34
35 HTTPDCONFD = '/etc/httpd/conf.d'
36 SAML2_TEMPLATE = '/usr/share/ipsilon/templates/install/saml2/sp.conf'
37 SAML2_CONFFILE = '/etc/httpd/conf.d/ipsilon-saml.conf'
38 SAML2_HTTPDIR = '/etc/httpd/saml2'
39 SAML2_PROTECTED = '/saml2protected'
40
41 #Installation arguments
42 args = dict()
43
44 # Regular logging
45 logger = logging.getLogger()
46
47
48 def openlogs():
49     global logger  # pylint: disable=W0603
50     logger = logging.getLogger()
51     lh = logging.StreamHandler(sys.stderr)
52     logger.addHandler(lh)
53
54
55 def saml2():
56     logger.info('Installing SAML2 Service Provider')
57
58     if args['saml_idp_metadata'] is None:
59         #TODO: detect via SRV records ?
60         raise ValueError('An IDP metadata file/url is required.')
61
62     idpmeta = None
63
64     try:
65         if os.path.exists(args['saml_idp_metadata']):
66             with open(args['saml_idp_metadata']) as f:
67                 idpmeta = f.read()
68         elif args['saml_idp_metadata'].startswith('file://'):
69             with open(args['saml_idp_metadata'][7:]) as f:
70                 idpmeta = f.read()
71         else:
72             r = requests.get(args['saml_idp_metadata'])
73             r.raise_for_status()
74             idpmeta = r.content
75     except Exception, e:  # pylint: disable=broad-except
76         logger.error("Failed to retrieve IDP Metadata file!\n" +
77                      "Error: [%s]" % repr(e))
78         raise
79
80     path = None
81     if args['saml_httpd']:
82         path = os.path.join(SAML2_HTTPDIR, args['hostname'])
83         os.makedirs(path, 0750)
84     else:
85         path = os.getcwd()
86
87     url = 'https://' + args['hostname']
88     url_sp = url + args['saml_sp']
89     url_logout = url + args['saml_sp_logout']
90     url_post = url + args['saml_sp_post']
91
92     # Generate metadata
93     m = Metadata('sp')
94     c = Certificate(path)
95     c.generate('certificate', args['hostname'])
96     m.set_entity_id(url_sp)
97     m.add_certs(c)
98     m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout)
99     m.add_service(SAML2_SERVICE_MAP['response-post'], url_post, index="0")
100     sp_metafile = os.path.join(path, 'metadata.xml')
101     m.output(sp_metafile)
102
103     if args['saml_httpd']:
104         idp_metafile = os.path.join(path, 'idp-metadata.xml')
105         with open(idp_metafile, 'w+') as f:
106             f.write(idpmeta)
107
108         saml_protect = 'auth'
109         saml_auth=''
110         if args['saml_base'] != args['saml_auth']:
111             saml_protect = 'info'
112             saml_auth = '<Location %s>\n' \
113                         '    MellonEnable "auth"\n' \
114                         '</Location>\n' % args['saml_auth']
115
116         psp = '# '
117         if args['saml_auth'] == SAML2_PROTECTED:
118             # default location, enable the default page
119             psp = ''
120
121         with open(SAML2_TEMPLATE) as f:
122             template = f.read()
123         t = Template(template)
124         hunk = t.substitute(saml_base=args['saml_base'],
125                             saml_protect=saml_protect,
126                             saml_sp_key=c.key,
127                             saml_sp_cert=c.cert,
128                             saml_sp_meta=sp_metafile,
129                             saml_idp_meta=idp_metafile,
130                             saml_sp=args['saml_sp'],
131                             saml_auth=saml_auth, sp=psp)
132
133         with open(SAML2_CONFFILE, 'w+') as f:
134             f.write(hunk)
135
136         pw = pwd.getpwnam(args['httpd_user'])
137         for root, dirs, files in os.walk(SAML2_HTTPDIR):
138             for name in dirs:
139                 target = os.path.join(root, name)
140                 os.chown(target, pw.pw_uid, pw.pw_gid)
141                 os.chmod(target, 0700)
142             for name in files:
143                 target = os.path.join(root, name)
144                 os.chown(target, pw.pw_uid, pw.pw_gid)
145                 os.chmod(target, 0600)
146
147         logger.info('SAML Service Provider configured.')
148         logger.info('You should be able to restart the HTTPD server and' +
149                     ' then access it at %s%s' % (url, args['saml_auth']))
150     else:
151         logger.info('SAML Service Provider configuration ready.')
152         logger.info('Use the certificate, key and metadata.xml files to' +
153                     ' configure your Service Provider')
154
155
156 def install():
157     if args['saml']:
158         saml2()
159
160
161 def saml2_uninstall():
162     try:
163         shutil.rmtree(os.path.join(SAML2_HTTPDIR, args['hostname']))
164     except Exception, e:  # pylint: disable=broad-except
165         log_exception(e)
166     try:
167         os.remove(SAML2_CONFFILE)
168     except Exception, e:  # pylint: disable=broad-except
169         log_exception(e)
170
171
172 def uninstall():
173     logger.info('Uninstalling Service Provider')
174     #FXIME: ask confirmation
175     saml2_uninstall()
176     logger.info('Uninstalled SAML2 data')
177
178
179 def log_exception(e):
180     if 'debug' in args and args['debug']:
181         logger.exception(e)
182     else:
183         logger.error(e)
184
185
186 def parse_args():
187     global args
188
189     fc = argparse.ArgumentDefaultsHelpFormatter
190     parser = argparse.ArgumentParser(description='Client Install Options',
191                                      formatter_class=fc)
192     parser.add_argument('--version',
193                         action='version', version='%(prog)s 0.1')
194     parser.add_argument('--hostname', default=socket.getfqdn(),
195                         help="Machine's fully qualified host name")
196     parser.add_argument('--admin-user', default='admin',
197                         help="Account allowed to create a SP")
198     parser.add_argument('--httpd-user', default='apache',
199                         help="Web server account used to read certs")
200     parser.add_argument('--saml', action='store_true', default=False,
201                         help="Whether to install a saml2 SP")
202     parser.add_argument('--saml-idp-metadata', default=None,
203                         help="A URL pointing at the IDP Metadata (FILE or HTTP)")
204     parser.add_argument('--saml-httpd', action='store_true', default=False,
205                         help="Automatically configure httpd")
206     parser.add_argument('--saml-base', default='/',
207                         help="Where saml2 authdata is available")
208     parser.add_argument('--saml-auth', default=SAML2_PROTECTED,
209                         help="Where saml2 authentication is enforced")
210     parser.add_argument('--saml-sp', default='/saml2',
211                         help="Where saml communication happens")
212     parser.add_argument('--saml-sp-logout', default='/saml2/logout',
213                         help="Single Logout URL")
214     parser.add_argument('--saml-sp-post', default='/saml2/postResponse',
215                         help="Post response URL")
216     parser.add_argument('--debug', action='store_true', default=False,
217                         help="Turn on script debugging")
218     parser.add_argument('--uninstall', action='store_true',
219                         help="Uninstall the server and all data")
220
221     args = vars(parser.parse_args())
222
223     if len(args['hostname'].split('.')) < 2:
224         raise ValueError('Hostname: %s is not a FQDN.')
225
226     # At least one on this list needs to be specified or we do nothing
227     sp_list = ['saml']
228     present = False
229     for sp in sp_list:
230         if args[sp]:
231             present = True
232     if not present and not args['uninstall']:
233         raise ValueError('Nothing to install, please select a Service type.')
234
235
236 if __name__ == '__main__':
237     out = 0
238     openlogs()
239     try:
240         parse_args()
241
242         if 'uninstall' in args and args['uninstall'] is True:
243             uninstall()
244
245         install()
246     except Exception, e:  # pylint: disable=broad-except
247         log_exception(e)
248         if 'uninstall' in args and args['uninstall'] is True:
249             print 'Uninstallation aborted.'
250         else:
251             print 'Installation aborted.'
252         out = 1
253     finally:
254         if out == 0:
255             if 'uninstall' in args and args['uninstall'] is True:
256                 print 'Uninstallation complete.'
257             else:
258                 print 'Installation complete.'
259     sys.exit(out)