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 connection
= ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
198 trace_level
=2 if self
.backend
.debug
else 0,
199 retry_max
=10, retry_delay
=3)
201 # Set maximum timeout for operations
202 connection
.set_option(ldap
.OPT_TIMEOUT
, 10)
206 def _authenticate(self
):
207 # Authenticate against LDAP server using Kerberos
208 self
.ldap
.sasl_gssapi_bind_s()
211 logging
.info("Testing LDAP connection...")
215 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
217 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
218 logging
.debug("Performing LDAP query (%s): %s" \
219 % (search_base
or self
.search_base
, query
))
223 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
224 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
)
226 # Log time it took to perform the query
227 logging
.debug("Query took %.2fms" % ((time
.time() - t
) * 1000.0))
231 def _count(self
, query
):
232 res
= self
._query
(query
, attrlist
=["dn"], limit
=INT_MAX
)
236 def _search(self
, query
, attrlist
=None, limit
=0):
238 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
239 account
= self
.get_by_dn(dn
)
240 accounts
.append(account
)
244 def _get_attrs(self
, dn
):
246 Fetches all attributes for the given distinguished name
248 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
249 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
251 for dn
, attrs
in results
:
254 def get_by_dn(self
, dn
):
255 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
257 attrs
= self
._get
_attrs
(dn
)
260 # Cache all attributes for 5 min
261 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
263 return Account(self
.backend
, dn
, attrs
)
267 return t
.strftime("%Y%m%d%H%M%SZ")
269 def get_created_after(self
, ts
):
270 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
272 def count_created_after(self
, ts
):
273 return self
._count
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
275 def search(self
, query
):
276 accounts
= self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)(displayName=*%s*)(mail=*%s*)))" \
277 % (query
, query
, query
, query
))
279 return sorted(accounts
)
281 def _search_one(self
, query
):
282 results
= self
._search
(query
, limit
=1)
284 for result
in results
:
287 def uid_is_valid(self
, uid
):
288 # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
289 m
= re
.match(r
"^[a-z_][a-z0-9_-]{3,31}$", uid
)
295 def uid_exists(self
, uid
):
296 if self
.get_by_uid(uid
):
299 res
= self
.db
.get("SELECT 1 FROM account_activations \
300 WHERE uid = %s AND expires_at > NOW()", uid
)
305 # Account with uid does not exist, yet
308 def get_by_uid(self
, uid
):
309 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
311 def get_by_mail(self
, mail
):
312 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
314 def find_account(self
, s
):
315 account
= self
.get_by_uid(s
)
319 return self
.get_by_mail(s
)
321 def get_by_sip_id(self
, sip_id
):
325 return self
._search
_one
(
326 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
329 def get_by_phone_number(self
, number
):
333 return self
._search
_one
(
334 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
335 % (number
, number
, number
, number
))
337 async def check_spam(self
, uid
, email
, address
):
338 sfs
= StopForumSpam(self
.backend
, uid
, email
, address
)
341 score
= await sfs
.check()
345 def auth(self
, username
, password
):
347 account
= self
.backend
.accounts
.find_account(username
)
350 if account
and account
.check_password(password
):
355 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
356 # Convert all uids to lowercase
359 # Check if UID is valid
360 if not self
.uid_is_valid(uid
):
361 raise ValueError("UID is invalid: %s" % uid
)
363 # Check if UID is unique
364 if self
.uid_exists(uid
):
365 raise ValueError("UID exists: %s" % uid
)
367 # Generate a random activation code
368 activation_code
= util
.random_string(36)
370 # Create an entry in our database until the user
371 # has activated the account
372 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
373 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
374 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
376 # Send an account activation email
377 self
.backend
.messages
.send_template("auth/messages/register",
378 recipients
=[email
], priority
=100, uid
=uid
,
379 activation_code
=activation_code
, email
=email
,
380 first_name
=first_name
, last_name
=last_name
)
382 def activate(self
, uid
, activation_code
):
383 res
= self
.db
.get("DELETE FROM account_activations \
384 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
385 RETURNING *", uid
, activation_code
)
387 # Return nothing when account was not found
391 # Return the account if it has already been created
392 account
= self
.get_by_uid(uid
)
396 # Create a new account on the LDAP database
397 account
= self
.create(uid
, res
.email
,
398 first_name
=res
.first_name
, last_name
=res
.last_name
,
399 country_code
=res
.country_code
)
401 # Non-EU users do not need to consent to promo emails
402 if account
.country_code
and not account
.country_code
in countries
.EU_COUNTRIES
:
403 account
.consents_to_promotional_emails
= True
405 # Invite newly registered users to newsletter
406 self
.backend
.messages
.send_template(
407 "newsletter/subscribe", address
="%s <%s>" % (account
, account
.email
))
409 # Send email about account registration
410 self
.backend
.messages
.send_template("people/messages/new-account",
411 recipients
=["moderators@ipfire.org"], account
=account
)
413 # Launch drip campaigns
414 for campaign
in ("signup", "christmas"):
415 self
.backend
.campaigns
.launch(campaign
, account
)
419 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
420 cn
= "%s %s" % (first_name
, last_name
)
424 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
425 "mail" : email
.encode(),
429 "sn" : last_name
.encode(),
430 "givenName" : first_name
.encode(),
433 logging
.info("Creating new account: %s: %s" % (uid
, account
))
436 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
438 # Create account on LDAP
439 self
.accounts
._authenticate
()
440 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
443 account
= self
.get_by_dn(dn
)
445 # Optionally set country code
447 account
.country_code
= country_code
454 def create_session(self
, account
, host
):
455 session_id
= util
.random_string(64)
457 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
458 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
460 # Session could not be created
464 logging
.info("Created session %s for %s which expires %s" \
465 % (res
.session_id
, account
, res
.time_expires
))
466 return res
.session_id
, res
.time_expires
468 def destroy_session(self
, session_id
, host
):
469 logging
.info("Destroying session %s" % session_id
)
471 self
.db
.execute("DELETE FROM sessions \
472 WHERE session_id = %s AND host = %s", session_id
, host
)
474 def get_by_session(self
, session_id
, host
):
475 logging
.debug("Looking up session %s" % session_id
)
477 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
478 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
481 # Session does not exist or has expired
485 # Update the session expiration time
486 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
487 WHERE session_id = %s AND host = %s", session_id
, host
)
489 return self
.get_by_uid(res
.uid
)
492 # Cleanup expired sessions
493 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
495 # Cleanup expired account activations
496 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
498 # Cleanup expired account password resets
499 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
503 def decode_discourse_payload(self
, payload
, signature
):
505 calculated_signature
= self
.sign_discourse_payload(payload
)
507 if not hmac
.compare_digest(signature
, calculated_signature
):
508 raise ValueError("Invalid signature: %s" % signature
)
510 # Decode the query string
511 qs
= base64
.b64decode(payload
).decode()
513 # Parse the query string
515 for key
, val
in urllib
.parse
.parse_qsl(qs
):
520 def encode_discourse_payload(self
, **args
):
521 # Encode the arguments into an URL-formatted string
522 qs
= urllib
.parse
.urlencode(args
).encode()
525 return base64
.b64encode(qs
).decode()
527 def sign_discourse_payload(self
, payload
, secret
=None):
529 secret
= self
.settings
.get("discourse_sso_secret")
531 # Calculate a HMAC using SHA256
532 h
= hmac
.new(secret
.encode(),
533 msg
=payload
.encode(), digestmod
="sha256")
541 for country
in iso3166
.countries
:
542 count
= self
._count
("(&(objectClass=person)(st=%s))" % country
.alpha2
)
550 class Account(LDAPObject
):
558 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
560 def __lt__(self
, other
):
561 if isinstance(other
, self
.__class
__):
562 return self
.name
< other
.name
564 def _clear_cache(self
):
565 # Delete cached attributes
566 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
569 def kerberos_attributes(self
):
570 res
= self
.backend
.accounts
._query
(
571 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
573 "krbLastSuccessfulAuth",
574 "krbLastPasswordChange",
576 "krbLoginFailedCount",
579 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
581 for dn
, attrs
in res
:
582 return { key
: attrs
[key
][0] for key
in attrs
}
587 def last_successful_authentication(self
):
589 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
593 return self
._parse
_date
(s
)
596 def last_failed_authentication(self
):
598 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
602 return self
._parse
_date
(s
)
605 def failed_login_count(self
):
607 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
616 def passwd(self
, password
):
620 # The new password must have a score of 3 or better
621 quality
= self
.check_password_quality(password
)
622 if quality
["score"] < 3:
623 raise ValueError("Password too weak")
625 self
.accounts
._authenticate
()
626 self
.ldap
.passwd_s(self
.dn
, None, password
)
628 def check_password(self
, password
):
630 Bind to the server with given credentials and return
631 true if password is corrent and false if not.
633 Raises exceptions from the server on any other errors.
638 logging
.debug("Checking credentials for %s" % self
.dn
)
640 # Create a new LDAP connection
641 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
642 conn
= ldap
.initialize(ldap_uri
)
645 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
646 except ldap
.INVALID_CREDENTIALS
:
647 logging
.debug("Account credentials are invalid for %s" % self
)
650 logging
.info("Successfully authenticated %s" % self
)
654 def check_password_quality(self
, password
):
656 Passwords are passed through zxcvbn to make sure
657 that they are strong enough.
659 return zxcvbn
.zxcvbn(password
, user_inputs
=(
660 self
.first_name
, self
.last_name
,
663 def request_password_reset(self
, address
=None):
664 reset_code
= util
.random_string(64)
666 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
667 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
669 # Send a password reset email
670 self
.backend
.messages
.send_template("auth/messages/password-reset",
671 recipients
=[self
.email
], priority
=100, account
=self
, reset_code
=reset_code
)
673 def reset_password(self
, reset_code
, new_password
):
674 # Delete the reset token
675 res
= self
.db
.query("DELETE FROM account_password_resets \
676 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
677 RETURNING *", self
.uid
, reset_code
)
679 # The reset code was invalid
681 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
683 # Perform password change
684 return self
.passwd(new_password
)
687 return self
.is_member_of_group("sudo")
690 return self
.is_member_of_group("staff")
692 def is_moderator(self
):
693 return self
.is_member_of_group("moderators")
696 return "posixAccount" in self
.classes
699 return "postfixMailUser" in self
.classes
702 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
704 def can_be_managed_by(self
, account
):
706 Returns True if account is allowed to manage this account
708 # Admins can manage all accounts
709 if account
.is_admin():
712 # Users can manage themselves
713 return self
== account
717 return self
._get
_strings
("objectClass")
721 return self
._get
_string
("uid")
725 return self
._get
_string
("cn")
729 def get_nickname(self
):
730 return self
._get
_string
("displayName")
732 def set_nickname(self
, nickname
):
733 self
._set
_string
("displayName", nickname
)
735 nickname
= property(get_nickname
, set_nickname
)
739 def get_first_name(self
):
740 return self
._get
_string
("givenName")
742 def set_first_name(self
, first_name
):
743 self
._set
_string
("givenName", first_name
)
746 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
748 first_name
= property(get_first_name
, set_first_name
)
752 def get_last_name(self
):
753 return self
._get
_string
("sn")
755 def set_last_name(self
, last_name
):
756 self
._set
_string
("sn", last_name
)
759 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
761 last_name
= property(get_last_name
, set_last_name
)
765 return self
.backend
.groups
._get
_groups
("(| \
766 (&(objectClass=groupOfNames)(member=%s)) \
767 (&(objectClass=posixGroup)(memberUid=%s)) \
768 )" % (self
.dn
, self
.uid
))
770 def is_member_of_group(self
, gid
):
772 Returns True if this account is a member of this group
774 return gid
in (g
.gid
for g
in self
.groups
)
776 # Created/Modified at
779 def created_at(self
):
780 return self
._get
_timestamp
("createTimestamp")
783 def modified_at(self
):
784 return self
._get
_timestamp
("modifyTimestamp")
793 address
+= self
.street
.splitlines()
795 if self
.postal_code
and self
.city
:
796 if self
.country_code
in ("AT", "DE"):
797 address
.append("%s %s" % (self
.postal_code
, self
.city
))
799 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
801 address
.append(self
.city
or self
.postal_code
)
803 if self
.country_name
:
804 address
.append(self
.country_name
)
808 def get_street(self
):
809 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
811 def set_street(self
, street
):
812 self
._set
_string
("street", street
)
814 street
= property(get_street
, set_street
)
817 return self
._get
_string
("l") or ""
819 def set_city(self
, city
):
820 self
._set
_string
("l", city
)
822 city
= property(get_city
, set_city
)
824 def get_postal_code(self
):
825 return self
._get
_string
("postalCode") or ""
827 def set_postal_code(self
, postal_code
):
828 self
._set
_string
("postalCode", postal_code
)
830 postal_code
= property(get_postal_code
, set_postal_code
)
832 # XXX This should be c
833 def get_country_code(self
):
834 return self
._get
_string
("st")
836 def set_country_code(self
, country_code
):
837 self
._set
_string
("st", country_code
)
839 country_code
= property(get_country_code
, set_country_code
)
842 def country_name(self
):
843 if self
.country_code
:
844 return countries
.get_name(self
.country_code
)
848 return self
._get
_string
("mail")
852 return "%s <%s>" % (self
, self
.email
)
854 # Mail Routing Address
856 def get_mail_routing_address(self
):
857 return self
._get
_string
("mailRoutingAddress", None)
859 def set_mail_routing_address(self
, address
):
860 self
._set
_string
("mailRoutingAddress", address
or None)
862 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
866 if "sipUser" in self
.classes
:
867 return self
._get
_string
("sipAuthenticationUser")
869 if "sipRoutingObject" in self
.classes
:
870 return self
._get
_string
("sipLocalAddress")
873 def sip_password(self
):
874 return self
._get
_string
("sipPassword")
877 def _generate_sip_password():
878 return util
.random_string(8)
882 return "%s@ipfire.org" % self
.sip_id
885 def agent_status(self
):
886 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
888 def uses_sip_forwarding(self
):
889 if self
.sip_routing_address
:
896 def get_sip_routing_address(self
):
897 if "sipRoutingObject" in self
.classes
:
898 return self
._get
_string
("sipRoutingAddress")
900 def set_sip_routing_address(self
, address
):
904 # Don't do anything if nothing has changed
905 if self
.get_sip_routing_address() == address
:
909 # This is no longer a SIP user any more
912 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
913 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
914 (ldap
.MOD_DELETE
, "sipPassword", None),
916 except ldap
.NO_SUCH_ATTRIBUTE
:
919 # Set new routing object
922 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
923 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
924 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
927 # If this is a change, we cannot add this again
928 except ldap
.TYPE_OR_VALUE_EXISTS
:
929 self
._set
_string
("sipRoutingAddress", address
)
933 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
934 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
935 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
937 except ldap
.NO_SUCH_ATTRIBUTE
:
941 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
942 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
943 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
946 # XXX Cache is invalid here
948 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
951 def sip_registrations(self
):
952 sip_registrations
= []
954 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
957 sip_registrations
.append(reg
)
959 return sip_registrations
962 def sip_channels(self
):
963 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
965 def get_cdr(self
, date
=None, limit
=None):
966 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
971 def phone_number(self
):
973 Returns the IPFire phone number
976 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
979 def fax_number(self
):
981 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
983 def get_phone_numbers(self
):
986 for field
in ("telephoneNumber", "homePhone", "mobile"):
987 for number
in self
._get
_phone
_numbers
(field
):
992 def set_phone_numbers(self
, phone_numbers
):
993 # Sort phone numbers by landline and mobile
994 _landline_numbers
= []
997 for number
in phone_numbers
:
999 number
= phonenumbers
.parse(number
, None)
1000 except phonenumbers
.phonenumberutil
.NumberParseException
:
1003 # Convert to string (in E.164 format)
1004 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1006 # Separate mobile numbers
1007 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1008 _mobile_numbers
.append(s
)
1010 _landline_numbers
.append(s
)
1013 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1014 self
._set
_strings
("mobile", _mobile_numbers
)
1016 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1019 def _all_telephone_numbers(self
):
1020 ret
= [ self
.sip_id
, ]
1022 if self
.phone_number
:
1023 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1026 for number
in self
.phone_numbers
:
1027 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1034 def get_description(self
):
1035 return self
._get
_string
("description")
1037 def set_description(self
, description
):
1038 self
._set
_string
("description", description
)
1040 description
= property(get_description
, set_description
)
1044 def has_avatar(self
):
1045 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
1046 if has_avatar
is None:
1047 has_avatar
= True if self
.get_avatar() else False
1049 # Cache avatar status for up to 24 hours
1050 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1054 def avatar_url(self
, size
=None):
1055 url
= "https://people.ipfire.org/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1058 url
+= "&size=%s" % size
1062 def get_avatar(self
, size
=None):
1063 photo
= self
._get
_bytes
("jpegPhoto")
1065 # Exit if no avatar is available
1069 # Return the raw image if no size was requested
1073 # Try to retrieve something from the cache
1074 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1078 # Generate a new thumbnail
1079 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1081 # Save to cache for 15m
1082 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1087 def avatar_hash(self
):
1088 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1090 h
= hashlib
.new("md5")
1091 h
.update(self
.get_avatar() or b
"")
1092 hash = h
.hexdigest()[:7]
1094 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1098 def upload_avatar(self
, avatar
):
1099 self
._set
("jpegPhoto", avatar
)
1101 # Delete cached avatar status
1102 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1104 # Delete avatar hash
1105 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1107 # Consent to promotional emails
1109 def get_consents_to_promotional_emails(self
):
1110 return self
.is_member_of_group("promotional-consent")
1112 def set_contents_to_promotional_emails(self
, value
):
1113 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1114 assert group
, "Could not find group: promotional-consent"
1117 group
.add_member(self
)
1119 group
.del_member(self
)
1121 consents_to_promotional_emails
= property(
1122 get_consents_to_promotional_emails
,
1123 set_contents_to_promotional_emails
,
1127 class StopForumSpam(Object
):
1128 def init(self
, uid
, email
, address
):
1129 self
.uid
, self
.email
, self
.address
= uid
, email
, address
1131 async def send_request(self
, **kwargs
):
1135 arguments
.update(kwargs
)
1138 request
= tornado
.httpclient
.HTTPRequest(
1139 "https://api.stopforumspam.org/api", method
="POST",
1140 connect_timeout
=2, request_timeout
=5)
1141 request
.body
= urllib
.parse
.urlencode(arguments
)
1144 response
= await self
.backend
.http_client
.fetch(request
)
1146 # Decode the JSON response
1147 return json
.loads(response
.body
.decode())
1149 async def check_address(self
):
1150 response
= await self
.send_request(ip
=self
.address
)
1153 confidence
= response
["ip"]["confidence"]
1157 logging
.debug("Confidence for %s: %s" % (self
.address
, confidence
))
1161 async def check_username(self
):
1162 response
= await self
.send_request(username
=self
.uid
)
1165 confidence
= response
["username"]["confidence"]
1169 logging
.debug("Confidence for %s: %s" % (self
.uid
, confidence
))
1173 async def check_email(self
):
1174 response
= await self
.send_request(email
=self
.email
)
1177 confidence
= response
["email"]["confidence"]
1181 logging
.debug("Confidence for %s: %s" % (self
.email
, confidence
))
1185 async def check(self
, threshold
=95):
1187 This function tries to detect if we have a spammer.
1189 To honour the privacy of our users, we only send the IP
1190 address and username and if those are on the database, we
1191 will send the email address as well.
1193 confidences
= [await self
.check_address(), await self
.check_username()]
1195 if any((c
< threshold
for c
in confidences
)):
1196 confidences
.append(await self
.check_email())
1198 # Build a score based on the lowest confidence
1199 return 100 - min(confidences
)
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__":