Los plugins tienen acceso a datos sensibles y operan con privilegios elevados. Un plugin mal diseñado puede exponer información confidencial, permitir modificaciones no autorizadas, o crear vulnerabilidades de seguridad. Esta lección cubre principios de seguridad y prácticas de código limpio.
Objetivos de aprendizaje
- Manejar credenciales de forma segura
- Entender y usar impersonación correctamente
- Validar entradas y evitar inyecciones
- Aplicar principios de código limpio
Nunca hardcodees credenciales
Este es el error de seguridad más grave y también el más común. He visto plugins en producción con API keys, contraseñas de bases de datos, y tokens de servicio directamente en el código:
// ABSOLUTAMENTE MAL - Nunca hagas esto
private const string ApiKey = "sk-prod-a1b2c3d4e5f6...";
private const string DbPassword = "SuperSecretPassword123";
Estas credenciales quedan en el assembly compilado, que está almacenado en Dataverse. Cualquiera con acceso al sistema puede extraerlas.
Secure Configuration
La opción más básica es usar Secure Configuration del step:
public class IntegracionExternaPlugin : IPlugin
{
private readonly string _apiKey;
public IntegracionExternaPlugin(string unsecure, string secure)
{
// Secure config está encriptado y solo admins pueden configurarlo
var config = JsonConvert.DeserializeObject<Config>(secure);
_apiKey = config.ApiKey;
}
public void Execute(IServiceProvider serviceProvider)
{
// Usar _apiKey para llamadas externas
}
}
Environment Variables
Dataverse soporta variables de entorno que se gestionan fuera del código:
public string GetEnvironmentVariable(IOrganizationService service, string variableName)
{
var query = new QueryExpression("environmentvariablevalue");
query.ColumnSet = new ColumnSet("value");
query.Criteria.AddCondition("environmentvariabledefinitionid", ConditionOperator.Equal,
GetDefinitionId(service, variableName));
var result = service.RetrieveMultiple(query);
return result.Entities.FirstOrDefault()?.GetAttributeValue<string>("value");
}
Azure Key Vault (mejor opción)
Para proyectos enterprise, usa Azure Key Vault. El plugin llama a una Azure Function que tiene acceso al Key Vault mediante Managed Identity:
// Azure Function que accede a Key Vault
[FunctionName("GetSecret")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req)
{
var client = new SecretClient(
new Uri("https://mi-keyvault.vault.azure.net/"),
new DefaultAzureCredential());
KeyVaultSecret secret = await client.GetSecretAsync("mi-api-key");
return new OkObjectResult(new { value = secret.Value });
}
Impersonación: poder y responsabilidad
Al crear el IOrganizationService, puedes elegir qué usuario usar:
// Servicio con permisos del usuario que ejecuta
var userService = factory.CreateOrganizationService(context.UserId);
// Servicio como SYSTEM - ignora toda la seguridad
var systemService = factory.CreateOrganizationService(null);
El servicio con null tiene poderes de SYSTEM: puede ver y modificar cualquier registro, ignorando roles de seguridad, teams, y field-level security.
Esto es útil para procesos que necesitan acceso completo (sincronizaciones, procesos batch), pero es peligroso si se usa sin cuidado. Reglas:
- Por defecto, usa el servicio del usuario (context.UserId)
- Solo usa null cuando tengas una razón clara y documentada
- Si el plugin hace operaciones como SYSTEM, documéntalo en comentarios
- Nunca expongas datos obtenidos con SYSTEM que el usuario no debería ver
Validación de entradas
Nunca asumas que los datos de InputParameters son válidos o están en el formato esperado:
// MAL: Asume que todo está presente y es correcto
Entity target = (Entity)context.InputParameters["Target"];
string nombre = target["name"].ToString(); // Puede ser null
// BIEN: Valida antes de usar
if (!context.InputParameters.Contains("Target"))
{
trace.Trace("No se encontró Target en InputParameters");
return;
}
Entity target = context.InputParameters["Target"] as Entity;
if (target == null)
{
trace.Trace("Target no es una Entity");
return;
}
string nombre = target.GetAttributeValue<string>("name");
if (string.IsNullOrWhiteSpace(nombre))
{
throw new InvalidPluginExecutionException(
"El nombre es obligatorio y no puede estar vacío.");
}
No traces información sensible
ITracingService es excelente para diagnóstico, pero los logs pueden ser vistos por administradores y quedan almacenados:
// MAL: Expone datos sensibles en logs
trace.Trace($"Password del usuario: {password}");
trace.Trace($"Número de tarjeta: {creditCardNumber}");
trace.Trace($"SSN: {socialSecurityNumber}");
// BIEN: Enmascara información sensible
trace.Trace($"Password: ****");
trace.Trace($"Tarjeta: ****{creditCardNumber.Substring(creditCardNumber.Length - 4)}");
trace.Trace($"Monto procesado: {amount}"); // Datos no sensibles están bien
Principios de código limpio
Código limpio no es solo estética, es seguridad: código confuso esconde bugs.
Nombres descriptivos:
// MAL
var x = CalcularX(y, z);
// BIEN
var descuentoTotal = CalcularDescuentoCliente(cliente, montoCompra);
Funciones pequeñas: Cada función hace una sola cosa. Si necesitas un comentario explicando qué hace una sección, probablemente debería ser su propia función.
Sin código duplicado: Si copias y pegas, extrae a una función.
Constantes con nombre:
// MAL
if (level == 3)
// BIEN
private const int CUSTOMER_LEVEL_GOLD = 3;
if (level == CUSTOMER_LEVEL_GOLD)
Puntos clave
- Nunca hardcodees credenciales en el código
- Usa Secure Configuration, Environment Variables, o Key Vault
- Por defecto usa el servicio del usuario, no SYSTEM
- Valida todas las entradas antes de usarlas
- No traces información sensible
- Escribe código limpio y legible