]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
talk: Add new base template
[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
MT
6import hashlib
7import ldap
27066195 8import logging
eea71144
MT
9import urllib.parse
10import urllib.request
940227cb 11
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
MT
138
139 def __repr__(self):
140 return "<%s %s>" % (self.__class__.__name__, self.dn)
141
541c952b
MT
142 def __eq__(self, other):
143 if isinstance(other, self.__class__):
144 return self.dn == other.dn
145
146 def __lt__(self, other):
147 if isinstance(other, self.__class__):
148 return self.name < other.name
940227cb
MT
149
150 @property
66862195
MT
151 def ldap(self):
152 return self.accounts.ldap
940227cb
MT
153
154 @property
155 def attributes(self):
66862195 156 return self.__attrs
940227cb 157
66862195 158 def _get_first_attribute(self, attr, default=None):
11347e46 159 if attr not in self.attributes:
66862195 160 return default
940227cb 161
66862195
MT
162 res = self.attributes.get(attr, [])
163 if res:
436c9058 164 return res[0].decode()
940227cb
MT
165
166 def get(self, key):
167 try:
168 attribute = self.attributes[key]
169 except KeyError:
170 raise AttributeError(key)
171
172 if len(attribute) == 1:
173 return attribute[0]
174
175 return attribute
176
940227cb
MT
177 def check_password(self, password):
178 """
179 Bind to the server with given credentials and return
180 true if password is corrent and false if not.
181
182 Raises exceptions from the server on any other errors.
183 """
184
185 logging.debug("Checking credentials for %s" % self.dn)
186 try:
66862195 187 self.ldap.simple_bind_s(self.dn, password.encode("utf-8"))
940227cb 188 except ldap.INVALID_CREDENTIALS:
60024cc8 189 logging.debug("Account credentials are invalid.")
940227cb
MT
190 return False
191
60024cc8 192 logging.debug("Successfully authenticated.")
940227cb
MT
193 return True
194
940227cb 195 def is_admin(self):
d82bc8e3 196 return "wheel" in self.groups
66862195
MT
197
198 def is_talk_enabled(self):
06c1d39c
MT
199 return "sipUser" in self.classes or "sipRoutingObject" in self.classes \
200 or self.telephone_numbers or self.address
66862195
MT
201
202 @property
203 def classes(self):
204 return self.attributes.get("objectClass", [])
205
206 @property
207 def uid(self):
208 return self._get_first_attribute("uid")
940227cb 209
a6dc0bad
MT
210 @property
211 def name(self):
66862195
MT
212 return self._get_first_attribute("cn")
213
214 @property
215 def first_name(self):
216 return self._get_first_attribute("givenName")
217
218 @property
219 def groups(self):
220 if not hasattr(self, "_groups"):
221 self._groups = []
222
223 res = self.accounts._search("(&(objectClass=posixGroup) \
224 (memberUid=%s))" % self.uid, ["cn"])
225
226 for dn, attrs in res:
227 cns = attrs.get("cn")
228 if cns:
229 self._groups.append(cns[0])
230
231 return self._groups
232
233 @property
234 def address(self):
235 address = self._get_first_attribute("homePostalAddress", "")
236 address = address.replace(", ", "\n")
237
238 return address
a6dc0bad 239
940227cb
MT
240 @property
241 def email(self):
66862195 242 name = self.name.lower()
940227cb 243 name = name.replace(" ", ".")
78fdedae
MT
244 name = name.replace("Ä", "Ae")
245 name = name.replace("Ö", "Oe")
246 name = name.replace("Ü", "Ue")
247 name = name.replace("ä", "ae")
248 name = name.replace("ö", "oe")
249 name = name.replace("ü", "ue")
940227cb 250
66862195 251 for mail in self.attributes.get("mail", []):
7aee4b8d 252 if mail.decode().startswith("%s@ipfire.org" % name):
940227cb
MT
253 return mail
254
2cd9af74
MT
255 # If everything else fails, we will go with the UID
256 return "%s@ipfire.org" % self.uid
940227cb 257
66862195
MT
258 @property
259 def sip_id(self):
260 if "sipUser" in self.classes:
261 return self._get_first_attribute("sipAuthenticationUser")
262
263 if "sipRoutingObject" in self.classes:
264 return self._get_first_attribute("sipLocalAddress")
265
2f51147a
MT
266 @property
267 def sip_password(self):
268 return self._get_first_attribute("sipPassword")
269
66862195
MT
270 @property
271 def sip_url(self):
272 return "%s@ipfire.org" % self.sip_id
273
274 def uses_sip_forwarding(self):
275 if self.sip_routing_url:
276 return True
277
278 return False
279
280 @property
281 def sip_routing_url(self):
282 if "sipRoutingObject" in self.classes:
283 return self._get_first_attribute("sipRoutingAddress")
284
285 def sip_is_online(self):
286 assert self.sip_id
287
288 if not hasattr(self, "_is_online"):
289 self._is_online = self.backend.talk.user_is_online(self.sip_id)
290
291 return self._is_online
292
293 @property
294 def telephone_numbers(self):
6ff61434
MT
295 return self._telephone_numbers + self.mobile_telephone_numbers \
296 + self.home_telephone_numbers
297
298 @property
299 def _telephone_numbers(self):
300 return self.attributes.get("telephoneNumber") or []
301
302 @property
303 def home_telephone_numbers(self):
304 return self.attributes.get("homePhone") or []
305
306 @property
307 def mobile_telephone_numbers(self):
308 return self.attributes.get("mobile") or []
66862195 309
2cd9af74
MT
310 def avatar_url(self, size=None):
311 if self.backend.debug:
312 hostname = "accounts.dev.ipfire.org"
313 else:
314 hostname = "accounts.ipfire.org"
315
4da2cb99 316 url = "https://%s/avatar/%s.jpg" % (hostname, self.uid)
2cd9af74
MT
317
318 if size:
319 url += "?size=%s" % size
320
321 return url
322
323 gravatar_icon = avatar_url
324
325 def get_avatar(self, size=None):
326 avatar = self._get_first_attribute("jpegPhoto")
327 if not avatar:
328 return
329
330 if not size:
331 return avatar
332
333 return self._resize_avatar(avatar, size)
334
335 def _resize_avatar(self, image, size):
11347e46 336 image = io.StringIO(image)
2cd9af74
MT
337 image = PIL.Image.open(image)
338
339 # Resize the image to the desired resolution
340 image.thumbnail((size, size), PIL.Image.ANTIALIAS)
341
11347e46 342 f = io.StringIO()
2cd9af74
MT
343
344 # If writing out the image does not work with optimization,
345 # we try to write it out without any optimization.
346 try:
347 image.save(f, "JPEG", optimize=True)
348 except:
349 image.save(f, "JPEG")
350
351 return f.getvalue()
352
353 def get_gravatar_url(self, size=128):
354 try:
355 gravatar_email = self.email.lower()
356 except:
357 gravatar_email = "nobody@ipfire.org"
358
359 # construct the url
4da2cb99 360 gravatar_url = "https://www.gravatar.com/avatar/" + \
2cd9af74 361 hashlib.md5(gravatar_email).hexdigest() + "?"
11347e46 362 gravatar_url += urllib.parse.urlencode({'d': "mm", 's': str(size)})
2cd9af74
MT
363
364 return gravatar_url
365
60024cc8 366
940227cb
MT
367if __name__ == "__main__":
368 a = Accounts()
369
11347e46 370 print(a.list())