]>
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):
53 logging
.debug("Performing LDAP query: %s" % query
)
57 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
58 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):
67 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
68 account
= self
.get_by_dn(dn
)
69 accounts
.append(account
)
73 def _get_attrs(self
, dn
):
75 Fetches all attributes for the given distinguished name
77 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1)
79 for dn
, attrs
in results
:
82 def get_by_dn(self
, dn
):
83 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
85 attrs
= self
._get
_attrs
(dn
)
88 # Cache all attributes for 5 min
89 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
91 return Account(self
.backend
, dn
, attrs
)
93 def search(self
, query
):
94 # Search for exact matches
95 accounts
= self
._search
(
96 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
97 % (query
, query
, query
, query
, query
, query
))
99 # Find accounts by name
101 for account
in self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query
, query
)):
102 if not account
in accounts
:
103 accounts
.append(account
)
105 return sorted(accounts
)
107 def _search_one(self
, query
):
108 results
= self
._search
(query
, limit
=1)
110 for result
in results
:
113 def uid_exists(self
, uid
):
114 if self
.get_by_uid(uid
):
117 res
= self
.db
.get("SELECT 1 FROM account_activations \
118 WHERE uid = %s AND expires_at > NOW()", uid
)
123 # Account with uid does not exist, yet
126 def get_by_uid(self
, uid
):
127 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
129 def get_by_mail(self
, mail
):
130 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
132 def find_account(self
, s
):
133 account
= self
.get_by_uid(s
)
137 return self
.get_by_mail(s
)
139 def get_by_sip_id(self
, sip_id
):
143 return self
._search
_one
(
144 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
147 def get_by_phone_number(self
, number
):
151 return self
._search
_one
(
152 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
153 % (number
, number
, number
, number
))
157 def register(self
, uid
, email
, first_name
, last_name
):
158 # Check if UID is unique
159 if self
.get_by_uid(uid
):
160 raise ValueError("UID exists: %s" % uid
)
162 # Generate a random activation code
163 activation_code
= util
.random_string(36)
165 # Create an entry in our database until the user
166 # has activated the account
167 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
168 email, first_name, last_name) VALUES(%s, %s, %s, %s, %s)",
169 uid
, activation_code
, email
, first_name
, last_name
)
171 # Send an account activation email
172 self
.backend
.messages
.send_template("auth/messages/register",
173 recipients
=[email
], priority
=100, uid
=uid
,
174 activation_code
=activation_code
, email
=email
,
175 first_name
=first_name
, last_name
=last_name
)
177 def activate(self
, uid
, activation_code
):
178 res
= self
.db
.get("DELETE FROM account_activations \
179 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
180 RETURNING *", uid
, activation_code
)
182 # Return nothing when account was not found
186 # Create a new account on the LDAP database
187 return self
.create(uid
, res
.email
,
188 first_name
=res
.first_name
, last_name
=res
.last_name
)
190 def create(self
, uid
, email
, first_name
, last_name
):
193 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
194 "mail" : email
.encode(),
197 "cn" : b
"%s %s" % (first_name
.encode(), last_name
.encode()),
198 "sn" : last_name
.encode(),
199 "givenName" : first_name
.encode(),
202 logging
.info("Creating new account: %s: %s" % (uid
, account
))
205 dn
= "uid=%s,ou=People,dc=mcfly,dc=local" % uid
207 # Create account on LDAP
208 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
211 return self
.get_by_dn(dn
)
215 def _cleanup_expired_sessions(self
):
216 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
218 def create_session(self
, account
, host
):
219 self
._cleanup
_expired
_sessions
()
221 res
= self
.db
.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
222 RETURNING session_id, time_expires", host
, account
.uid
)
224 # Session could not be created
228 logging
.info("Created session %s for %s which expires %s" \
229 % (res
.session_id
, account
, res
.time_expires
))
230 return res
.session_id
, res
.time_expires
232 def destroy_session(self
, session_id
, host
):
233 logging
.info("Destroying session %s" % session_id
)
235 self
.db
.execute("DELETE FROM sessions \
236 WHERE session_id = %s AND host = %s", session_id
, host
)
237 self
._cleanup
_expired
_sessions
()
239 def get_by_session(self
, session_id
, host
):
240 logging
.debug("Looking up session %s" % session_id
)
242 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
243 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
246 # Session does not exist or has expired
250 # Update the session expiration time
251 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
252 WHERE session_id = %s AND host = %s", session_id
, host
)
254 return self
.get_by_uid(res
.uid
)
257 class Account(Object
):
258 def __init__(self
, backend
, dn
, attrs
=None):
259 Object
.__init
__(self
, backend
)
262 self
.attributes
= attrs
or {}
268 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
270 def __eq__(self
, other
):
271 if isinstance(other
, self
.__class
__):
272 return self
.dn
== other
.dn
274 def __lt__(self
, other
):
275 if isinstance(other
, self
.__class
__):
276 return self
.name
< other
.name
280 return self
.accounts
.ldap
282 def _exists(self
, key
):
291 for value
in self
.attributes
.get(key
, []):
294 def _get_bytes(self
, key
, default
=None):
295 for value
in self
._get
(key
):
300 def _get_strings(self
, key
):
301 for value
in self
._get
(key
):
304 def _get_string(self
, key
, default
=None):
305 for value
in self
._get
_strings
(key
):
310 def _get_phone_numbers(self
, key
):
311 for value
in self
._get
_strings
(key
):
312 yield phonenumbers
.parse(value
, None)
314 def _modify(self
, modlist
):
315 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
317 # Run modify operation
318 self
.ldap
.modify_s(self
.dn
, modlist
)
320 # Delete cached attributes
321 self
.memcache
.delete("accounts:%s:attrs")
323 def _set(self
, key
, values
):
324 current
= self
._get
(key
)
326 # Don't do anything if nothing has changed
327 if list(current
) == values
:
330 # Remove all old values and add all new ones
333 if self
._exists
(key
):
334 modlist
.append((ldap
.MOD_DELETE
, key
, None))
338 modlist
.append((ldap
.MOD_ADD
, key
, values
))
340 # Run modify operation
341 self
._modify
(modlist
)
344 self
.attributes
.update({ key
: values
})
346 def _set_bytes(self
, key
, values
):
347 return self
._set
(key
, values
)
349 def _set_strings(self
, key
, values
):
350 return self
._set
(key
, [e
.encode() for e
in values
if e
])
352 def _set_string(self
, key
, value
):
353 return self
._set
_strings
(key
, [value
,])
355 def _add(self
, key
, values
):
357 (ldap
.MOD_ADD
, key
, values
),
360 self
._modify
(modlist
)
362 def _add_strings(self
, key
, values
):
363 return self
._add
(key
, [e
.encode() for e
in values
])
365 def _add_string(self
, key
, value
):
366 return self
._add
_strings
(key
, [value
,])
368 def _delete(self
, key
, values
):
370 (ldap
.MOD_DELETE
, key
, values
),
373 self
._modify
(modlist
)
375 def _delete_strings(self
, key
, values
):
376 return self
._delete
(key
, [e
.encode() for e
in values
])
378 def _delete_string(self
, key
, value
):
379 return self
._delete
_strings
(key
, [value
,])
381 def passwd(self
, password
):
385 # The new password must have a score of 3 or better
386 quality
= self
.check_password_quality(password
)
387 if quality
["score"] < 3:
388 raise ValueError("Password too weak")
390 self
.ldap
.passwd_s(self
.dn
, None, password
)
392 def check_password(self
, password
):
394 Bind to the server with given credentials and return
395 true if password is corrent and false if not.
397 Raises exceptions from the server on any other errors.
402 logging
.debug("Checking credentials for %s" % self
.dn
)
404 # Create a new LDAP connection
405 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
406 conn
= ldap
.initialize(ldap_uri
)
409 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
410 except ldap
.INVALID_CREDENTIALS
:
411 logging
.debug("Account credentials are invalid for %s" % self
)
414 logging
.info("Successfully authenticated %s" % self
)
418 def check_password_quality(self
, password
):
420 Passwords are passed through zxcvbn to make sure
421 that they are strong enough.
423 return zxcvbn
.zxcvbn(password
, user_inputs
=(
424 self
.first_name
, self
.last_name
,
428 return "wheel" in self
.groups
431 return "staff" in self
.groups
434 return "posixAccount" in self
.classes
437 return "postfixMailUser" in self
.classes
440 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
442 def can_be_managed_by(self
, account
):
444 Returns True if account is allowed to manage this account
446 # Admins can manage all accounts
447 if account
.is_admin():
450 # Users can manage themselves
451 return self
== account
455 return self
._get
_strings
("objectClass")
459 return self
._get
_string
("uid")
463 return self
._get
_string
("cn")
467 def get_first_name(self
):
468 return self
._get
_string
("givenName")
470 def set_first_name(self
, first_name
):
471 self
._set
_string
("givenName", first_name
)
474 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
476 first_name
= property(get_first_name
, set_first_name
)
480 def get_last_name(self
):
481 return self
._get
_string
("sn")
483 def set_last_name(self
, last_name
):
484 self
._set
_string
("sn", last_name
)
487 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
489 last_name
= property(get_last_name
, set_last_name
)
493 groups
= self
.memcache
.get("accounts:%s:groups" % self
.dn
)
497 # Fetch groups from LDAP
498 groups
= self
._get
_groups
()
500 # Cache groups for 5 min
501 self
.memcache
.set("accounts:%s:groups" % self
.dn
, groups
, 300)
505 def _get_groups(self
):
508 res
= self
.accounts
._query
("(&(objectClass=posixGroup) \
509 (memberUid=%s))" % self
.uid
, ["cn"])
511 for dn
, attrs
in res
:
512 cns
= attrs
.get("cn")
514 groups
.append(cns
[0].decode())
525 address
+= self
.street
.splitlines()
527 if self
.postal_code
and self
.city
:
528 if self
.country_code
in ("AT", "DE"):
529 address
.append("%s %s" % (self
.postal_code
, self
.city
))
531 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
533 address
.append(self
.city
or self
.postal_code
)
535 if self
.country_name
:
536 address
.append(self
.country_name
)
540 def get_street(self
):
541 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
543 def set_street(self
, street
):
544 self
._set
_string
("street", street
)
546 street
= property(get_street
, set_street
)
549 return self
._get
_string
("l") or ""
551 def set_city(self
, city
):
552 self
._set
_string
("l", city
)
554 city
= property(get_city
, set_city
)
556 def get_postal_code(self
):
557 return self
._get
_string
("postalCode") or ""
559 def set_postal_code(self
, postal_code
):
560 self
._set
_string
("postalCode", postal_code
)
562 postal_code
= property(get_postal_code
, set_postal_code
)
564 # XXX This should be c
565 def get_country_code(self
):
566 return self
._get
_string
("st")
568 def set_country_code(self
, country_code
):
569 self
._set
_string
("st", country_code
)
571 country_code
= property(get_country_code
, set_country_code
)
574 def country_name(self
):
575 if self
.country_code
:
576 return countries
.get_name(self
.country_code
)
580 return self
._get
_string
("mail")
582 # Mail Routing Address
584 def get_mail_routing_address(self
):
585 return self
._get
_string
("mailRoutingAddress", None)
587 def set_mail_routing_address(self
, address
):
588 self
._set
_string
("mailRoutingAddress", address
or None)
590 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
594 if "sipUser" in self
.classes
:
595 return self
._get
_string
("sipAuthenticationUser")
597 if "sipRoutingObject" in self
.classes
:
598 return self
._get
_string
("sipLocalAddress")
601 def sip_password(self
):
602 return self
._get
_string
("sipPassword")
605 def _generate_sip_password():
606 return util
.random_string(8)
610 return "%s@ipfire.org" % self
.sip_id
612 def uses_sip_forwarding(self
):
613 if self
.sip_routing_address
:
620 def get_sip_routing_address(self
):
621 if "sipRoutingObject" in self
.classes
:
622 return self
._get
_string
("sipRoutingAddress")
624 def set_sip_routing_address(self
, address
):
628 # Don't do anything if nothing has changed
629 if self
.get_sip_routing_address() == address
:
633 # This is no longer a SIP user any more
636 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
637 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
638 (ldap
.MOD_DELETE
, "sipPassword", None),
640 except ldap
.NO_SUCH_ATTRIBUTE
:
643 # Set new routing object
646 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
647 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
648 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
651 # If this is a change, we cannot add this again
652 except ldap
.TYPE_OR_VALUE_EXISTS
:
653 self
._set
_string
("sipRoutingAddress", address
)
657 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
658 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
659 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
661 except ldap
.NO_SUCH_ATTRIBUTE
:
665 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
666 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
667 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
670 # XXX Cache is invalid here
672 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
675 def sip_registrations(self
):
676 sip_registrations
= []
678 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
681 sip_registrations
.append(reg
)
683 return sip_registrations
686 def sip_channels(self
):
687 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
689 def get_cdr(self
, date
=None, limit
=None):
690 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
695 def phone_number(self
):
697 Returns the IPFire phone number
700 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
703 def fax_number(self
):
705 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
707 def get_phone_numbers(self
):
710 for field
in ("telephoneNumber", "homePhone", "mobile"):
711 for number
in self
._get
_phone
_numbers
(field
):
716 def set_phone_numbers(self
, phone_numbers
):
717 # Sort phone numbers by landline and mobile
718 _landline_numbers
= []
721 for number
in phone_numbers
:
723 number
= phonenumbers
.parse(number
, None)
724 except phonenumbers
.phonenumberutil
.NumberParseException
:
727 # Convert to string (in E.164 format)
728 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
730 # Separate mobile numbers
731 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
732 _mobile_numbers
.append(s
)
734 _landline_numbers
.append(s
)
737 self
._set
_strings
("telephoneNumber", _landline_numbers
)
738 self
._set
_strings
("mobile", _mobile_numbers
)
740 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
743 def _all_telephone_numbers(self
):
744 ret
= [ self
.sip_id
, ]
746 if self
.phone_number
:
747 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
750 for number
in self
.phone_numbers
:
751 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
756 def avatar_url(self
, size
=None):
757 if self
.backend
.debug
:
758 hostname
= "http://people.dev.ipfire.org"
760 hostname
= "https://people.ipfire.org"
762 url
= "%s/users/%s.jpg" % (hostname
, self
.uid
)
765 url
+= "?size=%s" % size
769 def get_avatar(self
, size
=None):
770 avatar
= self
._get
_bytes
("jpegPhoto")
777 return self
._resize
_avatar
(avatar
, size
)
779 def _resize_avatar(self
, image
, size
):
780 image
= PIL
.Image
.open(io
.BytesIO(image
))
782 # Resize the image to the desired resolution (and make it square)
783 thumbnail
= PIL
.ImageOps
.fit(image
, (size
, size
), PIL
.Image
.ANTIALIAS
)
785 with io
.BytesIO() as f
:
786 # If writing out the image does not work with optimization,
787 # we try to write it out without any optimization.
789 thumbnail
.save(f
, image
.format
, optimize
=True, quality
=98)
791 thumbnail
.save(f
, image
.format
, quality
=98)
795 def upload_avatar(self
, avatar
):
796 self
._set
("jpegPhoto", avatar
)
804 for key
in self
._get
_strings
("sshPublicKey"):
805 s
= sshpubkeys
.SSHKey()
809 except (sshpubkeys
.InvalidKeyError
, NotImplementedError) as e
:
810 logging
.warning("Could not parse SSH key %s: %s" % (key
, e
))
817 def get_ssh_key_by_hash_sha256(self
, hash_sha256
):
818 for key
in self
.ssh_keys
:
819 if not key
.hash_sha256() == hash_sha256
:
824 def add_ssh_key(self
, key
):
825 k
= sshpubkeys
.SSHKey()
827 # Try to parse the key
830 # Check for types and sufficient sizes
831 if k
.key_type
== b
"ssh-rsa":
833 raise sshpubkeys
.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
835 elif k
.key_type
== b
"ssh-dss":
836 raise sshpubkeys
.InvalidKeyError("DSA keys are not supported")
838 # Ignore any duplicates
839 if key
in (k
.keydata
for k
in self
.ssh_keys
):
840 logging
.debug("SSH Key has already been added for %s: %s" % (self
, key
))
843 # Prepare transaction
846 # Add object class if user is not in it, yet
847 if not "ldapPublicKey" in self
.classes
:
848 modlist
.append((ldap
.MOD_ADD
, "objectClass", b
"ldapPublicKey"))
851 modlist
.append((ldap
.MOD_ADD
, "sshPublicKey", key
.encode()))
854 self
._modify
(modlist
)
857 self
.ssh_keys
.append(k
)
859 def delete_ssh_key(self
, key
):
860 if not key
in (k
.keydata
for k
in self
.ssh_keys
):
863 # Delete key from LDAP
864 if len(self
.ssh_keys
) > 1:
865 self
._delete
_string
("sshPublicKey", key
)
868 (ldap
.MOD_DELETE
, "objectClass", b
"ldapPublicKey"),
869 (ldap
.MOD_DELETE
, "sshPublicKey", key
.encode()),
873 if __name__
== "__main__":