]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
talk: Show online status in sidebar
[ipfire.org.git] / src / backend / accounts.py
1 #!/usr/bin/python
2 # encoding: utf-8
3
4 import PIL
5 import io
6 import ldap
7 import logging
8 import urllib.parse
9 import urllib.request
10
11 from .decorators import *
12 from .misc import Object
13
14 class Accounts(Object):
15 @property
16 def ldap(self):
17 if not hasattr(self, "_ldap"):
18 # Connect to LDAP server
19 ldap_uri = self.settings.get("ldap_uri")
20 self._ldap = ldap.initialize(ldap_uri)
21
22 # Bind with username and password
23 bind_dn = self.settings.get("ldap_bind_dn")
24 if bind_dn:
25 bind_pw = self.settings.get("ldap_bind_pw", "")
26 self._ldap.simple_bind(bind_dn, bind_pw)
27
28 return self._ldap
29
30 def _search(self, query, attrlist=None, limit=0):
31 logging.debug("Performing LDAP query: %s" % query)
32
33 search_base = self.settings.get("ldap_search_base")
34
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
43
44 return results
45
46 def search(self, query, limit=0):
47 results = self._search(query, limit=limit)
48
49 accounts = []
50 for dn, attrs in results:
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):
64 # Only return developers (group with ID 1000)
65 return self.search("(&(objectClass=posixAccount)(gidNumber=1000))")
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)
83
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))
87
88 # Session stuff
89
90 def _cleanup_expired_sessions(self):
91 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
92
93 def create_session(self, account, host):
94 self._cleanup_expired_sessions()
95
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
131
132 class Account(Object):
133 def __init__(self, backend, dn, attrs=None):
134 Object.__init__(self, backend)
135 self.dn = dn
136
137 self.__attrs = attrs or {}
138
139 def __str__(self):
140 return self.name
141
142 def __repr__(self):
143 return "<%s %s>" % (self.__class__.__name__, self.dn)
144
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
152
153 @property
154 def ldap(self):
155 return self.accounts.ldap
156
157 @property
158 def attributes(self):
159 return self.__attrs
160
161 def _get_first_attribute(self, attr, default=None):
162 if attr not in self.attributes:
163 return default
164
165 res = self.attributes.get(attr, [])
166 if res:
167 return res[0].decode()
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
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:
190 self.ldap.simple_bind_s(self.dn, password.encode("utf-8"))
191 except ldap.INVALID_CREDENTIALS:
192 logging.debug("Account credentials are invalid.")
193 return False
194
195 logging.debug("Successfully authenticated.")
196 return True
197
198 def is_admin(self):
199 return "wheel" in self.groups
200
201 def is_talk_enabled(self):
202 return "sipUser" in self.classes or "sipRoutingObject" in self.classes \
203 or self.telephone_numbers or self.address
204
205 @property
206 def classes(self):
207 return (x.decode() for x in self.attributes.get("objectClass", []))
208
209 @property
210 def uid(self):
211 return self._get_first_attribute("uid")
212
213 @property
214 def name(self):
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:
232 self._groups.append(cns[0].decode())
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
242
243 @property
244 def email(self):
245 name = self.name.lower()
246 name = name.replace(" ", ".")
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")
253
254 for mail in self.attributes.get("mail", []):
255 if mail.decode().startswith("%s@ipfire.org" % name):
256 return mail
257
258 # If everything else fails, we will go with the UID
259 return "%s@ipfire.org" % self.uid
260
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
269 @property
270 def sip_password(self):
271 return self._get_first_attribute("sipPassword")
272
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
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
307 @property
308 def telephone_numbers(self):
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 []
323
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
330 url = "https://%s/avatar/%s.jpg" % (hostname, self.uid)
331
332 if size:
333 url += "?size=%s" % size
334
335 return url
336
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):
348 image = io.StringIO(image)
349 image = PIL.Image.open(image)
350
351 # Resize the image to the desired resolution
352 image.thumbnail((size, size), PIL.Image.ANTIALIAS)
353
354 f = io.StringIO()
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
365
366 if __name__ == "__main__":
367 a = Accounts()
368
369 print(a.list())