]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
talk: Show online status in sidebar
[ipfire.org.git] / src / backend / accounts.py
CommitLineData
940227cb 1#!/usr/bin/python
78fdedae 2# encoding: utf-8
940227cb 3
2cd9af74 4import PIL
11347e46 5import io
940227cb 6import ldap
27066195 7import logging
eea71144
MT
8import urllib.parse
9import urllib.request
940227cb 10
917434b8 11from .decorators import *
11347e46 12from .misc import Object
940227cb 13
a6dc0bad 14class 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 132class 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
366if __name__ == "__main__":
367 a = Accounts()
368
11347e46 369 print(a.list())