]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
talk: Properly size all icons
[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
66862195 101 # Session stuff
940227cb 102
66862195
MT
103 def _cleanup_expired_sessions(self):
104 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
940227cb 105
66862195
MT
106 def create_session(self, account, host):
107 self._cleanup_expired_sessions()
940227cb 108
66862195
MT
109 res = self.db.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
110 RETURNING session_id, time_expires", host, account.uid)
111
112 # Session could not be created
113 if not res:
114 return None, None
115
116 logging.info("Created session %s for %s which expires %s" \
117 % (res.session_id, account, res.time_expires))
118 return res.session_id, res.time_expires
119
120 def destroy_session(self, session_id, host):
121 logging.info("Destroying session %s" % session_id)
122
123 self.db.execute("DELETE FROM sessions \
124 WHERE session_id = %s AND host = %s", session_id, host)
125 self._cleanup_expired_sessions()
126
127 def get_by_session(self, session_id, host):
128 logging.debug("Looking up session %s" % session_id)
129
130 res = self.db.get("SELECT uid FROM sessions WHERE session_id = %s \
131 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
132 session_id, host)
133
134 # Session does not exist or has expired
135 if not res:
136 return
137
138 # Update the session expiration time
139 self.db.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
140 WHERE session_id = %s AND host = %s", session_id, host)
141
142 return self.get_by_uid(res.uid)
143
940227cb 144
a6dc0bad 145class Account(Object):
66862195 146 def __init__(self, backend, dn, attrs=None):
a6dc0bad 147 Object.__init__(self, backend)
940227cb
MT
148 self.dn = dn
149
66862195 150 self.__attrs = attrs or {}
940227cb 151
917434b8
MT
152 def __str__(self):
153 return self.name
154
940227cb
MT
155 def __repr__(self):
156 return "<%s %s>" % (self.__class__.__name__, self.dn)
157
541c952b
MT
158 def __eq__(self, other):
159 if isinstance(other, self.__class__):
160 return self.dn == other.dn
161
162 def __lt__(self, other):
163 if isinstance(other, self.__class__):
164 return self.name < other.name
940227cb
MT
165
166 @property
66862195
MT
167 def ldap(self):
168 return self.accounts.ldap
940227cb
MT
169
170 @property
171 def attributes(self):
66862195 172 return self.__attrs
940227cb 173
66862195 174 def _get_first_attribute(self, attr, default=None):
11347e46 175 if attr not in self.attributes:
66862195 176 return default
940227cb 177
66862195
MT
178 res = self.attributes.get(attr, [])
179 if res:
f6ed3d4d 180 return res[0]
940227cb
MT
181
182 def get(self, key):
183 try:
184 attribute = self.attributes[key]
185 except KeyError:
186 raise AttributeError(key)
187
188 if len(attribute) == 1:
189 return attribute[0]
190
191 return attribute
192
940227cb
MT
193 def check_password(self, password):
194 """
195 Bind to the server with given credentials and return
196 true if password is corrent and false if not.
197
198 Raises exceptions from the server on any other errors.
199 """
200
201 logging.debug("Checking credentials for %s" % self.dn)
202 try:
66862195 203 self.ldap.simple_bind_s(self.dn, password.encode("utf-8"))
940227cb 204 except ldap.INVALID_CREDENTIALS:
60024cc8 205 logging.debug("Account credentials are invalid.")
940227cb
MT
206 return False
207
60024cc8 208 logging.debug("Successfully authenticated.")
940227cb
MT
209 return True
210
940227cb 211 def is_admin(self):
d82bc8e3 212 return "wheel" in self.groups
66862195
MT
213
214 def is_talk_enabled(self):
06c1d39c
MT
215 return "sipUser" in self.classes or "sipRoutingObject" in self.classes \
216 or self.telephone_numbers or self.address
66862195
MT
217
218 @property
219 def classes(self):
917434b8 220 return (x.decode() for x in self.attributes.get("objectClass", []))
66862195
MT
221
222 @property
223 def uid(self):
f6ed3d4d 224 return self._get_first_attribute("uid").decode()
940227cb 225
a6dc0bad
MT
226 @property
227 def name(self):
f6ed3d4d 228 return self._get_first_attribute("cn").decode()
66862195
MT
229
230 @property
231 def first_name(self):
f1b290e5 232 return self._get_first_attribute("givenName").decode()
66862195 233
1bae74c7 234 @lazy_property
66862195 235 def groups(self):
1bae74c7 236 groups = []
66862195 237
1bae74c7
MT
238 res = self.accounts._query("(&(objectClass=posixGroup) \
239 (memberUid=%s))" % self.uid, ["cn"])
66862195 240
1bae74c7
MT
241 for dn, attrs in res:
242 cns = attrs.get("cn")
243 if cns:
244 groups.append(cns[0].decode())
66862195 245
1bae74c7 246 return groups
66862195
MT
247
248 @property
249 def address(self):
5c714ba6 250 address = self._get_first_attribute("homePostalAddress", "".encode()).decode()
66862195
MT
251 address = address.replace(", ", "\n")
252
253 return address
a6dc0bad 254
940227cb
MT
255 @property
256 def email(self):
66862195 257 name = self.name.lower()
940227cb 258 name = name.replace(" ", ".")
78fdedae
MT
259 name = name.replace("Ä", "Ae")
260 name = name.replace("Ö", "Oe")
261 name = name.replace("Ü", "Ue")
262 name = name.replace("ä", "ae")
263 name = name.replace("ö", "oe")
264 name = name.replace("ü", "ue")
940227cb 265
66862195 266 for mail in self.attributes.get("mail", []):
7aee4b8d 267 if mail.decode().startswith("%s@ipfire.org" % name):
940227cb
MT
268 return mail
269
2cd9af74
MT
270 # If everything else fails, we will go with the UID
271 return "%s@ipfire.org" % self.uid
940227cb 272
66862195
MT
273 @property
274 def sip_id(self):
275 if "sipUser" in self.classes:
f6ed3d4d 276 return self._get_first_attribute("sipAuthenticationUser").decode()
66862195
MT
277
278 if "sipRoutingObject" in self.classes:
f6ed3d4d 279 return self._get_first_attribute("sipLocalAddress").decode()
66862195 280
2f51147a
MT
281 @property
282 def sip_password(self):
f6ed3d4d 283 return self._get_first_attribute("sipPassword").decode()
2f51147a 284
66862195
MT
285 @property
286 def sip_url(self):
287 return "%s@ipfire.org" % self.sip_id
288
289 def uses_sip_forwarding(self):
290 if self.sip_routing_url:
291 return True
292
293 return False
294
295 @property
296 def sip_routing_url(self):
297 if "sipRoutingObject" in self.classes:
f6ed3d4d 298 return self._get_first_attribute("sipRoutingAddress").decode()
66862195 299
917434b8
MT
300 @lazy_property
301 def sip_registrations(self):
302 sip_registrations = []
303
304 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
305 reg.account = self
306
307 sip_registrations.append(reg)
308
309 return sip_registrations
310
66862195
MT
311 @property
312 def telephone_numbers(self):
6ff61434
MT
313 return self._telephone_numbers + self.mobile_telephone_numbers \
314 + self.home_telephone_numbers
315
316 @property
317 def _telephone_numbers(self):
318 return self.attributes.get("telephoneNumber") or []
319
320 @property
321 def home_telephone_numbers(self):
322 return self.attributes.get("homePhone") or []
323
324 @property
325 def mobile_telephone_numbers(self):
326 return self.attributes.get("mobile") or []
66862195 327
2cd9af74
MT
328 def avatar_url(self, size=None):
329 if self.backend.debug:
76d3ab8c 330 hostname = "http://accounts.dev.ipfire.org"
2cd9af74 331 else:
76d3ab8c 332 hostname = "https://accounts.ipfire.org"
2cd9af74 333
76d3ab8c 334 url = "%s/avatar/%s.jpg" % (hostname, self.uid)
2cd9af74
MT
335
336 if size:
337 url += "?size=%s" % size
338
339 return url
340
2cd9af74
MT
341 def get_avatar(self, size=None):
342 avatar = self._get_first_attribute("jpegPhoto")
343 if not avatar:
344 return
345
346 if not size:
347 return avatar
348
349 return self._resize_avatar(avatar, size)
350
351 def _resize_avatar(self, image, size):
1a226c83
MT
352 image = PIL.Image.open(io.BytesIO(image))
353
354 # Convert RGBA images into RGB because JPEG doesn't support alpha-channels
355 if image.mode == "RGBA":
356 image = image.convert("RGB")
357
358 # Resize the image to the desired resolution
359 image.thumbnail((size, size), PIL.Image.ANTIALIAS)
360
361 with io.BytesIO() as f:
362 # If writing out the image does not work with optimization,
363 # we try to write it out without any optimization.
364 try:
365 image.save(f, "JPEG", optimize=True, quality=98)
366 except:
367 image.save(f, "JPEG", quality=98)
368
369 return f.getvalue()
2cd9af74 370
60024cc8 371
940227cb
MT
372if __name__ == "__main__":
373 a = Accounts()
374
11347e46 375 print(a.list())