19 import tornado
.httpclient
24 from . import countries
26 from .decorators
import *
27 from .misc
import Object
29 # Set the client keytab name
30 os
.environ
["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
32 class LDAPObject(Object
):
33 def init(self
, dn
, attrs
=None):
36 self
.attributes
= attrs
or {}
38 def __eq__(self
, other
):
39 if isinstance(other
, self
.__class
__):
40 return self
.dn
== other
.dn
44 return self
.accounts
.ldap
46 def _exists(self
, key
):
55 for value
in self
.attributes
.get(key
, []):
58 def _get_bytes(self
, key
, default
=None):
59 for value
in self
._get
(key
):
64 def _get_strings(self
, key
):
65 for value
in self
._get
(key
):
68 def _get_string(self
, key
, default
=None):
69 for value
in self
._get
_strings
(key
):
74 def _get_phone_numbers(self
, key
):
75 for value
in self
._get
_strings
(key
):
76 yield phonenumbers
.parse(value
, None)
78 def _get_timestamp(self
, key
):
79 value
= self
._get
_string
(key
)
81 # Parse the timestamp value and returns a datetime object
83 return datetime
.datetime
.strptime(value
, "%Y%m%d%H%M%SZ")
85 def _modify(self
, modlist
):
86 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
88 # Authenticate before performing any write operations
89 self
.accounts
._authenticate
()
91 # Run modify operation
92 self
.ldap
.modify_s(self
.dn
, modlist
)
97 def _clear_cache(self
):
103 def _set(self
, key
, values
):
104 current
= self
._get
(key
)
106 # Don't do anything if nothing has changed
107 if list(current
) == values
:
110 # Remove all old values and add all new ones
113 if self
._exists
(key
):
114 modlist
.append((ldap
.MOD_DELETE
, key
, None))
118 modlist
.append((ldap
.MOD_ADD
, key
, values
))
120 # Run modify operation
121 self
._modify
(modlist
)
124 self
.attributes
.update({ key
: values
})
126 def _set_bytes(self
, key
, values
):
127 return self
._set
(key
, values
)
129 def _set_strings(self
, key
, values
):
130 return self
._set
(key
, [e
.encode() for e
in values
if e
])
132 def _set_string(self
, key
, value
):
133 return self
._set
_strings
(key
, [value
,])
135 def _add(self
, key
, values
):
137 (ldap
.MOD_ADD
, key
, values
),
140 self
._modify
(modlist
)
142 def _add_strings(self
, key
, values
):
143 return self
._add
(key
, [e
.encode() for e
in values
])
145 def _add_string(self
, key
, value
):
146 return self
._add
_strings
(key
, [value
,])
148 def _delete(self
, key
, values
):
150 (ldap
.MOD_DELETE
, key
, values
),
153 self
._modify
(modlist
)
155 def _delete_strings(self
, key
, values
):
156 return self
._delete
(key
, [e
.encode() for e
in values
])
158 def _delete_string(self
, key
, value
):
159 return self
._delete
_strings
(key
, [value
,])
162 def objectclasses(self
):
163 return self
._get
_strings
("objectClass")
167 return datetime
.datetime
.strptime(s
.decode(), "%Y%m%d%H%M%SZ")
170 class Accounts(Object
):
172 self
.search_base
= self
.settings
.get("ldap_search_base")
175 count
= self
.memcache
.get("accounts:count")
178 count
= self
._count
("(objectClass=person)")
180 self
.memcache
.set("accounts:count", count
, 300)
185 accounts
= self
._search
("(objectClass=person)")
187 return iter(sorted(accounts
))
191 # Connect to LDAP server
192 ldap_uri
= self
.settings
.get("ldap_uri")
194 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
196 # Connect to the LDAP server
197 connection
= ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
198 trace_level
=2 if self
.backend
.debug
else 0,
199 retry_max
=sys
.maxsize
, retry_delay
=3)
201 # Set maximum timeout for operations
202 connection
.set_option(ldap
.OPT_TIMEOUT
, 10)
206 def _authenticate(self
):
207 # Authenticate against LDAP server using Kerberos
208 self
.ldap
.sasl_gssapi_bind_s()
210 async def test_ldap(self
):
211 logging
.info("Testing LDAP connection...")
215 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
217 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
218 logging
.debug("Performing LDAP query (%s): %s" \
219 % (search_base
or self
.search_base
, query
))
223 # Ask for up to 512 results being returned at a time
224 page_control
= ldap
.controls
.SimplePagedResultsControl(True, size
=512, cookie
="")
231 response
= self
.ldap
.search_ext(search_base
or self
.search_base
,
232 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
,
233 serverctrls
=[page_control
],
237 type, data
, rmsgid
, serverctrls
= self
.ldap
.result3(response
)
239 # Append to local copy
243 controls
= [c
for c
in serverctrls
244 if c
.controlType
== ldap
.controls
.SimplePagedResultsControl
.controlType
]
249 # Set the cookie for more results
250 page_control
.cookie
= controls
[0].cookie
252 # There are no more results
253 if not page_control
.cookie
:
256 # Log time it took to perform the query
257 logging
.debug("Query took %.2fms (%s page(s))" % ((time
.time() - t
) * 1000.0, pages
))
261 def _count(self
, query
):
262 res
= self
._query
(query
, attrlist
=["dn"])
266 def _search(self
, query
, attrlist
=None, limit
=0):
268 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
269 account
= self
.get_by_dn(dn
)
270 accounts
.append(account
)
274 def _get_attrs(self
, dn
):
276 Fetches all attributes for the given distinguished name
278 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
279 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
281 for dn
, attrs
in results
:
284 def get_by_dn(self
, dn
):
285 attrs
= self
.memcache
.get("accounts:%s:attrs" % dn
)
287 attrs
= self
._get
_attrs
(dn
)
290 # Cache all attributes for 5 min
291 self
.memcache
.set("accounts:%s:attrs" % dn
, attrs
, 300)
293 return Account(self
.backend
, dn
, attrs
)
297 return t
.strftime("%Y%m%d%H%M%SZ")
299 def get_recently_registered(self
, limit
=None):
300 # Check the last two weeks
301 t
= datetime
.datetime
.utcnow() - datetime
.timedelta(days
=14)
303 # Fetch all accounts created after t
304 accounts
= self
.get_created_after(t
)
306 # Order by creation date and put latest first
307 accounts
.sort(key
=lambda a
: a
.created_at
, reverse
=True)
310 if accounts
and limit
:
311 accounts
= accounts
[:limit
]
315 def get_created_after(self
, ts
):
316 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
318 def count_created_after(self
, ts
):
319 return self
._count
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
321 def search(self
, query
):
322 # Try finding an exact match
323 account
= self
._search
_one
(
325 "(objectClass=person)"
329 "(mailAlternateAddress=%s)"
331 ")" % (query
, query
, query
))
335 # Otherwise search for a substring match
336 accounts
= self
._search
(
338 "(objectClass=person)"
345 ")" % (query
, query
, query
, query
))
347 return sorted(accounts
)
349 def _search_one(self
, query
):
350 results
= self
._search
(query
, limit
=1)
352 for result
in results
:
355 def uid_is_valid(self
, uid
):
356 # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
357 m
= re
.match(r
"^[a-z_][a-z0-9_-]{3,31}$", uid
)
363 def uid_exists(self
, uid
):
364 if self
.get_by_uid(uid
):
367 res
= self
.db
.get("SELECT 1 FROM account_activations \
368 WHERE uid = %s AND expires_at > NOW()", uid
)
373 # Account with uid does not exist, yet
376 def mail_is_valid(self
, mail
):
377 username
, delim
, domain
= mail
.partition("@")
379 # There must be an @ and a domain part
383 # The domain cannot end on a dot
384 if domain
.endswith("."):
387 # The domain should at least have one dot to fully qualified
388 if not "." in domain
:
391 # Looks like a valid email address
394 def mail_is_blacklisted(self
, mail
):
395 username
, delim
, domain
= mail
.partition("@")
398 return self
.domain_is_blacklisted(domain
)
400 def domain_is_blacklisted(self
, domain
):
401 res
= self
.db
.get("SELECT TRUE AS found FROM blacklisted_domains \
402 WHERE domain = %s OR %s LIKE '%%.' || domain", domain
, domain
)
404 if res
and res
.found
:
409 def get_by_uid(self
, uid
):
410 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
412 def get_by_mail(self
, mail
):
413 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
415 def find_account(self
, s
):
416 account
= self
.get_by_uid(s
)
420 return self
.get_by_mail(s
)
422 def get_by_sip_id(self
, sip_id
):
426 return self
._search
_one
(
427 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
430 def get_by_phone_number(self
, number
):
434 return self
._search
_one
(
435 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
436 % (number
, number
, number
, number
))
439 def pending_registrations(self
):
440 res
= self
.db
.get("SELECT COUNT(*) AS c FROM account_activations")
444 async def check_spam(self
, email
, address
):
445 sfs
= StopForumSpam(self
.backend
, email
, address
)
448 score
= await sfs
.check()
452 def auth(self
, username
, password
):
454 account
= self
.backend
.accounts
.find_account(username
)
457 if account
and account
.check_password(password
):
462 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
463 # Convert all uids to lowercase
466 # Check if UID is valid
467 if not self
.uid_is_valid(uid
):
468 raise ValueError("UID is invalid: %s" % uid
)
470 # Check if UID is unique
471 if self
.uid_exists(uid
):
472 raise ValueError("UID exists: %s" % uid
)
474 # Check if the email address is valid
475 if not self
.mail_is_valid(email
):
476 raise ValueError("Email is invalid: %s" % email
)
478 # Check if the email address is blacklisted
479 if self
.mail_is_blacklisted(email
):
480 raise ValueError("Email is blacklisted: %s" % email
)
482 # Generate a random activation code
483 activation_code
= util
.random_string(36)
485 # Create an entry in our database until the user
486 # has activated the account
487 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
488 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
489 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
491 # Send an account activation email
492 self
.backend
.messages
.send_template("auth/messages/register",
493 priority
=100, uid
=uid
, activation_code
=activation_code
, email
=email
,
494 first_name
=first_name
, last_name
=last_name
)
496 def activate(self
, uid
, activation_code
):
497 res
= self
.db
.get("DELETE FROM account_activations \
498 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
499 RETURNING *", uid
, activation_code
)
501 # Return nothing when account was not found
505 # Return the account if it has already been created
506 account
= self
.get_by_uid(uid
)
510 # Create a new account on the LDAP database
511 account
= self
.create(uid
, res
.email
,
512 first_name
=res
.first_name
, last_name
=res
.last_name
,
513 country_code
=res
.country_code
)
515 # Non-EU users do not need to consent to promo emails
516 if account
.country_code
and not account
.country_code
in countries
.EU_COUNTRIES
:
517 account
.consents_to_promotional_emails
= True
519 # Send email about account registration
520 self
.backend
.messages
.send_template("people/messages/new-account",
523 # Launch drip campaigns
524 for campaign
in ("signup", "christmas"):
525 self
.backend
.campaigns
.launch(campaign
, account
)
529 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
530 cn
= "%s %s" % (first_name
, last_name
)
534 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
535 "mail" : email
.encode(),
539 "sn" : last_name
.encode(),
540 "givenName" : first_name
.encode(),
543 logging
.info("Creating new account: %s: %s" % (uid
, account
))
546 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
548 # Create account on LDAP
549 self
.accounts
._authenticate
()
550 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
553 account
= self
.get_by_dn(dn
)
555 # Optionally set country code
557 account
.country_code
= country_code
564 def create_session(self
, account
, host
):
565 session_id
= util
.random_string(64)
567 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
568 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
570 # Session could not be created
574 logging
.info("Created session %s for %s which expires %s" \
575 % (res
.session_id
, account
, res
.time_expires
))
576 return res
.session_id
, res
.time_expires
578 def destroy_session(self
, session_id
, host
):
579 logging
.info("Destroying session %s" % session_id
)
581 self
.db
.execute("DELETE FROM sessions \
582 WHERE session_id = %s AND host = %s", session_id
, host
)
584 def get_by_session(self
, session_id
, host
):
585 logging
.debug("Looking up session %s" % session_id
)
587 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
588 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
591 # Session does not exist or has expired
595 # Update the session expiration time
596 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
597 WHERE session_id = %s AND host = %s", session_id
, host
)
599 return self
.get_by_uid(res
.uid
)
602 # Cleanup expired sessions
603 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
605 # Cleanup expired account activations
606 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
608 # Cleanup expired account password resets
609 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
611 async def _delete(self
, *args
, **kwargs
):
616 who
= self
.get_by_uid("ms")
619 account
= self
.get_by_uid(uid
)
622 with self
.db
.transaction():
623 await account
.delete(who
)
627 def decode_discourse_payload(self
, payload
, signature
):
629 calculated_signature
= self
.sign_discourse_payload(payload
)
631 if not hmac
.compare_digest(signature
, calculated_signature
):
632 raise ValueError("Invalid signature: %s" % signature
)
634 # Decode the query string
635 qs
= base64
.b64decode(payload
).decode()
637 # Parse the query string
639 for key
, val
in urllib
.parse
.parse_qsl(qs
):
644 def encode_discourse_payload(self
, **args
):
645 # Encode the arguments into an URL-formatted string
646 qs
= urllib
.parse
.urlencode(args
).encode()
649 return base64
.b64encode(qs
).decode()
651 def sign_discourse_payload(self
, payload
, secret
=None):
653 secret
= self
.settings
.get("discourse_sso_secret")
655 # Calculate a HMAC using SHA256
656 h
= hmac
.new(secret
.encode(),
657 msg
=payload
.encode(), digestmod
="sha256")
665 for country
in iso3166
.countries
:
666 count
= self
._count
("(&(objectClass=person)(st=%s))" % country
.alpha2
)
673 async def get_all_emails(self
):
674 # Returns all email addresses
675 for dn
, attrs
in self
._query
("(objectClass=person)", attrlist
=("mail",)):
676 mails
= attrs
.get("mail", None)
684 class Account(LDAPObject
):
692 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
694 def __lt__(self
, other
):
695 if isinstance(other
, self
.__class
__):
696 return self
.name
< other
.name
698 return NotImplemented
700 def _clear_cache(self
):
701 # Delete cached attributes
702 self
.memcache
.delete("accounts:%s:attrs" % self
.dn
)
705 def kerberos_attributes(self
):
706 res
= self
.backend
.accounts
._query
(
707 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
709 "krbLastSuccessfulAuth",
710 "krbLastPasswordChange",
712 "krbLoginFailedCount",
715 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
717 for dn
, attrs
in res
:
718 return { key
: attrs
[key
][0] for key
in attrs
}
723 def last_successful_authentication(self
):
725 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
729 return self
._parse
_date
(s
)
732 def last_failed_authentication(self
):
734 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
738 return self
._parse
_date
(s
)
741 def failed_login_count(self
):
743 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
752 def passwd(self
, password
):
756 # The new password must have a score of 3 or better
757 quality
= self
.check_password_quality(password
)
758 if quality
["score"] < 3:
759 raise ValueError("Password too weak")
761 self
.accounts
._authenticate
()
762 self
.ldap
.passwd_s(self
.dn
, None, password
)
764 def check_password(self
, password
):
766 Bind to the server with given credentials and return
767 true if password is corrent and false if not.
769 Raises exceptions from the server on any other errors.
774 logging
.debug("Checking credentials for %s" % self
.dn
)
776 # Create a new LDAP connection
777 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
778 conn
= ldap
.initialize(ldap_uri
)
781 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
782 except ldap
.INVALID_CREDENTIALS
:
783 logging
.debug("Account credentials are invalid for %s" % self
)
786 logging
.info("Successfully authenticated %s" % self
)
790 def check_password_quality(self
, password
):
792 Passwords are passed through zxcvbn to make sure
793 that they are strong enough.
795 return zxcvbn
.zxcvbn(password
, user_inputs
=(
796 self
.first_name
, self
.last_name
,
799 def request_password_reset(self
, address
=None):
800 reset_code
= util
.random_string(64)
802 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
803 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
805 # Send a password reset email
806 self
.backend
.messages
.send_template("auth/messages/password-reset",
807 priority
=100, account
=self
, reset_code
=reset_code
)
809 def reset_password(self
, reset_code
, new_password
):
810 # Delete the reset token
811 res
= self
.db
.query("DELETE FROM account_password_resets \
812 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
813 RETURNING *", self
.uid
, reset_code
)
815 # The reset code was invalid
817 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
819 # Perform password change
820 return self
.passwd(new_password
)
823 return self
.is_member_of_group("sudo")
826 return self
.is_member_of_group("staff")
828 def is_moderator(self
):
829 return self
.is_member_of_group("moderators")
832 return "posixAccount" in self
.classes
835 return "postfixMailUser" in self
.classes
839 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
842 return self
.is_member_of_group("lwl-staff")
844 def can_be_managed_by(self
, account
):
846 Returns True if account is allowed to manage this account
848 # Admins can manage all accounts
849 if account
.is_admin():
852 # Users can manage themselves
853 return self
== account
857 return self
._get
_strings
("objectClass")
861 return self
._get
_string
("uid")
865 return self
._get
_string
("cn")
869 async def delete(self
, user
):
873 # Check if this user can be deleted
874 if not self
.can_be_deleted(user
):
875 raise RuntimeError("Cannot delete user %s" % self
)
877 async with asyncio
.TaskGroup() as tasks
:
878 t
= datetime
.datetime
.now()
880 # Disable this account on Bugzilla
882 self
._disable
_on
_bugzilla
("Deleted by %s, %s" % (user
, t
)),
885 # XXX Delete on Discourse
889 def can_be_deleted(self
, user
):
891 Return True if the user can be deleted by user
894 if not self
.can_be_managed_by(user
):
897 # Cannot delete shell users
906 def get_nickname(self
):
907 return self
._get
_string
("displayName")
909 def set_nickname(self
, nickname
):
910 self
._set
_string
("displayName", nickname
)
912 nickname
= property(get_nickname
, set_nickname
)
916 def get_first_name(self
):
917 return self
._get
_string
("givenName")
919 def set_first_name(self
, first_name
):
920 self
._set
_string
("givenName", first_name
)
923 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
925 first_name
= property(get_first_name
, set_first_name
)
929 def get_last_name(self
):
930 return self
._get
_string
("sn")
932 def set_last_name(self
, last_name
):
933 self
._set
_string
("sn", last_name
)
936 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
938 last_name
= property(get_last_name
, set_last_name
)
942 return self
.backend
.groups
._get
_groups
("(| \
943 (&(objectClass=groupOfNames)(member=%s)) \
944 (&(objectClass=posixGroup)(memberUid=%s)) \
945 )" % (self
.dn
, self
.uid
))
947 def is_member_of_group(self
, gid
):
949 Returns True if this account is a member of this group
951 return gid
in (g
.gid
for g
in self
.groups
)
953 # Created/Modified at
956 def created_at(self
):
957 return self
._get
_timestamp
("createTimestamp")
960 def modified_at(self
):
961 return self
._get
_timestamp
("modifyTimestamp")
970 address
+= self
.street
.splitlines()
972 if self
.postal_code
and self
.city
:
973 if self
.country_code
in ("AT", "DE"):
974 address
.append("%s %s" % (self
.postal_code
, self
.city
))
976 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
978 address
.append(self
.city
or self
.postal_code
)
980 if self
.country_name
:
981 address
.append(self
.country_name
)
983 return [line
for line
in address
if line
]
985 def get_street(self
):
986 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
988 def set_street(self
, street
):
989 self
._set
_string
("street", street
)
991 street
= property(get_street
, set_street
)
994 return self
._get
_string
("l") or ""
996 def set_city(self
, city
):
997 self
._set
_string
("l", city
)
999 city
= property(get_city
, set_city
)
1001 def get_postal_code(self
):
1002 return self
._get
_string
("postalCode") or ""
1004 def set_postal_code(self
, postal_code
):
1005 self
._set
_string
("postalCode", postal_code
)
1007 postal_code
= property(get_postal_code
, set_postal_code
)
1009 # XXX This should be c
1010 def get_country_code(self
):
1011 return self
._get
_string
("st")
1013 def set_country_code(self
, country_code
):
1014 self
._set
_string
("st", country_code
)
1016 country_code
= property(get_country_code
, set_country_code
)
1019 def country_name(self
):
1020 if self
.country_code
:
1021 return self
.backend
.get_country_name(self
.country_code
)
1025 return self
._get
_string
("mail")
1029 return "%s <%s>" % (self
, self
.email
)
1032 def alternate_email_addresses(self
):
1033 addresses
= self
._get
_strings
("mailAlternateAddress")
1035 return sorted(addresses
)
1037 # Mail Routing Address
1039 def get_mail_routing_address(self
):
1040 return self
._get
_string
("mailRoutingAddress", None)
1042 def set_mail_routing_address(self
, address
):
1043 self
._set
_string
("mailRoutingAddress", address
or None)
1045 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
1049 if "sipUser" in self
.classes
:
1050 return self
._get
_string
("sipAuthenticationUser")
1052 if "sipRoutingObject" in self
.classes
:
1053 return self
._get
_string
("sipLocalAddress")
1056 def sip_password(self
):
1057 return self
._get
_string
("sipPassword")
1060 def _generate_sip_password():
1061 return util
.random_string(8)
1065 return "%s@ipfire.org" % self
.sip_id
1068 def agent_status(self
):
1069 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
1071 def uses_sip_forwarding(self
):
1072 if self
.sip_routing_address
:
1079 def get_sip_routing_address(self
):
1080 if "sipRoutingObject" in self
.classes
:
1081 return self
._get
_string
("sipRoutingAddress")
1083 def set_sip_routing_address(self
, address
):
1087 # Don't do anything if nothing has changed
1088 if self
.get_sip_routing_address() == address
:
1092 # This is no longer a SIP user any more
1095 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
1096 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
1097 (ldap
.MOD_DELETE
, "sipPassword", None),
1099 except ldap
.NO_SUCH_ATTRIBUTE
:
1102 # Set new routing object
1105 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
1106 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
1107 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
1110 # If this is a change, we cannot add this again
1111 except ldap
.TYPE_OR_VALUE_EXISTS
:
1112 self
._set
_string
("sipRoutingAddress", address
)
1116 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
1117 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
1118 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
1120 except ldap
.NO_SUCH_ATTRIBUTE
:
1124 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
1125 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
1126 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
1129 # XXX Cache is invalid here
1131 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
1134 def sip_registrations(self
):
1135 sip_registrations
= []
1137 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
1140 sip_registrations
.append(reg
)
1142 return sip_registrations
1145 def sip_channels(self
):
1146 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
1148 def get_cdr(self
, date
=None, limit
=None):
1149 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
1154 def phone_number(self
):
1156 Returns the IPFire phone number
1159 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
1162 def fax_number(self
):
1164 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
1166 def get_phone_numbers(self
):
1169 for field
in ("telephoneNumber", "homePhone", "mobile"):
1170 for number
in self
._get
_phone
_numbers
(field
):
1175 def set_phone_numbers(self
, phone_numbers
):
1176 # Sort phone numbers by landline and mobile
1177 _landline_numbers
= []
1178 _mobile_numbers
= []
1180 for number
in phone_numbers
:
1182 number
= phonenumbers
.parse(number
, None)
1183 except phonenumbers
.phonenumberutil
.NumberParseException
:
1186 # Convert to string (in E.164 format)
1187 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1189 # Separate mobile numbers
1190 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1191 _mobile_numbers
.append(s
)
1193 _landline_numbers
.append(s
)
1196 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1197 self
._set
_strings
("mobile", _mobile_numbers
)
1199 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1202 def _all_telephone_numbers(self
):
1203 ret
= [ self
.sip_id
, ]
1205 if self
.phone_number
:
1206 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1209 for number
in self
.phone_numbers
:
1210 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1217 def get_description(self
):
1218 return self
._get
_string
("description")
1220 def set_description(self
, description
):
1221 self
._set
_string
("description", description
)
1223 description
= property(get_description
, set_description
)
1227 def has_avatar(self
):
1228 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
1229 if has_avatar
is None:
1230 has_avatar
= True if self
.get_avatar() else False
1232 # Cache avatar status for up to 24 hours
1233 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1237 def avatar_url(self
, size
=None, absolute
=False):
1238 url
= "/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1240 # Return an absolute URL
1242 url
= urllib
.parse
.urljoin("https://people.ipfire.org", url
)
1245 url
+= "&size=%s" % size
1249 def get_avatar(self
, size
=None):
1250 photo
= self
._get
_bytes
("jpegPhoto")
1252 # Exit if no avatar is available
1256 # Return the raw image if no size was requested
1260 # Try to retrieve something from the cache
1261 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1265 # Generate a new thumbnail
1266 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1268 # Save to cache for 15m
1269 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1274 def avatar_hash(self
):
1275 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1277 h
= hashlib
.new("md5")
1278 h
.update(self
.get_avatar() or b
"")
1279 hash = h
.hexdigest()[:7]
1281 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1285 def upload_avatar(self
, avatar
):
1286 self
._set
("jpegPhoto", avatar
)
1288 # Delete cached avatar status
1289 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1291 # Delete avatar hash
1292 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1294 # Consent to promotional emails
1296 def get_consents_to_promotional_emails(self
):
1297 return self
.is_member_of_group("promotional-consent")
1299 def set_contents_to_promotional_emails(self
, value
):
1300 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1301 assert group
, "Could not find group: promotional-consent"
1304 group
.add_member(self
)
1306 group
.del_member(self
)
1308 consents_to_promotional_emails
= property(
1309 get_consents_to_promotional_emails
,
1310 set_contents_to_promotional_emails
,
1315 async def _disable_on_bugzilla(self
, text
=None):
1317 Disables the user on Bugzilla
1319 user
= await self
.backend
.bugzilla
.get_user(self
.email
)
1321 # Do nothing if the user does not exist
1326 await user
.disable(text
)
1329 class StopForumSpam(Object
):
1330 def init(self
, email
, address
):
1331 self
.email
, self
.address
= email
, address
1333 async def send_request(self
, **kwargs
):
1337 arguments
.update(kwargs
)
1340 request
= tornado
.httpclient
.HTTPRequest(
1341 "https://api.stopforumspam.org/api", method
="POST",
1342 connect_timeout
=2, request_timeout
=5)
1343 request
.body
= urllib
.parse
.urlencode(arguments
)
1346 response
= await self
.backend
.http_client
.fetch(request
)
1348 # Decode the JSON response
1349 return json
.loads(response
.body
.decode())
1351 async def check_address(self
):
1352 response
= await self
.send_request(ip
=self
.address
)
1355 confidence
= response
["ip"]["confidence"]
1359 logging
.debug("Confidence for %s: %s" % (self
.address
, confidence
))
1363 async def check_email(self
):
1364 response
= await self
.send_request(email
=self
.email
)
1367 confidence
= response
["email"]["confidence"]
1371 logging
.debug("Confidence for %s: %s" % (self
.email
, confidence
))
1375 async def check(self
, threshold
=95):
1377 This function tries to detect if we have a spammer.
1379 To honour the privacy of our users, we only send the IP
1380 address and username and if those are on the database, we
1381 will send the email address as well.
1383 confidences
= [await self
.check_address(), await self
.check_email()]
1385 # Build a score based on the lowest confidence
1386 return 100 - min(confidences
)
1389 class Groups(Object
):
1391 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1392 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1394 # Everyone is a member of people
1395 "cn=people,ou=Group,dc=ipfire,dc=org",
1399 def search_base(self
):
1400 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1402 def _query(self
, *args
, **kwargs
):
1404 "search_base" : self
.backend
.groups
.search_base
,
1407 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1410 groups
= self
.get_all()
1414 def _get_groups(self
, query
, **kwargs
):
1415 res
= self
._query
(query
, **kwargs
)
1418 for dn
, attrs
in res
:
1419 # Skip any hidden groups
1420 if dn
in self
.hidden_groups
:
1423 g
= Group(self
.backend
, dn
, attrs
)
1426 return sorted(groups
)
1428 def _get_group(self
, query
, **kwargs
):
1433 groups
= self
._get
_groups
(query
, **kwargs
)
1438 return self
._get
_groups
(
1439 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1442 def get_by_gid(self
, gid
):
1443 return self
._get
_group
(
1444 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1448 class Group(LDAPObject
):
1450 if self
.description
:
1451 return "<%s %s (%s)>" % (
1452 self
.__class
__.__name
__,
1457 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1460 return self
.description
or self
.gid
1462 def __lt__(self
, other
):
1463 if isinstance(other
, self
.__class
__):
1464 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1471 Returns the number of members in this group
1475 for attr
in ("member", "memberUid"):
1476 a
= self
.attributes
.get(attr
, None)
1483 return iter(self
.members
)
1487 return self
._get
_string
("cn")
1490 def description(self
):
1491 return self
._get
_string
("description")
1495 return self
._get
_string
("mail")
1501 # Get all members by DN
1502 for dn
in self
._get
_strings
("member"):
1503 member
= self
.backend
.accounts
.get_by_dn(dn
)
1505 members
.append(member
)
1507 # Get all members by UID
1508 for uid
in self
._get
_strings
("memberUid"):
1509 member
= self
.backend
.accounts
.get_by_uid(uid
)
1511 members
.append(member
)
1513 return sorted(members
)
1515 def add_member(self
, account
):
1517 Adds a member to this group
1519 # Do nothing if this user is already in the group
1520 if account
.is_member_of_group(self
.gid
):
1523 if "posixGroup" in self
.objectclasses
:
1524 self
._add
_string
("memberUid", account
.uid
)
1526 self
._add
_string
("member", account
.dn
)
1528 # Append to cached list of members
1529 self
.members
.append(account
)
1532 def del_member(self
, account
):
1534 Removes a member from a group
1536 # Do nothing if this user is not in the group
1537 if not account
.is_member_of_group(self
.gid
):
1540 if "posixGroup" in self
.objectclasses
:
1541 self
._delete
_string
("memberUid", account
.uid
)
1543 self
._delete
_string
("member", account
.dn
)
1546 if __name__
== "__main__":