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