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
832 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
835 return self
.is_member_of_group("lwl-staff")
837 def can_be_managed_by(self
, account
):
839 Returns True if account is allowed to manage this account
841 # Admins can manage all accounts
842 if account
.is_admin():
845 # Users can manage themselves
846 return self
== account
850 return self
._get
_strings
("objectClass")
854 return self
._get
_string
("uid")
858 return self
._get
_string
("cn")
862 async def delete(self
, user
):
866 # Check if this user can be deleted
867 if not self
.can_be_deleted_by(user
):
868 raise RuntimeError("Cannot delete user %s" % self
)
870 logging
.info("Deleting user %s" % self
)
872 async with asyncio
.TaskGroup() as tasks
:
873 t
= datetime
.datetime
.now()
875 # Disable this account on Bugzilla
877 self
._disable
_on
_bugzilla
("Deleted by %s, %s" % (user
, t
)),
880 # XXX Delete on Discourse
885 def can_be_deleted_by(self
, user
):
887 Return True if the user can be deleted by user
890 if not self
.can_be_managed_by(user
):
893 # Cannot delete shell users
902 Deletes this object from LDAP
904 # Delete the Kerberos Principal
905 self
._delete
_dn
(self
.kerberos_principal_dn
)
908 self
._delete
_dn
(self
.dn
)
912 def get_nickname(self
):
913 return self
._get
_string
("displayName")
915 def set_nickname(self
, nickname
):
916 self
._set
_string
("displayName", nickname
)
918 nickname
= property(get_nickname
, set_nickname
)
922 def get_first_name(self
):
923 return self
._get
_string
("givenName")
925 def set_first_name(self
, first_name
):
926 self
._set
_string
("givenName", first_name
)
929 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
931 first_name
= property(get_first_name
, set_first_name
)
935 def get_last_name(self
):
936 return self
._get
_string
("sn")
938 def set_last_name(self
, last_name
):
939 self
._set
_string
("sn", last_name
)
942 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
944 last_name
= property(get_last_name
, set_last_name
)
948 return self
.backend
.groups
._get
_groups
("(| \
949 (&(objectClass=groupOfNames)(member=%s)) \
950 (&(objectClass=posixGroup)(memberUid=%s)) \
951 )" % (self
.dn
, self
.uid
))
953 def is_member_of_group(self
, gid
):
955 Returns True if this account is a member of this group
957 return gid
in (g
.gid
for g
in self
.groups
)
959 # Created/Modified at
962 def created_at(self
):
963 return self
._get
_timestamp
("createTimestamp")
966 def modified_at(self
):
967 return self
._get
_timestamp
("modifyTimestamp")
976 address
+= self
.street
.splitlines()
978 if self
.postal_code
and self
.city
:
979 if self
.country_code
in ("AT", "DE"):
980 address
.append("%s %s" % (self
.postal_code
, self
.city
))
982 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
984 address
.append(self
.city
or self
.postal_code
)
986 if self
.country_name
:
987 address
.append(self
.country_name
)
989 return [line
for line
in address
if line
]
991 def get_street(self
):
992 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
994 def set_street(self
, street
):
995 self
._set
_string
("street", street
)
997 street
= property(get_street
, set_street
)
1000 return self
._get
_string
("l") or ""
1002 def set_city(self
, city
):
1003 self
._set
_string
("l", city
)
1005 city
= property(get_city
, set_city
)
1007 def get_postal_code(self
):
1008 return self
._get
_string
("postalCode") or ""
1010 def set_postal_code(self
, postal_code
):
1011 self
._set
_string
("postalCode", postal_code
)
1013 postal_code
= property(get_postal_code
, set_postal_code
)
1015 # XXX This should be c
1016 def get_country_code(self
):
1017 return self
._get
_string
("st")
1019 def set_country_code(self
, country_code
):
1020 self
._set
_string
("st", country_code
)
1022 country_code
= property(get_country_code
, set_country_code
)
1025 def country_name(self
):
1026 if self
.country_code
:
1027 return self
.backend
.get_country_name(self
.country_code
)
1031 return self
._get
_string
("mail")
1035 return "%s <%s>" % (self
, self
.email
)
1038 def alternate_email_addresses(self
):
1039 addresses
= self
._get
_strings
("mailAlternateAddress")
1041 return sorted(addresses
)
1043 # Mail Routing Address
1045 def get_mail_routing_address(self
):
1046 return self
._get
_string
("mailRoutingAddress", None)
1048 def set_mail_routing_address(self
, address
):
1049 self
._set
_string
("mailRoutingAddress", address
or None)
1051 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
1055 if "sipUser" in self
.classes
:
1056 return self
._get
_string
("sipAuthenticationUser")
1058 if "sipRoutingObject" in self
.classes
:
1059 return self
._get
_string
("sipLocalAddress")
1062 def sip_password(self
):
1063 return self
._get
_string
("sipPassword")
1066 def _generate_sip_password():
1067 return util
.random_string(8)
1071 return "%s@ipfire.org" % self
.sip_id
1073 def uses_sip_forwarding(self
):
1074 if self
.sip_routing_address
:
1081 def get_sip_routing_address(self
):
1082 if "sipRoutingObject" in self
.classes
:
1083 return self
._get
_string
("sipRoutingAddress")
1085 def set_sip_routing_address(self
, address
):
1089 # Don't do anything if nothing has changed
1090 if self
.get_sip_routing_address() == address
:
1094 # This is no longer a SIP user any more
1097 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
1098 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
1099 (ldap
.MOD_DELETE
, "sipPassword", None),
1101 except ldap
.NO_SUCH_ATTRIBUTE
:
1104 # Set new routing object
1107 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
1108 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
1109 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
1112 # If this is a change, we cannot add this again
1113 except ldap
.TYPE_OR_VALUE_EXISTS
:
1114 self
._set
_string
("sipRoutingAddress", address
)
1118 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
1119 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
1120 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
1122 except ldap
.NO_SUCH_ATTRIBUTE
:
1126 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
1127 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
1128 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
1131 # XXX Cache is invalid here
1133 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
1137 async def get_sip_registrations(self
):
1138 if not self
.has_sip():
1141 return await self
.backend
.asterisk
.get_registrations(self
.sip_id
)
1145 async def get_sip_channels(self
):
1146 if not self
.has_sip():
1149 return await self
.backend
.asterisk
.get_sip_channels(self
.sip_id
)
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 Groups(Object
):
1331 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1332 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1334 # Everyone is a member of people
1335 "cn=people,ou=Group,dc=ipfire,dc=org",
1339 def search_base(self
):
1340 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1342 def _query(self
, *args
, **kwargs
):
1344 "search_base" : self
.backend
.groups
.search_base
,
1347 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1350 groups
= self
.get_all()
1354 def _get_groups(self
, query
, **kwargs
):
1355 res
= self
._query
(query
, **kwargs
)
1358 for dn
, attrs
in res
:
1359 # Skip any hidden groups
1360 if dn
in self
.hidden_groups
:
1363 g
= Group(self
.backend
, dn
, attrs
)
1366 return sorted(groups
)
1368 def _get_group(self
, query
, **kwargs
):
1373 groups
= self
._get
_groups
(query
, **kwargs
)
1378 return self
._get
_groups
(
1379 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1382 def get_by_gid(self
, gid
):
1383 return self
._get
_group
(
1384 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1388 class Group(LDAPObject
):
1390 if self
.description
:
1391 return "<%s %s (%s)>" % (
1392 self
.__class
__.__name
__,
1397 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1400 return self
.description
or self
.gid
1402 def __lt__(self
, other
):
1403 if isinstance(other
, self
.__class
__):
1404 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1406 return NotImplemented
1413 Returns the number of members in this group
1417 for attr
in ("member", "memberUid"):
1418 a
= self
.attributes
.get(attr
, None)
1425 return iter(self
.members
)
1429 return self
._get
_string
("cn")
1432 def description(self
):
1433 return self
._get
_string
("description")
1437 return self
._get
_string
("mail")
1443 # Get all members by DN
1444 for dn
in self
._get
_strings
("member"):
1445 member
= self
.backend
.accounts
.get_by_dn(dn
)
1447 members
.append(member
)
1449 # Get all members by UID
1450 for uid
in self
._get
_strings
("memberUid"):
1451 member
= self
.backend
.accounts
.get_by_uid(uid
)
1453 members
.append(member
)
1455 return sorted(members
)
1457 def add_member(self
, account
):
1459 Adds a member to this group
1461 # Do nothing if this user is already in the group
1462 if account
.is_member_of_group(self
.gid
):
1465 if "posixGroup" in self
.objectclasses
:
1466 self
._add
_string
("memberUid", account
.uid
)
1468 self
._add
_string
("member", account
.dn
)
1470 # Append to cached list of members
1471 self
.members
.append(account
)
1474 def del_member(self
, account
):
1476 Removes a member from a group
1478 # Do nothing if this user is not in the group
1479 if not account
.is_member_of_group(self
.gid
):
1482 if "posixGroup" in self
.objectclasses
:
1483 self
._delete
_string
("memberUid", account
.uid
)
1485 self
._delete
_string
("member", account
.dn
)
1488 if __name__
== "__main__":