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 accounts
= self
._search
("(objectClass=person)")
185 return iter(sorted(accounts
))
189 # Connect to LDAP server
190 ldap_uri
= self
.settings
.get("ldap_uri")
192 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
194 # Connect to the LDAP server
195 return ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
196 retry_max
=10, retry_delay
=3)
198 def _authenticate(self
):
199 # Authenticate against LDAP server using Kerberos
200 self
.ldap
.sasl_gssapi_bind_s()
203 logging
.info("Testing LDAP connection...")
207 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
209 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
210 logging
.debug("Performing LDAP query (%s): %s" \
211 % (search_base
or self
.search_base
, query
))
215 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
216 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
)
218 # Log time it took to perform the query
219 logging
.debug("Query took %.2fms" % ((time
.time() - t
) * 1000.0))
223 def _count(self
, query
):
224 res
= self
._query
(query
, attrlist
=["dn"])
228 def _search(self
, query
, attrlist
=None, limit
=0):
230 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
231 account
= self
.get_by_dn(dn
)
232 accounts
.append(account
)
236 def _get_attrs(self
, dn
):
238 Fetches all attributes for the given distinguished name
240 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
241 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
243 for dn
, attrs
in results
:
246 def get_by_dn(self
, dn
):
247 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
249 attrs
= self
._get
_attrs
(dn
)
252 # Cache all attributes for 5 min
253 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
255 return Account(self
.backend
, dn
, attrs
)
259 return t
.strftime("%Y%m%d%H%M%SZ")
261 def get_created_after(self
, ts
):
262 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
264 def count_created_after(self
, ts
):
265 return self
._count
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
267 def search(self
, query
):
268 accounts
= self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)(displayName=*%s*)(mail=*%s*)))" \
269 % (query
, query
, query
, query
))
271 return sorted(accounts
)
273 def _search_one(self
, query
):
274 results
= self
._search
(query
, limit
=1)
276 for result
in results
:
279 def uid_is_valid(self
, uid
):
280 # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
281 m
= re
.match(r
"^[a-z_][a-z0-9_-]{3,31}$", uid
)
287 def uid_exists(self
, uid
):
288 if self
.get_by_uid(uid
):
291 res
= self
.db
.get("SELECT 1 FROM account_activations \
292 WHERE uid = %s AND expires_at > NOW()", uid
)
297 # Account with uid does not exist, yet
300 def get_by_uid(self
, uid
):
301 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
303 def get_by_mail(self
, mail
):
304 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
306 def find_account(self
, s
):
307 account
= self
.get_by_uid(s
)
311 return self
.get_by_mail(s
)
313 def get_by_sip_id(self
, sip_id
):
317 return self
._search
_one
(
318 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
321 def get_by_phone_number(self
, number
):
325 return self
._search
_one
(
326 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
327 % (number
, number
, number
, number
))
329 async def check_spam(self
, uid
, email
, address
):
330 sfs
= StopForumSpam(self
.backend
, uid
, email
, address
)
333 score
= await sfs
.check()
337 def auth(self
, username
, password
):
339 account
= self
.backend
.accounts
.find_account(username
)
342 if account
and account
.check_password(password
):
347 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
348 # Convert all uids to lowercase
351 # Check if UID is valid
352 if not self
.uid_is_valid(uid
):
353 raise ValueError("UID is invalid: %s" % uid
)
355 # Check if UID is unique
356 if self
.uid_exists(uid
):
357 raise ValueError("UID exists: %s" % uid
)
359 # Generate a random activation code
360 activation_code
= util
.random_string(36)
362 # Create an entry in our database until the user
363 # has activated the account
364 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
365 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
366 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
368 # Send an account activation email
369 self
.backend
.messages
.send_template("auth/messages/register",
370 recipients
=[email
], priority
=100, uid
=uid
,
371 activation_code
=activation_code
, email
=email
,
372 first_name
=first_name
, last_name
=last_name
)
374 def activate(self
, uid
, activation_code
):
375 res
= self
.db
.get("DELETE FROM account_activations \
376 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
377 RETURNING *", uid
, activation_code
)
379 # Return nothing when account was not found
383 # Return the account if it has already been created
384 account
= self
.get_by_uid(uid
)
388 # Create a new account on the LDAP database
389 account
= self
.create(uid
, res
.email
,
390 first_name
=res
.first_name
, last_name
=res
.last_name
,
391 country_code
=res
.country_code
)
393 # Non-EU users do not need to consent to promo emails
394 if account
.country_code
and not account
.country_code
in countries
.EU_COUNTRIES
:
395 account
.consents_to_promotional_emails
= True
397 # Invite newly registered users to newsletter
398 self
.backend
.messages
.send_template(
399 "newsletter/subscribe", address
="%s <%s>" % (account
, account
.email
))
401 # Send email about account registration
402 self
.backend
.messages
.send_template("people/messages/new-account",
403 recipients
=["moderators@ipfire.org"], account
=account
)
405 # Launch drip campaigns
406 for campaign
in ("signup", "christmas"):
407 self
.backend
.campaigns
.launch(campaign
, account
)
411 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
412 cn
= "%s %s" % (first_name
, last_name
)
416 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
417 "mail" : email
.encode(),
421 "sn" : last_name
.encode(),
422 "givenName" : first_name
.encode(),
425 logging
.info("Creating new account: %s: %s" % (uid
, account
))
428 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
430 # Create account on LDAP
431 self
.accounts
._authenticate
()
432 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
435 account
= self
.get_by_dn(dn
)
437 # Optionally set country code
439 account
.country_code
= country_code
446 def create_session(self
, account
, host
):
447 session_id
= util
.random_string(64)
449 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
450 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
452 # Session could not be created
456 logging
.info("Created session %s for %s which expires %s" \
457 % (res
.session_id
, account
, res
.time_expires
))
458 return res
.session_id
, res
.time_expires
460 def destroy_session(self
, session_id
, host
):
461 logging
.info("Destroying session %s" % session_id
)
463 self
.db
.execute("DELETE FROM sessions \
464 WHERE session_id = %s AND host = %s", session_id
, host
)
466 def get_by_session(self
, session_id
, host
):
467 logging
.debug("Looking up session %s" % session_id
)
469 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
470 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
473 # Session does not exist or has expired
477 # Update the session expiration time
478 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
479 WHERE session_id = %s AND host = %s", session_id
, host
)
481 return self
.get_by_uid(res
.uid
)
484 # Cleanup expired sessions
485 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
487 # Cleanup expired account activations
488 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
490 # Cleanup expired account password resets
491 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
495 def decode_discourse_payload(self
, payload
, signature
):
497 calculated_signature
= self
.sign_discourse_payload(payload
)
499 if not hmac
.compare_digest(signature
, calculated_signature
):
500 raise ValueError("Invalid signature: %s" % signature
)
502 # Decode the query string
503 qs
= base64
.b64decode(payload
).decode()
505 # Parse the query string
507 for key
, val
in urllib
.parse
.parse_qsl(qs
):
512 def encode_discourse_payload(self
, **args
):
513 # Encode the arguments into an URL-formatted string
514 qs
= urllib
.parse
.urlencode(args
).encode()
517 return base64
.b64encode(qs
).decode()
519 def sign_discourse_payload(self
, payload
, secret
=None):
521 secret
= self
.settings
.get("discourse_sso_secret")
523 # Calculate a HMAC using SHA256
524 h
= hmac
.new(secret
.encode(),
525 msg
=payload
.encode(), digestmod
="sha256")
533 for country
in iso3166
.countries
:
534 count
= self
._count
("(&(objectClass=person)(st=%s))" % country
.alpha2
)
542 class Account(LDAPObject
):
550 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
552 def __lt__(self
, other
):
553 if isinstance(other
, self
.__class
__):
554 return self
.name
< other
.name
556 def _clear_cache(self
):
557 # Delete cached attributes
558 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
561 def kerberos_attributes(self
):
562 res
= self
.backend
.accounts
._query
(
563 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
565 "krbLastSuccessfulAuth",
566 "krbLastPasswordChange",
568 "krbLoginFailedCount",
571 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
573 for dn
, attrs
in res
:
574 return { key
: attrs
[key
][0] for key
in attrs
}
579 def last_successful_authentication(self
):
581 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
585 return self
._parse
_date
(s
)
588 def last_failed_authentication(self
):
590 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
594 return self
._parse
_date
(s
)
597 def failed_login_count(self
):
599 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
608 def passwd(self
, password
):
612 # The new password must have a score of 3 or better
613 quality
= self
.check_password_quality(password
)
614 if quality
["score"] < 3:
615 raise ValueError("Password too weak")
617 self
.accounts
._authenticate
()
618 self
.ldap
.passwd_s(self
.dn
, None, password
)
620 def check_password(self
, password
):
622 Bind to the server with given credentials and return
623 true if password is corrent and false if not.
625 Raises exceptions from the server on any other errors.
630 logging
.debug("Checking credentials for %s" % self
.dn
)
632 # Create a new LDAP connection
633 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
634 conn
= ldap
.initialize(ldap_uri
)
637 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
638 except ldap
.INVALID_CREDENTIALS
:
639 logging
.debug("Account credentials are invalid for %s" % self
)
642 logging
.info("Successfully authenticated %s" % self
)
646 def check_password_quality(self
, password
):
648 Passwords are passed through zxcvbn to make sure
649 that they are strong enough.
651 return zxcvbn
.zxcvbn(password
, user_inputs
=(
652 self
.first_name
, self
.last_name
,
655 def request_password_reset(self
, address
=None):
656 reset_code
= util
.random_string(64)
658 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
659 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
661 # Send a password reset email
662 self
.backend
.messages
.send_template("auth/messages/password-reset",
663 recipients
=[self
.email
], priority
=100, account
=self
, reset_code
=reset_code
)
665 def reset_password(self
, reset_code
, new_password
):
666 # Delete the reset token
667 res
= self
.db
.query("DELETE FROM account_password_resets \
668 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
669 RETURNING *", self
.uid
, reset_code
)
671 # The reset code was invalid
673 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
675 # Perform password change
676 return self
.passwd(new_password
)
679 return self
.is_member_of_group("sudo")
682 return self
.is_member_of_group("staff")
684 def is_moderator(self
):
685 return self
.is_member_of_group("moderators")
688 return "posixAccount" in self
.classes
691 return "postfixMailUser" in self
.classes
694 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
696 def can_be_managed_by(self
, account
):
698 Returns True if account is allowed to manage this account
700 # Admins can manage all accounts
701 if account
.is_admin():
704 # Users can manage themselves
705 return self
== account
709 return self
._get
_strings
("objectClass")
713 return self
._get
_string
("uid")
717 return self
._get
_string
("cn")
721 def get_nickname(self
):
722 return self
._get
_string
("displayName")
724 def set_nickname(self
, nickname
):
725 self
._set
_string
("displayName", nickname
)
727 nickname
= property(get_nickname
, set_nickname
)
731 def get_first_name(self
):
732 return self
._get
_string
("givenName")
734 def set_first_name(self
, first_name
):
735 self
._set
_string
("givenName", first_name
)
738 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
740 first_name
= property(get_first_name
, set_first_name
)
744 def get_last_name(self
):
745 return self
._get
_string
("sn")
747 def set_last_name(self
, last_name
):
748 self
._set
_string
("sn", last_name
)
751 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
753 last_name
= property(get_last_name
, set_last_name
)
757 return self
.backend
.groups
._get
_groups
("(| \
758 (&(objectClass=groupOfNames)(member=%s)) \
759 (&(objectClass=posixGroup)(memberUid=%s)) \
760 )" % (self
.dn
, self
.uid
))
762 def is_member_of_group(self
, gid
):
764 Returns True if this account is a member of this group
766 return gid
in (g
.gid
for g
in self
.groups
)
768 # Created/Modified at
771 def created_at(self
):
772 return self
._get
_timestamp
("createTimestamp")
775 def modified_at(self
):
776 return self
._get
_timestamp
("modifyTimestamp")
785 address
+= self
.street
.splitlines()
787 if self
.postal_code
and self
.city
:
788 if self
.country_code
in ("AT", "DE"):
789 address
.append("%s %s" % (self
.postal_code
, self
.city
))
791 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
793 address
.append(self
.city
or self
.postal_code
)
795 if self
.country_name
:
796 address
.append(self
.country_name
)
800 def get_street(self
):
801 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
803 def set_street(self
, street
):
804 self
._set
_string
("street", street
)
806 street
= property(get_street
, set_street
)
809 return self
._get
_string
("l") or ""
811 def set_city(self
, city
):
812 self
._set
_string
("l", city
)
814 city
= property(get_city
, set_city
)
816 def get_postal_code(self
):
817 return self
._get
_string
("postalCode") or ""
819 def set_postal_code(self
, postal_code
):
820 self
._set
_string
("postalCode", postal_code
)
822 postal_code
= property(get_postal_code
, set_postal_code
)
824 # XXX This should be c
825 def get_country_code(self
):
826 return self
._get
_string
("st")
828 def set_country_code(self
, country_code
):
829 self
._set
_string
("st", country_code
)
831 country_code
= property(get_country_code
, set_country_code
)
834 def country_name(self
):
835 if self
.country_code
:
836 return countries
.get_name(self
.country_code
)
840 return self
._get
_string
("mail")
844 return "%s <%s>" % (self
, self
.email
)
846 # Mail Routing Address
848 def get_mail_routing_address(self
):
849 return self
._get
_string
("mailRoutingAddress", None)
851 def set_mail_routing_address(self
, address
):
852 self
._set
_string
("mailRoutingAddress", address
or None)
854 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
858 if "sipUser" in self
.classes
:
859 return self
._get
_string
("sipAuthenticationUser")
861 if "sipRoutingObject" in self
.classes
:
862 return self
._get
_string
("sipLocalAddress")
865 def sip_password(self
):
866 return self
._get
_string
("sipPassword")
869 def _generate_sip_password():
870 return util
.random_string(8)
874 return "%s@ipfire.org" % self
.sip_id
877 def agent_status(self
):
878 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
880 def uses_sip_forwarding(self
):
881 if self
.sip_routing_address
:
888 def get_sip_routing_address(self
):
889 if "sipRoutingObject" in self
.classes
:
890 return self
._get
_string
("sipRoutingAddress")
892 def set_sip_routing_address(self
, address
):
896 # Don't do anything if nothing has changed
897 if self
.get_sip_routing_address() == address
:
901 # This is no longer a SIP user any more
904 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
905 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
906 (ldap
.MOD_DELETE
, "sipPassword", None),
908 except ldap
.NO_SUCH_ATTRIBUTE
:
911 # Set new routing object
914 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
915 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
916 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
919 # If this is a change, we cannot add this again
920 except ldap
.TYPE_OR_VALUE_EXISTS
:
921 self
._set
_string
("sipRoutingAddress", address
)
925 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
926 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
927 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
929 except ldap
.NO_SUCH_ATTRIBUTE
:
933 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
934 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
935 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
938 # XXX Cache is invalid here
940 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
943 def sip_registrations(self
):
944 sip_registrations
= []
946 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
949 sip_registrations
.append(reg
)
951 return sip_registrations
954 def sip_channels(self
):
955 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
957 def get_cdr(self
, date
=None, limit
=None):
958 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
963 def phone_number(self
):
965 Returns the IPFire phone number
968 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
971 def fax_number(self
):
973 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
975 def get_phone_numbers(self
):
978 for field
in ("telephoneNumber", "homePhone", "mobile"):
979 for number
in self
._get
_phone
_numbers
(field
):
984 def set_phone_numbers(self
, phone_numbers
):
985 # Sort phone numbers by landline and mobile
986 _landline_numbers
= []
989 for number
in phone_numbers
:
991 number
= phonenumbers
.parse(number
, None)
992 except phonenumbers
.phonenumberutil
.NumberParseException
:
995 # Convert to string (in E.164 format)
996 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
998 # Separate mobile numbers
999 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1000 _mobile_numbers
.append(s
)
1002 _landline_numbers
.append(s
)
1005 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1006 self
._set
_strings
("mobile", _mobile_numbers
)
1008 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1011 def _all_telephone_numbers(self
):
1012 ret
= [ self
.sip_id
, ]
1014 if self
.phone_number
:
1015 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1018 for number
in self
.phone_numbers
:
1019 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1026 def get_description(self
):
1027 return self
._get
_string
("description")
1029 def set_description(self
, description
):
1030 self
._set
_string
("description", description
)
1032 description
= property(get_description
, set_description
)
1036 def has_avatar(self
):
1037 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
1038 if has_avatar
is None:
1039 has_avatar
= True if self
.get_avatar() else False
1041 # Cache avatar status for up to 24 hours
1042 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1046 def avatar_url(self
, size
=None):
1047 url
= "https://people.ipfire.org/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1050 url
+= "&size=%s" % size
1054 def get_avatar(self
, size
=None):
1055 photo
= self
._get
_bytes
("jpegPhoto")
1057 # Exit if no avatar is available
1061 # Return the raw image if no size was requested
1065 # Try to retrieve something from the cache
1066 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1070 # Generate a new thumbnail
1071 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1073 # Save to cache for 15m
1074 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1079 def avatar_hash(self
):
1080 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1082 h
= hashlib
.new("md5")
1083 h
.update(self
.get_avatar() or b
"")
1084 hash = h
.hexdigest()[:7]
1086 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1090 def upload_avatar(self
, avatar
):
1091 self
._set
("jpegPhoto", avatar
)
1093 # Delete cached avatar status
1094 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1096 # Delete avatar hash
1097 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1099 # Consent to promotional emails
1101 def get_consents_to_promotional_emails(self
):
1102 return self
.is_member_of_group("promotional-consent")
1104 def set_contents_to_promotional_emails(self
, value
):
1105 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1106 assert group
, "Could not find group: promotional-consent"
1109 group
.add_member(self
)
1111 group
.del_member(self
)
1113 consents_to_promotional_emails
= property(
1114 get_consents_to_promotional_emails
,
1115 set_contents_to_promotional_emails
,
1119 class StopForumSpam(Object
):
1120 def init(self
, uid
, email
, address
):
1121 self
.uid
, self
.email
, self
.address
= uid
, email
, address
1123 async def send_request(self
, **kwargs
):
1127 arguments
.update(kwargs
)
1130 request
= tornado
.httpclient
.HTTPRequest(
1131 "https://api.stopforumspam.org/api", method
="POST")
1132 request
.body
= urllib
.parse
.urlencode(arguments
)
1135 response
= await self
.backend
.http_client
.fetch(request
)
1137 # Decode the JSON response
1138 return json
.loads(response
.body
.decode())
1140 async def check_address(self
):
1141 response
= await self
.send_request(ip
=self
.address
)
1144 confidence
= response
["ip"]["confidence"]
1148 logging
.debug("Confidence for %s: %s" % (self
.address
, confidence
))
1152 async def check_username(self
):
1153 response
= await self
.send_request(username
=self
.uid
)
1156 confidence
= response
["username"]["confidence"]
1160 logging
.debug("Confidence for %s: %s" % (self
.uid
, confidence
))
1164 async def check_email(self
):
1165 response
= await self
.send_request(email
=self
.email
)
1168 confidence
= response
["email"]["confidence"]
1172 logging
.debug("Confidence for %s: %s" % (self
.email
, confidence
))
1176 async def check(self
, threshold
=95):
1178 This function tries to detect if we have a spammer.
1180 To honour the privacy of our users, we only send the IP
1181 address and username and if those are on the database, we
1182 will send the email address as well.
1184 confidences
= [await self
.check_address(), await self
.check_username()]
1186 if any((c
< threshold
for c
in confidences
)):
1187 confidences
.append(await self
.check_email())
1189 # Build a score based on the lowest confidence
1190 return 100 - min(confidences
)
1193 class Groups(Object
):
1195 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1196 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1198 # Everyone is a member of people
1199 "cn=people,ou=Group,dc=ipfire,dc=org",
1203 def search_base(self
):
1204 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1206 def _query(self
, *args
, **kwargs
):
1208 "search_base" : self
.backend
.groups
.search_base
,
1211 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1214 groups
= self
.get_all()
1218 def _get_groups(self
, query
, **kwargs
):
1219 res
= self
._query
(query
, **kwargs
)
1222 for dn
, attrs
in res
:
1223 # Skip any hidden groups
1224 if dn
in self
.hidden_groups
:
1227 g
= Group(self
.backend
, dn
, attrs
)
1230 return sorted(groups
)
1232 def _get_group(self
, query
, **kwargs
):
1237 groups
= self
._get
_groups
(query
, **kwargs
)
1242 return self
._get
_groups
(
1243 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1246 def get_by_gid(self
, gid
):
1247 return self
._get
_group
(
1248 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1252 class Group(LDAPObject
):
1254 if self
.description
:
1255 return "<%s %s (%s)>" % (
1256 self
.__class
__.__name
__,
1261 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1264 return self
.description
or self
.gid
1266 def __lt__(self
, other
):
1267 if isinstance(other
, self
.__class
__):
1268 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1275 Returns the number of members in this group
1279 for attr
in ("member", "memberUid"):
1280 a
= self
.attributes
.get(attr
, None)
1287 return iter(self
.members
)
1291 return self
._get
_string
("cn")
1294 def description(self
):
1295 return self
._get
_string
("description")
1299 return self
._get
_string
("mail")
1305 # Get all members by DN
1306 for dn
in self
._get
_strings
("member"):
1307 member
= self
.backend
.accounts
.get_by_dn(dn
)
1309 members
.append(member
)
1311 # Get all members by UID
1312 for uid
in self
._get
_strings
("memberUid"):
1313 member
= self
.backend
.accounts
.get_by_uid(uid
)
1315 members
.append(member
)
1317 return sorted(members
)
1319 def add_member(self
, account
):
1321 Adds a member to this group
1323 # Do nothing if this user is already in the group
1324 if account
.is_member_of_group(self
.gid
):
1327 if "posixGroup" in self
.objectclasses
:
1328 self
._add
_string
("memberUid", account
.uid
)
1330 self
._add
_string
("member", account
.dn
)
1332 # Append to cached list of members
1333 self
.members
.append(account
)
1336 def del_member(self
, account
):
1338 Removes a member from a group
1340 # Do nothing if this user is not in the group
1341 if not account
.is_member_of_group(self
.gid
):
1344 if "posixGroup" in self
.objectclasses
:
1345 self
._delete
_string
("memberUid", account
.uid
)
1347 self
._delete
_string
("member", account
.dn
)
1350 if __name__
== "__main__":