18 import tornado
.httpclient
23 from . import countries
25 from .decorators
import *
26 from .misc
import Object
30 # Set the client keytab name
31 os
.environ
["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
33 class LDAPObject(Object
):
34 def init(self
, dn
, attrs
=None):
37 self
.attributes
= attrs
or {}
39 def __eq__(self
, other
):
40 if isinstance(other
, self
.__class
__):
41 return self
.dn
== other
.dn
45 return self
.accounts
.ldap
47 def _exists(self
, key
):
56 for value
in self
.attributes
.get(key
, []):
59 def _get_bytes(self
, key
, default
=None):
60 for value
in self
._get
(key
):
65 def _get_strings(self
, key
):
66 for value
in self
._get
(key
):
69 def _get_string(self
, key
, default
=None):
70 for value
in self
._get
_strings
(key
):
75 def _get_phone_numbers(self
, key
):
76 for value
in self
._get
_strings
(key
):
77 yield phonenumbers
.parse(value
, None)
79 def _get_timestamp(self
, key
):
80 value
= self
._get
_string
(key
)
82 # Parse the timestamp value and returns a datetime object
84 return datetime
.datetime
.strptime(value
, "%Y%m%d%H%M%SZ")
86 def _modify(self
, modlist
):
87 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
89 # Authenticate before performing any write operations
90 self
.accounts
._authenticate
()
92 # Run modify operation
93 self
.ldap
.modify_s(self
.dn
, modlist
)
98 def _clear_cache(self
):
104 def _set(self
, key
, values
):
105 current
= self
._get
(key
)
107 # Don't do anything if nothing has changed
108 if list(current
) == values
:
111 # Remove all old values and add all new ones
114 if self
._exists
(key
):
115 modlist
.append((ldap
.MOD_DELETE
, key
, None))
119 modlist
.append((ldap
.MOD_ADD
, key
, values
))
121 # Run modify operation
122 self
._modify
(modlist
)
125 self
.attributes
.update({ key
: values
})
127 def _set_bytes(self
, key
, values
):
128 return self
._set
(key
, values
)
130 def _set_strings(self
, key
, values
):
131 return self
._set
(key
, [e
.encode() for e
in values
if e
])
133 def _set_string(self
, key
, value
):
134 return self
._set
_strings
(key
, [value
,])
136 def _add(self
, key
, values
):
138 (ldap
.MOD_ADD
, key
, values
),
141 self
._modify
(modlist
)
143 def _add_strings(self
, key
, values
):
144 return self
._add
(key
, [e
.encode() for e
in values
])
146 def _add_string(self
, key
, value
):
147 return self
._add
_strings
(key
, [value
,])
149 def _delete(self
, key
, values
):
151 (ldap
.MOD_DELETE
, key
, values
),
154 self
._modify
(modlist
)
156 def _delete_strings(self
, key
, values
):
157 return self
._delete
(key
, [e
.encode() for e
in values
])
159 def _delete_string(self
, key
, value
):
160 return self
._delete
_strings
(key
, [value
,])
163 def objectclasses(self
):
164 return self
._get
_strings
("objectClass")
168 return datetime
.datetime
.strptime(s
.decode(), "%Y%m%d%H%M%SZ")
171 class Accounts(Object
):
173 self
.search_base
= self
.settings
.get("ldap_search_base")
176 count
= self
.memcache
.get("accounts:count")
179 count
= self
._count
("(objectClass=person)")
181 self
.memcache
.set("accounts:count", count
, 300)
186 accounts
= self
._search
("(objectClass=person)")
188 return iter(sorted(accounts
))
192 # Connect to LDAP server
193 ldap_uri
= self
.settings
.get("ldap_uri")
195 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
197 # Connect to the LDAP server
198 connection
= ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
199 trace_level
=2 if self
.backend
.debug
else 0,
200 retry_max
=sys
.maxsize
, retry_delay
=3)
202 # Set maximum timeout for operations
203 connection
.set_option(ldap
.OPT_TIMEOUT
, 10)
207 def _authenticate(self
):
208 # Authenticate against LDAP server using Kerberos
209 self
.ldap
.sasl_gssapi_bind_s()
212 logging
.info("Testing LDAP connection...")
216 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
218 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
219 logging
.debug("Performing LDAP query (%s): %s" \
220 % (search_base
or self
.search_base
, query
))
224 # Ask for up to 512 results being returned at a time
225 page_control
= ldap
.controls
.SimplePagedResultsControl(True, size
=512, cookie
="")
232 response
= self
.ldap
.search_ext(search_base
or self
.search_base
,
233 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
,
234 serverctrls
=[page_control
],
238 type, data
, rmsgid
, serverctrls
= self
.ldap
.result3(response
)
240 # Append to local copy
244 controls
= [c
for c
in serverctrls
245 if c
.controlType
== ldap
.controls
.SimplePagedResultsControl
.controlType
]
250 # Set the cookie for more results
251 page_control
.cookie
= controls
[0].cookie
253 # There are no more results
254 if not page_control
.cookie
:
257 # Log time it took to perform the query
258 logging
.debug("Query took %.2fms (%s page(s))" % ((time
.time() - t
) * 1000.0, pages
))
262 def _count(self
, query
):
263 res
= self
._query
(query
, attrlist
=["dn"])
267 def _search(self
, query
, attrlist
=None, limit
=0):
269 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
270 account
= self
.get_by_dn(dn
)
271 accounts
.append(account
)
275 def _get_attrs(self
, dn
):
277 Fetches all attributes for the given distinguished name
279 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
280 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
282 for dn
, attrs
in results
:
285 def get_by_dn(self
, dn
):
286 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
288 attrs
= self
._get
_attrs
(dn
)
291 # Cache all attributes for 5 min
292 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
294 return Account(self
.backend
, dn
, attrs
)
298 return t
.strftime("%Y%m%d%H%M%SZ")
300 def get_created_after(self
, ts
):
301 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
303 def count_created_after(self
, ts
):
304 return self
._count
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
306 def search(self
, query
):
307 accounts
= self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)(displayName=*%s*)(mail=*%s*)))" \
308 % (query
, query
, query
, query
))
310 return sorted(accounts
)
312 def _search_one(self
, query
):
313 results
= self
._search
(query
, limit
=1)
315 for result
in results
:
318 def uid_is_valid(self
, uid
):
319 # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
320 m
= re
.match(r
"^[a-z_][a-z0-9_-]{3,31}$", uid
)
326 def uid_exists(self
, uid
):
327 if self
.get_by_uid(uid
):
330 res
= self
.db
.get("SELECT 1 FROM account_activations \
331 WHERE uid = %s AND expires_at > NOW()", uid
)
336 # Account with uid does not exist, yet
339 def mail_is_valid(self
, mail
):
340 username
, delim
, domain
= mail
.partition("@")
342 # There must be an @ and a domain part
346 # The domain cannot end on a dot
347 if domain
.endswith("."):
350 # The domain should at least have one dot to fully qualified
351 if not "." in domain
:
354 # Looks like a valid email address
357 def mail_is_blacklisted(self
, mail
):
358 username
, delim
, domain
= mail
.partition("@")
361 return self
.domain_is_blacklisted(domain
)
363 def domain_is_blacklisted(self
, domain
):
364 res
= self
.db
.get("SELECT TRUE AS found FROM blacklisted_domains \
365 WHERE domain = %s OR %s LIKE '%%.' || domain", domain
, domain
)
367 if res
and res
.found
:
372 def get_by_uid(self
, uid
):
373 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
375 def get_by_mail(self
, mail
):
376 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
378 def find_account(self
, s
):
379 account
= self
.get_by_uid(s
)
383 return self
.get_by_mail(s
)
385 def get_by_sip_id(self
, sip_id
):
389 return self
._search
_one
(
390 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
393 def get_by_phone_number(self
, number
):
397 return self
._search
_one
(
398 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
399 % (number
, number
, number
, number
))
402 def pending_registrations(self
):
403 res
= self
.db
.get("SELECT COUNT(*) AS c FROM account_activations")
407 async def check_spam(self
, email
, address
):
408 sfs
= StopForumSpam(self
.backend
, email
, address
)
411 score
= await sfs
.check()
415 def auth(self
, username
, password
):
417 account
= self
.backend
.accounts
.find_account(username
)
420 if account
and account
.check_password(password
):
425 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
426 # Convert all uids to lowercase
429 # Check if UID is valid
430 if not self
.uid_is_valid(uid
):
431 raise ValueError("UID is invalid: %s" % uid
)
433 # Check if UID is unique
434 if self
.uid_exists(uid
):
435 raise ValueError("UID exists: %s" % uid
)
437 # Check if the email address is valid
438 if not self
.mail_is_valid(email
):
439 raise ValueError("Email is invalid: %s" % email
)
441 # Check if the email address is blacklisted
442 if self
.mail_is_blacklisted(email
):
443 raise ValueError("Email is blacklisted: %s" % email
)
445 # Generate a random activation code
446 activation_code
= util
.random_string(36)
448 # Create an entry in our database until the user
449 # has activated the account
450 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
451 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
452 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
454 # Send an account activation email
455 self
.backend
.messages
.send_template("auth/messages/register",
456 priority
=100, uid
=uid
, activation_code
=activation_code
, email
=email
,
457 first_name
=first_name
, last_name
=last_name
)
459 def activate(self
, uid
, activation_code
):
460 res
= self
.db
.get("DELETE FROM account_activations \
461 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
462 RETURNING *", uid
, activation_code
)
464 # Return nothing when account was not found
468 # Return the account if it has already been created
469 account
= self
.get_by_uid(uid
)
473 # Create a new account on the LDAP database
474 account
= self
.create(uid
, res
.email
,
475 first_name
=res
.first_name
, last_name
=res
.last_name
,
476 country_code
=res
.country_code
)
478 # Non-EU users do not need to consent to promo emails
479 if account
.country_code
and not account
.country_code
in countries
.EU_COUNTRIES
:
480 account
.consents_to_promotional_emails
= True
482 # Send email about account registration
483 self
.backend
.messages
.send_template("people/messages/new-account",
486 # Launch drip campaigns
487 for campaign
in ("signup", "christmas"):
488 self
.backend
.campaigns
.launch(campaign
, account
)
492 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
493 cn
= "%s %s" % (first_name
, last_name
)
497 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
498 "mail" : email
.encode(),
502 "sn" : last_name
.encode(),
503 "givenName" : first_name
.encode(),
506 logging
.info("Creating new account: %s: %s" % (uid
, account
))
509 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
511 # Create account on LDAP
512 self
.accounts
._authenticate
()
513 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
516 account
= self
.get_by_dn(dn
)
518 # Optionally set country code
520 account
.country_code
= country_code
527 def create_session(self
, account
, host
):
528 session_id
= util
.random_string(64)
530 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
531 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
533 # Session could not be created
537 logging
.info("Created session %s for %s which expires %s" \
538 % (res
.session_id
, account
, res
.time_expires
))
539 return res
.session_id
, res
.time_expires
541 def destroy_session(self
, session_id
, host
):
542 logging
.info("Destroying session %s" % session_id
)
544 self
.db
.execute("DELETE FROM sessions \
545 WHERE session_id = %s AND host = %s", session_id
, host
)
547 def get_by_session(self
, session_id
, host
):
548 logging
.debug("Looking up session %s" % session_id
)
550 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
551 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
554 # Session does not exist or has expired
558 # Update the session expiration time
559 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
560 WHERE session_id = %s AND host = %s", session_id
, host
)
562 return self
.get_by_uid(res
.uid
)
565 # Cleanup expired sessions
566 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
568 # Cleanup expired account activations
569 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
571 # Cleanup expired account password resets
572 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
576 def decode_discourse_payload(self
, payload
, signature
):
578 calculated_signature
= self
.sign_discourse_payload(payload
)
580 if not hmac
.compare_digest(signature
, calculated_signature
):
581 raise ValueError("Invalid signature: %s" % signature
)
583 # Decode the query string
584 qs
= base64
.b64decode(payload
).decode()
586 # Parse the query string
588 for key
, val
in urllib
.parse
.parse_qsl(qs
):
593 def encode_discourse_payload(self
, **args
):
594 # Encode the arguments into an URL-formatted string
595 qs
= urllib
.parse
.urlencode(args
).encode()
598 return base64
.b64encode(qs
).decode()
600 def sign_discourse_payload(self
, payload
, secret
=None):
602 secret
= self
.settings
.get("discourse_sso_secret")
604 # Calculate a HMAC using SHA256
605 h
= hmac
.new(secret
.encode(),
606 msg
=payload
.encode(), digestmod
="sha256")
614 for country
in iso3166
.countries
:
615 count
= self
._count
("(&(objectClass=person)(st=%s))" % country
.alpha2
)
622 async def get_all_emails(self
):
623 # Returns all email addresses
624 for dn
, attrs
in self
._query
("(objectClass=person)", attrlist
=("mail",)):
625 mails
= attrs
.get("mail", None)
633 class Account(LDAPObject
):
641 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
643 def __lt__(self
, other
):
644 if isinstance(other
, self
.__class
__):
645 return self
.name
< other
.name
647 def _clear_cache(self
):
648 # Delete cached attributes
649 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
652 def kerberos_attributes(self
):
653 res
= self
.backend
.accounts
._query
(
654 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
656 "krbLastSuccessfulAuth",
657 "krbLastPasswordChange",
659 "krbLoginFailedCount",
662 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
664 for dn
, attrs
in res
:
665 return { key
: attrs
[key
][0] for key
in attrs
}
670 def last_successful_authentication(self
):
672 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
676 return self
._parse
_date
(s
)
679 def last_failed_authentication(self
):
681 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
685 return self
._parse
_date
(s
)
688 def failed_login_count(self
):
690 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
699 def passwd(self
, password
):
703 # The new password must have a score of 3 or better
704 quality
= self
.check_password_quality(password
)
705 if quality
["score"] < 3:
706 raise ValueError("Password too weak")
708 self
.accounts
._authenticate
()
709 self
.ldap
.passwd_s(self
.dn
, None, password
)
711 def check_password(self
, password
):
713 Bind to the server with given credentials and return
714 true if password is corrent and false if not.
716 Raises exceptions from the server on any other errors.
721 logging
.debug("Checking credentials for %s" % self
.dn
)
723 # Create a new LDAP connection
724 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
725 conn
= ldap
.initialize(ldap_uri
)
728 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
729 except ldap
.INVALID_CREDENTIALS
:
730 logging
.debug("Account credentials are invalid for %s" % self
)
733 logging
.info("Successfully authenticated %s" % self
)
737 def check_password_quality(self
, password
):
739 Passwords are passed through zxcvbn to make sure
740 that they are strong enough.
742 return zxcvbn
.zxcvbn(password
, user_inputs
=(
743 self
.first_name
, self
.last_name
,
746 def request_password_reset(self
, address
=None):
747 reset_code
= util
.random_string(64)
749 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
750 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
752 # Send a password reset email
753 self
.backend
.messages
.send_template("auth/messages/password-reset",
754 priority
=100, account
=self
, reset_code
=reset_code
)
756 def reset_password(self
, reset_code
, new_password
):
757 # Delete the reset token
758 res
= self
.db
.query("DELETE FROM account_password_resets \
759 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
760 RETURNING *", self
.uid
, reset_code
)
762 # The reset code was invalid
764 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
766 # Perform password change
767 return self
.passwd(new_password
)
770 return self
.is_member_of_group("sudo")
773 return self
.is_member_of_group("staff")
775 def is_moderator(self
):
776 return self
.is_member_of_group("moderators")
779 return "posixAccount" in self
.classes
782 return "postfixMailUser" in self
.classes
785 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
787 def can_be_managed_by(self
, account
):
789 Returns True if account is allowed to manage this account
791 # Admins can manage all accounts
792 if account
.is_admin():
795 # Users can manage themselves
796 return self
== account
800 return self
._get
_strings
("objectClass")
804 return self
._get
_string
("uid")
808 return self
._get
_string
("cn")
812 def get_nickname(self
):
813 return self
._get
_string
("displayName")
815 def set_nickname(self
, nickname
):
816 self
._set
_string
("displayName", nickname
)
818 nickname
= property(get_nickname
, set_nickname
)
822 def get_first_name(self
):
823 return self
._get
_string
("givenName")
825 def set_first_name(self
, first_name
):
826 self
._set
_string
("givenName", first_name
)
829 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
831 first_name
= property(get_first_name
, set_first_name
)
835 def get_last_name(self
):
836 return self
._get
_string
("sn")
838 def set_last_name(self
, last_name
):
839 self
._set
_string
("sn", last_name
)
842 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
844 last_name
= property(get_last_name
, set_last_name
)
848 return self
.backend
.groups
._get
_groups
("(| \
849 (&(objectClass=groupOfNames)(member=%s)) \
850 (&(objectClass=posixGroup)(memberUid=%s)) \
851 )" % (self
.dn
, self
.uid
))
853 def is_member_of_group(self
, gid
):
855 Returns True if this account is a member of this group
857 return gid
in (g
.gid
for g
in self
.groups
)
859 # Created/Modified at
862 def created_at(self
):
863 return self
._get
_timestamp
("createTimestamp")
866 def modified_at(self
):
867 return self
._get
_timestamp
("modifyTimestamp")
876 address
+= self
.street
.splitlines()
878 if self
.postal_code
and self
.city
:
879 if self
.country_code
in ("AT", "DE"):
880 address
.append("%s %s" % (self
.postal_code
, self
.city
))
882 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
884 address
.append(self
.city
or self
.postal_code
)
886 if self
.country_name
:
887 address
.append(self
.country_name
)
891 def get_street(self
):
892 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
894 def set_street(self
, street
):
895 self
._set
_string
("street", street
)
897 street
= property(get_street
, set_street
)
900 return self
._get
_string
("l") or ""
902 def set_city(self
, city
):
903 self
._set
_string
("l", city
)
905 city
= property(get_city
, set_city
)
907 def get_postal_code(self
):
908 return self
._get
_string
("postalCode") or ""
910 def set_postal_code(self
, postal_code
):
911 self
._set
_string
("postalCode", postal_code
)
913 postal_code
= property(get_postal_code
, set_postal_code
)
915 # XXX This should be c
916 def get_country_code(self
):
917 return self
._get
_string
("st")
919 def set_country_code(self
, country_code
):
920 self
._set
_string
("st", country_code
)
922 country_code
= property(get_country_code
, set_country_code
)
925 def country_name(self
):
926 if self
.country_code
:
927 return self
.backend
.get_country_name(self
.country_code
)
931 return self
._get
_string
("mail")
935 return "%s <%s>" % (self
, self
.email
)
937 # Mail Routing Address
939 def get_mail_routing_address(self
):
940 return self
._get
_string
("mailRoutingAddress", None)
942 def set_mail_routing_address(self
, address
):
943 self
._set
_string
("mailRoutingAddress", address
or None)
945 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
949 if "sipUser" in self
.classes
:
950 return self
._get
_string
("sipAuthenticationUser")
952 if "sipRoutingObject" in self
.classes
:
953 return self
._get
_string
("sipLocalAddress")
956 def sip_password(self
):
957 return self
._get
_string
("sipPassword")
960 def _generate_sip_password():
961 return util
.random_string(8)
965 return "%s@ipfire.org" % self
.sip_id
968 def agent_status(self
):
969 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
971 def uses_sip_forwarding(self
):
972 if self
.sip_routing_address
:
979 def get_sip_routing_address(self
):
980 if "sipRoutingObject" in self
.classes
:
981 return self
._get
_string
("sipRoutingAddress")
983 def set_sip_routing_address(self
, address
):
987 # Don't do anything if nothing has changed
988 if self
.get_sip_routing_address() == address
:
992 # This is no longer a SIP user any more
995 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
996 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
997 (ldap
.MOD_DELETE
, "sipPassword", None),
999 except ldap
.NO_SUCH_ATTRIBUTE
:
1002 # Set new routing object
1005 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
1006 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
1007 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
1010 # If this is a change, we cannot add this again
1011 except ldap
.TYPE_OR_VALUE_EXISTS
:
1012 self
._set
_string
("sipRoutingAddress", address
)
1016 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
1017 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
1018 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
1020 except ldap
.NO_SUCH_ATTRIBUTE
:
1024 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
1025 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
1026 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
1029 # XXX Cache is invalid here
1031 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
1034 def sip_registrations(self
):
1035 sip_registrations
= []
1037 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
1040 sip_registrations
.append(reg
)
1042 return sip_registrations
1045 def sip_channels(self
):
1046 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
1048 def get_cdr(self
, date
=None, limit
=None):
1049 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
1054 def phone_number(self
):
1056 Returns the IPFire phone number
1059 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
1062 def fax_number(self
):
1064 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
1066 def get_phone_numbers(self
):
1069 for field
in ("telephoneNumber", "homePhone", "mobile"):
1070 for number
in self
._get
_phone
_numbers
(field
):
1075 def set_phone_numbers(self
, phone_numbers
):
1076 # Sort phone numbers by landline and mobile
1077 _landline_numbers
= []
1078 _mobile_numbers
= []
1080 for number
in phone_numbers
:
1082 number
= phonenumbers
.parse(number
, None)
1083 except phonenumbers
.phonenumberutil
.NumberParseException
:
1086 # Convert to string (in E.164 format)
1087 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1089 # Separate mobile numbers
1090 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1091 _mobile_numbers
.append(s
)
1093 _landline_numbers
.append(s
)
1096 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1097 self
._set
_strings
("mobile", _mobile_numbers
)
1099 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1102 def _all_telephone_numbers(self
):
1103 ret
= [ self
.sip_id
, ]
1105 if self
.phone_number
:
1106 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1109 for number
in self
.phone_numbers
:
1110 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1117 def get_description(self
):
1118 return self
._get
_string
("description")
1120 def set_description(self
, description
):
1121 self
._set
_string
("description", description
)
1123 description
= property(get_description
, set_description
)
1127 def has_avatar(self
):
1128 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
1129 if has_avatar
is None:
1130 has_avatar
= True if self
.get_avatar() else False
1132 # Cache avatar status for up to 24 hours
1133 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1137 def avatar_url(self
, size
=None):
1138 url
= "https://people.ipfire.org/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1141 url
+= "&size=%s" % size
1145 def get_avatar(self
, size
=None):
1146 photo
= self
._get
_bytes
("jpegPhoto")
1148 # Exit if no avatar is available
1152 # Return the raw image if no size was requested
1156 # Try to retrieve something from the cache
1157 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1161 # Generate a new thumbnail
1162 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1164 # Save to cache for 15m
1165 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1170 def avatar_hash(self
):
1171 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1173 h
= hashlib
.new("md5")
1174 h
.update(self
.get_avatar() or b
"")
1175 hash = h
.hexdigest()[:7]
1177 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1181 def upload_avatar(self
, avatar
):
1182 self
._set
("jpegPhoto", avatar
)
1184 # Delete cached avatar status
1185 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1187 # Delete avatar hash
1188 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1190 # Consent to promotional emails
1192 def get_consents_to_promotional_emails(self
):
1193 return self
.is_member_of_group("promotional-consent")
1195 def set_contents_to_promotional_emails(self
, value
):
1196 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1197 assert group
, "Could not find group: promotional-consent"
1200 group
.add_member(self
)
1202 group
.del_member(self
)
1204 consents_to_promotional_emails
= property(
1205 get_consents_to_promotional_emails
,
1206 set_contents_to_promotional_emails
,
1210 class StopForumSpam(Object
):
1211 def init(self
, email
, address
):
1212 self
.email
, self
.address
= email
, address
1214 async def send_request(self
, **kwargs
):
1218 arguments
.update(kwargs
)
1221 request
= tornado
.httpclient
.HTTPRequest(
1222 "https://api.stopforumspam.org/api", method
="POST",
1223 connect_timeout
=2, request_timeout
=5)
1224 request
.body
= urllib
.parse
.urlencode(arguments
)
1227 response
= await self
.backend
.http_client
.fetch(request
)
1229 # Decode the JSON response
1230 return json
.loads(response
.body
.decode())
1232 async def check_address(self
):
1233 response
= await self
.send_request(ip
=self
.address
)
1236 confidence
= response
["ip"]["confidence"]
1240 logging
.debug("Confidence for %s: %s" % (self
.address
, confidence
))
1244 async def check_email(self
):
1245 response
= await self
.send_request(email
=self
.email
)
1248 confidence
= response
["email"]["confidence"]
1252 logging
.debug("Confidence for %s: %s" % (self
.email
, confidence
))
1256 async def check(self
, threshold
=95):
1258 This function tries to detect if we have a spammer.
1260 To honour the privacy of our users, we only send the IP
1261 address and username and if those are on the database, we
1262 will send the email address as well.
1264 confidences
= [await self
.check_address(), await self
.check_email()]
1266 # Build a score based on the lowest confidence
1267 return 100 - min(confidences
)
1270 class Groups(Object
):
1272 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1273 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1275 # Everyone is a member of people
1276 "cn=people,ou=Group,dc=ipfire,dc=org",
1280 def search_base(self
):
1281 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1283 def _query(self
, *args
, **kwargs
):
1285 "search_base" : self
.backend
.groups
.search_base
,
1288 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1291 groups
= self
.get_all()
1295 def _get_groups(self
, query
, **kwargs
):
1296 res
= self
._query
(query
, **kwargs
)
1299 for dn
, attrs
in res
:
1300 # Skip any hidden groups
1301 if dn
in self
.hidden_groups
:
1304 g
= Group(self
.backend
, dn
, attrs
)
1307 return sorted(groups
)
1309 def _get_group(self
, query
, **kwargs
):
1314 groups
= self
._get
_groups
(query
, **kwargs
)
1319 return self
._get
_groups
(
1320 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1323 def get_by_gid(self
, gid
):
1324 return self
._get
_group
(
1325 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1329 class Group(LDAPObject
):
1331 if self
.description
:
1332 return "<%s %s (%s)>" % (
1333 self
.__class
__.__name
__,
1338 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1341 return self
.description
or self
.gid
1343 def __lt__(self
, other
):
1344 if isinstance(other
, self
.__class
__):
1345 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1352 Returns the number of members in this group
1356 for attr
in ("member", "memberUid"):
1357 a
= self
.attributes
.get(attr
, None)
1364 return iter(self
.members
)
1368 return self
._get
_string
("cn")
1371 def description(self
):
1372 return self
._get
_string
("description")
1376 return self
._get
_string
("mail")
1382 # Get all members by DN
1383 for dn
in self
._get
_strings
("member"):
1384 member
= self
.backend
.accounts
.get_by_dn(dn
)
1386 members
.append(member
)
1388 # Get all members by UID
1389 for uid
in self
._get
_strings
("memberUid"):
1390 member
= self
.backend
.accounts
.get_by_uid(uid
)
1392 members
.append(member
)
1394 return sorted(members
)
1396 def add_member(self
, account
):
1398 Adds a member to this group
1400 # Do nothing if this user is already in the group
1401 if account
.is_member_of_group(self
.gid
):
1404 if "posixGroup" in self
.objectclasses
:
1405 self
._add
_string
("memberUid", account
.uid
)
1407 self
._add
_string
("member", account
.dn
)
1409 # Append to cached list of members
1410 self
.members
.append(account
)
1413 def del_member(self
, account
):
1415 Removes a member from a group
1417 # Do nothing if this user is not in the group
1418 if not account
.is_member_of_group(self
.gid
):
1421 if "posixGroup" in self
.objectclasses
:
1422 self
._delete
_string
("memberUid", account
.uid
)
1424 self
._delete
_string
("member", account
.dn
)
1427 if __name__
== "__main__":