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