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
1074 def agent_status(self
):
1075 return self
.backend
.talk
.freeswitch
.get_agent_status(self
)
1077 def uses_sip_forwarding(self
):
1078 if self
.sip_routing_address
:
1085 def get_sip_routing_address(self
):
1086 if "sipRoutingObject" in self
.classes
:
1087 return self
._get
_string
("sipRoutingAddress")
1089 def set_sip_routing_address(self
, address
):
1093 # Don't do anything if nothing has changed
1094 if self
.get_sip_routing_address() == address
:
1098 # This is no longer a SIP user any more
1101 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
1102 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
1103 (ldap
.MOD_DELETE
, "sipPassword", None),
1105 except ldap
.NO_SUCH_ATTRIBUTE
:
1108 # Set new routing object
1111 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
1112 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
1113 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
1116 # If this is a change, we cannot add this again
1117 except ldap
.TYPE_OR_VALUE_EXISTS
:
1118 self
._set
_string
("sipRoutingAddress", address
)
1122 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
1123 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
1124 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
1126 except ldap
.NO_SUCH_ATTRIBUTE
:
1130 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
1131 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
1132 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
1135 # XXX Cache is invalid here
1137 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
1141 async def get_sip_registrations(self
):
1142 if not self
.has_sip():
1145 return await self
.backend
.asterisk
.get_registrations(self
.sip_id
)
1149 async def get_sip_channels(self
):
1150 if not self
.has_sip():
1153 return await self
.backend
.asterisk
.get_sip_channels(self
.sip_id
)
1158 def phone_number(self
):
1160 Returns the IPFire phone number
1163 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
1166 def fax_number(self
):
1168 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
1170 def get_phone_numbers(self
):
1173 for field
in ("telephoneNumber", "homePhone", "mobile"):
1174 for number
in self
._get
_phone
_numbers
(field
):
1179 def set_phone_numbers(self
, phone_numbers
):
1180 # Sort phone numbers by landline and mobile
1181 _landline_numbers
= []
1182 _mobile_numbers
= []
1184 for number
in phone_numbers
:
1186 number
= phonenumbers
.parse(number
, None)
1187 except phonenumbers
.phonenumberutil
.NumberParseException
:
1190 # Convert to string (in E.164 format)
1191 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1193 # Separate mobile numbers
1194 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
1195 _mobile_numbers
.append(s
)
1197 _landline_numbers
.append(s
)
1200 self
._set
_strings
("telephoneNumber", _landline_numbers
)
1201 self
._set
_strings
("mobile", _mobile_numbers
)
1203 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
1206 def _all_telephone_numbers(self
):
1207 ret
= [ self
.sip_id
, ]
1209 if self
.phone_number
:
1210 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
1213 for number
in self
.phone_numbers
:
1214 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
1221 def get_description(self
):
1222 return self
._get
_string
("description")
1224 def set_description(self
, description
):
1225 self
._set
_string
("description", description
)
1227 description
= property(get_description
, set_description
)
1231 def has_avatar(self
):
1232 has_avatar
= self
.memcache
.get("accounts:%s:has-avatar" % self
.uid
)
1233 if has_avatar
is None:
1234 has_avatar
= True if self
.get_avatar() else False
1236 # Cache avatar status for up to 24 hours
1237 self
.memcache
.set("accounts:%s:has-avatar" % self
.uid
, has_avatar
, 3600 * 24)
1241 def avatar_url(self
, size
=None, absolute
=False):
1242 url
= "/users/%s.jpg?h=%s" % (self
.uid
, self
.avatar_hash
)
1244 # Return an absolute URL
1246 url
= urllib
.parse
.urljoin("https://people.ipfire.org", url
)
1249 url
+= "&size=%s" % size
1253 def get_avatar(self
, size
=None):
1254 photo
= self
._get
_bytes
("jpegPhoto")
1256 # Exit if no avatar is available
1260 # Return the raw image if no size was requested
1264 # Try to retrieve something from the cache
1265 avatar
= self
.memcache
.get("accounts:%s:avatar:%s" % (self
.dn
, size
))
1269 # Generate a new thumbnail
1270 avatar
= util
.generate_thumbnail(photo
, size
, square
=True)
1272 # Save to cache for 15m
1273 self
.memcache
.set("accounts:%s:avatar:%s" % (self
.dn
, size
), avatar
, 900)
1278 def avatar_hash(self
):
1279 hash = self
.memcache
.get("accounts:%s:avatar-hash" % self
.dn
)
1281 h
= hashlib
.new("md5")
1282 h
.update(self
.get_avatar() or b
"")
1283 hash = h
.hexdigest()[:7]
1285 self
.memcache
.set("accounts:%s:avatar-hash" % self
.dn
, hash, 86400)
1289 def upload_avatar(self
, avatar
):
1290 self
._set
("jpegPhoto", avatar
)
1292 # Delete cached avatar status
1293 self
.memcache
.delete("accounts:%s:has-avatar" % self
.dn
)
1295 # Delete avatar hash
1296 self
.memcache
.delete("accounts:%s:avatar-hash" % self
.dn
)
1298 # Consent to promotional emails
1300 def get_consents_to_promotional_emails(self
):
1301 return self
.is_member_of_group("promotional-consent")
1303 def set_contents_to_promotional_emails(self
, value
):
1304 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
1305 assert group
, "Could not find group: promotional-consent"
1308 group
.add_member(self
)
1310 group
.del_member(self
)
1312 consents_to_promotional_emails
= property(
1313 get_consents_to_promotional_emails
,
1314 set_contents_to_promotional_emails
,
1319 async def _disable_on_bugzilla(self
, text
=None):
1321 Disables the user on Bugzilla
1323 user
= await self
.backend
.bugzilla
.get_user(self
.email
)
1325 # Do nothing if the user does not exist
1330 await user
.disable(text
)
1333 class Groups(Object
):
1335 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1336 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1338 # Everyone is a member of people
1339 "cn=people,ou=Group,dc=ipfire,dc=org",
1343 def search_base(self
):
1344 return "ou=Group,%s" % self
.backend
.accounts
.search_base
1346 def _query(self
, *args
, **kwargs
):
1348 "search_base" : self
.backend
.groups
.search_base
,
1351 return self
.backend
.accounts
._query
(*args
, **kwargs
)
1354 groups
= self
.get_all()
1358 def _get_groups(self
, query
, **kwargs
):
1359 res
= self
._query
(query
, **kwargs
)
1362 for dn
, attrs
in res
:
1363 # Skip any hidden groups
1364 if dn
in self
.hidden_groups
:
1367 g
= Group(self
.backend
, dn
, attrs
)
1370 return sorted(groups
)
1372 def _get_group(self
, query
, **kwargs
):
1377 groups
= self
._get
_groups
(query
, **kwargs
)
1382 return self
._get
_groups
(
1383 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1386 def get_by_gid(self
, gid
):
1387 return self
._get
_group
(
1388 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid
,
1392 class Group(LDAPObject
):
1394 if self
.description
:
1395 return "<%s %s (%s)>" % (
1396 self
.__class
__.__name
__,
1401 return "<%s %s>" % (self
.__class
__.__name
__, self
.gid
)
1404 return self
.description
or self
.gid
1406 def __lt__(self
, other
):
1407 if isinstance(other
, self
.__class
__):
1408 return (self
.description
or self
.gid
) < (other
.description
or other
.gid
)
1410 return NotImplemented
1417 Returns the number of members in this group
1421 for attr
in ("member", "memberUid"):
1422 a
= self
.attributes
.get(attr
, None)
1429 return iter(self
.members
)
1433 return self
._get
_string
("cn")
1436 def description(self
):
1437 return self
._get
_string
("description")
1441 return self
._get
_string
("mail")
1447 # Get all members by DN
1448 for dn
in self
._get
_strings
("member"):
1449 member
= self
.backend
.accounts
.get_by_dn(dn
)
1451 members
.append(member
)
1453 # Get all members by UID
1454 for uid
in self
._get
_strings
("memberUid"):
1455 member
= self
.backend
.accounts
.get_by_uid(uid
)
1457 members
.append(member
)
1459 return sorted(members
)
1461 def add_member(self
, account
):
1463 Adds a member to this group
1465 # Do nothing if this user is already in the group
1466 if account
.is_member_of_group(self
.gid
):
1469 if "posixGroup" in self
.objectclasses
:
1470 self
._add
_string
("memberUid", account
.uid
)
1472 self
._add
_string
("member", account
.dn
)
1474 # Append to cached list of members
1475 self
.members
.append(account
)
1478 def del_member(self
, account
):
1480 Removes a member from a group
1482 # Do nothing if this user is not in the group
1483 if not account
.is_member_of_group(self
.gid
):
1486 if "posixGroup" in self
.objectclasses
:
1487 self
._delete
_string
("memberUid", account
.uid
)
1489 self
._delete
_string
("member", account
.dn
)
1492 if __name__
== "__main__":