Add uninstallation support to infosssd
[cascardo/ipsilon.git] / ipsilon / info / infosssd.py
1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
2
3 # Info plugin for mod_lookup_identity Apache module via SSSD
4 # http://www.adelton.com/apache/mod_lookup_identity/
5
6 from ipsilon.info.common import InfoProviderBase
7 from ipsilon.info.common import InfoProviderInstaller
8 from ipsilon.util.plugin import PluginObject
9 from ipsilon.util.policy import Policy
10 from ipsilon.util import config as pconfig
11 from string import Template
12 import cherrypy
13 import time
14 import subprocess
15 import SSSDConfig
16 import logging
17
18 SSSD_CONF = '/etc/sssd/sssd.conf'
19
20 # LDAP attributes to tell SSSD to fetch over the InfoPipe
21 SSSD_ATTRS = ['mail',
22               'street',
23               'locality',
24               'postalCode',
25               'telephoneNumber',
26               'givenname',
27               'sn']
28
29 # Map the mod_lookup_identity env variables to Ipsilon. The inverse of
30 # this is in the httpd template.
31 sssd_mapping = [
32     ['REMOTE_USER_GECOS', 'fullname'],
33     ['REMOTE_USER_EMAIL', 'email'],
34     ['REMOTE_USER_FIRSTNAME', 'givenname'],
35     ['REMOTE_USER_LASTNAME', 'surname'],
36     ['REMOTE_USER_STREET', 'street'],
37     ['REMOTE_USER_STATE', 'state'],
38     ['REMOTE_USER_POSTALCODE', 'postcode'],
39     ['REMOTE_USER_TELEPHONENUMBER', 'phone'],
40 ]
41
42
43 class InfoProvider(InfoProviderBase):
44
45     def __init__(self, *pargs):
46         super(InfoProvider, self).__init__(*pargs)
47         self.mapper = Policy(sssd_mapping)
48         self.name = 'sssd'
49         self.new_config(
50             self.name,
51             pconfig.Condition(
52                 'preconfigured',
53                 'SSSD can only be used when pre-configured',
54                 False),
55         )
56
57     def _get_user_data(self, user):
58         reply = dict()
59         groups = []
60         expectgroups = int(cherrypy.request.wsgi_environ.get(
61             'REMOTE_USER_GROUP_N', 0))
62         for key in cherrypy.request.wsgi_environ:
63             if key.startswith('REMOTE_USER_'):
64                 if key == 'REMOTE_USER_GROUP_N':
65                     continue
66                 if key.startswith('REMOTE_USER_GROUP_'):
67                     groups.append(cherrypy.request.wsgi_environ[key])
68                 else:
69                     reply[key] = cherrypy.request.wsgi_environ[key]
70         if len(groups) != expectgroups:
71             self.error('Number of groups expected was not found. Expected'
72                        ' %d got %d' % (expectgroups, len(groups)))
73         return reply, groups
74
75     def get_user_attrs(self, user):
76         reply = dict()
77         try:
78             attrs, groups = self._get_user_data(user)
79             userattrs, extras = self.mapper.map_attributes(attrs)
80             reply = userattrs
81             reply['_groups'] = groups
82             reply['_extras'] = {'sssd': extras}
83
84         except KeyError:
85             pass
86
87         return reply
88
89     def save_plugin_config(self, *args, **kwargs):
90         raise ValueError('Configuration cannot be modified live for SSSD')
91
92     def get_config_obj(self):
93         return None
94
95     def enable(self):
96         self.refresh_plugin_config()
97         if not self.get_config_value('preconfigured'):
98             raise Exception("SSSD Can be enabled only if pre-configured")
99         super(InfoProvider, self).enable()
100
101
102 CONF_TEMPLATE = """
103 LoadModule lookup_identity_module modules/mod_lookup_identity.so
104
105 <Location /${instance}>
106   LookupUserAttr sn REMOTE_USER_LASTNAME
107   LookupUserAttr locality REMOTE_USER_STATE
108   LookupUserAttr street REMOTE_USER_STREET
109   LookupUserAttr telephoneNumber REMOTE_USER_TELEPHONENUMBER
110   LookupUserAttr givenname REMOTE_USER_FIRSTNAME
111   LookupUserAttr mail REMOTE_USER_EMAIL
112   LookupUserAttr postalCode REMOTE_USER_POSTALCODE
113   LookupUserGroupsIter REMOTE_USER_GROUP
114 </Location>
115 """
116
117
118 class Installer(InfoProviderInstaller):
119
120     def __init__(self, *pargs):
121         super(Installer, self).__init__()
122         self.name = 'sssd'
123         self.pargs = pargs
124
125     def install_args(self, group):
126         group.add_argument('--info-sssd', choices=['yes', 'no'],
127                            default='no',
128                            help='Use mod_lookup_identity and SSSD to populate'
129                                 ' user attrs')
130         group.add_argument('--info-sssd-domain', action='append',
131                            help='SSSD domain to enable mod_lookup_identity'
132                                 ' for')
133
134     def configure(self, opts, changes):
135         if opts['info_sssd'] != 'yes':
136             return
137
138         configured = 0
139
140         confopts = {'instance': opts['instance']}
141
142         tmpl = Template(CONF_TEMPLATE)
143         hunk = tmpl.substitute(**confopts)
144         with open(opts['httpd_conf'], 'a') as httpd_conf:
145             httpd_conf.write(hunk)
146
147         try:
148             sssdconfig = SSSDConfig.SSSDConfig()
149             sssdconfig.import_config()
150         except Exception as e:  # pylint: disable=broad-except
151             # Unable to read existing SSSD config so it is probably not
152             # configured.
153             logging.info('Loading SSSD config failed: %s', e)
154             return False
155
156         if not opts['info_sssd_domain']:
157             domains = sssdconfig.list_domains()
158         else:
159             domains = opts['info_sssd_domain']
160
161         changes['domains'] = {}
162         for domain in domains:
163             changes['domains'][domain] = {}
164             try:
165                 sssd_domain = sssdconfig.get_domain(domain)
166             except SSSDConfig.NoDomainError:
167                 logging.info('No SSSD domain %s', domain)
168                 continue
169             else:
170                 try:
171                     changes['domains'][domain] = {
172                         'ldap_user_extra_attrs':
173                             sssd_domain.get_option('ldap_user_extra_attrs')}
174                 except SSSDConfig.NoOptionError:
175                     pass
176                 sssd_domain.set_option(
177                     'ldap_user_extra_attrs', ', '.join(SSSD_ATTRS)
178                 )
179                 sssdconfig.save_domain(sssd_domain)
180                 configured += 1
181                 logging.info("Configured SSSD domain %s", domain)
182
183         if configured == 0:
184             logging.info('No SSSD domains configured')
185             return False
186
187         changes['ifp'] = {}
188         try:
189             sssdconfig.new_service('ifp')
190             changes['ifp']['new'] = True
191         except SSSDConfig.ServiceAlreadyExists:
192             changes['ifp']['new'] = False
193
194         sssdconfig.activate_service('ifp')
195
196         ifp = sssdconfig.get_service('ifp')
197         if not changes['ifp']['new']:
198             try:
199                 changes['ifp']['allowed_uids'] = ifp.get_option('allowed_uids')
200             except SSSDConfig.NoOptionError:
201                 pass
202             try:
203                 changes['ifp']['user_attributes'] = ifp.get_option(
204                     'user_attributes')
205             except SSSDConfig.NoOptionError:
206                 pass
207         ifp.set_option('allowed_uids', 'apache, root')
208         ifp.set_option('user_attributes', '+' + ', +'.join(SSSD_ATTRS))
209
210         sssdconfig.save_service(ifp)
211         sssdconfig.write(SSSD_CONF)
212
213         # for selinux enabled platforms, ignore if it fails just report
214         try:
215             subprocess.call(['/usr/sbin/setsebool', '-P',
216                              'httpd_dbus_sssd=on'])
217         except Exception:  # pylint: disable=broad-except
218             pass
219
220         try:
221             subprocess.call(['/sbin/service', 'sssd', 'restart'])
222         except Exception:  # pylint: disable=broad-except
223             pass
224
225         # Give SSSD a chance to restart
226         time.sleep(5)
227
228         # Add configuration data to database
229         po = PluginObject(*self.pargs)
230         po.name = 'sssd'
231         po.wipe_data()
232         po.wipe_config_values()
233         config = {'preconfigured': 'True'}
234         po.save_plugin_config(config)
235
236         # Update global config to add info plugin
237         po.is_enabled = True
238         po.save_enabled_state()
239
240     def unconfigure(self, opts, changes):
241         try:
242             sssdconfig = SSSDConfig.SSSDConfig()
243             sssdconfig.import_config()
244         except Exception as e:  # pylint: disable=broad-except
245             # Unable to read existing SSSD config so it is probably not
246             # configured.
247             logging.info('Loading SSSD config failed: %s', e)
248             return False
249
250         for domain in changes['domains']:
251             try:
252                 sssd_domain = sssdconfig.get_domain(domain.encode('utf-8'))
253             except SSSDConfig.NoDomainError:
254                 logging.info('No SSSD domain %s', domain)
255                 continue
256             else:
257                 if 'ldap_user_extra_attrs' in changes['domains'][domain]:
258                     sssd_domain.set_option('ldap_user_extra_attrs',
259                                            changes['domains'][domain][
260                                                'ldap_user_extra_attrs'].encode(
261                                                    'utf-8'))
262                 else:
263                     sssd_domain.remove_option('ldap_user_extra_attrs')
264                 sssdconfig.save_domain(sssd_domain)
265
266         if changes['ifp']['new']:
267             # We created the service newly, let's remove
268             sssdconfig.delete_service('ifp')
269         else:
270             ifp = sssdconfig.get_service('ifp')
271             if 'allowed_uids' in changes['ifp']:
272                 ifp.set_option('allowed_uids',
273                                changes['ifp']['allowed_uids'].encode('utf-8'))
274             if 'user_attributes' in changes['ifp']:
275                 ifp.set_option('user_attributes',
276                                changes['ifp']['user_attributes'].encode(
277                                    'utf-8'))
278             sssdconfig.save_service(ifp)
279
280         sssdconfig.write(SSSD_CONF)
281
282         try:
283             subprocess.call(['/sbin/service', 'sssd', 'restart'])
284         except Exception:  # pylint: disable=broad-except
285             pass
286
287         # Give SSSD a chance to restart
288         time.sleep(5)