connexions.py
1 |
# -*- coding: UTF-8 -*-
|
---|---|
2 |
###########################################################################
|
3 |
# Eole NG - 2007
|
4 |
# Copyright Pole de Competence Eole (Ministere Education - Academie Dijon)
|
5 |
# Licence CeCill cf /root/LicenceEole.txt
|
6 |
# eole@ac-dijon.fr
|
7 |
# modifié par Cadoles 2012
|
8 |
#
|
9 |
# connectes.py
|
10 |
#
|
11 |
# Librairie de gestion des connectés
|
12 |
#
|
13 |
###########################################################################
|
14 |
|
15 |
import MySQLdb |
16 |
import sys, os, tdb, time, glob, re #, struct |
17 |
sys.path.append('/usr/share/eole/controlevnc')
|
18 |
|
19 |
from scribe.login import Ldap, log_connexion_db |
20 |
from scribe.ldapconf import USER_FILTER |
21 |
from pyeole.diagnose import test_tcp |
22 |
from config import mysql_password, mysql_host, master_ip |
23 |
|
24 |
tdb_file = '/var/run/samba/smbXsrv_session_global.tdb'
|
25 |
separator = '\x00'
|
26 |
PROC_TCP = "/proc/net/tcp"
|
27 |
TDB_HANDLER = tdb.Tdb(tdb_file, os.O_RDONLY) |
28 |
|
29 |
def remove_duplicated_user_for_ip(sockets): |
30 |
last_users = {} |
31 |
def _get_timestamp_of_inode(inode): |
32 |
'''
|
33 |
To retrieve the process's creation timestamp, check every running process and look for one using
|
34 |
the given inode.
|
35 |
'''
|
36 |
for item in glob.glob('/proc/[0-9]*/fd/[0-9]*'): |
37 |
try:
|
38 |
if re.search(inode, os.readlink(item)):
|
39 |
return os.stat(os.path.dirname(item)).st_ctime
|
40 |
except:
|
41 |
pass
|
42 |
|
43 |
def _hex2dec(s): |
44 |
return str(int(s,16)) |
45 |
|
46 |
def _ip(s): |
47 |
ip = [(_hex2dec(s[6:8])),(_hex2dec(s[4:6])),(_hex2dec(s[2:4])),(_hex2dec(s[0:2]))] |
48 |
return '.'.join(ip) |
49 |
|
50 |
def _convert_ip_port(array): |
51 |
host,port = array.split(':')
|
52 |
return 'ipv4:'+_ip(host)+':'+_hex2dec(port) |
53 |
|
54 |
with open(PROC_TCP,'r') as f: |
55 |
all_connexions = f.readlines() |
56 |
all_connexions.pop(0)
|
57 |
|
58 |
duplicated = {} |
59 |
for ip, lst in sockets.items(): |
60 |
if len(lst) != 1: |
61 |
for ls in lst: |
62 |
duplicated[ls[0]] = (ip, ls[1]) |
63 |
else:
|
64 |
last_users[ip] = (lst[0][1], ip) |
65 |
|
66 |
if duplicated != {}:
|
67 |
_last_users = {} |
68 |
for line in all_connexions: |
69 |
line_array = [x for x in line.split(' ') if x !=''] # Split lines and remove empty spaces. |
70 |
comp_socket = _convert_ip_port(line_array[2])
|
71 |
# only ESTABLISHMENT and corresponding socket
|
72 |
if line_array[3] == '01' and comp_socket in duplicated.keys(): |
73 |
timestamp = _get_timestamp_of_inode(line_array[9])
|
74 |
ip, username = duplicated[comp_socket] |
75 |
# si l'ip n'a pas encore été traité ou si l'ip est traité mais avec une date plus ancienne
|
76 |
# c'est le nouveau username qui est associé à l'IP
|
77 |
if ip not in _last_users or _last_users[ip][2] < timestamp: |
78 |
_last_users[ip] = (username, ip, timestamp) |
79 |
for ip, _last_user in _last_users.items(): |
80 |
last_users[ip] = (_last_user[0], _last_user[1]) |
81 |
|
82 |
return last_users.values()
|
83 |
|
84 |
class ComputerNameError(Exception): |
85 |
pass
|
86 |
|
87 |
|
88 |
class Session: |
89 |
def __init__(self, only=None): |
90 |
"""lit le contenu de sessionid.tdb
|
91 |
only: get session information only for one IP
|
92 |
"""
|
93 |
cnt = 0
|
94 |
max_cnt = 10
|
95 |
for cnt in range(max_cnt): |
96 |
try:
|
97 |
self._init_sessions(only)
|
98 |
break
|
99 |
except:
|
100 |
TDB_HANDLER.reopen() |
101 |
if cnt == max_cnt-1: |
102 |
# pour voir ce qui plante
|
103 |
self._init_sessions(only)
|
104 |
time.sleep(0.2)
|
105 |
|
106 |
def _init_sessions(self, only): |
107 |
self.sessions = []
|
108 |
sessions = {} |
109 |
for key in TDB_HANDLER.iterkeys(): |
110 |
#key must endswith \x00
|
111 |
data = TDB_HANDLER.get(key) |
112 |
if data is None: |
113 |
continue
|
114 |
|
115 |
#(tid, ) = struct.unpack("<L", data[4:8])
|
116 |
sdata = data.split(separator) |
117 |
#parse les colonnes de tdb a la recherche du nom de la socket
|
118 |
#l'IP est utilisé dans la socket
|
119 |
for col in sdata: |
120 |
col = str(col)
|
121 |
if col[:5] == 'ipv4:' and col[-4:] != ':445': |
122 |
socket = col |
123 |
ip = socket[5:].split(':')[0] |
124 |
break
|
125 |
#le nom d'utilisateur est dans la dernière colonne
|
126 |
if only is not None: |
127 |
if ip != only:
|
128 |
continue
|
129 |
username = sdata[-2]
|
130 |
if username == '': |
131 |
continue
|
132 |
if self.is_user_account(username): |
133 |
sessions.setdefault(ip, []).append((socket, username)) |
134 |
self.sessions = remove_duplicated_user_for_ip(sessions)
|
135 |
|
136 |
def is_user_account(self, nom): |
137 |
if not nom.endswith('$'): |
138 |
ldap_conn = Ldap() |
139 |
if ldap_conn.get_user_attributs('%s'%nom) != {}: |
140 |
return True |
141 |
|
142 |
def get_users_by_ip(self, ip): |
143 |
"""renvoi users par ip
|
144 |
"""
|
145 |
ret = [] |
146 |
for uid, _ip in self.sessions: |
147 |
if _ip == ip:
|
148 |
ret.append(uid) |
149 |
return ret
|
150 |
|
151 |
def get_sessions_by_ip(self, ip): |
152 |
"""renvoi les sessions d'une IP
|
153 |
"""
|
154 |
ret = [] |
155 |
for uid, _ip in self.sessions: |
156 |
if _ip == ip:
|
157 |
ret.append((uid, _ip)) |
158 |
return ret
|
159 |
|
160 |
def has_session(self, ip): |
161 |
for uid, _ip in self.sessions: |
162 |
if _ip == ip:
|
163 |
return ip
|
164 |
|
165 |
def is_connected(self, user): |
166 |
for _user, _ in self.sessions: |
167 |
if _user == user:
|
168 |
return user
|
169 |
|
170 |
def get_ip(self, user): |
171 |
"""retourne l'ip de la (des) station(s) ou est connecte <user>
|
172 |
"""
|
173 |
res = [] |
174 |
for _user, _ip in self.sessions: |
175 |
if _user == user:
|
176 |
res.append(_ip) |
177 |
return res
|
178 |
|
179 |
class Connexions: |
180 |
def __init__(self): |
181 |
self.db = MySQLdb.connect(host=mysql_host, user='controlevnc', |
182 |
passwd=mysql_password, db='controlevnc')
|
183 |
self.cursor = self.db.cursor() |
184 |
self.sessions = None |
185 |
|
186 |
def __del__(self): |
187 |
self.db.commit()
|
188 |
self.db.close()
|
189 |
|
190 |
def service_start(self, ip, mac): |
191 |
"""met True à up dans la base de donnée pour l'IP correspondante
|
192 |
"""
|
193 |
self.cursor.execute("SELECT up FROM log WHERE ip='%s'" % ip) |
194 |
row = self.cursor.fetchone()
|
195 |
if row:
|
196 |
if row[0] != True: |
197 |
#si l'ip est déjà dans la base n'est pas up=True
|
198 |
self.cursor.execute("UPDATE log SET up=True, mac='%s' WHERE ip='%s'" % (mac, ip)) |
199 |
else:
|
200 |
#si l'IP n'existe pas dans la base, creer la machine
|
201 |
self.cursor.execute("INSERT INTO log (ip, up, mac) VALUES ('%s', True, '%s')" \ |
202 |
% (ip, mac)) |
203 |
return True |
204 |
|
205 |
def service_stop(self, ip): |
206 |
"""met False à up dans la base de donnée pour l'IP correspondante
|
207 |
"""
|
208 |
self.cursor.execute("SELECT up FROM log WHERE ip='%s'" % ip) |
209 |
row = self.cursor.fetchone()
|
210 |
if row:
|
211 |
if row[0] != False: |
212 |
#si l'ip est déjà dans la base n'est pas up=False
|
213 |
self.cursor.execute("UPDATE log SET up=False WHERE ip='%s'"%ip) |
214 |
return True |
215 |
|
216 |
def get_stations(self): |
217 |
"""
|
218 |
retourne la liste des machines dans la base de donnée des machines
|
219 |
"""
|
220 |
self.cursor.execute("SELECT netbios, ip, mac FROM log") |
221 |
ret = [] |
222 |
for row in self.cursor.fetchall(): |
223 |
ret.append(','.join(row))
|
224 |
return ';'.join(ret) |
225 |
|
226 |
def get_station_up(self, verify): |
227 |
"""
|
228 |
retourne la liste des machines dans la base de donnée des machines
|
229 |
avec session ouverte (information du Client Scribe).
|
230 |
Si verify (True|False):
|
231 |
- comparaison avec la liste des sessions samba ;
|
232 |
- test si la présence du port 8788 (Client Scribe), dans ce cas le nom
|
233 |
de l'utilisation n'est pas mentionné).
|
234 |
"""
|
235 |
#FIXME : devrait ajouter les machines de la session nom présente dans
|
236 |
#bdd
|
237 |
self.cursor.execute("SELECT ip, netbios, user FROM log WHERE up=True") |
238 |
ret = [] |
239 |
session_ips = [ip for user, ip in self.get_sessions().sessions] |
240 |
for row in self.cursor.fetchall(): |
241 |
#si le poste a une session samba ou est accessible sur le port 8788
|
242 |
if not verify or row[0] in session_ips: |
243 |
ret.append(', '.join(row))
|
244 |
elif test_tcp(row[0], 8788): |
245 |
ret.append("%s, %s," % (row[0], row[1])) |
246 |
return ';'.join(ret) |
247 |
|
248 |
def get_connected_by_group(self, group): |
249 |
"""voir les connecter pour un groupe"""
|
250 |
sql = "SELECT ip, user, os, prim_group, netbios FROM log WHERE up=True and ip IN "
|
251 |
sql += "(SELECT ip FROM `group` WHERE groupname='%s');" % group
|
252 |
self.cursor.execute(sql)
|
253 |
members = [] |
254 |
ip_infos = {} |
255 |
for row in self.cursor.fetchall(): |
256 |
ip, user, os, prim_group, netbios = row |
257 |
members.append(user) |
258 |
ip_infos[ip] = (os, prim_group, netbios) |
259 |
|
260 |
sessions = self.get_sessions()
|
261 |
liste = [] |
262 |
#FIXME : supprimer les comptes en trop dans la bdd !
|
263 |
for username, ip in sessions.sessions: |
264 |
try:
|
265 |
if username not in members: |
266 |
continue
|
267 |
os, prigrp, netbios = ip_infos[ip] |
268 |
|
269 |
liste.append((username, prigrp, netbios, os, ip)) |
270 |
except:
|
271 |
continue
|
272 |
liste.sort() |
273 |
return liste
|
274 |
|
275 |
def get_connected_student(self, group=None): |
276 |
"""renvoie la liste des connectes pour n'avoir que les élèves du
|
277 |
groupe [USERNAME]
|
278 |
"""
|
279 |
# retourne que les eleves
|
280 |
prigrp = "eleves"
|
281 |
# récupération des connectés
|
282 |
#croise parce que cherche prigrp eleves + groupe + connecté
|
283 |
"""retourne les utilisateurs présent dans 2 groupes"""
|
284 |
sql = "SELECT ip, os, user, netbios FROM log WHERE up=True and ip IN "
|
285 |
if group is None: |
286 |
sql += "(SELECT ip FROM `group` WHERE groupname='%s')" % prigrp
|
287 |
else:
|
288 |
sql += "(SELECT ip FROM `group` WHERE EXISTS"
|
289 |
sql += "(SELECT ip FROM `group` WHERE groupname='%s')" % prigrp
|
290 |
sql += "AND groupname='%s');" % group
|
291 |
self.cursor.execute(sql)
|
292 |
membres = [] |
293 |
ip_infos = {} |
294 |
for row in self.cursor.fetchall(): |
295 |
ip, os, user, netbios = row |
296 |
membres.append(user) |
297 |
ip_infos[ip] = (os, netbios) |
298 |
# le groupe est vide
|
299 |
if not membres: |
300 |
return []
|
301 |
sessions = self.get_sessions()
|
302 |
liste = [] |
303 |
for user, ip in sessions.sessions: |
304 |
try:
|
305 |
# uid, grp, machine, os(détecté), ip
|
306 |
if user not in membres: |
307 |
continue
|
308 |
os, nom_mach = ip_infos[ip] |
309 |
|
310 |
liste.append((user, prigrp, nom_mach, os, ip)) |
311 |
except:
|
312 |
continue
|
313 |
liste.sort() |
314 |
return liste
|
315 |
|
316 |
def get_os_from_ip(self, ip): |
317 |
sql = "SELECT os FROM log WHERE ip='%s'" % ip
|
318 |
self.cursor.execute(sql)
|
319 |
row = self.cursor.fetchone()
|
320 |
if row:
|
321 |
return row[0] |
322 |
|
323 |
def get_ip_from_netbios(self, netbios): |
324 |
sql = "SELECT ip FROM log WHERE netbios='%s'" % netbios
|
325 |
self.cursor.execute(sql)
|
326 |
row = self.cursor.fetchone()
|
327 |
if row:
|
328 |
return row[0] |
329 |
|
330 |
def get_netbios_from_ip(self, ip): |
331 |
sql = "SELECT netbios FROM log WHERE ip='%s'" % ip
|
332 |
self.cursor.execute(sql)
|
333 |
row = self.cursor.fetchone()
|
334 |
if row:
|
335 |
return row[0] |
336 |
|
337 |
def get_mac_from_ip(self, ip): |
338 |
sql = "SELECT mac FROM log WHERE ip='%s'" % ip
|
339 |
self.cursor.execute(sql)
|
340 |
row = self.cursor.fetchone()
|
341 |
if row:
|
342 |
return row[0] |
343 |
|
344 |
def get_netbios(self): |
345 |
sql = "SELECT netbios FROM log"
|
346 |
self.cursor.execute(sql)
|
347 |
ret = [] |
348 |
row = self.cursor.fetchall()
|
349 |
if row:
|
350 |
for r in row: |
351 |
ret.append(r[0])
|
352 |
return ret
|
353 |
|
354 |
def get_sid(self, user): |
355 |
#FIXME manque UP
|
356 |
sql = "SELECT sid FROM log WHERE user='%s'" % user
|
357 |
self.cursor.execute(sql)
|
358 |
row = self.cursor.fetchone()
|
359 |
if row:
|
360 |
return row[0] |
361 |
#si pas dans la base de donnée
|
362 |
return Ldap().ldap_search("(&%s(uid=%s))" % (USER_FILTER, user), |
363 |
['sambaSID'], True)['sambaSID'][0] |
364 |
|
365 |
def get_sessions(self, only=None): |
366 |
if not self.sessions: |
367 |
self.sessions = Session(only)
|
368 |
return self.sessions |
369 |
|
370 |
def writedbg(self, msg): |
371 |
debuglogfile = '/tmp/controlevnc-debug.log'
|
372 |
try:
|
373 |
with open(debuglogfile, 'a') as dbgfile: |
374 |
dbgfile.write('%s %s\n'%(time.strftime('%Y/%m/%d %H:%M:%S'), msg)) |
375 |
except: pass |
376 |
|
377 |
def get_infos(self, ip, os_type): |
378 |
try: self.writedbg('####################################### get_infos') |
379 |
except: pass |
380 |
sessions = self.get_sessions(only=ip)
|
381 |
try: self.writedbg('%s session : %s'%(ip, sessions)) |
382 |
except: pass |
383 |
users = sessions.get_users_by_ip(ip) |
384 |
try: self.writedbg('%s users : %s'%(ip, users)) |
385 |
except: pass |
386 |
#récupération des informations sur l'utilisateur dans la base de données
|
387 |
#FIXME manque UP
|
388 |
sql = "SELECT user, sid, display_name, os, netbios FROM log "
|
389 |
sql += "WHERE ip='%s'" % ip
|
390 |
self.cursor.execute(sql)
|
391 |
row = self.cursor.fetchone()
|
392 |
try: self.writedbg('%s row : %s'%(ip, row)) |
393 |
except: pass |
394 |
try: self.writedbg('%s rowlist : %s'%(ip, [i for i in self.cursor])) |
395 |
except: pass |
396 |
if row:
|
397 |
# Si personne dans la session
|
398 |
# ou un et un seul utilisateur identique à la base de donnée
|
399 |
# ou l'utilisateur de la base est dans la liste des sessions
|
400 |
# (il arrive que net status sessions renvoi plusieurs utilisateurs
|
401 |
# retourne les valeurs de la base
|
402 |
# et si _os n'est pas vide
|
403 |
user, sid, display_name, _os, netbios = row |
404 |
if users == [] or (len(users) == 1 and users[0] == user) or \ |
405 |
(len(users) > 1 and user in users) and _os: |
406 |
groups = self._get_sql_groups(ip)
|
407 |
return user, sid, display_name, groups, _os, netbios
|
408 |
try: self.writedbg('%s else row : %s'%(ip, row)) |
409 |
except: pass |
410 |
# si pas d'utilisateur dans la base de donnée
|
411 |
# et dans net sessions status
|
412 |
if users == []:
|
413 |
raise Exception("Personne ne s'est connecte sur %s"%ip) |
414 |
# utilisateur de la base et connecté différent
|
415 |
# ou pas d'utilisateur dans la base de donnée
|
416 |
# fait confiance à net status sessions
|
417 |
# on récupère les informations dans LDAP
|
418 |
user = users[-1]
|
419 |
|
420 |
ldap_conn = Ldap() |
421 |
user_attrib = ldap_conn.get_user_attributs(user) |
422 |
try:
|
423 |
sid = user_attrib['sambaSID'][0].strip() |
424 |
except:
|
425 |
raise Exception('utilisateur inconnu {0}, les informations de la session sont {1}'.format(user, sessions.sessions)) |
426 |
display_name = user_attrib['displayName'][0].strip() |
427 |
gidnumber = user_attrib['gidNumber'][0].strip() |
428 |
groups = [] |
429 |
for dn, grp in ldap_conn.get_groups(user): |
430 |
name = grp['cn'][0] |
431 |
if grp['gidNumber'][0] == gidnumber: |
432 |
prim_group = name |
433 |
groups.append(name) |
434 |
del(ldap_conn)
|
435 |
netbios = "" # sessions.get_netbios(ip) => le netbios n'est plus disponible dans l'objet session |
436 |
|
437 |
try: self.writedbg('%s LDAP : %s'%(ip, [user, sid, display_name, prim_group, groups, netbios, os_type, ip])) |
438 |
except: pass |
439 |
# met à jour la base de donnée
|
440 |
log_connexion_db(user, sid, display_name, prim_group, groups, netbios, |
441 |
os_type, ip) |
442 |
|
443 |
return user, sid, display_name, groups, os_type, netbios
|
444 |
|
445 |
def _get_sql_groups(self, ip): |
446 |
"""retourne les groupes de l'utilisateur connecté à une IP sans
|
447 |
vérifier l'utilisateur"""
|
448 |
sql = "SELECT groupname FROM `group` WHERE ip='%s'" % ip
|
449 |
self.cursor.execute(sql)
|
450 |
groups = [group[0] for group in self.cursor.fetchall()] |
451 |
return groups
|
452 |
|
453 |
def get_groups(self, ip): |
454 |
"""retourne les groupes de l'utilisateur connecter sur IP
|
455 |
"""
|
456 |
sessions = self.get_sessions(ip)
|
457 |
users = sessions.get_users_by_ip(ip) |
458 |
|
459 |
#récupération des informations sur l'utilisateur dans la base de données
|
460 |
sql = "SELECT user FROM log WHERE up=True AND ip='%s'" % ip
|
461 |
self.cursor.execute(sql)
|
462 |
user = self.cursor.fetchone()
|
463 |
if user:
|
464 |
# Si personne dans la session
|
465 |
# ou un et un seul utilisateur identique à la base de donnée
|
466 |
# ou l'utilisateur de la base est dans la liste des sessions
|
467 |
# (il arrive que net status sessions renvoi plusieurs utilisateurs
|
468 |
# retourne les valeurs de la base
|
469 |
if users == [] or (len(users) == 1 and users[0] == user) or \ |
470 |
(len(users) > 1 and user in users): |
471 |
#si pas vide
|
472 |
return self._get_sql_groups(ip) |
473 |
|
474 |
# si pas d'utilisateur dans la base de donnée
|
475 |
# et dans net sessions status
|
476 |
if users == []:
|
477 |
raise Exception("Personne ne s'est connecte sur %s"%ip) |
478 |
# utilisateur de la base et connecté différent
|
479 |
# ou pas d'utilisateur dans la base de donnée
|
480 |
# fait confiance à net status sessions
|
481 |
# on récupère les informations dans LDAP
|
482 |
user = users[-1]
|
483 |
|
484 |
ldap_conn = Ldap() |
485 |
groups = [] |
486 |
for dn, grp in ldap_conn.get_groups(user): |
487 |
groups.append(grp['cn'][0]) |
488 |
del(ldap_conn)
|
489 |
|
490 |
return groups
|
491 |
|
492 |
def get_user_from_ip(self, ip): |
493 |
"""retourne l'utilisateur connecté sur ip (#13163)
|
494 |
"""
|
495 |
return self.get_user(ip) |
496 |
|
497 |
def get_user(self, ip): |
498 |
"""retourne l'utilisateur connecter sur IP
|
499 |
"""
|
500 |
sessions = self.get_sessions(ip)
|
501 |
users = sessions.get_users_by_ip(ip) |
502 |
|
503 |
#récupération des informations sur l'utilisateur dans la base de données
|
504 |
sql = "SELECT user FROM log WHERE up=True AND ip='%s'" % ip
|
505 |
self.cursor.execute(sql)
|
506 |
row = self.cursor.fetchone()
|
507 |
if row:
|
508 |
user = row |
509 |
# Si personne dans la session
|
510 |
# ou un et un seul utilisateur identique à la base de donnée
|
511 |
# ou l'utilisateur de la base est dans la liste des sessions
|
512 |
# (il arrive que net status sessions renvoi plusieurs utilisateurs
|
513 |
# retourne les valeurs de la base
|
514 |
if users == [] or (len(users) == 1 and users[0] == user) or \ |
515 |
(len(users) > 1 and user in users): |
516 |
#si pas vide
|
517 |
return user
|
518 |
|
519 |
# si pas d'utilisateur dans la base de donnée
|
520 |
# et dans net sessions status
|
521 |
if users == []:
|
522 |
raise Exception("Personne ne s'est connecte sur %s"%ip) |
523 |
# utilisateur de la base et connecté différent
|
524 |
# ou pas d'utilisateur dans la base de donnée
|
525 |
# fait confiance à net status sessions
|
526 |
# on récupère les informations dans LDAP
|
527 |
return users[-1] |
528 |
|
529 |
def isprof(self, ip): |
530 |
"""renvoie True si l'utilisateur connecté sur <ip> est membre de
|
531 |
"professeurs"
|
532 |
"""
|
533 |
if ip == master_ip:
|
534 |
return True |
535 |
return 'professeurs' in self.get_groups(ip) |
536 |
|
537 |
def isadmin(self, ip): |
538 |
"""renvoie True si l'utilisateur connecté sur <ip> est membre de
|
539 |
"DomainAdmins"
|
540 |
"""
|
541 |
if ip == master_ip:
|
542 |
return True |
543 |
return 'DomainAdmins' in self.get_groups(ip) |
544 |
|
545 |
def has_session(self, ip): |
546 |
sessions = self.get_sessions(ip)
|
547 |
return sessions.has_session(ip)
|
548 |
|
549 |
def is_connected(self, user): |
550 |
sessions = self.get_sessions()
|
551 |
return sessions.is_connected(user) == user
|