]>
Commit | Line | Data |
---|---|---|
940227cb | 1 | #!/usr/bin/python |
78fdedae | 2 | # encoding: utf-8 |
940227cb | 3 | |
2cd9af74 | 4 | import PIL |
11347e46 | 5 | import io |
940227cb | 6 | import ldap |
27066195 | 7 | import logging |
eea71144 MT |
8 | import urllib.parse |
9 | import urllib.request | |
940227cb | 10 | |
917434b8 | 11 | from .decorators import * |
11347e46 | 12 | from .misc import Object |
940227cb | 13 | |
a6dc0bad | 14 | class Accounts(Object): |
940227cb | 15 | @property |
66862195 MT |
16 | def ldap(self): |
17 | if not hasattr(self, "_ldap"): | |
18 | # Connect to LDAP server | |
27066195 | 19 | ldap_uri = self.settings.get("ldap_uri") |
66862195 | 20 | self._ldap = ldap.initialize(ldap_uri) |
940227cb | 21 | |
66862195 | 22 | # Bind with username and password |
27066195 | 23 | bind_dn = self.settings.get("ldap_bind_dn") |
940227cb | 24 | if bind_dn: |
9068dba1 | 25 | bind_pw = self.settings.get("ldap_bind_pw", "") |
66862195 MT |
26 | self._ldap.simple_bind(bind_dn, bind_pw) |
27 | ||
28 | return self._ldap | |
940227cb | 29 | |
66862195 MT |
30 | def _search(self, query, attrlist=None, limit=0): |
31 | logging.debug("Performing LDAP query: %s" % query) | |
940227cb | 32 | |
66862195 | 33 | search_base = self.settings.get("ldap_search_base") |
a395634c | 34 | |
a69e87a1 MT |
35 | try: |
36 | results = self.ldap.search_ext_s(search_base, ldap.SCOPE_SUBTREE, | |
37 | query, attrlist=attrlist, sizelimit=limit) | |
38 | except: | |
39 | # Close current connection | |
40 | del self._ldap | |
41 | ||
42 | raise | |
940227cb | 43 | |
66862195 | 44 | return results |
940227cb | 45 | |
66862195 MT |
46 | def search(self, query, limit=0): |
47 | results = self._search(query, limit=limit) | |
940227cb | 48 | |
66862195 | 49 | accounts = [] |
940227cb | 50 | for dn, attrs in results: |
66862195 MT |
51 | account = Account(self.backend, dn, attrs) |
52 | accounts.append(account) | |
53 | ||
54 | return sorted(accounts) | |
55 | ||
56 | def search_one(self, query): | |
57 | result = self.search(query, limit=1) | |
58 | assert len(result) <= 1 | |
59 | ||
60 | if result: | |
61 | return result[0] | |
62 | ||
63 | def get_all(self): | |
5a913423 MT |
64 | # Only return developers (group with ID 1000) |
65 | return self.search("(&(objectClass=posixAccount)(gidNumber=1000))") | |
66862195 MT |
66 | |
67 | list = get_all | |
68 | ||
69 | def get_by_uid(self, uid): | |
70 | return self.search_one("(&(objectClass=posixAccount)(uid=%s))" % uid) | |
71 | ||
72 | def get_by_mail(self, mail): | |
73 | return self.search_one("(&(objectClass=posixAccount)(mail=%s))" % mail) | |
74 | ||
75 | find = get_by_uid | |
76 | ||
77 | def find_account(self, s): | |
78 | account = self.get_by_uid(s) | |
79 | if account: | |
80 | return account | |
81 | ||
82 | return self.get_by_mail(s) | |
940227cb | 83 | |
66862195 MT |
84 | def get_by_sip_id(self, sip_id): |
85 | return self.search_one("(|(&(objectClass=sipUser)(sipAuthenticationUser=%s)) \ | |
86 | (&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" % (sip_id, sip_id)) | |
940227cb | 87 | |
66862195 | 88 | # Session stuff |
940227cb | 89 | |
66862195 MT |
90 | def _cleanup_expired_sessions(self): |
91 | self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()") | |
940227cb | 92 | |
66862195 MT |
93 | def create_session(self, account, host): |
94 | self._cleanup_expired_sessions() | |
940227cb | 95 | |
66862195 MT |
96 | res = self.db.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \ |
97 | RETURNING session_id, time_expires", host, account.uid) | |
98 | ||
99 | # Session could not be created | |
100 | if not res: | |
101 | return None, None | |
102 | ||
103 | logging.info("Created session %s for %s which expires %s" \ | |
104 | % (res.session_id, account, res.time_expires)) | |
105 | return res.session_id, res.time_expires | |
106 | ||
107 | def destroy_session(self, session_id, host): | |
108 | logging.info("Destroying session %s" % session_id) | |
109 | ||
110 | self.db.execute("DELETE FROM sessions \ | |
111 | WHERE session_id = %s AND host = %s", session_id, host) | |
112 | self._cleanup_expired_sessions() | |
113 | ||
114 | def get_by_session(self, session_id, host): | |
115 | logging.debug("Looking up session %s" % session_id) | |
116 | ||
117 | res = self.db.get("SELECT uid FROM sessions WHERE session_id = %s \ | |
118 | AND host = %s AND NOW() BETWEEN time_created AND time_expires", | |
119 | session_id, host) | |
120 | ||
121 | # Session does not exist or has expired | |
122 | if not res: | |
123 | return | |
124 | ||
125 | # Update the session expiration time | |
126 | self.db.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \ | |
127 | WHERE session_id = %s AND host = %s", session_id, host) | |
128 | ||
129 | return self.get_by_uid(res.uid) | |
130 | ||
940227cb | 131 | |
a6dc0bad | 132 | class Account(Object): |
66862195 | 133 | def __init__(self, backend, dn, attrs=None): |
a6dc0bad | 134 | Object.__init__(self, backend) |
940227cb MT |
135 | self.dn = dn |
136 | ||
66862195 | 137 | self.__attrs = attrs or {} |
940227cb | 138 | |
917434b8 MT |
139 | def __str__(self): |
140 | return self.name | |
141 | ||
940227cb MT |
142 | def __repr__(self): |
143 | return "<%s %s>" % (self.__class__.__name__, self.dn) | |
144 | ||
541c952b MT |
145 | def __eq__(self, other): |
146 | if isinstance(other, self.__class__): | |
147 | return self.dn == other.dn | |
148 | ||
149 | def __lt__(self, other): | |
150 | if isinstance(other, self.__class__): | |
151 | return self.name < other.name | |
940227cb MT |
152 | |
153 | @property | |
66862195 MT |
154 | def ldap(self): |
155 | return self.accounts.ldap | |
940227cb MT |
156 | |
157 | @property | |
158 | def attributes(self): | |
66862195 | 159 | return self.__attrs |
940227cb | 160 | |
66862195 | 161 | def _get_first_attribute(self, attr, default=None): |
11347e46 | 162 | if attr not in self.attributes: |
66862195 | 163 | return default |
940227cb | 164 | |
66862195 MT |
165 | res = self.attributes.get(attr, []) |
166 | if res: | |
436c9058 | 167 | return res[0].decode() |
940227cb MT |
168 | |
169 | def get(self, key): | |
170 | try: | |
171 | attribute = self.attributes[key] | |
172 | except KeyError: | |
173 | raise AttributeError(key) | |
174 | ||
175 | if len(attribute) == 1: | |
176 | return attribute[0] | |
177 | ||
178 | return attribute | |
179 | ||
940227cb MT |
180 | def check_password(self, password): |
181 | """ | |
182 | Bind to the server with given credentials and return | |
183 | true if password is corrent and false if not. | |
184 | ||
185 | Raises exceptions from the server on any other errors. | |
186 | """ | |
187 | ||
188 | logging.debug("Checking credentials for %s" % self.dn) | |
189 | try: | |
66862195 | 190 | self.ldap.simple_bind_s(self.dn, password.encode("utf-8")) |
940227cb | 191 | except ldap.INVALID_CREDENTIALS: |
60024cc8 | 192 | logging.debug("Account credentials are invalid.") |
940227cb MT |
193 | return False |
194 | ||
60024cc8 | 195 | logging.debug("Successfully authenticated.") |
940227cb MT |
196 | return True |
197 | ||
940227cb | 198 | def is_admin(self): |
d82bc8e3 | 199 | return "wheel" in self.groups |
66862195 MT |
200 | |
201 | def is_talk_enabled(self): | |
06c1d39c MT |
202 | return "sipUser" in self.classes or "sipRoutingObject" in self.classes \ |
203 | or self.telephone_numbers or self.address | |
66862195 MT |
204 | |
205 | @property | |
206 | def classes(self): | |
917434b8 | 207 | return (x.decode() for x in self.attributes.get("objectClass", [])) |
66862195 MT |
208 | |
209 | @property | |
210 | def uid(self): | |
211 | return self._get_first_attribute("uid") | |
940227cb | 212 | |
a6dc0bad MT |
213 | @property |
214 | def name(self): | |
66862195 MT |
215 | return self._get_first_attribute("cn") |
216 | ||
217 | @property | |
218 | def first_name(self): | |
219 | return self._get_first_attribute("givenName") | |
220 | ||
221 | @property | |
222 | def groups(self): | |
223 | if not hasattr(self, "_groups"): | |
224 | self._groups = [] | |
225 | ||
226 | res = self.accounts._search("(&(objectClass=posixGroup) \ | |
227 | (memberUid=%s))" % self.uid, ["cn"]) | |
228 | ||
229 | for dn, attrs in res: | |
230 | cns = attrs.get("cn") | |
231 | if cns: | |
b78ec69b | 232 | self._groups.append(cns[0].decode()) |
66862195 MT |
233 | |
234 | return self._groups | |
235 | ||
236 | @property | |
237 | def address(self): | |
238 | address = self._get_first_attribute("homePostalAddress", "") | |
239 | address = address.replace(", ", "\n") | |
240 | ||
241 | return address | |
a6dc0bad | 242 | |
940227cb MT |
243 | @property |
244 | def email(self): | |
66862195 | 245 | name = self.name.lower() |
940227cb | 246 | name = name.replace(" ", ".") |
78fdedae MT |
247 | name = name.replace("Ä", "Ae") |
248 | name = name.replace("Ö", "Oe") | |
249 | name = name.replace("Ü", "Ue") | |
250 | name = name.replace("ä", "ae") | |
251 | name = name.replace("ö", "oe") | |
252 | name = name.replace("ü", "ue") | |
940227cb | 253 | |
66862195 | 254 | for mail in self.attributes.get("mail", []): |
7aee4b8d | 255 | if mail.decode().startswith("%s@ipfire.org" % name): |
940227cb MT |
256 | return mail |
257 | ||
2cd9af74 MT |
258 | # If everything else fails, we will go with the UID |
259 | return "%s@ipfire.org" % self.uid | |
940227cb | 260 | |
66862195 MT |
261 | @property |
262 | def sip_id(self): | |
263 | if "sipUser" in self.classes: | |
264 | return self._get_first_attribute("sipAuthenticationUser") | |
265 | ||
266 | if "sipRoutingObject" in self.classes: | |
267 | return self._get_first_attribute("sipLocalAddress") | |
268 | ||
2f51147a MT |
269 | @property |
270 | def sip_password(self): | |
271 | return self._get_first_attribute("sipPassword") | |
272 | ||
66862195 MT |
273 | @property |
274 | def sip_url(self): | |
275 | return "%s@ipfire.org" % self.sip_id | |
276 | ||
277 | def uses_sip_forwarding(self): | |
278 | if self.sip_routing_url: | |
279 | return True | |
280 | ||
281 | return False | |
282 | ||
283 | @property | |
284 | def sip_routing_url(self): | |
285 | if "sipRoutingObject" in self.classes: | |
286 | return self._get_first_attribute("sipRoutingAddress") | |
287 | ||
288 | def sip_is_online(self): | |
289 | assert self.sip_id | |
290 | ||
291 | if not hasattr(self, "_is_online"): | |
292 | self._is_online = self.backend.talk.user_is_online(self.sip_id) | |
293 | ||
294 | return self._is_online | |
295 | ||
917434b8 MT |
296 | @lazy_property |
297 | def sip_registrations(self): | |
298 | sip_registrations = [] | |
299 | ||
300 | for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url): | |
301 | reg.account = self | |
302 | ||
303 | sip_registrations.append(reg) | |
304 | ||
305 | return sip_registrations | |
306 | ||
66862195 MT |
307 | @property |
308 | def telephone_numbers(self): | |
6ff61434 MT |
309 | return self._telephone_numbers + self.mobile_telephone_numbers \ |
310 | + self.home_telephone_numbers | |
311 | ||
312 | @property | |
313 | def _telephone_numbers(self): | |
314 | return self.attributes.get("telephoneNumber") or [] | |
315 | ||
316 | @property | |
317 | def home_telephone_numbers(self): | |
318 | return self.attributes.get("homePhone") or [] | |
319 | ||
320 | @property | |
321 | def mobile_telephone_numbers(self): | |
322 | return self.attributes.get("mobile") or [] | |
66862195 | 323 | |
2cd9af74 MT |
324 | def avatar_url(self, size=None): |
325 | if self.backend.debug: | |
326 | hostname = "accounts.dev.ipfire.org" | |
327 | else: | |
328 | hostname = "accounts.ipfire.org" | |
329 | ||
4da2cb99 | 330 | url = "https://%s/avatar/%s.jpg" % (hostname, self.uid) |
2cd9af74 MT |
331 | |
332 | if size: | |
333 | url += "?size=%s" % size | |
334 | ||
335 | return url | |
336 | ||
2cd9af74 MT |
337 | def get_avatar(self, size=None): |
338 | avatar = self._get_first_attribute("jpegPhoto") | |
339 | if not avatar: | |
340 | return | |
341 | ||
342 | if not size: | |
343 | return avatar | |
344 | ||
345 | return self._resize_avatar(avatar, size) | |
346 | ||
347 | def _resize_avatar(self, image, size): | |
11347e46 | 348 | image = io.StringIO(image) |
2cd9af74 MT |
349 | image = PIL.Image.open(image) |
350 | ||
351 | # Resize the image to the desired resolution | |
352 | image.thumbnail((size, size), PIL.Image.ANTIALIAS) | |
353 | ||
11347e46 | 354 | f = io.StringIO() |
2cd9af74 MT |
355 | |
356 | # If writing out the image does not work with optimization, | |
357 | # we try to write it out without any optimization. | |
358 | try: | |
359 | image.save(f, "JPEG", optimize=True) | |
360 | except: | |
361 | image.save(f, "JPEG") | |
362 | ||
363 | return f.getvalue() | |
364 | ||
60024cc8 | 365 | |
940227cb MT |
366 | if __name__ == "__main__": |
367 | a = Accounts() | |
368 | ||
11347e46 | 369 | print(a.list()) |