18 novembro 2022

Integridade de dados e race condition com sinais (signals) no Django

Para quem já trabalhou com Django, sabe dos ótimos recursos disponíveis dentro do framework, além do ilustríssimo e ótimo ORM, uma estrutura de rotas, comunidade enorme e uma vasta experiência para fornecer ferramentas de qualidade para resolução de problemas no desenvolvimento de aplicações web. Mas além desses recursos ele conta também com um o sinais ou signals um disparador de eventos que acontece antes ou depois que uma ação é feita.

Uma das formas mais comuns de usar sinais no Django é quando precisamos executar uma determinada tarefa depois de salvar as informações no banco de dados, claro, partindo de um modelo (model) qualquer. O código seria assim:

from django.db.models.signals import pre_save
from django.core.mail import send_mail
from django.dispatch import receiver

from bank.models import Account

@receiver(post_save, sender=Account)
def send_mail(sender, instance, **kwargs):
    if instance.balance < 0:
        send_mail(
            subject="Saldo baixo!",
            message="Você entrou no cheque especial",
            from_email="admin@domain.tld",
            recipient_list=["user@domain.tld"]
        )

Essa seria uma possível implementação, porém falha, dado que parte da instrução da função send_email depende dos dados do modelo, como é visto logo abaixo da definição dela. O problema é que logo após o sinal ser acionado, o banco de dados ainda está processando o salvamento das informações, por tanto pode ser que no momento da consulta do instance.balance o valor seja o anterior e não 0, como seria esperado, isso é conhecido também como race condition.

A resolução do problema é simples e utilizando os próprios recursos do Django. Dado que as duas atividades estão perdendo a comunicação ao longo de suas execuções e uma depende da outra. É necessário esperar uma ser concluída para em seguida acionar a outra. Para isso é necessário garantir que os dados foram salvos para na sequência fazer o disparo da tarefa (send_mail). Para isso podemos usar o modulo de transaction e a função on_commit que é a responsável por garatir que o sinal será chamado ao fim da "transação" de dados no banco (ou seja depois que ele salvar os dados).

O uso do on_commit para essa finalidade teria uma implementação assim:

[...]

from django.db import transaction

def do_mail(instance):
    if instance.balance < 0:
        send_mail(
        subject="Saldo baixo!",
        message="Você entrou no cheque especial",
        from_email="admin@domain.tld",
        recipient_list=["user@domain.tld"]
    )

@receiver(post_save, sender=Account)
def send_mail(sender, instance, **kwargs):
    transaction.on_commit(lambda: do_email(instance))

Os sinais do Django é uma ferramenta muito útil em muitos casos, mesmo tendo possibilidade de adicionar ferramentas externas para atividades de baixa escala o uso desse recurso se faz necessário, porém é importante entender alguns detalhes para extrair o máximo possível de funcionalidade durante o desenvolvimento de aplicações web.

01 maio 2022

Armadilhas das variáveis de ambiente em testes unitários Python

Atualmente com o grande uso de computação em nuvem, também, é comum nós desenvolvedores criamos ferramentas que precisam de um certo dinamismo em alguns pontos do software, como por exemplo, chaves de acesso, informações do “host” do banco de dados e exposição de porta. Para ter essa fácil troca de valores o caminho mais fácil é usar variáveis de ambiente. Porém em um cenário de testes com Python e UnitTest (pacote encontrado na biblioteca padrão), alguns problemas podem aparecer se não utilizada os recursos corretos para se compor os testes.

Tomemos como exemplo o seguinte código para teste:

import os
from urllib.parse import urlparse
from unittest import TestCase


def resolver_service():
    url = os.getenv('SERVICE_URL', 'https://duck.com/')
    service = urlparse(url)
    return service.scheme, service.netloc


class Test(TestCase):
    def test_service_default(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'https')
        self.assertEqual(name, 'duck.com')

Ao ser executado, ele retorna os valores (padrões) esperado. Porém precisamos alterar esses mesmo valores para validar seu real funcionamento. Uma das formas de inserir valores de variávies de ambiente no Python é injetando diretamento no os.environ com o nome da variável. Extendendo o exemplo com mais 2 unidades de teste.

class Test(TestCase):
    def test_service_default(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'https')
        self.assertEqual(name, 'duck.com')

    def test_service_ftp(self):
        os.environ['SERVICE_URL'] = 'ftp://example.tld'
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'ftp')
        self.assertEqual(name, 'example.tld')

    def test_service_https(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'https')
        self.assertEqual(name, 'duck.com')

Ao executar temos um erro no último teste:

..F
======================================================================
FAIL: test_service_https (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/example_erro_testing.py", line 29, in test_service_https
    self.assertEqual(protocol, 'https')
AssertionError: 'ftp' != 'https'
- ftp
+ https

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

O último teste está falhando, pois está carregando o valor do teste anterior. O esperado era pegar o valor padrão que seria https. Ou seja, o Python não limpa os valores por padrão para a próxima execução. Mas ele tem ferramentas adequadas para esse tipo de trabalho, que seriam os mocks. No caso iremos usar o patch para ser mais exato, patch.dict que após a execução dos testes, restaura os valores padrões do dicionário que é a estrutura que estamos manipulando no Python para ter os valores da variável de ambiente.

class Test(TestCase):
    def test_service_default(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'https')
        self.assertEqual(name, 'duck.com')

    @patch.dict('os.environ', {'SERVICE_URL': 'ftp://example.tld'})
    def test_service_ftp(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'ftp')
        self.assertEqual(name, 'example.tld')

    def test_service_https(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'https')
        self.assertEqual(name, 'duck.com')

Com isso já teremos nossos testes funcionando e escrito da forma correta quando se quer manipular valores de variáveis de ambiente.

09 abril 2022

Reconhecimento facial no laptop para o bem ou para o mal

Com o passar do tempo, tenho me adequado ao modo de vida “preguiçoso” - ou moderno. Acredito que por passar muitas vezes executando as mesmas tarefas que muitas vezes, são tediosas e suscetíveis a erros, chega um momento que a cabeça pede ajuda. Uma dessas tarefas é a questão das senhas. Digitar senhas é um fardo quando se precisa digitar dezenas e dezenas de vezes. Com mais e mais vazamento de dados, a responsabilidade de se manter se seguro aumenta, o que aumenta também o trabalho de manter sua vida digital minimamente segura (se é que é possível), porém se você não está disposto a gastar alguns “milhares” de dólares com licenças de sistemas operacionais fechados e confiar sua segurança (ou seus dados) a outro, o trabalho precisa ser feito na mão. Porém, graças a grande e boa comunidade do Software Livre, o trabalho é feito uma única vez para todo o sempre e com atualização e colabaração de muitos desenvolvedores em volta do mundo.

Uma das tecnologias já em uso pela indústria é o reconhecimento facial, tecnologia essa que tem muitas ressalvas no âmbito comercial[1] e muitas vezes até no pessoal[2]. Porém não deixa de ser uma tecnologia interessante para o grande público, quando pensamos em melhorar nosso dia a dia, como por exemplo, nunca mais digitar senhas ao ligar o computador ou executar uma tarefa com nível de permissão mais alto que exige senhas.

Pois bem, se você usa smartphone (o que é provável), deve saber da existência desse recurso de autenticação fácil em aparelhos Android ou iOS. Assim como nesses sistemas no Linux também existe uma maneira de “habilitar” essa funcionalidade de autenticação. Por meio do Howdy[3], um módulo PAM escrito atualmente em Python[4] é possível ter essa funcionalidade em questão de minutos. O único requisito é ter um computador com webcam.

Com a autenticação facial funcionando, não é mais preciso digitar senhas tanto nas tarefas de linha de comando quanto nas interfaces gráficas. Como é um módulo PAM, é possível destravar quase tudo com o reconhecimento facial, com exceção do molho de chaves “Keyring”. E também é possível especificar aplicações ou comandos disponíveis para requisitar o reconhecimento facial.

As coisas parecem bem mais fluidas, não é necessário digitar dezenas de vezes a senha pois esbarrou numa tecla errada, ou ficar “dançando” os dedos na impressão digital. Claro que não se resume apenas a simplicidade, pois toda facilidade nem sempre vem sem algum peso. Existem algumas ressalvas de segurança em relação a esse tipo de sistema de autenticação, por exemplo, ter apenas esse método de autenticação, ou até mesmo sobre burlar o sistema de autenticação com uma foto de alta qualidade. O próprio Howdy nos permiti 3 níveis de autenticação segura, uma com reconhecimento rápido, outra intermediário e uma terceira que leva muita mais tempo para reconhecer, porém é mais seguro pois evita fraudes com fotos de nossos rostos, por exemplo.

Notas e Referências