]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
17 from . import countries
19 from .decorators
import *
20 from .misc
import Object
22 class Accounts(Object
):
24 # Only return developers (group with ID 1000)
25 accounts
= self
._search
("(&(objectClass=posixAccount)(gidNumber=1000))")
27 return iter(sorted(accounts
))
31 # Connect to LDAP server
32 ldap_uri
= self
.settings
.get("ldap_uri")
33 conn
= ldap
.initialize(ldap_uri
)
35 # Bind with username and password
36 bind_dn
= self
.settings
.get("ldap_bind_dn")
38 bind_pw
= self
.settings
.get("ldap_bind_pw", "")
39 conn
.simple_bind(bind_dn
, bind_pw
)
43 def _query(self
, query
, attrlist
=None, limit
=0):
44 logging
.debug("Performing LDAP query: %s" % query
)
46 search_base
= self
.settings
.get("ldap_search_base")
49 results
= self
.ldap
.search_ext_s(search_base
, ldap
.SCOPE_SUBTREE
,
50 query
, attrlist
=attrlist
, sizelimit
=limit
)
52 # 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=person) \
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=person)(|(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 uid_exists(self
, uid
):
90 if self
.get_by_uid(uid
):
93 res
= self
.db
.get("SELECT 1 FROM account_activations \
94 WHERE uid = %s AND expires_at > NOW()", uid
)
99 # Account with uid does not exist, yet
102 def get_by_uid(self
, uid
):
103 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
105 def get_by_mail(self
, mail
):
106 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
110 def find_account(self
, s
):
111 account
= self
.get_by_uid(s
)
115 return self
.get_by_mail(s
)
117 def get_by_sip_id(self
, sip_id
):
118 return self
._search
_one
("(|(&(objectClass=sipUser)(sipAuthenticationUser=%s)) \
119 (&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" % (sip_id
, sip_id
))
121 def get_by_phone_number(self
, number
):
122 return self
._search
_one
("(&(objectClass=inetOrgPerson) \
123 (|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
124 % (number
, number
, number
, number
))
128 def create(self
, uid
, email
, first_name
, last_name
):
129 # Check if UID is unique
130 if self
.get_by_uid(uid
):
131 raise ValueError("UID exists: %s" % uid
)
133 activation_code
= util
.random_string(24)
137 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
138 "userPassword" : activation_code
.encode(),
139 "mail" : email
.encode(),
142 "cn" : b
"%s %s" % (first_name
.encode(), last_name
.encode()),
143 "sn" : last_name
.encode(),
144 "givenName" : first_name
.encode(),
147 # Create account on LDAP
148 self
.ldap
.add_s("uid=%s,ou=People,dc=mcfly,dc=local" % uid
, ldap
.modlist
.addModlist(account
))
150 # TODO Send email with activation code
155 def _cleanup_expired_sessions(self
):
156 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
158 def create_session(self
, account
, host
):
159 self
._cleanup
_expired
_sessions
()
161 res
= self
.db
.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
162 RETURNING session_id, time_expires", host
, account
.uid
)
164 # Session could not be created
168 logging
.info("Created session %s for %s which expires %s" \
169 % (res
.session_id
, account
, res
.time_expires
))
170 return res
.session_id
, res
.time_expires
172 def destroy_session(self
, session_id
, host
):
173 logging
.info("Destroying session %s" % session_id
)
175 self
.db
.execute("DELETE FROM sessions \
176 WHERE session_id = %s AND host = %s", session_id
, host
)
177 self
._cleanup
_expired
_sessions
()
179 def get_by_session(self
, session_id
, host
):
180 logging
.debug("Looking up session %s" % session_id
)
182 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
183 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
186 # Session does not exist or has expired
190 # Update the session expiration time
191 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
192 WHERE session_id = %s AND host = %s", session_id
, host
)
194 return self
.get_by_uid(res
.uid
)
197 class Account(Object
):
198 def __init__(self
, backend
, dn
, attrs
=None):
199 Object
.__init
__(self
, backend
)
202 self
.attributes
= attrs
or {}
208 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
210 def __eq__(self
, other
):
211 if isinstance(other
, self
.__class
__):
212 return self
.dn
== other
.dn
214 def __lt__(self
, other
):
215 if isinstance(other
, self
.__class
__):
216 return self
.name
< other
.name
220 return self
.accounts
.ldap
222 def _exists(self
, key
):
231 for value
in self
.attributes
.get(key
, []):
234 def _get_bytes(self
, key
, default
=None):
235 for value
in self
._get
(key
):
240 def _get_strings(self
, key
):
241 for value
in self
._get
(key
):
244 def _get_string(self
, key
, default
=None):
245 for value
in self
._get
_strings
(key
):
250 def _get_phone_numbers(self
, key
):
251 for value
in self
._get
_strings
(key
):
252 yield phonenumbers
.parse(value
, None)
254 def _modify(self
, modlist
):
255 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
257 # Run modify operation
258 self
.ldap
.modify_s(self
.dn
, modlist
)
260 def _set(self
, key
, values
):
261 current
= self
._get
(key
)
263 # Don't do anything if nothing has changed
264 if list(current
) == values
:
267 # Remove all old values and add all new ones
270 if self
._exists
(key
):
271 modlist
.append((ldap
.MOD_DELETE
, key
, None))
275 modlist
.append((ldap
.MOD_ADD
, key
, values
))
277 # Run modify operation
278 self
._modify
(modlist
)
281 self
.attributes
.update({ key
: values
})
283 def _set_bytes(self
, key
, values
):
284 return self
._set
(key
, values
)
286 def _set_strings(self
, key
, values
):
287 return self
._set
(key
, [e
.encode() for e
in values
if e
])
289 def _set_string(self
, key
, value
):
290 return self
._set
_strings
(key
, [value
,])
292 def _add(self
, key
, values
):
294 (ldap
.MOD_ADD
, key
, values
),
297 self
._modify
(modlist
)
299 def _add_strings(self
, key
, values
):
300 return self
._add
(key
, [e
.encode() for e
in values
])
302 def _add_string(self
, key
, value
):
303 return self
._add
_strings
(key
, [value
,])
305 def _delete(self
, key
, values
):
307 (ldap
.MOD_DELETE
, key
, values
),
310 self
._modify
(modlist
)
312 def _delete_strings(self
, key
, values
):
313 return self
._delete
(key
, [e
.encode() for e
in values
])
315 def _delete_string(self
, key
, value
):
316 return self
._delete
_strings
(key
, [value
,])
318 def passwd(self
, password
):
322 # The new password must have a score of 3 or better
323 quality
= self
.check_password_quality(password
)
324 if quality
["score"] < 3:
325 raise ValueError("Password too weak")
327 self
.ldap
.passwd_s(self
.dn
, None, password
)
329 def check_password(self
, password
):
331 Bind to the server with given credentials and return
332 true if password is corrent and false if not.
334 Raises exceptions from the server on any other errors.
339 logging
.debug("Checking credentials for %s" % self
.dn
)
341 # Create a new LDAP connection
342 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
343 conn
= ldap
.initialize(ldap_uri
)
346 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
347 except ldap
.INVALID_CREDENTIALS
:
348 logging
.debug("Account credentials are invalid for %s" % self
)
351 logging
.info("Successfully authenticated %s" % self
)
355 def check_password_quality(self
, password
):
357 Passwords are passed through zxcvbn to make sure
358 that they are strong enough.
360 return zxcvbn
.zxcvbn(password
, user_inputs
=(
361 self
.first_name
, self
.last_name
,
365 return "wheel" in self
.groups
368 return "staff" in self
.groups
371 return "posixAccount" in self
.classes
374 return "postfixMailUser" in self
.classes
377 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
379 def can_be_managed_by(self
, account
):
381 Returns True if account is allowed to manage this account
383 # Admins can manage all accounts
384 if account
.is_admin():
387 # Users can manage themselves
388 return self
== account
392 return self
._get
_strings
("objectClass")
396 return self
._get
_string
("uid")
400 return self
._get
_string
("cn")
404 def get_first_name(self
):
405 return self
._get
_string
("givenName")
407 def set_first_name(self
, first_name
):
408 self
._set
_string
("givenName", first_name
)
411 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
413 first_name
= property(get_first_name
, set_first_name
)
417 def get_last_name(self
):
418 return self
._get
_string
("sn")
420 def set_last_name(self
, last_name
):
421 self
._set
_string
("sn", last_name
)
424 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
426 last_name
= property(get_last_name
, set_last_name
)
432 res
= self
.accounts
._query
("(&(objectClass=posixGroup) \
433 (memberUid=%s))" % self
.uid
, ["cn"])
435 for dn
, attrs
in res
:
436 cns
= attrs
.get("cn")
438 groups
.append(cns
[0].decode())
449 address
+= self
.street
.splitlines()
451 if self
.postal_code
and self
.city
:
452 if self
.country_code
in ("AT", "DE"):
453 address
.append("%s %s" % (self
.postal_code
, self
.city
))
455 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
457 address
.append(self
.city
or self
.postal_code
)
459 if self
.country_name
:
460 address
.append(self
.country_name
)
464 def get_street(self
):
465 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
467 def set_street(self
, street
):
468 self
._set
_string
("street", street
)
470 street
= property(get_street
, set_street
)
473 return self
._get
_string
("l") or ""
475 def set_city(self
, city
):
476 self
._set
_string
("l", city
)
478 city
= property(get_city
, set_city
)
480 def get_postal_code(self
):
481 return self
._get
_string
("postalCode") or ""
483 def set_postal_code(self
, postal_code
):
484 self
._set
_string
("postalCode", postal_code
)
486 postal_code
= property(get_postal_code
, set_postal_code
)
488 # XXX This should be c
489 def get_country_code(self
):
490 return self
._get
_string
("st")
492 def set_country_code(self
, country_code
):
493 self
._set
_string
("st", country_code
)
495 country_code
= property(get_country_code
, set_country_code
)
498 def country_name(self
):
499 if self
.country_code
:
500 return countries
.get_name(self
.country_code
)
504 name
= self
.name
.lower()
505 name
= name
.replace(" ", ".")
506 name
= name
.replace("Ä", "Ae")
507 name
= name
.replace("Ö", "Oe")
508 name
= name
.replace("Ü", "Ue")
509 name
= name
.replace("ä", "ae")
510 name
= name
.replace("ö", "oe")
511 name
= name
.replace("ü", "ue")
513 for mail
in self
.attributes
.get("mail", []):
514 if mail
.decode().startswith("%s@ipfire.org" % name
):
517 # If everything else fails, we will go with the UID
518 return "%s@ipfire.org" % self
.uid
520 # Mail Routing Address
522 def get_mail_routing_address(self
):
523 return self
._get
_string
("mailRoutingAddress", None)
525 def set_mail_routing_address(self
, address
):
526 self
._set
_string
("mailRoutingAddress", address
or None)
528 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
532 if "sipUser" in self
.classes
:
533 return self
._get
_string
("sipAuthenticationUser")
535 if "sipRoutingObject" in self
.classes
:
536 return self
._get
_string
("sipLocalAddress")
539 def sip_password(self
):
540 return self
._get
_string
("sipPassword")
543 def _generate_sip_password():
544 return util
.random_string(8)
548 return "%s@ipfire.org" % self
.sip_id
550 def uses_sip_forwarding(self
):
551 if self
.sip_routing_address
:
558 def get_sip_routing_address(self
):
559 if "sipRoutingObject" in self
.classes
:
560 return self
._get
_string
("sipRoutingAddress")
562 def set_sip_routing_address(self
, address
):
566 # Don't do anything if nothing has changed
567 if self
.get_sip_routing_address() == address
:
572 # This is no longer a SIP user any more
573 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
574 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
575 (ldap
.MOD_DELETE
, "sipPassword", None),
577 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
578 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
579 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
583 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
584 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
585 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
587 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
588 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
589 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
593 self
._modify
(modlist
)
595 # XXX Cache is invalid here
597 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
600 def sip_registrations(self
):
601 sip_registrations
= []
603 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
606 sip_registrations
.append(reg
)
608 return sip_registrations
611 def sip_channels(self
):
612 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
614 def get_cdr(self
, date
=None, limit
=None):
615 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
620 def phone_number(self
):
622 Returns the IPFire phone number
625 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
628 def fax_number(self
):
630 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
632 def get_phone_numbers(self
):
635 for field
in ("telephoneNumber", "homePhone", "mobile"):
636 for number
in self
._get
_phone
_numbers
(field
):
641 def set_phone_numbers(self
, phone_numbers
):
642 # Sort phone numbers by landline and mobile
643 _landline_numbers
= []
646 for number
in phone_numbers
:
648 number
= phonenumbers
.parse(number
, None)
649 except phonenumbers
.phonenumberutil
.NumberParseException
:
652 # Convert to string (in E.164 format)
653 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
655 # Separate mobile numbers
656 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
657 _mobile_numbers
.append(s
)
659 _landline_numbers
.append(s
)
662 self
._set
_strings
("telephoneNumber", _landline_numbers
)
663 self
._set
_strings
("mobile", _mobile_numbers
)
665 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
668 def _all_telephone_numbers(self
):
669 ret
= [ self
.sip_id
, ]
671 if self
.phone_number
:
672 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
675 for number
in self
.phone_numbers
:
676 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
681 def avatar_url(self
, size
=None):
682 if self
.backend
.debug
:
683 hostname
= "http://people.dev.ipfire.org"
685 hostname
= "https://people.ipfire.org"
687 url
= "%s/users/%s.jpg" % (hostname
, self
.uid
)
690 url
+= "?size=%s" % size
694 def get_avatar(self
, size
=None):
695 avatar
= self
._get
_bytes
("jpegPhoto")
702 return self
._resize
_avatar
(avatar
, size
)
704 def _resize_avatar(self
, image
, size
):
705 image
= PIL
.Image
.open(io
.BytesIO(image
))
707 # Convert RGBA images into RGB because JPEG doesn't support alpha-channels
708 if image
.mode
== "RGBA":
709 image
= image
.convert("RGB")
711 # Resize the image to the desired resolution (and make it square)
712 thumbnail
= PIL
.ImageOps
.fit(image
, (size
, size
), PIL
.Image
.ANTIALIAS
)
714 with io
.BytesIO() as f
:
715 # If writing out the image does not work with optimization,
716 # we try to write it out without any optimization.
718 thumbnail
.save(f
, "JPEG", optimize
=True, quality
=98)
720 thumbnail
.save(f
, "JPEG", quality
=98)
724 def upload_avatar(self
, avatar
):
725 self
._set
("jpegPhoto", avatar
)
733 for key
in self
._get
_strings
("sshPublicKey"):
734 s
= sshpubkeys
.SSHKey()
738 except (sshpubkeys
.InvalidKeyError
, NotImplementedError) as e
:
739 logging
.warning("Could not parse SSH key %s: %s" % (key
, e
))
746 def get_ssh_key_by_hash_sha256(self
, hash_sha256
):
747 for key
in self
.ssh_keys
:
748 if not key
.hash_sha256() == hash_sha256
:
753 def add_ssh_key(self
, key
):
754 k
= sshpubkeys
.SSHKey()
756 # Try to parse the key
759 # Check for types and sufficient sizes
760 if k
.key_type
== b
"ssh-rsa":
762 raise sshpubkeys
.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
764 elif k
.key_type
== b
"ssh-dss":
765 raise sshpubkeys
.InvalidKeyError("DSA keys are not supported")
767 # Ignore any duplicates
768 if key
in (k
.keydata
for k
in self
.ssh_keys
):
769 logging
.debug("SSH Key has already been added for %s: %s" % (self
, key
))
772 # Prepare transaction
775 # Add object class if user is not in it, yet
776 if not "ldapPublicKey" in self
.classes
:
777 modlist
.append((ldap
.MOD_ADD
, "objectClass", b
"ldapPublicKey"))
780 modlist
.append((ldap
.MOD_ADD
, "sshPublicKey", key
.encode()))
783 self
._modify
(modlist
)
786 self
.ssh_keys
.append(k
)
788 def delete_ssh_key(self
, key
):
789 if not key
in (k
.keydata
for k
in self
.ssh_keys
):
792 # Delete key from LDAP
793 if len(self
.ssh_keys
) > 1:
794 self
._delete
_string
("sshPublicKey", key
)
797 (ldap
.MOD_DELETE
, "objectClass", b
"ldapPublicKey"),
798 (ldap
.MOD_DELETE
, "sshPublicKey", key
.encode()),
802 if __name__
== "__main__":