17 import tornado
.httpclient
22 from . import countries
24 from .decorators
import *
25 from .misc
import Object
27 # Set the client keytab name
28 os
.environ
["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
30 class LDAPObject(Object
):
31 def init(self
, dn
, attrs
=None):
34 self
.attributes
= attrs
or {}
36 def __eq__(self
, other
):
37 if isinstance(other
, self
.__class
__):
38 return self
.dn
== other
.dn
42 return self
.accounts
.ldap
44 def _exists(self
, key
):
53 for value
in self
.attributes
.get(key
, []):
56 def _get_bytes(self
, key
, default
=None):
57 for value
in self
._get
(key
):
62 def _get_strings(self
, key
):
63 for value
in self
._get
(key
):
66 def _get_string(self
, key
, default
=None):
67 for value
in self
._get
_strings
(key
):
72 def _get_phone_numbers(self
, key
):
73 for value
in self
._get
_strings
(key
):
74 yield phonenumbers
.parse(value
, None)
76 def _get_timestamp(self
, key
):
77 value
= self
._get
_string
(key
)
79 # Parse the timestamp value and returns a datetime object
81 return datetime
.datetime
.strptime(value
, "%Y%m%d%H%M%SZ")
83 def _modify(self
, modlist
):
84 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
86 # Authenticate before performing any write operations
87 self
.accounts
._authenticate
()
89 # Run modify operation
90 self
.ldap
.modify_s(self
.dn
, modlist
)
95 def _clear_cache(self
):
101 def _set(self
, key
, values
):
102 current
= self
._get
(key
)
104 # Don't do anything if nothing has changed
105 if list(current
) == values
:
108 # Remove all old values and add all new ones
111 if self
._exists
(key
):
112 modlist
.append((ldap
.MOD_DELETE
, key
, None))
116 modlist
.append((ldap
.MOD_ADD
, key
, values
))
118 # Run modify operation
119 self
._modify
(modlist
)
122 self
.attributes
.update({ key
: values
})
124 def _set_bytes(self
, key
, values
):
125 return self
._set
(key
, values
)
127 def _set_strings(self
, key
, values
):
128 return self
._set
(key
, [e
.encode() for e
in values
if e
])
130 def _set_string(self
, key
, value
):
131 return self
._set
_strings
(key
, [value
,])
133 def _add(self
, key
, values
):
135 (ldap
.MOD_ADD
, key
, values
),
138 self
._modify
(modlist
)
140 def _add_strings(self
, key
, values
):
141 return self
._add
(key
, [e
.encode() for e
in values
])
143 def _add_string(self
, key
, value
):
144 return self
._add
_strings
(key
, [value
,])
146 def _delete(self
, key
, values
):
148 (ldap
.MOD_DELETE
, key
, values
),
151 self
._modify
(modlist
)
153 def _delete_strings(self
, key
, values
):
154 return self
._delete
(key
, [e
.encode() for e
in values
])
156 def _delete_string(self
, key
, value
):
157 return self
._delete
_strings
(key
, [value
,])
160 def objectclasses(self
):
161 return self
._get
_strings
("objectClass")
165 return datetime
.datetime
.strptime(s
.decode(), "%Y%m%d%H%M%SZ")
168 class Accounts(Object
):
170 self
.search_base
= self
.settings
.get("ldap_search_base")
173 count
= self
.memcache
.get("accounts:count")
176 count
= self
._count
("(objectClass=person)")
178 self
.memcache
.set("accounts:count", count
, 300)
183 # Only return developers (group with ID 1000)
184 accounts
= self
._search
("(&(objectClass=posixAccount)(gidNumber=1000))")
186 return iter(sorted(accounts
))
190 # Connect to LDAP server
191 ldap_uri
= self
.settings
.get("ldap_uri")
193 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
195 # Connect to the LDAP server
196 return ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
197 retry_max
=10, retry_delay
=3)
199 def _authenticate(self
):
200 # Authenticate against LDAP server using Kerberos
201 self
.ldap
.sasl_gssapi_bind_s()
204 logging
.info("Testing LDAP connection...")
208 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
210 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
211 logging
.debug("Performing LDAP query (%s): %s" \
212 % (search_base
or self
.search_base
, query
))
216 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
217 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
)
219 # Log time it took to perform the query
220 logging
.debug("Query took %.2fms" % ((time
.time() - t
) * 1000.0))
224 def _count(self
, query
):
225 res
= self
._query
(query
, attrlist
=["dn"])
229 def _search(self
, query
, attrlist
=None, limit
=0):
231 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
232 account
= self
.get_by_dn(dn
)
233 accounts
.append(account
)
237 def _get_attrs(self
, dn
):
239 Fetches all attributes for the given distinguished name
241 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
242 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
244 for dn
, attrs
in results
:
247 def get_by_dn(self
, dn
):
248 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
250 attrs
= self
._get
_attrs
(dn
)
253 # Cache all attributes for 5 min
254 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
256 return Account(self
.backend
, dn
, attrs
)
260 return t
.strftime("%Y%m%d%H%M%SZ")
262 def get_created_after(self
, ts
):
263 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
265 def count_created_after(self
, ts
):
266 return self
._count
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
268 def search(self
, query
):
269 accounts
= self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)(displayName=*%s*)(mail=*%s*)))" \
270 % (query
, query
, query
, query
))
272 return sorted(accounts
)
274 def _search_one(self
, query
):
275 results
= self
._search
(query
, limit
=1)
277 for result
in results
:
280 def uid_is_valid(self
, uid
):
281 # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
282 m
= re
.match(r
"^[a-z_][a-z0-9_-]{3,31}$", uid
)
288 def uid_exists(self
, uid
):
289 if self
.get_by_uid(uid
):
292 res
= self
.db
.get("SELECT 1 FROM account_activations \
293 WHERE uid = %s AND expires_at > NOW()", uid
)
298 # Account with uid does not exist, yet
301 def get_by_uid(self
, uid
):
302 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
304 def get_by_mail(self
, mail
):
305 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
307 def find_account(self
, s
):
308 account
= self
.get_by_uid(s
)
312 return self
.get_by_mail(s
)
314 def get_by_sip_id(self
, sip_id
):
318 return self
._search
_one
(
319 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
322 def get_by_phone_number(self
, number
):
326 return self
._search
_one
(
327 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
328 % (number
, number
, number
, number
))
330 async def check_spam(self
, uid
, email
, address
):
331 sfs
= StopForumSpam(self
.backend
, uid
, email
, address
)
334 score
= await sfs
.check()
338 def auth(self
, username
, password
):
340 account
= self
.backend
.accounts
.find_account(username
)
343 if account
and account
.check_password(password
):
348 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
349 # Convert all uids to lowercase
352 # Check if UID is valid
353 if not self
.uid_is_valid(uid
):
354 raise ValueError("UID is invalid: %s" % uid
)
356 # Check if UID is unique
357 if self
.uid_exists(uid
):
358 raise ValueError("UID exists: %s" % uid
)
360 # Generate a random activation code
361 activation_code
= util
.random_string(36)
363 # Create an entry in our database until the user
364 # has activated the account
365 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
366 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
367 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
369 # Send an account activation email
370 self
.backend
.messages
.send_template("auth/messages/register",
371 recipients
=[email
], priority
=100, uid
=uid
,
372 activation_code
=activation_code
, email
=email
,
373 first_name
=first_name
, last_name
=last_name
)
375 def activate(self
, uid
, activation_code
):
376 res
= self
.db
.get("DELETE FROM account_activations \
377 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
378 RETURNING *", uid
, activation_code
)
380 # Return nothing when account was not found
384 # Return the account if it has already been created
385 account
= self
.get_by_uid(uid
)
389 # Create a new account on the LDAP database
390 account
= self
.create(uid
, res
.email
,
391 first_name
=res
.first_name
, last_name
=res
.last_name
,
392 country_code
=res
.country_code
)
394 # Non-EU users do not need to consent to promo emails
395 if account
.country_code
and not account
.country_code
in countries
.EU_COUNTRIES
:
396 account
.consents_to_promotional_emails
= True
398 # Invite newly registered users to newsletter
399 self
.backend
.messages
.send_template(
400 "newsletter/subscribe", address
="%s <%s>" % (account
, account
.email
))
402 # Send email about account registration
403 self
.backend
.messages
.send_template("people/messages/new-account",
404 recipients
=["moderators@ipfire.org"], account
=account
)
406 # Launch drip campaigns
407 for campaign
in ("signup", "christmas"):
408 self
.backend
.campaigns
.launch(campaign
, account
)
412 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
413 cn
= "%s %s" % (first_name
, last_name
)
417 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
418 "mail" : email
.encode(),
422 "sn" : last_name
.encode(),
423 "givenName" : first_name
.encode(),
426 logging
.info("Creating new account: %s: %s" % (uid
, account
))
429 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
431 # Create account on LDAP
432 self
.accounts
._authenticate
()
433 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
436 account
= self
.get_by_dn(dn
)
438 # Optionally set country code
440 account
.country_code
= country_code
447 def create_session(self
, account
, host
):
448 session_id
= util
.random_string(64)
450 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
451 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
453 # Session could not be created
457 logging
.info("Created session %s for %s which expires %s" \
458 % (res
.session_id
, account
, res
.time_expires
))
459 return res
.session_id
, res
.time_expires
461 def destroy_session(self
, session_id
, host
):
462 logging
.info("Destroying session %s" % session_id
)
464 self
.db
.execute("DELETE FROM sessions \
465 WHERE session_id = %s AND host = %s", session_id
, host
)
467 def get_by_session(self
, session_id
, host
):
468 logging
.debug("Looking up session %s" % session_id
)
470 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
471 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
474 # Session does not exist or has expired
478 # Update the session expiration time
479 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
480 WHERE session_id = %s AND host = %s", session_id
, host
)
482 return self
.get_by_uid(res
.uid
)
485 # Cleanup expired sessions
486 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
488 # Cleanup expired account activations
489 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
491 # Cleanup expired account password resets
492 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
496 def decode_discourse_payload(self
, payload
, signature
):
498 calculated_signature
= self
.sign_discourse_payload(payload
)
500 if not hmac
.compare_digest(signature
, calculated_signature
):
501 raise ValueError("Invalid signature: %s" % signature
)
503 # Decode the query string
504 qs
= base64
.b64decode(payload
).decode()
506 # Parse the query string
508 for key
, val
in urllib
.parse
.parse_qsl(qs
):
513 def encode_discourse_payload(self
, **args
):
514 # Encode the arguments into an URL-formatted string
515 qs
= urllib
.parse
.urlencode(args
).encode()
518 return base64
.b64encode(qs
).decode()
520 def sign_discourse_payload(self
, payload
, secret
=None):
522 secret
= self
.settings
.get("discourse_sso_secret")
524 # Calculate a HMAC using SHA256
525 h
= hmac
.new(secret
.encode(),
526 msg
=payload
.encode(), digestmod
="sha256")
534 for country
in iso3166
.countries
:
535 count
= self
._count
("(&(objectClass=person)(st=%s))" % country
.alpha2
)
543 class Account(LDAPObject
):
551 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
553 def __lt__(self
, other
):
554 if isinstance(other
, self
.__class
__):
555 return self
.name
< other
.name
557 def _clear_cache(self
):
558 # Delete cached attributes
559 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
562 def kerberos_attributes(self
):
563 res
= self
.backend
.accounts
._query
(
564 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
566 "krbLastSuccessfulAuth",
567 "krbLastPasswordChange",
569 "krbLoginFailedCount",
572 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
574 for dn
, attrs
in res
:
575 return { key
: attrs
[key
][0] for key
in attrs
}
580 def last_successful_authentication(self
):
582 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
586 return self
._parse
_date
(s
)
589 def last_failed_authentication(self
):
591 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
595 return self
._parse
_date
(s
)
598 def failed_login_count(self
):
600 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
609 def passwd(self
, password
):
613 # The new password must have a score of 3 or better
614 quality
= self
.check_password_quality(password
)
615 if quality
["score"] < 3:
616 raise ValueError("Password too weak")
618 self
.accounts
._authenticate
()
619 self
.ldap
.passwd_s(self
.dn
, None, password
)
621 def check_password(self
, password
):
623 Bind to the server with given credentials and return
624 true if password is corrent and false if not.
626 Raises exceptions from the server on any other errors.
631 logging
.debug("Checking credentials for %s" % self
.dn
)
633 # Create a new LDAP connection
634 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
635 conn
= ldap
.initialize(ldap_uri
)
638 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
639 except ldap
.INVALID_CREDENTIALS
:
640 logging
.debug("Account credentials are invalid for %s" % self
)
643 logging
.info("Successfully authenticated %s" % self
)
647 def check_password_quality(self
, password
):
649 Passwords are passed through zxcvbn to make sure
650 that they are strong enough.
652 return zxcvbn
.zxcvbn(password
, user_inputs
=(
653 self
.first_name
, self
.last_name
,
656 def request_password_reset(self
, address
=None):
657 reset_code
= util
.random_string(64)
659 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
660 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
662 # Send a password reset email
663 self
.backend
.messages
.send_template("auth/messages/password-reset",
664 recipients
=[self
.email
], priority
=100, account
=self
, reset_code
=reset_code
)
666 def reset_password(self
, reset_code
, new_password
):
667 # Delete the reset token
668 res
= self
.db
.query("DELETE FROM account_password_resets \
669 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
670 RETURNING *", self
.uid
, reset_code
)
672 # The reset code was invalid
674 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
676 # Perform password change
677 return self
.passwd(new_password
)
680 return self
.is_member_of_group("sudo")
683 return self
.is_member_of_group("staff")
685 def is_moderator(self
):
686 return self
.is_member_of_group("moderators")
689 return "posixAccount" in self
.classes
692 return "postfixMailUser" in self
.classes
695 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
697 def can_be_managed_by(self
, account
):
699 Returns True if account is allowed to manage this account
701 # Admins can manage all accounts
702 if account
.is_admin():
705 # Users can manage themselves
706 return self
== account
710 return self
._get
_strings
("objectClass")
714 return self
._get
_string
("uid")
718 return self
._get
_string
("cn")
722 def get_nickname(self
):
723 return self
._get
_string
("displayName")
725 def set_nickname(self
, nickname
):
726 self
._set
_string
("displayName", nickname
)
728 nickname
= property(get_nickname
, set_nickname
)
732 def get_first_name(self
):
733 return self
._get
_string
("givenName")
735 def set_first_name(self
, first_name
):
736 self
._set
_string
("givenName", first_name
)
739 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
741 first_name
= property(get_first_name
, set_first_name
)
745 def get_last_name(self
):
746 return self
._get
_string
("sn")
748 def set_last_name(self
, last_name
):
749 self
._set
_string
("sn", last_name
)
752 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
754 last_name
= property(get_last_name
, set_last_name
)
758 return self
.backend
.groups
._get
_groups
("(| \
759 (&(objectClass=groupOfNames)(member=%s)) \
760 (&(objectClass=posixGroup)(memberUid=%s)) \
761 )" % (self
.dn
, self
.uid
))
763 def is_member_of_group(self
, gid
):
765 Returns True if this account is a member of this group
767 return gid
in (g
.gid
for g
in self
.groups
)
769 # Created/Modified at
772 def created_at(self
):
773 return self
._get
_timestamp
("createTimestamp")
776 def modified_at(self
):
777 return self
._get
_timestamp
("modifyTimestamp")
786 address
+= self
.street
.splitlines()
788 if self
.postal_code
and self
.city
:
789 if self
.country_code
in ("AT", "DE"):
790 address
.append("%s %s" % (self
.postal_code
, self
.city
))
792 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
794 address
.append(self
.city
or self
.postal_code
)
796 if self
.country_name
:
797 address
.append(self
.country_name
)
801 def get_street(self
):
802 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
804 def set_street(self
, street
):
805 self
._set
_string
("street", street
)
807 street
= property(get_street
, set_street
)
810 return self
._get
_string
("l") or ""
812 def set_city(self
, city
):
813 self
._set
_string
("l", city
)
815 city
= property(get_city
, set_city
)
817 def get_postal_code(self
):
818 return self
._get
_string
("postalCode") or ""
820 def set_postal_code(self
, postal_code
):
821 self
._set
_string
("postalCode", postal_code
)
823 postal_code
= property(get_postal_code
, set_postal_code
)
825 # XXX This should be c
826 def get_country_code(self
):
827 return self
._get
_string
("st")
829 def set_country_code(self
, country_code
):
830 self
._set
_string
("st", country_code
)
832 country_code
= property(get_country_code
, set_country_code
)
835 def country_name(self
):
836 if self
.country_code
:
837 return countries
.get_name(self
.country_code
)
841 return self
._get
_string
("mail")
845 return "%s <%s>" % (self
, self
.email
)
847 # Mail Routing Address
849 def get_mail_routing_address(self
):
850 return self
._get
_string
("mailRoutingAddress", None)
852 def set_mail_routing_address(self
, address
):
853 self
._set
_string
("mailRoutingAddress", address
or None)
855 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
859 if "sipUser" in self
.classes
:
860 return self
._get
_string
("sipAuthenticationUser")
862 if "sipRoutingObject" in self
.classes
:
863 return self
._get
_string
("sipLocalAddress")
866 def sip_password(self
):
867 return self
._get
_string
("sipPassword")
870 def _generate_sip_password():
871 return util
.random_string(8)
875 return "%s@ipfire.org" % self
.sip_id
878 def agent_status(self
):
879 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
881 def uses_sip_forwarding(self
):
882 if self
.sip_routing_address
:
889 def get_sip_routing_address(self
):
890 if "sipRoutingObject" in self
.classes
:
891 return self
._get
_string
("sipRoutingAddress")
893 def set_sip_routing_address(self
, address
):
897 # Don't do anything if nothing has changed
898 if self
.get_sip_routing_address() == address
:
902 # This is no longer a SIP user any more
905 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
906 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
907 (ldap
.MOD_DELETE
, "sipPassword", None),
909 except ldap
.NO_SUCH_ATTRIBUTE
:
912 # Set new routing object
915 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
916 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
917 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
920 # If this is a change, we cannot add this again
921 except ldap
.TYPE_OR_VALUE_EXISTS
:
922 self
._set
_string
("sipRoutingAddress", address
)
926 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
927 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
928 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
930 except ldap
.NO_SUCH_ATTRIBUTE
:
934 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
935 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
936 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
939 # XXX Cache is invalid here
941 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
944 def sip_registrations(self
):
945 sip_registrations
= []
947 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
950 sip_registrations
.append(reg
)
952 return sip_registrations
955 def sip_channels(self
):
956 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
958 def get_cdr(self
, date
=None, limit
=None):
959 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
964 def phone_number(self
):
966 Returns the IPFire phone number
969 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
972 def fax_number(self
):
974 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
976 def get_phone_numbers(self
):
979 for field
in ("telephoneNumber", "homePhone", "mobile"):
980 for number
in self
._get
_phone
_numbers
(field
):
985 def set_phone_numbers(self
, phone_numbers
):
986 # Sort phone numbers by landline and mobile
987 _landline_numbers
= []
990 for number
in phone_numbers
:
992 number
= phonenumbers
.parse(number
, None)
993 except phonenumbers
.phonenumberutil
.NumberParseException
:
996 # Convert to string (in E.164 format)
997 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
999 # Separate mobile numbers
1000 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1001 _mobile_numbers
.append(s
)
1003 _landline_numbers
.append(s
)
1006 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1007 self
._set
_strings
("mobile", _mobile_numbers
)
1009 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1012 def _all_telephone_numbers(self
):
1013 ret
= [ self
.sip_id
, ]
1015 if self
.phone_number
:
1016 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1019 for number
in self
.phone_numbers
:
1020 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1027 def get_description(self
):
1028 return self
._get
_string
("description")
1030 def set_description(self
, description
):
1031 self
._set
_string
("description", description
)
1033 description
= property(get_description
, set_description
)
1037 def has_avatar(self
):
1038 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
1039 if has_avatar
is None:
1040 has_avatar
= True if self
.get_avatar() else False
1042 # Cache avatar status for up to 24 hours
1043 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1047 def avatar_url(self
, size
=None):
1048 url
= "https://people.ipfire.org/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1051 url
+= "&size=%s" % size
1055 def get_avatar(self
, size
=None):
1056 photo
= self
._get
_bytes
("jpegPhoto")
1058 # Exit if no avatar is available
1062 # Return the raw image if no size was requested
1066 # Try to retrieve something from the cache
1067 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1071 # Generate a new thumbnail
1072 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1074 # Save to cache for 15m
1075 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1080 def avatar_hash(self
):
1081 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1083 h
= hashlib
.new("md5")
1084 h
.update(self
.get_avatar() or b
"")
1085 hash = h
.hexdigest()[:7]
1087 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1091 def upload_avatar(self
, avatar
):
1092 self
._set
("jpegPhoto", avatar
)
1094 # Delete cached avatar status
1095 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1097 # Delete avatar hash
1098 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1100 # Consent to promotional emails
1102 def get_consents_to_promotional_emails(self
):
1103 return self
.is_member_of_group("promotional-consent")
1105 def set_contents_to_promotional_emails(self
, value
):
1106 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1107 assert group
, "Could not find group: promotional-consent"
1110 group
.add_member(self
)
1112 group
.del_member(self
)
1114 consents_to_promotional_emails
= property(
1115 get_consents_to_promotional_emails
,
1116 set_contents_to_promotional_emails
,
1120 class StopForumSpam(Object
):
1121 def init(self
, uid
, email
, address
):
1122 self
.uid
, self
.email
, self
.address
= uid
, email
, address
1124 async def send_request(self
, **kwargs
):
1128 arguments
.update(kwargs
)
1131 request
= tornado
.httpclient
.HTTPRequest(
1132 "https://api.stopforumspam.org/api", method
="POST")
1133 request
.body
= urllib
.parse
.urlencode(arguments
)
1136 response
= await self
.backend
.http_client
.fetch(request
)
1138 # Decode the JSON response
1139 return json
.loads(response
.body
.decode())
1141 async def check_address(self
):
1142 response
= await self
.send_request(ip
=self
.address
)
1145 confidence
= response
["ip"]["confidence"]
1149 logging
.debug("Confidence for %s: %s" % (self
.address
, confidence
))
1153 async def check_username(self
):
1154 response
= await self
.send_request(username
=self
.uid
)
1157 confidence
= response
["username"]["confidence"]
1161 logging
.debug("Confidence for %s: %s" % (self
.uid
, confidence
))
1165 async def check_email(self
):
1166 response
= await self
.send_request(email
=self
.email
)
1169 confidence
= response
["email"]["confidence"]
1173 logging
.debug("Confidence for %s: %s" % (self
.email
, confidence
))
1177 async def check(self
, threshold
=95):
1179 This function tries to detect if we have a spammer.
1181 To honour the privacy of our users, we only send the IP
1182 address and username and if those are on the database, we
1183 will send the email address as well.
1185 confidences
= [await self
.check_address(), await self
.check_username()]
1187 if any((c
< threshold
for c
in confidences
)):
1188 confidences
.append(await self
.check_email())
1190 # Build a score based on the lowest confidence
1191 return 100 - min(confidences
)
1194 class Groups(Object
):
1196 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1197 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1199 # Everyone is a member of people
1200 "cn=people,ou=Group,dc=ipfire,dc=org",
1204 def search_base(self
):
1205 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1207 def _query(self
, *args
, **kwargs
):
1209 "search_base" : self
.backend
.groups
.search_base
,
1212 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1215 groups
= self
.get_all()
1219 def _get_groups(self
, query
, **kwargs
):
1220 res
= self
._query
(query
, **kwargs
)
1223 for dn
, attrs
in res
:
1224 # Skip any hidden groups
1225 if dn
in self
.hidden_groups
:
1228 g
= Group(self
.backend
, dn
, attrs
)
1231 return sorted(groups
)
1233 def _get_group(self
, query
, **kwargs
):
1238 groups
= self
._get
_groups
(query
, **kwargs
)
1243 return self
._get
_groups
(
1244 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1247 def get_by_gid(self
, gid
):
1248 return self
._get
_group
(
1249 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1253 class Group(LDAPObject
):
1255 if self
.description
:
1256 return "<%s %s (%s)>" % (
1257 self
.__class
__.__name
__,
1262 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1265 return self
.description
or self
.gid
1267 def __lt__(self
, other
):
1268 if isinstance(other
, self
.__class
__):
1269 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1276 Returns the number of members in this group
1280 for attr
in ("member", "memberUid"):
1281 a
= self
.attributes
.get(attr
, None)
1288 return iter(self
.members
)
1292 return self
._get
_string
("cn")
1295 def description(self
):
1296 return self
._get
_string
("description")
1300 return self
._get
_string
("mail")
1306 # Get all members by DN
1307 for dn
in self
._get
_strings
("member"):
1308 member
= self
.backend
.accounts
.get_by_dn(dn
)
1310 members
.append(member
)
1312 # Get all members by UID
1313 for uid
in self
._get
_strings
("memberUid"):
1314 member
= self
.backend
.accounts
.get_by_uid(uid
)
1316 members
.append(member
)
1318 return sorted(members
)
1320 def add_member(self
, account
):
1322 Adds a member to this group
1324 # Do nothing if this user is already in the group
1325 if account
.is_member_of_group(self
.gid
):
1328 if "posixGroup" in self
.objectclasses
:
1329 self
._add
_string
("memberUid", account
.uid
)
1331 self
._add
_string
("member", account
.dn
)
1333 # Append to cached list of members
1334 self
.members
.append(account
)
1337 def del_member(self
, account
):
1339 Removes a member from a group
1341 # Do nothing if this user is not in the group
1342 if not account
.is_member_of_group(self
.gid
):
1345 if "posixGroup" in self
.objectclasses
:
1346 self
._delete
_string
("memberUid", account
.uid
)
1348 self
._delete
_string
("member", account
.dn
)
1351 if __name__
== "__main__":