]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
18 from . import countries
20 from .decorators
import *
21 from .misc
import Object
23 class Accounts(Object
):
25 self
.search_base
= self
.settings
.get("ldap_search_base")
28 # Only return developers (group with ID 1000)
29 accounts
= self
._search
("(&(objectClass=posixAccount)(gidNumber=1000))")
31 return iter(sorted(accounts
))
35 # Connect to LDAP server
36 ldap_uri
= self
.settings
.get("ldap_uri")
38 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
40 # Connect to the LDAP server
41 conn
= ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
42 retry_max
=10, retry_delay
=3)
44 # Bind with username and password
45 bind_dn
= self
.settings
.get("ldap_bind_dn")
47 bind_pw
= self
.settings
.get("ldap_bind_pw", "")
48 conn
.simple_bind(bind_dn
, bind_pw
)
52 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None, scope
=None):
53 logging
.debug("Performing LDAP query: %s" % query
)
57 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
58 scope
or ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
)
60 # Log time it took to perform the query
61 logging
.debug("Query took %.2fms" % ((time
.time() - t
) * 1000.0))
65 def _search(self
, query
, attrlist
=None, limit
=0):
66 dns
= self
._search
_dns
(query
, limit
=limit
)
68 accounts
= self
._get
_accounts
_from
_dns
(dns
)
72 def _search_dns(self
, query
, limit
=0):
74 dns
= self
.memcache
.get("accounts:search:%s" % query
)
76 # Return DNs only for this search query
78 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
82 self
.memcache
.set("accounts:search:%s" % query
, dns
, 600)
86 def _get_accounts_from_dns(self
, dns
):
88 yield self
.get_by_dn(dn
)
90 def get_by_dn(self
, dn
):
91 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
93 attrs
= self
._get
_attrs
(dn
)
96 # Cache all attributes for 5 min
97 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
99 return Account(self
.backend
, dn
, attrs
)
101 def _get_attrs(self
, dn
):
103 Fetches all attributes for the given distinguished name
105 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1)
107 for dn
, attrs
in results
:
110 def search(self
, query
):
111 # Search for exact matches
112 accounts
= self
._search
(
113 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
114 % (query
, query
, query
, query
, query
, query
))
116 # Find accounts by name
118 for account
in self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query
, query
)):
119 if not account
in accounts
:
120 accounts
.append(account
)
122 return sorted(accounts
)
124 def _search_one(self
, query
):
125 results
= self
._search
(query
, limit
=1)
127 for result
in results
:
130 def uid_exists(self
, uid
):
131 if self
.get_by_uid(uid
):
134 res
= self
.db
.get("SELECT 1 FROM account_activations \
135 WHERE uid = %s AND expires_at > NOW()", uid
)
140 # Account with uid does not exist, yet
143 def get_by_uid(self
, uid
):
144 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
146 def get_by_mail(self
, mail
):
147 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
149 def find_account(self
, s
):
150 account
= self
.get_by_uid(s
)
154 return self
.get_by_mail(s
)
156 def get_by_sip_id(self
, sip_id
):
160 return self
._search
_one
(
161 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
164 def get_by_phone_number(self
, number
):
168 return self
._search
_one
(
169 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
170 % (number
, number
, number
, number
))
174 def register(self
, uid
, email
, first_name
, last_name
):
175 # Check if UID is unique
176 if self
.get_by_uid(uid
):
177 raise ValueError("UID exists: %s" % uid
)
179 # Generate a random activation code
180 activation_code
= util
.random_string(36)
182 # Create an entry in our database until the user
183 # has activated the account
184 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
185 email, first_name, last_name) VALUES(%s, %s, %s, %s, %s)",
186 uid
, activation_code
, email
, first_name
, last_name
)
188 # Send an account activation email
189 self
.backend
.messages
.send_template("auth/messages/register",
190 recipients
=[email
], priority
=100, uid
=uid
,
191 activation_code
=activation_code
, email
=email
,
192 first_name
=first_name
, last_name
=last_name
)
194 def activate(self
, uid
, activation_code
):
195 res
= self
.db
.get("DELETE FROM account_activations \
196 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
197 RETURNING *", uid
, activation_code
)
199 # Return nothing when account was not found
203 # Create a new account on the LDAP database
204 return self
.create(uid
, res
.email
,
205 first_name
=res
.first_name
, last_name
=res
.last_name
)
207 def create(self
, uid
, email
, first_name
, last_name
):
210 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
211 "mail" : email
.encode(),
214 "cn" : b
"%s %s" % (first_name
.encode(), last_name
.encode()),
215 "sn" : last_name
.encode(),
216 "givenName" : first_name
.encode(),
219 logging
.info("Creating new account: %s: %s" % (uid
, account
))
222 dn
= "uid=%s,ou=People,dc=mcfly,dc=local" % uid
224 # Create account on LDAP
225 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
228 return self
.get_by_dn(dn
)
232 def _cleanup_expired_sessions(self
):
233 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
235 def create_session(self
, account
, host
):
236 self
._cleanup
_expired
_sessions
()
238 res
= self
.db
.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
239 RETURNING session_id, time_expires", host
, account
.uid
)
241 # Session could not be created
245 logging
.info("Created session %s for %s which expires %s" \
246 % (res
.session_id
, account
, res
.time_expires
))
247 return res
.session_id
, res
.time_expires
249 def destroy_session(self
, session_id
, host
):
250 logging
.info("Destroying session %s" % session_id
)
252 self
.db
.execute("DELETE FROM sessions \
253 WHERE session_id = %s AND host = %s", session_id
, host
)
254 self
._cleanup
_expired
_sessions
()
256 def get_by_session(self
, session_id
, host
):
257 logging
.debug("Looking up session %s" % session_id
)
259 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
260 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
263 # Session does not exist or has expired
267 # Update the session expiration time
268 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
269 WHERE session_id = %s AND host = %s", session_id
, host
)
271 return self
.get_by_uid(res
.uid
)
274 class Account(Object
):
275 def __init__(self
, backend
, dn
, attrs
=None):
276 Object
.__init
__(self
, backend
)
279 self
.attributes
= attrs
or {}
285 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
287 def __eq__(self
, other
):
288 if isinstance(other
, self
.__class
__):
289 return self
.dn
== other
.dn
291 def __lt__(self
, other
):
292 if isinstance(other
, self
.__class
__):
293 return self
.name
< other
.name
297 return self
.accounts
.ldap
299 def _exists(self
, key
):
308 for value
in self
.attributes
.get(key
, []):
311 def _get_bytes(self
, key
, default
=None):
312 for value
in self
._get
(key
):
317 def _get_strings(self
, key
):
318 for value
in self
._get
(key
):
321 def _get_string(self
, key
, default
=None):
322 for value
in self
._get
_strings
(key
):
327 def _get_phone_numbers(self
, key
):
328 for value
in self
._get
_strings
(key
):
329 yield phonenumbers
.parse(value
, None)
331 def _modify(self
, modlist
):
332 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
334 # Run modify operation
335 self
.ldap
.modify_s(self
.dn
, modlist
)
337 # Delete cached attributes
338 self
.memcache
.delete("accounts:%s:attrs")
340 def _set(self
, key
, values
):
341 current
= self
._get
(key
)
343 # Don't do anything if nothing has changed
344 if list(current
) == values
:
347 # Remove all old values and add all new ones
350 if self
._exists
(key
):
351 modlist
.append((ldap
.MOD_DELETE
, key
, None))
355 modlist
.append((ldap
.MOD_ADD
, key
, values
))
357 # Run modify operation
358 self
._modify
(modlist
)
361 self
.attributes
.update({ key
: values
})
363 def _set_bytes(self
, key
, values
):
364 return self
._set
(key
, values
)
366 def _set_strings(self
, key
, values
):
367 return self
._set
(key
, [e
.encode() for e
in values
if e
])
369 def _set_string(self
, key
, value
):
370 return self
._set
_strings
(key
, [value
,])
372 def _add(self
, key
, values
):
374 (ldap
.MOD_ADD
, key
, values
),
377 self
._modify
(modlist
)
379 def _add_strings(self
, key
, values
):
380 return self
._add
(key
, [e
.encode() for e
in values
])
382 def _add_string(self
, key
, value
):
383 return self
._add
_strings
(key
, [value
,])
385 def _delete(self
, key
, values
):
387 (ldap
.MOD_DELETE
, key
, values
),
390 self
._modify
(modlist
)
392 def _delete_strings(self
, key
, values
):
393 return self
._delete
(key
, [e
.encode() for e
in values
])
395 def _delete_string(self
, key
, value
):
396 return self
._delete
_strings
(key
, [value
,])
398 def passwd(self
, password
):
402 # The new password must have a score of 3 or better
403 quality
= self
.check_password_quality(password
)
404 if quality
["score"] < 3:
405 raise ValueError("Password too weak")
407 self
.ldap
.passwd_s(self
.dn
, None, password
)
409 def check_password(self
, password
):
411 Bind to the server with given credentials and return
412 true if password is corrent and false if not.
414 Raises exceptions from the server on any other errors.
419 logging
.debug("Checking credentials for %s" % self
.dn
)
421 # Create a new LDAP connection
422 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
423 conn
= ldap
.initialize(ldap_uri
)
426 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
427 except ldap
.INVALID_CREDENTIALS
:
428 logging
.debug("Account credentials are invalid for %s" % self
)
431 logging
.info("Successfully authenticated %s" % self
)
435 def check_password_quality(self
, password
):
437 Passwords are passed through zxcvbn to make sure
438 that they are strong enough.
440 return zxcvbn
.zxcvbn(password
, user_inputs
=(
441 self
.first_name
, self
.last_name
,
445 return "wheel" in self
.groups
448 return "staff" in self
.groups
451 return "posixAccount" in self
.classes
454 return "postfixMailUser" in self
.classes
457 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
459 def can_be_managed_by(self
, account
):
461 Returns True if account is allowed to manage this account
463 # Admins can manage all accounts
464 if account
.is_admin():
467 # Users can manage themselves
468 return self
== account
472 return self
._get
_strings
("objectClass")
476 return self
._get
_string
("uid")
480 return self
._get
_string
("cn")
484 def get_first_name(self
):
485 return self
._get
_string
("givenName")
487 def set_first_name(self
, first_name
):
488 self
._set
_string
("givenName", first_name
)
491 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
493 first_name
= property(get_first_name
, set_first_name
)
497 def get_last_name(self
):
498 return self
._get
_string
("sn")
500 def set_last_name(self
, last_name
):
501 self
._set
_string
("sn", last_name
)
504 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
506 last_name
= property(get_last_name
, set_last_name
)
510 groups
= self
.memcache
.get("accounts:%s:groups" % self
.dn
)
514 # Fetch groups from LDAP
515 groups
= self
._get
_groups
()
517 # Cache groups for 5 min
518 self
.memcache
.set("accounts:%s:groups" % self
.dn
, groups
, 300)
522 def _get_groups(self
):
525 res
= self
.accounts
._query
("(&(objectClass=posixGroup) \
526 (memberUid=%s))" % self
.uid
, ["cn"])
528 for dn
, attrs
in res
:
529 cns
= attrs
.get("cn")
531 groups
.append(cns
[0].decode())
542 address
+= self
.street
.splitlines()
544 if self
.postal_code
and self
.city
:
545 if self
.country_code
in ("AT", "DE"):
546 address
.append("%s %s" % (self
.postal_code
, self
.city
))
548 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
550 address
.append(self
.city
or self
.postal_code
)
552 if self
.country_name
:
553 address
.append(self
.country_name
)
557 def get_street(self
):
558 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
560 def set_street(self
, street
):
561 self
._set
_string
("street", street
)
563 street
= property(get_street
, set_street
)
566 return self
._get
_string
("l") or ""
568 def set_city(self
, city
):
569 self
._set
_string
("l", city
)
571 city
= property(get_city
, set_city
)
573 def get_postal_code(self
):
574 return self
._get
_string
("postalCode") or ""
576 def set_postal_code(self
, postal_code
):
577 self
._set
_string
("postalCode", postal_code
)
579 postal_code
= property(get_postal_code
, set_postal_code
)
581 # XXX This should be c
582 def get_country_code(self
):
583 return self
._get
_string
("st")
585 def set_country_code(self
, country_code
):
586 self
._set
_string
("st", country_code
)
588 country_code
= property(get_country_code
, set_country_code
)
591 def country_name(self
):
592 if self
.country_code
:
593 return countries
.get_name(self
.country_code
)
597 return self
._get
_string
("mail")
599 # Mail Routing Address
601 def get_mail_routing_address(self
):
602 return self
._get
_string
("mailRoutingAddress", None)
604 def set_mail_routing_address(self
, address
):
605 self
._set
_string
("mailRoutingAddress", address
or None)
607 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
611 if "sipUser" in self
.classes
:
612 return self
._get
_string
("sipAuthenticationUser")
614 if "sipRoutingObject" in self
.classes
:
615 return self
._get
_string
("sipLocalAddress")
618 def sip_password(self
):
619 return self
._get
_string
("sipPassword")
622 def _generate_sip_password():
623 return util
.random_string(8)
627 return "%s@ipfire.org" % self
.sip_id
629 def uses_sip_forwarding(self
):
630 if self
.sip_routing_address
:
637 def get_sip_routing_address(self
):
638 if "sipRoutingObject" in self
.classes
:
639 return self
._get
_string
("sipRoutingAddress")
641 def set_sip_routing_address(self
, address
):
645 # Don't do anything if nothing has changed
646 if self
.get_sip_routing_address() == address
:
650 # This is no longer a SIP user any more
653 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
654 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
655 (ldap
.MOD_DELETE
, "sipPassword", None),
657 except ldap
.NO_SUCH_ATTRIBUTE
:
660 # Set new routing object
663 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
664 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
665 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
668 # If this is a change, we cannot add this again
669 except ldap
.TYPE_OR_VALUE_EXISTS
:
670 self
._set
_string
("sipRoutingAddress", address
)
674 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
675 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
676 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
678 except ldap
.NO_SUCH_ATTRIBUTE
:
682 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
683 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
684 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
687 # XXX Cache is invalid here
689 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
692 def sip_registrations(self
):
693 sip_registrations
= []
695 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
698 sip_registrations
.append(reg
)
700 return sip_registrations
703 def sip_channels(self
):
704 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
706 def get_cdr(self
, date
=None, limit
=None):
707 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
712 def phone_number(self
):
714 Returns the IPFire phone number
717 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
720 def fax_number(self
):
722 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
724 def get_phone_numbers(self
):
727 for field
in ("telephoneNumber", "homePhone", "mobile"):
728 for number
in self
._get
_phone
_numbers
(field
):
733 def set_phone_numbers(self
, phone_numbers
):
734 # Sort phone numbers by landline and mobile
735 _landline_numbers
= []
738 for number
in phone_numbers
:
740 number
= phonenumbers
.parse(number
, None)
741 except phonenumbers
.phonenumberutil
.NumberParseException
:
744 # Convert to string (in E.164 format)
745 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
747 # Separate mobile numbers
748 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
749 _mobile_numbers
.append(s
)
751 _landline_numbers
.append(s
)
754 self
._set
_strings
("telephoneNumber", _landline_numbers
)
755 self
._set
_strings
("mobile", _mobile_numbers
)
757 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
760 def _all_telephone_numbers(self
):
761 ret
= [ self
.sip_id
, ]
763 if self
.phone_number
:
764 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
767 for number
in self
.phone_numbers
:
768 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
773 def avatar_url(self
, size
=None):
774 if self
.backend
.debug
:
775 hostname
= "http://people.dev.ipfire.org"
777 hostname
= "https://people.ipfire.org"
779 url
= "%s/users/%s.jpg" % (hostname
, self
.uid
)
782 url
+= "?size=%s" % size
786 def get_avatar(self
, size
=None):
787 avatar
= self
._get
_bytes
("jpegPhoto")
794 return self
._resize
_avatar
(avatar
, size
)
796 def _resize_avatar(self
, image
, size
):
797 image
= PIL
.Image
.open(io
.BytesIO(image
))
799 # Resize the image to the desired resolution (and make it square)
800 thumbnail
= PIL
.ImageOps
.fit(image
, (size
, size
), PIL
.Image
.ANTIALIAS
)
802 with io
.BytesIO() as f
:
803 # If writing out the image does not work with optimization,
804 # we try to write it out without any optimization.
806 thumbnail
.save(f
, image
.format
, optimize
=True, quality
=98)
808 thumbnail
.save(f
, image
.format
, quality
=98)
812 def upload_avatar(self
, avatar
):
813 self
._set
("jpegPhoto", avatar
)
821 for key
in self
._get
_strings
("sshPublicKey"):
822 s
= sshpubkeys
.SSHKey()
826 except (sshpubkeys
.InvalidKeyError
, NotImplementedError) as e
:
827 logging
.warning("Could not parse SSH key %s: %s" % (key
, e
))
834 def get_ssh_key_by_hash_sha256(self
, hash_sha256
):
835 for key
in self
.ssh_keys
:
836 if not key
.hash_sha256() == hash_sha256
:
841 def add_ssh_key(self
, key
):
842 k
= sshpubkeys
.SSHKey()
844 # Try to parse the key
847 # Check for types and sufficient sizes
848 if k
.key_type
== b
"ssh-rsa":
850 raise sshpubkeys
.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
852 elif k
.key_type
== b
"ssh-dss":
853 raise sshpubkeys
.InvalidKeyError("DSA keys are not supported")
855 # Ignore any duplicates
856 if key
in (k
.keydata
for k
in self
.ssh_keys
):
857 logging
.debug("SSH Key has already been added for %s: %s" % (self
, key
))
860 # Prepare transaction
863 # Add object class if user is not in it, yet
864 if not "ldapPublicKey" in self
.classes
:
865 modlist
.append((ldap
.MOD_ADD
, "objectClass", b
"ldapPublicKey"))
868 modlist
.append((ldap
.MOD_ADD
, "sshPublicKey", key
.encode()))
871 self
._modify
(modlist
)
874 self
.ssh_keys
.append(k
)
876 def delete_ssh_key(self
, key
):
877 if not key
in (k
.keydata
for k
in self
.ssh_keys
):
880 # Delete key from LDAP
881 if len(self
.ssh_keys
) > 1:
882 self
._delete
_string
("sshPublicKey", key
)
885 (ldap
.MOD_DELETE
, "objectClass", b
"ldapPublicKey"),
886 (ldap
.MOD_DELETE
, "sshPublicKey", key
.encode()),
890 if __name__
== "__main__":