sábado, 30 de enero de 2021

Login failed for user 'X'


Hace bastante tiempo que no venía por aquí para compartir otra entrada en relación a SQL Server o .NET. Pero en esta ocasión la situación lo amerita, debido que un cliente me pidió solucionar un problema de conexión que jamás había visto en 18 años y me gustaría compartir lo que he ido haciendo para encontrar la solución.

Problema: No te puedes conectar con el SSMS a la instancia local utilizando autenticación de SQL Server, el mensaje que devuelve al momento de conectarse es el famosísimo:

Login failed for user 'X' (Microsoft SQL Server, Error: 18456)

Si abres la ventana para obtener mayor información del error te muestra:

Server Name: .
Error Number: 18456
Severity: 14
State: 1
Line Number: 65536

Y si vas al archivo ErrorLog de la instancia te encuentras algo así:

Logon       Error: 18456, Severity: 14, State: 8.
Logon       Login failed for user 'X'. Reason: Password did not match that for the login provided. [CLIENT: <local machine>]

Antes de que comiences a pensar en lo que podría ser, te comparto la configuración verificada:
  1. Autenticación de SQL Server habilitada en la instancia y ésta ya fue reiniciada en cuanto fue aplicado el cambio.
  2. Ya se verificó que la contraseña sea correcta, inclusive estableciendo la contraseña en el login copiando su valor desde un bloc de notas.
  3. Debido a que la conexión es local, ya se verificó que el protocolo Shared Memory esté habilitado.
  4. Service Pack 4 instalado sobre la instancia (SQL Server 2012) y aplicado también en el SQL Server Management Studio.
Pruebas que ya se realizaron y su resultado:
  1. Conexión local utilizando sqlcmd y con autenticación de SQL: la conexión fue exitosa.
  2. Conexión remota con SQL Server Management Studio y autenticación de SQL: la conexión fue exitosa. 
Con todo lo anterior podemos saber que el problema no es la autenticación debido a que sí se ha podido establecer una conexión tanto local como remota con autenticación de SQL Server.    El único lugar donde está fallando la conexión es en el SSMS local, ya se intentó por ".", "localhost" y por dirección IP, y el resultado siempre es el mismo.

Para poder averiguar un poco más de lo que podría estar haciendo el SSMS al momento de intentar iniciar la conexión con autenticación de SQL Server, utilizamos el procmon de sysinternals pero nada de lo que encontramos ayudó a intentar identificar el problema.

Releí el mensaje de error que se almacena en el archivo ErrorLog de la instancia y encontré algo extraño, que está reportando que la contraseña no es correcta para el login.    Lo que me parece súper extraño por lo siguiente:
  • Si la contraseña la copio desde un bloc de notas y la pego en la línea de comandos con el sqlcmd, sí se conecta.
  • Si la contraseña la copio desde un bloc de notas y la pego en la caja de texto de contraseña del SSMS, ¡no conecta!
La siguiente prueba que se me ocurrió fue cambiarle la contraseña al login "X" y ponerle una contraseña vacía, ¿y qué crees?, ¡SÍ CONECTÓ DESDE EL SSMS!

Lo anterior me lleva a pensar que por alguna muy extraña razón (y me parece extraña porque al momento de escribir esto la desconozco) la contraseña escrita en la caja de texto de la pantalla de ingreso del SSMS, está siendo alterada de alguna manera antes de llegar a SQL Server.

Para no dejar algo de lado y sin probar, se me ocurrió hacer un programa que pidiera servidor, usuario y contraseña para conectarse, en los framework 2.0, 3.5, 4 y 4.5.   Todas las pruebas fueron exitosas.

No encontré alguna referencia del lenguaje de programación utilizado para el SSMS 2012, no sé si fue hecho en C++ o en el .NET Framework.    Pero para verificar que no solamente fuera local el problema, generé un login en otro servidor de base de datos de pruebas y que está expuesto a la internet, hice la prueba de conexión ¡y no conectó!, el error reportado en el otro servidor es el mismo: Password did not match that for the login provided.

Esto comienza a preocuparme un poco más de lo normal, primero por la incertidumbre de no saber lo que está sucediendo, y segundo porque la contraseña que escribes en la caja de texto no es la misma que le está llegando al servidor destino, ¿habrá sido comprometido el servidor?, lo revisaré más a fondo para ver si encuentro algo extraño (sí, más)

En cuanto tenga más noticias, regresaré a actualizar esta entrada.

Se realizó una revisión de componentes del sistema operativo y también se corrió una revisión completa contra malware, ambas operaciones no reportaron problemas.

Lamentablemente, no se encontró otra alternativa que reinstalar el SQL Server Management Studio y el problema dejó de ocurrir.    Y pongo "lamentablemente" porque no me deja satisfecho la solución, pero el tiempo ya se tenía encima y no había forma de seguir investigando para encontrar el problema de raíz.

lunes, 29 de octubre de 2018

Leyendo RSS desde SQL Server



En esta entrada quiero abordar una forma en la que podemos obtener información de un RSS e integrarla a una base de datos.

Es común que haya información que resulte relevante o interesante para la operación de una empresa y ésta se encuentre disponible a través de fuentes RSS, el cual es un formato bien definido a través de un esquema XML que podemos encontrar en RSS 2.0 at Harvard Law.

Elegí esa dirección para mostrar el esquema del documento porque siento que la documentación ahí mostrada está ordenada de una forma muy sencilla de entender.

Para implementar una solución que nos permita obtener fuentes RSS necesitamos de un módulo que lea la información de la URL donde está ubicado el XML y posteriormente se lo dé a SQL Server para que tome el XML y lo convierta a una representación entidad-relación que es mucho más manejable.

Este proyecto lo dividiremos en los siguientes pasos:

  1. Tomar una fuente RSS como muestra.
  2. Crear un módulo en .NET que pueda ser integrado a SQL Server.
  3. Crear un assembly en SQL Server y crear la función que devuelva la información contenida en la URL que reciba como parámetro.

URL de muestra

Tomaré como base el RSS de deportes del sitio web de ESPN, si abrimos la liga nos mostrará un XML con el contenido de la fuente.

Módulo de .NET

Voy a crear un proyecto del tipo Biblioteca de Clases en Visual Studio 2017 y lo llamaré SQLRSSReader, en él crearemos un par de clases:

  1. RSSItem para representar cada entrada de la fuente RSS.
  2. Lector para implementar la función que SQL Server llamará.

A continuación el código de RSSItem:

namespace SQLRSSReader
{
    public class RSSItem
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public string Link { get; set; }
        public string PubDate { get; set; }
    }
}

Para exponer una función tabular a SQL Server, es necesario que se den las siguientes condiciones:

  1. Tener el atributo SqlFunctionAttribute.
  2. Pública y estática.
  3. Devolver IEnumerable.
  4. Los tipos de parámetros de entrada deben ser de SQL Server.

La función quedaría de la siguiente manera:

[SqlFunction(FillRowMethodName = "LlenarItem")]
public static IEnumerable ObtenerRSS(SqlString url)
{
    HttpWebRequest webRequest = HttpWebRequest.Create(url.Value) as HttpWebRequest;
    HttpWebResponse webResponse = webRequest.GetResponse() as HttpWebResponse;
    List<RSSItem> result = null;

    using (StreamReader sr = new StreamReader(webResponse.GetResponseStream()))
    {
        XmlDocument xdRSS = new XmlDocument();
        xdRSS.LoadXml(sr.ReadToEnd());
        result = new List<RSSItem>();
        foreach (XmlNode xnItem in xdRSS.SelectNodes("/rss/channel/item"))
        {
            result.Add(new RSSItem()
            {
                Description = (xnItem.SelectSingleNode("./description") != null ? xnItem.SelectSingleNode("./description").InnerText : null),
                Link = (xnItem.SelectSingleNode("./link") != null ? xnItem.SelectSingleNode("./link").InnerText : null),
                PubDate = (xnItem.SelectSingleNode("./pubDate") != null ? xnItem.SelectSingleNode("./pubDate").InnerText : null),
                Title = (xnItem.SelectSingleNode("./title") != null ? xnItem.SelectSingleNode("./title").InnerText : null)
            });
        }
    }

    return result;
}

Lo que hace nuestra función es descargar el RSS, llenar una lista de objetos RSSItem y devolverla como resultado.

Notarás que el atributo SqlFunction tiene un parámetro llamado FillRowMethodName, en él se coloca el nombre de la función que se mandará a llamar para llenar la tabla resultante.    Por cada objeto que contenga IEnumerable se llamará a la función a la que apunta FillRowMethodName.

Ahora veamos la función LlenarItem:

public static void LlenarItem(object rssItemObject, out SqlString Title, out SqlString Description, out SqlString Link, out SqlString PubDate)
{
    RSSItem rssItem = rssItemObject as RSSItem;
    if (rssItem.Title != null)
        Title = new SqlString(rssItem.Title);
    else
        Title = SqlString.Null;
    if (rssItem.Description != null)
        Description = new SqlString(rssItem.Description);
    else
        Description = SqlString.Null;
    if (rssItem.Link != null)
        Link = new SqlString(rssItem.Link);
    else
        Link = SqlString.Null;
    if (rssItem.PubDate != null)
        PubDate = new SqlString(rssItem.PubDate);
    else
        PubDate = SqlString.Null;
}

Vamos a analizar los parámetros uno por uno:

  1. rssItemObject.   Es donde recibiremos la instancia de RSSItem que se quiere operar en ese momento.
  2. Title.    Primera columna de la tabla resultante.
  3. Description.    Segunda columna de la tabla resultante.
  4. Link.    Tercera columna de la tabla resultante.
  5. PubDate.    Cuarta columna de la tabla resultante.

También debemos notar que cada una de las columnas tiene "out", eso nos obliga a darle un valor (aunque sea null) antes de terminar la ejecución de la función.

Compilemos nuestro proyecto para generar el DLL correspondiente.

Función en SQL Server

Para probar nuestra función vamos a crear una base de datos llamada SQLBlog y le habilitaremos la opción TrustWorthy para que se le considere una base de datos de confianza.

USE master
GO
IF EXISTS (SELECT name FROM sys.databases WHERE name = N'SQLBlog')
BEGIN
ALTER DATABASE SQLBlog
    SET SINGLE_USER
    WITH ROLLBACK IMMEDIATE;
    DROP DATABASE SQLBlog
END
GO
CREATE DATABASE SQLBlog
ON PRIMARY (
    NAME = 'SQLBlog_dat',
    FILENAME = 'C:\LaCarpeta\SQLBlog.mdf'
)
LOG ON (
    NAME = 'SQLBlog_log',
    FILENAME = 'C:\LaCarpeta\SQLBlog.ldf'
)
GO
ALTER DATABASE SQLBlog
SET TRUSTWORTHY ON

Ahora vamos a crear el ensamblado que alojará el DLL que acabamos de crear en .NET:

USE SQLBlog
GO
CREATE ASSEMBLY SQLRSSReader
FROM 'C:\ElEnsamblado\SQLRSSReader.dll'
WITH PERMISSION_SET = UNSAFE

Finalmente creamos la función y la probamos:

CREATE FUNCTION udfGetRSS(
    @URL NVARCHAR(256)
)
RETURNS TABLE(
    Title NVARCHAR(200),
    Description NVARCHAR(4000),
    Link NVARCHAR(256),
    PubDate NVARCHAR(50)
)
AS EXTERNAL NAME SQLRSSReader.[SQLRSSReader.Lector].ObtenerRSS
GO
SELECT * FROM udfGetRSS(N'http://www.espn.com/espn/rss/news')

Antes de terminar con esta entrada quiero que notes lo siguiente:

  1. El ensamblado debe ser creado con PERMISSION_SET en UNSAFE porque va a realizar lectura de sitios web, es decir que necesita salir del servidor.
  2. Las cadenas expuestas por la función deben ser del tipo UNICODE, por ello notarás que son NVARCHAR.
  3. La notación para apuntar a la función es la siguiente: AssemblyName.[Namespace.Class].Function

Prueba con otra dirección, por ejemplo: https://www.tendencias21.net/xml/syndication.rss

Espero te haya resultado de utilidad esta entrada, ¡saludos!



jueves, 25 de octubre de 2018

Vistas, funciones y procedimientos almacenados



¿Para qué utilizar vistas, funciones y procedimientos almacenados?, ¿solamente para que se vea bonita la base de datos con muchos objetos de todos los tipos posibles?

Tenemos dos objetivos principales cuando ocupamos estos objetos en nuestra base de datos:

  1. Encapsular la forma en que se obtienen los datos de la base de datos.
  2. Implementar una capa de seguridad por encima del acceso a los datos.

Encapsulamiento

Para atacar este tema lo voy a dividir en dos partes, en "Vistas y funciones" y en "Procedimientos almacenados".

Funciones y vistas

No sé si te haya tocado trabajar con la base de datos de algún ERP o con la base de datos de un sistema a cuyo programador le resultó divertido ofuscar los nombres de los objetos.

La irremediable consecuencia de lo anterior es que la extracción de datos se vuelve un dolor de cabeza debido a que los nombres de los objetos no es descriptivo por sí mismo.

Supongamos que tenemos una tabla de este estilo:

CREATE TABLE XT56(
    A INT IDENTITY(1, 1),
    B VARCHAR(80) NOT NULL,
    C DECIMAL(8, 2) NOT NULL,
    D BIT NOT NULL DEFAULT 1,
    CONSTRAINT pkXT56 PRIMARY KEY (Id)
)

Resulta muy complejo establecer para qué es la tabla XT56 y el objetivo funcional de la información que está guardando.    Si después de un trabajo de revisión del sistema y de la base de datos determinamos que es la tabla de productos, bien podríamos hacer lo siguiente para clarificar la extracción de los productos:

CREATE VIEW vProductos
AS
    SELECT A AS Id, B AS Nombre, C AS Precio, D AS Activo
    FROM XT56

Al utilizar esta vista podremos obtener la información de una forma mucho más clara y entendible.

¿Qué pasaría si frecuentemente estamos obteniendo la lista de productos activos cuyo precio sea mayor o igual a un valor X?    Podemos encapsular el código para clarificar la forma en que devuelve la información y también para reutilizar un algoritmo que ya resolvió un problema en específico:

CREATE FUNCTION udfDameProductosPorPrecio(
    @Precio DECIMAL(8, 2)
)
RETURNS TABLE
AS
    RETURN
    SELECT A AS Id, B AS Nombre, C AS Precio
    FROM XT56
    WHERE C = @Precio
    AND D = 1

A través del encapsulamiento podemos proyectar la información de la forma en la que lo necesitamos y también crear objetos que nos permitan reutilizar algo que ya fue resuelto previamente.

Procedimientos almacenados

En un grupo de programación en T-SQL siempre habrá alguien que tenga una forma óptima de codificar y que resuelva los problemas utilizando la menor cantidad de recursos posibles.

Una excelente idea es que esta persona se encargue de resolver los problemas operativos (en la base datos) más complejos y encapsule el algoritmo dentro de un procedimiento almacenado, de manera tal que éste pueda ser utilizado por otros sin tener que estarse quebrando la cabeza en cómo resolver un problema.

Planes de ejecución

Otra gran ventaja de los objetos parametrizados (funciones y procedimientos almacenados) es que permiten que se almacene el plan de ejecución de éstos y así no tenga que recalcularse la siguiente ocasión que se requiera su utilización.

El caché de planes de ejecución es una cosa muy valiosa de SQL Server debido a que reduce el trabajo de uno de los procesos más caros, el cálculo del plan de ejecución.

Capa de seguridad

Habitualmente no es buena idea que se tenga acceso directo de lectura y/o escritura a las tablas de una base de datos.   En este momento se me ocurren por lo menos las siguientes X razones:

  1. El usuario debe poder leer datos de una tabla, pero ella contiene información confidencial a la que no debería tener acceso el usuario.    Es decir, solamente necesita acceso a un subconjunto de columnas.
  2. El usuario debe poder insertar datos pero necesitamos estar absolutamente seguros que la información que ingrese sea consistente con la especificación de requerimientos.
  3. Si el usuario tiene permiso de escritura sobre la tabla, entonces podrá eliminar información y hacerlo de forma equivocada.

Seguramente habrá más de uno que inmediatamente pensó "pero si le puedo dar permisos muy granulares a un usuario de manera tal que solamente pueda leer o escribir ciertas columnas".    Coincido totalmente en que se podría, pero la administración de la seguridad se haría un dolor de cabeza y el dar esos permisos granulares no nos asegura que la información insertada sea adecuada.

Supongamos que tenemos una tabla como la siguiente:

CREATE TABLE Productos(
    Id INT IDENTITY(1, 1),
    Nombre VARCHAR(80) NOT NULL,
    Precio DECIMAL(8, 2) NOT NULL,
    Activo BIT NOT NULL DEFAULT 1,
    CONSTRAINT pkProductos PRIMARY KEY (Id)
)

Si un usuario únicamente tuviera permitido actualizar precios, podríamos crearle un procedimiento como el siguiente:

CREATE PROC pActualizaPrecioProducto(
    @Id INT,
    @Precio DECIMAL(8, 2)
)
AS
BEGIN
    UPDATE Productos
    SET Precio = @Precio
    WHERE Id = @Id
END

Vamos a mejorar este tema, vamos a crear una tabla de histórico de precios guardando auditoría de quién lo ejecutó:

CREATE TABLE ProductosHistoricoPrecio(
    Id INT IDENTITY(1, 1)
    Id_Producto INT NOT NULL,
    PrecioAnterior DECIMAL(8, 2),
    PrecioNuevo DECIMAL(8, 2),
    Usuario SYSNAME,
    Fecha DATETIME2(0) NOT NULL DEFAULT GETDATE(),
    CONSTRAINT pkProductosHistoricoPrecio PRIMARY KEY (Id),
    CONSTRAINT fkProductosHistoricoPrecio_Productos FOREIGN KEY (Id_Producto) REFERENCES Productos (Id)
)

Y nuestro procedimiento almacenado quedaría de la siguiente manera:

CREATE PROC pActualizaPrecioProducto(
    @Id INT,
    @Precio DECIMAL(8, 2)
)
AS
BEGIN
    BEGIN TRAN
    BEGIN TRY
        INSERT INTO HistoricoProductosPrecio (Id_Producto, PrecioAnterior, PrecioNuevo, Usuario)
        SELECT Id, Precio, @Precio, SUSER_NAME()
        FROM Productos
        WHERE Id = @Id

        UPDATE Productos
        SET Precio = @Precio
        WHERE Id = @Id

        COMMIT
    END TRY
    BEGIN CATCH
        ROLLBACK

        DECLARE @Error VARCHAR(4000) = ERROR_MESSAGE()
        THROW 50001, @Error, 1
    END CATCH
END

Si a nuestro usuario le damos permiso de ejecución sobre el procedimiento almacenado:

  1. No será necesario que tenga permiso de escritura sobre HistoricoProductosPrecio ni sobre Productos.
  2. No es necesario que sepa que se está guardando un histórico de precios, tarea que podría olvidar realizar si él actualizara directamente el precio del producto.

Espero te haya resultado de utilidad esta entrada, ¡saludos!