54d0ba247bad3c0526c2671b16cc58c918a31045
[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         for domain in domains:
162             try:
163                 sssd_domain = sssdconfig.get_domain(domain)
164             except SSSDConfig.NoDomainError:
165                 logging.info('No SSSD domain %s', domain)
166                 continue
167             else:
168                 sssd_domain.set_option(
169                     'ldap_user_extra_attrs', ', '.join(SSSD_ATTRS)
170                 )
171                 sssdconfig.save_domain(sssd_domain)
172                 configured += 1
173                 logging.info("Configured SSSD domain %s", domain)
174
175         if configured == 0:
176             logging.info('No SSSD domains configured')
177             return False
178
179         try:
180             sssdconfig.new_service('ifp')
181         except SSSDConfig.ServiceAlreadyExists:
182             pass
183
184         sssdconfig.activate_service('ifp')
185
186         ifp = sssdconfig.get_service('ifp')
187         ifp.set_option('allowed_uids', 'apache, root')
188         ifp.set_option('user_attributes', '+' + ', +'.join(SSSD_ATTRS))
189
190         sssdconfig.save_service(ifp)
191         sssdconfig.write(SSSD_CONF)
192
193         # for selinux enabled platforms, ignore if it fails just report
194         try:
195             subprocess.call(['/usr/sbin/setsebool', '-P',
196                              'httpd_dbus_sssd=on'])
197         except Exception:  # pylint: disable=broad-except
198             pass
199
200         try:
201             subprocess.call(['/sbin/service', 'sssd', 'restart'])
202         except Exception:  # pylint: disable=broad-except
203             pass
204
205         # Give SSSD a chance to restart
206         time.sleep(5)
207
208         # Add configuration data to database
209         po = PluginObject(*self.pargs)
210         po.name = 'sssd'
211         po.wipe_data()
212         po.wipe_config_values()
213         config = {'preconfigured': 'True'}
214         po.save_plugin_config(config)
215
216         # Update global config to add info plugin
217         po.is_enabled = True
218         po.save_enabled_state()