SSSD info plugin is immutable if not preconfigured
[cascardo/ipsilon.git] / ipsilon / info / infosssd.py
1 # Copyright (C) 2014 Ipsilon Project Contributors
2 #
3 # See the file named COPYING for the project license
4
5 # Info plugin for mod_lookup_identity Apache module via SSSD
6 # http://www.adelton.com/apache/mod_lookup_identity/
7
8 from ipsilon.info.common import InfoProviderBase
9 from ipsilon.info.common import InfoProviderInstaller
10 from ipsilon.util.plugin import PluginObject
11 from ipsilon.util.policy import Policy
12 from ipsilon.util import config as pconfig
13 from string import Template
14 import cherrypy
15 import time
16 import subprocess
17 import SSSDConfig
18
19 SSSD_CONF = '/etc/sssd/sssd.conf'
20
21 # LDAP attributes to tell SSSD to fetch over the InfoPipe
22 SSSD_ATTRS = ['mail',
23               'street',
24               'locality',
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_POSTALCODE', 'postcode'],
40     ['REMOTE_USER_TELEPHONENUMBER', 'phone'],
41 ]
42
43
44 class InfoProvider(InfoProviderBase):
45
46     def __init__(self, *pargs):
47         super(InfoProvider, self).__init__(*pargs)
48         self.mapper = Policy(sssd_mapping)
49         self.name = 'sssd'
50         self.new_config(
51             self.name,
52             pconfig.Condition(
53                 'preconfigured',
54                 'SSSD can only be used when pre-configured',
55                 False),
56         )
57
58     def _get_user_data(self, user):
59         reply = dict()
60         groups = []
61         expectgroups = int(cherrypy.request.wsgi_environ.get(
62             'REMOTE_USER_GROUP_N', 0))
63         for key in cherrypy.request.wsgi_environ:
64             if key.startswith('REMOTE_USER_'):
65                 if key == 'REMOTE_USER_GROUP_N':
66                     continue
67                 if key.startswith('REMOTE_USER_GROUP_'):
68                     groups.append(cherrypy.request.wsgi_environ[key])
69                 else:
70                     reply[key] = cherrypy.request.wsgi_environ[key]
71         if len(groups) != expectgroups:
72             self.error('Number of groups expected was not found. Expected'
73                        ' %d got %d' % (expectgroups, len(groups)))
74         return reply, groups
75
76     def get_user_attrs(self, user):
77         reply = dict()
78         try:
79             attrs, groups = self._get_user_data(user)
80             userattrs, extras = self.mapper.map_attributes(attrs)
81             reply = userattrs
82             reply['_groups'] = groups
83             reply['_extras'] = {'sssd': extras}
84
85         except KeyError:
86             pass
87
88         return reply
89
90     def save_plugin_config(self, *args, **kwargs):
91         raise ValueError('Configuration cannot be modified live for SSSD')
92
93     def get_config_obj(self):
94         return None
95
96     def enable(self):
97         self.refresh_plugin_config()
98         if not self.get_config_value('preconfigured'):
99             raise Exception("SSSD Can be enabled only if pre-configured")
100         super(InfoProvider, self).enable()
101
102
103 CONF_TEMPLATE = """
104 LoadModule lookup_identity_module modules/mod_lookup_identity.so
105
106 <Location /${instance}>
107   LookupUserAttr sn REMOTE_USER_LASTNAME
108   LookupUserAttr locality REMOTE_USER_STATE
109   LookupUserAttr street REMOTE_USER_STREET
110   LookupUserAttr telephoneNumber REMOTE_USER_TELEPHONENUMBER
111   LookupUserAttr givenname REMOTE_USER_FIRSTNAME
112   LookupUserAttr mail REMOTE_USER_EMAIL
113   LookupUserAttr postalCode REMOTE_USER_POSTALCODE
114   LookupUserGroupsIter REMOTE_USER_GROUP
115 </Location>
116 """
117
118
119 class Installer(InfoProviderInstaller):
120
121     def __init__(self, *pargs):
122         super(Installer, self).__init__()
123         self.name = 'sssd'
124         self.pargs = pargs
125
126     def install_args(self, group):
127         group.add_argument('--info-sssd', choices=['yes', 'no'],
128                            default='no',
129                            help='Use mod_lookup_identity and SSSD to populate'
130                                 ' user attrs')
131         group.add_argument('--info-sssd-domain', action='append',
132                            help='SSSD domain to enable mod_lookup_identity'
133                                 ' for')
134
135     def configure(self, opts):
136         if opts['info_sssd'] != 'yes':
137             return
138
139         configured = 0
140
141         confopts = {'instance': opts['instance']}
142
143         tmpl = Template(CONF_TEMPLATE)
144         hunk = tmpl.substitute(**confopts)  # pylint: disable=star-args
145         with open(opts['httpd_conf'], 'a') as httpd_conf:
146             httpd_conf.write(hunk)
147
148         try:
149             sssdconfig = SSSDConfig.SSSDConfig()
150             sssdconfig.import_config()
151         except Exception as e:  # pylint: disable=broad-except
152             # Unable to read existing SSSD config so it is probably not
153             # configured.
154             print 'Loading SSSD config failed: %s' % e
155             return False
156
157         if not opts['info_sssd_domain']:
158             domains = sssdconfig.list_domains()
159         else:
160             domains = opts['info_sssd_domain']
161
162         for domain in domains:
163             try:
164                 sssd_domain = sssdconfig.get_domain(domain)
165             except SSSDConfig.NoDomainError:
166                 print 'No SSSD domain %s' % domain
167                 continue
168             else:
169                 sssd_domain.set_option(
170                     'ldap_user_extra_attrs', ', '.join(SSSD_ATTRS)
171                 )
172                 sssdconfig.save_domain(sssd_domain)
173                 configured += 1
174                 print "Configured SSSD domain %s" % domain
175
176         if configured == 0:
177             print 'No SSSD domains configured'
178             return False
179
180         try:
181             sssdconfig.new_service('ifp')
182         except SSSDConfig.ServiceAlreadyExists:
183             pass
184
185         sssdconfig.activate_service('ifp')
186
187         ifp = sssdconfig.get_service('ifp')
188         ifp.set_option('allowed_uids', 'apache, root')
189         ifp.set_option('user_attributes', '+' + ', +'.join(SSSD_ATTRS))
190
191         sssdconfig.save_service(ifp)
192         sssdconfig.write(SSSD_CONF)
193
194         # for selinux enabled platforms, ignore if it fails just report
195         try:
196             subprocess.call(['/usr/sbin/setsebool', '-P',
197                              'httpd_dbus_sssd=on'])
198         except Exception:  # pylint: disable=broad-except
199             pass
200
201         try:
202             subprocess.call(['/sbin/service', 'sssd', 'restart'])
203         except Exception:  # pylint: disable=broad-except
204             pass
205
206         # Give SSSD a chance to restart
207         time.sleep(5)
208
209         # Add configuration data to database
210         po = PluginObject(*self.pargs)
211         po.name = 'sssd'
212         po.wipe_data()
213         po.wipe_config_values()
214         config = {'preconfigured': True}
215         po.save_plugin_config(config)
216
217         # Update global config to add info plugin
218         po.is_enabled = True
219         po.save_enabled_state()