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 return self
._count
("(objectClass=person)")
184 accounts
= self
._search
("(objectClass=person)")
186 return iter(sorted(accounts
))
190 # Connect to LDAP server
191 ldap_uri
= self
.settings
.get("ldap_uri")
193 logging
.debug("Connecting to LDAP server: %s" % ldap_uri
)
195 # Connect to the LDAP server
196 connection
= ldap
.ldapobject
.ReconnectLDAPObject(ldap_uri
,
197 trace_level
=2 if self
.backend
.debug
else 0,
198 retry_max
=sys
.maxsize
, retry_delay
=3)
200 # Set maximum timeout for operations
201 connection
.set_option(ldap
.OPT_TIMEOUT
, 10)
205 def _authenticate(self
):
206 # Authenticate against LDAP server using Kerberos
207 self
.ldap
.sasl_gssapi_bind_s()
209 async def test_ldap(self
):
210 logging
.info("Testing LDAP connection...")
214 logging
.info("Successfully authenticated as %s" % self
.ldap
.whoami_s())
216 def _query(self
, query
, attrlist
=None, limit
=0, search_base
=None):
217 logging
.debug("Performing LDAP query (%s): %s" \
218 % (search_base
or self
.search_base
, query
))
222 # Ask for up to 512 results being returned at a time
223 page_control
= ldap
.controls
.SimplePagedResultsControl(True, size
=512, cookie
="")
230 response
= self
.ldap
.search_ext(search_base
or self
.search_base
,
231 ldap
.SCOPE_SUBTREE
, query
, attrlist
=attrlist
, sizelimit
=limit
,
232 serverctrls
=[page_control
],
236 type, data
, rmsgid
, serverctrls
= self
.ldap
.result3(response
)
238 # Append to local copy
242 controls
= [c
for c
in serverctrls
243 if c
.controlType
== ldap
.controls
.SimplePagedResultsControl
.controlType
]
248 # Set the cookie for more results
249 page_control
.cookie
= controls
[0].cookie
251 # There are no more results
252 if not page_control
.cookie
:
255 # Log time it took to perform the query
256 logging
.debug("Query took %.2fms (%s page(s))" % ((time
.time() - t
) * 1000.0, pages
))
260 def _count(self
, query
):
261 res
= self
._query
(query
, attrlist
=["dn"])
265 def _search(self
, query
, attrlist
=None, limit
=0):
267 for dn
, attrs
in self
._query
(query
, attrlist
=["dn"], limit
=limit
):
268 account
= self
.get_by_dn(dn
)
269 accounts
.append(account
)
273 def _get_attrs(self
, dn
):
275 Fetches all attributes for the given distinguished name
277 results
= self
._query
("(objectClass=*)", search_base
=dn
, limit
=1,
278 attrlist
=("*", "createTimestamp", "modifyTimestamp"))
280 for dn
, attrs
in results
:
283 def get_by_dn(self
, dn
):
284 attrs
= self
._get
_attrs
(dn
)
286 return Account(self
.backend
, dn
, attrs
)
290 return t
.strftime("%Y%m%d%H%M%SZ")
292 def get_recently_registered(self
, limit
=None):
293 # Check the last two weeks
294 t
= datetime
.datetime
.utcnow() - datetime
.timedelta(days
=14)
296 # Fetch all accounts created after t
297 accounts
= self
.get_created_after(t
)
299 # Order by creation date and put latest first
300 accounts
.sort(key
=lambda a
: a
.created_at
, reverse
=True)
303 if accounts
and limit
:
304 accounts
= accounts
[:limit
]
308 def get_created_after(self
, ts
):
309 return self
._search
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
311 def count_created_after(self
, ts
):
312 return self
._count
("(&(objectClass=person)(createTimestamp>=%s))" % self
._format
_date
(ts
))
314 def search(self
, query
):
315 # Try finding an exact match
316 account
= self
._search
_one
(
318 "(objectClass=person)"
322 "(mailAlternateAddress=%s)"
324 ")" % (query
, query
, query
))
328 # Otherwise search for a substring match
329 accounts
= self
._search
(
331 "(objectClass=person)"
338 ")" % (query
, query
, query
, query
))
340 return sorted(accounts
)
342 def _search_one(self
, query
):
343 results
= self
._search
(query
, limit
=1)
345 for result
in results
:
348 def uid_is_valid(self
, uid
):
349 # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
350 m
= re
.match(r
"^[a-z_][a-z0-9_-]{3,31}$", uid
)
356 def uid_exists(self
, uid
):
357 if self
.get_by_uid(uid
):
360 res
= self
.db
.get("SELECT 1 FROM account_activations \
361 WHERE uid = %s AND expires_at > NOW()", uid
)
366 # Account with uid does not exist, yet
369 def mail_is_valid(self
, mail
):
370 username
, delim
, domain
= mail
.partition("@")
372 # There must be an @ and a domain part
376 # The domain cannot end on a dot
377 if domain
.endswith("."):
380 # The domain should at least have one dot to fully qualified
381 if not "." in domain
:
384 # Looks like a valid email address
387 def mail_is_blacklisted(self
, mail
):
388 username
, delim
, domain
= mail
.partition("@")
391 return self
.domain_is_blacklisted(domain
)
393 def domain_is_blacklisted(self
, domain
):
394 res
= self
.db
.get("SELECT TRUE AS found FROM blacklisted_domains \
395 WHERE domain = %s OR %s LIKE '%%.' || domain", domain
, domain
)
397 if res
and res
.found
:
402 def get_by_uid(self
, uid
):
403 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
405 def get_by_mail(self
, mail
):
406 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
408 def find_account(self
, s
):
409 account
= self
.get_by_uid(s
)
413 return self
.get_by_mail(s
)
415 def get_by_sip_id(self
, sip_id
):
419 return self
._search
_one
(
420 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
423 def get_by_phone_number(self
, number
):
427 return self
._search
_one
(
428 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
429 % (number
, number
, number
, number
))
432 def pending_registrations(self
):
433 res
= self
.db
.get("SELECT COUNT(*) AS c FROM account_activations")
437 def auth(self
, username
, password
):
439 account
= self
.backend
.accounts
.find_account(username
)
442 if account
and account
.check_password(password
):
447 def join(self
, uid
, email
, first_name
, last_name
, country_code
=None):
448 # Convert all uids to lowercase
451 # Check if UID is valid
452 if not self
.uid_is_valid(uid
):
453 raise ValueError("UID is invalid: %s" % uid
)
455 # Check if UID is unique
456 if self
.uid_exists(uid
):
457 raise ValueError("UID exists: %s" % uid
)
459 # Check if the email address is valid
460 if not self
.mail_is_valid(email
):
461 raise ValueError("Email is invalid: %s" % email
)
463 # Check if the email address is blacklisted
464 if self
.mail_is_blacklisted(email
):
465 raise ValueError("Email is blacklisted: %s" % email
)
467 # Generate a random activation code
468 activation_code
= util
.random_string(36)
470 # Create an entry in our database until the user
471 # has activated the account
472 self
.db
.execute("INSERT INTO account_activations(uid, activation_code, \
473 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
474 uid
, activation_code
, email
, first_name
, last_name
, country_code
)
476 # Send an account activation email
477 self
.backend
.messages
.send_template("auth/messages/join",
478 priority
=100, uid
=uid
, activation_code
=activation_code
, email
=email
,
479 first_name
=first_name
, last_name
=last_name
)
481 def activate(self
, uid
, activation_code
):
482 res
= self
.db
.get("DELETE FROM account_activations \
483 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
484 RETURNING *", uid
, activation_code
)
486 # Return nothing when account was not found
490 # Return the account if it has already been created
491 account
= self
.get_by_uid(uid
)
495 # Create a new account on the LDAP database
496 account
= self
.create(uid
, res
.email
,
497 first_name
=res
.first_name
, last_name
=res
.last_name
,
498 country_code
=res
.country_code
)
500 # Non-EU users do not need to consent to promo emails
501 if account
.country_code
and not account
.country_code
in countries
.EU_COUNTRIES
:
502 account
.consents_to_promotional_emails
= True
504 # Send email about account registration
505 self
.backend
.messages
.send_template("people/messages/new-account",
508 # Launch drip campaigns
509 for campaign
in ("signup", "christmas"):
510 self
.backend
.campaigns
.launch(campaign
, account
)
514 def create(self
, uid
, email
, first_name
, last_name
, country_code
=None):
515 cn
= "%s %s" % (first_name
, last_name
)
519 "objectClass" : [b
"top", b
"person", b
"inetOrgPerson"],
520 "mail" : email
.encode(),
524 "sn" : last_name
.encode(),
525 "givenName" : first_name
.encode(),
528 logging
.info("Creating new account: %s: %s" % (uid
, account
))
531 dn
= "uid=%s,ou=People,dc=ipfire,dc=org" % uid
533 # Create account on LDAP
534 self
.accounts
._authenticate
()
535 self
.ldap
.add_s(dn
, ldap
.modlist
.addModlist(account
))
538 account
= self
.get_by_dn(dn
)
540 # Optionally set country code
542 account
.country_code
= country_code
549 def create_session(self
, account
, host
):
550 session_id
= util
.random_string(64)
552 res
= self
.db
.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
553 RETURNING session_id, time_expires", host
, account
.uid
, session_id
)
555 # Session could not be created
559 logging
.info("Created session %s for %s which expires %s" \
560 % (res
.session_id
, account
, res
.time_expires
))
561 return res
.session_id
, res
.time_expires
563 def destroy_session(self
, session_id
, host
):
564 logging
.info("Destroying session %s" % session_id
)
566 self
.db
.execute("DELETE FROM sessions \
567 WHERE session_id = %s AND host = %s", session_id
, host
)
569 def get_by_session(self
, session_id
, host
):
570 logging
.debug("Looking up session %s" % session_id
)
572 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
573 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
576 # Session does not exist or has expired
580 # Update the session expiration time
581 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
582 WHERE session_id = %s AND host = %s", session_id
, host
)
584 return self
.get_by_uid(res
.uid
)
587 # Cleanup expired sessions
588 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
590 # Cleanup expired account activations
591 self
.db
.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
593 # Cleanup expired account password resets
594 self
.db
.execute("DELETE FROM account_password_resets WHERE expires_at <= NOW()")
596 async def _delete(self
, *args
, **kwargs
):
601 who
= self
.get_by_uid("ms")
604 account
= self
.get_by_uid(uid
)
607 with self
.db
.transaction():
608 await account
.delete(who
)
612 def decode_discourse_payload(self
, payload
, signature
):
614 calculated_signature
= self
.sign_discourse_payload(payload
)
616 if not hmac
.compare_digest(signature
, calculated_signature
):
617 raise ValueError("Invalid signature: %s" % signature
)
619 # Decode the query string
620 qs
= base64
.b64decode(payload
).decode()
622 # Parse the query string
624 for key
, val
in urllib
.parse
.parse_qsl(qs
):
629 def encode_discourse_payload(self
, **args
):
630 # Encode the arguments into an URL-formatted string
631 qs
= urllib
.parse
.urlencode(args
).encode()
634 return base64
.b64encode(qs
).decode()
636 def sign_discourse_payload(self
, payload
, secret
=None):
638 secret
= self
.settings
.get("discourse_sso_secret")
640 # Calculate a HMAC using SHA256
641 h
= hmac
.new(secret
.encode(),
642 msg
=payload
.encode(), digestmod
="sha256")
650 for country
in iso3166
.countries
:
651 count
= self
._count
("(&(objectClass=person)(c=%s))" % country
.alpha2
)
658 async def get_all_emails(self
):
659 # Returns all email addresses
660 for dn
, attrs
in self
._query
("(objectClass=person)", attrlist
=("mail",)):
661 mails
= attrs
.get("mail", None)
669 class Account(LDAPObject
):
677 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
679 def __lt__(self
, other
):
680 if isinstance(other
, self
.__class
__):
681 return self
.name
< other
.name
683 return NotImplemented
686 def kerberos_principal_dn(self
):
687 return "krbPrincipalName=%s@IPFIRE.ORG,cn=IPFIRE.ORG,cn=krb5,dc=ipfire,dc=org" % self
.uid
690 def kerberos_attributes(self
):
691 res
= self
.backend
.accounts
._query
(
692 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self
.uid
,
694 "krbLastSuccessfulAuth",
695 "krbLastPasswordChange",
697 "krbLoginFailedCount",
700 search_base
="cn=krb5,%s" % self
.backend
.accounts
.search_base
)
702 for dn
, attrs
in res
:
703 return { key
: attrs
[key
][0] for key
in attrs
}
708 def last_successful_authentication(self
):
710 s
= self
.kerberos_attributes
["krbLastSuccessfulAuth"]
714 return self
._parse
_date
(s
)
717 def last_failed_authentication(self
):
719 s
= self
.kerberos_attributes
["krbLastFailedAuth"]
723 return self
._parse
_date
(s
)
726 def failed_login_count(self
):
728 count
= self
.kerberos_attributes
["krbLoginFailedCount"].decode()
737 def passwd(self
, password
):
741 # The new password must have a score of 3 or better
742 quality
= self
.check_password_quality(password
)
743 if quality
["score"] < 3:
744 raise ValueError("Password too weak")
746 self
.accounts
._authenticate
()
747 self
.ldap
.passwd_s(self
.dn
, None, password
)
749 def check_password(self
, password
):
751 Bind to the server with given credentials and return
752 true if password is corrent and false if not.
754 Raises exceptions from the server on any other errors.
759 logging
.debug("Checking credentials for %s" % self
.dn
)
762 os
.environ
["KRB5_KTNAME"] = "/etc/ipfire.org/www.keytab"
764 # Check the credentials against the Kerberos database
766 kerberos
.checkPassword(self
.uid
, password
, "www/%s" % FQDN
, "IPFIRE.ORG")
768 # Catch any authentication errors
769 except kerberos
.BasicAuthError
as e
:
770 logging
.debug("Could not authenticate %s: %s" % (self
.uid
, e
))
774 # Otherwise return True
776 logging
.info("Successfully authenticated %s" % self
)
780 def check_password_quality(self
, password
):
782 Passwords are passed through zxcvbn to make sure
783 that they are strong enough.
785 return zxcvbn
.zxcvbn(password
, user_inputs
=(
786 self
.first_name
, self
.last_name
,
789 def request_password_reset(self
, address
=None):
790 reset_code
= util
.random_string(64)
792 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
793 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
795 # Send a password reset email
796 self
.backend
.messages
.send_template("auth/messages/password-reset",
797 priority
=100, account
=self
, reset_code
=reset_code
)
799 def reset_password(self
, reset_code
, new_password
):
800 # Delete the reset token
801 res
= self
.db
.query("DELETE FROM account_password_resets \
802 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
803 RETURNING *", self
.uid
, reset_code
)
805 # The reset code was invalid
807 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
809 # Perform password change
810 return self
.passwd(new_password
)
813 return self
.is_member_of_group("sudo")
816 return self
.is_member_of_group("staff")
818 def is_moderator(self
):
819 return self
.is_member_of_group("moderators")
822 return "posixAccount" in self
.classes
825 return "postfixMailUser" in self
.classes
828 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
830 def is_blog_author(self
):
831 return self
.is_member_of_group("blog-authors")
834 return self
.is_member_of_group("lwl-staff")
836 def can_be_managed_by(self
, account
):
838 Returns True if account is allowed to manage this account
840 # Admins can manage all accounts
841 if account
.is_admin():
844 # Users can manage themselves
845 return self
== account
849 return self
._get
_strings
("objectClass")
853 return self
._get
_string
("uid")
857 return self
._get
_string
("cn")
861 async def delete(self
, user
):
865 # Check if this user can be deleted
866 if not self
.can_be_deleted_by(user
):
867 raise RuntimeError("Cannot delete user %s" % self
)
869 logging
.info("Deleting user %s" % self
)
871 async with asyncio
.TaskGroup() as tasks
:
872 t
= datetime
.datetime
.now()
874 # Disable this account on Bugzilla
876 self
._disable
_on
_bugzilla
("Deleted by %s, %s" % (user
, t
)),
879 # XXX Delete on Discourse
884 def can_be_deleted_by(self
, user
):
886 Return True if the user can be deleted by user
889 if not self
.can_be_managed_by(user
):
892 # Cannot delete shell users
901 Deletes this object from LDAP
903 # Delete the Kerberos Principal
904 self
._delete
_dn
(self
.kerberos_principal_dn
)
907 self
._delete
_dn
(self
.dn
)
911 def get_nickname(self
):
912 return self
._get
_string
("displayName")
914 def set_nickname(self
, nickname
):
915 self
._set
_string
("displayName", nickname
)
917 nickname
= property(get_nickname
, set_nickname
)
921 def get_first_name(self
):
922 return self
._get
_string
("givenName")
924 def set_first_name(self
, first_name
):
925 self
._set
_string
("givenName", first_name
)
928 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
930 first_name
= property(get_first_name
, set_first_name
)
934 def get_last_name(self
):
935 return self
._get
_string
("sn")
937 def set_last_name(self
, last_name
):
938 self
._set
_string
("sn", last_name
)
941 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
943 last_name
= property(get_last_name
, set_last_name
)
947 return self
.backend
.groups
._get
_groups
("(| \
948 (&(objectClass=groupOfNames)(member=%s)) \
949 (&(objectClass=posixGroup)(memberUid=%s)) \
950 )" % (self
.dn
, self
.uid
))
952 def is_member_of_group(self
, gid
):
954 Returns True if this account is a member of this group
956 return gid
in (g
.gid
for g
in self
.groups
)
958 # Created/Modified at
961 def created_at(self
):
962 return self
._get
_timestamp
("createTimestamp")
965 def modified_at(self
):
966 return self
._get
_timestamp
("modifyTimestamp")
975 address
+= self
.street
.splitlines()
977 if self
.postal_code
and self
.city
:
978 if self
.country_code
in ("AT", "DE"):
979 address
.append("%s %s" % (self
.postal_code
, self
.city
))
981 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
983 address
.append(self
.city
or self
.postal_code
)
985 if self
.country_name
:
986 address
.append(self
.country_name
)
988 return [line
for line
in address
if line
]
990 def get_street(self
):
991 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
993 def set_street(self
, street
):
994 self
._set
_string
("street", street
)
996 street
= property(get_street
, set_street
)
999 return self
._get
_string
("l") or ""
1001 def set_city(self
, city
):
1002 self
._set
_string
("l", city
)
1004 city
= property(get_city
, set_city
)
1006 def get_postal_code(self
):
1007 return self
._get
_string
("postalCode") or ""
1009 def set_postal_code(self
, postal_code
):
1010 self
._set
_string
("postalCode", postal_code
)
1012 postal_code
= property(get_postal_code
, set_postal_code
)
1014 def get_state(self
):
1015 return self
._get
_string
("st")
1017 def set_state(self
, state
):
1018 self
._set
_string
("st", state
)
1020 state
= property(get_state
, set_state
)
1022 def get_country_code(self
):
1023 return self
._get
_string
("c")
1025 def set_country_code(self
, country_code
):
1026 self
._set
_string
("c", country_code
)
1028 country_code
= property(get_country_code
, set_country_code
)
1031 def country_name(self
):
1032 if self
.country_code
:
1033 return self
.backend
.get_country_name(self
.country_code
)
1039 # If a nickname is set, only use the nickname
1040 if self
.nickname
and len(self
.nickname
) >= 2:
1041 for m
in re
.findall(r
"(\w+)", self
.nickname
):
1042 initials
.append(m
[0])
1044 # If we only detected one character, we will use the first two
1045 if len(initials
) < 2:
1046 initials
= [self
.nickname
[0], self
.nickname
[1]]
1048 # Otherwise use the first and last name
1051 initials
.append(self
.first_name
[0])
1054 initials
.append(self
.last_name
[0])
1056 # Truncate to two initials
1057 initials
= initials
[:2]
1059 return [i
.upper() for i
in initials
]
1065 return self
._get
_string
("mail")
1069 return "%s <%s>" % (self
, self
.email
)
1072 def alternate_email_addresses(self
):
1073 addresses
= self
._get
_strings
("mailAlternateAddress")
1075 return sorted(addresses
)
1077 # Mail Routing Address
1079 def get_mail_routing_address(self
):
1080 return self
._get
_string
("mailRoutingAddress", None)
1082 def set_mail_routing_address(self
, address
):
1083 self
._set
_string
("mailRoutingAddress", address
or None)
1085 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
1089 if "sipUser" in self
.classes
:
1090 return self
._get
_string
("sipAuthenticationUser")
1092 if "sipRoutingObject" in self
.classes
:
1093 return self
._get
_string
("sipLocalAddress")
1096 def sip_password(self
):
1097 return self
._get
_string
("sipPassword")
1100 def _generate_sip_password():
1101 return util
.random_string(8)
1105 return "%s@ipfire.org" % self
.sip_id
1107 def uses_sip_forwarding(self
):
1108 if self
.sip_routing_address
:
1115 def get_sip_routing_address(self
):
1116 if "sipRoutingObject" in self
.classes
:
1117 return self
._get
_string
("sipRoutingAddress")
1119 def set_sip_routing_address(self
, address
):
1123 # Don't do anything if nothing has changed
1124 if self
.get_sip_routing_address() == address
:
1128 # This is no longer a SIP user any more
1131 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
1132 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
1133 (ldap
.MOD_DELETE
, "sipPassword", None),
1135 except ldap
.NO_SUCH_ATTRIBUTE
:
1138 # Set new routing object
1141 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
1142 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
1143 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
1146 # If this is a change, we cannot add this again
1147 except ldap
.TYPE_OR_VALUE_EXISTS
:
1148 self
._set
_string
("sipRoutingAddress", address
)
1152 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
1153 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
1154 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
1156 except ldap
.NO_SUCH_ATTRIBUTE
:
1160 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
1161 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
1162 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
1165 # XXX Cache is invalid here
1167 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
1171 async def get_sip_registrations(self
):
1172 if not self
.has_sip():
1175 return await self
.backend
.asterisk
.get_registrations(self
.sip_id
)
1179 async def get_sip_channels(self
):
1180 if not self
.has_sip():
1183 return await self
.backend
.asterisk
.get_sip_channels(self
.sip_id
)
1188 def phone_number(self
):
1190 Returns the IPFire phone number
1193 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
1196 def fax_number(self
):
1198 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
1200 def get_phone_numbers(self
):
1203 for field
in ("telephoneNumber", "homePhone", "mobile"):
1204 for number
in self
._get
_phone
_numbers
(field
):
1209 def set_phone_numbers(self
, phone_numbers
):
1210 # Sort phone numbers by landline and mobile
1211 _landline_numbers
= []
1212 _mobile_numbers
= []
1214 for number
in phone_numbers
:
1216 number
= phonenumbers
.parse(number
, None)
1217 except phonenumbers
.phonenumberutil
.NumberParseException
:
1220 # Convert to string (in E.164 format)
1221 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1223 # Separate mobile numbers
1224 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1225 _mobile_numbers
.append(s
)
1227 _landline_numbers
.append(s
)
1230 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1231 self
._set
_strings
("mobile", _mobile_numbers
)
1233 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1236 def _all_telephone_numbers(self
):
1237 ret
= [ self
.sip_id
, ]
1239 if self
.phone_number
:
1240 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1243 for number
in self
.phone_numbers
:
1244 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1251 def get_description(self
):
1252 return self
._get
_string
("description")
1254 def set_description(self
, description
):
1255 self
._set
_string
("description", description
)
1257 description
= property(get_description
, set_description
)
1262 def avatar_hash(self
):
1263 # Fetch the timestamp (or fall back to the last LDAP change)
1264 t
= self
._fetch
_avatar
_timestamp
() or self
.modified_at
1266 # Create the payload
1267 payload
= "%s-%s" % (self
.uid
, t
)
1269 # Compute a hash over the payload
1270 h
= hashlib
.new("blake2b", payload
.encode())
1272 return h
.hexdigest()[:7]
1274 def avatar_url(self
, size
=None, absolute
=False):
1275 # This cannot be async because we are calling it from the template engine
1276 url
= "/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1278 # Return an absolute URL
1280 url
= urllib
.parse
.urljoin("https://www.ipfire.org", url
)
1283 url
+= "&size=%s" % size
1287 async def get_avatar(self
, size
=None, format
=None):
1288 # Check the PostgreSQL database
1289 photo
= self
._fetch
_avatar
()
1293 photo
= self
._get
_bytes
("jpegPhoto")
1295 # Exit if no avatar is available
1299 # Return the raw image if no size was requested
1303 # Compose the cache key
1304 key
= "accounts:%s:avatar:%s:%s:%s" \
1305 % (self
.uid
, self
.avatar_hash
, format
or "N/A", size
)
1307 # Try to retrieve something from the cache
1308 avatar
= await self
.backend
.cache
.get(key
)
1312 # Generate a new thumbnail
1313 avatar
= util
.generate_thumbnail(photo
, size
, square
=True, format
=format
)
1315 # Save to cache for 24h
1316 await self
.backend
.cache
.set(key
, avatar
, 86400)
1320 def _fetch_avatar(self
):
1322 Fetches the original avatar blob as being uploaded by the user
1324 res
= self
.db
.get("""
1339 def _fetch_avatar_timestamp(self
):
1340 res
= self
.db
.get("""
1353 return res
.created_at
1355 async def upload_avatar(self
, avatar
):
1356 # Remove all previous avatars
1361 deleted_at = CURRENT_TIMESTAMP
1369 # Store the new avatar in the database
1381 """, self
.uid
, avatar
,
1384 # Remove anything in the LDAP database
1385 photo
= self
._get
_bytes
("jpegPhoto")
1387 self
._delete
("jpegPhoto", [photo
])
1389 # Consent to promotional emails
1391 def get_consents_to_promotional_emails(self
):
1392 return self
.is_member_of_group("promotional-consent")
1394 def set_contents_to_promotional_emails(self
, value
):
1395 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1396 assert group
, "Could not find group: promotional-consent"
1399 group
.add_member(self
)
1401 group
.del_member(self
)
1403 consents_to_promotional_emails
= property(
1404 get_consents_to_promotional_emails
,
1405 set_contents_to_promotional_emails
,
1410 async def _disable_on_bugzilla(self
, text
=None):
1412 Disables the user on Bugzilla
1414 user
= await self
.backend
.bugzilla
.get_user(self
.email
)
1416 # Do nothing if the user does not exist
1421 await user
.disable(text
)
1425 async def get_lists(self
):
1426 return await self
.backend
.lists
.get_subscribed_lists(self
)
1429 class Groups(Object
):
1431 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1432 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1434 # Everyone is a member of people
1435 "cn=people,ou=Group,dc=ipfire,dc=org",
1439 def search_base(self
):
1440 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1442 def _query(self
, *args
, **kwargs
):
1444 "search_base" : self
.backend
.groups
.search_base
,
1447 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1450 groups
= self
.get_all()
1454 def _get_groups(self
, query
, **kwargs
):
1455 res
= self
._query
(query
, **kwargs
)
1458 for dn
, attrs
in res
:
1459 # Skip any hidden groups
1460 if dn
in self
.hidden_groups
:
1463 g
= Group(self
.backend
, dn
, attrs
)
1466 return sorted(groups
)
1468 def _get_group(self
, query
, **kwargs
):
1473 groups
= self
._get
_groups
(query
, **kwargs
)
1478 return self
._get
_groups
(
1479 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1482 def get_by_gid(self
, gid
):
1483 return self
._get
_group
(
1484 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1488 class Group(LDAPObject
):
1490 if self
.description
:
1491 return "<%s %s (%s)>" % (
1492 self
.__class
__.__name
__,
1497 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1500 return self
.description
or self
.gid
1502 def __lt__(self
, other
):
1503 if isinstance(other
, self
.__class
__):
1504 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1506 return NotImplemented
1513 Returns the number of members in this group
1517 for attr
in ("member", "memberUid"):
1518 a
= self
.attributes
.get(attr
, None)
1525 return iter(self
.members
)
1529 return self
._get
_string
("cn")
1532 def description(self
):
1533 return self
._get
_string
("description")
1537 return self
._get
_string
("mail")
1543 # Get all members by DN
1544 for dn
in self
._get
_strings
("member"):
1545 member
= self
.backend
.accounts
.get_by_dn(dn
)
1547 members
.append(member
)
1549 # Get all members by UID
1550 for uid
in self
._get
_strings
("memberUid"):
1551 member
= self
.backend
.accounts
.get_by_uid(uid
)
1553 members
.append(member
)
1555 return sorted(members
)
1557 def add_member(self
, account
):
1559 Adds a member to this group
1561 # Do nothing if this user is already in the group
1562 if account
.is_member_of_group(self
.gid
):
1565 if "posixGroup" in self
.objectclasses
:
1566 self
._add
_string
("memberUid", account
.uid
)
1568 self
._add
_string
("member", account
.dn
)
1570 # Append to cached list of members
1571 self
.members
.append(account
)
1574 def del_member(self
, account
):
1576 Removes a member from a group
1578 # Do nothing if this user is not in the group
1579 if not account
.is_member_of_group(self
.gid
):
1582 if "posixGroup" in self
.objectclasses
:
1583 self
._delete
_string
("memberUid", account
.uid
)
1585 self
._delete
_string
("member", account
.dn
)
1588 if __name__
== "__main__":