Executando verificação de segurança...
3

Apenas um usuário por view, como você programaria isso?

python > django

Como você programaria um sistema em que o usuário não possa acessar a mesma view com os mesmos parâmetros já aberta por outro? A parte fácil eu já implementei.

De que forma poderia ser seguro e à prova de erros catastróficos?

Além disso, como você faria para que a atividade do usuário retirasse essa trava? E como garantir que a trava seja liberada quando o usuário sair da página?

Quem conhece o SAP sabe como funciona, e eu gostaria de implementar algo parecido no meu site, mas estou com dificuldades.

Já fiz uma implementação, mas não ficou boa. Quero ajuda para melhorar. Eu uso Django, mas podemos conversar também sobre jQuery, Alpine, HTMX, e qual seria a melhor opção e a melhor maneira de implementar isso.

O sistema precisa ser à prova do tempo. Ou seja, independente de atualizações ou alterações no código, o resultado deve ser sempre o mesmo: bloquear a view e desbloquear quando necessário.

Antes de mostrar o código, o que estou fazendo atualmente é criar um decorador chamado protected_view, que verifica se o usuário está logado com alguns parâmetros e se já há alguém bloqueando aquela view. Caso não haja ninguém, ele insere um novo registro no banco de dados com o path da view e o usuário que a bloqueou. Depois disso, um código JavaScript verifica, a cada 5 segundos, se o usuário ainda está naquela view; caso contrário, o valor é deletado. No entanto, estou achando essa implementação bem ruim.

Meu código

models

from django.db import models


class LockedView(models.Model):
    view_informations = models.JSONField()
    locked_by = models.ForeignKey(
        "accounts.Account", on_delete=models.SET_NULL, null=True
    )

    is_open = models.BooleanField(default=False)

    def __str__(self):
        return "This view are open by %s" % self.locked_by.email

view

#Vou mudar esse generic.views de pasta
from apps.accounts.utils.generic.views import UpdateView 
from django.contrib.auth.mixins import LoginRequiredMixin

class ProfileUpdateTestView(LoginRequiredMixin, UpdateView):
    model = Profile
    fields = [
        "name",
        "avatar",
        "phone",
        "house_address_street",
        "house_address_number",
        "buss_stop_address_street",
        "buss_stop_address_number",
        "city",
        "turn",
        "role",
    ]
    template_name = "profile.html"
    success_url = "/profile"
    login_url = "/login"
    app_name = "accounts" # algo que criei para utilizar m2m , fk no frontend
    protected = True

    def get_object(self, queryset=None):
        return self.model.objects.get(pk=self.kwargs.get("pk"))

    def form_valid(self, form):
        messages.success(self.request, "Profile updated")
        return super().form_valid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["instance"] = self.get_object()
        context["account"] = self.get_object().user
        return context

mixin

from apps.core.decorators import protected_view

class ProtectionViewMixin(View):
    @protected_view
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)

decorator

from django.contrib import messages
from django.db.transaction import atomic
from django.http import HttpRequest
from django.shortcuts import redirect

from apps.core.models import LockedView


@atomic
def protected_view(func):
    def wrapper(cls, request: HttpRequest, *args, **kwargs):
        path = request.get_full_path()

        if not hasattr(cls, "protected") or not cls.protected:
            return func(cls, request, *args, **kwargs)

        if not request.user.is_authenticated:
            messages.error(request, "You need to be logged in to access this view")
            return redirect("/login")

        is_open = LockedView.objects.filter(view_informations__paths__icontains=path)
        if is_open.exists():
            locked_view = is_open.first()
            if locked_view.is_open and request.user != locked_view.locked_by:
                messages.error(request, f"This view is open by {locked_view.locked_by}")
                return redirect("/")
            elif locked_view.is_open and request.user == locked_view.locked_by:
                return func(cls, request, *args, **kwargs)
        else:
            LockedView.objects.create(
                view_informations=dict(paths=[path]),
                locked_by=request.user,
                is_open=True,
            )

        return func(cls, request, *args, **kwargs)

    return wrapper

js

function getCSRFToken() {
  const CSRFToken = $("body").attr('hx-headers');
  const parsedToken = CSRFToken ? JSON.parse(CSRFToken) : null;
  return parsedToken ? parsedToken['X-CSRFToken'] : null;
}

async function sendRequest(url, data) {
  const CSRFToken = getCSRFToken();
  if (!CSRFToken) {
    return;
  }

  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': CSRFToken
      },
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      throw new Error(`Error on request: ${response.statusText}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Error on request', error);
  }
}

async function verifyPath() {
  const pathAtual = window.location.pathname;
  const user = $("body").attr('data-user');

  const response = await sendRequest('/core/check-locked/', {
    path: pathAtual,
    user: user
  });

  if (response && !response.locked) {
    await sendRequest('/core/unlock-view/', { user: user });
  }
}

setInterval(verifyPath, 5000);

is_locked and unlock_view functions

import json

from django.http import JsonResponse

from apps.core.models import LockedView


def is_locked(request, *args, **kwargs):
    if request.content_type == "application/json":
        data = json.loads(request.body.decode("utf-8"))

        user = data.get("user")
        path = data.get("path")
        is_locked_by = LockedView.objects.filter(
            locked_by__email__iexact=user, view_informations__paths__contains=path
        ).exists()

        if is_locked_by:
            return JsonResponse({"locked": True})
        else:
            return JsonResponse({"locked": False})
    else:
        return JsonResponse({"error": "Invalid content type"}, status=400)


def unlock_view(request, *args, **kwargs):
    if request.content_type == "application/json":
        data = json.loads(request.body.decode("utf-8"))

        user = data.get("user")
        locked_by = LockedView.objects.filter(locked_by__email__iexact=user)

        if locked_by.exists():
            locked_by.delete()
        return JsonResponse({}, status=200)
    else:
        return JsonResponse({"error": "Invalid content type"}, status=400)
Carregando publicação patrocinada...
3

Para evitar acesso simultâneo à mesma view com os mesmos parâmetros, uma solução eficaz é combinar uma tabela de "checkout" no banco de dados com um mecanismo de "heartbeat" via WebSockets. A tabela de chefkoit registra qual usuário está visualizando qual view com quais parâmetros, impedindo novos acessos. O heartbeat, enviado periodicamente pelo cliente, mantém o checkout ativo enquanto o usuário estiver na página; caso o heartbeat cesse (por fechamento da aba ou perda de conexão), o checkout é removido, liberando o acesso para outros usuários.

1

Eu havia pensando em colocar um websock, esse heartbet é tipo um pulso né? fica enviando pulsos e recebendo pra saber o que deve fazer. Entendi.

Preciso pensar mais na lógica desse sistema pra então implementar, o Tryverdf deu uma idéia parecida com o que eu já estava fazendo pelo que eu entendi só que com JWT tokens.

Seria uma boa e mais seguro talvez, mas preciso sentar e verificar a possibilidade de cada um.

1

Como um bom SAPeiro sei bem do que você está falando.
Não sei se você conhece por baixo dos panos esse processo no SAP, mas ele tem uma função de ENQUEUE e DEQUEUE que basicamente faz o lock e unlock de uma chave que você passa, podendo ser um número de pedido, funcionário e etc. e a tela sempre tenta fazer um ENQUEUE mas se alguém fez antes ele devolve que está bloqueado.
Com websocket isso fincionará bem e uma tabela auxiliar para armazenar sase lock.

1

O que você quer implementar, implica em uma abordagem de controle de sessão. Há algumas maneiras de se fazer isso, e boas ideias já foram apresentadas aqui.
Pensando em algo simples para validar a ideia, Eu implementaria usando um banco de chave-valor para salvar a sessão, sendo o endereço da view a chave. Você insere essa verificação em um middleware e toda request vai passar por essa verificação. Quando uma nova sessão for iniciada, você salva aquela view no banco chave-valor e pronto.
Aí você pode criar mecanismos de invalidação para garantir desbloqueio de sessão em caso de inatividade, e por aí vai.
Bom trabalho pra ti :)

Um forte abraço!

Sua implementação vai por esse caminho. A ideia de um banco de chave-valor vai na direção de ser mais rápido em leitura. Eu sinceramente, não ficaria preocupado em tentar outra abordagem, sem primeiro testar se o que você já fez, vai funcionar, ou não nos cenários de uso da sua aplicação.

1

Sua solução é bem inteligente, o unico problema do JS estar em looping de verificação é realmente q o JS do lado do cliente é uma paulada no processamento, mas se fizer como o clacerda sinalizou, e inserir um websocket, problema resolvido

Gostaria de entender melhor o caso de uso, q talvez tenha algum outro tipo de solução possivel para esse problema

1

Eu entendi o que quer fazer e acho a resposta do clacerda válida.
No entanto, tenho algumas dúvidas: a view necessariamente precisa ser bloqueada para outro usuário?

O que quero dizer é: pense em um site de reservas de assento de cinema, por exemplo. O usuário tem uma janela de tempo até efetuar o pagamento. Caso o tempo expire, ele deve reiniciar o processo e, caso o assento esteja indisponível no momento do pagamento, uma mensagem de erro é exibida e ele também deve reiniciar o processo (agora, com o estado do assento escolhido anteriormente atualizado, ou seja, indisponível).

Esse cenário seria possível em sua aplicação?

2

Não entrária nesse caso , pois o que eu quero fazer, é inutilizar aquela view para outro usuário enquanto um está editando. Isso serviria para alteração de dados importantes, evitando troca simultâneas.

0

Talvez não faça sentido porque você citou esse tal de SAP que não conheço e não sei como funciona 😅
Enfim, desculpa se mais atrapalhei do que ajudei

1

cria um sistema de Permissão x perfil X usuário com JWT.

a cada view protegida você vai ter um método que cria uma permissão dinâmica para esse view que vai ser criada ao usuário acessar essa rota, após a permissão criada você atribui ela a um perfil e esse perfil ao usuário. se algum outro usuário tentar entrar na mesma rota ele vai ser bloqueado pois ele não tem a permissão necessária. utilize JWT para transformar a permissão em um token e atribuir esse token a sessão do usuário uma vez o usuário saindo da sessão a rota vai estar livre para outro poder utilizar.

fiz algo parecido para um sistema que lidava com dados sensíveis e as vezes eles precisavam ser editados diversas vezes então para garantir que as pessoas pudessem estar lidando com a informações atualizadas adicionei um sistema de fila para que cada um dos requests só fosse direcionado a rota quando realmente tivesse sido liberada pelo usuário que a estava utilizando. enfim boa sorte aí mano qq coisa se puder ajudar mais entra em contato

1

Hm vou tirar um dias pra pensar como vou implementar essa lógica, também vou tentar a lógica do websocket do clacerda, vou criar duas branches diferentes para então tentar.

Mas como você lidaria com a inatividade e a saída do usuário daquela tela? Teria uma amostra de como você fez? Eu entendi, o que você fez, criou um modelo DynamicJwt, que contem um user_id que no caso do django é só criar uma fk, e colocou um charfiel para o código JWT. Cada view tem seu próprio JWT sendo gerado constante? ou ele é fixo? e ai quando o usuário acessa , a tebela DynamicJwt é preenchida com o usuário, e quando ela está diferente de null ou blank, ele pode ser acessado?

É parecido com o que fiz, porém eu coloquei os paths em um jsonfields dentro de um array
e quando eu vou procurar eu procuro como view_informations__paths__contains=path eu gostei e realmente funciona, o porém vem da inatividade e da saída do usuário da view, que eu tentie resolver porcamente com um js, nem sei se isso é seguro.

{
    "paths": ["profile/1/", "profile/2/"]
}
1

como você vai definir um JWT você pode definir nele a duração da sessão. e com JS consultar enviar uma requisição para verificar se o usuário ainda pode visualizar a página. assim caso ele passe do tempo limite e ele perde o token e a página da um get nela mesmo e como ele não vai ter a permissão dessa vez ele não vai conseguir acessar a view.

tenho um exemplo bem básico disso que posso te encaminhar fiz em flask e tbm um em django