]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
people: Don't show duplicate calls
[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):
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 150class 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
380if __name__ == "__main__":
381 a = Accounts()
382
11347e46 383 print(a.list())