]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
44952a34e75a11b8cbef96e80cda15c878b0e7ec
15 import tornado
.httpclient
20 from . import countries
22 from .decorators
import *
23 from .misc
import Object
25 # Set the client keytab name
26 os
.environ
["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
28 class Accounts(Object
):
30 self
.search_base
= self
.settings
.get("ldap_search_base")
33 # Only return developers (group with ID 1000)
34 accounts
= self
._search
("(&(objectClass=posixAccount)(gidNumber=1000))")
36 return iter(sorted(accounts
))
40 # Connect to LDAP server
41 ldap_uri
= self
.settings
.get("ldap_uri")
43 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
45 # Connect to the LDAP server
46 return ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
47 retry_max
=10, retry_delay
=3)
49 def _authenticate(self
):
50 # Authenticate against LDAP server using Kerberos
51 self
.ldap
.sasl_gssapi_bind_s()
54 logging
.info("Testing LDAP connection...")
58 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
60 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
61 logging
.debug("Performing LDAP query (%s): %s" \
62 % (search_base
or self
.search_base
, query
))
66 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
67 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
)
69 # Log time it took to perform the query
70 logging
.debug("Query took %.2fms" % ((time
.time() - t
) * 1000.0))
74 def _search(self
, query
, attrlist
=None, limit
=0):
76 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
77 account
= self
.get_by_dn(dn
)
78 accounts
.append(account
)
82 def _get_attrs(self
, dn
):
84 Fetches all attributes for the given distinguished name
86 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
87 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
89 for dn
, attrs
in results
:
92 def get_by_dn(self
, dn
):
93 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
95 attrs
= self
._get
_attrs
(dn
)
98 # Cache all attributes for 5 min
99 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
101 return Account(self
.backend
, dn
, attrs
)
103 def get_created_after(self
, ts
):
104 t
= ts
.strftime("%Y%m%d%H%M%SZ")
106 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % t
)
108 def search(self
, query
):
109 # Search for exact matches
110 accounts
= self
._search
(
111 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
112 % (query
, query
, query
, query
, query
, query
))
114 # Find accounts by name
116 for account
in self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query
, query
)):
117 if not account
in accounts
:
118 accounts
.append(account
)
120 return sorted(accounts
)
122 def _search_one(self
, query
):
123 results
= self
._search
(query
, limit
=1)
125 for result
in results
:
128 def uid_is_valid(self
, uid
):
129 # UID must be at least four characters
133 # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
134 m
= re
.match(r
"^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\$)$", uid
)
140 def uid_exists(self
, uid
):
141 if self
.get_by_uid(uid
):
144 res
= self
.db
.get("SELECT 1 FROM account_activations \
145 WHERE uid = %s AND expires_at > NOW()", uid
)
150 # Account with uid does not exist, yet
153 def get_by_uid(self
, uid
):
154 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
156 def get_by_mail(self
, mail
):
157 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
159 def find_account(self
, s
):
160 account
= self
.get_by_uid(s
)
164 return self
.get_by_mail(s
)
166 def get_by_sip_id(self
, sip_id
):
170 return self
._search
_one
(
171 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
174 def get_by_phone_number(self
, number
):
178 return self
._search
_one
(
179 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
180 % (number
, number
, number
, number
))
182 @tornado.gen
.coroutine
183 def check_spam(self
, uid
, email
, address
):
184 sfs
= StopForumSpam(self
.backend
, uid
, email
, address
)
187 score
= yield sfs
.check()
191 def auth(self
, username
, password
):
193 account
= self
.backend
.accounts
.find_account(username
)
196 if account
and account
.check_password(password
):
201 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
202 # Convert all uids to lowercase
205 # Check if UID is valid
206 if not self
.uid_is_valid(uid
):
207 raise ValueError("UID is invalid: %s" % uid
)
209 # Check if UID is unique
210 if self
.uid_exists(uid
):
211 raise ValueError("UID exists: %s" % uid
)
213 # Generate a random activation code
214 activation_code
= util
.random_string(36)
216 # Create an entry in our database until the user
217 # has activated the account
218 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
219 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
220 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
222 # Send an account activation email
223 self
.backend
.messages
.send_template("auth/messages/register",
224 recipients
=[email
], priority
=100, uid
=uid
,
225 activation_code
=activation_code
, email
=email
,
226 first_name
=first_name
, last_name
=last_name
)
228 def activate(self
, uid
, activation_code
):
229 res
= self
.db
.get("DELETE FROM account_activations \
230 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
231 RETURNING *", uid
, activation_code
)
233 # Return nothing when account was not found
237 # Return the account if it has already been created
238 account
= self
.get_by_uid(uid
)
242 # Create a new account on the LDAP database
243 account
= self
.create(uid
, res
.email
,
244 first_name
=res
.first_name
, last_name
=res
.last_name
,
245 country_code
=res
.country_code
)
247 # Send email about account registration
248 self
.backend
.messages
.send_template("people/messages/new-account",
249 recipients
=["moderators@ipfire.org"], account
=account
)
253 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
254 cn
= "%s %s" % (first_name
, last_name
)
258 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
259 "mail" : email
.encode(),
263 "sn" : last_name
.encode(),
264 "givenName" : first_name
.encode(),
267 logging
.info("Creating new account: %s: %s" % (uid
, account
))
270 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
272 # Create account on LDAP
273 self
.accounts
._authenticate
()
274 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
277 account
= self
.get_by_dn(dn
)
279 # Optionally set country code
281 account
.country_code
= country_code
288 def create_session(self
, account
, host
):
289 session_id
= util
.random_string(64)
291 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
292 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
294 # Session could not be created
298 logging
.info("Created session %s for %s which expires %s" \
299 % (res
.session_id
, account
, res
.time_expires
))
300 return res
.session_id
, res
.time_expires
302 def destroy_session(self
, session_id
, host
):
303 logging
.info("Destroying session %s" % session_id
)
305 self
.db
.execute("DELETE FROM sessions \
306 WHERE session_id = %s AND host = %s", session_id
, host
)
308 def get_by_session(self
, session_id
, host
):
309 logging
.debug("Looking up session %s" % session_id
)
311 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
312 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
315 # Session does not exist or has expired
319 # Update the session expiration time
320 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
321 WHERE session_id = %s AND host = %s", session_id
, host
)
323 return self
.get_by_uid(res
.uid
)
326 # Cleanup expired sessions
327 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
329 # Cleanup expired account activations
330 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
334 def decode_discourse_payload(self
, payload
, signature
):
336 calculated_signature
= self
.sign_discourse_payload(payload
)
338 if not hmac
.compare_digest(signature
, calculated_signature
):
339 raise ValueError("Invalid signature: %s" % signature
)
341 # Decode the query string
342 qs
= base64
.b64decode(payload
).decode()
344 # Parse the query string
346 for key
, val
in urllib
.parse
.parse_qsl(qs
):
351 def encode_discourse_payload(self
, **args
):
352 # Encode the arguments into an URL-formatted string
353 qs
= urllib
.parse
.urlencode(args
).encode()
356 return base64
.b64encode(qs
).decode()
358 def sign_discourse_payload(self
, payload
, secret
=None):
360 secret
= self
.settings
.get("discourse_sso_secret")
362 # Calculate a HMAC using SHA256
363 h
= hmac
.new(secret
.encode(),
364 msg
=payload
.encode(), digestmod
="sha256")
369 class Account(Object
):
370 def __init__(self
, backend
, dn
, attrs
=None):
371 Object
.__init
__(self
, backend
)
374 self
.attributes
= attrs
or {}
383 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
385 def __eq__(self
, other
):
386 if isinstance(other
, self
.__class
__):
387 return self
.dn
== other
.dn
389 def __lt__(self
, other
):
390 if isinstance(other
, self
.__class
__):
391 return self
.name
< other
.name
395 return self
.accounts
.ldap
397 def _exists(self
, key
):
406 for value
in self
.attributes
.get(key
, []):
409 def _get_bytes(self
, key
, default
=None):
410 for value
in self
._get
(key
):
415 def _get_strings(self
, key
):
416 for value
in self
._get
(key
):
419 def _get_string(self
, key
, default
=None):
420 for value
in self
._get
_strings
(key
):
425 def _get_phone_numbers(self
, key
):
426 for value
in self
._get
_strings
(key
):
427 yield phonenumbers
.parse(value
, None)
429 def _get_timestamp(self
, key
):
430 value
= self
._get
_string
(key
)
432 # Parse the timestamp value and returns a datetime object
434 return datetime
.datetime
.strptime(value
, "%Y%m%d%H%M%SZ")
436 def _modify(self
, modlist
):
437 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
439 # Authenticate before performing any write operations
440 self
.accounts
._authenticate
()
442 # Run modify operation
443 self
.ldap
.modify_s(self
.dn
, modlist
)
445 # Delete cached attributes
446 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
448 def _set(self
, key
, values
):
449 current
= self
._get
(key
)
451 # Don't do anything if nothing has changed
452 if list(current
) == values
:
455 # Remove all old values and add all new ones
458 if self
._exists
(key
):
459 modlist
.append((ldap
.MOD_DELETE
, key
, None))
463 modlist
.append((ldap
.MOD_ADD
, key
, values
))
465 # Run modify operation
466 self
._modify
(modlist
)
469 self
.attributes
.update({ key
: values
})
471 def _set_bytes(self
, key
, values
):
472 return self
._set
(key
, values
)
474 def _set_strings(self
, key
, values
):
475 return self
._set
(key
, [e
.encode() for e
in values
if e
])
477 def _set_string(self
, key
, value
):
478 return self
._set
_strings
(key
, [value
,])
480 def _add(self
, key
, values
):
482 (ldap
.MOD_ADD
, key
, values
),
485 self
._modify
(modlist
)
487 def _add_strings(self
, key
, values
):
488 return self
._add
(key
, [e
.encode() for e
in values
])
490 def _add_string(self
, key
, value
):
491 return self
._add
_strings
(key
, [value
,])
493 def _delete(self
, key
, values
):
495 (ldap
.MOD_DELETE
, key
, values
),
498 self
._modify
(modlist
)
500 def _delete_strings(self
, key
, values
):
501 return self
._delete
(key
, [e
.encode() for e
in values
])
503 def _delete_string(self
, key
, value
):
504 return self
._delete
_strings
(key
, [value
,])
507 def kerberos_attributes(self
):
508 res
= self
.backend
.accounts
._query
(
509 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
511 "krbLastSuccessfulAuth",
512 "krbLastPasswordChange",
514 "krbLoginFailedCount",
517 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
519 for dn
, attrs
in res
:
520 return { key
: attrs
[key
][0] for key
in attrs
}
526 return datetime
.datetime
.strptime(s
.decode(), "%Y%m%d%H%M%SZ")
529 def last_successful_authentication(self
):
531 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
535 return self
._parse
_date
(s
)
538 def last_failed_authentication(self
):
540 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
544 return self
._parse
_date
(s
)
547 def failed_login_count(self
):
549 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
558 def passwd(self
, password
):
562 # The new password must have a score of 3 or better
563 quality
= self
.check_password_quality(password
)
564 if quality
["score"] < 3:
565 raise ValueError("Password too weak")
567 self
.accounts
._authenticate
()
568 self
.ldap
.passwd_s(self
.dn
, None, password
)
570 def check_password(self
, password
):
572 Bind to the server with given credentials and return
573 true if password is corrent and false if not.
575 Raises exceptions from the server on any other errors.
580 logging
.debug("Checking credentials for %s" % self
.dn
)
582 # Create a new LDAP connection
583 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
584 conn
= ldap
.initialize(ldap_uri
)
587 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
588 except ldap
.INVALID_CREDENTIALS
:
589 logging
.debug("Account credentials are invalid for %s" % self
)
592 logging
.info("Successfully authenticated %s" % self
)
596 def check_password_quality(self
, password
):
598 Passwords are passed through zxcvbn to make sure
599 that they are strong enough.
601 return zxcvbn
.zxcvbn(password
, user_inputs
=(
602 self
.first_name
, self
.last_name
,
606 return self
.is_member_of_group("sudo")
609 return self
.is_member_of_group("staff")
611 def is_moderator(self
):
612 return self
.is_member_of_group("moderators")
615 return "posixAccount" in self
.classes
618 return "postfixMailUser" in self
.classes
621 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
623 def can_be_managed_by(self
, account
):
625 Returns True if account is allowed to manage this account
627 # Admins can manage all accounts
628 if account
.is_admin():
631 # Users can manage themselves
632 return self
== account
636 return self
._get
_strings
("objectClass")
640 return self
._get
_string
("uid")
644 return self
._get
_string
("cn")
648 def get_nickname(self
):
649 return self
._get
_string
("displayName")
651 def set_nickname(self
, nickname
):
652 self
._set
_string
("displayName", nickname
)
654 nickname
= property(get_nickname
, set_nickname
)
658 def get_first_name(self
):
659 return self
._get
_string
("givenName")
661 def set_first_name(self
, first_name
):
662 self
._set
_string
("givenName", first_name
)
665 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
667 first_name
= property(get_first_name
, set_first_name
)
671 def get_last_name(self
):
672 return self
._get
_string
("sn")
674 def set_last_name(self
, last_name
):
675 self
._set
_string
("sn", last_name
)
678 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
680 last_name
= property(get_last_name
, set_last_name
)
684 groups
= self
.memcache
.get("accounts:%s:groups" % self
.dn
)
686 # Fetch groups from LDAP
687 groups
= self
._get
_groups
()
689 # Cache groups for 5 min
690 self
.memcache
.set("accounts:%s:groups" % self
.dn
, groups
, 300)
692 return sorted((Group(self
.backend
, gid
) for gid
in groups
))
694 def _get_groups(self
):
697 res
= self
.accounts
._query
("(| \
698 (&(objectClass=groupOfNames)(member=%s)) \
699 (&(objectClass=posixGroup)(memberUid=%s)) \
700 )" % (self
.dn
, self
.uid
), ["cn"])
702 for dn
, attrs
in res
:
703 cns
= attrs
.get("cn")
705 groups
.append(cns
[0].decode())
709 def is_member_of_group(self
, gid
):
711 Returns True if this account is a member of this group
713 return gid
in (g
.gid
for g
in self
.groups
)
715 # Created/Modified at
718 def created_at(self
):
719 return self
._get
_timestamp
("createTimestamp")
722 def modified_at(self
):
723 return self
._get
_timestamp
("modifyTimestamp")
732 address
+= self
.street
.splitlines()
734 if self
.postal_code
and self
.city
:
735 if self
.country_code
in ("AT", "DE"):
736 address
.append("%s %s" % (self
.postal_code
, self
.city
))
738 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
740 address
.append(self
.city
or self
.postal_code
)
742 if self
.country_name
:
743 address
.append(self
.country_name
)
747 def get_street(self
):
748 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
750 def set_street(self
, street
):
751 self
._set
_string
("street", street
)
753 street
= property(get_street
, set_street
)
756 return self
._get
_string
("l") or ""
758 def set_city(self
, city
):
759 self
._set
_string
("l", city
)
761 city
= property(get_city
, set_city
)
763 def get_postal_code(self
):
764 return self
._get
_string
("postalCode") or ""
766 def set_postal_code(self
, postal_code
):
767 self
._set
_string
("postalCode", postal_code
)
769 postal_code
= property(get_postal_code
, set_postal_code
)
771 # XXX This should be c
772 def get_country_code(self
):
773 return self
._get
_string
("st")
775 def set_country_code(self
, country_code
):
776 self
._set
_string
("st", country_code
)
778 country_code
= property(get_country_code
, set_country_code
)
781 def country_name(self
):
782 if self
.country_code
:
783 return countries
.get_name(self
.country_code
)
787 return self
._get
_string
("mail")
789 # Mail Routing Address
791 def get_mail_routing_address(self
):
792 return self
._get
_string
("mailRoutingAddress", None)
794 def set_mail_routing_address(self
, address
):
795 self
._set
_string
("mailRoutingAddress", address
or None)
797 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
801 if "sipUser" in self
.classes
:
802 return self
._get
_string
("sipAuthenticationUser")
804 if "sipRoutingObject" in self
.classes
:
805 return self
._get
_string
("sipLocalAddress")
808 def sip_password(self
):
809 return self
._get
_string
("sipPassword")
812 def _generate_sip_password():
813 return util
.random_string(8)
817 return "%s@ipfire.org" % self
.sip_id
819 def uses_sip_forwarding(self
):
820 if self
.sip_routing_address
:
827 def get_sip_routing_address(self
):
828 if "sipRoutingObject" in self
.classes
:
829 return self
._get
_string
("sipRoutingAddress")
831 def set_sip_routing_address(self
, address
):
835 # Don't do anything if nothing has changed
836 if self
.get_sip_routing_address() == address
:
840 # This is no longer a SIP user any more
843 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
844 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
845 (ldap
.MOD_DELETE
, "sipPassword", None),
847 except ldap
.NO_SUCH_ATTRIBUTE
:
850 # Set new routing object
853 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
854 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
855 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
858 # If this is a change, we cannot add this again
859 except ldap
.TYPE_OR_VALUE_EXISTS
:
860 self
._set
_string
("sipRoutingAddress", address
)
864 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
865 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
866 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
868 except ldap
.NO_SUCH_ATTRIBUTE
:
872 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
873 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
874 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
877 # XXX Cache is invalid here
879 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
882 def sip_registrations(self
):
883 sip_registrations
= []
885 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
888 sip_registrations
.append(reg
)
890 return sip_registrations
893 def sip_channels(self
):
894 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
896 def get_cdr(self
, date
=None, limit
=None):
897 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
902 def phone_number(self
):
904 Returns the IPFire phone number
907 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
910 def fax_number(self
):
912 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
914 def get_phone_numbers(self
):
917 for field
in ("telephoneNumber", "homePhone", "mobile"):
918 for number
in self
._get
_phone
_numbers
(field
):
923 def set_phone_numbers(self
, phone_numbers
):
924 # Sort phone numbers by landline and mobile
925 _landline_numbers
= []
928 for number
in phone_numbers
:
930 number
= phonenumbers
.parse(number
, None)
931 except phonenumbers
.phonenumberutil
.NumberParseException
:
934 # Convert to string (in E.164 format)
935 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
937 # Separate mobile numbers
938 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
939 _mobile_numbers
.append(s
)
941 _landline_numbers
.append(s
)
944 self
._set
_strings
("telephoneNumber", _landline_numbers
)
945 self
._set
_strings
("mobile", _mobile_numbers
)
947 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
950 def _all_telephone_numbers(self
):
951 ret
= [ self
.sip_id
, ]
953 if self
.phone_number
:
954 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
957 for number
in self
.phone_numbers
:
958 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
963 def has_avatar(self
):
964 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
965 if has_avatar
is None:
966 has_avatar
= True if self
.get_avatar() else False
968 # Cache avatar status for up to 24 hours
969 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
973 def avatar_url(self
, size
=None):
974 url
= "https://people.ipfire.org/users/%s.jpg" % self
.uid
977 url
+= "?size=%s" % size
981 def get_avatar(self
, size
=None):
982 photo
= self
._get
_bytes
("jpegPhoto")
984 # Exit if no avatar is available
988 # Return the raw image if no size was requested
992 # Try to retrieve something from the cache
993 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
997 # Generate a new thumbnail
998 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1000 # Save to cache for 15m
1001 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1005 def upload_avatar(self
, avatar
):
1006 self
._set
("jpegPhoto", avatar
)
1008 # Delete cached avatar status
1009 self
.memcache
.delete("accounts:%s:has-avatar" % self
.uid
)
1012 class StopForumSpam(Object
):
1013 def init(self
, uid
, email
, address
):
1014 self
.uid
, self
.email
, self
.address
= uid
, email
, address
1016 @tornado.gen
.coroutine
1017 def send_request(self
, **kwargs
):
1021 arguments
.update(kwargs
)
1024 request
= tornado
.httpclient
.HTTPRequest(
1025 "https://api.stopforumspam.org/api", method
="POST")
1026 request
.body
= urllib
.parse
.urlencode(arguments
)
1029 response
= yield self
.backend
.http_client
.fetch(request
)
1031 # Decode the JSON response
1032 return json
.loads(response
.body
.decode())
1034 @tornado.gen
.coroutine
1035 def check_address(self
):
1036 response
= yield self
.send_request(ip
=self
.address
)
1039 confidence
= response
["ip"]["confidence"]
1043 logging
.debug("Confidence for %s: %s" % (self
.address
, confidence
))
1047 @tornado.gen
.coroutine
1048 def check_username(self
):
1049 response
= yield self
.send_request(username
=self
.uid
)
1052 confidence
= response
["username"]["confidence"]
1056 logging
.debug("Confidence for %s: %s" % (self
.uid
, confidence
))
1060 @tornado.gen
.coroutine
1061 def check_email(self
):
1062 response
= yield self
.send_request(email
=self
.email
)
1065 confidence
= response
["email"]["confidence"]
1069 logging
.debug("Confidence for %s: %s" % (self
.email
, confidence
))
1073 @tornado.gen
.coroutine
1074 def check(self
, threshold
=95):
1076 This function tries to detect if we have a spammer.
1078 To honour the privacy of our users, we only send the IP
1079 address and username and if those are on the database, we
1080 will send the email address as well.
1082 confidences
= yield [self
.check_address(), self
.check_username()]
1084 if any((c
< threshold
for c
in confidences
)):
1085 confidences
+= yield [self
.check_email()]
1087 # Build a score based on the lowest confidence
1088 return 100 - min(confidences
)
1092 class Groups(Object
):
1094 def search_base(self
):
1095 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1098 class Group(Object
):
1099 def init(self
, gid
):
1103 if self
.description
:
1104 return "<%s %s (%s)>" % (
1105 self
.__class
__.__name
__,
1110 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1113 return self
.description
or self
.gid
1115 def __eq__(self
, other
):
1116 if isinstance(other
, self
.__class
__):
1117 return self
.gid
== other
.gid
1119 def __lt__(self
, other
):
1120 if isinstance(other
, self
.__class
__):
1121 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1124 def attributes(self
):
1125 attrs
= self
.memcache
.get("groups:%s:attrs" % self
.gid
)
1127 attrs
= self
._get
_attrs
()
1129 # Cache this for 5 mins
1130 self
.memcache
.set("groups:%s:attrs" % self
.gid
, attrs
, 300)
1134 def _get_attrs(self
):
1135 res
= self
.backend
.accounts
._query
(
1136 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % self
.gid
,
1137 search_base
=self
.backend
.groups
.search_base
, limit
=1)
1139 for dn
, attrs
in res
:
1143 def description(self
):
1145 description
= self
.attributes
["description"][0]
1149 return description
.decode()
1152 if __name__
== "__main__":