]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
16 from . import countries
18 from .decorators
import *
19 from .misc
import Object
21 # Set the client keytab name
22 os
.environ
["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
24 class Accounts(Object
):
26 self
.search_base
= self
.settings
.get("ldap_search_base")
29 # Only return developers (group with ID 1000)
30 accounts
= self
._search
("(&(objectClass=posixAccount)(gidNumber=1000))")
32 return iter(sorted(accounts
))
36 # Connect to LDAP server
37 ldap_uri
= self
.settings
.get("ldap_uri")
39 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
41 # Connect to the LDAP server
42 return ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
43 retry_max
=10, retry_delay
=3)
45 def _authenticate(self
):
46 # Authenticate against LDAP server using Kerberos
47 self
.ldap
.sasl_gssapi_bind_s()
50 logging
.info("Testing LDAP connection...")
54 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
56 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
57 logging
.debug("Performing LDAP query: %s" % query
)
61 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
62 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
)
64 # Log time it took to perform the query
65 logging
.debug("Query took %.2fms" % ((time
.time() - t
) * 1000.0))
69 def _search(self
, query
, attrlist
=None, limit
=0):
71 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
72 account
= self
.get_by_dn(dn
)
73 accounts
.append(account
)
77 def _get_attrs(self
, dn
):
79 Fetches all attributes for the given distinguished name
81 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1)
83 for dn
, attrs
in results
:
86 def get_by_dn(self
, dn
):
87 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
89 attrs
= self
._get
_attrs
(dn
)
92 # Cache all attributes for 5 min
93 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
95 return Account(self
.backend
, dn
, attrs
)
97 def search(self
, query
):
98 # Search for exact matches
99 accounts
= self
._search
(
100 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
101 % (query
, query
, query
, query
, query
, query
))
103 # Find accounts by name
105 for account
in self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query
, query
)):
106 if not account
in accounts
:
107 accounts
.append(account
)
109 return sorted(accounts
)
111 def _search_one(self
, query
):
112 results
= self
._search
(query
, limit
=1)
114 for result
in results
:
117 def uid_exists(self
, uid
):
118 if self
.get_by_uid(uid
):
121 res
= self
.db
.get("SELECT 1 FROM account_activations \
122 WHERE uid = %s AND expires_at > NOW()", uid
)
127 # Account with uid does not exist, yet
130 def get_by_uid(self
, uid
):
131 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
133 def get_by_mail(self
, mail
):
134 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
136 def find_account(self
, s
):
137 account
= self
.get_by_uid(s
)
141 return self
.get_by_mail(s
)
143 def get_by_sip_id(self
, sip_id
):
147 return self
._search
_one
(
148 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
151 def get_by_phone_number(self
, number
):
155 return self
._search
_one
(
156 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
157 % (number
, number
, number
, number
))
161 def register(self
, uid
, email
, first_name
, last_name
):
162 # Check if UID is unique
163 if self
.get_by_uid(uid
):
164 raise ValueError("UID exists: %s" % uid
)
166 # Generate a random activation code
167 activation_code
= util
.random_string(36)
169 # Create an entry in our database until the user
170 # has activated the account
171 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
172 email, first_name, last_name) VALUES(%s, %s, %s, %s, %s)",
173 uid
, activation_code
, email
, first_name
, last_name
)
175 # Send an account activation email
176 self
.backend
.messages
.send_template("auth/messages/register",
177 recipients
=[email
], priority
=100, uid
=uid
,
178 activation_code
=activation_code
, email
=email
,
179 first_name
=first_name
, last_name
=last_name
)
181 def activate(self
, uid
, activation_code
):
182 res
= self
.db
.get("DELETE FROM account_activations \
183 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
184 RETURNING *", uid
, activation_code
)
186 # Return nothing when account was not found
190 # Create a new account on the LDAP database
191 return self
.create(uid
, res
.email
,
192 first_name
=res
.first_name
, last_name
=res
.last_name
)
194 def create(self
, uid
, email
, first_name
, last_name
):
195 cn
= "%s %s" % (first_name
, last_name
)
199 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
200 "mail" : email
.encode(),
204 "sn" : last_name
.encode(),
205 "givenName" : first_name
.encode(),
208 logging
.info("Creating new account: %s: %s" % (uid
, account
))
211 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
213 # Create account on LDAP
214 self
.accounts
._authenticate
()
215 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
218 return self
.get_by_dn(dn
)
222 def create_session(self
, account
, host
):
223 res
= self
.db
.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
224 RETURNING session_id, time_expires", host
, account
.uid
)
226 # Session could not be created
230 logging
.info("Created session %s for %s which expires %s" \
231 % (res
.session_id
, account
, res
.time_expires
))
232 return res
.session_id
, res
.time_expires
234 def destroy_session(self
, session_id
, host
):
235 logging
.info("Destroying session %s" % session_id
)
237 self
.db
.execute("DELETE FROM sessions \
238 WHERE session_id = %s AND host = %s", session_id
, host
)
240 def get_by_session(self
, session_id
, host
):
241 logging
.debug("Looking up session %s" % session_id
)
243 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
244 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
247 # Session does not exist or has expired
251 # Update the session expiration time
252 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
253 WHERE session_id = %s AND host = %s", session_id
, host
)
255 return self
.get_by_uid(res
.uid
)
258 # Cleanup expired sessions
259 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
261 # Cleanup expired account activations
262 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
265 class Account(Object
):
266 def __init__(self
, backend
, dn
, attrs
=None):
267 Object
.__init
__(self
, backend
)
270 self
.attributes
= attrs
or {}
279 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
281 def __eq__(self
, other
):
282 if isinstance(other
, self
.__class
__):
283 return self
.dn
== other
.dn
285 def __lt__(self
, other
):
286 if isinstance(other
, self
.__class
__):
287 return self
.name
< other
.name
291 return self
.accounts
.ldap
293 def _exists(self
, key
):
302 for value
in self
.attributes
.get(key
, []):
305 def _get_bytes(self
, key
, default
=None):
306 for value
in self
._get
(key
):
311 def _get_strings(self
, key
):
312 for value
in self
._get
(key
):
315 def _get_string(self
, key
, default
=None):
316 for value
in self
._get
_strings
(key
):
321 def _get_phone_numbers(self
, key
):
322 for value
in self
._get
_strings
(key
):
323 yield phonenumbers
.parse(value
, None)
325 def _modify(self
, modlist
):
326 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
328 # Authenticate before performing any write operations
329 self
.accounts
._authenticate
()
331 # Run modify operation
332 self
.ldap
.modify_s(self
.dn
, modlist
)
334 # Delete cached attributes
335 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
337 def _set(self
, key
, values
):
338 current
= self
._get
(key
)
340 # Don't do anything if nothing has changed
341 if list(current
) == values
:
344 # Remove all old values and add all new ones
347 if self
._exists
(key
):
348 modlist
.append((ldap
.MOD_DELETE
, key
, None))
352 modlist
.append((ldap
.MOD_ADD
, key
, values
))
354 # Run modify operation
355 self
._modify
(modlist
)
358 self
.attributes
.update({ key
: values
})
360 def _set_bytes(self
, key
, values
):
361 return self
._set
(key
, values
)
363 def _set_strings(self
, key
, values
):
364 return self
._set
(key
, [e
.encode() for e
in values
if e
])
366 def _set_string(self
, key
, value
):
367 return self
._set
_strings
(key
, [value
,])
369 def _add(self
, key
, values
):
371 (ldap
.MOD_ADD
, key
, values
),
374 self
._modify
(modlist
)
376 def _add_strings(self
, key
, values
):
377 return self
._add
(key
, [e
.encode() for e
in values
])
379 def _add_string(self
, key
, value
):
380 return self
._add
_strings
(key
, [value
,])
382 def _delete(self
, key
, values
):
384 (ldap
.MOD_DELETE
, key
, values
),
387 self
._modify
(modlist
)
389 def _delete_strings(self
, key
, values
):
390 return self
._delete
(key
, [e
.encode() for e
in values
])
392 def _delete_string(self
, key
, value
):
393 return self
._delete
_strings
(key
, [value
,])
395 def passwd(self
, password
):
399 # The new password must have a score of 3 or better
400 quality
= self
.check_password_quality(password
)
401 if quality
["score"] < 3:
402 raise ValueError("Password too weak")
404 self
.accounts
._authenticate
()
405 self
.ldap
.passwd_s(self
.dn
, None, password
)
407 def check_password(self
, password
):
409 Bind to the server with given credentials and return
410 true if password is corrent and false if not.
412 Raises exceptions from the server on any other errors.
417 logging
.debug("Checking credentials for %s" % self
.dn
)
419 # Create a new LDAP connection
420 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
421 conn
= ldap
.initialize(ldap_uri
)
424 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
425 except ldap
.INVALID_CREDENTIALS
:
426 logging
.debug("Account credentials are invalid for %s" % self
)
429 logging
.info("Successfully authenticated %s" % self
)
433 def check_password_quality(self
, password
):
435 Passwords are passed through zxcvbn to make sure
436 that they are strong enough.
438 return zxcvbn
.zxcvbn(password
, user_inputs
=(
439 self
.first_name
, self
.last_name
,
443 return "wheel" in self
.groups
446 return "staff" in self
.groups
449 return "posixAccount" in self
.classes
452 return "postfixMailUser" in self
.classes
455 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
457 def can_be_managed_by(self
, account
):
459 Returns True if account is allowed to manage this account
461 # Admins can manage all accounts
462 if account
.is_admin():
465 # Users can manage themselves
466 return self
== account
470 return self
._get
_strings
("objectClass")
474 return self
._get
_string
("uid")
478 return self
._get
_string
("cn")
482 def get_nickname(self
):
483 return self
._get
_string
("displayName")
485 def set_nickname(self
, nickname
):
486 self
._set
_string
("displayName", nickname
)
488 nickname
= property(get_nickname
, set_nickname
)
492 def get_first_name(self
):
493 return self
._get
_string
("givenName")
495 def set_first_name(self
, first_name
):
496 self
._set
_string
("givenName", first_name
)
499 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
501 first_name
= property(get_first_name
, set_first_name
)
505 def get_last_name(self
):
506 return self
._get
_string
("sn")
508 def set_last_name(self
, last_name
):
509 self
._set
_string
("sn", last_name
)
512 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
514 last_name
= property(get_last_name
, set_last_name
)
518 groups
= self
.memcache
.get("accounts:%s:groups" % self
.dn
)
522 # Fetch groups from LDAP
523 groups
= self
._get
_groups
()
525 # Cache groups for 5 min
526 self
.memcache
.set("accounts:%s:groups" % self
.dn
, groups
, 300)
530 def _get_groups(self
):
533 res
= self
.accounts
._query
("(&(objectClass=posixGroup) \
534 (memberUid=%s))" % self
.uid
, ["cn"])
536 for dn
, attrs
in res
:
537 cns
= attrs
.get("cn")
539 groups
.append(cns
[0].decode())
550 address
+= self
.street
.splitlines()
552 if self
.postal_code
and self
.city
:
553 if self
.country_code
in ("AT", "DE"):
554 address
.append("%s %s" % (self
.postal_code
, self
.city
))
556 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
558 address
.append(self
.city
or self
.postal_code
)
560 if self
.country_name
:
561 address
.append(self
.country_name
)
565 def get_street(self
):
566 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
568 def set_street(self
, street
):
569 self
._set
_string
("street", street
)
571 street
= property(get_street
, set_street
)
574 return self
._get
_string
("l") or ""
576 def set_city(self
, city
):
577 self
._set
_string
("l", city
)
579 city
= property(get_city
, set_city
)
581 def get_postal_code(self
):
582 return self
._get
_string
("postalCode") or ""
584 def set_postal_code(self
, postal_code
):
585 self
._set
_string
("postalCode", postal_code
)
587 postal_code
= property(get_postal_code
, set_postal_code
)
589 # XXX This should be c
590 def get_country_code(self
):
591 return self
._get
_string
("st")
593 def set_country_code(self
, country_code
):
594 self
._set
_string
("st", country_code
)
596 country_code
= property(get_country_code
, set_country_code
)
599 def country_name(self
):
600 if self
.country_code
:
601 return countries
.get_name(self
.country_code
)
605 return self
._get
_string
("mail")
607 # Mail Routing Address
609 def get_mail_routing_address(self
):
610 return self
._get
_string
("mailRoutingAddress", None)
612 def set_mail_routing_address(self
, address
):
613 self
._set
_string
("mailRoutingAddress", address
or None)
615 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
619 if "sipUser" in self
.classes
:
620 return self
._get
_string
("sipAuthenticationUser")
622 if "sipRoutingObject" in self
.classes
:
623 return self
._get
_string
("sipLocalAddress")
626 def sip_password(self
):
627 return self
._get
_string
("sipPassword")
630 def _generate_sip_password():
631 return util
.random_string(8)
635 return "%s@ipfire.org" % self
.sip_id
637 def uses_sip_forwarding(self
):
638 if self
.sip_routing_address
:
645 def get_sip_routing_address(self
):
646 if "sipRoutingObject" in self
.classes
:
647 return self
._get
_string
("sipRoutingAddress")
649 def set_sip_routing_address(self
, address
):
653 # Don't do anything if nothing has changed
654 if self
.get_sip_routing_address() == address
:
658 # This is no longer a SIP user any more
661 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
662 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
663 (ldap
.MOD_DELETE
, "sipPassword", None),
665 except ldap
.NO_SUCH_ATTRIBUTE
:
668 # Set new routing object
671 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
672 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
673 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
676 # If this is a change, we cannot add this again
677 except ldap
.TYPE_OR_VALUE_EXISTS
:
678 self
._set
_string
("sipRoutingAddress", address
)
682 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
683 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
684 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
686 except ldap
.NO_SUCH_ATTRIBUTE
:
690 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
691 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
692 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
695 # XXX Cache is invalid here
697 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
700 def sip_registrations(self
):
701 sip_registrations
= []
703 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
706 sip_registrations
.append(reg
)
708 return sip_registrations
711 def sip_channels(self
):
712 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
714 def get_cdr(self
, date
=None, limit
=None):
715 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
720 def phone_number(self
):
722 Returns the IPFire phone number
725 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
728 def fax_number(self
):
730 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
732 def get_phone_numbers(self
):
735 for field
in ("telephoneNumber", "homePhone", "mobile"):
736 for number
in self
._get
_phone
_numbers
(field
):
741 def set_phone_numbers(self
, phone_numbers
):
742 # Sort phone numbers by landline and mobile
743 _landline_numbers
= []
746 for number
in phone_numbers
:
748 number
= phonenumbers
.parse(number
, None)
749 except phonenumbers
.phonenumberutil
.NumberParseException
:
752 # Convert to string (in E.164 format)
753 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
755 # Separate mobile numbers
756 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
757 _mobile_numbers
.append(s
)
759 _landline_numbers
.append(s
)
762 self
._set
_strings
("telephoneNumber", _landline_numbers
)
763 self
._set
_strings
("mobile", _mobile_numbers
)
765 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
768 def _all_telephone_numbers(self
):
769 ret
= [ self
.sip_id
, ]
771 if self
.phone_number
:
772 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
775 for number
in self
.phone_numbers
:
776 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
781 def avatar_url(self
, size
=None):
782 if self
.backend
.debug
:
783 hostname
= "http://people.dev.ipfire.org"
785 hostname
= "https://people.ipfire.org"
787 url
= "%s/users/%s.jpg" % (hostname
, self
.uid
)
790 url
+= "?size=%s" % size
794 def get_avatar(self
, size
=None):
795 photo
= self
._get
_bytes
("jpegPhoto")
797 # Exit if no avatar is available
801 # Return the raw image if no size was requested
805 # Try to retrieve something from the cache
806 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
810 # Generate a new thumbnail
811 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
813 # Save to cache for 15m
814 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
818 def upload_avatar(self
, avatar
):
819 self
._set
("jpegPhoto", avatar
)
827 for key
in self
._get
_strings
("sshPublicKey"):
828 s
= sshpubkeys
.SSHKey()
832 except (sshpubkeys
.InvalidKeyError
, NotImplementedError) as e
:
833 logging
.warning("Could not parse SSH key %s: %s" % (key
, e
))
840 def get_ssh_key_by_hash_sha256(self
, hash_sha256
):
841 for key
in self
.ssh_keys
:
842 if not key
.hash_sha256() == hash_sha256
:
847 def add_ssh_key(self
, key
):
848 k
= sshpubkeys
.SSHKey()
850 # Try to parse the key
853 # Check for types and sufficient sizes
854 if k
.key_type
== b
"ssh-rsa":
856 raise sshpubkeys
.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
858 elif k
.key_type
== b
"ssh-dss":
859 raise sshpubkeys
.InvalidKeyError("DSA keys are not supported")
861 # Ignore any duplicates
862 if key
in (k
.keydata
for k
in self
.ssh_keys
):
863 logging
.debug("SSH Key has already been added for %s: %s" % (self
, key
))
866 # Prepare transaction
869 # Add object class if user is not in it, yet
870 if not "ldapPublicKey" in self
.classes
:
871 modlist
.append((ldap
.MOD_ADD
, "objectClass", b
"ldapPublicKey"))
874 modlist
.append((ldap
.MOD_ADD
, "sshPublicKey", key
.encode()))
877 self
._modify
(modlist
)
880 self
.ssh_keys
.append(k
)
882 def delete_ssh_key(self
, key
):
883 if not key
in (k
.keydata
for k
in self
.ssh_keys
):
886 # Delete key from LDAP
887 if len(self
.ssh_keys
) > 1:
888 self
._delete
_string
("sshPublicKey", key
)
891 (ldap
.MOD_DELETE
, "objectClass", b
"ldapPublicKey"),
892 (ldap
.MOD_DELETE
, "sshPublicKey", key
.encode()),
896 if __name__
== "__main__":