Require SSL on SP when using --saml-secure-setup
[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 ipsilon.tools import files
25 import argparse
26 import ConfigParser
27 import logging
28 import os
29 import pwd
30 import requests
31 import shutil
32 import socket
33 import sys
34
35
36 HTTPDCONFD = '/etc/httpd/conf.d'
37 SAML2_TEMPLATE = '/usr/share/ipsilon/templates/install/saml2/sp.conf'
38 SAML2_CONFFILE = '/etc/httpd/conf.d/ipsilon-saml.conf'
39 SAML2_HTTPDIR = '/etc/httpd/saml2'
40 SAML2_PROTECTED = '/saml2protected'
41
42 #Installation arguments
43 args = dict()
44
45 # Regular logging
46 logger = logging.getLogger()
47
48
49 def openlogs():
50     global logger  # pylint: disable=W0603
51     logger = logging.getLogger()
52     lh = logging.StreamHandler(sys.stderr)
53     logger.addHandler(lh)
54
55
56 def saml2():
57     logger.info('Installing SAML2 Service Provider')
58
59     if args['saml_idp_metadata'] is None:
60         #TODO: detect via SRV records ?
61         raise ValueError('An IDP metadata file/url is required.')
62
63     idpmeta = None
64
65     try:
66         if os.path.exists(args['saml_idp_metadata']):
67             with open(args['saml_idp_metadata']) as f:
68                 idpmeta = f.read()
69         elif args['saml_idp_metadata'].startswith('file://'):
70             with open(args['saml_idp_metadata'][7:]) as f:
71                 idpmeta = f.read()
72         else:
73             r = requests.get(args['saml_idp_metadata'])
74             r.raise_for_status()
75             idpmeta = r.content
76     except Exception, e:  # pylint: disable=broad-except
77         logger.error("Failed to retrieve IDP Metadata file!\n" +
78                      "Error: [%s]" % repr(e))
79         raise
80
81     path = None
82     if not args['saml_no_httpd']:
83         path = os.path.join(SAML2_HTTPDIR, args['hostname'])
84         os.makedirs(path, 0750)
85     else:
86         path = os.getcwd()
87
88     proto = 'https'
89     if not args['saml_secure_setup']:
90         proto = 'http'
91     url = '%s://%s' % (proto, args['hostname'])
92     url_sp = url + args['saml_sp']
93     url_logout = url + args['saml_sp_logout']
94     url_post = url + args['saml_sp_post']
95
96     # Generate metadata
97     m = Metadata('sp')
98     c = Certificate(path)
99     c.generate('certificate', args['hostname'])
100     m.set_entity_id(url_sp)
101     m.add_certs(c)
102     m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout)
103     m.add_service(SAML2_SERVICE_MAP['response-post'], url_post, index="0")
104     sp_metafile = os.path.join(path, 'metadata.xml')
105     m.output(sp_metafile)
106
107     if not args['saml_no_httpd']:
108         idp_metafile = os.path.join(path, 'idp-metadata.xml')
109         with open(idp_metafile, 'w+') as f:
110             f.write(idpmeta)
111
112         saml_protect = 'auth'
113         saml_auth=''
114         if args['saml_base'] != args['saml_auth']:
115             saml_protect = 'info'
116             saml_auth = '<Location %s>\n' \
117                         '    MellonEnable "auth"\n' \
118                         '</Location>\n' % args['saml_auth']
119
120         psp = '# '
121         if args['saml_auth'] == SAML2_PROTECTED:
122             # default location, enable the default page
123             psp = ''
124
125         saml_secure = 'Off'
126         ssl_require = '#'
127         ssl_rewrite = '#'
128         if args['saml_secure_setup']:
129             saml_secure = 'On'
130             ssl_require = ''
131             ssl_rewrite = ''
132
133         samlopts = {'saml_base': args['saml_base'],
134                     'saml_protect': saml_protect,
135                     'saml_sp_key': c.key,
136                     'saml_sp_cert': c.cert,
137                     'saml_sp_meta': sp_metafile,
138                     'saml_idp_meta': idp_metafile,
139                     'saml_sp': args['saml_sp'],
140                     'saml_secure_on': saml_secure,
141                     'saml_auth': saml_auth,
142                     'ssl_require': ssl_require,
143                     'ssl_rewrite': ssl_rewrite,
144                     'sp_hostname': args['hostname'],
145                     'sp': psp}
146         files.write_from_template(SAML2_CONFFILE, SAML2_TEMPLATE, samlopts)
147
148         files.fix_user_dirs(SAML2_HTTPDIR, args['httpd_user'])
149
150         logger.info('SAML Service Provider configured.')
151         logger.info('You should be able to restart the HTTPD server and' +
152                     ' then access it at %s%s' % (url, args['saml_auth']))
153     else:
154         logger.info('SAML Service Provider configuration ready.')
155         logger.info('Use the certificate, key and metadata.xml files to' +
156                     ' configure your Service Provider')
157
158
159 def install():
160     if args['saml']:
161         saml2()
162
163
164 def saml2_uninstall():
165     try:
166         shutil.rmtree(os.path.join(SAML2_HTTPDIR, args['hostname']))
167     except Exception, e:  # pylint: disable=broad-except
168         log_exception(e)
169     try:
170         os.remove(SAML2_CONFFILE)
171     except Exception, e:  # pylint: disable=broad-except
172         log_exception(e)
173
174
175 def uninstall():
176     logger.info('Uninstalling Service Provider')
177     #FXIME: ask confirmation
178     saml2_uninstall()
179     logger.info('Uninstalled SAML2 data')
180
181
182 def log_exception(e):
183     if 'debug' in args and args['debug']:
184         logger.exception(e)
185     else:
186         logger.error(e)
187
188
189 def parse_config_profile(args):
190     config = ConfigParser.ConfigParser()
191     files = config.read(args['config_profile'])
192     if len(files) == 0:
193         raise ConfigurationError('Config Profile file %s not found!' %
194                                  args['config_profile'])
195
196     if 'globals' in config.sections():
197         G = config.options('globals')
198         for g in G:
199             val = config.get('globals', g)
200             if val == 'False':
201                 val = False
202             elif val == 'True':
203                 val = True
204             if g in globals():
205                 globals()[g] = val
206             else:
207                 for k in globals().keys():
208                     if k.lower() == g.lower():
209                         globals()[k] = val
210                         break
211
212     if 'arguments' in config.sections():
213         A = config.options('arguments')
214         for a in A:
215             val = config.get('arguments', a)
216             if val == 'False':
217                 val = False
218             elif val == 'True':
219                 val = True
220             args[a] = val
221
222     return args
223
224
225 def parse_args():
226     global args
227
228     fc = argparse.ArgumentDefaultsHelpFormatter
229     parser = argparse.ArgumentParser(description='Client Install Options',
230                                      formatter_class=fc)
231     parser.add_argument('--version',
232                         action='version', version='%(prog)s 0.1')
233     parser.add_argument('--hostname', default=socket.getfqdn(),
234                         help="Machine's fully qualified host name")
235     parser.add_argument('--admin-user', default='admin',
236                         help="Account allowed to create a SP")
237     parser.add_argument('--httpd-user', default='apache',
238                         help="Web server account used to read certs")
239     parser.add_argument('--saml', action='store_true', default=True,
240                         help="Whether to install a saml2 SP")
241     parser.add_argument('--saml-idp-metadata', default=None,
242                         help="A URL pointing at the IDP Metadata (FILE or HTTP)")
243     parser.add_argument('--saml-no-httpd', action='store_true', default=False,
244                         help="Do not configure httpd")
245     parser.add_argument('--saml-base', default='/',
246                         help="Where saml2 authdata is available")
247     parser.add_argument('--saml-auth', default=SAML2_PROTECTED,
248                         help="Where saml2 authentication is enforced")
249     parser.add_argument('--saml-sp', default='/saml2',
250                         help="Where saml communication happens")
251     parser.add_argument('--saml-sp-logout', default='/saml2/logout',
252                         help="Single Logout URL")
253     parser.add_argument('--saml-sp-post', default='/saml2/postResponse',
254                         help="Post response URL")
255     parser.add_argument('--saml-secure-setup', action='store_true',
256                         default=True, help="Turn on all security checks")
257     parser.add_argument('--debug', action='store_true', default=False,
258                         help="Turn on script debugging")
259     parser.add_argument('--config-profile', default=None,
260                         help="File containing install options")
261     parser.add_argument('--uninstall', action='store_true',
262                         help="Uninstall the server and all data")
263
264     args = vars(parser.parse_args())
265
266     if args['config_profile']:
267         args = parse_config_profile(args)
268
269     if len(args['hostname'].split('.')) < 2:
270         raise ValueError('Hostname: %s is not a FQDN.')
271
272     # At least one on this list needs to be specified or we do nothing
273     sp_list = ['saml']
274     present = False
275     for sp in sp_list:
276         if args[sp]:
277             present = True
278     if not present and not args['uninstall']:
279         raise ValueError('Nothing to install, please select a Service type.')
280
281
282 if __name__ == '__main__':
283     out = 0
284     openlogs()
285     try:
286         parse_args()
287
288         if 'uninstall' in args and args['uninstall'] is True:
289             uninstall()
290
291         install()
292     except Exception, e:  # pylint: disable=broad-except
293         log_exception(e)
294         if 'uninstall' in args and args['uninstall'] is True:
295             print 'Uninstallation aborted.'
296         else:
297             print 'Installation aborted.'
298         out = 1
299     finally:
300         if out == 0:
301             if 'uninstall' in args and args['uninstall'] is True:
302                 print 'Uninstallation complete.'
303             else:
304                 print 'Installation complete.'
305     sys.exit(out)