21 import tornado
.httpclient
26 from . import countries
28 from .decorators
import *
29 from .misc
import Object
31 # Set the client keytab name
32 os
.environ
["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
34 FQDN
= socket
.gethostname()
36 class LDAPObject(Object
):
37 def init(self
, dn
, attrs
=None):
40 self
.attributes
= attrs
or {}
42 def __eq__(self
, other
):
43 if isinstance(other
, self
.__class
__):
44 return self
.dn
== other
.dn
50 return self
.accounts
.ldap
52 def _exists(self
, key
):
61 for value
in self
.attributes
.get(key
, []):
64 def _get_bytes(self
, key
, default
=None):
65 for value
in self
._get
(key
):
70 def _get_strings(self
, key
):
71 for value
in self
._get
(key
):
74 def _get_string(self
, key
, default
=None):
75 for value
in self
._get
_strings
(key
):
80 def _get_phone_numbers(self
, key
):
81 for value
in self
._get
_strings
(key
):
82 yield phonenumbers
.parse(value
, None)
84 def _get_timestamp(self
, key
):
85 value
= self
._get
_string
(key
)
87 # Parse the timestamp value and returns a datetime object
89 return datetime
.datetime
.strptime(value
, "%Y%m%d%H%M%SZ")
91 def _modify(self
, modlist
):
92 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
94 # Authenticate before performing any write operations
95 self
.accounts
._authenticate
()
97 # Run modify operation
98 self
.ldap
.modify_s(self
.dn
, modlist
)
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
,])
158 def _delete_dn(self
, dn
):
159 logging
.debug("Deleting %s" % dn
)
161 # Authenticate before performing any delete operations
162 self
.accounts
._authenticate
()
164 # Run delete operation
165 self
.ldap
.delete_s(dn
)
168 def objectclasses(self
):
169 return self
._get
_strings
("objectClass")
173 return datetime
.datetime
.strptime(s
.decode(), "%Y%m%d%H%M%SZ")
176 class Accounts(Object
):
178 self
.search_base
= self
.settings
.get("ldap_search_base")
181 count
= self
.memcache
.get("accounts:count")
184 count
= self
._count
("(objectClass=person)")
186 self
.memcache
.set("accounts:count", count
, 300)
191 accounts
= self
._search
("(objectClass=person)")
193 return iter(sorted(accounts
))
197 # Connect to LDAP server
198 ldap_uri
= self
.settings
.get("ldap_uri")
200 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
202 # Connect to the LDAP server
203 connection
= ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
204 trace_level
=2 if self
.backend
.debug
else 0,
205 retry_max
=sys
.maxsize
, retry_delay
=3)
207 # Set maximum timeout for operations
208 connection
.set_option(ldap
.OPT_TIMEOUT
, 10)
212 def _authenticate(self
):
213 # Authenticate against LDAP server using Kerberos
214 self
.ldap
.sasl_gssapi_bind_s()
216 async def test_ldap(self
):
217 logging
.info("Testing LDAP connection...")
221 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
223 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
224 logging
.debug("Performing LDAP query (%s): %s" \
225 % (search_base
or self
.search_base
, query
))
229 # Ask for up to 512 results being returned at a time
230 page_control
= ldap
.controls
.SimplePagedResultsControl(True, size
=512, cookie
="")
237 response
= self
.ldap
.search_ext(search_base
or self
.search_base
,
238 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
,
239 serverctrls
=[page_control
],
243 type, data
, rmsgid
, serverctrls
= self
.ldap
.result3(response
)
245 # Append to local copy
249 controls
= [c
for c
in serverctrls
250 if c
.controlType
== ldap
.controls
.SimplePagedResultsControl
.controlType
]
255 # Set the cookie for more results
256 page_control
.cookie
= controls
[0].cookie
258 # There are no more results
259 if not page_control
.cookie
:
262 # Log time it took to perform the query
263 logging
.debug("Query took %.2fms (%s page(s))" % ((time
.time() - t
) * 1000.0, pages
))
267 def _count(self
, query
):
268 res
= self
._query
(query
, attrlist
=["dn"])
272 def _search(self
, query
, attrlist
=None, limit
=0):
274 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
275 account
= self
.get_by_dn(dn
)
276 accounts
.append(account
)
280 def _get_attrs(self
, dn
):
282 Fetches all attributes for the given distinguished name
284 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
285 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
287 for dn
, attrs
in results
:
290 def get_by_dn(self
, dn
):
291 attrs
= self
._get
_attrs
(dn
)
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 def auth(self
, username
, password
):
446 account
= self
.backend
.accounts
.find_account(username
)
449 if account
and account
.check_password(password
):
454 def register(self
, uid
, email
, first_name
, last_name
, country_code
=None):
455 # Convert all uids to lowercase
458 # Check if UID is valid
459 if not self
.uid_is_valid(uid
):
460 raise ValueError("UID is invalid: %s" % uid
)
462 # Check if UID is unique
463 if self
.uid_exists(uid
):
464 raise ValueError("UID exists: %s" % uid
)
466 # Check if the email address is valid
467 if not self
.mail_is_valid(email
):
468 raise ValueError("Email is invalid: %s" % email
)
470 # Check if the email address is blacklisted
471 if self
.mail_is_blacklisted(email
):
472 raise ValueError("Email is blacklisted: %s" % email
)
474 # Generate a random activation code
475 activation_code
= util
.random_string(36)
477 # Create an entry in our database until the user
478 # has activated the account
479 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
480 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
481 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
483 # Send an account activation email
484 self
.backend
.messages
.send_template("auth/messages/register",
485 priority
=100, uid
=uid
, activation_code
=activation_code
, email
=email
,
486 first_name
=first_name
, last_name
=last_name
)
488 def activate(self
, uid
, activation_code
):
489 res
= self
.db
.get("DELETE FROM account_activations \
490 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
491 RETURNING *", uid
, activation_code
)
493 # Return nothing when account was not found
497 # Return the account if it has already been created
498 account
= self
.get_by_uid(uid
)
502 # Create a new account on the LDAP database
503 account
= self
.create(uid
, res
.email
,
504 first_name
=res
.first_name
, last_name
=res
.last_name
,
505 country_code
=res
.country_code
)
507 # Non-EU users do not need to consent to promo emails
508 if account
.country_code
and not account
.country_code
in countries
.EU_COUNTRIES
:
509 account
.consents_to_promotional_emails
= True
511 # Send email about account registration
512 self
.backend
.messages
.send_template("people/messages/new-account",
515 # Launch drip campaigns
516 for campaign
in ("signup", "christmas"):
517 self
.backend
.campaigns
.launch(campaign
, account
)
521 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
522 cn
= "%s %s" % (first_name
, last_name
)
526 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
527 "mail" : email
.encode(),
531 "sn" : last_name
.encode(),
532 "givenName" : first_name
.encode(),
535 logging
.info("Creating new account: %s: %s" % (uid
, account
))
538 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
540 # Create account on LDAP
541 self
.accounts
._authenticate
()
542 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
545 account
= self
.get_by_dn(dn
)
547 # Optionally set country code
549 account
.country_code
= country_code
556 def create_session(self
, account
, host
):
557 session_id
= util
.random_string(64)
559 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
560 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
562 # Session could not be created
566 logging
.info("Created session %s for %s which expires %s" \
567 % (res
.session_id
, account
, res
.time_expires
))
568 return res
.session_id
, res
.time_expires
570 def destroy_session(self
, session_id
, host
):
571 logging
.info("Destroying session %s" % session_id
)
573 self
.db
.execute("DELETE FROM sessions \
574 WHERE session_id = %s AND host = %s", session_id
, host
)
576 def get_by_session(self
, session_id
, host
):
577 logging
.debug("Looking up session %s" % session_id
)
579 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
580 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
583 # Session does not exist or has expired
587 # Update the session expiration time
588 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
589 WHERE session_id = %s AND host = %s", session_id
, host
)
591 return self
.get_by_uid(res
.uid
)
594 # Cleanup expired sessions
595 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
597 # Cleanup expired account activations
598 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
600 # Cleanup expired account password resets
601 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
603 async def _delete(self
, *args
, **kwargs
):
608 who
= self
.get_by_uid("ms")
611 account
= self
.get_by_uid(uid
)
614 with self
.db
.transaction():
615 await account
.delete(who
)
619 def decode_discourse_payload(self
, payload
, signature
):
621 calculated_signature
= self
.sign_discourse_payload(payload
)
623 if not hmac
.compare_digest(signature
, calculated_signature
):
624 raise ValueError("Invalid signature: %s" % signature
)
626 # Decode the query string
627 qs
= base64
.b64decode(payload
).decode()
629 # Parse the query string
631 for key
, val
in urllib
.parse
.parse_qsl(qs
):
636 def encode_discourse_payload(self
, **args
):
637 # Encode the arguments into an URL-formatted string
638 qs
= urllib
.parse
.urlencode(args
).encode()
641 return base64
.b64encode(qs
).decode()
643 def sign_discourse_payload(self
, payload
, secret
=None):
645 secret
= self
.settings
.get("discourse_sso_secret")
647 # Calculate a HMAC using SHA256
648 h
= hmac
.new(secret
.encode(),
649 msg
=payload
.encode(), digestmod
="sha256")
657 for country
in iso3166
.countries
:
658 count
= self
._count
("(&(objectClass=person)(st=%s))" % country
.alpha2
)
665 async def get_all_emails(self
):
666 # Returns all email addresses
667 for dn
, attrs
in self
._query
("(objectClass=person)", attrlist
=("mail",)):
668 mails
= attrs
.get("mail", None)
676 class Account(LDAPObject
):
684 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
686 def __lt__(self
, other
):
687 if isinstance(other
, self
.__class
__):
688 return self
.name
< other
.name
690 return NotImplemented
693 def kerberos_principal_dn(self
):
694 return "krbPrincipalName=%s@IPFIRE.ORG,cn=IPFIRE.ORG,cn=krb5,dc=ipfire,dc=org" % self
.uid
697 def kerberos_attributes(self
):
698 res
= self
.backend
.accounts
._query
(
699 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
701 "krbLastSuccessfulAuth",
702 "krbLastPasswordChange",
704 "krbLoginFailedCount",
707 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
709 for dn
, attrs
in res
:
710 return { key
: attrs
[key
][0] for key
in attrs
}
715 def last_successful_authentication(self
):
717 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
721 return self
._parse
_date
(s
)
724 def last_failed_authentication(self
):
726 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
730 return self
._parse
_date
(s
)
733 def failed_login_count(self
):
735 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
744 def passwd(self
, password
):
748 # The new password must have a score of 3 or better
749 quality
= self
.check_password_quality(password
)
750 if quality
["score"] < 3:
751 raise ValueError("Password too weak")
753 self
.accounts
._authenticate
()
754 self
.ldap
.passwd_s(self
.dn
, None, password
)
756 def check_password(self
, password
):
758 Bind to the server with given credentials and return
759 true if password is corrent and false if not.
761 Raises exceptions from the server on any other errors.
766 logging
.debug("Checking credentials for %s" % self
.dn
)
768 # Check the credentials against the Kerberos database
770 kerberos
.checkPassword(self
.uid
, password
, "host/%s" % FQDN
, "IPFIRE.ORG")
772 # Catch any authentication errors
773 except kerberos
.BasicAuthError
as e
:
774 logging
.debug("Could not authenticate %s: %s" % (self
.uid
, e
))
778 # Otherwise return True
780 logging
.info("Successfully authenticated %s" % self
)
784 def check_password_quality(self
, password
):
786 Passwords are passed through zxcvbn to make sure
787 that they are strong enough.
789 return zxcvbn
.zxcvbn(password
, user_inputs
=(
790 self
.first_name
, self
.last_name
,
793 def request_password_reset(self
, address
=None):
794 reset_code
= util
.random_string(64)
796 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
797 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
799 # Send a password reset email
800 self
.backend
.messages
.send_template("auth/messages/password-reset",
801 priority
=100, account
=self
, reset_code
=reset_code
)
803 def reset_password(self
, reset_code
, new_password
):
804 # Delete the reset token
805 res
= self
.db
.query("DELETE FROM account_password_resets \
806 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
807 RETURNING *", self
.uid
, reset_code
)
809 # The reset code was invalid
811 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
813 # Perform password change
814 return self
.passwd(new_password
)
817 return self
.is_member_of_group("sudo")
820 return self
.is_member_of_group("staff")
822 def is_moderator(self
):
823 return self
.is_member_of_group("moderators")
826 return "posixAccount" in self
.classes
829 return "postfixMailUser" in self
.classes
833 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
836 return self
.is_member_of_group("lwl-staff")
838 def can_be_managed_by(self
, account
):
840 Returns True if account is allowed to manage this account
842 # Admins can manage all accounts
843 if account
.is_admin():
846 # Users can manage themselves
847 return self
== account
851 return self
._get
_strings
("objectClass")
855 return self
._get
_string
("uid")
859 return self
._get
_string
("cn")
863 async def delete(self
, user
):
867 # Check if this user can be deleted
868 if not self
.can_be_deleted_by(user
):
869 raise RuntimeError("Cannot delete user %s" % self
)
871 logging
.info("Deleting user %s" % self
)
873 async with asyncio
.TaskGroup() as tasks
:
874 t
= datetime
.datetime
.now()
876 # Disable this account on Bugzilla
878 self
._disable
_on
_bugzilla
("Deleted by %s, %s" % (user
, t
)),
881 # XXX Delete on Discourse
886 def can_be_deleted_by(self
, user
):
888 Return True if the user can be deleted by user
891 if not self
.can_be_managed_by(user
):
894 # Cannot delete shell users
903 Deletes this object from LDAP
905 # Delete the Kerberos Principal
906 self
._delete
_dn
(self
.kerberos_principal_dn
)
909 self
._delete
_dn
(self
.dn
)
913 def get_nickname(self
):
914 return self
._get
_string
("displayName")
916 def set_nickname(self
, nickname
):
917 self
._set
_string
("displayName", nickname
)
919 nickname
= property(get_nickname
, set_nickname
)
923 def get_first_name(self
):
924 return self
._get
_string
("givenName")
926 def set_first_name(self
, first_name
):
927 self
._set
_string
("givenName", first_name
)
930 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
932 first_name
= property(get_first_name
, set_first_name
)
936 def get_last_name(self
):
937 return self
._get
_string
("sn")
939 def set_last_name(self
, last_name
):
940 self
._set
_string
("sn", last_name
)
943 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
945 last_name
= property(get_last_name
, set_last_name
)
949 return self
.backend
.groups
._get
_groups
("(| \
950 (&(objectClass=groupOfNames)(member=%s)) \
951 (&(objectClass=posixGroup)(memberUid=%s)) \
952 )" % (self
.dn
, self
.uid
))
954 def is_member_of_group(self
, gid
):
956 Returns True if this account is a member of this group
958 return gid
in (g
.gid
for g
in self
.groups
)
960 # Created/Modified at
963 def created_at(self
):
964 return self
._get
_timestamp
("createTimestamp")
967 def modified_at(self
):
968 return self
._get
_timestamp
("modifyTimestamp")
977 address
+= self
.street
.splitlines()
979 if self
.postal_code
and self
.city
:
980 if self
.country_code
in ("AT", "DE"):
981 address
.append("%s %s" % (self
.postal_code
, self
.city
))
983 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
985 address
.append(self
.city
or self
.postal_code
)
987 if self
.country_name
:
988 address
.append(self
.country_name
)
990 return [line
for line
in address
if line
]
992 def get_street(self
):
993 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
995 def set_street(self
, street
):
996 self
._set
_string
("street", street
)
998 street
= property(get_street
, set_street
)
1001 return self
._get
_string
("l") or ""
1003 def set_city(self
, city
):
1004 self
._set
_string
("l", city
)
1006 city
= property(get_city
, set_city
)
1008 def get_postal_code(self
):
1009 return self
._get
_string
("postalCode") or ""
1011 def set_postal_code(self
, postal_code
):
1012 self
._set
_string
("postalCode", postal_code
)
1014 postal_code
= property(get_postal_code
, set_postal_code
)
1016 # XXX This should be c
1017 def get_country_code(self
):
1018 return self
._get
_string
("st")
1020 def set_country_code(self
, country_code
):
1021 self
._set
_string
("st", country_code
)
1023 country_code
= property(get_country_code
, set_country_code
)
1026 def country_name(self
):
1027 if self
.country_code
:
1028 return self
.backend
.get_country_name(self
.country_code
)
1032 return self
._get
_string
("mail")
1036 return "%s <%s>" % (self
, self
.email
)
1039 def alternate_email_addresses(self
):
1040 addresses
= self
._get
_strings
("mailAlternateAddress")
1042 return sorted(addresses
)
1044 # Mail Routing Address
1046 def get_mail_routing_address(self
):
1047 return self
._get
_string
("mailRoutingAddress", None)
1049 def set_mail_routing_address(self
, address
):
1050 self
._set
_string
("mailRoutingAddress", address
or None)
1052 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
1056 if "sipUser" in self
.classes
:
1057 return self
._get
_string
("sipAuthenticationUser")
1059 if "sipRoutingObject" in self
.classes
:
1060 return self
._get
_string
("sipLocalAddress")
1063 def sip_password(self
):
1064 return self
._get
_string
("sipPassword")
1067 def _generate_sip_password():
1068 return util
.random_string(8)
1072 return "%s@ipfire.org" % self
.sip_id
1075 def agent_status(self
):
1076 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
1078 def uses_sip_forwarding(self
):
1079 if self
.sip_routing_address
:
1086 def get_sip_routing_address(self
):
1087 if "sipRoutingObject" in self
.classes
:
1088 return self
._get
_string
("sipRoutingAddress")
1090 def set_sip_routing_address(self
, address
):
1094 # Don't do anything if nothing has changed
1095 if self
.get_sip_routing_address() == address
:
1099 # This is no longer a SIP user any more
1102 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
1103 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
1104 (ldap
.MOD_DELETE
, "sipPassword", None),
1106 except ldap
.NO_SUCH_ATTRIBUTE
:
1109 # Set new routing object
1112 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
1113 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
1114 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
1117 # If this is a change, we cannot add this again
1118 except ldap
.TYPE_OR_VALUE_EXISTS
:
1119 self
._set
_string
("sipRoutingAddress", address
)
1123 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
1124 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
1125 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
1127 except ldap
.NO_SUCH_ATTRIBUTE
:
1131 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
1132 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
1133 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
1136 # XXX Cache is invalid here
1138 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
1141 def sip_registrations(self
):
1142 sip_registrations
= []
1144 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
1147 sip_registrations
.append(reg
)
1149 return sip_registrations
1152 def sip_channels(self
):
1153 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
1155 def get_cdr(self
, date
=None, limit
=None):
1156 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
1161 def phone_number(self
):
1163 Returns the IPFire phone number
1166 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
1169 def fax_number(self
):
1171 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
1173 def get_phone_numbers(self
):
1176 for field
in ("telephoneNumber", "homePhone", "mobile"):
1177 for number
in self
._get
_phone
_numbers
(field
):
1182 def set_phone_numbers(self
, phone_numbers
):
1183 # Sort phone numbers by landline and mobile
1184 _landline_numbers
= []
1185 _mobile_numbers
= []
1187 for number
in phone_numbers
:
1189 number
= phonenumbers
.parse(number
, None)
1190 except phonenumbers
.phonenumberutil
.NumberParseException
:
1193 # Convert to string (in E.164 format)
1194 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1196 # Separate mobile numbers
1197 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1198 _mobile_numbers
.append(s
)
1200 _landline_numbers
.append(s
)
1203 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1204 self
._set
_strings
("mobile", _mobile_numbers
)
1206 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1209 def _all_telephone_numbers(self
):
1210 ret
= [ self
.sip_id
, ]
1212 if self
.phone_number
:
1213 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1216 for number
in self
.phone_numbers
:
1217 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1224 def get_description(self
):
1225 return self
._get
_string
("description")
1227 def set_description(self
, description
):
1228 self
._set
_string
("description", description
)
1230 description
= property(get_description
, set_description
)
1234 def has_avatar(self
):
1235 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
1236 if has_avatar
is None:
1237 has_avatar
= True if self
.get_avatar() else False
1239 # Cache avatar status for up to 24 hours
1240 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1244 def avatar_url(self
, size
=None, absolute
=False):
1245 url
= "/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1247 # Return an absolute URL
1249 url
= urllib
.parse
.urljoin("https://people.ipfire.org", url
)
1252 url
+= "&size=%s" % size
1256 def get_avatar(self
, size
=None):
1257 photo
= self
._get
_bytes
("jpegPhoto")
1259 # Exit if no avatar is available
1263 # Return the raw image if no size was requested
1267 # Try to retrieve something from the cache
1268 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1272 # Generate a new thumbnail
1273 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1275 # Save to cache for 15m
1276 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1281 def avatar_hash(self
):
1282 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1284 h
= hashlib
.new("md5")
1285 h
.update(self
.get_avatar() or b
"")
1286 hash = h
.hexdigest()[:7]
1288 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1292 def upload_avatar(self
, avatar
):
1293 self
._set
("jpegPhoto", avatar
)
1295 # Delete cached avatar status
1296 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1298 # Delete avatar hash
1299 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1301 # Consent to promotional emails
1303 def get_consents_to_promotional_emails(self
):
1304 return self
.is_member_of_group("promotional-consent")
1306 def set_contents_to_promotional_emails(self
, value
):
1307 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1308 assert group
, "Could not find group: promotional-consent"
1311 group
.add_member(self
)
1313 group
.del_member(self
)
1315 consents_to_promotional_emails
= property(
1316 get_consents_to_promotional_emails
,
1317 set_contents_to_promotional_emails
,
1322 async def _disable_on_bugzilla(self
, text
=None):
1324 Disables the user on Bugzilla
1326 user
= await self
.backend
.bugzilla
.get_user(self
.email
)
1328 # Do nothing if the user does not exist
1333 await user
.disable(text
)
1336 class Groups(Object
):
1338 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1339 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1341 # Everyone is a member of people
1342 "cn=people,ou=Group,dc=ipfire,dc=org",
1346 def search_base(self
):
1347 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1349 def _query(self
, *args
, **kwargs
):
1351 "search_base" : self
.backend
.groups
.search_base
,
1354 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1357 groups
= self
.get_all()
1361 def _get_groups(self
, query
, **kwargs
):
1362 res
= self
._query
(query
, **kwargs
)
1365 for dn
, attrs
in res
:
1366 # Skip any hidden groups
1367 if dn
in self
.hidden_groups
:
1370 g
= Group(self
.backend
, dn
, attrs
)
1373 return sorted(groups
)
1375 def _get_group(self
, query
, **kwargs
):
1380 groups
= self
._get
_groups
(query
, **kwargs
)
1385 return self
._get
_groups
(
1386 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1389 def get_by_gid(self
, gid
):
1390 return self
._get
_group
(
1391 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1395 class Group(LDAPObject
):
1397 if self
.description
:
1398 return "<%s %s (%s)>" % (
1399 self
.__class
__.__name
__,
1404 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1407 return self
.description
or self
.gid
1409 def __lt__(self
, other
):
1410 if isinstance(other
, self
.__class
__):
1411 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1413 return NotImplemented
1420 Returns the number of members in this group
1424 for attr
in ("member", "memberUid"):
1425 a
= self
.attributes
.get(attr
, None)
1432 return iter(self
.members
)
1436 return self
._get
_string
("cn")
1439 def description(self
):
1440 return self
._get
_string
("description")
1444 return self
._get
_string
("mail")
1450 # Get all members by DN
1451 for dn
in self
._get
_strings
("member"):
1452 member
= self
.backend
.accounts
.get_by_dn(dn
)
1454 members
.append(member
)
1456 # Get all members by UID
1457 for uid
in self
._get
_strings
("memberUid"):
1458 member
= self
.backend
.accounts
.get_by_uid(uid
)
1460 members
.append(member
)
1462 return sorted(members
)
1464 def add_member(self
, account
):
1466 Adds a member to this group
1468 # Do nothing if this user is already in the group
1469 if account
.is_member_of_group(self
.gid
):
1472 if "posixGroup" in self
.objectclasses
:
1473 self
._add
_string
("memberUid", account
.uid
)
1475 self
._add
_string
("member", account
.dn
)
1477 # Append to cached list of members
1478 self
.members
.append(account
)
1481 def del_member(self
, account
):
1483 Removes a member from a group
1485 # Do nothing if this user is not in the group
1486 if not account
.is_member_of_group(self
.gid
):
1489 if "posixGroup" in self
.objectclasses
:
1490 self
._delete
_string
("memberUid", account
.uid
)
1492 self
._delete
_string
("member", account
.dn
)
1495 if __name__
== "__main__":