May 2011 - Artículos

Apuntes para una seguridad a nivel de usuario en Access 2010. Proteger el back-end

Las versiones de Access anteriores a la 2007 incluían un sistema de seguridad a nivel de usuario basado en grupos de trabajo bastante completo… salvo por el hecho de que, al menos en los últimos años, no servía absolutamente para nada. Resultaba del todo inútil identificar usuario y contraseña y asignar permisos en consecuencia si, con una sencilla búsqueda en interné, encontrábamos varias aplicaciones gratuitas con las que piratear las contraseñas. En consecuencia, en Access 2007 se abandona esta seguridad tan insegura.

A cambio, no se ha implementado ningún sistema de seguridad a nivel de usuario, aunque se afirma que la seguridad ha mejorado notablemente, pues ahora las bases de datos se pueden proteger por contraseña al tiempo que se cifran por un algoritmo muy seguro. A partir de ese cifrado con contraseña, cada uno debe implementar su propio sistema de seguridad por usuarios. También podríamos guardar los datos en SQL-Server o en Sharepoint, utilizar la seguridad de éstos y olvidarnos de crear la nuestra propia, pero esto a menudo no es asequible.

Con archivos accdb, que es de lo que vamos a tratar, una posibilidad interesante es crear un sistema basado en los eventos de datos de Access 2010, de esa manera la lógica de la seguridad se traslada al back-end y se centra en las tablas en vez de hacerlo en la aplicación que las maneja.

Pero, para que el sistema sea seguro, tenemos que conseguir que el back-end sea completamente inaccesible para quien no cuente con los permisos necesarios y que ello no le impida ser accesible desde el fornt-end. La solución para esto ya está estudiada y vale lo mismo para Access 2007 que para Access 2010; será de lo que tratemos en este artículo y dejaremos para otro posterior cómo usar las macros de eventos de datos de Access 2010 para asegurar nuestros datos y, en un tercero, veremos cón utilizar las macros de datos para hacer un archivo de seguimiento, un log, de los cambios en los datos del back-end.

Proteger el back-end en Access 2007/2010

A partir de Access 2007, si protegemos nuestros datos con una contraseña, quedan a buen recaudo, puesto que la contraseña es mucho más segura que en versiones anteriores y, además, los datos se cifran.

El problema está en cómo hacer que nuestra aplicación acceda a esos datos sin que la contraseña quede expuesta. Podemos evitar tener tablas vinculadas y abrir los recordsets de todos los objetos mediante código, pero eso resulta pesado y se pierde la sencillez, que es la principal ventaja de Access. Al vincular tablas de una BD protegida, Access pide la contraseña y la guarda en la propiedad Connect del objeto TableDef correspondiente a esa tabla, de manera que basta con leer la propiedad Connect de la tabla correspondiente para conocer la contraseña

Es lo que se plantea en este hilo en UtterAccess http://www.utteraccess.com/forum/Access-2007-security-t1242310.html&p=1243573#entry1243573 y ahí mismo proponen una solución que, aunque luego se complica, es bastante sencilla y es de la que partimos: Las tablas se vinculan sin contraseña, lo cual al intentar abrirlas produciría un error, pero al inicio de la aplicación creamos, mediante código en el que indicamos la contraseña, un recordset que se mantiene abierto toda la aplicación y, al quedar abierta la conexión con la BD protegida, es innecesario indicarle de nuevo la contraseña, por lo que podemos abrir las tablas vinculadas.

Ocurre algo parecido cuando utilizamos conexiones ODBC, que la contraseña se guarda y queda accesible, y la solución es la misma, no guardar la conexión con contraseña, abrir una conexión mediante código y mantenerla abierta durante toda la aplicación. En http://blogs.office.com/b/microsoft-access/archive/2011/04/08/power-tip-improve-the-security-of-database-connections.aspx, otra vez en guiri, nos cuentan cómo hacerlo.

El código puede hacerse inaccesible, por ejemplo, convirtiendo la aplicación en ACCDE y/o protegiendo a su vez, previamente, el proyecto VBA con otra contraseña. De esta manera, las tablas sólo se pueden abrir desde nuestra aplicación y, desde ella, controlamos quién y cómo puede acceder a los datos. Cualquiera podría importar nuestras tablas vinculadas desde otra BD, pero no sabiendo la contraseña, no le serviría de nada. También podría, si es un usuario autorizado, abrir directamente a las tablas de nuestra aplicación y saltarse así los permisos que damos mediante código, pero eso también podemos evitarlo, por ejemplo, comprobando que la aplicación se ejecuta en modo runtime justo antes de abrir el recordset que proporciana la clave al resto de la aplicación y, cuando sepamos asegurar los datos con macros de datos, ni siquiera podrán modicar nada sin permiso, aunque accedan a las tablas.

En resumen, los pasos para proteger nuestro back-end de una forma muy sencilla serían los siguientes:

1º- Vincular las tablas del back-end antes de cifrarlo con contraseña (de esa manera la contraseña no se guardará en la propiedad Connect)

2º - Cifrar con contraseña el back-end

image    image

3º -  Al inicio de nuestra aplicación abrir un formulario invisible (o un módulo de clase) en el que asignaremos la contraseña mediante código, y que mantendrá abierta la conexión con el back-end durante toda la aplicación.

Este último punto puede parecer difícil, pero es sencillo haciéndolo de la siguiente manera:

  • Vinculamos una tabla con datos irrelevantes. Nos pedirá contraseña y se guardará, pero más tarde la borraremos. Como ya existiría esa misma tabla vinculada, se guardará con el mismo nombre y un ordinal, por ejemplo TablaTonta1.

image

  • A partir de la tabla recién vinculada, creamos el formulario con sus campos.

image

  • Cambiamos la propiedad “Origen del registro” del formulario para dejarla en blanco.

image

  • En el evento Load del formulario abrimos un recordset que tome los datos de la BD protegida y lo asignamos como recordset del formulario.
Private Sub Form_Load()
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset("SELECT TablaTonta.* FROM TablaTonta IN '' [MS Access;PWD=Anchoas de Santoña;DATABASE=C:\Users\Chea\Documents\Back-end protegido.accdb]", dbOpenDynaset)
Set Me.Recordset = rs
End Sub
  • Probamos que el formulario se abre correctamente y cambiamos la propiead Visible del formulario a No.

    Hay un truco para obtener fácilmente una cadena SQL correcta que abra un recordset de una tabla en una BD externa protegida por contraseña:

        • En el editor de VB abrimos la ventana de inmediato, decimos que nos imprima la propiedad Connect de la tabla que tenemos vinculada con contraseña y copiamos el texto de la propiedad connect al portapapeles

    image

        • Abrimos el asistente para consultas para crear una nueva consulta y, antes de añadir una nueva tabla, copiamos el texto de la propiedad Connect a la propiedad de la consulta Cadena de conexión de origen

    image

        • Añadimos ahora la tabla que queramos. Observe que lo que se muestra ahora son las tablas y consultas de la base protegida (Sí, he dicho consultas, se puede hacer un Select a una consulta de una BD externa)
        • Seleccionamos los campos que queramos y copiamos el texto SQL resultante.

    image

    Con ese texto SQL podemos crear el recordset que asignaremos como origen de nuestro formulario.

      • Borramos la tabla que acabamos de vincular con contraseña: Ya no la necesitas, la única mención a la contraseña que existe en nuestra aplicación está oculta en el código que asigna el recordset al formulario oculto.

    Ya tenemos la manera de abrir una conexión con el back-end protegido que, permaneciendo abierta durante toda la aplicación, permita abrir otras tablas de ese back-end sin necesidad de indicar de nuevo la contraseña. Es decir, tenemos la llave para abrir los datos protegidos, sólo queda verificar el usuario antes de usar esa llave, por ejemplo, en el mismo formulario oculto, antes de asignar el recordset, podemos comprobar el usuario y sus permisos y, si no son los adecuados, en vez de asignar el recordset del formulario cerramos la aplicación. La manera de controlar usuario y permisos depende de cada cuál y no tiene mayor complicación, cuanto más personal sea, más segura, pero una forma muy sencilla podría ser tener una tabla con usuarios contraseñas y permisos en el back-end, con la que abriremos, mediante código, un recordset de la misma manera que hacíamos con la tabla tonta, para verificar los permisos, procurando primero haber borrado cualquier vínculo a la tabla.

    Con estos sencillos pasos conseguimos que no se pueda acceder a nuestros datos directamente, sino solo a través de nuestra aplicación, de manera que podemos controlar quién y cómo lo hace. Es bastante seguro para lo fácil que resulta, pero en el propio hilo en UtterAccess que citábamos más arriba, advierten de posibles grietas y posibles soluciones.

    La pega es que estamos protegiendo, o accediendo, a todo el archivo de back- end a la vez, y, si queremos dar permisos ppersonalizados por usuarios y tablas, debemos hacerlo por código, que sólo controla la edición de datos cuanto se hace a través de un formulario. El problema más obvio es que desde una instalación completa de Access, un usuario autorizado para abrir la aplicación puede pulsaf F11 para hacer que se muestren las tablas y editar los datos, saltándose todo el sistema de autorizaciones que hayamos implementado mediante código en los formularios.

    Si estamos usando Access 2007, se me ocurre la solución de forzar a que la aplicación se ejecute en modo runtime, que no muestra las tabla; simplemente bastaría con verificar en el formulario de apertura si SysCmd(acSysCmdRuntime) es verdadero y, en caso contrario, cerrar la aplicación. Otra opción sería tener varios archivos de back-end agrupando las tablas por niveles de privilegios.

    En cambio, si usamos Access 2010, cabe otra posibilidad, utilizar macros de datos en el back-end que comprueben los permisos del usuario antes de insertar, modificar o borrar datos en la tabla y, si carece de ellos, provocar un error que impida la actualización. Toda la lógica de la seguridad por usuarios se traslada al back-end, se centraliza y se hace más sencilla y, por tanto, más segura. De ello trataremos en un próximos artículo.

    Lo que aquí se ha expuesto no es más que unos apuntes que puedan servir de punto de partida para que cada cual desarrolle su propio sistema de seguridad; no obstante, sin complicar demasiado las cosas, podemos tomar algunas medidas adicionales:

    • Verificar que UserControl sea verdadero para evitar automatización
    • Comprobar que CodeProject es el mismo que CurrentProject para evitar la carga como librería
    • Proteger con contraseña el proyecto VBA y convertirlo en ACCDE.
    • Sustituir el formulario por un módulo de clase en el que el recordset sea Friend.
    • Ocultar las tablas utilizando el atributo dbHiddenObject.

    En un próximo artículo veremos, cómo utilizando un back-end cifrado con contraseña, podemos valernos de las macros de eventos de datos de Access 2010 para impedir que un usuario no autorizado pueda insertar, modificar o borrar datos, incluso aunque tuviera acceso a las tablas vinculadas del front-ed.

    Gestión de turnos de trabajo con Access

    En determinadas empresas, o en determinados servicios, se trabajan muchas más horas a las semana de las que puede hacer un trabajador. Servicios médicos, policía, bomberos, fundiciones, funerarias, etc. dan servicio 24 horas al día 7 días a la semana y, como no hay ningún trabajador capaz de tanto, deben hacer distintos turnos que cubran todo el servicio. También es necesario hacerlos en empresas, como centros comerciales, que sin abrir 24 horas abren más horas, o durante más días, de lo que corresponde a una jornada laboral normal.

    Los turnos son distintos dependiendo del tipo de trabajo, necesidades de la empresa, etc., por ejemplo, puede resultar que, dado el tipo de trabajo, por la noche se necesito menos personal, que se cierren los domingos o que no se cierre nunca, que se tenga derecho a más o menos días de descanso, etc. Dependiendo de esas necesidades, cada empresa organiza sus turnos de una manera distinta pero los resultados tienen cosas en común: Se elabora una secuencia de los distintos turnos o tipos de jornada que debe hacer un trabajador y se organiza a los trabajadores en grupos, de manera que empezando cada uno la secuencia en una fecha distinta, se solapen unos con otros y cubran toda la jornada.

    Supongamos un caso muy sencillo: Cuatro trabajadores deben cubrir mañana tarde y noche y libran un día de cada cuatro. La secuencia sería mañana, tarde, noche, libre MTNL y cada trabajador la iniciaría con un día de diferencia.

    Trabajador 1: MTNLMTNLMTNLMTNLMTNL…

    Trabajador 2: TNLMTNLMTNLMTNLMTNLM…

    Trabajador 3: NLMTNLMTNLMTNLMTNLMT…

    Trabajador 4: LMTNLMTNLMTNLMTNLMTN…

    Cada columna representa un día y con este esquema se garantiza que todos los días haya un trabajador de tarde, otro de noche y otro librando.

    La mayor parte de los esquemas serán más complejos que éste, pero lo normal es que todos tengan una secuencia de turnos fija comenzando en distinto momento para cada grupo de trabajadores. Tomando esas secuencias y esos grupos como punto de partida, vamos a proponer una serie de soluciones que faciliten la gestión de grupos de trabajo utilizando una aplicación hecha en Access.

    Averiguar el turno para una fecha

    Necesitamos saber qué turno de trabajo le corresponde a un trabajador o grupo en determinada fecha. La primera tentación puede ser recorrer un bucle partiendo desde un turno conocido, sin embargo, hay otro planteamiento que resulta familiar, pues es el del calendario, es decir, de la misma manera que ordenamos las fechas, que son números consecutivos, en 7 columnas, una para cada día de la semana, y luego podemos mediante una función saber qué día de la semana es una determinada fecha,  podemos ordenarlas en tantas columnas como tenga la secuencia.

    Enero:

    M

    T

    N

    L

    1

    2

    3

    4

    4

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

     

    Para averiguar la columna que corresponde a un determinado item, hace años que descubrí el Mediterráneo al hacerme esta función:

     

    '---------------------------------------------------------------------------------------
    ' Procedure : fColumna
    ' DateTime  : 17/10/10 15:10
    ' Author    : Chea
    ' Purpose   : Dada una relación de números consecutivos ordenados en columnas, devuelve la
    '             columna que le corresponde a un determinado item
    '---------------------------------------------------------------------------------------
    '
    Public Function fColumna(item As Long, Columnas As Long) As Long
    fColumna = 1 + ((item + Columnas - 1) Mod (Columnas))
    End Function

     

    La función vale para resolver muchos problemas si se pueden plantear a partir de números consecutivos ordenados en columnas, pero es algo genérico que presupone que la primera columna es el número 1, lo cual, como en el ejemplo, no siempre es el caso y tenemos que considerar en qué columna se encuentra el número 1. Para eso con los turnos de trabajo he creado una función específica.

    '---------------------------------------------------------------------------------------
    ' Procedure : fTurnoFecha
    ' DateTime  : 17/10/10 15:12
    ' Author    : Chea
    ' Purpose   : Dada una determinada cadencia de turnos expresada en una cadena, sCadena, en la que
    '             los turnos vienen separados por comas, obtiene el turno que corresponde a una determinada
    '             fecha, FechaCalculo. Es necesario conocer una fecha de referencia, PrimerDiaTurno,
    '             que coincida con el primer día de la cadencia.
    '             Usando una fecha fija como PrimerDiaturno,podemos pasar un parámetro opcional,
    '             lDesplazamiento, para indicar cuantos días nos desplazamos del PrimerDiaturno.
    '             Por ejemplo, podemos tener distintos grupos, cada uno empezando en un día
    '             distinto, el primero tendrá desplazamiento 0, el segundo empezará un día más tarde, etc.
    '---------------------------------------------------------------------------------------
    '
    Public Function fTurnoFecha(FechaCalculo As Date, sCadencia As String, PrimerDiaTurno As Date, Optional lDesplazamiento As Long) As String
    Dim v As Variant
    Dim lColumnas As Long
    Dim lCompensacion As Long
    
    v = Split(sCadencia, ",")
    lColumnas = 1 + UBound(v)
    
    lCompensacion = fColumna(Int(PrimerDiaTurno + lDesplazamiento), lColumnas) - 1
    fTurnoFecha = v(fColumna(Int(FechaCalculo) - lCompensacion, lColumnas) - 1)
     
    End Function

     

    A la función le pasamos FechaCalculo, que es la fecha para la que queremos obtener el turno, sCadencia, que es una cadena en la que indicamos la secuencia del turno, con los items separados por comas, por ejemplo “M,T,N,L” o “Mañana,Tarde,Noche,Libre”. También le pasamos PrimerDiaTurno que es una fecha de la que sepamos con certeza que se corresponde con el primer día de la secuencia o, de manera opcional, lDesplazamiento, que podemos utilizar para indicar cuantos días se desplaza de una fecha fija, por ejemplo 0, el primer día del turno, calculándolo por tanteo.

    Por ejemplo, el día en que, tras una ardua negociación de empresa y trabajadores, se elaboraron las planillas, se comprobó que el trabajador 2 debía iniciar su secuencia el 4 de enero del 2007 y se anotó esta fecha para posteriores cálculos, según esto, para cálcular cómo le toca la Nochebuena del 2010, el cálculo en la ventana de inmediato sería:

     

    ? fTurnoFecha (#2010/12/24#,"Mañana,Tarde,Noche,Libre",#2007/01/04#)
    Noche

    Aparte de para hacerle la puñeta al trabajador 2, la función se puede utilizar en una consulta para obtener una relación de todos los turnos de todos los trabajadores para todos los días de un periodo determinado.

     

    Elaborar un calendario con turnos

    Tomemos una tabla de empleados, con indicación de a qué grupo pertenecen

    EmpleadosTurnos
    Id Empleado Grupo
    1 Pedro 1
    2 Andrés 1
    3 Jaime 1
    4 Juan 2
    5 Felipe 2
    6 Bartolomé 2
    7 Tomás 3
    8 Mateo 3
    9 Santiago 3
    10 Judas 4
    11 Simón 4
    13 Matías 4

    Aunque el último ID sea 13, se trata de 12 empleados, pues despidieron a uno por conducta desleal. Están organizados en 4 grupos cada uno de los cuales se inicia al día siguiente del grupo anterior. Vamos a crear el calendario correspondiente a octubre del 2010 utilizando una consulta.

    Sabemos que si disponemos de una tabla Numeros poblada de numeros consecutivos empezando por el 1, podemos obtener una secuencia de fechas a base de sumar una FechaInicial menos uno al campo Numero. Este truco y el de no relacionar tablas para que el resultado sea un producto cartesiano lo vamos a utilizar en la siguiente consulta.

     

     

    image

     

    El texto SQL es el siguiente:

     

    SELECT EmpleadosTurnos.Empleado, [Numero]+CDate("01-10-10")-1 AS FechaVirtual, EmpleadosTurnos.Grupo, fturnofecha([fechavirtual],"Mañana,Tarde,Noche, Libre",CDate("04-01-07"),[Grupo]-1) AS Turno
    FROM Numeros, EmpleadosTurnos
    WHERE ((([Numero]+CDate("01-10-10")-1)<=CVDate("31-10-10")))
    ORDER BY EmpleadosTurnos.Empleado, [Numero]+CDate("01-10-10")-1;

    Y el resultado, resumiendo, porque serían 372 filas, mostraría algo parecido a lo siguiente completado hasta mostrar todos los empleados con todas las fechas de octubre del 2010 y el turno de cada fecha

     

    qTurnosVirtuales
    Empleado FechaVirtual Grupo Turno
    Andrés 01/10/10 1 Noche
    Andrés 02/10/10 1 Libre
    Andrés 03/10/10 1 Mañana
    Andrés 04/10/10 1 Tarde
    Andrés 05/10/10 1 Noche
    Andrés 06/10/10 1 Libre
    Andrés 07/10/10 1 Mañana
    Andrés 08/10/10 1 Tarde
    Andrés 09/10/10 1 Noche
    Andrés 10/10/10 1 Libre
    Andrés 27/10/10 1 Mañana
    Andrés 28/10/10 1 Tarde
    Andrés 29/10/10 1 Noche
    Andrés 30/10/10 1 Libre
    Andrés 31/10/10 1 Mañana
    Bartolomé 01/10/10 2 Tarde
    Bartolomé 02/10/10 2 Noche
    Bartolomé 03/10/10 2 Libre
    Bartolomé 04/10/10 2 Mañana
    Bartolomé 05/10/10 2 Tarde
    Bartolomé 06/10/10 2 Noche
    Bartolomé 07/10/10 2 Libre
    ..

     

    La consulta la podemos personalizar para pasarle las fechas inicial y final del periodo como parámetros y también podemos tomarla como puento de partida para una consulta de datos anexado o de actualización que para cada día del año y cada empleado deje anotado el turno de trabajo. Aunque no es necesario volcar los datos en una tabla, pues podemos utilizar la consulta con cualquier periodo de fechas, si resulta muy conveniente, pues además de los distintos turnos pueden darse otras situaciones, como bajas, vacaciones, cambios de turno que hacen necesario trabajar con datos guardados.

    A menudo se pretende realizar una planilla que refleje los trabajadores que entran en cada turno cada día, utilizando los días como encabezado de columna.

     

     

    qTurnosVirtuales_Tabla de referencias cruzadas
    Empleado Grupo 01/10/10 02/10/10 25/10/10 26/10/10 27/10/10 28/10/10 29/10/10 30/10/10 31/10/10
    Andrés 1 Noche Libre Noche Libre Mañana Tarde Noche Libre Mañana
    Jaime 1 Noche Libre Noche Libre Mañana Tarde Noche Libre Mañana
    Pedro 1 Noche Libre Noche Libre Mañana Tarde Noche Libre Mañana
    Bartolomé 2 Tarde Noche Tarde Noche Libre Mañana Tarde Noche Libre
    Felipe 2 Tarde Noche Tarde Noche Libre Mañana Tarde Noche Libre
    Juan 2 Tarde Noche Tarde Noche Libre Mañana Tarde Noche Libre
    Mateo 3 Mañana Tarde Mañana Tarde Noche Libre Mañana Tarde Noche
    Santiago 3 Mañana Tarde Mañana Tarde Noche Libre Mañana Tarde Noche
    Tomás 3 Mañana Tarde Mañana Tarde Noche Libre Mañana Tarde Noche
    Judas 4 Libre Mañana Libre Mañana Tarde Noche Libre Mañana Tarde
    Matías 4 Libre Mañana Libre Mañana Tarde Noche Libre Mañana Tarde
    Simón 4 Libre Mañana Libre Mañana Tarde Noche Libre Mañana Tarde

     

    Evidentemente es fácil de hacer con una consulta de referencias cruzadas.

     

    image

     

    El texto SQL sería:

    TRANSFORM First(qTurnosVirtuales.Turno) AS PrimeroDeTurno
    SELECT qTurnosVirtuales.Empleado, qTurnosVirtuales.Grupo
    FROM qTurnosVirtuales
    GROUP BY qTurnosVirtuales.Empleado, qTurnosVirtuales.Grupo
    ORDER BY qTurnosVirtuales.Grupo
    PIVOT qTurnosVirtuales.FechaVirtual;

     

    Si nuestra consulta de referencias cruzadas la convertimos en un informe y usamos formato condicional, el resultado es la mar de vistoso (o la mar de hortera, según gustos). En el ejemplo le hemos quitado columnas para que nos quepa bien en la página.

    image

     

    En fin, se trata de una serie de ideas que pueden servir a alguien que debe plantearse la gestión de turnos de trabajo, pero no pretende ser ni “la solución” ni la forma canónica de plantearlo.

    Publicado por Chea con 7 comment(s)
    Archivado en: ,