18 import tornado
.httpclient
23 from . import countries
25 from .decorators
import *
26 from .misc
import Object
30 # Set the client keytab name
31 os
.environ
["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
33 class LDAPObject(Object
):
34 def init(self
, dn
, attrs
=None):
37 self
.attributes
= attrs
or {}
39 def __eq__(self
, other
):
40 if isinstance(other
, self
.__class
__):
41 return self
.dn
== other
.dn
45 return self
.accounts
.ldap
47 def _exists(self
, key
):
56 for value
in self
.attributes
.get(key
, []):
59 def _get_bytes(self
, key
, default
=None):
60 for value
in self
._get
(key
):
65 def _get_strings(self
, key
):
66 for value
in self
._get
(key
):
69 def _get_string(self
, key
, default
=None):
70 for value
in self
._get
_strings
(key
):
75 def _get_phone_numbers(self
, key
):
76 for value
in self
._get
_strings
(key
):
77 yield phonenumbers
.parse(value
, None)
79 def _get_timestamp(self
, key
):
80 value
= self
._get
_string
(key
)
82 # Parse the timestamp value and returns a datetime object
84 return datetime
.datetime
.strptime(value
, "%Y%m%d%H%M%SZ")
86 def _modify(self
, modlist
):
87 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
89 # Authenticate before performing any write operations
90 self
.accounts
._authenticate
()
92 # Run modify operation
93 self
.ldap
.modify_s(self
.dn
, modlist
)
98 def _clear_cache(self
):
104 def _set(self
, key
, values
):
105 current
= self
._get
(key
)
107 # Don't do anything if nothing has changed
108 if list(current
) == values
:
111 # Remove all old values and add all new ones
114 if self
._exists
(key
):
115 modlist
.append((ldap
.MOD_DELETE
, key
, None))
119 modlist
.append((ldap
.MOD_ADD
, key
, values
))
121 # Run modify operation
122 self
._modify
(modlist
)
125 self
.attributes
.update({ key
: values
})
127 def _set_bytes(self
, key
, values
):
128 return self
._set
(key
, values
)
130 def _set_strings(self
, key
, values
):
131 return self
._set
(key
, [e
.encode() for e
in values
if e
])
133 def _set_string(self
, key
, value
):
134 return self
._set
_strings
(key
, [value
,])
136 def _add(self
, key
, values
):
138 (ldap
.MOD_ADD
, key
, values
),
141 self
._modify
(modlist
)
143 def _add_strings(self
, key
, values
):
144 return self
._add
(key
, [e
.encode() for e
in values
])
146 def _add_string(self
, key
, value
):
147 return self
._add
_strings
(key
, [value
,])
149 def _delete(self
, key
, values
):
151 (ldap
.MOD_DELETE
, key
, values
),
154 self
._modify
(modlist
)
156 def _delete_strings(self
, key
, values
):
157 return self
._delete
(key
, [e
.encode() for e
in values
])
159 def _delete_string(self
, key
, value
):
160 return self
._delete
_strings
(key
, [value
,])
163 def objectclasses(self
):
164 return self
._get
_strings
("objectClass")
168 return datetime
.datetime
.strptime(s
.decode(), "%Y%m%d%H%M%SZ")
171 class Accounts(Object
):
173 self
.search_base
= self
.settings
.get("ldap_search_base")
176 count
= self
.memcache
.get("accounts:count")
179 count
= self
._count
("(objectClass=person)")
181 self
.memcache
.set("accounts:count", count
, 300)
186 accounts
= self
._search
("(objectClass=person)")
188 return iter(sorted(accounts
))
192 # Connect to LDAP server
193 ldap_uri
= self
.settings
.get("ldap_uri")
195 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
197 # Connect to the LDAP server
198 connection
= ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
199 trace_level
=2 if self
.backend
.debug
else 0,
200 retry_max
=sys
.maxsize
, retry_delay
=3)
202 # Set maximum timeout for operations
203 connection
.set_option(ldap
.OPT_TIMEOUT
, 10)
207 def _authenticate(self
):
208 # Authenticate against LDAP server using Kerberos
209 self
.ldap
.sasl_gssapi_bind_s()
211 async def test_ldap(self
):
212 logging
.info("Testing LDAP connection...")
216 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
218 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
219 logging
.debug("Performing LDAP query (%s): %s" \
220 % (search_base
or self
.search_base
, query
))
224 # Ask for up to 512 results being returned at a time
225 page_control
= ldap
.controls
.SimplePagedResultsControl(True, size
=512, cookie
="")
232 response
= self
.ldap
.search_ext(search_base
or self
.search_base
,
233 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
,
234 serverctrls
=[page_control
],
238 type, data
, rmsgid
, serverctrls
= self
.ldap
.result3(response
)
240 # Append to local copy
244 controls
= [c
for c
in serverctrls
245 if c
.controlType
== ldap
.controls
.SimplePagedResultsControl
.controlType
]
250 # Set the cookie for more results
251 page_control
.cookie
= controls
[0].cookie
253 # There are no more results
254 if not page_control
.cookie
:
257 # Log time it took to perform the query
258 logging
.debug("Query took %.2fms (%s page(s))" % ((time
.time() - t
) * 1000.0, pages
))
262 def _count(self
, query
):
263 res
= self
._query
(query
, attrlist
=["dn"])
267 def _search(self
, query
, attrlist
=None, limit
=0):
269 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
270 account
= self
.get_by_dn(dn
)
271 accounts
.append(account
)
275 def _get_attrs(self
, dn
):
277 Fetches all attributes for the given distinguished name
279 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
280 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
282 for dn
, attrs
in results
:
285 def get_by_dn(self
, dn
):
286 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
288 attrs
= self
._get
_attrs
(dn
)
291 # Cache all attributes for 5 min
292 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
294 return Account(self
.backend
, dn
, attrs
)
298 return t
.strftime("%Y%m%d%H%M%SZ")
300 def get_created_after(self
, ts
):
301 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
303 def count_created_after(self
, ts
):
304 return self
._count
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
306 def search(self
, query
):
307 accounts
= self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)(displayName=*%s*)(mail=*%s*)))" \
308 % (query
, query
, query
, query
))
310 return sorted(accounts
)
312 def _search_one(self
, query
):
313 results
= self
._search
(query
, limit
=1)
315 for result
in results
:
318 def uid_is_valid(self
, uid
):
319 # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
320 m
= re
.match(r
"^[a-z_][a-z0-9_-]{3,31}$", uid
)
326 def uid_exists(self
, uid
):
327 if self
.get_by_uid(uid
):
330 res
= self
.db
.get("SELECT 1 FROM account_activations \
331 WHERE uid = %s AND expires_at > NOW()", uid
)
336 # Account with uid does not exist, yet
339 def mail_is_valid(self
, mail
):
340 username
, delim
, domain
= mail
.partition("@")
342 # There must be an @ and a domain part
346 # The domain cannot end on a dot
347 if domain
.endswith("."):
350 # The domain should at least have one dot to fully qualified
351 if not "." in domain
:
354 # Looks like a valid email address
357 def mail_is_blacklisted(self
, mail
):
358 username
, delim
, domain
= mail
.partition("@")
361 return self
.domain_is_blacklisted(domain
)
363 def domain_is_blacklisted(self
, domain
):
364 res
= self
.db
.get("SELECT TRUE AS found FROM blacklisted_domains \
365 WHERE domain = %s OR %s LIKE '%%.' || domain", domain
, domain
)
367 if res
and res
.found
:
372 def get_by_uid(self
, uid
):
373 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
375 def get_by_mail(self
, mail
):
376 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
378 def find_account(self
, s
):
379 account
= self
.get_by_uid(s
)
383 return self
.get_by_mail(s
)
385 def get_by_sip_id(self
, sip_id
):
389 return self
._search
_one
(
390 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
393 def get_by_phone_number(self
, number
):
397 return self
._search
_one
(
398 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
399 % (number
, number
, number
, number
))
402 def pending_registrations(self
):
403 res
= self
.db
.get("SELECT COUNT(*) AS c FROM account_activations")
407 def auth(self
, username
, password
):
409 account
= self
.backend
.accounts
.find_account(username
)
412 if account
and account
.check_password(password
):
417 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
418 # Convert all uids to lowercase
421 # Check if UID is valid
422 if not self
.uid_is_valid(uid
):
423 raise ValueError("UID is invalid: %s" % uid
)
425 # Check if UID is unique
426 if self
.uid_exists(uid
):
427 raise ValueError("UID exists: %s" % uid
)
429 # Check if the email address is valid
430 if not self
.mail_is_valid(email
):
431 raise ValueError("Email is invalid: %s" % email
)
433 # Check if the email address is blacklisted
434 if self
.mail_is_blacklisted(email
):
435 raise ValueError("Email is blacklisted: %s" % email
)
437 # Generate a random activation code
438 activation_code
= util
.random_string(36)
440 # Create an entry in our database until the user
441 # has activated the account
442 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
443 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
444 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
446 # Send an account activation email
447 self
.backend
.messages
.send_template("auth/messages/register",
448 priority
=100, uid
=uid
, activation_code
=activation_code
, email
=email
,
449 first_name
=first_name
, last_name
=last_name
)
451 def activate(self
, uid
, activation_code
):
452 res
= self
.db
.get("DELETE FROM account_activations \
453 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
454 RETURNING *", uid
, activation_code
)
456 # Return nothing when account was not found
460 # Return the account if it has already been created
461 account
= self
.get_by_uid(uid
)
465 # Create a new account on the LDAP database
466 account
= self
.create(uid
, res
.email
,
467 first_name
=res
.first_name
, last_name
=res
.last_name
,
468 country_code
=res
.country_code
)
470 # Non-EU users do not need to consent to promo emails
471 if account
.country_code
and not account
.country_code
in countries
.EU_COUNTRIES
:
472 account
.consents_to_promotional_emails
= True
474 # Send email about account registration
475 self
.backend
.messages
.send_template("people/messages/new-account",
478 # Launch drip campaigns
479 for campaign
in ("signup", "christmas"):
480 self
.backend
.campaigns
.launch(campaign
, account
)
484 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
485 cn
= "%s %s" % (first_name
, last_name
)
489 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
490 "mail" : email
.encode(),
494 "sn" : last_name
.encode(),
495 "givenName" : first_name
.encode(),
498 logging
.info("Creating new account: %s: %s" % (uid
, account
))
501 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
503 # Create account on LDAP
504 self
.accounts
._authenticate
()
505 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
508 account
= self
.get_by_dn(dn
)
510 # Optionally set country code
512 account
.country_code
= country_code
519 def create_session(self
, account
, host
):
520 session_id
= util
.random_string(64)
522 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
523 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
525 # Session could not be created
529 logging
.info("Created session %s for %s which expires %s" \
530 % (res
.session_id
, account
, res
.time_expires
))
531 return res
.session_id
, res
.time_expires
533 def destroy_session(self
, session_id
, host
):
534 logging
.info("Destroying session %s" % session_id
)
536 self
.db
.execute("DELETE FROM sessions \
537 WHERE session_id = %s AND host = %s", session_id
, host
)
539 def get_by_session(self
, session_id
, host
):
540 logging
.debug("Looking up session %s" % session_id
)
542 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
543 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
546 # Session does not exist or has expired
550 # Update the session expiration time
551 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
552 WHERE session_id = %s AND host = %s", session_id
, host
)
554 return self
.get_by_uid(res
.uid
)
557 # Cleanup expired sessions
558 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
560 # Cleanup expired account activations
561 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
563 # Cleanup expired account password resets
564 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
568 def decode_discourse_payload(self
, payload
, signature
):
570 calculated_signature
= self
.sign_discourse_payload(payload
)
572 if not hmac
.compare_digest(signature
, calculated_signature
):
573 raise ValueError("Invalid signature: %s" % signature
)
575 # Decode the query string
576 qs
= base64
.b64decode(payload
).decode()
578 # Parse the query string
580 for key
, val
in urllib
.parse
.parse_qsl(qs
):
585 def encode_discourse_payload(self
, **args
):
586 # Encode the arguments into an URL-formatted string
587 qs
= urllib
.parse
.urlencode(args
).encode()
590 return base64
.b64encode(qs
).decode()
592 def sign_discourse_payload(self
, payload
, secret
=None):
594 secret
= self
.settings
.get("discourse_sso_secret")
596 # Calculate a HMAC using SHA256
597 h
= hmac
.new(secret
.encode(),
598 msg
=payload
.encode(), digestmod
="sha256")
606 for country
in iso3166
.countries
:
607 count
= self
._count
("(&(objectClass=person)(st=%s))" % country
.alpha2
)
614 async def get_all_emails(self
):
615 # Returns all email addresses
616 for dn
, attrs
in self
._query
("(objectClass=person)", attrlist
=("mail",)):
617 mails
= attrs
.get("mail", None)
625 class Account(LDAPObject
):
633 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
635 def __lt__(self
, other
):
636 if isinstance(other
, self
.__class
__):
637 return self
.name
< other
.name
639 def _clear_cache(self
):
640 # Delete cached attributes
641 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
644 def kerberos_attributes(self
):
645 res
= self
.backend
.accounts
._query
(
646 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
648 "krbLastSuccessfulAuth",
649 "krbLastPasswordChange",
651 "krbLoginFailedCount",
654 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
656 for dn
, attrs
in res
:
657 return { key
: attrs
[key
][0] for key
in attrs
}
662 def last_successful_authentication(self
):
664 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
668 return self
._parse
_date
(s
)
671 def last_failed_authentication(self
):
673 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
677 return self
._parse
_date
(s
)
680 def failed_login_count(self
):
682 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
691 def passwd(self
, password
):
695 # The new password must have a score of 3 or better
696 quality
= self
.check_password_quality(password
)
697 if quality
["score"] < 3:
698 raise ValueError("Password too weak")
700 self
.accounts
._authenticate
()
701 self
.ldap
.passwd_s(self
.dn
, None, password
)
703 def check_password(self
, password
):
705 Bind to the server with given credentials and return
706 true if password is corrent and false if not.
708 Raises exceptions from the server on any other errors.
713 logging
.debug("Checking credentials for %s" % self
.dn
)
715 # Create a new LDAP connection
716 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
717 conn
= ldap
.initialize(ldap_uri
)
720 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
721 except ldap
.INVALID_CREDENTIALS
:
722 logging
.debug("Account credentials are invalid for %s" % self
)
725 logging
.info("Successfully authenticated %s" % self
)
729 def check_password_quality(self
, password
):
731 Passwords are passed through zxcvbn to make sure
732 that they are strong enough.
734 return zxcvbn
.zxcvbn(password
, user_inputs
=(
735 self
.first_name
, self
.last_name
,
738 def request_password_reset(self
, address
=None):
739 reset_code
= util
.random_string(64)
741 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
742 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
744 # Send a password reset email
745 self
.backend
.messages
.send_template("auth/messages/password-reset",
746 priority
=100, account
=self
, reset_code
=reset_code
)
748 def reset_password(self
, reset_code
, new_password
):
749 # Delete the reset token
750 res
= self
.db
.query("DELETE FROM account_password_resets \
751 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
752 RETURNING *", self
.uid
, reset_code
)
754 # The reset code was invalid
756 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
758 # Perform password change
759 return self
.passwd(new_password
)
762 return self
.is_member_of_group("sudo")
765 return self
.is_member_of_group("staff")
767 def is_moderator(self
):
768 return self
.is_member_of_group("moderators")
771 return "posixAccount" in self
.classes
774 return "postfixMailUser" in self
.classes
777 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
779 def can_be_managed_by(self
, account
):
781 Returns True if account is allowed to manage this account
783 # Admins can manage all accounts
784 if account
.is_admin():
787 # Users can manage themselves
788 return self
== account
792 return self
._get
_strings
("objectClass")
796 return self
._get
_string
("uid")
800 return self
._get
_string
("cn")
804 def get_nickname(self
):
805 return self
._get
_string
("displayName")
807 def set_nickname(self
, nickname
):
808 self
._set
_string
("displayName", nickname
)
810 nickname
= property(get_nickname
, set_nickname
)
814 def get_first_name(self
):
815 return self
._get
_string
("givenName")
817 def set_first_name(self
, first_name
):
818 self
._set
_string
("givenName", first_name
)
821 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
823 first_name
= property(get_first_name
, set_first_name
)
827 def get_last_name(self
):
828 return self
._get
_string
("sn")
830 def set_last_name(self
, last_name
):
831 self
._set
_string
("sn", last_name
)
834 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
836 last_name
= property(get_last_name
, set_last_name
)
840 return self
.backend
.groups
._get
_groups
("(| \
841 (&(objectClass=groupOfNames)(member=%s)) \
842 (&(objectClass=posixGroup)(memberUid=%s)) \
843 )" % (self
.dn
, self
.uid
))
845 def is_member_of_group(self
, gid
):
847 Returns True if this account is a member of this group
849 return gid
in (g
.gid
for g
in self
.groups
)
851 # Created/Modified at
854 def created_at(self
):
855 return self
._get
_timestamp
("createTimestamp")
858 def modified_at(self
):
859 return self
._get
_timestamp
("modifyTimestamp")
868 address
+= self
.street
.splitlines()
870 if self
.postal_code
and self
.city
:
871 if self
.country_code
in ("AT", "DE"):
872 address
.append("%s %s" % (self
.postal_code
, self
.city
))
874 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
876 address
.append(self
.city
or self
.postal_code
)
878 if self
.country_name
:
879 address
.append(self
.country_name
)
883 def get_street(self
):
884 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
886 def set_street(self
, street
):
887 self
._set
_string
("street", street
)
889 street
= property(get_street
, set_street
)
892 return self
._get
_string
("l") or ""
894 def set_city(self
, city
):
895 self
._set
_string
("l", city
)
897 city
= property(get_city
, set_city
)
899 def get_postal_code(self
):
900 return self
._get
_string
("postalCode") or ""
902 def set_postal_code(self
, postal_code
):
903 self
._set
_string
("postalCode", postal_code
)
905 postal_code
= property(get_postal_code
, set_postal_code
)
907 # XXX This should be c
908 def get_country_code(self
):
909 return self
._get
_string
("st")
911 def set_country_code(self
, country_code
):
912 self
._set
_string
("st", country_code
)
914 country_code
= property(get_country_code
, set_country_code
)
917 def country_name(self
):
918 if self
.country_code
:
919 return self
.backend
.get_country_name(self
.country_code
)
923 return self
._get
_string
("mail")
927 return "%s <%s>" % (self
, self
.email
)
929 # Mail Routing Address
931 def get_mail_routing_address(self
):
932 return self
._get
_string
("mailRoutingAddress", None)
934 def set_mail_routing_address(self
, address
):
935 self
._set
_string
("mailRoutingAddress", address
or None)
937 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
941 if "sipUser" in self
.classes
:
942 return self
._get
_string
("sipAuthenticationUser")
944 if "sipRoutingObject" in self
.classes
:
945 return self
._get
_string
("sipLocalAddress")
948 def sip_password(self
):
949 return self
._get
_string
("sipPassword")
952 def _generate_sip_password():
953 return util
.random_string(8)
957 return "%s@ipfire.org" % self
.sip_id
960 def agent_status(self
):
961 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
963 def uses_sip_forwarding(self
):
964 if self
.sip_routing_address
:
971 def get_sip_routing_address(self
):
972 if "sipRoutingObject" in self
.classes
:
973 return self
._get
_string
("sipRoutingAddress")
975 def set_sip_routing_address(self
, address
):
979 # Don't do anything if nothing has changed
980 if self
.get_sip_routing_address() == address
:
984 # This is no longer a SIP user any more
987 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
988 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
989 (ldap
.MOD_DELETE
, "sipPassword", None),
991 except ldap
.NO_SUCH_ATTRIBUTE
:
994 # Set new routing object
997 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
998 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
999 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
1002 # If this is a change, we cannot add this again
1003 except ldap
.TYPE_OR_VALUE_EXISTS
:
1004 self
._set
_string
("sipRoutingAddress", address
)
1008 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
1009 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
1010 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
1012 except ldap
.NO_SUCH_ATTRIBUTE
:
1016 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
1017 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
1018 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
1021 # XXX Cache is invalid here
1023 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
1026 def sip_registrations(self
):
1027 sip_registrations
= []
1029 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
1032 sip_registrations
.append(reg
)
1034 return sip_registrations
1037 def sip_channels(self
):
1038 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
1040 def get_cdr(self
, date
=None, limit
=None):
1041 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
1046 def phone_number(self
):
1048 Returns the IPFire phone number
1051 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
1054 def fax_number(self
):
1056 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
1058 def get_phone_numbers(self
):
1061 for field
in ("telephoneNumber", "homePhone", "mobile"):
1062 for number
in self
._get
_phone
_numbers
(field
):
1067 def set_phone_numbers(self
, phone_numbers
):
1068 # Sort phone numbers by landline and mobile
1069 _landline_numbers
= []
1070 _mobile_numbers
= []
1072 for number
in phone_numbers
:
1074 number
= phonenumbers
.parse(number
, None)
1075 except phonenumbers
.phonenumberutil
.NumberParseException
:
1078 # Convert to string (in E.164 format)
1079 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1081 # Separate mobile numbers
1082 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1083 _mobile_numbers
.append(s
)
1085 _landline_numbers
.append(s
)
1088 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1089 self
._set
_strings
("mobile", _mobile_numbers
)
1091 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1094 def _all_telephone_numbers(self
):
1095 ret
= [ self
.sip_id
, ]
1097 if self
.phone_number
:
1098 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1101 for number
in self
.phone_numbers
:
1102 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1109 def get_description(self
):
1110 return self
._get
_string
("description")
1112 def set_description(self
, description
):
1113 self
._set
_string
("description", description
)
1115 description
= property(get_description
, set_description
)
1119 def has_avatar(self
):
1120 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
1121 if has_avatar
is None:
1122 has_avatar
= True if self
.get_avatar() else False
1124 # Cache avatar status for up to 24 hours
1125 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1129 def avatar_url(self
, size
=None):
1130 url
= "https://people.ipfire.org/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1133 url
+= "&size=%s" % size
1137 def get_avatar(self
, size
=None):
1138 photo
= self
._get
_bytes
("jpegPhoto")
1140 # Exit if no avatar is available
1144 # Return the raw image if no size was requested
1148 # Try to retrieve something from the cache
1149 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1153 # Generate a new thumbnail
1154 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1156 # Save to cache for 15m
1157 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1162 def avatar_hash(self
):
1163 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1165 h
= hashlib
.new("md5")
1166 h
.update(self
.get_avatar() or b
"")
1167 hash = h
.hexdigest()[:7]
1169 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1173 def upload_avatar(self
, avatar
):
1174 self
._set
("jpegPhoto", avatar
)
1176 # Delete cached avatar status
1177 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1179 # Delete avatar hash
1180 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1182 # Consent to promotional emails
1184 def get_consents_to_promotional_emails(self
):
1185 return self
.is_member_of_group("promotional-consent")
1187 def set_contents_to_promotional_emails(self
, value
):
1188 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1189 assert group
, "Could not find group: promotional-consent"
1192 group
.add_member(self
)
1194 group
.del_member(self
)
1196 consents_to_promotional_emails
= property(
1197 get_consents_to_promotional_emails
,
1198 set_contents_to_promotional_emails
,
1202 class Groups(Object
):
1204 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1205 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1207 # Everyone is a member of people
1208 "cn=people,ou=Group,dc=ipfire,dc=org",
1212 def search_base(self
):
1213 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1215 def _query(self
, *args
, **kwargs
):
1217 "search_base" : self
.backend
.groups
.search_base
,
1220 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1223 groups
= self
.get_all()
1227 def _get_groups(self
, query
, **kwargs
):
1228 res
= self
._query
(query
, **kwargs
)
1231 for dn
, attrs
in res
:
1232 # Skip any hidden groups
1233 if dn
in self
.hidden_groups
:
1236 g
= Group(self
.backend
, dn
, attrs
)
1239 return sorted(groups
)
1241 def _get_group(self
, query
, **kwargs
):
1246 groups
= self
._get
_groups
(query
, **kwargs
)
1251 return self
._get
_groups
(
1252 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1255 def get_by_gid(self
, gid
):
1256 return self
._get
_group
(
1257 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1261 class Group(LDAPObject
):
1263 if self
.description
:
1264 return "<%s %s (%s)>" % (
1265 self
.__class
__.__name
__,
1270 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1273 return self
.description
or self
.gid
1275 def __lt__(self
, other
):
1276 if isinstance(other
, self
.__class
__):
1277 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1284 Returns the number of members in this group
1288 for attr
in ("member", "memberUid"):
1289 a
= self
.attributes
.get(attr
, None)
1296 return iter(self
.members
)
1300 return self
._get
_string
("cn")
1303 def description(self
):
1304 return self
._get
_string
("description")
1308 return self
._get
_string
("mail")
1314 # Get all members by DN
1315 for dn
in self
._get
_strings
("member"):
1316 member
= self
.backend
.accounts
.get_by_dn(dn
)
1318 members
.append(member
)
1320 # Get all members by UID
1321 for uid
in self
._get
_strings
("memberUid"):
1322 member
= self
.backend
.accounts
.get_by_uid(uid
)
1324 members
.append(member
)
1326 return sorted(members
)
1328 def add_member(self
, account
):
1330 Adds a member to this group
1332 # Do nothing if this user is already in the group
1333 if account
.is_member_of_group(self
.gid
):
1336 if "posixGroup" in self
.objectclasses
:
1337 self
._add
_string
("memberUid", account
.uid
)
1339 self
._add
_string
("member", account
.dn
)
1341 # Append to cached list of members
1342 self
.members
.append(account
)
1345 def del_member(self
, account
):
1347 Removes a member from a group
1349 # Do nothing if this user is not in the group
1350 if not account
.is_member_of_group(self
.gid
):
1353 if "posixGroup" in self
.objectclasses
:
1354 self
._delete
_string
("memberUid", account
.uid
)
1356 self
._delete
_string
("member", account
.dn
)
1359 if __name__
== "__main__":