Always use saml by default
[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     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 not args['saml_no_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         samlopts = {'saml_base': args['saml_base'],
122                     'saml_protect': saml_protect,
123                     'saml_sp_key': c.key,
124                     'saml_sp_cert': c.cert,
125                     'saml_sp_meta': sp_metafile,
126                     'saml_idp_meta': idp_metafile,
127                     'saml_sp': args['saml_sp'],
128                     'saml_auth': saml_auth,
129                     'sp': psp}
130         files.write_from_template(SAML2_CONFFILE, SAML2_TEMPLATE, samlopts)
131
132         files.fix_user_dirs(SAML2_HTTPDIR, args['httpd_user'])
133
134         logger.info('SAML Service Provider configured.')
135         logger.info('You should be able to restart the HTTPD server and' +
136                     ' then access it at %s%s' % (url, args['saml_auth']))
137     else:
138         logger.info('SAML Service Provider configuration ready.')
139         logger.info('Use the certificate, key and metadata.xml files to' +
140                     ' configure your Service Provider')
141
142
143 def install():
144     if args['saml']:
145         saml2()
146
147
148 def saml2_uninstall():
149     try:
150         shutil.rmtree(os.path.join(SAML2_HTTPDIR, args['hostname']))
151     except Exception, e:  # pylint: disable=broad-except
152         log_exception(e)
153     try:
154         os.remove(SAML2_CONFFILE)
155     except Exception, e:  # pylint: disable=broad-except
156         log_exception(e)
157
158
159 def uninstall():
160     logger.info('Uninstalling Service Provider')
161     #FXIME: ask confirmation
162     saml2_uninstall()
163     logger.info('Uninstalled SAML2 data')
164
165
166 def log_exception(e):
167     if 'debug' in args and args['debug']:
168         logger.exception(e)
169     else:
170         logger.error(e)
171
172
173 def parse_args():
174     global args
175
176     fc = argparse.ArgumentDefaultsHelpFormatter
177     parser = argparse.ArgumentParser(description='Client Install Options',
178                                      formatter_class=fc)
179     parser.add_argument('--version',
180                         action='version', version='%(prog)s 0.1')
181     parser.add_argument('--hostname', default=socket.getfqdn(),
182                         help="Machine's fully qualified host name")
183     parser.add_argument('--admin-user', default='admin',
184                         help="Account allowed to create a SP")
185     parser.add_argument('--httpd-user', default='apache',
186                         help="Web server account used to read certs")
187     parser.add_argument('--saml', action='store_true', default=True,
188                         help="Whether to install a saml2 SP")
189     parser.add_argument('--saml-idp-metadata', default=None,
190                         help="A URL pointing at the IDP Metadata (FILE or HTTP)")
191     parser.add_argument('--saml-no-httpd', action='store_true', default=False,
192                         help="Do not configure httpd")
193     parser.add_argument('--saml-base', default='/',
194                         help="Where saml2 authdata is available")
195     parser.add_argument('--saml-auth', default=SAML2_PROTECTED,
196                         help="Where saml2 authentication is enforced")
197     parser.add_argument('--saml-sp', default='/saml2',
198                         help="Where saml communication happens")
199     parser.add_argument('--saml-sp-logout', default='/saml2/logout',
200                         help="Single Logout URL")
201     parser.add_argument('--saml-sp-post', default='/saml2/postResponse',
202                         help="Post response URL")
203     parser.add_argument('--debug', action='store_true', default=False,
204                         help="Turn on script debugging")
205     parser.add_argument('--uninstall', action='store_true',
206                         help="Uninstall the server and all data")
207
208     args = vars(parser.parse_args())
209
210     if len(args['hostname'].split('.')) < 2:
211         raise ValueError('Hostname: %s is not a FQDN.')
212
213     # At least one on this list needs to be specified or we do nothing
214     sp_list = ['saml']
215     present = False
216     for sp in sp_list:
217         if args[sp]:
218             present = True
219     if not present and not args['uninstall']:
220         raise ValueError('Nothing to install, please select a Service type.')
221
222
223 if __name__ == '__main__':
224     out = 0
225     openlogs()
226     try:
227         parse_args()
228
229         if 'uninstall' in args and args['uninstall'] is True:
230             uninstall()
231
232         install()
233     except Exception, e:  # pylint: disable=broad-except
234         log_exception(e)
235         if 'uninstall' in args and args['uninstall'] is True:
236             print 'Uninstallation aborted.'
237         else:
238             print 'Installation aborted.'
239         out = 1
240     finally:
241         if out == 0:
242             if 'uninstall' in args and args['uninstall'] is True:
243                 print 'Uninstallation complete.'
244             else:
245                 print 'Installation complete.'
246     sys.exit(out)