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