set SELinux boolean httpd_can_connect_ldap when install infolap and authldap
[cascardo/ipsilon.git] / ipsilon / info / infoldap.py
1 # Copyright (C) 2014 Ipsilon Project Contributors
2 #
3 # See the file named COPYING for the project license
4
5 from ipsilon.info.common import InfoProviderBase
6 from ipsilon.info.common import InfoProviderInstaller
7 from ipsilon.util.plugin import PluginObject
8 from ipsilon.util.policy import Policy
9 from ipsilon.util import config as pconfig
10 import ldap
11 import subprocess
12
13
14 # TODO: fetch mapping from configuration
15 ldap_mapping = [
16     ['cn', 'fullname'],
17     ['commonname', 'fullname'],
18     ['sn', 'surname'],
19     ['mail', 'email'],
20     ['destinationindicator', 'country'],
21     ['postalcode', 'postcode'],
22     ['st', 'state'],
23     ['statetorprovincename', 'state'],
24     ['streetaddress', 'street'],
25     ['telephonenumber', 'phone'],
26 ]
27
28
29 class InfoProvider(InfoProviderBase):
30
31     def __init__(self, *pargs):
32         super(InfoProvider, self).__init__(*pargs)
33         self.mapper = Policy(ldap_mapping)
34         self.name = 'ldap'
35         self.description = """
36 Info plugin that uses LDAP to retrieve user data. """
37         self.new_config(
38             self.name,
39             pconfig.String(
40                 'server url',
41                 'The LDAP server url.',
42                 'ldap://example.com'),
43             pconfig.Template(
44                 'user dn template',
45                 'Template to turn username into DN.',
46                 'uid=%(username)s,ou=People,dc=example,dc=com'),
47             pconfig.Pick(
48                 'tls',
49                 'What TLS level show be required',
50                 ['Demand', 'Allow', 'Try', 'Never', 'NoTLS'],
51                 'Demand'),
52             pconfig.String(
53                 'bind dn',
54                 'DN to bind as, if empty uses anonymous bind.',
55                 'uid=ipsilon,ou=People,dc=example,dc=com'),
56             pconfig.String(
57                 'bind password',
58                 'Password to use for bind operation'),
59         )
60
61     @property
62     def server_url(self):
63         return self.get_config_value('server url')
64
65     @property
66     def tls(self):
67         return self.get_config_value('tls')
68
69     @property
70     def bind_dn(self):
71         return self.get_config_value('bind dn')
72
73     @property
74     def bind_password(self):
75         return self.get_config_value('bind password')
76
77     @property
78     def user_dn_tmpl(self):
79         return self.get_config_value('user dn template')
80
81     def _ldap_bind(self):
82
83         tls = self.tls.lower()
84         tls_req_opt = None
85         if tls == "never":
86             tls_req_opt = ldap.OPT_X_TLS_NEVER
87         elif tls == "demand":
88             tls_req_opt = ldap.OPT_X_TLS_DEMAND
89         elif tls == "allow":
90             tls_req_opt = ldap.OPT_X_TLS_ALLOW
91         elif tls == "try":
92             tls_req_opt = ldap.OPT_X_TLS_TRY
93         if tls_req_opt is not None:
94             ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_opt)
95
96         conn = ldap.initialize(self.server_url)
97
98         if tls != "notls":
99             if not self.server_url.startswith("ldaps"):
100                 conn.start_tls_s()
101
102         conn.simple_bind_s(self.bind_dn, self.bind_password)
103
104         return conn
105
106     def _get_user_data(self, conn, dn):
107         result = conn.search_s(dn, ldap.SCOPE_BASE)
108         if result is None or result == []:
109             raise Exception('User object could not be found!')
110         elif len(result) > 1:
111             raise Exception('No unique user object could be found!')
112         data = dict()
113         for name, value in result[0][1].iteritems():
114             if type(value) is list and len(value) == 1:
115                 value = value[0]
116             data[name] = value
117         return data
118
119     def _get_user_groups(self, conn, dn, ldapattrs):
120         # TODO: fixme to support RFC2307bis schemas
121         if 'memberuid' in ldapattrs:
122             return ldapattrs['memberuid']
123         else:
124             return []
125
126     def get_user_data_from_conn(self, conn, dn):
127         reply = dict()
128         try:
129             ldapattrs = self._get_user_data(conn, dn)
130             userattrs, extras = self.mapper.map_attributes(ldapattrs)
131             groups = self._get_user_groups(conn, dn, ldapattrs)
132             reply = userattrs
133             reply['_groups'] = groups
134             reply['_extras'] = {'ldap': extras}
135         except Exception, e:  # pylint: disable=broad-except
136             self.error(e)
137
138         return reply
139
140     def get_user_attrs(self, user):
141         try:
142             conn = self._ldap_bind()
143             dn = self.user_dn_tmpl % {'username': user}
144             return self.get_user_data_from_conn(conn, dn)
145         except Exception, e:  # pylint: disable=broad-except
146             self.error(e)
147             return {}
148
149
150 class Installer(InfoProviderInstaller):
151
152     def __init__(self, *pargs):
153         super(Installer, self).__init__()
154         self.name = 'ldap'
155         self.pargs = pargs
156
157     def install_args(self, group):
158         group.add_argument('--info-ldap', choices=['yes', 'no'], default='no',
159                            help='Use LDAP to populate user attrs')
160         group.add_argument('--info-ldap-server-url', action='store',
161                            help='LDAP Server Url')
162         group.add_argument('--info-ldap-bind-dn', action='store',
163                            help='LDAP Bind DN')
164         group.add_argument('--info-ldap-bind-pwd', action='store',
165                            help='LDAP Bind Password')
166         group.add_argument('--info-ldap-user-dn-template', action='store',
167                            help='LDAP User DN Template')
168
169     def configure(self, opts):
170         if opts['info_ldap'] != 'yes':
171             return
172
173         # Add configuration data to database
174         po = PluginObject(*self.pargs)
175         po.name = 'ldap'
176         po.wipe_data()
177         po.wipe_config_values()
178         config = dict()
179         if 'info_ldap_server_url' in opts:
180             config['server url'] = opts['info_ldap_server_url']
181         elif 'ldap_server_url' in opts:
182             config['server url'] = opts['ldap_server_url']
183         config = {'bind dn': opts['info_ldap_bind_dn']}
184         config = {'bind password': opts['info_ldap_bind_pwd']}
185         config = {'user dn template': opts['info_ldap_user_dn_template']}
186         if 'info_ldap_bind_dn' in opts:
187             config['bind dn'] = opts['info_ldap_bind_dn']
188         if 'info_ldap_bind_pwd' in opts:
189             config['bind password'] = opts['info_ldap_bind_pwd']
190         if 'info_ldap_user_dn_template' in opts:
191             config['user dn template'] = opts['info_ldap_user_dn_template']
192         elif 'ldap_bind_dn_template' in opts:
193             config['user dn template'] = opts['ldap_bind_dn_template']
194         config['tls'] = 'Demand'
195         po.save_plugin_config(config)
196
197         # Update global config to add info plugin
198         po.is_enabled = True
199         po.save_enabled_state()
200
201         # For selinux enabled platforms permit httpd to connect to ldap,
202         # ignore if it fails
203         try:
204             subprocess.call(['/usr/sbin/setsebool', '-P',
205                              'httpd_can_connect_ldap=on'])
206         except Exception:  # pylint: disable=broad-except
207             pass