Perfil e status ativo vinham so do cookie assinado (session.py), entao um funcionario desativado (ATIVO=0) ou rebaixado (ADMIN->PADRAO) continuava com acesso total ate o cookie expirar (ate 12h) - inclusive a /funcionarios se era admin. exigir_login (app/auth.py) agora reconfirma contra o banco a cada requisicao quando o login nao e o da conta de bootstrap (ADMIN_USER do .env, que nao tem linha em FUNCIONARIO_PAINEL e continua sempre admin via variavel de ambiente): busca db_funcionarios.obter_por_login(login); se nao existir mais ou ATIVO != 1, levanta NaoAutenticado (forca novo login); caso contrario, monta o UsuarioLogado com o PERFIL atual do banco, nao o do cookie - rebaixamento tambem passa a ter efeito imediato. Testes de regressao em tests/test_auth.py: - test_exigir_login_autentica_funcionario_ativo - test_exigir_login_revoga_sessao_ao_desativar_funcionario - test_exigir_admin_usa_perfil_atual_do_banco_nao_do_cookie - test_exigir_login_conta_bootstrap_nao_consulta_banco Ajustou tambem test_exigir_admin_rejeita_padrao (usava um login fake sem linha em FUNCIONARIO_PAINEL, que agora conflita com a checagem nova - passou a usar a fixture funcionario_teste). Suite inteira roda 147 passed apos a mudanca. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
98 lines
3.8 KiB
Python
98 lines
3.8 KiB
Python
"""
|
|
Login por sessão. Duas fontes de credencial, checadas nessa ordem em
|
|
autenticar_credenciais() (chamada tanto por POST /login quanto,
|
|
indiretamente, por quem valida o cookie de sessão):
|
|
|
|
1. ADMIN_USER/ADMIN_PASSWORD (variáveis de ambiente) — conta única de
|
|
bootstrap, útil para o primeiro acesso (cadastrar o primeiro
|
|
funcionário em /funcionarios) e como plano B se a tabela de
|
|
funcionários ficar vazia/inacessível. Continua exigindo as duas
|
|
variáveis preenchidas (nunca autentica credencial vazia). Sempre
|
|
perfil ADMIN, não depende do banco.
|
|
2. FUNCIONARIO_PAINEL (app/db/funcionarios.py) — conta individual por
|
|
funcionário, com senha em hash (app/senha.py). Só autentica se ATIVO=1.
|
|
Perfil vem da coluna PERFIL ("ADMIN" ou "PADRAO").
|
|
|
|
A credencial em si só é usada em POST /login (app/routers/auth.py), que
|
|
troca por um cookie de sessão assinado (app/session.py) — as rotas do
|
|
painel não recebem usuário/senha a cada requisição, só o cookie.
|
|
"""
|
|
import os
|
|
import secrets
|
|
from dataclasses import dataclass
|
|
|
|
from fastapi import Depends, HTTPException, Request, status
|
|
|
|
from . import session
|
|
from .db import funcionarios as db_funcionarios
|
|
from .senha import verificar_senha
|
|
|
|
|
|
@dataclass
|
|
class UsuarioLogado:
|
|
login: str
|
|
nome: str
|
|
perfil: str # "ADMIN" ou "PADRAO"
|
|
|
|
def __str__(self) -> str:
|
|
return self.nome
|
|
|
|
@property
|
|
def eh_admin(self) -> bool:
|
|
return self.perfil == "ADMIN"
|
|
|
|
|
|
class NaoAutenticado(Exception):
|
|
def __init__(self, caminho_original: str = "/"):
|
|
self.caminho_original = caminho_original
|
|
|
|
|
|
def autenticar_credenciais(usuario: str, senha: str) -> UsuarioLogado | None:
|
|
usuario_admin = os.environ.get("ADMIN_USER", "")
|
|
senha_admin = os.environ.get("ADMIN_PASSWORD", "")
|
|
|
|
if usuario_admin and senha_admin:
|
|
usuario_ok = secrets.compare_digest(usuario, usuario_admin)
|
|
senha_ok = secrets.compare_digest(senha, senha_admin)
|
|
if usuario_ok and senha_ok:
|
|
return UsuarioLogado(login=usuario_admin, nome=usuario_admin, perfil="ADMIN")
|
|
|
|
funcionario = db_funcionarios.obter_por_login(usuario)
|
|
if funcionario and funcionario["ATIVO"] and verificar_senha(senha, funcionario["SENHA_HASH"]):
|
|
return UsuarioLogado(login=funcionario["LOGIN"], nome=funcionario["NOME"], perfil=funcionario["PERFIL"])
|
|
|
|
return None
|
|
|
|
|
|
def exigir_login(request: Request) -> UsuarioLogado:
|
|
"""
|
|
Decodifica o cookie de sessão e, para logins que existem em
|
|
FUNCIONARIO_PAINEL, reconfirma ATIVO/PERFIL contra o banco a cada
|
|
requisição — o cookie assinado tem validade de até 12h e não é
|
|
revogado, então sem essa checagem desativar ou rebaixar um funcionário
|
|
não teria efeito imediato (continuaria com acesso até o cookie
|
|
expirar). A conta de bootstrap (ADMIN_USER do .env) não tem linha em
|
|
FUNCIONARIO_PAINEL e continua sempre admin via variável de ambiente,
|
|
sem consulta ao banco.
|
|
"""
|
|
token = request.cookies.get(session.SESSION_COOKIE_NAME)
|
|
dados = session.ler_token(token)
|
|
if dados is None:
|
|
raise NaoAutenticado(caminho_original=request.url.path)
|
|
|
|
usuario_admin = os.environ.get("ADMIN_USER", "")
|
|
if usuario_admin and dados["login"] == usuario_admin:
|
|
return UsuarioLogado(login=dados["login"], nome=dados["nome"], perfil="ADMIN")
|
|
|
|
funcionario = db_funcionarios.obter_por_login(dados["login"])
|
|
if funcionario is None or not funcionario["ATIVO"]:
|
|
raise NaoAutenticado(caminho_original=request.url.path)
|
|
|
|
return UsuarioLogado(login=funcionario["LOGIN"], nome=funcionario["NOME"], perfil=funcionario["PERFIL"])
|
|
|
|
|
|
def exigir_admin(usuario: UsuarioLogado = Depends(exigir_login)) -> UsuarioLogado:
|
|
if not usuario.eh_admin:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Só administradores acessam esta página.")
|
|
return usuario
|