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