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