]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
18 from .decorators
import *
19 from .misc
import Object
21 class Accounts(Object
):
23 # Only return developers (group with ID 1000)
24 accounts
= self
._search
("(&(objectClass=posixAccount)(gidNumber=1000))")
26 return iter(sorted(accounts
))
30 # Connect to LDAP server
31 ldap_uri
= self
.settings
.get("ldap_uri")
32 conn
= ldap
.initialize(ldap_uri
)
34 # Bind with username and password
35 bind_dn
= self
.settings
.get("ldap_bind_dn")
37 bind_pw
= self
.settings
.get("ldap_bind_pw", "")
38 conn
.simple_bind(bind_dn
, bind_pw
)
42 def _query(self
, query
, attrlist
=None, limit
=0):
43 logging
.debug("Performing LDAP query: %s" % query
)
45 search_base
= self
.settings
.get("ldap_search_base")
48 results
= self
.ldap
.search_ext_s(search_base
, ldap
.SCOPE_SUBTREE
,
49 query
, attrlist
=attrlist
, sizelimit
=limit
)
51 # Close current connection
58 def _search(self
, query
, attrlist
=None, limit
=0):
61 for dn
, attrs
in self
._query
(query
, attrlist
=attrlist
, limit
=limit
):
62 account
= Account(self
.backend
, dn
, attrs
)
63 accounts
.append(account
)
67 def search(self
, query
):
68 # Search for exact matches
69 accounts
= self
._search
("(&(objectClass=person) \
70 (|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
71 % (query
, query
, query
, query
, query
, query
))
73 # Find accounts by name
75 for account
in self
._search
("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query
, query
)):
76 if not account
in accounts
:
77 accounts
.append(account
)
79 return sorted(accounts
)
81 def _search_one(self
, query
):
82 result
= self
._search
(query
, limit
=1)
83 assert len(result
) <= 1
88 def get_by_uid(self
, uid
):
89 return self
._search
_one
("(&(objectClass=person)(uid=%s))" % uid
)
91 def get_by_mail(self
, mail
):
92 return self
._search
_one
("(&(objectClass=inetOrgPerson)(mail=%s))" % mail
)
96 def find_account(self
, s
):
97 account
= self
.get_by_uid(s
)
101 return self
.get_by_mail(s
)
103 def get_by_sip_id(self
, sip_id
):
104 return self
._search
_one
("(|(&(objectClass=sipUser)(sipAuthenticationUser=%s)) \
105 (&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" % (sip_id
, sip_id
))
107 def get_by_phone_number(self
, number
):
108 return self
._search
_one
("(&(objectClass=inetOrgPerson) \
109 (|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
110 % (number
, number
, number
, number
))
114 def _cleanup_expired_sessions(self
):
115 self
.db
.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
117 def create_session(self
, account
, host
):
118 self
._cleanup
_expired
_sessions
()
120 res
= self
.db
.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
121 RETURNING session_id, time_expires", host
, account
.uid
)
123 # Session could not be created
127 logging
.info("Created session %s for %s which expires %s" \
128 % (res
.session_id
, account
, res
.time_expires
))
129 return res
.session_id
, res
.time_expires
131 def destroy_session(self
, session_id
, host
):
132 logging
.info("Destroying session %s" % session_id
)
134 self
.db
.execute("DELETE FROM sessions \
135 WHERE session_id = %s AND host = %s", session_id
, host
)
136 self
._cleanup
_expired
_sessions
()
138 def get_by_session(self
, session_id
, host
):
139 logging
.debug("Looking up session %s" % session_id
)
141 res
= self
.db
.get("SELECT uid FROM sessions WHERE session_id = %s \
142 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
145 # Session does not exist or has expired
149 # Update the session expiration time
150 self
.db
.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
151 WHERE session_id = %s AND host = %s", session_id
, host
)
153 return self
.get_by_uid(res
.uid
)
156 class Account(Object
):
157 def __init__(self
, backend
, dn
, attrs
=None):
158 Object
.__init
__(self
, backend
)
161 self
.attributes
= attrs
or {}
167 return "<%s %s>" % (self
.__class
__.__name
__, self
.dn
)
169 def __eq__(self
, other
):
170 if isinstance(other
, self
.__class
__):
171 return self
.dn
== other
.dn
173 def __lt__(self
, other
):
174 if isinstance(other
, self
.__class
__):
175 return self
.name
< other
.name
179 return self
.accounts
.ldap
181 def _exists(self
, key
):
190 for value
in self
.attributes
.get(key
, []):
193 def _get_bytes(self
, key
, default
=None):
194 for value
in self
._get
(key
):
199 def _get_strings(self
, key
):
200 for value
in self
._get
(key
):
203 def _get_string(self
, key
, default
=None):
204 for value
in self
._get
_strings
(key
):
209 def _get_phone_numbers(self
, key
):
210 for value
in self
._get
_strings
(key
):
211 yield phonenumbers
.parse(value
, None)
213 def _modify(self
, modlist
):
214 logging
.debug("Modifying %s: %s" % (self
.dn
, modlist
))
216 # Run modify operation
217 self
.ldap
.modify_s(self
.dn
, modlist
)
219 def _set(self
, key
, values
):
220 current
= self
._get
(key
)
222 # Don't do anything if nothing has changed
223 if list(current
) == values
:
226 # Remove all old values and add all new ones
229 if self
._exists
(key
):
230 modlist
.append((ldap
.MOD_DELETE
, key
, None))
234 modlist
.append((ldap
.MOD_ADD
, key
, values
))
236 # Run modify operation
237 self
._modify
(modlist
)
240 self
.attributes
.update({ key
: values
})
242 def _set_bytes(self
, key
, values
):
243 return self
._set
(key
, values
)
245 def _set_strings(self
, key
, values
):
246 return self
._set
(key
, [e
.encode() for e
in values
if e
])
248 def _set_string(self
, key
, value
):
249 return self
._set
_strings
(key
, [value
,])
251 def _add(self
, key
, values
):
253 (ldap
.MOD_ADD
, key
, values
),
256 self
._modify
(modlist
)
258 def _add_strings(self
, key
, values
):
259 return self
._add
(key
, [e
.encode() for e
in values
])
261 def _add_string(self
, key
, value
):
262 return self
._add
_strings
(key
, [value
,])
264 def _delete(self
, key
, values
):
266 (ldap
.MOD_DELETE
, key
, values
),
269 self
._modify
(modlist
)
271 def _delete_strings(self
, key
, values
):
272 return self
._delete
(key
, [e
.encode() for e
in values
])
274 def _delete_string(self
, key
, value
):
275 return self
._delete
_strings
(key
, [value
,])
277 def passwd(self
, password
):
281 # The new password must have a score of 3 or better
282 quality
= self
.check_password_quality(password
)
283 if quality
["score"] < 3:
284 raise ValueError("Password too weak")
286 self
.ldap
.passwd_s(self
.dn
, None, password
)
288 def check_password(self
, password
):
290 Bind to the server with given credentials and return
291 true if password is corrent and false if not.
293 Raises exceptions from the server on any other errors.
298 logging
.debug("Checking credentials for %s" % self
.dn
)
300 # Create a new LDAP connection
301 ldap_uri
= self
.backend
.settings
.get("ldap_uri")
302 conn
= ldap
.initialize(ldap_uri
)
305 conn
.simple_bind_s(self
.dn
, password
.encode("utf-8"))
306 except ldap
.INVALID_CREDENTIALS
:
307 logging
.debug("Account credentials are invalid for %s" % self
)
310 logging
.info("Successfully authenticated %s" % self
)
314 def check_password_quality(self
, password
):
316 Passwords are passed through zxcvbn to make sure
317 that they are strong enough.
319 return zxcvbn
.zxcvbn(password
, user_inputs
=(
320 self
.first_name
, self
.last_name
,
324 return "wheel" in self
.groups
326 def is_talk_enabled(self
):
327 return "sipUser" in self
.classes
or "sipRoutingObject" in self
.classes \
328 or self
.telephone_numbers
or self
.address
330 def can_be_managed_by(self
, account
):
332 Returns True if account is allowed to manage this account
334 # Admins can manage all accounts
335 if account
.is_admin():
338 # Users can manage themselves
339 return self
== account
343 return self
._get
_strings
("objectClass")
347 return self
._get
_string
("uid")
351 return self
._get
_string
("cn")
355 def get_first_name(self
):
356 return self
._get
_string
("givenName")
358 def set_first_name(self
, first_name
):
359 self
._set
_string
("givenName", first_name
)
362 self
._set
_string
("cn", "%s %s" % (first_name
, self
.last_name
))
364 first_name
= property(get_first_name
, set_first_name
)
368 def get_last_name(self
):
369 return self
._get
_string
("sn")
371 def set_last_name(self
, last_name
):
372 self
._set
_string
("sn", last_name
)
375 self
._set
_string
("cn", "%s %s" % (self
.first_name
, last_name
))
377 last_name
= property(get_last_name
, set_last_name
)
383 res
= self
.accounts
._query
("(&(objectClass=posixGroup) \
384 (memberUid=%s))" % self
.uid
, ["cn"])
386 for dn
, attrs
in res
:
387 cns
= attrs
.get("cn")
389 groups
.append(cns
[0].decode())
395 def get_address(self
):
396 address
= self
._get
_string
("homePostalAddress")
399 return (line
.strip() for line
in address
.split(","))
403 def set_address(self
, address
):
404 data
= ", ".join(address
.splitlines())
406 self
._set
_bytes
("homePostalAddress", data
.encode())
408 address
= property(get_address
, set_address
)
412 name
= self
.name
.lower()
413 name
= name
.replace(" ", ".")
414 name
= name
.replace("Ä", "Ae")
415 name
= name
.replace("Ö", "Oe")
416 name
= name
.replace("Ü", "Ue")
417 name
= name
.replace("ä", "ae")
418 name
= name
.replace("ö", "oe")
419 name
= name
.replace("ü", "ue")
421 for mail
in self
.attributes
.get("mail", []):
422 if mail
.decode().startswith("%s@ipfire.org" % name
):
425 # If everything else fails, we will go with the UID
426 return "%s@ipfire.org" % self
.uid
428 # Mail Routing Address
430 def get_mail_routing_address(self
):
431 return self
._get
_string
("mailRoutingAddress", None)
433 def set_mail_routing_address(self
, address
):
434 self
._set
_string
("mailRoutingAddress", address
or None)
436 mail_routing_address
= property(get_mail_routing_address
, set_mail_routing_address
)
440 if "sipUser" in self
.classes
:
441 return self
._get
_string
("sipAuthenticationUser")
443 if "sipRoutingObject" in self
.classes
:
444 return self
._get
_string
("sipLocalAddress")
447 def sip_password(self
):
448 return self
._get
_string
("sipPassword")
451 def _generate_sip_password():
452 return util
.random_string(8)
456 return "%s@ipfire.org" % self
.sip_id
458 def uses_sip_forwarding(self
):
459 if self
.sip_routing_address
:
466 def get_sip_routing_address(self
):
467 if "sipRoutingObject" in self
.classes
:
468 return self
._get
_string
("sipRoutingAddress")
470 def set_sip_routing_address(self
, address
):
474 # Don't do anything if nothing has changed
475 if self
.get_sip_routing_address() == address
:
480 # This is no longer a SIP user any more
481 (ldap
.MOD_DELETE
, "objectClass", b
"sipUser"),
482 (ldap
.MOD_DELETE
, "sipAuthenticationUser", None),
483 (ldap
.MOD_DELETE
, "sipPassword", None),
485 (ldap
.MOD_ADD
, "objectClass", b
"sipRoutingObject"),
486 (ldap
.MOD_ADD
, "sipLocalAddress", self
.sip_id
.encode()),
487 (ldap
.MOD_ADD
, "sipRoutingAddress", address
.encode()),
491 (ldap
.MOD_DELETE
, "objectClass", b
"sipRoutingObject"),
492 (ldap
.MOD_DELETE
, "sipLocalAddress", None),
493 (ldap
.MOD_DELETE
, "sipRoutingAddress", None),
495 (ldap
.MOD_ADD
, "objectClass", b
"sipUser"),
496 (ldap
.MOD_ADD
, "sipAuthenticationUser", self
.sip_id
.encode()),
497 (ldap
.MOD_ADD
, "sipPassword", self
._generate
_sip
_password
().encode()),
501 self
._modify
(modlist
)
503 # XXX Cache is invalid here
505 sip_routing_address
= property(get_sip_routing_address
, set_sip_routing_address
)
508 def sip_registrations(self
):
509 sip_registrations
= []
511 for reg
in self
.backend
.talk
.freeswitch
.get_sip_registrations(self
.sip_url
):
514 sip_registrations
.append(reg
)
516 return sip_registrations
519 def sip_channels(self
):
520 return self
.backend
.talk
.freeswitch
.get_sip_channels(self
)
522 def get_cdr(self
, date
=None, limit
=None):
523 return self
.backend
.talk
.freeswitch
.get_cdr_by_account(self
, date
=date
, limit
=limit
)
528 def phone_number(self
):
530 Returns the IPFire phone number
533 return phonenumbers
.parse("+4923636035%s" % self
.sip_id
)
536 def fax_number(self
):
538 return phonenumbers
.parse("+49236360359%s" % self
.sip_id
)
540 def get_phone_numbers(self
):
543 for field
in ("telephoneNumber", "homePhone", "mobile"):
544 for number
in self
._get
_phone
_numbers
(field
):
549 def set_phone_numbers(self
, phone_numbers
):
550 # Sort phone numbers by landline and mobile
551 _landline_numbers
= []
554 for number
in phone_numbers
:
556 number
= phonenumbers
.parse(number
, None)
557 except phonenumbers
.phonenumberutil
.NumberParseException
:
560 # Convert to string (in E.164 format)
561 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
563 # Separate mobile numbers
564 if phonenumbers
.number_type(number
) == phonenumbers
.PhoneNumberType
.MOBILE
:
565 _mobile_numbers
.append(s
)
567 _landline_numbers
.append(s
)
570 self
._set
_strings
("telephoneNumber", _landline_numbers
)
571 self
._set
_strings
("mobile", _mobile_numbers
)
573 phone_numbers
= property(get_phone_numbers
, set_phone_numbers
)
576 def _all_telephone_numbers(self
):
577 ret
= [ self
.sip_id
, ]
579 if self
.phone_number
:
580 s
= phonenumbers
.format_number(self
.phone_number
, phonenumbers
.PhoneNumberFormat
.E164
)
583 for number
in self
.phone_numbers
:
584 s
= phonenumbers
.format_number(number
, phonenumbers
.PhoneNumberFormat
.E164
)
589 def avatar_url(self
, size
=None):
590 if self
.backend
.debug
:
591 hostname
= "http://people.dev.ipfire.org"
593 hostname
= "https://people.ipfire.org"
595 url
= "%s/users/%s.jpg" % (hostname
, self
.uid
)
598 url
+= "?size=%s" % size
602 def get_avatar(self
, size
=None):
603 avatar
= self
._get
_bytes
("jpegPhoto")
610 return self
._resize
_avatar
(avatar
, size
)
612 def _resize_avatar(self
, image
, size
):
613 image
= PIL
.Image
.open(io
.BytesIO(image
))
615 # Convert RGBA images into RGB because JPEG doesn't support alpha-channels
616 if image
.mode
== "RGBA":
617 image
= image
.convert("RGB")
619 # Resize the image to the desired resolution (and make it square)
620 thumbnail
= PIL
.ImageOps
.fit(image
, (size
, size
), PIL
.Image
.ANTIALIAS
)
622 with io
.BytesIO() as f
:
623 # If writing out the image does not work with optimization,
624 # we try to write it out without any optimization.
626 thumbnail
.save(f
, "JPEG", optimize
=True, quality
=98)
628 thumbnail
.save(f
, "JPEG", quality
=98)
632 def upload_avatar(self
, avatar
):
633 self
._set
("jpegPhoto", avatar
)
641 for key
in self
._get
_strings
("sshPublicKey"):
642 s
= sshpubkeys
.SSHKey()
646 except (sshpubkeys
.InvalidKeyError
, NotImplementedError) as e
:
647 logging
.warning("Could not parse SSH key %s: %s" % (key
, e
))
654 def get_ssh_key_by_hash_sha256(self
, hash_sha256
):
655 for key
in self
.ssh_keys
:
656 if not key
.hash_sha256() == hash_sha256
:
661 def add_ssh_key(self
, key
):
662 k
= sshpubkeys
.SSHKey()
664 # Try to parse the key
667 # Check for types and sufficient sizes
668 if k
.key_type
== b
"ssh-rsa":
670 raise sshpubkeys
.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
672 elif k
.key_type
== b
"ssh-dss":
673 raise sshpubkeys
.InvalidKeyError("DSA keys are not supported")
675 # Ignore any duplicates
676 if key
in (k
.keydata
for k
in self
.ssh_keys
):
677 logging
.debug("SSH Key has already been added for %s: %s" % (self
, key
))
680 # Prepare transaction
683 # Add object class if user is not in it, yet
684 if not "ldapPublicKey" in self
.classes
:
685 modlist
.append((ldap
.MOD_ADD
, "objectClass", b
"ldapPublicKey"))
688 modlist
.append((ldap
.MOD_ADD
, "sshPublicKey", key
.encode()))
691 self
._modify
(modlist
)
694 self
.ssh_keys
.append(k
)
696 def delete_ssh_key(self
, key
):
697 if not key
in (k
.keydata
for k
in self
.ssh_keys
):
700 # Delete key from LDAP
701 if len(self
.ssh_keys
) > 1:
702 self
._delete
_string
("sshPublicKey", key
)
705 (ldap
.MOD_DELETE
, "objectClass", b
"ldapPublicKey"),
706 (ldap
.MOD_DELETE
, "sshPublicKey", key
.encode()),
710 if __name__
== "__main__":