]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
2c9c47ec54fb32ded21e7e8d0d958c026940c671
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
)
377 # Launch drip campaigns
378 for campaign
in ("signup", "christmas"):
379 self
.backend
.campaigns
.launch(campaign
, account
)
383 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
384 cn
= "%s %s" % (first_name
, last_name
)
388 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
389 "mail" : email
.encode(),
393 "sn" : last_name
.encode(),
394 "givenName" : first_name
.encode(),
397 logging
.info("Creating new account: %s: %s" % (uid
, account
))
400 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
402 # Create account on LDAP
403 self
.accounts
._authenticate
()
404 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
407 account
= self
.get_by_dn(dn
)
409 # Optionally set country code
411 account
.country_code
= country_code
418 def create_session(self
, account
, host
):
419 session_id
= util
.random_string(64)
421 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
422 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
424 # Session could not be created
428 logging
.info("Created session %s for %s which expires %s" \
429 % (res
.session_id
, account
, res
.time_expires
))
430 return res
.session_id
, res
.time_expires
432 def destroy_session(self
, session_id
, host
):
433 logging
.info("Destroying session %s" % session_id
)
435 self
.db
.execute("DELETE FROM sessions \
436 WHERE session_id = %s AND host = %s", session_id
, host
)
438 def get_by_session(self
, session_id
, host
):
439 logging
.debug("Looking up session %s" % session_id
)
441 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
442 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
445 # Session does not exist or has expired
449 # Update the session expiration time
450 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
451 WHERE session_id = %s AND host = %s", session_id
, host
)
453 return self
.get_by_uid(res
.uid
)
456 # Cleanup expired sessions
457 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
459 # Cleanup expired account activations
460 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
462 # Cleanup expired account password resets
463 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
467 def decode_discourse_payload(self
, payload
, signature
):
469 calculated_signature
= self
.sign_discourse_payload(payload
)
471 if not hmac
.compare_digest(signature
, calculated_signature
):
472 raise ValueError("Invalid signature: %s" % signature
)
474 # Decode the query string
475 qs
= base64
.b64decode(payload
).decode()
477 # Parse the query string
479 for key
, val
in urllib
.parse
.parse_qsl(qs
):
484 def encode_discourse_payload(self
, **args
):
485 # Encode the arguments into an URL-formatted string
486 qs
= urllib
.parse
.urlencode(args
).encode()
489 return base64
.b64encode(qs
).decode()
491 def sign_discourse_payload(self
, payload
, secret
=None):
493 secret
= self
.settings
.get("discourse_sso_secret")
495 # Calculate a HMAC using SHA256
496 h
= hmac
.new(secret
.encode(),
497 msg
=payload
.encode(), digestmod
="sha256")
502 class Account(LDAPObject
):
510 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
512 def __lt__(self
, other
):
513 if isinstance(other
, self
.__class
__):
514 return self
.name
< other
.name
516 def _clear_cache(self
):
517 # Delete cached attributes
518 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
521 def kerberos_attributes(self
):
522 res
= self
.backend
.accounts
._query
(
523 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
525 "krbLastSuccessfulAuth",
526 "krbLastPasswordChange",
528 "krbLoginFailedCount",
531 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
533 for dn
, attrs
in res
:
534 return { key
: attrs
[key
][0] for key
in attrs
}
540 return datetime
.datetime
.strptime(s
.decode(), "%Y%m%d%H%M%SZ")
543 def last_successful_authentication(self
):
545 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
549 return self
._parse
_date
(s
)
552 def last_failed_authentication(self
):
554 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
558 return self
._parse
_date
(s
)
561 def failed_login_count(self
):
563 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
572 def passwd(self
, password
):
576 # The new password must have a score of 3 or better
577 quality
= self
.check_password_quality(password
)
578 if quality
["score"] < 3:
579 raise ValueError("Password too weak")
581 self
.accounts
._authenticate
()
582 self
.ldap
.passwd_s(self
.dn
, None, password
)
584 def check_password(self
, password
):
586 Bind to the server with given credentials and return
587 true if password is corrent and false if not.
589 Raises exceptions from the server on any other errors.
594 logging
.debug("Checking credentials for %s" % self
.dn
)
596 # Create a new LDAP connection
597 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
598 conn
= ldap
.initialize(ldap_uri
)
601 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
602 except ldap
.INVALID_CREDENTIALS
:
603 logging
.debug("Account credentials are invalid for %s" % self
)
606 logging
.info("Successfully authenticated %s" % self
)
610 def check_password_quality(self
, password
):
612 Passwords are passed through zxcvbn to make sure
613 that they are strong enough.
615 return zxcvbn
.zxcvbn(password
, user_inputs
=(
616 self
.first_name
, self
.last_name
,
619 def request_password_reset(self
, address
=None):
620 reset_code
= util
.random_string(64)
622 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
623 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
625 # Send a password reset email
626 self
.backend
.messages
.send_template("auth/messages/password-reset",
627 recipients
=[self
.email
], priority
=100, account
=self
, reset_code
=reset_code
)
629 def reset_password(self
, reset_code
, new_password
):
630 # Delete the reset token
631 res
= self
.db
.query("DELETE FROM account_password_resets \
632 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
633 RETURNING *", self
.uid
, reset_code
)
635 # The reset code was invalid
637 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
639 # Perform password change
640 return self
.passwd(new_password
)
643 return self
.is_member_of_group("sudo")
646 return self
.is_member_of_group("staff")
648 def is_moderator(self
):
649 return self
.is_member_of_group("moderators")
652 return "posixAccount" in self
.classes
655 return "postfixMailUser" in self
.classes
658 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
660 def can_be_managed_by(self
, account
):
662 Returns True if account is allowed to manage this account
664 # Admins can manage all accounts
665 if account
.is_admin():
668 # Users can manage themselves
669 return self
== account
673 return self
._get
_strings
("objectClass")
677 return self
._get
_string
("uid")
681 return self
._get
_string
("cn")
685 def get_nickname(self
):
686 return self
._get
_string
("displayName")
688 def set_nickname(self
, nickname
):
689 self
._set
_string
("displayName", nickname
)
691 nickname
= property(get_nickname
, set_nickname
)
695 def get_first_name(self
):
696 return self
._get
_string
("givenName")
698 def set_first_name(self
, first_name
):
699 self
._set
_string
("givenName", first_name
)
702 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
704 first_name
= property(get_first_name
, set_first_name
)
708 def get_last_name(self
):
709 return self
._get
_string
("sn")
711 def set_last_name(self
, last_name
):
712 self
._set
_string
("sn", last_name
)
715 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
717 last_name
= property(get_last_name
, set_last_name
)
721 return self
.backend
.groups
._get
_groups
("(| \
722 (&(objectClass=groupOfNames)(member=%s)) \
723 (&(objectClass=posixGroup)(memberUid=%s)) \
724 )" % (self
.dn
, self
.uid
))
726 def is_member_of_group(self
, gid
):
728 Returns True if this account is a member of this group
730 return gid
in (g
.gid
for g
in self
.groups
)
732 # Created/Modified at
735 def created_at(self
):
736 return self
._get
_timestamp
("createTimestamp")
739 def modified_at(self
):
740 return self
._get
_timestamp
("modifyTimestamp")
749 address
+= self
.street
.splitlines()
751 if self
.postal_code
and self
.city
:
752 if self
.country_code
in ("AT", "DE"):
753 address
.append("%s %s" % (self
.postal_code
, self
.city
))
755 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
757 address
.append(self
.city
or self
.postal_code
)
759 if self
.country_name
:
760 address
.append(self
.country_name
)
764 def get_street(self
):
765 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
767 def set_street(self
, street
):
768 self
._set
_string
("street", street
)
770 street
= property(get_street
, set_street
)
773 return self
._get
_string
("l") or ""
775 def set_city(self
, city
):
776 self
._set
_string
("l", city
)
778 city
= property(get_city
, set_city
)
780 def get_postal_code(self
):
781 return self
._get
_string
("postalCode") or ""
783 def set_postal_code(self
, postal_code
):
784 self
._set
_string
("postalCode", postal_code
)
786 postal_code
= property(get_postal_code
, set_postal_code
)
788 # XXX This should be c
789 def get_country_code(self
):
790 return self
._get
_string
("st")
792 def set_country_code(self
, country_code
):
793 self
._set
_string
("st", country_code
)
795 country_code
= property(get_country_code
, set_country_code
)
798 def country_name(self
):
799 if self
.country_code
:
800 return countries
.get_name(self
.country_code
)
804 return self
._get
_string
("mail")
808 return "%s <%s>" % (self
, self
.email
)
810 # Mail Routing Address
812 def get_mail_routing_address(self
):
813 return self
._get
_string
("mailRoutingAddress", None)
815 def set_mail_routing_address(self
, address
):
816 self
._set
_string
("mailRoutingAddress", address
or None)
818 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
822 if "sipUser" in self
.classes
:
823 return self
._get
_string
("sipAuthenticationUser")
825 if "sipRoutingObject" in self
.classes
:
826 return self
._get
_string
("sipLocalAddress")
829 def sip_password(self
):
830 return self
._get
_string
("sipPassword")
833 def _generate_sip_password():
834 return util
.random_string(8)
838 return "%s@ipfire.org" % self
.sip_id
841 def agent_status(self
):
842 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
844 def uses_sip_forwarding(self
):
845 if self
.sip_routing_address
:
852 def get_sip_routing_address(self
):
853 if "sipRoutingObject" in self
.classes
:
854 return self
._get
_string
("sipRoutingAddress")
856 def set_sip_routing_address(self
, address
):
860 # Don't do anything if nothing has changed
861 if self
.get_sip_routing_address() == address
:
865 # This is no longer a SIP user any more
868 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
869 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
870 (ldap
.MOD_DELETE
, "sipPassword", None),
872 except ldap
.NO_SUCH_ATTRIBUTE
:
875 # Set new routing object
878 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
879 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
880 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
883 # If this is a change, we cannot add this again
884 except ldap
.TYPE_OR_VALUE_EXISTS
:
885 self
._set
_string
("sipRoutingAddress", address
)
889 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
890 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
891 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
893 except ldap
.NO_SUCH_ATTRIBUTE
:
897 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
898 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
899 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
902 # XXX Cache is invalid here
904 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
907 def sip_registrations(self
):
908 sip_registrations
= []
910 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
913 sip_registrations
.append(reg
)
915 return sip_registrations
918 def sip_channels(self
):
919 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
921 def get_cdr(self
, date
=None, limit
=None):
922 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
927 def phone_number(self
):
929 Returns the IPFire phone number
932 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
935 def fax_number(self
):
937 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
939 def get_phone_numbers(self
):
942 for field
in ("telephoneNumber", "homePhone", "mobile"):
943 for number
in self
._get
_phone
_numbers
(field
):
948 def set_phone_numbers(self
, phone_numbers
):
949 # Sort phone numbers by landline and mobile
950 _landline_numbers
= []
953 for number
in phone_numbers
:
955 number
= phonenumbers
.parse(number
, None)
956 except phonenumbers
.phonenumberutil
.NumberParseException
:
959 # Convert to string (in E.164 format)
960 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
962 # Separate mobile numbers
963 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
964 _mobile_numbers
.append(s
)
966 _landline_numbers
.append(s
)
969 self
._set
_strings
("telephoneNumber", _landline_numbers
)
970 self
._set
_strings
("mobile", _mobile_numbers
)
972 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
975 def _all_telephone_numbers(self
):
976 ret
= [ self
.sip_id
, ]
978 if self
.phone_number
:
979 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
982 for number
in self
.phone_numbers
:
983 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
990 def get_description(self
):
991 return self
._get
_string
("description")
993 def set_description(self
, description
):
994 self
._set
_string
("description", description
)
996 description
= property(get_description
, set_description
)
1000 def has_avatar(self
):
1001 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
1002 if has_avatar
is None:
1003 has_avatar
= True if self
.get_avatar() else False
1005 # Cache avatar status for up to 24 hours
1006 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1010 def avatar_url(self
, size
=None):
1011 url
= "https://people.ipfire.org/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1014 url
+= "&size=%s" % size
1018 def get_avatar(self
, size
=None):
1019 photo
= self
._get
_bytes
("jpegPhoto")
1021 # Exit if no avatar is available
1025 # Return the raw image if no size was requested
1029 # Try to retrieve something from the cache
1030 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1034 # Generate a new thumbnail
1035 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1037 # Save to cache for 15m
1038 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1043 def avatar_hash(self
):
1044 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1046 h
= hashlib
.new("md5")
1047 h
.update(self
.get_avatar() or b
"")
1048 hash = h
.hexdigest()[:7]
1050 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1054 def upload_avatar(self
, avatar
):
1055 self
._set
("jpegPhoto", avatar
)
1057 # Delete cached avatar status
1058 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1060 # Delete avatar hash
1061 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1064 class StopForumSpam(Object
):
1065 def init(self
, uid
, email
, address
):
1066 self
.uid
, self
.email
, self
.address
= uid
, email
, address
1068 async def send_request(self
, **kwargs
):
1072 arguments
.update(kwargs
)
1075 request
= tornado
.httpclient
.HTTPRequest(
1076 "https://api.stopforumspam.org/api", method
="POST")
1077 request
.body
= urllib
.parse
.urlencode(arguments
)
1080 response
= await self
.backend
.http_client
.fetch(request
)
1082 # Decode the JSON response
1083 return json
.loads(response
.body
.decode())
1085 async def check_address(self
):
1086 response
= await self
.send_request(ip
=self
.address
)
1089 confidence
= response
["ip"]["confidence"]
1093 logging
.debug("Confidence for %s: %s" % (self
.address
, confidence
))
1097 async def check_username(self
):
1098 response
= await self
.send_request(username
=self
.uid
)
1101 confidence
= response
["username"]["confidence"]
1105 logging
.debug("Confidence for %s: %s" % (self
.uid
, confidence
))
1109 async def check_email(self
):
1110 response
= await self
.send_request(email
=self
.email
)
1113 confidence
= response
["email"]["confidence"]
1117 logging
.debug("Confidence for %s: %s" % (self
.email
, confidence
))
1121 async def check(self
, threshold
=95):
1123 This function tries to detect if we have a spammer.
1125 To honour the privacy of our users, we only send the IP
1126 address and username and if those are on the database, we
1127 will send the email address as well.
1129 confidences
= [await self
.check_address(), await self
.check_username()]
1131 if any((c
< threshold
for c
in confidences
)):
1132 confidences
.append(await self
.check_email())
1134 # Build a score based on the lowest confidence
1135 return 100 - min(confidences
)
1138 class Groups(Object
):
1140 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1141 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1143 # Everyone is a member of people
1144 "cn=people,ou=Group,dc=ipfire,dc=org",
1148 def search_base(self
):
1149 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1151 def _query(self
, *args
, **kwargs
):
1153 "search_base" : self
.backend
.groups
.search_base
,
1156 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1159 groups
= self
.get_all()
1163 def _get_groups(self
, query
, **kwargs
):
1164 res
= self
._query
(query
, **kwargs
)
1167 for dn
, attrs
in res
:
1168 # Skip any hidden groups
1169 if dn
in self
.hidden_groups
:
1172 g
= Group(self
.backend
, dn
, attrs
)
1175 return sorted(groups
)
1177 def _get_group(self
, query
, **kwargs
):
1182 groups
= self
._get
_groups
(query
, **kwargs
)
1187 return self
._get
_groups
(
1188 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1191 def get_by_gid(self
, gid
):
1192 return self
._get
_group
(
1193 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1197 class Group(LDAPObject
):
1199 if self
.description
:
1200 return "<%s %s (%s)>" % (
1201 self
.__class
__.__name
__,
1206 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1209 return self
.description
or self
.gid
1211 def __lt__(self
, other
):
1212 if isinstance(other
, self
.__class
__):
1213 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1220 Returns the number of members in this group
1224 for attr
in ("member", "memberUid"):
1225 a
= self
.attributes
.get(attr
, None)
1232 return iter(self
.members
)
1236 return self
._get
_string
("cn")
1239 def description(self
):
1240 return self
._get
_string
("description")
1244 return self
._get
_string
("mail")
1250 # Get all members by DN
1251 for dn
in self
._get
_strings
("member"):
1252 member
= self
.backend
.accounts
.get_by_dn(dn
)
1254 members
.append(member
)
1256 # Get all members by UID
1257 for uid
in self
._get
_strings
("memberUid"):
1258 member
= self
.backend
.accounts
.get_by_uid(uid
)
1260 members
.append(member
)
1262 return sorted(members
)
1264 def add_member(self
, account
):
1266 Adds a member to this group
1268 if "posixGroup" in self
.objectclasses
:
1269 self
._add
_string
("memberUid", account
.uid
)
1271 self
._add
_string
("member", account
.dn
)
1273 # Append to cached list of members
1274 self
.members
.append(account
)
1277 if __name__
== "__main__":