Desconexión pasiva de usuarios en bases de datos Access

En los anteriores artículos Información de usuarios conectados (NetWkstaUserEnum y NetUserGetInfo) y Usuarios conectados a base de datos Access (User Roster) habíamos llegado a crear un Administrador de usuarios Access que nos proporcionaba la información de los usuarios que teníamos conectados a una base de datos que nosotros le pasábamos. Los datos que nos mostraba eran los siguientes:
- Hora: Hora de la monitorización
- BD: Usuario de base de datos (para versiones 2007 o posteriores siempre Admin)
- Equipo: Equipo de la red desde el que conecta
- Usuario Windows: Usuario de de la red de Active Directory o de red
- Nombre completo: Nombre completo de Active Directory o de red
- Dominio: Dominio de Active Directory o de red
- Servidor: Servidor al que conecta cada usuario
- Estado de la conexión (conectado/no conectado)
- Cierre forzado (si/no) Base de datos cerrada de forma no habitual
Como decía en el anterior artículo, con esto podría ser suficiente para informar a los usuarios conectados de nuestra intención realizar modificaciones en la base de datos y pedirles que salieran de sus aplicaciones. Pero como todos los desarrolladores sabemos, siempre queda algún usuario conectado que no podemos localizar o que hace caso omiso de nuestras peticiones.
Para solventar este problema, en este artículo voy a explicar el primer tipo de desconexión que tendrá la aplicación de Administración de usuarios, la desconexión pasiva.
Desconexión pasiva
Aunque lo llamemos desconexión pasiva, realmente no se desconecta a los usuarios, simplemente no se les deja volver a conectar. Este tipo de desconexión se suele utilizar al final de la jornada de trabajo, impidiendo así que los usuarios se conecten al empezar la jornada siguiente.
Al igual que en el resto de artículos sobre el Administrador de usuarios, voy a explicar un poco por encima el código que he tenido que modificar para que funciones la desconexión y los problemas que me he encontrado. Os aconsejo que reviséis los anteriores artículos (Información de usuarios conectados (NetWkstaUserEnum y NetUserGetInfo) y Usuarios conectados a base de datos Access (User Roster)) para más información.
Necesitaremos de nuevo los 2 módulos que utilizamos en el anterior artículo, pero esta vez modificaremos el módulo conexiones para que permita la desconexión pasiva. Para ello, necesitaremos declarar la conexión como pública para que siga activa una vez salgamos de la función en la que la abrimos (conexión persistente). Aquí es donde he tenido la mayoría de los problemas.
Empezamos haciendo pública la conexión en la sección de declaraciones de nuestro módulo:
Option Compare Database Option Explicit Public Conexion As New ADODB.Connection 'Para crear la conexión. Tengo que utilizar ADO, aunque no me guste. Lo hago público para que sea persistente (desconexión pasiva)
En otros ejemplos de desconexión pasiva de usuarios he visto que declaran tanto las conexiones como los recordsets como objetos, pero de momento no veo la utilidad de hacerlo, a mí me funciona perfectamente de esta forma.
Una vez declarada la conexión, tenemos que asignarle las propiedades que nos interesan tal y como hacíamos en la anterior versión, pero tenemos que asegurarnos de que no hayamos abierto ya la conexión (permanecerá abierta mientras tengamos activa la aplicación) utilizando la propiedad «state» de las conexiones ADO. Veamos cómo implementarlo:
If Conexion.state = 0 Then
Conexion.Provider = CurrentProject.Connection.Provider 'Por si acaso asignamos el proveedor aunque se supone que será Microsoft ACE OLEDB 12.0
If Len(Trim(Contraseña)) > 0 Then 'Si el usuario mete la contraseña. Si no tiene contraseña funciona bien aunque metas
Conexion.Properties("Jet OLEDB:Database Password") = Contraseña
End If
Conexion.Open "Data Source=" & Fichero
End If
Por supuesto, si seleccionamos un backend nuevo, también tendremos que volver a crear la conexión. Una posible idea es cerrarla al seleccionar el fichero (si no sabéis como crear un filedialog, estaré encantado de contestaros en los comentarios) de la siguiente manera:
If Not Conexion Is Nothing Then
If Conexion.state = 1 Then
Conexion.Close 'Cerramos la conexión. 1 es adStateOpen
End If
Set Conexion = Nothing
End If
Y ahora vamos a establecer la propiedad que hará que se active la desconexión pasiva (he modificado la función principal para que además de recibir el fichero de base de datos y la contraseña, reciba otro parámetro booleano que controle esta propiedad):
Public Function Usuarios_Conectados(ByVal Fichero As String, ByVal Contraseña As String, ByVal Opcion As Boolean)
Y establecemos la propiedad:
'Desconexión pasiva, conectamos y desconectamos
If Opcion = True Then
Conexion.Properties("Jet OLEDB:Connection Control") = 1 'Impedimos que se conecten nuevos usuarios
Else
Conexion.Properties("Jet OLEDB:Connection Control") = 2 'Se permiten las nuevas conexiones
End If
El resto del código debería de ser igual que en la anterior versión, pero hay un problema a la hora de abrir el esquema, que por cierto me hizo perder varias horas. En la anterior versión abríamos el esquema User Roster de la siguiente manera:
Set rsConexiones = Conexion.OpenSchema(schema:=adSchemaProviderSpecific, schemaID:=schemaID) ' Conexión sacada de la web de Microsoft
Declarando la variable schemaID anteriormente:
Dim schemaID As String
'**********************************************************************
'En nuestro caso utilizamos JET_SCHEMA_USERROSTER -> {947bb102-5d43-11d1-bdbf-00c04fb92675}
schemaID = "{947bb102-5d43-11d1-bdbf-00c04fb92675}"
'**********************************************************************
Pues bien, de esta forma no funciona siempre (no me preguntéis el motivo) así que después de mucho investigar donde estaba el error, he encontrado esta otra forma de abrirlo que parece que funciona siempre:
Set rsConexiones = Conexion.OpenSchema(adSchemaProviderSpecific, , schemaID) ' Conexión sacada de la web de Microsoft. NO tira con a veces la variable... locura para encontrarlo
'Set rsConexiones = Conexion.OpenSchema(adSchemaProviderSpecific, , "{947bb102-5d43-11d1-bdbf-00c04fb92675}")'Parece que al final funciona la de arriba...
Edito: Parece que finalmente funciona con la versión inicial. Me parece más clara así que la dejo como al principio.
Y con esto ya tenemos la versión de nuestro administrador de usuarios modificada para que permita desconexión pasiva de usuarios. Ahora bastará con llamar a la función Usuarios_Conectados con el tercer parámetro con el valor «TRUE» para que nuestra aplicación empiece con la desconexión pasiva de usuarios.
Tendremos que mantener la aplicación abierta para que se mantenga nuestra desconexión pasiva ya que al cerrarla se pierde la conexión persistente. Así y todo, no estaría de más cerrarla por código al cerrar el formulario:
If Not Conexion Is Nothing Then
If Conexion.state = 1 Then
Conexion.Close 'Cerramos la conexión. 1 es adStateOpen
End If
Set Conexion = Nothing
End If
Aquí os paso los 2 módulos ya implementados, recordad que necesitamos tener una tabla llamada CONEXIONES y llamar a la función pasándole la ruta del fichero, la contraseña (si es necesario) y TRUE/FALSE para activar/desactivar la desconexión pasiva.
Módulo Conexiones
'*************************************************************************************************************************************
'Autor: Arkaitz Arteaga
'Más artículos: www.programadordepalo.com
'Mail de contacto: admin@programadordepalo.com
'Fecha: Enero 2014
'Version: 1.0
'*************************************************************************************************************************************
'Sacado de la web de Microsoft (necesario Microsoft ActiveX Data Objects 2.x Library)
' Para abrir connection.OpenSchema(querytype, criteria, schemaID)
' schemaID -> Hay diferentes tipos, en este caso utilizaremos JET_SCHEMA_USERROSTER -> {947bb102-5d43-11d1-bdbf-00c04fb92675}
'-Nombre del equipo que está utilizando el usuario.
'-Nombre de seguridad, es decir, el identificador de usuario. Siempre va a ser 'admin' a partir de 2007
'-Si el usuario está actualmente conectado a la base de datos.
'-Si la conexión del usuario se terminó con normalidad.
'
'*************************************************************************************************************************************
'*************************************************************************************************************************************
'Copyright: Por favor, no cuesta nada mantener un enlace a mi web en el código.
'Incluso pudes dejar los formularios tal cual, con un enlace a mi web.
'Si vas a utilizar este código con fines lucrativos (es decir, te van a pagar por ello) contacta conmigo por favor.
'*************************************************************************************************************************************
Option Compare Database
Option Explicit
Public Const VERSION As String = "2.2"
Public Conexion As New ADODB.Connection 'Para crear la conexión. Tengo que utilizar ADO, aunque no me guste. Lo hago público para que sea persistente (desconexión pasiva)
Public horaConectado As String 'Hora de la conexión recurrente
Public Function Usuarios_Conectados(ByVal Fichero As String, ByVal Contraseña As String, ByVal Opcion As Boolean)
On Error GoTo AlgoPasa
'Dim Conexion As New ADODB.Connection 'Sin conexión persistente se puede hacer desde aqui
Dim rsConexiones As New ADODB.Recordset 'Para la tabla que tendrá las conexiones
Dim rsConectados As DAO.Recordset 'Para la tabla local que tendrá los usuarios conectados. Ya lo sabéis, no me gusta ADO, ;)
Dim MiBD As DAO.Database 'Muchos criticaréis esta forma, pero así utilizamos ADO y DAO de manera que los no iniciados aprendan
Dim datosWin As USUARIO_FINAL 'Guardo los datos que devuelve la función UsuarioWindows
Dim schemaID As String
'**********************************************************************
'En nuestro caso utilizamos JET_SCHEMA_USERROSTER -> {947bb102-5d43-11d1-bdbf-00c04fb92675}
schemaID = "{947bb102-5d43-11d1-bdbf-00c04fb92675}"
'**********************************************************************
If Conexion.state = 0 Then
Conexion.Provider = CurrentProject.Connection.Provider 'Por si acaso asignamos el proveedor aunque se supone que será Microsoft ACE OLEDB 12.0
If Len(Trim(Contraseña)) > 0 Then 'Si el usuario mete la contraseña. Si no tiene contraseña funciona bien aunque metas
Conexion.Properties("Jet OLEDB:Database Password") = Contraseña
End If
Conexion.Open "Data Source=" & Fichero
End If
'Desconexión pasiva, conectamos y desconectamos
If Opcion = True Then
Conexion.Properties("Jet OLEDB:Connection Control") = 1 'Impedimos que se conecten nuevos usuarios
Else
Conexion.Properties("Jet OLEDB:Connection Control") = 2 'Se permiten las nuevas conexiones
End If
Set rsConexiones = Conexion.OpenSchema(adSchemaProviderSpecific, , schemaID) ' Conexión sacada de la web de Microsoft. NO tira con a veces la variable... locura para encontrarlo
'Set rsConexiones = Conexion.OpenSchema(adSchemaProviderSpecific, , "{947bb102-5d43-11d1-bdbf-00c04fb92675}")'Parece que al final funciona la de arriba...
Set MiBD = CurrentDb
CurrentDb.Execute "DELETE * FROM CONEXIONES", dbFailOnError 'Vamos a borrar las conexiones anteriores
Set rsConectados = MiBD.OpenRecordset("CONEXIONES")
While rsConexiones.EOF = False
rsConectados.AddNew
rsConectados("HORA") = Now
rsConectados("USUARIO") = Trim(rsConexiones!LOGIN_NAME)
rsConectados("EQUIPO") = Trim(rsConexiones!COMPUTER_NAME)
rsConectados("CONECTADO") = rsConexiones!CONNECTED
rsConectados("ESTADO") = Trim(rsConexiones!SUSPECT_STATE)
datosWin = UsuarioWindows(rsConectados("EQUIPO")) 'Llamo a la función UsuarioWindows para cada usuario
rsConectados("USUARIO_WINDOWS") = datosWin.info_usuario
rsConectados("NOMBRE_COMPLETO") = datosWin.info_completo
rsConectados("DOMINIO") = datosWin.info_dominio
rsConectados("SERVIDOR") = datosWin.info_servidor
rsConectados.Update
rsConexiones.MoveNext
Wend
rsConectados.Close
Set rsConectados = Nothing
rsConexiones.Close
Set rsConexiones = Nothing
Exit Function
AlgoPasa:
MsgBox ("Error al comprobar las conexiones de usuarios. " & Err.Description), vbCritical, "Error al comprobar las conexiones"
End Function
Módulo redWindows
'*************************************************************************************************************************************
'Autor: Arkaitz Arteaga
'Más artículos: www.programadordepalo.com
'Mail de contacto: admin@programadordepalo.com
'Fecha: Enero 2014
'Version: 1.0
'*************************************************************************************************************************************
'Módulo para mostrar el usuario de Windows. Basado en http://msdn.microsoft.com/en-us/library/windows/desktop/aa370669%28v=vs.85%29.aspx
'De aqui la información de cada usuario http://msdn.microsoft.com/en-us/library/bb706729.aspx y http://support.microsoft.com/kb/151774/es
'OJO, he tenido problemas con las conversiones. Al final pasando a NetUserGetInfo los valores de servidor y de usuario remoto con arrays de bytes funciona.
'También había problemas ya que estaba pasando el equipo remoto en vez del servidor de base de datos de cada usuario
'
'*************************************************************************************************************************************
'*************************************************************************************************************************************
'Copyright: Por favor, no cuesta nada mantener un enlace a mi web en el código.
'Incluso pudes dejar los formularios tal cual, con un enlace a mi web.
'Si vas a utilizar este código con fines lucrativos (es decir, te van a pagar por ello) contacta conmigo por favor.
'*************************************************************************************************************************************
Option Compare Database
Option Explicit
'*************************************************************************************************************************************
'Traduzco que me aclaro mejor
'*************************************************************************************************************************************
Private Const NO_HAY_ERROR As Long = 0& 'Lo ponemos a 0. Se utiliza para comprobar luego que no hay errores
Private Const LONGITUD_PREFERIDA As Long = -1 'Buscando en la web de Microsfot "A constant of type DWORD that is set to –1"
Private Const MAS_DATOS As Long = 234&
Private Type usuario
info_usuario As Long 'Usuario actualmente logeado en esa máquina
info_dominio As Long 'Dominio al que se ha logeado el usuario
info_otros As Long 'En nuestro caso null
info_servidor As Long 'Servidor donde se autenticó el usuario. Muy importante para luego utilizar NetUserGetInfo
End Type
'En este guardo lo que me devuelve NetWkstaUserEnum y NetUserGetInfo, y luego lo devuelvo con la función. Lo hago público para poder dar de alta al llamar
Public Type USUARIO_FINAL
info_usuario As String
info_dominio As String
info_otros As String 'En nuestro caso null
info_servidor As String
'info_comentario As String 'No veo más campos con información, tal vez en otros tipos de redes...
info_completo As String 'Aqui guardo el nombre completo que devuelve NetUserGetInfo
End Type
'Lo mismo para sacar NetUserGetInfo*********Al fin parece que funciona, le estaba pasando el equipo remoto y no el servidor. Le paso level 10
Private Type INFORMACION_USUARIO
info_nombre As Long 'Aqui sale el nombre de usuario
info_ucomentario As Long
info_comentario As Long
info_completo As Long 'Aqui sale el nombre completo
End Type
Private Type INFORMACION_USUARIO_FINAL
info_nombre As String
info_ucomentario As String
info_comentario As String
info_completo As String
End Type
'***********************************************************************************
'Copio tal cual de las declaraciones de Microsoft para la función NetWkstaUserEnum
'***********************************************************************************
Private Declare Function NetWkstaUserEnum Lib "netapi32" _
(ByVal equipo As Long, _
ByVal nivel As Long, _
buffer As Long, _
ByVal longitudp As Long, _
entradasLeidas As Long, _
entradasTotales As Long, _
reanudar As Long) As Long
'***********************************************************************************
'Me daba problemas pasando strings como aparece en la web de Microsoft, tal vez convirtiendo a UNICODE hubiera funcionado.
'Queda por probar hacerlo con strings. Con bytes no hay problema
'***********************************************************************************
Private Declare Function NetUserGetInfo Lib "netapi32" _
(servidor As Byte, _
usuario As Byte, _
ByVal nivel As Integer, _
buffer As Long) As Integer
'***********************************************************************************
'Las copio tal cual de otros ejemplos
'***********************************************************************************
Private Declare Function NetApiBufferFree Lib "netapi32" _
(ByVal buffer As Long) As Long
Private Declare Sub CopyMemory Lib "Kernel32" _
Alias "RtlMoveMemory" _
(pTo As Any, uFrom As Any, _
ByVal lSize As Long)
Private Declare Function lstrlenW Lib "Kernel32" _
(ByVal lpString As Long) As Long
'***********************************************************************************
'***********************************************************************************
Function UsuarioWindows(NombreEquipo As String) As USUARIO_FINAL
Dim dservidor As Long
Dim entraLeidas As Long
Dim entraTotales As Long
Dim entraReanuda As Long
Dim estado As Long
Dim bufNet As Long
Dim tamaStruct As Long
Dim servidor As String
Dim info1 As usuario
Dim pasaInfo As USUARIO_FINAL
Dim usuInfo As INFORMACION_USUARIO_FINAL
Dim bservidor() As Byte
Dim busuario() As Byte
On Error GoTo algo_pasa
'NombreEquipo = Trim(NombreEquipo) 'No lo quita, será así.
servidor = "\\" & NombreEquipo & vbNullString
dservidor = StrPtr(servidor) 'StrPtr - devuelve la dirección del búfer de cadena UNICODE.
estado = NetWkstaUserEnum(dservidor, _
1, _
bufNet, _
LONGITUD_PREFERIDA, _
entraLeidas, _
entraTotales, _
entraReanuda)
If estado = NO_HAY_ERROR Or _
estado = MAS_DATOS Then
If entraLeidas > 0 Then
tamaStruct = LenB(info1)
CopyMemory info1, ByVal bufNet, tamaStruct
UsuarioWindows.info_usuario = Trim(PasaLoString(info1.info_usuario)) 'Trim por si acaso, aunque no mete mucha historia
UsuarioWindows.info_dominio = Trim(PasaLoString(info1.info_dominio))
UsuarioWindows.info_servidor = Trim(PasaLoString(info1.info_servidor))
bservidor = UsuarioWindows.info_servidor & Chr$(0) 'Convertimos
busuario = UsuarioWindows.info_usuario & Chr$(0)
'************************************************************************************
'No me funciona.... Pues ahora parece que si, le estaba pasando el equipo remoto en vez del servidor
usuInfo = UsuarioWindowsInformacion(bservidor(), busuario())
'************************************************************************************
UsuarioWindows.info_completo = usuInfo.info_completo
Else:
UsuarioWindows.info_usuario = "Posible maquina Win98"
End If
Else:
UsuarioWindows.info_usuario = "Error"
End If
Call NetApiBufferFree(bufNet)
Exit Function
algo_pasa:
MsgBox ("Error al revisar las conexiones a la base de datos" & Err.Description), vbCritical, "Error al revisar las conexiones"
End Function
Private Function UsuarioWindowsInformacion(eServidor() As Byte, eUsuario() As Byte) As INFORMACION_USUARIO_FINAL
'******************************************************
'Modificado de aqui
'http://msdn.microsoft.com/en-us/library/bb706729.aspx
'******************************************************
Dim usuario As INFORMACION_USUARIO
Dim buff As Long
On Error GoTo algo_pasa
If NetUserGetInfo(eServidor(0), eUsuario(0), 10, buff) = NO_HAY_ERROR Then 'es 10 -> Return user and account names and comments. The bufptr parameter points to a USER_INFO_10 structure.
CopyMemory usuario, ByVal buff, Len(usuario)
UsuarioWindowsInformacion.info_nombre = Trim(PasaLoString(usuario.info_nombre))
UsuarioWindowsInformacion.info_completo = Trim(PasaLoString(usuario.info_completo))
UsuarioWindowsInformacion.info_comentario = Trim(PasaLoString(usuario.info_comentario))
UsuarioWindowsInformacion.info_ucomentario = Trim(PasaLoString(usuario.info_ucomentario))
NetApiBufferFree buff
End If
Exit Function
algo_pasa:
MsgBox ("Error al revisar el usuario Windows" & Err.Description), vbCritical, "Error al revisar las conexiones"
End Function
Private Function PasaLoString(ByVal loPasa As Long) As String
''******************************************************
'Tal cual de la web de Microsoft
''******************************************************
Dim tmp() As Byte
Dim tmplon As Long
On Error GoTo algo_pasa
If loPasa <> 0 Then
tmplon = lstrlenW(loPasa) * 2 '2 bytes cada char
If tmplon <> 0 Then
ReDim tmp(0 To (tmplon - 1)) As Byte
CopyMemory tmp(0), ByVal loPasa, tmplon
PasaLoString = tmp
End If
End If
Exit Function
algo_pasa:
MsgBox ("Error al transformar" & Err.Description), vbCritical, "Error al revisar las conexiones"
End Function
Espero que os sirva, pero si tenéis alguna duda, os responderé en los comentarios de la web.
Arkaitz Arteaga
Latest posts by Arkaitz Arteaga (see all)
- Access: Encriptar contraseñas con SHA-256 utilizando biblioteca de clases .NET con C# - 4 mayo, 2014
- Rendimiento de Access contra backend Access en servidor de archivos remoto. Cuarta parte. - 27 abril, 2014
- Rendimiento de Access contra backend Access en servidor de archivos remoto. Aclaración. - 21 abril, 2014
- Utilizar biblioteca de clases .NET en Access. Tercera aproximación a la Interoperabilidad COM - 14 abril, 2014
- Vincular tablas en Access con Visual Basic - 11 abril, 2014