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