17 import tornado
.httpclient
22 from . import countries
24 from .decorators
import *
25 from .misc
import Object
29 # Set the client keytab name
30 os
.environ
["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
32 class LDAPObject(Object
):
33 def init(self
, dn
, attrs
=None):
36 self
.attributes
= attrs
or {}
38 def __eq__(self
, other
):
39 if isinstance(other
, self
.__class
__):
40 return self
.dn
== other
.dn
44 return self
.accounts
.ldap
46 def _exists(self
, key
):
55 for value
in self
.attributes
.get(key
, []):
58 def _get_bytes(self
, key
, default
=None):
59 for value
in self
._get
(key
):
64 def _get_strings(self
, key
):
65 for value
in self
._get
(key
):
68 def _get_string(self
, key
, default
=None):
69 for value
in self
._get
_strings
(key
):
74 def _get_phone_numbers(self
, key
):
75 for value
in self
._get
_strings
(key
):
76 yield phonenumbers
.parse(value
, None)
78 def _get_timestamp(self
, key
):
79 value
= self
._get
_string
(key
)
81 # Parse the timestamp value and returns a datetime object
83 return datetime
.datetime
.strptime(value
, "%Y%m%d%H%M%SZ")
85 def _modify(self
, modlist
):
86 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
88 # Authenticate before performing any write operations
89 self
.accounts
._authenticate
()
91 # Run modify operation
92 self
.ldap
.modify_s(self
.dn
, modlist
)
97 def _clear_cache(self
):
103 def _set(self
, key
, values
):
104 current
= self
._get
(key
)
106 # Don't do anything if nothing has changed
107 if list(current
) == values
:
110 # Remove all old values and add all new ones
113 if self
._exists
(key
):
114 modlist
.append((ldap
.MOD_DELETE
, key
, None))
118 modlist
.append((ldap
.MOD_ADD
, key
, values
))
120 # Run modify operation
121 self
._modify
(modlist
)
124 self
.attributes
.update({ key
: values
})
126 def _set_bytes(self
, key
, values
):
127 return self
._set
(key
, values
)
129 def _set_strings(self
, key
, values
):
130 return self
._set
(key
, [e
.encode() for e
in values
if e
])
132 def _set_string(self
, key
, value
):
133 return self
._set
_strings
(key
, [value
,])
135 def _add(self
, key
, values
):
137 (ldap
.MOD_ADD
, key
, values
),
140 self
._modify
(modlist
)
142 def _add_strings(self
, key
, values
):
143 return self
._add
(key
, [e
.encode() for e
in values
])
145 def _add_string(self
, key
, value
):
146 return self
._add
_strings
(key
, [value
,])
148 def _delete(self
, key
, values
):
150 (ldap
.MOD_DELETE
, key
, values
),
153 self
._modify
(modlist
)
155 def _delete_strings(self
, key
, values
):
156 return self
._delete
(key
, [e
.encode() for e
in values
])
158 def _delete_string(self
, key
, value
):
159 return self
._delete
_strings
(key
, [value
,])
162 def objectclasses(self
):
163 return self
._get
_strings
("objectClass")
167 return datetime
.datetime
.strptime(s
.decode(), "%Y%m%d%H%M%SZ")
170 class Accounts(Object
):
172 self
.search_base
= self
.settings
.get("ldap_search_base")
175 count
= self
.memcache
.get("accounts:count")
178 count
= self
._count
("(objectClass=person)")
180 self
.memcache
.set("accounts:count", count
, 300)
185 accounts
= self
._search
("(objectClass=person)")
187 return iter(sorted(accounts
))
191 # Connect to LDAP server
192 ldap_uri
= self
.settings
.get("ldap_uri")
194 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
196 # Connect to the LDAP server
197 return ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
198 retry_max
=10, retry_delay
=3)
200 def _authenticate(self
):
201 # Authenticate against LDAP server using Kerberos
202 self
.ldap
.sasl_gssapi_bind_s()
205 logging
.info("Testing LDAP connection...")
209 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
211 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
212 logging
.debug("Performing LDAP query (%s): %s" \
213 % (search_base
or self
.search_base
, query
))
217 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
218 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
)
220 # Log time it took to perform the query
221 logging
.debug("Query took %.2fms" % ((time
.time() - t
) * 1000.0))
225 def _count(self
, query
):
226 res
= self
._query
(query
, attrlist
=["dn"], limit
=INT_MAX
)
230 def _search(self
, query
, attrlist
=None, limit
=0):
232 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
233 account
= self
.get_by_dn(dn
)
234 accounts
.append(account
)
238 def _get_attrs(self
, dn
):
240 Fetches all attributes for the given distinguished name
242 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
243 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
245 for dn
, attrs
in results
:
248 def get_by_dn(self
, dn
):
249 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
251 attrs
= self
._get
_attrs
(dn
)
254 # Cache all attributes for 5 min
255 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
257 return Account(self
.backend
, dn
, attrs
)
261 return t
.strftime("%Y%m%d%H%M%SZ")
263 def get_created_after(self
, ts
):
264 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
266 def count_created_after(self
, ts
):
267 return self
._count
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
269 def search(self
, query
):
270 accounts
= self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)(displayName=*%s*)(mail=*%s*)))" \
271 % (query
, query
, query
, query
))
273 return sorted(accounts
)
275 def _search_one(self
, query
):
276 results
= self
._search
(query
, limit
=1)
278 for result
in results
:
281 def uid_is_valid(self
, uid
):
282 # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
283 m
= re
.match(r
"^[a-z_][a-z0-9_-]{3,31}$", uid
)
289 def uid_exists(self
, uid
):
290 if self
.get_by_uid(uid
):
293 res
= self
.db
.get("SELECT 1 FROM account_activations \
294 WHERE uid = %s AND expires_at > NOW()", uid
)
299 # Account with uid does not exist, yet
302 def get_by_uid(self
, uid
):
303 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
305 def get_by_mail(self
, mail
):
306 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
308 def find_account(self
, s
):
309 account
= self
.get_by_uid(s
)
313 return self
.get_by_mail(s
)
315 def get_by_sip_id(self
, sip_id
):
319 return self
._search
_one
(
320 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
323 def get_by_phone_number(self
, number
):
327 return self
._search
_one
(
328 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
329 % (number
, number
, number
, number
))
331 async def check_spam(self
, uid
, email
, address
):
332 sfs
= StopForumSpam(self
.backend
, uid
, email
, address
)
335 score
= await sfs
.check()
339 def auth(self
, username
, password
):
341 account
= self
.backend
.accounts
.find_account(username
)
344 if account
and account
.check_password(password
):
349 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
350 # Convert all uids to lowercase
353 # Check if UID is valid
354 if not self
.uid_is_valid(uid
):
355 raise ValueError("UID is invalid: %s" % uid
)
357 # Check if UID is unique
358 if self
.uid_exists(uid
):
359 raise ValueError("UID exists: %s" % uid
)
361 # Generate a random activation code
362 activation_code
= util
.random_string(36)
364 # Create an entry in our database until the user
365 # has activated the account
366 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
367 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
368 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
370 # Send an account activation email
371 self
.backend
.messages
.send_template("auth/messages/register",
372 recipients
=[email
], priority
=100, uid
=uid
,
373 activation_code
=activation_code
, email
=email
,
374 first_name
=first_name
, last_name
=last_name
)
376 def activate(self
, uid
, activation_code
):
377 res
= self
.db
.get("DELETE FROM account_activations \
378 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
379 RETURNING *", uid
, activation_code
)
381 # Return nothing when account was not found
385 # Return the account if it has already been created
386 account
= self
.get_by_uid(uid
)
390 # Create a new account on the LDAP database
391 account
= self
.create(uid
, res
.email
,
392 first_name
=res
.first_name
, last_name
=res
.last_name
,
393 country_code
=res
.country_code
)
395 # Non-EU users do not need to consent to promo emails
396 if account
.country_code
and not account
.country_code
in countries
.EU_COUNTRIES
:
397 account
.consents_to_promotional_emails
= True
399 # Invite newly registered users to newsletter
400 self
.backend
.messages
.send_template(
401 "newsletter/subscribe", address
="%s <%s>" % (account
, account
.email
))
403 # Send email about account registration
404 self
.backend
.messages
.send_template("people/messages/new-account",
405 recipients
=["moderators@ipfire.org"], account
=account
)
407 # Launch drip campaigns
408 for campaign
in ("signup", "christmas"):
409 self
.backend
.campaigns
.launch(campaign
, account
)
413 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
414 cn
= "%s %s" % (first_name
, last_name
)
418 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
419 "mail" : email
.encode(),
423 "sn" : last_name
.encode(),
424 "givenName" : first_name
.encode(),
427 logging
.info("Creating new account: %s: %s" % (uid
, account
))
430 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
432 # Create account on LDAP
433 self
.accounts
._authenticate
()
434 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
437 account
= self
.get_by_dn(dn
)
439 # Optionally set country code
441 account
.country_code
= country_code
448 def create_session(self
, account
, host
):
449 session_id
= util
.random_string(64)
451 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
452 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
454 # Session could not be created
458 logging
.info("Created session %s for %s which expires %s" \
459 % (res
.session_id
, account
, res
.time_expires
))
460 return res
.session_id
, res
.time_expires
462 def destroy_session(self
, session_id
, host
):
463 logging
.info("Destroying session %s" % session_id
)
465 self
.db
.execute("DELETE FROM sessions \
466 WHERE session_id = %s AND host = %s", session_id
, host
)
468 def get_by_session(self
, session_id
, host
):
469 logging
.debug("Looking up session %s" % session_id
)
471 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
472 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
475 # Session does not exist or has expired
479 # Update the session expiration time
480 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
481 WHERE session_id = %s AND host = %s", session_id
, host
)
483 return self
.get_by_uid(res
.uid
)
486 # Cleanup expired sessions
487 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
489 # Cleanup expired account activations
490 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
492 # Cleanup expired account password resets
493 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
497 def decode_discourse_payload(self
, payload
, signature
):
499 calculated_signature
= self
.sign_discourse_payload(payload
)
501 if not hmac
.compare_digest(signature
, calculated_signature
):
502 raise ValueError("Invalid signature: %s" % signature
)
504 # Decode the query string
505 qs
= base64
.b64decode(payload
).decode()
507 # Parse the query string
509 for key
, val
in urllib
.parse
.parse_qsl(qs
):
514 def encode_discourse_payload(self
, **args
):
515 # Encode the arguments into an URL-formatted string
516 qs
= urllib
.parse
.urlencode(args
).encode()
519 return base64
.b64encode(qs
).decode()
521 def sign_discourse_payload(self
, payload
, secret
=None):
523 secret
= self
.settings
.get("discourse_sso_secret")
525 # Calculate a HMAC using SHA256
526 h
= hmac
.new(secret
.encode(),
527 msg
=payload
.encode(), digestmod
="sha256")
535 for country
in iso3166
.countries
:
536 count
= self
._count
("(&(objectClass=person)(st=%s))" % country
.alpha2
)
544 class Account(LDAPObject
):
552 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
554 def __lt__(self
, other
):
555 if isinstance(other
, self
.__class
__):
556 return self
.name
< other
.name
558 def _clear_cache(self
):
559 # Delete cached attributes
560 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
563 def kerberos_attributes(self
):
564 res
= self
.backend
.accounts
._query
(
565 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
567 "krbLastSuccessfulAuth",
568 "krbLastPasswordChange",
570 "krbLoginFailedCount",
573 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
575 for dn
, attrs
in res
:
576 return { key
: attrs
[key
][0] for key
in attrs
}
581 def last_successful_authentication(self
):
583 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
587 return self
._parse
_date
(s
)
590 def last_failed_authentication(self
):
592 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
596 return self
._parse
_date
(s
)
599 def failed_login_count(self
):
601 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
610 def passwd(self
, password
):
614 # The new password must have a score of 3 or better
615 quality
= self
.check_password_quality(password
)
616 if quality
["score"] < 3:
617 raise ValueError("Password too weak")
619 self
.accounts
._authenticate
()
620 self
.ldap
.passwd_s(self
.dn
, None, password
)
622 def check_password(self
, password
):
624 Bind to the server with given credentials and return
625 true if password is corrent and false if not.
627 Raises exceptions from the server on any other errors.
632 logging
.debug("Checking credentials for %s" % self
.dn
)
634 # Create a new LDAP connection
635 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
636 conn
= ldap
.initialize(ldap_uri
)
639 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
640 except ldap
.INVALID_CREDENTIALS
:
641 logging
.debug("Account credentials are invalid for %s" % self
)
644 logging
.info("Successfully authenticated %s" % self
)
648 def check_password_quality(self
, password
):
650 Passwords are passed through zxcvbn to make sure
651 that they are strong enough.
653 return zxcvbn
.zxcvbn(password
, user_inputs
=(
654 self
.first_name
, self
.last_name
,
657 def request_password_reset(self
, address
=None):
658 reset_code
= util
.random_string(64)
660 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
661 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
663 # Send a password reset email
664 self
.backend
.messages
.send_template("auth/messages/password-reset",
665 recipients
=[self
.email
], priority
=100, account
=self
, reset_code
=reset_code
)
667 def reset_password(self
, reset_code
, new_password
):
668 # Delete the reset token
669 res
= self
.db
.query("DELETE FROM account_password_resets \
670 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
671 RETURNING *", self
.uid
, reset_code
)
673 # The reset code was invalid
675 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
677 # Perform password change
678 return self
.passwd(new_password
)
681 return self
.is_member_of_group("sudo")
684 return self
.is_member_of_group("staff")
686 def is_moderator(self
):
687 return self
.is_member_of_group("moderators")
690 return "posixAccount" in self
.classes
693 return "postfixMailUser" in self
.classes
696 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
698 def can_be_managed_by(self
, account
):
700 Returns True if account is allowed to manage this account
702 # Admins can manage all accounts
703 if account
.is_admin():
706 # Users can manage themselves
707 return self
== account
711 return self
._get
_strings
("objectClass")
715 return self
._get
_string
("uid")
719 return self
._get
_string
("cn")
723 def get_nickname(self
):
724 return self
._get
_string
("displayName")
726 def set_nickname(self
, nickname
):
727 self
._set
_string
("displayName", nickname
)
729 nickname
= property(get_nickname
, set_nickname
)
733 def get_first_name(self
):
734 return self
._get
_string
("givenName")
736 def set_first_name(self
, first_name
):
737 self
._set
_string
("givenName", first_name
)
740 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
742 first_name
= property(get_first_name
, set_first_name
)
746 def get_last_name(self
):
747 return self
._get
_string
("sn")
749 def set_last_name(self
, last_name
):
750 self
._set
_string
("sn", last_name
)
753 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
755 last_name
= property(get_last_name
, set_last_name
)
759 return self
.backend
.groups
._get
_groups
("(| \
760 (&(objectClass=groupOfNames)(member=%s)) \
761 (&(objectClass=posixGroup)(memberUid=%s)) \
762 )" % (self
.dn
, self
.uid
))
764 def is_member_of_group(self
, gid
):
766 Returns True if this account is a member of this group
768 return gid
in (g
.gid
for g
in self
.groups
)
770 # Created/Modified at
773 def created_at(self
):
774 return self
._get
_timestamp
("createTimestamp")
777 def modified_at(self
):
778 return self
._get
_timestamp
("modifyTimestamp")
787 address
+= self
.street
.splitlines()
789 if self
.postal_code
and self
.city
:
790 if self
.country_code
in ("AT", "DE"):
791 address
.append("%s %s" % (self
.postal_code
, self
.city
))
793 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
795 address
.append(self
.city
or self
.postal_code
)
797 if self
.country_name
:
798 address
.append(self
.country_name
)
802 def get_street(self
):
803 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
805 def set_street(self
, street
):
806 self
._set
_string
("street", street
)
808 street
= property(get_street
, set_street
)
811 return self
._get
_string
("l") or ""
813 def set_city(self
, city
):
814 self
._set
_string
("l", city
)
816 city
= property(get_city
, set_city
)
818 def get_postal_code(self
):
819 return self
._get
_string
("postalCode") or ""
821 def set_postal_code(self
, postal_code
):
822 self
._set
_string
("postalCode", postal_code
)
824 postal_code
= property(get_postal_code
, set_postal_code
)
826 # XXX This should be c
827 def get_country_code(self
):
828 return self
._get
_string
("st")
830 def set_country_code(self
, country_code
):
831 self
._set
_string
("st", country_code
)
833 country_code
= property(get_country_code
, set_country_code
)
836 def country_name(self
):
837 if self
.country_code
:
838 return countries
.get_name(self
.country_code
)
842 return self
._get
_string
("mail")
846 return "%s <%s>" % (self
, self
.email
)
848 # Mail Routing Address
850 def get_mail_routing_address(self
):
851 return self
._get
_string
("mailRoutingAddress", None)
853 def set_mail_routing_address(self
, address
):
854 self
._set
_string
("mailRoutingAddress", address
or None)
856 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
860 if "sipUser" in self
.classes
:
861 return self
._get
_string
("sipAuthenticationUser")
863 if "sipRoutingObject" in self
.classes
:
864 return self
._get
_string
("sipLocalAddress")
867 def sip_password(self
):
868 return self
._get
_string
("sipPassword")
871 def _generate_sip_password():
872 return util
.random_string(8)
876 return "%s@ipfire.org" % self
.sip_id
879 def agent_status(self
):
880 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
882 def uses_sip_forwarding(self
):
883 if self
.sip_routing_address
:
890 def get_sip_routing_address(self
):
891 if "sipRoutingObject" in self
.classes
:
892 return self
._get
_string
("sipRoutingAddress")
894 def set_sip_routing_address(self
, address
):
898 # Don't do anything if nothing has changed
899 if self
.get_sip_routing_address() == address
:
903 # This is no longer a SIP user any more
906 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
907 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
908 (ldap
.MOD_DELETE
, "sipPassword", None),
910 except ldap
.NO_SUCH_ATTRIBUTE
:
913 # Set new routing object
916 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
917 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
918 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
921 # If this is a change, we cannot add this again
922 except ldap
.TYPE_OR_VALUE_EXISTS
:
923 self
._set
_string
("sipRoutingAddress", address
)
927 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
928 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
929 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
931 except ldap
.NO_SUCH_ATTRIBUTE
:
935 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
936 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
937 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
940 # XXX Cache is invalid here
942 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
945 def sip_registrations(self
):
946 sip_registrations
= []
948 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
951 sip_registrations
.append(reg
)
953 return sip_registrations
956 def sip_channels(self
):
957 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
959 def get_cdr(self
, date
=None, limit
=None):
960 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
965 def phone_number(self
):
967 Returns the IPFire phone number
970 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
973 def fax_number(self
):
975 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
977 def get_phone_numbers(self
):
980 for field
in ("telephoneNumber", "homePhone", "mobile"):
981 for number
in self
._get
_phone
_numbers
(field
):
986 def set_phone_numbers(self
, phone_numbers
):
987 # Sort phone numbers by landline and mobile
988 _landline_numbers
= []
991 for number
in phone_numbers
:
993 number
= phonenumbers
.parse(number
, None)
994 except phonenumbers
.phonenumberutil
.NumberParseException
:
997 # Convert to string (in E.164 format)
998 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1000 # Separate mobile numbers
1001 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1002 _mobile_numbers
.append(s
)
1004 _landline_numbers
.append(s
)
1007 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1008 self
._set
_strings
("mobile", _mobile_numbers
)
1010 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1013 def _all_telephone_numbers(self
):
1014 ret
= [ self
.sip_id
, ]
1016 if self
.phone_number
:
1017 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1020 for number
in self
.phone_numbers
:
1021 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1028 def get_description(self
):
1029 return self
._get
_string
("description")
1031 def set_description(self
, description
):
1032 self
._set
_string
("description", description
)
1034 description
= property(get_description
, set_description
)
1038 def has_avatar(self
):
1039 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
1040 if has_avatar
is None:
1041 has_avatar
= True if self
.get_avatar() else False
1043 # Cache avatar status for up to 24 hours
1044 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1048 def avatar_url(self
, size
=None):
1049 url
= "https://people.ipfire.org/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1052 url
+= "&size=%s" % size
1056 def get_avatar(self
, size
=None):
1057 photo
= self
._get
_bytes
("jpegPhoto")
1059 # Exit if no avatar is available
1063 # Return the raw image if no size was requested
1067 # Try to retrieve something from the cache
1068 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1072 # Generate a new thumbnail
1073 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1075 # Save to cache for 15m
1076 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1081 def avatar_hash(self
):
1082 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1084 h
= hashlib
.new("md5")
1085 h
.update(self
.get_avatar() or b
"")
1086 hash = h
.hexdigest()[:7]
1088 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1092 def upload_avatar(self
, avatar
):
1093 self
._set
("jpegPhoto", avatar
)
1095 # Delete cached avatar status
1096 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1098 # Delete avatar hash
1099 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1101 # Consent to promotional emails
1103 def get_consents_to_promotional_emails(self
):
1104 return self
.is_member_of_group("promotional-consent")
1106 def set_contents_to_promotional_emails(self
, value
):
1107 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1108 assert group
, "Could not find group: promotional-consent"
1111 group
.add_member(self
)
1113 group
.del_member(self
)
1115 consents_to_promotional_emails
= property(
1116 get_consents_to_promotional_emails
,
1117 set_contents_to_promotional_emails
,
1121 class StopForumSpam(Object
):
1122 def init(self
, uid
, email
, address
):
1123 self
.uid
, self
.email
, self
.address
= uid
, email
, address
1125 async def send_request(self
, **kwargs
):
1129 arguments
.update(kwargs
)
1132 request
= tornado
.httpclient
.HTTPRequest(
1133 "https://api.stopforumspam.org/api", method
="POST",
1134 connect_timeout
=2, request_timeout
=5)
1135 request
.body
= urllib
.parse
.urlencode(arguments
)
1138 response
= await self
.backend
.http_client
.fetch(request
)
1140 # Decode the JSON response
1141 return json
.loads(response
.body
.decode())
1143 async def check_address(self
):
1144 response
= await self
.send_request(ip
=self
.address
)
1147 confidence
= response
["ip"]["confidence"]
1151 logging
.debug("Confidence for %s: %s" % (self
.address
, confidence
))
1155 async def check_username(self
):
1156 response
= await self
.send_request(username
=self
.uid
)
1159 confidence
= response
["username"]["confidence"]
1163 logging
.debug("Confidence for %s: %s" % (self
.uid
, confidence
))
1167 async def check_email(self
):
1168 response
= await self
.send_request(email
=self
.email
)
1171 confidence
= response
["email"]["confidence"]
1175 logging
.debug("Confidence for %s: %s" % (self
.email
, confidence
))
1179 async def check(self
, threshold
=95):
1181 This function tries to detect if we have a spammer.
1183 To honour the privacy of our users, we only send the IP
1184 address and username and if those are on the database, we
1185 will send the email address as well.
1187 confidences
= [await self
.check_address(), await self
.check_username()]
1189 if any((c
< threshold
for c
in confidences
)):
1190 confidences
.append(await self
.check_email())
1192 # Build a score based on the lowest confidence
1193 return 100 - min(confidences
)
1196 class Groups(Object
):
1198 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1199 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1201 # Everyone is a member of people
1202 "cn=people,ou=Group,dc=ipfire,dc=org",
1206 def search_base(self
):
1207 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1209 def _query(self
, *args
, **kwargs
):
1211 "search_base" : self
.backend
.groups
.search_base
,
1214 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1217 groups
= self
.get_all()
1221 def _get_groups(self
, query
, **kwargs
):
1222 res
= self
._query
(query
, **kwargs
)
1225 for dn
, attrs
in res
:
1226 # Skip any hidden groups
1227 if dn
in self
.hidden_groups
:
1230 g
= Group(self
.backend
, dn
, attrs
)
1233 return sorted(groups
)
1235 def _get_group(self
, query
, **kwargs
):
1240 groups
= self
._get
_groups
(query
, **kwargs
)
1245 return self
._get
_groups
(
1246 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1249 def get_by_gid(self
, gid
):
1250 return self
._get
_group
(
1251 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1255 class Group(LDAPObject
):
1257 if self
.description
:
1258 return "<%s %s (%s)>" % (
1259 self
.__class
__.__name
__,
1264 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1267 return self
.description
or self
.gid
1269 def __lt__(self
, other
):
1270 if isinstance(other
, self
.__class
__):
1271 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1278 Returns the number of members in this group
1282 for attr
in ("member", "memberUid"):
1283 a
= self
.attributes
.get(attr
, None)
1290 return iter(self
.members
)
1294 return self
._get
_string
("cn")
1297 def description(self
):
1298 return self
._get
_string
("description")
1302 return self
._get
_string
("mail")
1308 # Get all members by DN
1309 for dn
in self
._get
_strings
("member"):
1310 member
= self
.backend
.accounts
.get_by_dn(dn
)
1312 members
.append(member
)
1314 # Get all members by UID
1315 for uid
in self
._get
_strings
("memberUid"):
1316 member
= self
.backend
.accounts
.get_by_uid(uid
)
1318 members
.append(member
)
1320 return sorted(members
)
1322 def add_member(self
, account
):
1324 Adds a member to this group
1326 # Do nothing if this user is already in the group
1327 if account
.is_member_of_group(self
.gid
):
1330 if "posixGroup" in self
.objectclasses
:
1331 self
._add
_string
("memberUid", account
.uid
)
1333 self
._add
_string
("member", account
.dn
)
1335 # Append to cached list of members
1336 self
.members
.append(account
)
1339 def del_member(self
, account
):
1341 Removes a member from a group
1343 # Do nothing if this user is not in the group
1344 if not account
.is_member_of_group(self
.gid
):
1347 if "posixGroup" in self
.objectclasses
:
1348 self
._delete
_string
("memberUid", account
.uid
)
1350 self
._delete
_string
("member", account
.dn
)
1353 if __name__
== "__main__":