]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
18 from .decorators
import *
19 from .misc
import Object
21 class Accounts(Object
):
23 # Only return developers (group with ID 1000)
24 accounts
= self
._search
("(&(objectClass=posixAccount)(gidNumber=1000))")
26 return iter(sorted(accounts
))
30 # Connect to LDAP server
31 ldap_uri
= self
.settings
.get("ldap_uri")
32 conn
= ldap
.initialize(ldap_uri
)
34 # Bind with username and password
35 bind_dn
= self
.settings
.get("ldap_bind_dn")
37 bind_pw
= self
.settings
.get("ldap_bind_pw", "")
38 conn
.simple_bind(bind_dn
, bind_pw
)
42 def _query(self
, query
, attrlist
=None, limit
=0):
43 logging
.debug("Performing LDAP query: %s" % query
)
45 search_base
= self
.settings
.get("ldap_search_base")
48 results
= self
.ldap
.search_ext_s(search_base
, ldap
.SCOPE_SUBTREE
,
49 query
, attrlist
=attrlist
, sizelimit
=limit
)
51 # Close current connection
59 def _search(self
, query
, attrlist
=None, limit
=0):
62 for dn
, attrs
in self
._query
(query
, attrlist
=attrlist
, limit
=limit
):
63 account
= Account(self
.backend
, dn
, attrs
)
64 accounts
.append(account
)
68 def search(self
, query
):
69 # Search for exact matches
70 accounts
= self
._search
("(&(objectClass=posixAccount) \
71 (|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
72 % (query
, query
, query
, query
, query
, query
))
74 # Find accounts by name
76 for account
in self
._search
("(&(objectClass=posixAccount)(|(cn=*%s*)(uid=*%s*)))" % (query
, query
)):
77 if not account
in accounts
:
78 accounts
.append(account
)
80 return sorted(accounts
)
82 def _search_one(self
, query
):
83 result
= self
._search
(query
, limit
=1)
84 assert len(result
) <= 1
89 def get_by_uid(self
, uid
):
90 return self
._search
_one
("(&(objectClass=posixAccount)(uid=%s))" % uid
)
92 def get_by_mail(self
, mail
):
93 return self
._search
_one
("(&(objectClass=posixAccount)(mail=%s))" % mail
)
97 def find_account(self
, s
):
98 account
= self
.get_by_uid(s
)
102 return self
.get_by_mail(s
)
104 def get_by_sip_id(self
, sip_id
):
105 return self
._search
_one
("(|(&(objectClass=sipUser)(sipAuthenticationUser=%s)) \
106 (&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" % (sip_id
, sip_id
))
108 def get_by_phone_number(self
, number
):
109 return self
._search
_one
("(&(objectClass=posixAccount) \
110 (|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
111 % (number
, number
, number
, number
))
115 def _cleanup_expired_sessions(self
):
116 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
118 def create_session(self
, account
, host
):
119 self
._cleanup
_expired
_sessions
()
121 res
= self
.db
.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
122 RETURNING session_id, time_expires", host
, account
.uid
)
124 # Session could not be created
128 logging
.info("Created session %s for %s which expires %s" \
129 % (res
.session_id
, account
, res
.time_expires
))
130 return res
.session_id
, res
.time_expires
132 def destroy_session(self
, session_id
, host
):
133 logging
.info("Destroying session %s" % session_id
)
135 self
.db
.execute("DELETE FROM sessions \
136 WHERE session_id = %s AND host = %s", session_id
, host
)
137 self
._cleanup
_expired
_sessions
()
139 def get_by_session(self
, session_id
, host
):
140 logging
.debug("Looking up session %s" % session_id
)
142 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
143 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
146 # Session does not exist or has expired
150 # Update the session expiration time
151 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
152 WHERE session_id = %s AND host = %s", session_id
, host
)
154 return self
.get_by_uid(res
.uid
)
157 class Account(Object
):
158 def __init__(self
, backend
, dn
, attrs
=None):
159 Object
.__init
__(self
, backend
)
162 self
.attributes
= attrs
or {}
168 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
170 def __eq__(self
, other
):
171 if isinstance(other
, self
.__class
__):
172 return self
.dn
== other
.dn
174 def __lt__(self
, other
):
175 if isinstance(other
, self
.__class
__):
176 return self
.name
< other
.name
180 return self
.accounts
.ldap
182 def _exists(self
, key
):
191 for value
in self
.attributes
.get(key
, []):
194 def _get_bytes(self
, key
, default
=None):
195 for value
in self
._get
(key
):
200 def _get_strings(self
, key
):
201 for value
in self
._get
(key
):
204 def _get_string(self
, key
, default
=None):
205 for value
in self
._get
_strings
(key
):
210 def _get_phone_numbers(self
, key
):
211 for value
in self
._get
_strings
(key
):
212 yield phonenumbers
.parse(value
, None)
214 def _modify(self
, modlist
):
215 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
217 # Run modify operation
218 self
.ldap
.modify_s(self
.dn
, modlist
)
220 def _set(self
, key
, values
):
221 current
= self
._get
(key
)
223 # Don't do anything if nothing has changed
224 if list(current
) == values
:
227 # Remove all old values and add all new ones
230 if self
._exists
(key
):
231 modlist
.append((ldap
.MOD_DELETE
, key
, None))
235 modlist
.append((ldap
.MOD_ADD
, key
, values
))
237 # Run modify operation
238 self
._modify
(modlist
)
241 self
.attributes
.update({ key
: values
})
243 def _set_bytes(self
, key
, values
):
244 return self
._set
(key
, values
)
246 def _set_strings(self
, key
, values
):
247 return self
._set
(key
, [e
.encode() for e
in values
if e
])
249 def _set_string(self
, key
, value
):
250 return self
._set
_strings
(key
, [value
,])
252 def _add(self
, key
, values
):
254 (ldap
.MOD_ADD
, key
, values
),
257 self
._modify
(modlist
)
259 def _add_strings(self
, key
, values
):
260 return self
._add
(key
, [e
.encode() for e
in values
])
262 def _add_string(self
, key
, value
):
263 return self
._add
_strings
(key
, [value
,])
265 def _delete(self
, key
, values
):
267 (ldap
.MOD_DELETE
, key
, values
),
270 self
._modify
(modlist
)
272 def _delete_strings(self
, key
, values
):
273 return self
._delete
(key
, [e
.encode() for e
in values
])
275 def _delete_string(self
, key
, value
):
276 return self
._delete
_strings
(key
, [value
,])
278 def passwd(self
, password
):
282 # The new password must have a score of 3 or better
283 quality
= self
.check_password_quality(password
)
284 if quality
["score"] < 3:
285 raise ValueError("Password too weak")
287 self
.ldap
.passwd_s(self
.dn
, None, password
)
289 def check_password(self
, password
):
291 Bind to the server with given credentials and return
292 true if password is corrent and false if not.
294 Raises exceptions from the server on any other errors.
299 logging
.debug("Checking credentials for %s" % self
.dn
)
301 # Create a new LDAP connection
302 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
303 conn
= ldap
.initialize(ldap_uri
)
306 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
307 except ldap
.INVALID_CREDENTIALS
:
308 logging
.debug("Account credentials are invalid for %s" % self
)
311 logging
.info("Successfully authenticated %s" % self
)
315 def check_password_quality(self
, password
):
317 Passwords are passed through zxcvbn to make sure
318 that they are strong enough.
320 return zxcvbn
.zxcvbn(password
, user_inputs
=(
321 self
.first_name
, self
.last_name
,
325 return "wheel" in self
.groups
327 def is_talk_enabled(self
):
328 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes \
329 or self
.telephone_numbers
or self
.address
331 def can_be_managed_by(self
, account
):
333 Returns True if account is allowed to manage this account
335 # Admins can manage all accounts
336 if account
.is_admin():
339 # Users can manage themselves
340 return self
== account
344 return self
._get
_strings
("objectClass")
348 return self
._get
_string
("uid")
352 return self
._get
_string
("cn")
356 def get_first_name(self
):
357 return self
._get
_string
("givenName")
359 def set_first_name(self
, first_name
):
360 self
._set
_string
("givenName", first_name
)
363 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
365 first_name
= property(get_first_name
, set_first_name
)
369 def get_last_name(self
):
370 return self
._get
_string
("sn")
372 def set_last_name(self
, last_name
):
373 self
._set
_string
("sn", last_name
)
376 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
378 last_name
= property(get_last_name
, set_last_name
)
384 res
= self
.accounts
._query
("(&(objectClass=posixGroup) \
385 (memberUid=%s))" % self
.uid
, ["cn"])
387 for dn
, attrs
in res
:
388 cns
= attrs
.get("cn")
390 groups
.append(cns
[0].decode())
396 def get_address(self
):
397 address
= self
._get
_string
("homePostalAddress")
400 return (line
.strip() for line
in address
.split(","))
404 def set_address(self
, address
):
405 data
= ", ".join(address
.splitlines())
407 self
._set
_bytes
("homePostalAddress", data
.encode())
409 address
= property(get_address
, set_address
)
413 name
= self
.name
.lower()
414 name
= name
.replace(" ", ".")
415 name
= name
.replace("Ä", "Ae")
416 name
= name
.replace("Ö", "Oe")
417 name
= name
.replace("Ü", "Ue")
418 name
= name
.replace("ä", "ae")
419 name
= name
.replace("ö", "oe")
420 name
= name
.replace("ü", "ue")
422 for mail
in self
.attributes
.get("mail", []):
423 if mail
.decode().startswith("%s@ipfire.org" % name
):
426 # If everything else fails, we will go with the UID
427 return "%s@ipfire.org" % self
.uid
429 # Mail Routing Address
431 def get_mail_routing_address(self
):
432 return self
._get
_string
("mailRoutingAddress", None)
434 def set_mail_routing_address(self
, address
):
435 self
._set
_string
("mailRoutingAddress", address
or None)
437 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
441 if "sipUser" in self
.classes
:
442 return self
._get
_string
("sipAuthenticationUser")
444 if "sipRoutingObject" in self
.classes
:
445 return self
._get
_string
("sipLocalAddress")
448 def sip_password(self
):
449 return self
._get
_string
("sipPassword")
452 def _generate_sip_password():
453 return util
.random_string(8)
457 return "%s@ipfire.org" % self
.sip_id
459 def uses_sip_forwarding(self
):
460 if self
.sip_routing_address
:
467 def get_sip_routing_address(self
):
468 if "sipRoutingObject" in self
.classes
:
469 return self
._get
_string
("sipRoutingAddress")
471 def set_sip_routing_address(self
, address
):
475 # Don't do anything if nothing has changed
476 if self
.get_sip_routing_address() == address
:
481 # This is no longer a SIP user any more
482 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
483 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
484 (ldap
.MOD_DELETE
, "sipPassword", None),
486 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
487 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
488 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
492 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
493 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
494 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
496 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
497 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
498 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
502 self
._modify
(modlist
)
504 # XXX Cache is invalid here
506 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
509 def sip_registrations(self
):
510 sip_registrations
= []
512 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
515 sip_registrations
.append(reg
)
517 return sip_registrations
520 def sip_channels(self
):
521 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
523 def get_cdr(self
, date
=None, limit
=None):
524 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
529 def phone_number(self
):
531 Returns the IPFire phone number
534 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
537 def fax_number(self
):
539 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
541 def get_phone_numbers(self
):
544 for field
in ("telephoneNumber", "homePhone", "mobile"):
545 for number
in self
._get
_phone
_numbers
(field
):
550 def set_phone_numbers(self
, phone_numbers
):
551 # Sort phone numbers by landline and mobile
552 _landline_numbers
= []
555 for number
in phone_numbers
:
557 number
= phonenumbers
.parse(number
, None)
558 except phonenumbers
.phonenumberutil
.NumberParseException
:
561 # Convert to string (in E.164 format)
562 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
564 # Separate mobile numbers
565 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
566 _mobile_numbers
.append(s
)
568 _landline_numbers
.append(s
)
571 self
._set
_strings
("telephoneNumber", _landline_numbers
)
572 self
._set
_strings
("mobile", _mobile_numbers
)
574 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
577 def _all_telephone_numbers(self
):
578 ret
= [ self
.sip_id
, ]
580 if self
.phone_number
:
581 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
584 for number
in self
.phone_numbers
:
585 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
590 def avatar_url(self
, size
=None):
591 if self
.backend
.debug
:
592 hostname
= "http://people.dev.ipfire.org"
594 hostname
= "https://people.ipfire.org"
596 url
= "%s/users/%s.jpg" % (hostname
, self
.uid
)
599 url
+= "?size=%s" % size
603 def get_avatar(self
, size
=None):
604 avatar
= self
._get
_bytes
("jpegPhoto")
611 return self
._resize
_avatar
(avatar
, size
)
613 def _resize_avatar(self
, image
, size
):
614 image
= PIL
.Image
.open(io
.BytesIO(image
))
616 # Convert RGBA images into RGB because JPEG doesn't support alpha-channels
617 if image
.mode
== "RGBA":
618 image
= image
.convert("RGB")
620 # Resize the image to the desired resolution (and make it square)
621 thumbnail
= PIL
.ImageOps
.fit(image
, (size
, size
), PIL
.Image
.ANTIALIAS
)
623 with io
.BytesIO() as f
:
624 # If writing out the image does not work with optimization,
625 # we try to write it out without any optimization.
627 thumbnail
.save(f
, "JPEG", optimize
=True, quality
=98)
629 thumbnail
.save(f
, "JPEG", quality
=98)
633 def upload_avatar(self
, avatar
):
634 self
._set
("jpegPhoto", avatar
)
642 for key
in self
._get
_strings
("sshPublicKey"):
643 s
= sshpubkeys
.SSHKey()
647 except (sshpubkeys
.InvalidKeyError
, NotImplementedError) as e
:
648 logging
.warning("Could not parse SSH key %s: %s" % (key
, e
))
655 def get_ssh_key_by_hash_sha256(self
, hash_sha256
):
656 for key
in self
.ssh_keys
:
657 if not key
.hash_sha256() == hash_sha256
:
662 def add_ssh_key(self
, key
):
663 k
= sshpubkeys
.SSHKey()
665 # Try to parse the key
668 # Check for types and sufficient sizes
669 if k
.key_type
== b
"ssh-rsa":
671 raise sshpubkeys
.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
673 elif k
.key_type
== b
"ssh-dss":
674 raise sshpubkeys
.InvalidKeyError("DSA keys are not supported")
676 # Ignore any duplicates
677 if key
in (k
.keydata
for k
in self
.ssh_keys
):
678 logging
.debug("SSH Key has already been added for %s: %s" % (self
, key
))
682 self
._add
_string
("sshPublicKey", key
)
685 self
.ssh_keys
.append(k
)
687 def delete_ssh_key(self
, key
):
688 if not key
in (k
.keydata
for k
in self
.ssh_keys
):
691 # Delete key from LDAP
692 self
._delete
_string
("sshPublicKey", key
)
695 if __name__
== "__main__":