Allow turning off security at install time
[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 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 not args['saml_no_httpd']:
82         path = os.path.join(SAML2_HTTPDIR, args['hostname'])
83         os.makedirs(path, 0750)
84     else:
85         path = os.getcwd()
86
87     proto = 'https'
88     if not args['saml_secure_setup']:
89         proto = 'http'
90     url = '%s://%s' % (proto, args['hostname'])
91     url_sp = url + args['saml_sp']
92     url_logout = url + args['saml_sp_logout']
93     url_post = url + args['saml_sp_post']
94
95     # Generate metadata
96     m = Metadata('sp')
97     c = Certificate(path)
98     c.generate('certificate', args['hostname'])
99     m.set_entity_id(url_sp)
100     m.add_certs(c)
101     m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout)
102     m.add_service(SAML2_SERVICE_MAP['response-post'], url_post, index="0")
103     sp_metafile = os.path.join(path, 'metadata.xml')
104     m.output(sp_metafile)
105
106     if not args['saml_no_httpd']:
107         idp_metafile = os.path.join(path, 'idp-metadata.xml')
108         with open(idp_metafile, 'w+') as f:
109             f.write(idpmeta)
110
111         saml_protect = 'auth'
112         saml_auth=''
113         if args['saml_base'] != args['saml_auth']:
114             saml_protect = 'info'
115             saml_auth = '<Location %s>\n' \
116                         '    MellonEnable "auth"\n' \
117                         '</Location>\n' % args['saml_auth']
118
119         psp = '# '
120         if args['saml_auth'] == SAML2_PROTECTED:
121             # default location, enable the default page
122             psp = ''
123
124         saml_secure = 'Off'
125         if args['saml_secure_setup']:
126             saml_secure = 'On'
127
128         samlopts = {'saml_base': args['saml_base'],
129                     'saml_protect': saml_protect,
130                     'saml_sp_key': c.key,
131                     'saml_sp_cert': c.cert,
132                     'saml_sp_meta': sp_metafile,
133                     'saml_idp_meta': idp_metafile,
134                     'saml_sp': args['saml_sp'],
135                     'saml_secure_on': saml_secure,
136                     'saml_auth': saml_auth,
137                     'sp': psp}
138         files.write_from_template(SAML2_CONFFILE, SAML2_TEMPLATE, samlopts)
139
140         files.fix_user_dirs(SAML2_HTTPDIR, args['httpd_user'])
141
142         logger.info('SAML Service Provider configured.')
143         logger.info('You should be able to restart the HTTPD server and' +
144                     ' then access it at %s%s' % (url, args['saml_auth']))
145     else:
146         logger.info('SAML Service Provider configuration ready.')
147         logger.info('Use the certificate, key and metadata.xml files to' +
148                     ' configure your Service Provider')
149
150
151 def install():
152     if args['saml']:
153         saml2()
154
155
156 def saml2_uninstall():
157     try:
158         shutil.rmtree(os.path.join(SAML2_HTTPDIR, args['hostname']))
159     except Exception, e:  # pylint: disable=broad-except
160         log_exception(e)
161     try:
162         os.remove(SAML2_CONFFILE)
163     except Exception, e:  # pylint: disable=broad-except
164         log_exception(e)
165
166
167 def uninstall():
168     logger.info('Uninstalling Service Provider')
169     #FXIME: ask confirmation
170     saml2_uninstall()
171     logger.info('Uninstalled SAML2 data')
172
173
174 def log_exception(e):
175     if 'debug' in args and args['debug']:
176         logger.exception(e)
177     else:
178         logger.error(e)
179
180
181 def parse_args():
182     global args
183
184     fc = argparse.ArgumentDefaultsHelpFormatter
185     parser = argparse.ArgumentParser(description='Client Install Options',
186                                      formatter_class=fc)
187     parser.add_argument('--version',
188                         action='version', version='%(prog)s 0.1')
189     parser.add_argument('--hostname', default=socket.getfqdn(),
190                         help="Machine's fully qualified host name")
191     parser.add_argument('--admin-user', default='admin',
192                         help="Account allowed to create a SP")
193     parser.add_argument('--httpd-user', default='apache',
194                         help="Web server account used to read certs")
195     parser.add_argument('--saml', action='store_true', default=True,
196                         help="Whether to install a saml2 SP")
197     parser.add_argument('--saml-idp-metadata', default=None,
198                         help="A URL pointing at the IDP Metadata (FILE or HTTP)")
199     parser.add_argument('--saml-no-httpd', action='store_true', default=False,
200                         help="Do not configure httpd")
201     parser.add_argument('--saml-base', default='/',
202                         help="Where saml2 authdata is available")
203     parser.add_argument('--saml-auth', default=SAML2_PROTECTED,
204                         help="Where saml2 authentication is enforced")
205     parser.add_argument('--saml-sp', default='/saml2',
206                         help="Where saml communication happens")
207     parser.add_argument('--saml-sp-logout', default='/saml2/logout',
208                         help="Single Logout URL")
209     parser.add_argument('--saml-sp-post', default='/saml2/postResponse',
210                         help="Post response URL")
211     parser.add_argument('--saml-secure-setup', action='store_true',
212                         default=True, help="Turn on all security checks")
213     parser.add_argument('--debug', action='store_true', default=False,
214                         help="Turn on script debugging")
215     parser.add_argument('--uninstall', action='store_true',
216                         help="Uninstall the server and all data")
217
218     args = vars(parser.parse_args())
219
220     if len(args['hostname'].split('.')) < 2:
221         raise ValueError('Hostname: %s is not a FQDN.')
222
223     # At least one on this list needs to be specified or we do nothing
224     sp_list = ['saml']
225     present = False
226     for sp in sp_list:
227         if args[sp]:
228             present = True
229     if not present and not args['uninstall']:
230         raise ValueError('Nothing to install, please select a Service type.')
231
232
233 if __name__ == '__main__':
234     out = 0
235     openlogs()
236     try:
237         parse_args()
238
239         if 'uninstall' in args and args['uninstall'] is True:
240             uninstall()
241
242         install()
243     except Exception, e:  # pylint: disable=broad-except
244         log_exception(e)
245         if 'uninstall' in args and args['uninstall'] is True:
246             print 'Uninstallation aborted.'
247         else:
248             print 'Installation aborted.'
249         out = 1
250     finally:
251         if out == 0:
252             if 'uninstall' in args and args['uninstall'] is True:
253                 print 'Uninstallation complete.'
254             else:
255                 print 'Installation complete.'
256     sys.exit(out)