237b4398af881be33c678a7950895419d9639f75
[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                         '    Header append Cache-Control "no-cache"\n' \
119                         '</Location>\n' % args['saml_auth']
120
121         psp = '# '
122         if args['saml_auth'] == SAML2_PROTECTED:
123             # default location, enable the default page
124             psp = ''
125
126         saml_secure = 'Off'
127         ssl_require = '#'
128         ssl_rewrite = '#'
129         if args['saml_secure_setup']:
130             saml_secure = 'On'
131             ssl_require = ''
132             ssl_rewrite = ''
133
134         samlopts = {'saml_base': args['saml_base'],
135                     'saml_protect': saml_protect,
136                     'saml_sp_key': c.key,
137                     'saml_sp_cert': c.cert,
138                     'saml_sp_meta': sp_metafile,
139                     'saml_idp_meta': idp_metafile,
140                     'saml_sp': args['saml_sp'],
141                     'saml_secure_on': saml_secure,
142                     'saml_auth': saml_auth,
143                     'ssl_require': ssl_require,
144                     'ssl_rewrite': ssl_rewrite,
145                     'sp_hostname': args['hostname'],
146                     'sp': psp}
147         files.write_from_template(SAML2_CONFFILE, SAML2_TEMPLATE, samlopts)
148
149         files.fix_user_dirs(SAML2_HTTPDIR, args['httpd_user'])
150
151         logger.info('SAML Service Provider configured.')
152         logger.info('You should be able to restart the HTTPD server and' +
153                     ' then access it at %s%s' % (url, args['saml_auth']))
154     else:
155         logger.info('SAML Service Provider configuration ready.')
156         logger.info('Use the certificate, key and metadata.xml files to' +
157                     ' configure your Service Provider')
158
159
160 def install():
161     if args['saml']:
162         saml2()
163
164
165 def saml2_uninstall():
166     try:
167         shutil.rmtree(os.path.join(SAML2_HTTPDIR, args['hostname']))
168     except Exception, e:  # pylint: disable=broad-except
169         log_exception(e)
170     try:
171         os.remove(SAML2_CONFFILE)
172     except Exception, e:  # pylint: disable=broad-except
173         log_exception(e)
174
175
176 def uninstall():
177     logger.info('Uninstalling Service Provider')
178     #FXIME: ask confirmation
179     saml2_uninstall()
180     logger.info('Uninstalled SAML2 data')
181
182
183 def log_exception(e):
184     if 'debug' in args and args['debug']:
185         logger.exception(e)
186     else:
187         logger.error(e)
188
189
190 def parse_config_profile(args):
191     config = ConfigParser.ConfigParser()
192     files = config.read(args['config_profile'])
193     if len(files) == 0:
194         raise ConfigurationError('Config Profile file %s not found!' %
195                                  args['config_profile'])
196
197     if 'globals' in config.sections():
198         G = config.options('globals')
199         for g in G:
200             val = config.get('globals', g)
201             if val == 'False':
202                 val = False
203             elif val == 'True':
204                 val = True
205             if g in globals():
206                 globals()[g] = val
207             else:
208                 for k in globals().keys():
209                     if k.lower() == g.lower():
210                         globals()[k] = val
211                         break
212
213     if 'arguments' in config.sections():
214         A = config.options('arguments')
215         for a in A:
216             val = config.get('arguments', a)
217             if val == 'False':
218                 val = False
219             elif val == 'True':
220                 val = True
221             args[a] = val
222
223     return args
224
225
226 def parse_args():
227     global args
228
229     fc = argparse.ArgumentDefaultsHelpFormatter
230     parser = argparse.ArgumentParser(description='Client Install Options',
231                                      formatter_class=fc)
232     parser.add_argument('--version',
233                         action='version', version='%(prog)s 0.1')
234     parser.add_argument('--hostname', default=socket.getfqdn(),
235                         help="Machine's fully qualified host name")
236     parser.add_argument('--admin-user', default='admin',
237                         help="Account allowed to create a SP")
238     parser.add_argument('--httpd-user', default='apache',
239                         help="Web server account used to read certs")
240     parser.add_argument('--saml', action='store_true', default=True,
241                         help="Whether to install a saml2 SP")
242     parser.add_argument('--saml-idp-metadata', default=None,
243                         help="A URL pointing at the IDP Metadata (FILE or HTTP)")
244     parser.add_argument('--saml-no-httpd', action='store_true', default=False,
245                         help="Do not configure httpd")
246     parser.add_argument('--saml-base', default='/',
247                         help="Where saml2 authdata is available")
248     parser.add_argument('--saml-auth', default=SAML2_PROTECTED,
249                         help="Where saml2 authentication is enforced")
250     parser.add_argument('--saml-sp', default='/saml2',
251                         help="Where saml communication happens")
252     parser.add_argument('--saml-sp-logout', default='/saml2/logout',
253                         help="Single Logout URL")
254     parser.add_argument('--saml-sp-post', default='/saml2/postResponse',
255                         help="Post response URL")
256     parser.add_argument('--saml-secure-setup', action='store_true',
257                         default=True, help="Turn on all security checks")
258     parser.add_argument('--debug', action='store_true', default=False,
259                         help="Turn on script debugging")
260     parser.add_argument('--config-profile', default=None,
261                         help="File containing install options")
262     parser.add_argument('--uninstall', action='store_true',
263                         help="Uninstall the server and all data")
264
265     args = vars(parser.parse_args())
266
267     if args['config_profile']:
268         args = parse_config_profile(args)
269
270     if len(args['hostname'].split('.')) < 2:
271         raise ValueError('Hostname: %s is not a FQDN.')
272
273     # At least one on this list needs to be specified or we do nothing
274     sp_list = ['saml']
275     present = False
276     for sp in sp_list:
277         if args[sp]:
278             present = True
279     if not present and not args['uninstall']:
280         raise ValueError('Nothing to install, please select a Service type.')
281
282
283 if __name__ == '__main__':
284     out = 0
285     openlogs()
286     try:
287         parse_args()
288
289         if 'uninstall' in args and args['uninstall'] is True:
290             uninstall()
291
292         install()
293     except Exception, e:  # pylint: disable=broad-except
294         log_exception(e)
295         if 'uninstall' in args and args['uninstall'] is True:
296             print 'Uninstallation aborted.'
297         else:
298             print 'Installation aborted.'
299         out = 1
300     finally:
301         if out == 0:
302             if 'uninstall' in args and args['uninstall'] is True:
303                 print 'Uninstallation complete.'
304             else:
305                 print 'Installation complete.'
306     sys.exit(out)