Azure Cosmos DB y la gestión de permisos

En esta publicación, exploraremos Azure Cosmos DB y la gestión de permisos. Como un servicio de base de datos NoSQL, Azure Cosmos DB ofrece diversas opciones para garantizar la protección de los datos. Entre estas opciones se encuentran las llaves, Control de acceso basado en roles (RBAC) y tokens de recursos, que son los principales mecanismos que nos permiten controlar el acceso a los datos de manera efectiva.

Los tokens de recursos son un modelo de permisos específico de Azure Cosmos DB, permite otorgar acceso granular a los datos a usuarios y aplicaciones. Este enfoque es particularmente útil cuando se desea proporcionar acceso a clientes no confiables, ya que elimina la necesidad de compartir claves principales, entre las ventajas de utilizar tokens de recursos se encuentran la facilidad de administración, la posibilidad de revocar tokens individualmente y la granularidad del control de acceso.

Estructura de los permisos en Azure Cosmos DB

La estructura de permisos en Azure Cosmos DB está diseñada de manera jerárquica para facilitar la gestión. La jerarquía comprende los siguientes niveles: bases de datos, usuarios, permisos.

Cada nivel se identifica por un nombre único, que se utiliza para construir URI de solicitudes HTTP para acceder a los recursos. Por ejemplo:

https://{databaseaccount}.documents.azure.com/dbs/{db-id}/users/{user-name}/permissions/{permission-name}

Esta estructura de permisos se refleja en el SDK V3 para .NET, lo que facilita la creación, asignación y administración de permisos mediante código C#. La comprensión de la estructura de permisos es fundamental para entender como es la jerarquía de las clases que componen el SDK para .NET.

Creando aplicaciones con Azure Cosmos DB y protección de datos

Simulación de una aplicación real

En este escenario, vamos a simular una aplicación (en nuestro caso, una aplicación de consola) que consta de dos flujos o procesos distintos. El primero estará dedicado a la gestión de permisos, que incluirá la creación de usuarios y la asignación de permisos. El segundo flujo será responsable de consultar los datos, limitados por los permisos establecidos anteriormente

Un escenario más realista implicaría tener una WebAPI que se encargue de gestionar los permisos y genere los tokens que devolverá al cliente para que pueda realizar sus operaciones posteriores. Con este diseño, garantizamos que las claves de acceso están en nuestra aplicación de confianza y que el cliente solo tendrá un token que limitará el acceso a la información según sus permisos. En nuestro caso, hemos creado una aplicación muy simple, ya que nos enfocaremos únicamente en la gestión de permisos.

Hemos configurado una instancia de Azure Cosmos DB en la que hemos creado una base de datos llamada «database1«. Dentro de esta base de datos, hemos definido una colección denominada «products«, que utiliza el campo «clientId» como clave de partición. Esto nos permite organizar eficientemente los permisos según el cliente al que pertenecen.

Proceso de gestión de permisos

En este proceso, vamos a gestionar la creación de usuarios y permisos en Azure Cosmos DB. A continuación, vamos escribir el código necesario paso a paso y explicaremos su funcionamiento.

Creando la instancia de CosmosClient

Lo primero que vamos hacer es crear una instancia de CosmosClient que es la que usaremos para conectarnos a nuestra instancia de Azure Cosmos DB.

string key = "{PRIMARY_KEY}";
string accountEndpoint = "https://{AZURE_COSMOS_DB_NAME}-cosomodb.documents.azure.com:443";
using CosmosClient cosmosClient = new CosmosClient(accountEndpoint , key);

Inicialmente, establecemos una instancia de CosmosClient utilizando el accountEndpoint y la key, que pueden ser obtenidos desde el portal de Azure, accediendo al recurso correspondiente.

Azure Cosmos DB
Azure Cosmos DB

¿Por qué necesitamos utilizar la key en este paso inicial? Esto se debe a que nuestra primera tarea es configurar los permisos y usuarios, lo cual requiere ciertos privilegios específicos.

Ahora lo que debemos hacer es acceder a la base datos.

Database database = cosmosClient.GetDatabase(databaseId);

Hasta este punto, si has trabajado con Azure Cosmos DB en el pasado, es probable que estés familiarizado con este proceso, ya que es fundamental para iniciar las operaciones CRUD en la base de datos seleccionada. Ahora que nos conectamos a la base de datos, debemos crear el usuario y los permisos.

NOTA: En este ejemplo se esta usando el nuget Microsoft.Azure.Cosmos que nos permite trabajar con la versión 3 del SDK para .NET de Azure Cosmos DB.

Creando el usuario y el permiso

Ahora, procedemos con la creación del usuario. Supongamos que el nombre del usuario es «client1».

User client1= await database.CreateUserAsync("client1");

En el código anterior, estamos creando un usuario dentro de la base de datos de Azure Cosmos DB.

Para establecer el permiso, necesitamos obtener primero la instancia del contenedor asociado a la colección a la cual aplicaremos el permiso. En nuestro caso, esta colección se denomina «products«.

Container container = database.GetContainer("products");

Ahora creamos el permiso.

string permissionId = "client1_product";
await client1.CreatePermissionAsync(
        new PermissionProperties(
            id: permissionId,
            permissionMode: PermissionMode.Read,
            container: container,
            resourcePartitionKey: new PartitionKey("client1")));

Lo primero es generar un permissionId, este valor debe ser único, es el nombre con el que se va identificar el permiso, para simplificar solo concatenamos el nombre del usuario y nombre del contenedor.

En permissionMode se debe especificar que tipo de permiso es, solo hay 2 disponibles, READ y ALL, como deseamos que solo sea de lectura especificamos READ.

En container especificamos la instancia del contenedor que obtuvimos previamente a la cual queremos aplicar el permiso.

Por ultimo, tenemos resourcePartitionKey, aquí debemos indicar el valor del PartitionKey, como dijimos previamente, se esta segmentando por clientId, entonces para nuestro caso es «client1». El usuario y la partición no se requieren que sean igual, simplemente por comodidad se esta haciendo de esta manera.

Listo, con esto hemos creado un permiso con el identificador «client1_product» que permite a un usuario «client1» acceso de lectura al contenedor «product«.

Si ejecutas el código anterior la primera vez funcionará sin problema, pero las ejecuciones posteriores probablemente no. Esto se debe que no existe CreateOrUpdateUser ni CreateOrUpdatePermission, los métodos que usamos asumen que el usuario y permiso no están creados, si alguno ya existe previamente se va generar un error de duplicado, por eso se debe validar previamente si existen, lo cual nos lleva al siguiente problema, los métodos GetUser y GetPermission son los métodos que se usan respectivamente para obtener un usuario/permiso creado previamente, pero en caso que no exista, se obtendrá un error de elemento no encontrado.

Ahora teniendo en cuenta lo anterior, una solución es trabajar con try/catch y manejar el tipo de error para hacer una implementación más robusta. A continuación vamos a reescribir el código anterior para hacer las mejoras previamente mencionadas.

using System.Net;
using Microsoft.Azure.Cosmos;

string userId = "client1";
string containerId = "products";
string databaseId = "database1";
string partitionKeyValue = "client1";

string key = "{PRIMARY_KEY}";
string accountEndpoint = new("https://{AZURE_COSMOS_DB_NAME}-cosomodb.documents.azure.com:443/");

CosmosClient cosmosClient = new CosmosClient(accountEndpoint, key);
Database database = cosmosClient.GetDatabase(databaseId);
Container container = database.GetContainer(containerId);

User user;

try
{
    user = await database.GetUser(userId).ReadAsync();
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    user = await database.CreateUserAsync(userId);
}


string permissionId = $"{user}_{containerId}";
try
{
    await user.GetPermission(permissionId).ReadAsync();
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    await user.CreatePermissionAsync(
        new PermissionProperties(
            id: $"{user}_{containerId}",
            permissionMode: PermissionMode.Read,
            container: container,
            resourcePartitionKey: new PartitionKey(partitionKeyValue)));
}

El código anterior solo es un ejemplo de una posible implementación, no es necesariamente la más optima. En una implementación real, probablemente obtendrías un clientId desde los Claims del usuario y ese seria el valor del userId para que usaríamos.

Proceso de acceso a datos usando token de recursos en Azure Cosmos DB

Ahora vamos a crear el proceso para consultar los datos usando el token de recurso para hacer las operaciones y garantizar la protección de datos.

El proceso es muy simple, para limitar las operaciones a los permisos otorgados solo debemos crear el una instancia de cliente de cosmos usando token de permiso en vez de llave primaria, algo similar a lo siguiente.

string token= "{TOKEN_PERMISSION}";
string accountEndpoint = "https://{AZURE_COSMOS_DB_NAME}-cosomodb.documents.azure.com:443";
using CosmosClient cosmosClient = new CosmosClient(accountEndpoint , token);

Con esto, garantizamos que se aplique la protección de datos según hemos configurado en los permisos. Fácil. ¿Verdad? bueno realmente no, porque te estarás preguntando ¿Como obtengo el token? bueno, este es el primer aspecto que no me gusta, debemos crear previamente una instancia de cosmos usando la llave primaria para poder obtener el permiso y posteriormente el token.

using Microsoft.Azure.Cosmos;

string userId = "client1";
string databaseId = "database1";
string containerId = "products";
string key = "{PRIMARY_KEY}";
string accountEndpoint = new("https://{AZURE_COSMOS_DB_NAME}-cosomodb.documents.azure.com:443/");

// Obtenemos el token
CosmosClient cosmosClientAdmin = new CosmosClient(accountEndpoint, key);
Database database = cosmosClientAdmin.GetDatabase(databaseId);

// Como te comente antes los permisos están asociados a un usuario, por eso accedemos antes a este recurso.
User user = database.GetUser(userId);

// Obtenemos permiso, debemos buscar usando el "permissionId" creado previamente.
PermissionProperties permission = await user.GetPermission($"{user}_{containerId}").ReadAsync();

// Creamos una instancia de Cosmos limitada por permisos.
CosmosClient cosmosClientUser1 = new CosmosClient(accountEndpoint, permission.Token);

Un aspecto adicional que tampoco me agrada es que no se pueden crear una instancia CosmosClient usando mas de un token, lo que significa que por ejemplo en un caso de negocio que requiere acceder 2 contenedores a vez, deberás crear una instancia adicional de CosmosClient con el token asociado al otro permiso.

La implementación anterior no es la más optima, porque deberíamos considerar los casos que un permiso no exista previamente.

Conclusión

La protección de datos usando token de recurso es un mecanismo útil, debemos evaluar los casos de uso en donde podemos usarlo de manera eficiente, también debemos considerar algunas características adicionales como el uso de caché para reutilizar las instancias de CosmosClient. Y como todo en este mundo, nunca existe la solución perfecta universal, debemos evaluar nuestro requerimientos y tomar la decisiones si esta funcionalidad nos ayuda a solventar un problema a nuestro cliente.

Si quieres seguir aprendiendo sobre estos temas te invito a ver mis otras publicaciones.

Referencias

Microsoft Learn