]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
Use pip requirements instead of configure
[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
940227cb 21 @property
66862195
MT
22 def ldap(self):
23 if not hasattr(self, "_ldap"):
24 # Connect to LDAP server
27066195 25 ldap_uri = self.settings.get("ldap_uri")
66862195 26 self._ldap = ldap.initialize(ldap_uri)
940227cb 27
66862195 28 # Bind with username and password
27066195 29 bind_dn = self.settings.get("ldap_bind_dn")
940227cb 30 if bind_dn:
9068dba1 31 bind_pw = self.settings.get("ldap_bind_pw", "")
66862195
MT
32 self._ldap.simple_bind(bind_dn, bind_pw)
33
34 return self._ldap
940227cb 35
1bae74c7 36 def _query(self, query, attrlist=None, limit=0):
66862195 37 logging.debug("Performing LDAP query: %s" % query)
940227cb 38
66862195 39 search_base = self.settings.get("ldap_search_base")
a395634c 40
a69e87a1
MT
41 try:
42 results = self.ldap.search_ext_s(search_base, ldap.SCOPE_SUBTREE,
43 query, attrlist=attrlist, sizelimit=limit)
44 except:
45 # Close current connection
46 del self._ldap
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:
436c9058 180 return res[0].decode()
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):
224 return self._get_first_attribute("uid")
940227cb 225
a6dc0bad
MT
226 @property
227 def name(self):
66862195
MT
228 return self._get_first_attribute("cn")
229
230 @property
231 def first_name(self):
232 return self._get_first_attribute("givenName")
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):
250 address = self._get_first_attribute("homePostalAddress", "")
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:
276 return self._get_first_attribute("sipAuthenticationUser")
277
278 if "sipRoutingObject" in self.classes:
279 return self._get_first_attribute("sipLocalAddress")
280
2f51147a
MT
281 @property
282 def sip_password(self):
283 return self._get_first_attribute("sipPassword")
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:
298 return self._get_first_attribute("sipRoutingAddress")
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:
330 hostname = "accounts.dev.ipfire.org"
331 else:
332 hostname = "accounts.ipfire.org"
333
4da2cb99 334 url = "https://%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):
11347e46 352 image = io.StringIO(image)
2cd9af74
MT
353 image = PIL.Image.open(image)
354
355 # Resize the image to the desired resolution
356 image.thumbnail((size, size), PIL.Image.ANTIALIAS)
357
11347e46 358 f = io.StringIO()
2cd9af74
MT
359
360 # If writing out the image does not work with optimization,
361 # we try to write it out without any optimization.
362 try:
363 image.save(f, "JPEG", optimize=True)
364 except:
365 image.save(f, "JPEG")
366
367 return f.getvalue()
368
60024cc8 369
940227cb
MT
370if __name__ == "__main__":
371 a = Accounts()
372
11347e46 373 print(a.list())