]>
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): %s" \
58 % (search_base
or self
.search_base
, query
))
62 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
63 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
)
65 # Log time it took to perform the query
66 logging
.debug("Query took %.2fms" % ((time
.time() - t
) * 1000.0))
70 def _search(self
, query
, attrlist
=None, limit
=0):
72 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
73 account
= self
.get_by_dn(dn
)
74 accounts
.append(account
)
78 def _get_attrs(self
, dn
):
80 Fetches all attributes for the given distinguished name
82 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
83 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
85 for dn
, attrs
in results
:
88 def get_by_dn(self
, dn
):
89 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
91 attrs
= self
._get
_attrs
(dn
)
94 # Cache all attributes for 5 min
95 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
97 return Account(self
.backend
, dn
, attrs
)
99 def get_created_after(self
, ts
):
100 t
= ts
.strftime("%Y%m%d%H%M%SZ")
102 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % t
)
104 def search(self
, query
):
105 # Search for exact matches
106 accounts
= self
._search
(
107 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
108 % (query
, query
, query
, query
, query
, query
))
110 # Find accounts by name
112 for account
in self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query
, query
)):
113 if not account
in accounts
:
114 accounts
.append(account
)
116 return sorted(accounts
)
118 def _search_one(self
, query
):
119 results
= self
._search
(query
, limit
=1)
121 for result
in results
:
124 def uid_exists(self
, uid
):
125 if self
.get_by_uid(uid
):
128 res
= self
.db
.get("SELECT 1 FROM account_activations \
129 WHERE uid = %s AND expires_at > NOW()", uid
)
134 # Account with uid does not exist, yet
137 def get_by_uid(self
, uid
):
138 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
140 def get_by_mail(self
, mail
):
141 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
143 def find_account(self
, s
):
144 account
= self
.get_by_uid(s
)
148 return self
.get_by_mail(s
)
150 def get_by_sip_id(self
, sip_id
):
154 return self
._search
_one
(
155 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
158 def get_by_phone_number(self
, number
):
162 return self
._search
_one
(
163 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
164 % (number
, number
, number
, number
))
168 def register(self
, uid
, email
, first_name
, last_name
):
169 # Convert all uids to lowercase
172 # Check if UID is unique
173 if self
.uid_exists(uid
):
174 raise ValueError("UID exists: %s" % uid
)
176 # Generate a random activation code
177 activation_code
= util
.random_string(36)
179 # Create an entry in our database until the user
180 # has activated the account
181 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
182 email, first_name, last_name) VALUES(%s, %s, %s, %s, %s)",
183 uid
, activation_code
, email
, first_name
, last_name
)
185 # Send an account activation email
186 self
.backend
.messages
.send_template("auth/messages/register",
187 recipients
=[email
], priority
=100, uid
=uid
,
188 activation_code
=activation_code
, email
=email
,
189 first_name
=first_name
, last_name
=last_name
)
191 def activate(self
, uid
, activation_code
):
192 res
= self
.db
.get("DELETE FROM account_activations \
193 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
194 RETURNING *", uid
, activation_code
)
196 # Return nothing when account was not found
200 # Create a new account on the LDAP database
201 return self
.create(uid
, res
.email
,
202 first_name
=res
.first_name
, last_name
=res
.last_name
)
204 def create(self
, uid
, email
, first_name
, last_name
):
205 cn
= "%s %s" % (first_name
, last_name
)
209 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
210 "mail" : email
.encode(),
214 "sn" : last_name
.encode(),
215 "givenName" : first_name
.encode(),
218 logging
.info("Creating new account: %s: %s" % (uid
, account
))
221 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
223 # Create account on LDAP
224 self
.accounts
._authenticate
()
225 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
228 return self
.get_by_dn(dn
)
232 def create_session(self
, account
, host
):
233 res
= self
.db
.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
234 RETURNING session_id, time_expires", host
, account
.uid
)
236 # Session could not be created
240 logging
.info("Created session %s for %s which expires %s" \
241 % (res
.session_id
, account
, res
.time_expires
))
242 return res
.session_id
, res
.time_expires
244 def destroy_session(self
, session_id
, host
):
245 logging
.info("Destroying session %s" % session_id
)
247 self
.db
.execute("DELETE FROM sessions \
248 WHERE session_id = %s AND host = %s", session_id
, host
)
250 def get_by_session(self
, session_id
, host
):
251 logging
.debug("Looking up session %s" % session_id
)
253 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
254 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
257 # Session does not exist or has expired
261 # Update the session expiration time
262 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
263 WHERE session_id = %s AND host = %s", session_id
, host
)
265 return self
.get_by_uid(res
.uid
)
268 # Cleanup expired sessions
269 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
271 # Cleanup expired account activations
272 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
275 class Account(Object
):
276 def __init__(self
, backend
, dn
, attrs
=None):
277 Object
.__init
__(self
, backend
)
280 self
.attributes
= attrs
or {}
289 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
291 def __eq__(self
, other
):
292 if isinstance(other
, self
.__class
__):
293 return self
.dn
== other
.dn
295 def __lt__(self
, other
):
296 if isinstance(other
, self
.__class
__):
297 return self
.name
< other
.name
301 return self
.accounts
.ldap
303 def _exists(self
, key
):
312 for value
in self
.attributes
.get(key
, []):
315 def _get_bytes(self
, key
, default
=None):
316 for value
in self
._get
(key
):
321 def _get_strings(self
, key
):
322 for value
in self
._get
(key
):
325 def _get_string(self
, key
, default
=None):
326 for value
in self
._get
_strings
(key
):
331 def _get_phone_numbers(self
, key
):
332 for value
in self
._get
_strings
(key
):
333 yield phonenumbers
.parse(value
, None)
335 def _get_timestamp(self
, key
):
336 value
= self
._get
_string
(key
)
338 # Parse the timestamp value and returns a datetime object
339 return datetime
.datetime
.strptime(value
, "%Y%m%d%H%M%SZ")
341 def _modify(self
, modlist
):
342 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
344 # Authenticate before performing any write operations
345 self
.accounts
._authenticate
()
347 # Run modify operation
348 self
.ldap
.modify_s(self
.dn
, modlist
)
350 # Delete cached attributes
351 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
353 def _set(self
, key
, values
):
354 current
= self
._get
(key
)
356 # Don't do anything if nothing has changed
357 if list(current
) == values
:
360 # Remove all old values and add all new ones
363 if self
._exists
(key
):
364 modlist
.append((ldap
.MOD_DELETE
, key
, None))
368 modlist
.append((ldap
.MOD_ADD
, key
, values
))
370 # Run modify operation
371 self
._modify
(modlist
)
374 self
.attributes
.update({ key
: values
})
376 def _set_bytes(self
, key
, values
):
377 return self
._set
(key
, values
)
379 def _set_strings(self
, key
, values
):
380 return self
._set
(key
, [e
.encode() for e
in values
if e
])
382 def _set_string(self
, key
, value
):
383 return self
._set
_strings
(key
, [value
,])
385 def _add(self
, key
, values
):
387 (ldap
.MOD_ADD
, key
, values
),
390 self
._modify
(modlist
)
392 def _add_strings(self
, key
, values
):
393 return self
._add
(key
, [e
.encode() for e
in values
])
395 def _add_string(self
, key
, value
):
396 return self
._add
_strings
(key
, [value
,])
398 def _delete(self
, key
, values
):
400 (ldap
.MOD_DELETE
, key
, values
),
403 self
._modify
(modlist
)
405 def _delete_strings(self
, key
, values
):
406 return self
._delete
(key
, [e
.encode() for e
in values
])
408 def _delete_string(self
, key
, value
):
409 return self
._delete
_strings
(key
, [value
,])
411 def passwd(self
, password
):
415 # The new password must have a score of 3 or better
416 quality
= self
.check_password_quality(password
)
417 if quality
["score"] < 3:
418 raise ValueError("Password too weak")
420 self
.accounts
._authenticate
()
421 self
.ldap
.passwd_s(self
.dn
, None, password
)
423 def check_password(self
, password
):
425 Bind to the server with given credentials and return
426 true if password is corrent and false if not.
428 Raises exceptions from the server on any other errors.
433 logging
.debug("Checking credentials for %s" % self
.dn
)
435 # Create a new LDAP connection
436 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
437 conn
= ldap
.initialize(ldap_uri
)
440 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
441 except ldap
.INVALID_CREDENTIALS
:
442 logging
.debug("Account credentials are invalid for %s" % self
)
445 logging
.info("Successfully authenticated %s" % self
)
449 def check_password_quality(self
, password
):
451 Passwords are passed through zxcvbn to make sure
452 that they are strong enough.
454 return zxcvbn
.zxcvbn(password
, user_inputs
=(
455 self
.first_name
, self
.last_name
,
459 return "wheel" in self
.groups
462 return "staff" in self
.groups
465 return "posixAccount" in self
.classes
468 return "postfixMailUser" in self
.classes
471 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
473 def can_be_managed_by(self
, account
):
475 Returns True if account is allowed to manage this account
477 # Admins can manage all accounts
478 if account
.is_admin():
481 # Users can manage themselves
482 return self
== account
486 return self
._get
_strings
("objectClass")
490 return self
._get
_string
("uid")
494 return self
._get
_string
("cn")
498 def get_nickname(self
):
499 return self
._get
_string
("displayName")
501 def set_nickname(self
, nickname
):
502 self
._set
_string
("displayName", nickname
)
504 nickname
= property(get_nickname
, set_nickname
)
508 def get_first_name(self
):
509 return self
._get
_string
("givenName")
511 def set_first_name(self
, first_name
):
512 self
._set
_string
("givenName", first_name
)
515 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
517 first_name
= property(get_first_name
, set_first_name
)
521 def get_last_name(self
):
522 return self
._get
_string
("sn")
524 def set_last_name(self
, last_name
):
525 self
._set
_string
("sn", last_name
)
528 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
530 last_name
= property(get_last_name
, set_last_name
)
534 groups
= self
.memcache
.get("accounts:%s:groups" % self
.dn
)
538 # Fetch groups from LDAP
539 groups
= self
._get
_groups
()
541 # Cache groups for 5 min
542 self
.memcache
.set("accounts:%s:groups" % self
.dn
, groups
, 300)
546 def _get_groups(self
):
549 res
= self
.accounts
._query
("(&(objectClass=posixGroup) \
550 (memberUid=%s))" % self
.uid
, ["cn"])
552 for dn
, attrs
in res
:
553 cns
= attrs
.get("cn")
555 groups
.append(cns
[0].decode())
559 # Created/Modified at
562 def created_at(self
):
563 return self
._get
_timestamp
("createTimestamp")
566 def modified_at(self
):
567 return self
._get
_timestamp
("modifyTimestamp")
576 address
+= self
.street
.splitlines()
578 if self
.postal_code
and self
.city
:
579 if self
.country_code
in ("AT", "DE"):
580 address
.append("%s %s" % (self
.postal_code
, self
.city
))
582 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
584 address
.append(self
.city
or self
.postal_code
)
586 if self
.country_name
:
587 address
.append(self
.country_name
)
591 def get_street(self
):
592 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
594 def set_street(self
, street
):
595 self
._set
_string
("street", street
)
597 street
= property(get_street
, set_street
)
600 return self
._get
_string
("l") or ""
602 def set_city(self
, city
):
603 self
._set
_string
("l", city
)
605 city
= property(get_city
, set_city
)
607 def get_postal_code(self
):
608 return self
._get
_string
("postalCode") or ""
610 def set_postal_code(self
, postal_code
):
611 self
._set
_string
("postalCode", postal_code
)
613 postal_code
= property(get_postal_code
, set_postal_code
)
615 # XXX This should be c
616 def get_country_code(self
):
617 return self
._get
_string
("st")
619 def set_country_code(self
, country_code
):
620 self
._set
_string
("st", country_code
)
622 country_code
= property(get_country_code
, set_country_code
)
625 def country_name(self
):
626 if self
.country_code
:
627 return countries
.get_name(self
.country_code
)
631 return self
._get
_string
("mail")
633 # Mail Routing Address
635 def get_mail_routing_address(self
):
636 return self
._get
_string
("mailRoutingAddress", None)
638 def set_mail_routing_address(self
, address
):
639 self
._set
_string
("mailRoutingAddress", address
or None)
641 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
645 if "sipUser" in self
.classes
:
646 return self
._get
_string
("sipAuthenticationUser")
648 if "sipRoutingObject" in self
.classes
:
649 return self
._get
_string
("sipLocalAddress")
652 def sip_password(self
):
653 return self
._get
_string
("sipPassword")
656 def _generate_sip_password():
657 return util
.random_string(8)
661 return "%s@ipfire.org" % self
.sip_id
663 def uses_sip_forwarding(self
):
664 if self
.sip_routing_address
:
671 def get_sip_routing_address(self
):
672 if "sipRoutingObject" in self
.classes
:
673 return self
._get
_string
("sipRoutingAddress")
675 def set_sip_routing_address(self
, address
):
679 # Don't do anything if nothing has changed
680 if self
.get_sip_routing_address() == address
:
684 # This is no longer a SIP user any more
687 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
688 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
689 (ldap
.MOD_DELETE
, "sipPassword", None),
691 except ldap
.NO_SUCH_ATTRIBUTE
:
694 # Set new routing object
697 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
698 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
699 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
702 # If this is a change, we cannot add this again
703 except ldap
.TYPE_OR_VALUE_EXISTS
:
704 self
._set
_string
("sipRoutingAddress", address
)
708 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
709 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
710 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
712 except ldap
.NO_SUCH_ATTRIBUTE
:
716 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
717 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
718 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
721 # XXX Cache is invalid here
723 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
726 def sip_registrations(self
):
727 sip_registrations
= []
729 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
732 sip_registrations
.append(reg
)
734 return sip_registrations
737 def sip_channels(self
):
738 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
740 def get_cdr(self
, date
=None, limit
=None):
741 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
746 def phone_number(self
):
748 Returns the IPFire phone number
751 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
754 def fax_number(self
):
756 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
758 def get_phone_numbers(self
):
761 for field
in ("telephoneNumber", "homePhone", "mobile"):
762 for number
in self
._get
_phone
_numbers
(field
):
767 def set_phone_numbers(self
, phone_numbers
):
768 # Sort phone numbers by landline and mobile
769 _landline_numbers
= []
772 for number
in phone_numbers
:
774 number
= phonenumbers
.parse(number
, None)
775 except phonenumbers
.phonenumberutil
.NumberParseException
:
778 # Convert to string (in E.164 format)
779 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
781 # Separate mobile numbers
782 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
783 _mobile_numbers
.append(s
)
785 _landline_numbers
.append(s
)
788 self
._set
_strings
("telephoneNumber", _landline_numbers
)
789 self
._set
_strings
("mobile", _mobile_numbers
)
791 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
794 def _all_telephone_numbers(self
):
795 ret
= [ self
.sip_id
, ]
797 if self
.phone_number
:
798 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
801 for number
in self
.phone_numbers
:
802 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
807 def avatar_url(self
, size
=None):
808 if self
.backend
.debug
:
809 hostname
= "http://people.dev.ipfire.org"
811 hostname
= "https://people.ipfire.org"
813 url
= "%s/users/%s.jpg" % (hostname
, self
.uid
)
816 url
+= "?size=%s" % size
820 def get_avatar(self
, size
=None):
821 photo
= self
._get
_bytes
("jpegPhoto")
823 # Exit if no avatar is available
827 # Return the raw image if no size was requested
831 # Try to retrieve something from the cache
832 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
836 # Generate a new thumbnail
837 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
839 # Save to cache for 15m
840 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
844 def upload_avatar(self
, avatar
):
845 self
._set
("jpegPhoto", avatar
)
853 for key
in self
._get
_strings
("sshPublicKey"):
854 s
= sshpubkeys
.SSHKey()
858 except (sshpubkeys
.InvalidKeyError
, NotImplementedError) as e
:
859 logging
.warning("Could not parse SSH key %s: %s" % (key
, e
))
866 def get_ssh_key_by_hash_sha256(self
, hash_sha256
):
867 for key
in self
.ssh_keys
:
868 if not key
.hash_sha256() == hash_sha256
:
873 def add_ssh_key(self
, key
):
874 k
= sshpubkeys
.SSHKey()
876 # Try to parse the key
879 # Check for types and sufficient sizes
880 if k
.key_type
== b
"ssh-rsa":
882 raise sshpubkeys
.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
884 elif k
.key_type
== b
"ssh-dss":
885 raise sshpubkeys
.InvalidKeyError("DSA keys are not supported")
887 # Ignore any duplicates
888 if key
in (k
.keydata
for k
in self
.ssh_keys
):
889 logging
.debug("SSH Key has already been added for %s: %s" % (self
, key
))
892 # Prepare transaction
895 # Add object class if user is not in it, yet
896 if not "ldapPublicKey" in self
.classes
:
897 modlist
.append((ldap
.MOD_ADD
, "objectClass", b
"ldapPublicKey"))
900 modlist
.append((ldap
.MOD_ADD
, "sshPublicKey", key
.encode()))
903 self
._modify
(modlist
)
906 self
.ssh_keys
.append(k
)
908 def delete_ssh_key(self
, key
):
909 if not key
in (k
.keydata
for k
in self
.ssh_keys
):
912 # Delete key from LDAP
913 if len(self
.ssh_keys
) > 1:
914 self
._delete
_string
("sshPublicKey", key
)
917 (ldap
.MOD_DELETE
, "objectClass", b
"ldapPublicKey"),
918 (ldap
.MOD_DELETE
, "sshPublicKey", key
.encode()),
922 if __name__
== "__main__":