]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
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_exists(self
, uid
):
129 if self
.get_by_uid(uid
):
132 res
= self
.db
.get("SELECT 1 FROM account_activations \
133 WHERE uid = %s AND expires_at > NOW()", uid
)
138 # Account with uid does not exist, yet
141 def get_by_uid(self
, uid
):
142 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
144 def get_by_mail(self
, mail
):
145 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
147 def find_account(self
, s
):
148 account
= self
.get_by_uid(s
)
152 return self
.get_by_mail(s
)
154 def get_by_sip_id(self
, sip_id
):
158 return self
._search
_one
(
159 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
162 def get_by_phone_number(self
, number
):
166 return self
._search
_one
(
167 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
168 % (number
, number
, number
, number
))
170 @tornado.gen
.coroutine
171 def check_spam(self
, uid
, email
, address
):
172 sfs
= StopForumSpam(self
.backend
, uid
, email
, address
)
175 score
= yield sfs
.check()
179 def auth(self
, username
, password
):
181 account
= self
.backend
.accounts
.find_account(username
)
184 if account
and account
.check_password(password
):
189 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
190 # Convert all uids to lowercase
193 # Check if UID is unique
194 if self
.uid_exists(uid
):
195 raise ValueError("UID exists: %s" % uid
)
197 # Generate a random activation code
198 activation_code
= util
.random_string(36)
200 # Create an entry in our database until the user
201 # has activated the account
202 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
203 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
204 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
206 # Send an account activation email
207 self
.backend
.messages
.send_template("auth/messages/register",
208 recipients
=[email
], priority
=100, uid
=uid
,
209 activation_code
=activation_code
, email
=email
,
210 first_name
=first_name
, last_name
=last_name
)
212 def activate(self
, uid
, activation_code
):
213 res
= self
.db
.get("DELETE FROM account_activations \
214 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
215 RETURNING *", uid
, activation_code
)
217 # Return nothing when account was not found
221 # Return the account if it has already been created
222 account
= self
.get_by_uid(uid
)
226 # Create a new account on the LDAP database
227 account
= self
.create(uid
, res
.email
,
228 first_name
=res
.first_name
, last_name
=res
.last_name
,
229 country_code
=res
.country_code
)
231 # Send email about account registration
232 self
.backend
.messages
.send_template("people/messages/new-account",
233 recipients
=["admin@ipfire.org"], account
=account
)
237 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
238 cn
= "%s %s" % (first_name
, last_name
)
242 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
243 "mail" : email
.encode(),
247 "sn" : last_name
.encode(),
248 "givenName" : first_name
.encode(),
251 logging
.info("Creating new account: %s: %s" % (uid
, account
))
254 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
256 # Create account on LDAP
257 self
.accounts
._authenticate
()
258 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
261 account
= self
.get_by_dn(dn
)
263 # Optionally set country code
265 account
.country_code
= country_code
272 def create_session(self
, account
, host
):
273 res
= self
.db
.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
274 RETURNING session_id, time_expires", host
, account
.uid
)
276 # Session could not be created
280 logging
.info("Created session %s for %s which expires %s" \
281 % (res
.session_id
, account
, res
.time_expires
))
282 return res
.session_id
, res
.time_expires
284 def destroy_session(self
, session_id
, host
):
285 logging
.info("Destroying session %s" % session_id
)
287 self
.db
.execute("DELETE FROM sessions \
288 WHERE session_id = %s AND host = %s", session_id
, host
)
290 def get_by_session(self
, session_id
, host
):
291 logging
.debug("Looking up session %s" % session_id
)
293 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
294 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
297 # Session does not exist or has expired
301 # Update the session expiration time
302 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
303 WHERE session_id = %s AND host = %s", session_id
, host
)
305 return self
.get_by_uid(res
.uid
)
308 # Cleanup expired sessions
309 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
311 # Cleanup expired account activations
312 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
316 def decode_discourse_payload(self
, payload
, signature
):
318 calculated_signature
= self
.sign_discourse_payload(payload
)
320 if not hmac
.compare_digest(signature
, calculated_signature
):
321 raise ValueError("Invalid signature: %s" % signature
)
323 # Decode the query string
324 qs
= base64
.b64decode(payload
).decode()
326 # Parse the query string
328 for key
, val
in urllib
.parse
.parse_qsl(qs
):
333 def encode_discourse_payload(self
, **args
):
334 # Encode the arguments into an URL-formatted string
335 qs
= urllib
.parse
.urlencode(args
).encode()
338 return base64
.b64encode(qs
).decode()
340 def sign_discourse_payload(self
, payload
, secret
=None):
342 secret
= self
.settings
.get("discourse_sso_secret")
344 # Calculate a HMAC using SHA256
345 h
= hmac
.new(secret
.encode(),
346 msg
=payload
.encode(), digestmod
="sha256")
351 class Account(Object
):
352 def __init__(self
, backend
, dn
, attrs
=None):
353 Object
.__init
__(self
, backend
)
356 self
.attributes
= attrs
or {}
365 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
367 def __eq__(self
, other
):
368 if isinstance(other
, self
.__class
__):
369 return self
.dn
== other
.dn
371 def __lt__(self
, other
):
372 if isinstance(other
, self
.__class
__):
373 return self
.name
< other
.name
377 return self
.accounts
.ldap
379 def _exists(self
, key
):
388 for value
in self
.attributes
.get(key
, []):
391 def _get_bytes(self
, key
, default
=None):
392 for value
in self
._get
(key
):
397 def _get_strings(self
, key
):
398 for value
in self
._get
(key
):
401 def _get_string(self
, key
, default
=None):
402 for value
in self
._get
_strings
(key
):
407 def _get_phone_numbers(self
, key
):
408 for value
in self
._get
_strings
(key
):
409 yield phonenumbers
.parse(value
, None)
411 def _get_timestamp(self
, key
):
412 value
= self
._get
_string
(key
)
414 # Parse the timestamp value and returns a datetime object
416 return datetime
.datetime
.strptime(value
, "%Y%m%d%H%M%SZ")
418 def _modify(self
, modlist
):
419 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
421 # Authenticate before performing any write operations
422 self
.accounts
._authenticate
()
424 # Run modify operation
425 self
.ldap
.modify_s(self
.dn
, modlist
)
427 # Delete cached attributes
428 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
430 def _set(self
, key
, values
):
431 current
= self
._get
(key
)
433 # Don't do anything if nothing has changed
434 if list(current
) == values
:
437 # Remove all old values and add all new ones
440 if self
._exists
(key
):
441 modlist
.append((ldap
.MOD_DELETE
, key
, None))
445 modlist
.append((ldap
.MOD_ADD
, key
, values
))
447 # Run modify operation
448 self
._modify
(modlist
)
451 self
.attributes
.update({ key
: values
})
453 def _set_bytes(self
, key
, values
):
454 return self
._set
(key
, values
)
456 def _set_strings(self
, key
, values
):
457 return self
._set
(key
, [e
.encode() for e
in values
if e
])
459 def _set_string(self
, key
, value
):
460 return self
._set
_strings
(key
, [value
,])
462 def _add(self
, key
, values
):
464 (ldap
.MOD_ADD
, key
, values
),
467 self
._modify
(modlist
)
469 def _add_strings(self
, key
, values
):
470 return self
._add
(key
, [e
.encode() for e
in values
])
472 def _add_string(self
, key
, value
):
473 return self
._add
_strings
(key
, [value
,])
475 def _delete(self
, key
, values
):
477 (ldap
.MOD_DELETE
, key
, values
),
480 self
._modify
(modlist
)
482 def _delete_strings(self
, key
, values
):
483 return self
._delete
(key
, [e
.encode() for e
in values
])
485 def _delete_string(self
, key
, value
):
486 return self
._delete
_strings
(key
, [value
,])
488 def passwd(self
, password
):
492 # The new password must have a score of 3 or better
493 quality
= self
.check_password_quality(password
)
494 if quality
["score"] < 3:
495 raise ValueError("Password too weak")
497 self
.accounts
._authenticate
()
498 self
.ldap
.passwd_s(self
.dn
, None, password
)
500 def check_password(self
, password
):
502 Bind to the server with given credentials and return
503 true if password is corrent and false if not.
505 Raises exceptions from the server on any other errors.
510 logging
.debug("Checking credentials for %s" % self
.dn
)
512 # Create a new LDAP connection
513 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
514 conn
= ldap
.initialize(ldap_uri
)
517 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
518 except ldap
.INVALID_CREDENTIALS
:
519 logging
.debug("Account credentials are invalid for %s" % self
)
522 logging
.info("Successfully authenticated %s" % self
)
526 def check_password_quality(self
, password
):
528 Passwords are passed through zxcvbn to make sure
529 that they are strong enough.
531 return zxcvbn
.zxcvbn(password
, user_inputs
=(
532 self
.first_name
, self
.last_name
,
536 return "sudo" in self
.groups
539 return "staff" in self
.groups
542 return "posixAccount" in self
.classes
545 return "postfixMailUser" in self
.classes
548 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
550 def can_be_managed_by(self
, account
):
552 Returns True if account is allowed to manage this account
554 # Admins can manage all accounts
555 if account
.is_admin():
558 # Users can manage themselves
559 return self
== account
563 return self
._get
_strings
("objectClass")
567 return self
._get
_string
("uid")
571 return self
._get
_string
("cn")
575 def get_nickname(self
):
576 return self
._get
_string
("displayName")
578 def set_nickname(self
, nickname
):
579 self
._set
_string
("displayName", nickname
)
581 nickname
= property(get_nickname
, set_nickname
)
585 def get_first_name(self
):
586 return self
._get
_string
("givenName")
588 def set_first_name(self
, first_name
):
589 self
._set
_string
("givenName", first_name
)
592 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
594 first_name
= property(get_first_name
, set_first_name
)
598 def get_last_name(self
):
599 return self
._get
_string
("sn")
601 def set_last_name(self
, last_name
):
602 self
._set
_string
("sn", last_name
)
605 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
607 last_name
= property(get_last_name
, set_last_name
)
611 groups
= self
.memcache
.get("accounts:%s:groups" % self
.dn
)
615 # Fetch groups from LDAP
616 groups
= self
._get
_groups
()
618 # Cache groups for 5 min
619 self
.memcache
.set("accounts:%s:groups" % self
.dn
, groups
, 300)
623 def _get_groups(self
):
626 res
= self
.accounts
._query
("(&(objectClass=posixGroup) \
627 (memberUid=%s))" % self
.uid
, ["cn"])
629 for dn
, attrs
in res
:
630 cns
= attrs
.get("cn")
632 groups
.append(cns
[0].decode())
636 # Created/Modified at
639 def created_at(self
):
640 return self
._get
_timestamp
("createTimestamp")
643 def modified_at(self
):
644 return self
._get
_timestamp
("modifyTimestamp")
653 address
+= self
.street
.splitlines()
655 if self
.postal_code
and self
.city
:
656 if self
.country_code
in ("AT", "DE"):
657 address
.append("%s %s" % (self
.postal_code
, self
.city
))
659 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
661 address
.append(self
.city
or self
.postal_code
)
663 if self
.country_name
:
664 address
.append(self
.country_name
)
668 def get_street(self
):
669 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
671 def set_street(self
, street
):
672 self
._set
_string
("street", street
)
674 street
= property(get_street
, set_street
)
677 return self
._get
_string
("l") or ""
679 def set_city(self
, city
):
680 self
._set
_string
("l", city
)
682 city
= property(get_city
, set_city
)
684 def get_postal_code(self
):
685 return self
._get
_string
("postalCode") or ""
687 def set_postal_code(self
, postal_code
):
688 self
._set
_string
("postalCode", postal_code
)
690 postal_code
= property(get_postal_code
, set_postal_code
)
692 # XXX This should be c
693 def get_country_code(self
):
694 return self
._get
_string
("st")
696 def set_country_code(self
, country_code
):
697 self
._set
_string
("st", country_code
)
699 country_code
= property(get_country_code
, set_country_code
)
702 def country_name(self
):
703 if self
.country_code
:
704 return countries
.get_name(self
.country_code
)
708 return self
._get
_string
("mail")
710 # Mail Routing Address
712 def get_mail_routing_address(self
):
713 return self
._get
_string
("mailRoutingAddress", None)
715 def set_mail_routing_address(self
, address
):
716 self
._set
_string
("mailRoutingAddress", address
or None)
718 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
722 if "sipUser" in self
.classes
:
723 return self
._get
_string
("sipAuthenticationUser")
725 if "sipRoutingObject" in self
.classes
:
726 return self
._get
_string
("sipLocalAddress")
729 def sip_password(self
):
730 return self
._get
_string
("sipPassword")
733 def _generate_sip_password():
734 return util
.random_string(8)
738 return "%s@ipfire.org" % self
.sip_id
740 def uses_sip_forwarding(self
):
741 if self
.sip_routing_address
:
748 def get_sip_routing_address(self
):
749 if "sipRoutingObject" in self
.classes
:
750 return self
._get
_string
("sipRoutingAddress")
752 def set_sip_routing_address(self
, address
):
756 # Don't do anything if nothing has changed
757 if self
.get_sip_routing_address() == address
:
761 # This is no longer a SIP user any more
764 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
765 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
766 (ldap
.MOD_DELETE
, "sipPassword", None),
768 except ldap
.NO_SUCH_ATTRIBUTE
:
771 # Set new routing object
774 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
775 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
776 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
779 # If this is a change, we cannot add this again
780 except ldap
.TYPE_OR_VALUE_EXISTS
:
781 self
._set
_string
("sipRoutingAddress", address
)
785 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
786 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
787 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
789 except ldap
.NO_SUCH_ATTRIBUTE
:
793 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
794 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
795 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
798 # XXX Cache is invalid here
800 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
803 def sip_registrations(self
):
804 sip_registrations
= []
806 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
809 sip_registrations
.append(reg
)
811 return sip_registrations
814 def sip_channels(self
):
815 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
817 def get_cdr(self
, date
=None, limit
=None):
818 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
823 def phone_number(self
):
825 Returns the IPFire phone number
828 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
831 def fax_number(self
):
833 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
835 def get_phone_numbers(self
):
838 for field
in ("telephoneNumber", "homePhone", "mobile"):
839 for number
in self
._get
_phone
_numbers
(field
):
844 def set_phone_numbers(self
, phone_numbers
):
845 # Sort phone numbers by landline and mobile
846 _landline_numbers
= []
849 for number
in phone_numbers
:
851 number
= phonenumbers
.parse(number
, None)
852 except phonenumbers
.phonenumberutil
.NumberParseException
:
855 # Convert to string (in E.164 format)
856 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
858 # Separate mobile numbers
859 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
860 _mobile_numbers
.append(s
)
862 _landline_numbers
.append(s
)
865 self
._set
_strings
("telephoneNumber", _landline_numbers
)
866 self
._set
_strings
("mobile", _mobile_numbers
)
868 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
871 def _all_telephone_numbers(self
):
872 ret
= [ self
.sip_id
, ]
874 if self
.phone_number
:
875 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
878 for number
in self
.phone_numbers
:
879 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
884 def avatar_url(self
, size
=None):
885 url
= "https://people.ipfire.org/users/%s.jpg" % self
.uid
888 url
+= "?size=%s" % size
892 def get_avatar(self
, size
=None):
893 photo
= self
._get
_bytes
("jpegPhoto")
895 # Exit if no avatar is available
899 # Return the raw image if no size was requested
903 # Try to retrieve something from the cache
904 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
908 # Generate a new thumbnail
909 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
911 # Save to cache for 15m
912 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
916 def upload_avatar(self
, avatar
):
917 self
._set
("jpegPhoto", avatar
)
925 for key
in self
._get
_strings
("sshPublicKey"):
926 s
= sshpubkeys
.SSHKey()
930 except (sshpubkeys
.InvalidKeyError
, NotImplementedError) as e
:
931 logging
.warning("Could not parse SSH key %s: %s" % (key
, e
))
938 def get_ssh_key_by_hash_sha256(self
, hash_sha256
):
939 for key
in self
.ssh_keys
:
940 if not key
.hash_sha256() == hash_sha256
:
945 def add_ssh_key(self
, key
):
946 k
= sshpubkeys
.SSHKey()
948 # Try to parse the key
951 # Check for types and sufficient sizes
952 if k
.key_type
== b
"ssh-rsa":
954 raise sshpubkeys
.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
956 elif k
.key_type
== b
"ssh-dss":
957 raise sshpubkeys
.InvalidKeyError("DSA keys are not supported")
959 # Ignore any duplicates
960 if key
in (k
.keydata
for k
in self
.ssh_keys
):
961 logging
.debug("SSH Key has already been added for %s: %s" % (self
, key
))
964 # Prepare transaction
967 # Add object class if user is not in it, yet
968 if not "ldapPublicKey" in self
.classes
:
969 modlist
.append((ldap
.MOD_ADD
, "objectClass", b
"ldapPublicKey"))
972 modlist
.append((ldap
.MOD_ADD
, "sshPublicKey", key
.encode()))
975 self
._modify
(modlist
)
978 self
.ssh_keys
.append(k
)
980 def delete_ssh_key(self
, key
):
981 if not key
in (k
.keydata
for k
in self
.ssh_keys
):
984 # Delete key from LDAP
985 if len(self
.ssh_keys
) > 1:
986 self
._delete
_string
("sshPublicKey", key
)
989 (ldap
.MOD_DELETE
, "objectClass", b
"ldapPublicKey"),
990 (ldap
.MOD_DELETE
, "sshPublicKey", key
.encode()),
994 class StopForumSpam(Object
):
995 def init(self
, uid
, email
, address
):
996 self
.uid
, self
.email
, self
.address
= uid
, email
, address
998 @tornado.gen
.coroutine
999 def send_request(self
, **kwargs
):
1003 arguments
.update(kwargs
)
1006 request
= tornado
.httpclient
.HTTPRequest(
1007 "https://api.stopforumspam.org/api", method
="POST")
1008 request
.body
= urllib
.parse
.urlencode(arguments
)
1011 response
= yield self
.backend
.http_client
.fetch(request
)
1013 # Decode the JSON response
1014 return json
.loads(response
.body
.decode())
1016 @tornado.gen
.coroutine
1017 def check_address(self
):
1018 response
= yield self
.send_request(ip
=self
.address
)
1021 confidence
= response
["ip"]["confidence"]
1025 logging
.debug("Confidence for %s: %s" % (self
.address
, confidence
))
1029 @tornado.gen
.coroutine
1030 def check_username(self
):
1031 response
= yield self
.send_request(username
=self
.uid
)
1034 confidence
= response
["username"]["confidence"]
1038 logging
.debug("Confidence for %s: %s" % (self
.uid
, confidence
))
1042 @tornado.gen
.coroutine
1043 def check_email(self
):
1044 response
= yield self
.send_request(email
=self
.email
)
1047 confidence
= response
["email"]["confidence"]
1051 logging
.debug("Confidence for %s: %s" % (self
.email
, confidence
))
1055 @tornado.gen
.coroutine
1056 def check(self
, threshold
=95):
1058 This function tries to detect if we have a spammer.
1060 To honour the privacy of our users, we only send the IP
1061 address and username and if those are on the database, we
1062 will send the email address as well.
1064 confidences
= yield [self
.check_address(), self
.check_username()]
1066 if any((c
< threshold
for c
in confidences
)):
1067 confidences
+= yield [self
.check_email()]
1069 # Build a score based on the lowest confidence
1070 return 100 - min(confidences
)
1073 if __name__
== "__main__":