]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
15 from . import countries
17 from .decorators
import *
18 from .misc
import Object
20 class Accounts(Object
):
22 self
.search_base
= self
.settings
.get("ldap_search_base")
25 # Only return developers (group with ID 1000)
26 accounts
= self
._search
("(&(objectClass=posixAccount)(gidNumber=1000))")
28 return iter(sorted(accounts
))
32 # Connect to LDAP server
33 ldap_uri
= self
.settings
.get("ldap_uri")
35 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
37 # Connect to the LDAP server
38 return ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
39 retry_max
=10, retry_delay
=3)
41 def _authenticate(self
):
42 # Bind with username and password
43 self
.ldap
.simple_bind(
44 self
.settings
.get("ldap_bind_dn"),
45 self
.settings
.get("ldap_bind_pw", ""),
48 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
49 logging
.debug("Performing LDAP query: %s" % query
)
53 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
54 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
)
56 # Log time it took to perform the query
57 logging
.debug("Query took %.2fms" % ((time
.time() - t
) * 1000.0))
61 def _search(self
, query
, attrlist
=None, limit
=0):
63 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
64 account
= self
.get_by_dn(dn
)
65 accounts
.append(account
)
69 def _get_attrs(self
, dn
):
71 Fetches all attributes for the given distinguished name
73 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1)
75 for dn
, attrs
in results
:
78 def get_by_dn(self
, dn
):
79 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
81 attrs
= self
._get
_attrs
(dn
)
84 # Cache all attributes for 5 min
85 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
87 return Account(self
.backend
, dn
, attrs
)
89 def search(self
, query
):
90 # Search for exact matches
91 accounts
= self
._search
(
92 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
93 % (query
, query
, query
, query
, query
, query
))
95 # Find accounts by name
97 for account
in self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query
, query
)):
98 if not account
in accounts
:
99 accounts
.append(account
)
101 return sorted(accounts
)
103 def _search_one(self
, query
):
104 results
= self
._search
(query
, limit
=1)
106 for result
in results
:
109 def uid_exists(self
, uid
):
110 if self
.get_by_uid(uid
):
113 res
= self
.db
.get("SELECT 1 FROM account_activations \
114 WHERE uid = %s AND expires_at > NOW()", uid
)
119 # Account with uid does not exist, yet
122 def get_by_uid(self
, uid
):
123 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
125 def get_by_mail(self
, mail
):
126 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
128 def find_account(self
, s
):
129 account
= self
.get_by_uid(s
)
133 return self
.get_by_mail(s
)
135 def get_by_sip_id(self
, sip_id
):
139 return self
._search
_one
(
140 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
143 def get_by_phone_number(self
, number
):
147 return self
._search
_one
(
148 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
149 % (number
, number
, number
, number
))
153 def register(self
, uid
, email
, first_name
, last_name
):
154 # Check if UID is unique
155 if self
.get_by_uid(uid
):
156 raise ValueError("UID exists: %s" % uid
)
158 # Generate a random activation code
159 activation_code
= util
.random_string(36)
161 # Create an entry in our database until the user
162 # has activated the account
163 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
164 email, first_name, last_name) VALUES(%s, %s, %s, %s, %s)",
165 uid
, activation_code
, email
, first_name
, last_name
)
167 # Send an account activation email
168 self
.backend
.messages
.send_template("auth/messages/register",
169 recipients
=[email
], priority
=100, uid
=uid
,
170 activation_code
=activation_code
, email
=email
,
171 first_name
=first_name
, last_name
=last_name
)
173 def activate(self
, uid
, activation_code
):
174 res
= self
.db
.get("DELETE FROM account_activations \
175 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
176 RETURNING *", uid
, activation_code
)
178 # Return nothing when account was not found
182 # Create a new account on the LDAP database
183 return self
.create(uid
, res
.email
,
184 first_name
=res
.first_name
, last_name
=res
.last_name
)
186 def create(self
, uid
, email
, first_name
, last_name
):
187 cn
= "%s %s" % (first_name
, last_name
)
191 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
192 "mail" : email
.encode(),
196 "sn" : last_name
.encode(),
197 "givenName" : first_name
.encode(),
200 logging
.info("Creating new account: %s: %s" % (uid
, account
))
203 dn
= "uid=%s,ou=People,dc=mcfly,dc=local" % uid
205 # Create account on LDAP
206 self
.accounts
._authenticate
()
207 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
210 return self
.get_by_dn(dn
)
214 def create_session(self
, account
, host
):
215 res
= self
.db
.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
216 RETURNING session_id, time_expires", host
, account
.uid
)
218 # Session could not be created
222 logging
.info("Created session %s for %s which expires %s" \
223 % (res
.session_id
, account
, res
.time_expires
))
224 return res
.session_id
, res
.time_expires
226 def destroy_session(self
, session_id
, host
):
227 logging
.info("Destroying session %s" % session_id
)
229 self
.db
.execute("DELETE FROM sessions \
230 WHERE session_id = %s AND host = %s", session_id
, host
)
232 def get_by_session(self
, session_id
, host
):
233 logging
.debug("Looking up session %s" % session_id
)
235 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
236 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
239 # Session does not exist or has expired
243 # Update the session expiration time
244 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
245 WHERE session_id = %s AND host = %s", session_id
, host
)
247 return self
.get_by_uid(res
.uid
)
250 # Cleanup expired sessions
251 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
253 # Cleanup expired account activations
254 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
257 class Account(Object
):
258 def __init__(self
, backend
, dn
, attrs
=None):
259 Object
.__init
__(self
, backend
)
262 self
.attributes
= attrs
or {}
271 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
273 def __eq__(self
, other
):
274 if isinstance(other
, self
.__class
__):
275 return self
.dn
== other
.dn
277 def __lt__(self
, other
):
278 if isinstance(other
, self
.__class
__):
279 return self
.name
< other
.name
283 return self
.accounts
.ldap
285 def _exists(self
, key
):
294 for value
in self
.attributes
.get(key
, []):
297 def _get_bytes(self
, key
, default
=None):
298 for value
in self
._get
(key
):
303 def _get_strings(self
, key
):
304 for value
in self
._get
(key
):
307 def _get_string(self
, key
, default
=None):
308 for value
in self
._get
_strings
(key
):
313 def _get_phone_numbers(self
, key
):
314 for value
in self
._get
_strings
(key
):
315 yield phonenumbers
.parse(value
, None)
317 def _modify(self
, modlist
):
318 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
320 # Authenticate before performing any write operations
321 self
.accounts
._authenticate
()
323 # Run modify operation
324 self
.ldap
.modify_s(self
.dn
, modlist
)
326 # Delete cached attributes
327 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
329 def _set(self
, key
, values
):
330 current
= self
._get
(key
)
332 # Don't do anything if nothing has changed
333 if list(current
) == values
:
336 # Remove all old values and add all new ones
339 if self
._exists
(key
):
340 modlist
.append((ldap
.MOD_DELETE
, key
, None))
344 modlist
.append((ldap
.MOD_ADD
, key
, values
))
346 # Run modify operation
347 self
._modify
(modlist
)
350 self
.attributes
.update({ key
: values
})
352 def _set_bytes(self
, key
, values
):
353 return self
._set
(key
, values
)
355 def _set_strings(self
, key
, values
):
356 return self
._set
(key
, [e
.encode() for e
in values
if e
])
358 def _set_string(self
, key
, value
):
359 return self
._set
_strings
(key
, [value
,])
361 def _add(self
, key
, values
):
363 (ldap
.MOD_ADD
, key
, values
),
366 self
._modify
(modlist
)
368 def _add_strings(self
, key
, values
):
369 return self
._add
(key
, [e
.encode() for e
in values
])
371 def _add_string(self
, key
, value
):
372 return self
._add
_strings
(key
, [value
,])
374 def _delete(self
, key
, values
):
376 (ldap
.MOD_DELETE
, key
, values
),
379 self
._modify
(modlist
)
381 def _delete_strings(self
, key
, values
):
382 return self
._delete
(key
, [e
.encode() for e
in values
])
384 def _delete_string(self
, key
, value
):
385 return self
._delete
_strings
(key
, [value
,])
387 def passwd(self
, password
):
391 # The new password must have a score of 3 or better
392 quality
= self
.check_password_quality(password
)
393 if quality
["score"] < 3:
394 raise ValueError("Password too weak")
396 self
.accounts
._authenticate
()
397 self
.ldap
.passwd_s(self
.dn
, None, password
)
399 def check_password(self
, password
):
401 Bind to the server with given credentials and return
402 true if password is corrent and false if not.
404 Raises exceptions from the server on any other errors.
409 logging
.debug("Checking credentials for %s" % self
.dn
)
411 # Create a new LDAP connection
412 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
413 conn
= ldap
.initialize(ldap_uri
)
416 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
417 except ldap
.INVALID_CREDENTIALS
:
418 logging
.debug("Account credentials are invalid for %s" % self
)
421 logging
.info("Successfully authenticated %s" % self
)
425 def check_password_quality(self
, password
):
427 Passwords are passed through zxcvbn to make sure
428 that they are strong enough.
430 return zxcvbn
.zxcvbn(password
, user_inputs
=(
431 self
.first_name
, self
.last_name
,
435 return "wheel" in self
.groups
438 return "staff" in self
.groups
441 return "posixAccount" in self
.classes
444 return "postfixMailUser" in self
.classes
447 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
449 def can_be_managed_by(self
, account
):
451 Returns True if account is allowed to manage this account
453 # Admins can manage all accounts
454 if account
.is_admin():
457 # Users can manage themselves
458 return self
== account
462 return self
._get
_strings
("objectClass")
466 return self
._get
_string
("uid")
470 return self
._get
_string
("cn")
474 def get_nickname(self
):
475 return self
._get
_string
("displayName")
477 def set_nickname(self
, nickname
):
478 self
._set
_string
("displayName", nickname
)
480 nickname
= property(get_nickname
, set_nickname
)
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 photo
= self
._get
_bytes
("jpegPhoto")
789 # Exit if no avatar is available
793 # Return the raw image if no size was requested
797 # Try to retrieve something from the cache
798 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
802 # Generate a new thumbnail
803 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
805 # Save to cache for 15m
806 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
810 def upload_avatar(self
, avatar
):
811 self
._set
("jpegPhoto", avatar
)
819 for key
in self
._get
_strings
("sshPublicKey"):
820 s
= sshpubkeys
.SSHKey()
824 except (sshpubkeys
.InvalidKeyError
, NotImplementedError) as e
:
825 logging
.warning("Could not parse SSH key %s: %s" % (key
, e
))
832 def get_ssh_key_by_hash_sha256(self
, hash_sha256
):
833 for key
in self
.ssh_keys
:
834 if not key
.hash_sha256() == hash_sha256
:
839 def add_ssh_key(self
, key
):
840 k
= sshpubkeys
.SSHKey()
842 # Try to parse the key
845 # Check for types and sufficient sizes
846 if k
.key_type
== b
"ssh-rsa":
848 raise sshpubkeys
.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
850 elif k
.key_type
== b
"ssh-dss":
851 raise sshpubkeys
.InvalidKeyError("DSA keys are not supported")
853 # Ignore any duplicates
854 if key
in (k
.keydata
for k
in self
.ssh_keys
):
855 logging
.debug("SSH Key has already been added for %s: %s" % (self
, key
))
858 # Prepare transaction
861 # Add object class if user is not in it, yet
862 if not "ldapPublicKey" in self
.classes
:
863 modlist
.append((ldap
.MOD_ADD
, "objectClass", b
"ldapPublicKey"))
866 modlist
.append((ldap
.MOD_ADD
, "sshPublicKey", key
.encode()))
869 self
._modify
(modlist
)
872 self
.ssh_keys
.append(k
)
874 def delete_ssh_key(self
, key
):
875 if not key
in (k
.keydata
for k
in self
.ssh_keys
):
878 # Delete key from LDAP
879 if len(self
.ssh_keys
) > 1:
880 self
._delete
_string
("sshPublicKey", key
)
883 (ldap
.MOD_DELETE
, "objectClass", b
"ldapPublicKey"),
884 (ldap
.MOD_DELETE
, "sshPublicKey", key
.encode()),
888 if __name__
== "__main__":