]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
46a5061bf3779f81709ce57baa88529ff8640b31
16 import tornado
.httpclient
21 from . import countries
23 from .decorators
import *
24 from .misc
import Object
26 # Set the client keytab name
27 os
.environ
["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
29 class LDAPObject(Object
):
30 def init(self
, dn
, attrs
=None):
33 self
.attributes
= attrs
or {}
35 def __eq__(self
, other
):
36 if isinstance(other
, self
.__class
__):
37 return self
.dn
== other
.dn
41 return self
.accounts
.ldap
43 def _exists(self
, key
):
52 for value
in self
.attributes
.get(key
, []):
55 def _get_bytes(self
, key
, default
=None):
56 for value
in self
._get
(key
):
61 def _get_strings(self
, key
):
62 for value
in self
._get
(key
):
65 def _get_string(self
, key
, default
=None):
66 for value
in self
._get
_strings
(key
):
71 def _get_phone_numbers(self
, key
):
72 for value
in self
._get
_strings
(key
):
73 yield phonenumbers
.parse(value
, None)
75 def _get_timestamp(self
, key
):
76 value
= self
._get
_string
(key
)
78 # Parse the timestamp value and returns a datetime object
80 return datetime
.datetime
.strptime(value
, "%Y%m%d%H%M%SZ")
82 def _modify(self
, modlist
):
83 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
85 # Authenticate before performing any write operations
86 self
.accounts
._authenticate
()
88 # Run modify operation
89 self
.ldap
.modify_s(self
.dn
, modlist
)
94 def _clear_cache(self
):
100 def _set(self
, key
, values
):
101 current
= self
._get
(key
)
103 # Don't do anything if nothing has changed
104 if list(current
) == values
:
107 # Remove all old values and add all new ones
110 if self
._exists
(key
):
111 modlist
.append((ldap
.MOD_DELETE
, key
, None))
115 modlist
.append((ldap
.MOD_ADD
, key
, values
))
117 # Run modify operation
118 self
._modify
(modlist
)
121 self
.attributes
.update({ key
: values
})
123 def _set_bytes(self
, key
, values
):
124 return self
._set
(key
, values
)
126 def _set_strings(self
, key
, values
):
127 return self
._set
(key
, [e
.encode() for e
in values
if e
])
129 def _set_string(self
, key
, value
):
130 return self
._set
_strings
(key
, [value
,])
132 def _add(self
, key
, values
):
134 (ldap
.MOD_ADD
, key
, values
),
137 self
._modify
(modlist
)
139 def _add_strings(self
, key
, values
):
140 return self
._add
(key
, [e
.encode() for e
in values
])
142 def _add_string(self
, key
, value
):
143 return self
._add
_strings
(key
, [value
,])
145 def _delete(self
, key
, values
):
147 (ldap
.MOD_DELETE
, key
, values
),
150 self
._modify
(modlist
)
152 def _delete_strings(self
, key
, values
):
153 return self
._delete
(key
, [e
.encode() for e
in values
])
155 def _delete_string(self
, key
, value
):
156 return self
._delete
_strings
(key
, [value
,])
159 def objectclasses(self
):
160 return self
._get
_strings
("objectClass")
163 class Accounts(Object
):
165 self
.search_base
= self
.settings
.get("ldap_search_base")
168 # Only return developers (group with ID 1000)
169 accounts
= self
._search
("(&(objectClass=posixAccount)(gidNumber=1000))")
171 return iter(sorted(accounts
))
175 # Connect to LDAP server
176 ldap_uri
= self
.settings
.get("ldap_uri")
178 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
180 # Connect to the LDAP server
181 return ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
182 retry_max
=10, retry_delay
=3)
184 def _authenticate(self
):
185 # Authenticate against LDAP server using Kerberos
186 self
.ldap
.sasl_gssapi_bind_s()
189 logging
.info("Testing LDAP connection...")
193 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
195 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
196 logging
.debug("Performing LDAP query (%s): %s" \
197 % (search_base
or self
.search_base
, query
))
201 results
= self
.ldap
.search_ext_s(search_base
or self
.search_base
,
202 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
)
204 # Log time it took to perform the query
205 logging
.debug("Query took %.2fms" % ((time
.time() - t
) * 1000.0))
209 def _search(self
, query
, attrlist
=None, limit
=0):
211 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
212 account
= self
.get_by_dn(dn
)
213 accounts
.append(account
)
217 def _get_attrs(self
, dn
):
219 Fetches all attributes for the given distinguished name
221 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
222 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
224 for dn
, attrs
in results
:
227 def get_by_dn(self
, dn
):
228 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
230 attrs
= self
._get
_attrs
(dn
)
233 # Cache all attributes for 5 min
234 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
236 return Account(self
.backend
, dn
, attrs
)
238 def get_created_after(self
, ts
):
239 t
= ts
.strftime("%Y%m%d%H%M%SZ")
241 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % t
)
243 def search(self
, query
):
244 accounts
= self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)(displayName=*%s*)(mail=*%s*)))" \
245 % (query
, query
, query
, query
))
247 return sorted(accounts
)
249 def _search_one(self
, query
):
250 results
= self
._search
(query
, limit
=1)
252 for result
in results
:
255 def uid_is_valid(self
, uid
):
256 # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
257 m
= re
.match(r
"^[a-z_][a-z0-9_-]{3,31}$", uid
)
263 def uid_exists(self
, uid
):
264 if self
.get_by_uid(uid
):
267 res
= self
.db
.get("SELECT 1 FROM account_activations \
268 WHERE uid = %s AND expires_at > NOW()", uid
)
273 # Account with uid does not exist, yet
276 def get_by_uid(self
, uid
):
277 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
279 def get_by_mail(self
, mail
):
280 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
282 def find_account(self
, s
):
283 account
= self
.get_by_uid(s
)
287 return self
.get_by_mail(s
)
289 def get_by_sip_id(self
, sip_id
):
293 return self
._search
_one
(
294 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
297 def get_by_phone_number(self
, number
):
301 return self
._search
_one
(
302 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
303 % (number
, number
, number
, number
))
305 async def check_spam(self
, uid
, email
, address
):
306 sfs
= StopForumSpam(self
.backend
, uid
, email
, address
)
309 score
= await sfs
.check()
313 def auth(self
, username
, password
):
315 account
= self
.backend
.accounts
.find_account(username
)
318 if account
and account
.check_password(password
):
323 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
324 # Convert all uids to lowercase
327 # Check if UID is valid
328 if not self
.uid_is_valid(uid
):
329 raise ValueError("UID is invalid: %s" % uid
)
331 # Check if UID is unique
332 if self
.uid_exists(uid
):
333 raise ValueError("UID exists: %s" % uid
)
335 # Generate a random activation code
336 activation_code
= util
.random_string(36)
338 # Create an entry in our database until the user
339 # has activated the account
340 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
341 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
342 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
344 # Send an account activation email
345 self
.backend
.messages
.send_template("auth/messages/register",
346 recipients
=[email
], priority
=100, uid
=uid
,
347 activation_code
=activation_code
, email
=email
,
348 first_name
=first_name
, last_name
=last_name
)
350 def activate(self
, uid
, activation_code
):
351 res
= self
.db
.get("DELETE FROM account_activations \
352 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
353 RETURNING *", uid
, activation_code
)
355 # Return nothing when account was not found
359 # Return the account if it has already been created
360 account
= self
.get_by_uid(uid
)
364 # Create a new account on the LDAP database
365 account
= self
.create(uid
, res
.email
,
366 first_name
=res
.first_name
, last_name
=res
.last_name
,
367 country_code
=res
.country_code
)
369 # Invite newly registered users to newsletter
370 self
.backend
.messages
.send_template(
371 "newsletter/subscribe", address
="%s <%s>" % (account
, account
.email
))
373 # Send email about account registration
374 self
.backend
.messages
.send_template("people/messages/new-account",
375 recipients
=["moderators@ipfire.org"], account
=account
)
379 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
380 cn
= "%s %s" % (first_name
, last_name
)
384 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
385 "mail" : email
.encode(),
389 "sn" : last_name
.encode(),
390 "givenName" : first_name
.encode(),
393 logging
.info("Creating new account: %s: %s" % (uid
, account
))
396 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
398 # Create account on LDAP
399 self
.accounts
._authenticate
()
400 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
403 account
= self
.get_by_dn(dn
)
405 # Optionally set country code
407 account
.country_code
= country_code
414 def create_session(self
, account
, host
):
415 session_id
= util
.random_string(64)
417 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
418 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
420 # Session could not be created
424 logging
.info("Created session %s for %s which expires %s" \
425 % (res
.session_id
, account
, res
.time_expires
))
426 return res
.session_id
, res
.time_expires
428 def destroy_session(self
, session_id
, host
):
429 logging
.info("Destroying session %s" % session_id
)
431 self
.db
.execute("DELETE FROM sessions \
432 WHERE session_id = %s AND host = %s", session_id
, host
)
434 def get_by_session(self
, session_id
, host
):
435 logging
.debug("Looking up session %s" % session_id
)
437 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
438 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
441 # Session does not exist or has expired
445 # Update the session expiration time
446 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
447 WHERE session_id = %s AND host = %s", session_id
, host
)
449 return self
.get_by_uid(res
.uid
)
452 # Cleanup expired sessions
453 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
455 # Cleanup expired account activations
456 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
458 # Cleanup expired account password resets
459 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
463 def decode_discourse_payload(self
, payload
, signature
):
465 calculated_signature
= self
.sign_discourse_payload(payload
)
467 if not hmac
.compare_digest(signature
, calculated_signature
):
468 raise ValueError("Invalid signature: %s" % signature
)
470 # Decode the query string
471 qs
= base64
.b64decode(payload
).decode()
473 # Parse the query string
475 for key
, val
in urllib
.parse
.parse_qsl(qs
):
480 def encode_discourse_payload(self
, **args
):
481 # Encode the arguments into an URL-formatted string
482 qs
= urllib
.parse
.urlencode(args
).encode()
485 return base64
.b64encode(qs
).decode()
487 def sign_discourse_payload(self
, payload
, secret
=None):
489 secret
= self
.settings
.get("discourse_sso_secret")
491 # Calculate a HMAC using SHA256
492 h
= hmac
.new(secret
.encode(),
493 msg
=payload
.encode(), digestmod
="sha256")
498 class Account(LDAPObject
):
506 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
508 def __lt__(self
, other
):
509 if isinstance(other
, self
.__class
__):
510 return self
.name
< other
.name
512 def _clear_cache(self
):
513 # Delete cached attributes
514 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
517 def kerberos_attributes(self
):
518 res
= self
.backend
.accounts
._query
(
519 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
521 "krbLastSuccessfulAuth",
522 "krbLastPasswordChange",
524 "krbLoginFailedCount",
527 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
529 for dn
, attrs
in res
:
530 return { key
: attrs
[key
][0] for key
in attrs
}
536 return datetime
.datetime
.strptime(s
.decode(), "%Y%m%d%H%M%SZ")
539 def last_successful_authentication(self
):
541 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
545 return self
._parse
_date
(s
)
548 def last_failed_authentication(self
):
550 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
554 return self
._parse
_date
(s
)
557 def failed_login_count(self
):
559 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
568 def passwd(self
, password
):
572 # The new password must have a score of 3 or better
573 quality
= self
.check_password_quality(password
)
574 if quality
["score"] < 3:
575 raise ValueError("Password too weak")
577 self
.accounts
._authenticate
()
578 self
.ldap
.passwd_s(self
.dn
, None, password
)
580 def check_password(self
, password
):
582 Bind to the server with given credentials and return
583 true if password is corrent and false if not.
585 Raises exceptions from the server on any other errors.
590 logging
.debug("Checking credentials for %s" % self
.dn
)
592 # Create a new LDAP connection
593 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
594 conn
= ldap
.initialize(ldap_uri
)
597 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
598 except ldap
.INVALID_CREDENTIALS
:
599 logging
.debug("Account credentials are invalid for %s" % self
)
602 logging
.info("Successfully authenticated %s" % self
)
606 def check_password_quality(self
, password
):
608 Passwords are passed through zxcvbn to make sure
609 that they are strong enough.
611 return zxcvbn
.zxcvbn(password
, user_inputs
=(
612 self
.first_name
, self
.last_name
,
615 def request_password_reset(self
, address
=None):
616 reset_code
= util
.random_string(64)
618 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
619 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
621 # Send a password reset email
622 self
.backend
.messages
.send_template("auth/messages/password-reset",
623 recipients
=[self
.email
], priority
=100, account
=self
, reset_code
=reset_code
)
625 def reset_password(self
, reset_code
, new_password
):
626 # Delete the reset token
627 res
= self
.db
.query("DELETE FROM account_password_resets \
628 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
629 RETURNING *", self
.uid
, reset_code
)
631 # The reset code was invalid
633 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
635 # Perform password change
636 return self
.passwd(new_password
)
639 return self
.is_member_of_group("sudo")
642 return self
.is_member_of_group("staff")
644 def is_moderator(self
):
645 return self
.is_member_of_group("moderators")
648 return "posixAccount" in self
.classes
651 return "postfixMailUser" in self
.classes
654 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
656 def can_be_managed_by(self
, account
):
658 Returns True if account is allowed to manage this account
660 # Admins can manage all accounts
661 if account
.is_admin():
664 # Users can manage themselves
665 return self
== account
669 return self
._get
_strings
("objectClass")
673 return self
._get
_string
("uid")
677 return self
._get
_string
("cn")
681 def get_nickname(self
):
682 return self
._get
_string
("displayName")
684 def set_nickname(self
, nickname
):
685 self
._set
_string
("displayName", nickname
)
687 nickname
= property(get_nickname
, set_nickname
)
691 def get_first_name(self
):
692 return self
._get
_string
("givenName")
694 def set_first_name(self
, first_name
):
695 self
._set
_string
("givenName", first_name
)
698 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
700 first_name
= property(get_first_name
, set_first_name
)
704 def get_last_name(self
):
705 return self
._get
_string
("sn")
707 def set_last_name(self
, last_name
):
708 self
._set
_string
("sn", last_name
)
711 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
713 last_name
= property(get_last_name
, set_last_name
)
717 return self
.backend
.groups
._get
_groups
("(| \
718 (&(objectClass=groupOfNames)(member=%s)) \
719 (&(objectClass=posixGroup)(memberUid=%s)) \
720 )" % (self
.dn
, self
.uid
))
722 def is_member_of_group(self
, gid
):
724 Returns True if this account is a member of this group
726 return gid
in (g
.gid
for g
in self
.groups
)
728 # Created/Modified at
731 def created_at(self
):
732 return self
._get
_timestamp
("createTimestamp")
735 def modified_at(self
):
736 return self
._get
_timestamp
("modifyTimestamp")
745 address
+= self
.street
.splitlines()
747 if self
.postal_code
and self
.city
:
748 if self
.country_code
in ("AT", "DE"):
749 address
.append("%s %s" % (self
.postal_code
, self
.city
))
751 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
753 address
.append(self
.city
or self
.postal_code
)
755 if self
.country_name
:
756 address
.append(self
.country_name
)
760 def get_street(self
):
761 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
763 def set_street(self
, street
):
764 self
._set
_string
("street", street
)
766 street
= property(get_street
, set_street
)
769 return self
._get
_string
("l") or ""
771 def set_city(self
, city
):
772 self
._set
_string
("l", city
)
774 city
= property(get_city
, set_city
)
776 def get_postal_code(self
):
777 return self
._get
_string
("postalCode") or ""
779 def set_postal_code(self
, postal_code
):
780 self
._set
_string
("postalCode", postal_code
)
782 postal_code
= property(get_postal_code
, set_postal_code
)
784 # XXX This should be c
785 def get_country_code(self
):
786 return self
._get
_string
("st")
788 def set_country_code(self
, country_code
):
789 self
._set
_string
("st", country_code
)
791 country_code
= property(get_country_code
, set_country_code
)
794 def country_name(self
):
795 if self
.country_code
:
796 return countries
.get_name(self
.country_code
)
800 return self
._get
_string
("mail")
802 # Mail Routing Address
804 def get_mail_routing_address(self
):
805 return self
._get
_string
("mailRoutingAddress", None)
807 def set_mail_routing_address(self
, address
):
808 self
._set
_string
("mailRoutingAddress", address
or None)
810 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
814 if "sipUser" in self
.classes
:
815 return self
._get
_string
("sipAuthenticationUser")
817 if "sipRoutingObject" in self
.classes
:
818 return self
._get
_string
("sipLocalAddress")
821 def sip_password(self
):
822 return self
._get
_string
("sipPassword")
825 def _generate_sip_password():
826 return util
.random_string(8)
830 return "%s@ipfire.org" % self
.sip_id
833 def agent_status(self
):
834 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
836 def uses_sip_forwarding(self
):
837 if self
.sip_routing_address
:
844 def get_sip_routing_address(self
):
845 if "sipRoutingObject" in self
.classes
:
846 return self
._get
_string
("sipRoutingAddress")
848 def set_sip_routing_address(self
, address
):
852 # Don't do anything if nothing has changed
853 if self
.get_sip_routing_address() == address
:
857 # This is no longer a SIP user any more
860 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
861 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
862 (ldap
.MOD_DELETE
, "sipPassword", None),
864 except ldap
.NO_SUCH_ATTRIBUTE
:
867 # Set new routing object
870 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
871 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
872 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
875 # If this is a change, we cannot add this again
876 except ldap
.TYPE_OR_VALUE_EXISTS
:
877 self
._set
_string
("sipRoutingAddress", address
)
881 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
882 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
883 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
885 except ldap
.NO_SUCH_ATTRIBUTE
:
889 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
890 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
891 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
894 # XXX Cache is invalid here
896 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
899 def sip_registrations(self
):
900 sip_registrations
= []
902 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
905 sip_registrations
.append(reg
)
907 return sip_registrations
910 def sip_channels(self
):
911 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
913 def get_cdr(self
, date
=None, limit
=None):
914 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
919 def phone_number(self
):
921 Returns the IPFire phone number
924 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
927 def fax_number(self
):
929 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
931 def get_phone_numbers(self
):
934 for field
in ("telephoneNumber", "homePhone", "mobile"):
935 for number
in self
._get
_phone
_numbers
(field
):
940 def set_phone_numbers(self
, phone_numbers
):
941 # Sort phone numbers by landline and mobile
942 _landline_numbers
= []
945 for number
in phone_numbers
:
947 number
= phonenumbers
.parse(number
, None)
948 except phonenumbers
.phonenumberutil
.NumberParseException
:
951 # Convert to string (in E.164 format)
952 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
954 # Separate mobile numbers
955 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
956 _mobile_numbers
.append(s
)
958 _landline_numbers
.append(s
)
961 self
._set
_strings
("telephoneNumber", _landline_numbers
)
962 self
._set
_strings
("mobile", _mobile_numbers
)
964 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
967 def _all_telephone_numbers(self
):
968 ret
= [ self
.sip_id
, ]
970 if self
.phone_number
:
971 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
974 for number
in self
.phone_numbers
:
975 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
982 def get_description(self
):
983 return self
._get
_string
("description")
985 def set_description(self
, description
):
986 self
._set
_string
("description", description
)
988 description
= property(get_description
, set_description
)
992 def has_avatar(self
):
993 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
994 if has_avatar
is None:
995 has_avatar
= True if self
.get_avatar() else False
997 # Cache avatar status for up to 24 hours
998 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1002 def avatar_url(self
, size
=None):
1003 url
= "https://people.ipfire.org/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1006 url
+= "&size=%s" % size
1010 def get_avatar(self
, size
=None):
1011 photo
= self
._get
_bytes
("jpegPhoto")
1013 # Exit if no avatar is available
1017 # Return the raw image if no size was requested
1021 # Try to retrieve something from the cache
1022 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1026 # Generate a new thumbnail
1027 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1029 # Save to cache for 15m
1030 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1035 def avatar_hash(self
):
1036 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1038 h
= hashlib
.new("md5")
1039 h
.update(self
.get_avatar() or b
"")
1040 hash = h
.hexdigest()[:7]
1042 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1046 def upload_avatar(self
, avatar
):
1047 self
._set
("jpegPhoto", avatar
)
1049 # Delete cached avatar status
1050 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1052 # Delete avatar hash
1053 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1056 class StopForumSpam(Object
):
1057 def init(self
, uid
, email
, address
):
1058 self
.uid
, self
.email
, self
.address
= uid
, email
, address
1060 async def send_request(self
, **kwargs
):
1064 arguments
.update(kwargs
)
1067 request
= tornado
.httpclient
.HTTPRequest(
1068 "https://api.stopforumspam.org/api", method
="POST")
1069 request
.body
= urllib
.parse
.urlencode(arguments
)
1072 response
= await self
.backend
.http_client
.fetch(request
)
1074 # Decode the JSON response
1075 return json
.loads(response
.body
.decode())
1077 async def check_address(self
):
1078 response
= await self
.send_request(ip
=self
.address
)
1081 confidence
= response
["ip"]["confidence"]
1085 logging
.debug("Confidence for %s: %s" % (self
.address
, confidence
))
1089 async def check_username(self
):
1090 response
= await self
.send_request(username
=self
.uid
)
1093 confidence
= response
["username"]["confidence"]
1097 logging
.debug("Confidence for %s: %s" % (self
.uid
, confidence
))
1101 async def check_email(self
):
1102 response
= await self
.send_request(email
=self
.email
)
1105 confidence
= response
["email"]["confidence"]
1109 logging
.debug("Confidence for %s: %s" % (self
.email
, confidence
))
1113 async def check(self
, threshold
=95):
1115 This function tries to detect if we have a spammer.
1117 To honour the privacy of our users, we only send the IP
1118 address and username and if those are on the database, we
1119 will send the email address as well.
1121 confidences
= [await self
.check_address(), await self
.check_username()]
1123 if any((c
< threshold
for c
in confidences
)):
1124 confidences
.append(await self
.check_email())
1126 # Build a score based on the lowest confidence
1127 return 100 - min(confidences
)
1130 class Groups(Object
):
1132 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1133 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1135 # Everyone is a member of people
1136 "cn=people,ou=Group,dc=ipfire,dc=org",
1140 def search_base(self
):
1141 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1143 def _query(self
, *args
, **kwargs
):
1145 "search_base" : self
.backend
.groups
.search_base
,
1148 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1151 groups
= self
.get_all()
1155 def _get_groups(self
, query
, **kwargs
):
1156 res
= self
._query
(query
, **kwargs
)
1159 for dn
, attrs
in res
:
1160 # Skip any hidden groups
1161 if dn
in self
.hidden_groups
:
1164 g
= Group(self
.backend
, dn
, attrs
)
1167 return sorted(groups
)
1169 def _get_group(self
, query
, **kwargs
):
1174 groups
= self
._get
_groups
(query
, **kwargs
)
1179 return self
._get
_groups
(
1180 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1183 def get_by_gid(self
, gid
):
1184 return self
._get
_group
(
1185 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1189 class Group(LDAPObject
):
1191 if self
.description
:
1192 return "<%s %s (%s)>" % (
1193 self
.__class
__.__name
__,
1198 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1201 return self
.description
or self
.gid
1203 def __lt__(self
, other
):
1204 if isinstance(other
, self
.__class
__):
1205 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1212 Returns the number of members in this group
1216 for attr
in ("member", "memberUid"):
1217 a
= self
.attributes
.get(attr
, None)
1224 return iter(self
.members
)
1228 return self
._get
_string
("cn")
1231 def description(self
):
1232 return self
._get
_string
("description")
1236 return self
._get
_string
("mail")
1242 # Get all members by DN
1243 for dn
in self
._get
_strings
("member"):
1244 member
= self
.backend
.accounts
.get_by_dn(dn
)
1246 members
.append(member
)
1248 # Get all members by UID
1249 for uid
in self
._get
_strings
("memberUid"):
1250 member
= self
.backend
.accounts
.get_by_uid(uid
)
1252 members
.append(member
)
1254 return sorted(members
)
1256 def add_member(self
, account
):
1258 Adds a member to this group
1260 if "posixGroup" in self
.objectclasses
:
1261 self
._add
_string
("memberUid", account
.uid
)
1263 self
._add
_string
("member", account
.dn
)
1265 # Append to cached list of members
1266 self
.members
.append(account
)
1269 if __name__
== "__main__":