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
)
761 # Check the credentials against the Kerberos database
763 kerberos
.checkPassword(self
.uid
, password
, "host/%s" % FQDN
, "IPFIRE.ORG")
765 # Catch any authentication errors
766 except kerberos
.BasicAuthError
as e
:
767 logging
.debug("Could not authenticate %s: %s" % (self
.uid
, e
))
771 # Otherwise return True
773 logging
.info("Successfully authenticated %s" % self
)
777 def check_password_quality(self
, password
):
779 Passwords are passed through zxcvbn to make sure
780 that they are strong enough.
782 return zxcvbn
.zxcvbn(password
, user_inputs
=(
783 self
.first_name
, self
.last_name
,
786 def request_password_reset(self
, address
=None):
787 reset_code
= util
.random_string(64)
789 self
.db
.execute("INSERT INTO account_password_resets(uid, reset_code, address) \
790 VALUES(%s, %s, %s)", self
.uid
, reset_code
, address
)
792 # Send a password reset email
793 self
.backend
.messages
.send_template("auth/messages/password-reset",
794 priority
=100, account
=self
, reset_code
=reset_code
)
796 def reset_password(self
, reset_code
, new_password
):
797 # Delete the reset token
798 res
= self
.db
.query("DELETE FROM account_password_resets \
799 WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \
800 RETURNING *", self
.uid
, reset_code
)
802 # The reset code was invalid
804 raise ValueError("Invalid password reset token for %s: %s" % (self
, reset_code
))
806 # Perform password change
807 return self
.passwd(new_password
)
810 return self
.is_member_of_group("sudo")
813 return self
.is_member_of_group("staff")
815 def is_moderator(self
):
816 return self
.is_member_of_group("moderators")
819 return "posixAccount" in self
.classes
822 return "postfixMailUser" in self
.classes
825 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes
827 def is_blog_author(self
):
828 return self
.is_member_of_group("blog-authors")
831 return self
.is_member_of_group("lwl-staff")
833 def can_be_managed_by(self
, account
):
835 Returns True if account is allowed to manage this account
837 # Admins can manage all accounts
838 if account
.is_admin():
841 # Users can manage themselves
842 return self
== account
846 return self
._get
_strings
("objectClass")
850 return self
._get
_string
("uid")
854 return self
._get
_string
("cn")
858 async def delete(self
, user
):
862 # Check if this user can be deleted
863 if not self
.can_be_deleted_by(user
):
864 raise RuntimeError("Cannot delete user %s" % self
)
866 logging
.info("Deleting user %s" % self
)
868 async with asyncio
.TaskGroup() as tasks
:
869 t
= datetime
.datetime
.now()
871 # Disable this account on Bugzilla
873 self
._disable
_on
_bugzilla
("Deleted by %s, %s" % (user
, t
)),
876 # XXX Delete on Discourse
881 def can_be_deleted_by(self
, user
):
883 Return True if the user can be deleted by user
886 if not self
.can_be_managed_by(user
):
889 # Cannot delete shell users
898 Deletes this object from LDAP
900 # Delete the Kerberos Principal
901 self
._delete
_dn
(self
.kerberos_principal_dn
)
904 self
._delete
_dn
(self
.dn
)
908 def get_nickname(self
):
909 return self
._get
_string
("displayName")
911 def set_nickname(self
, nickname
):
912 self
._set
_string
("displayName", nickname
)
914 nickname
= property(get_nickname
, set_nickname
)
918 def get_first_name(self
):
919 return self
._get
_string
("givenName")
921 def set_first_name(self
, first_name
):
922 self
._set
_string
("givenName", first_name
)
925 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
927 first_name
= property(get_first_name
, set_first_name
)
931 def get_last_name(self
):
932 return self
._get
_string
("sn")
934 def set_last_name(self
, last_name
):
935 self
._set
_string
("sn", last_name
)
938 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
940 last_name
= property(get_last_name
, set_last_name
)
944 return self
.backend
.groups
._get
_groups
("(| \
945 (&(objectClass=groupOfNames)(member=%s)) \
946 (&(objectClass=posixGroup)(memberUid=%s)) \
947 )" % (self
.dn
, self
.uid
))
949 def is_member_of_group(self
, gid
):
951 Returns True if this account is a member of this group
953 return gid
in (g
.gid
for g
in self
.groups
)
955 # Created/Modified at
958 def created_at(self
):
959 return self
._get
_timestamp
("createTimestamp")
962 def modified_at(self
):
963 return self
._get
_timestamp
("modifyTimestamp")
972 address
+= self
.street
.splitlines()
974 if self
.postal_code
and self
.city
:
975 if self
.country_code
in ("AT", "DE"):
976 address
.append("%s %s" % (self
.postal_code
, self
.city
))
978 address
.append("%s, %s" % (self
.city
, self
.postal_code
))
980 address
.append(self
.city
or self
.postal_code
)
982 if self
.country_name
:
983 address
.append(self
.country_name
)
985 return [line
for line
in address
if line
]
987 def get_street(self
):
988 return self
._get
_string
("street") or self
._get
_string
("homePostalAddress")
990 def set_street(self
, street
):
991 self
._set
_string
("street", street
)
993 street
= property(get_street
, set_street
)
996 return self
._get
_string
("l") or ""
998 def set_city(self
, city
):
999 self
._set
_string
("l", city
)
1001 city
= property(get_city
, set_city
)
1003 def get_postal_code(self
):
1004 return self
._get
_string
("postalCode") or ""
1006 def set_postal_code(self
, postal_code
):
1007 self
._set
_string
("postalCode", postal_code
)
1009 postal_code
= property(get_postal_code
, set_postal_code
)
1011 def get_state(self
):
1012 return self
._get
_string
("st")
1014 def set_state(self
, state
):
1015 self
._set
_string
("st", state
)
1017 state
= property(get_state
, set_state
)
1019 def get_country_code(self
):
1020 return self
._get
_string
("c")
1022 def set_country_code(self
, country_code
):
1023 self
._set
_string
("c", country_code
)
1025 country_code
= property(get_country_code
, set_country_code
)
1028 def country_name(self
):
1029 if self
.country_code
:
1030 return self
.backend
.get_country_name(self
.country_code
)
1036 # If a nickname is set, only use the nickname
1038 for m
in re
.findall(r
"(\w+)", self
.nickname
):
1039 initials
.append(m
[0])
1041 # Otherwise use the first and last name
1044 initials
.append(self
.first_name
[0])
1047 initials
.append(self
.last_name
[0])
1049 # Truncate to two initials
1050 initials
= initials
[:2]
1052 return [i
.upper() for i
in initials
]
1058 return self
._get
_string
("mail")
1062 return "%s <%s>" % (self
, self
.email
)
1065 def alternate_email_addresses(self
):
1066 addresses
= self
._get
_strings
("mailAlternateAddress")
1068 return sorted(addresses
)
1070 # Mail Routing Address
1072 def get_mail_routing_address(self
):
1073 return self
._get
_string
("mailRoutingAddress", None)
1075 def set_mail_routing_address(self
, address
):
1076 self
._set
_string
("mailRoutingAddress", address
or None)
1078 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
1082 if "sipUser" in self
.classes
:
1083 return self
._get
_string
("sipAuthenticationUser")
1085 if "sipRoutingObject" in self
.classes
:
1086 return self
._get
_string
("sipLocalAddress")
1089 def sip_password(self
):
1090 return self
._get
_string
("sipPassword")
1093 def _generate_sip_password():
1094 return util
.random_string(8)
1098 return "%s@ipfire.org" % self
.sip_id
1100 def uses_sip_forwarding(self
):
1101 if self
.sip_routing_address
:
1108 def get_sip_routing_address(self
):
1109 if "sipRoutingObject" in self
.classes
:
1110 return self
._get
_string
("sipRoutingAddress")
1112 def set_sip_routing_address(self
, address
):
1116 # Don't do anything if nothing has changed
1117 if self
.get_sip_routing_address() == address
:
1121 # This is no longer a SIP user any more
1124 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
1125 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
1126 (ldap
.MOD_DELETE
, "sipPassword", None),
1128 except ldap
.NO_SUCH_ATTRIBUTE
:
1131 # Set new routing object
1134 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
1135 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
1136 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
1139 # If this is a change, we cannot add this again
1140 except ldap
.TYPE_OR_VALUE_EXISTS
:
1141 self
._set
_string
("sipRoutingAddress", address
)
1145 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
1146 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
1147 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
1149 except ldap
.NO_SUCH_ATTRIBUTE
:
1153 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
1154 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
1155 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
1158 # XXX Cache is invalid here
1160 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
1164 async def get_sip_registrations(self
):
1165 if not self
.has_sip():
1168 return await self
.backend
.asterisk
.get_registrations(self
.sip_id
)
1172 async def get_sip_channels(self
):
1173 if not self
.has_sip():
1176 return await self
.backend
.asterisk
.get_sip_channels(self
.sip_id
)
1181 def phone_number(self
):
1183 Returns the IPFire phone number
1186 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
1189 def fax_number(self
):
1191 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
1193 def get_phone_numbers(self
):
1196 for field
in ("telephoneNumber", "homePhone", "mobile"):
1197 for number
in self
._get
_phone
_numbers
(field
):
1202 def set_phone_numbers(self
, phone_numbers
):
1203 # Sort phone numbers by landline and mobile
1204 _landline_numbers
= []
1205 _mobile_numbers
= []
1207 for number
in phone_numbers
:
1209 number
= phonenumbers
.parse(number
, None)
1210 except phonenumbers
.phonenumberutil
.NumberParseException
:
1213 # Convert to string (in E.164 format)
1214 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1216 # Separate mobile numbers
1217 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1218 _mobile_numbers
.append(s
)
1220 _landline_numbers
.append(s
)
1223 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1224 self
._set
_strings
("mobile", _mobile_numbers
)
1226 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1229 def _all_telephone_numbers(self
):
1230 ret
= [ self
.sip_id
, ]
1232 if self
.phone_number
:
1233 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1236 for number
in self
.phone_numbers
:
1237 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1244 def get_description(self
):
1245 return self
._get
_string
("description")
1247 def set_description(self
, description
):
1248 self
._set
_string
("description", description
)
1250 description
= property(get_description
, set_description
)
1255 def avatar_hash(self
):
1256 # Fetch the timestamp (or fall back to the last LDAP change)
1257 t
= self
._fetch
_avatar
_timestamp
() or self
.modified_at
1259 # Create the payload
1260 payload
= "%s-%s" % (self
.uid
, t
)
1262 # Compute a hash over the payload
1263 h
= hashlib
.new("blake2b", payload
.encode())
1265 return h
.hexdigest()[:7]
1267 def avatar_url(self
, size
=None, absolute
=False):
1268 # This cannot be async because we are calling it from the template engine
1269 url
= "/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1271 # Return an absolute URL
1273 url
= urllib
.parse
.urljoin("https://www.ipfire.org", url
)
1276 url
+= "&size=%s" % size
1280 async def get_avatar(self
, size
=None):
1281 # Check the PostgreSQL database
1282 photo
= self
._fetch
_avatar
()
1286 photo
= self
._get
_bytes
("jpegPhoto")
1288 # Exit if no avatar is available
1292 # Return the raw image if no size was requested
1296 # Compose the cache key
1297 key
= "accounts:%s:avatar:%s:%s" % (self
.uid
, self
.avatar_hash
, size
)
1299 # Try to retrieve something from the cache
1300 avatar
= await self
.backend
.cache
.get(key
)
1304 # Generate a new thumbnail
1305 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1307 # Save to cache for 24h
1308 await self
.backend
.cache
.set(key
, avatar
, 86400)
1312 def _fetch_avatar(self
):
1314 Fetches the original avatar blob as being uploaded by the user
1316 res
= self
.db
.get("""
1331 def _fetch_avatar_timestamp(self
):
1332 res
= self
.db
.get("""
1345 return res
.created_at
1347 async def upload_avatar(self
, avatar
):
1348 # Remove all previous avatars
1353 deleted_at = CURRENT_TIMESTAMP
1361 # Store the new avatar in the database
1373 """, self
.uid
, avatar
,
1376 # Remove anything in the LDAP database
1377 photo
= self
._get
_bytes
("jpegPhoto")
1379 self
._delete
("jpegPhoto", [photo
])
1381 # Consent to promotional emails
1383 def get_consents_to_promotional_emails(self
):
1384 return self
.is_member_of_group("promotional-consent")
1386 def set_contents_to_promotional_emails(self
, value
):
1387 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1388 assert group
, "Could not find group: promotional-consent"
1391 group
.add_member(self
)
1393 group
.del_member(self
)
1395 consents_to_promotional_emails
= property(
1396 get_consents_to_promotional_emails
,
1397 set_contents_to_promotional_emails
,
1402 async def _disable_on_bugzilla(self
, text
=None):
1404 Disables the user on Bugzilla
1406 user
= await self
.backend
.bugzilla
.get_user(self
.email
)
1408 # Do nothing if the user does not exist
1413 await user
.disable(text
)
1417 async def get_lists(self
):
1418 return await self
.backend
.lists
.get_subscribed_lists(self
)
1421 class Groups(Object
):
1423 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1424 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1426 # Everyone is a member of people
1427 "cn=people,ou=Group,dc=ipfire,dc=org",
1431 def search_base(self
):
1432 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1434 def _query(self
, *args
, **kwargs
):
1436 "search_base" : self
.backend
.groups
.search_base
,
1439 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1442 groups
= self
.get_all()
1446 def _get_groups(self
, query
, **kwargs
):
1447 res
= self
._query
(query
, **kwargs
)
1450 for dn
, attrs
in res
:
1451 # Skip any hidden groups
1452 if dn
in self
.hidden_groups
:
1455 g
= Group(self
.backend
, dn
, attrs
)
1458 return sorted(groups
)
1460 def _get_group(self
, query
, **kwargs
):
1465 groups
= self
._get
_groups
(query
, **kwargs
)
1470 return self
._get
_groups
(
1471 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1474 def get_by_gid(self
, gid
):
1475 return self
._get
_group
(
1476 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1480 class Group(LDAPObject
):
1482 if self
.description
:
1483 return "<%s %s (%s)>" % (
1484 self
.__class
__.__name
__,
1489 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1492 return self
.description
or self
.gid
1494 def __lt__(self
, other
):
1495 if isinstance(other
, self
.__class
__):
1496 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1498 return NotImplemented
1505 Returns the number of members in this group
1509 for attr
in ("member", "memberUid"):
1510 a
= self
.attributes
.get(attr
, None)
1517 return iter(self
.members
)
1521 return self
._get
_string
("cn")
1524 def description(self
):
1525 return self
._get
_string
("description")
1529 return self
._get
_string
("mail")
1535 # Get all members by DN
1536 for dn
in self
._get
_strings
("member"):
1537 member
= self
.backend
.accounts
.get_by_dn(dn
)
1539 members
.append(member
)
1541 # Get all members by UID
1542 for uid
in self
._get
_strings
("memberUid"):
1543 member
= self
.backend
.accounts
.get_by_uid(uid
)
1545 members
.append(member
)
1547 return sorted(members
)
1549 def add_member(self
, account
):
1551 Adds a member to this group
1553 # Do nothing if this user is already in the group
1554 if account
.is_member_of_group(self
.gid
):
1557 if "posixGroup" in self
.objectclasses
:
1558 self
._add
_string
("memberUid", account
.uid
)
1560 self
._add
_string
("member", account
.dn
)
1562 # Append to cached list of members
1563 self
.members
.append(account
)
1566 def del_member(self
, account
):
1568 Removes a member from a group
1570 # Do nothing if this user is not in the group
1571 if not account
.is_member_of_group(self
.gid
):
1574 if "posixGroup" in self
.objectclasses
:
1575 self
._delete
_string
("memberUid", account
.uid
)
1577 self
._delete
_string
("member", account
.dn
)
1580 if __name__
== "__main__":