Add info plugin that utilizes Apache mod_lookup_identity plugin
[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.info.common import InfoMapping
11 from ipsilon.util.plugin import PluginObject
12 from string import Template
13 import cherrypy
14 import time
15 import subprocess
16 import SSSDConfig
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 = InfoMapping()
48         self.mapper.set_mapping(sssd_mapping)
49         self.name = 'sssd'
50         self.new_config(self.name)
51
52     def _get_user_data(self, user):
53         reply = dict()
54         groups = []
55         expectgroups = int(cherrypy.request.wsgi_environ.get(
56             'REMOTE_USER_GROUP_N', 0))
57         for key in cherrypy.request.wsgi_environ:
58             if key.startswith('REMOTE_USER_'):
59                 if key == 'REMOTE_USER_GROUP_N':
60                     continue
61                 if key.startswith('REMOTE_USER_GROUP_'):
62                     groups.append(cherrypy.request.wsgi_environ[key])
63                 else:
64                     reply[key] = cherrypy.request.wsgi_environ[key]
65         if len(groups) != expectgroups:
66             self.error('Number of groups expected was not found. Expected'
67                        ' %d got %d' % (expectgroups, len(groups)))
68         return reply, groups
69
70     def get_user_attrs(self, user):
71         reply = dict()
72         try:
73             attrs, groups = self._get_user_data(user)
74             userattrs, extras = self.mapper.map_attrs(attrs)
75             reply['userdata'] = userattrs
76             reply['groups'] = groups
77             reply['extras'] = {'sssd': extras}
78
79         except KeyError:
80             pass
81
82         return reply
83
84
85 CONF_TEMPLATE = """
86 LoadModule lookup_identity_module modules/mod_lookup_identity.so
87
88 <Location /${instance}>
89   LookupUserAttr sn REMOTE_USER_LASTNAME
90   LookupUserAttr locality REMOTE_USER_STATE
91   LookupUserAttr street REMOTE_USER_STREET
92   LookupUserAttr telephoneNumber REMOTE_USER_TELEPHONENUMBER
93   LookupUserAttr givenname REMOTE_USER_FIRSTNAME
94   LookupUserAttr mail REMOTE_USER_EMAIL
95   LookupUserAttr postalCode REMOTE_USER_POSTALCODE
96   LookupUserGroupsIter REMOTE_USER_GROUP
97 </Location>
98 """
99
100
101 class Installer(InfoProviderInstaller):
102
103     def __init__(self, *pargs):
104         super(Installer, self).__init__()
105         self.name = 'sssd'
106         self.pargs = pargs
107
108     def install_args(self, group):
109         group.add_argument('--info-sssd', choices=['yes', 'no'],
110                            default='no',
111                            help='Use mod_lookup_identity and SSSD to populate'
112                                 ' user attrs')
113         group.add_argument('--info-sssd-domain', action='store',
114                            help='SSSD domain to enable mod_lookup_identity'
115                                 ' for')
116
117     def configure(self, opts):
118         if opts['info_sssd'] != 'yes':
119             return
120
121         if not opts['info_sssd_domain']:
122             print 'info-identity-domain is required'
123             return False
124
125         confopts = {'instance': opts['instance']}
126
127         tmpl = Template(CONF_TEMPLATE)
128         hunk = tmpl.substitute(**confopts)  # pylint: disable=star-args
129         with open(opts['httpd_conf'], 'a') as httpd_conf:
130             httpd_conf.write(hunk)
131
132         try:
133             sssdconfig = SSSDConfig.SSSDConfig()
134             sssdconfig.import_config()
135         except Exception as e:  # pylint: disable=broad-except
136             # Unable to read existing SSSD config so it is probably not
137             # configured.
138             print 'Loading SSSD config failed: %s' % e
139             return False
140
141         try:
142             domain = sssdconfig.get_domain(opts['info_sssd_domain'])
143         except SSSDConfig.NoDomainError:
144             print 'No domain %s' % opts['info_sssd_domain']
145             return False
146
147         domain.set_option('ldap_user_extra_attrs', ', '.join(SSSD_ATTRS))
148
149         try:
150             sssdconfig.new_service('ifp')
151         except SSSDConfig.ServiceAlreadyExists:
152             pass
153
154         sssdconfig.activate_service('ifp')
155
156         ifp = sssdconfig.get_service('ifp')
157         ifp.set_option('allowed_uids', 'apache, root')
158         ifp.set_option('user_attributes', '+' + ', +'.join(SSSD_ATTRS))
159
160         sssdconfig.save_service(ifp)
161         sssdconfig.save_domain(domain)
162         sssdconfig.write(SSSD_CONF)
163
164         try:
165             subprocess.call(['/sbin/service', 'sssd', 'restart'])
166         except Exception:  # pylint: disable=broad-except
167             pass
168
169         # Give SSSD a chance to restart
170         time.sleep(5)
171
172         # Add configuration data to database
173         po = PluginObject(*self.pargs)
174         po.name = 'sssd'
175         po.wipe_data()
176         po.wipe_config_values()
177
178         # Update global config to add info plugin
179         po.is_enabled = True
180         po.save_enabled_state()