The sidebar "Fiscal" entry now goes to /sso/fiscal. For a logged-in, active employee the panel issues a JWT signed with Ed25519 (the private key exists only here; GTI Fiscal only holds the public key): iss/aud fixed, sub = funcionario:<ID> (LOGIN is editable), 60 s lifetime, unique jti that GTI Fiscal records to reject replays. No password, hash or panel cookie leaves the panel. The assertion is delivered in an auto-submitted POST, never in the URL, so it stays out of proxy logs, browser history and Referer. That page carries its own CSP allowing form-action only to the configured callback origin; the security-headers middleware now keeps a route-defined CSP. Without a session the employee goes through the normal login and comes back; plain HTTP is redirected to the public HTTPS address; `next` only accepts a relative path; the .env bootstrap account (no stable ID) is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73 lines
3.3 KiB
Python
73 lines
3.3 KiB
Python
"""
|
|
GET /sso/fiscal — ponto de entrada do item "Fiscal" da sidebar. Com o
|
|
funcionário logado e ativo, emite a asserção (app/sso.py) e a entrega ao
|
|
callback do Fiscal num POST auto-submetido; sem sessão, manda para o login
|
|
normal e volta para cá depois.
|
|
|
|
Só roda por HTTPS (acesso pelo domínio público, atrás do NPM): por HTTP
|
|
redireciona para SSO_GERENCIADOR_URL_PUBLICA. A conta de bootstrap do .env
|
|
não tem linha em FUNCIONARIO_PAINEL, portanto nenhum ID estável para virar
|
|
identidade no Fiscal — é recusada (o Fiscal tem o próprio admin de emergência).
|
|
"""
|
|
import os
|
|
from urllib.parse import urlencode
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
from .. import sso
|
|
from ..auth import NaoAutenticado, exigir_login
|
|
from ..db import funcionarios as db_funcionarios
|
|
from ..db import log as db_log
|
|
|
|
router = APIRouter()
|
|
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
|
|
def _pagina_erro(mensagem: str, status_code: int) -> HTMLResponse:
|
|
return HTMLResponse(
|
|
f'<!doctype html><meta charset="utf-8"><title>GTI Fiscal</title>'
|
|
f'<p style="font-family:system-ui;padding:24px">{mensagem} <a href="/">Voltar ao painel</a></p>',
|
|
status_code=status_code,
|
|
)
|
|
|
|
|
|
@router.get("/sso/fiscal")
|
|
def iniciar_sso_fiscal(request: Request, next: str | None = None):
|
|
destino = sso.proximo_seguro(next)
|
|
if not sso.configurado():
|
|
return _pagina_erro("O acesso ao GTI Fiscal não está configurado neste painel.", 503)
|
|
|
|
if request.url.scheme != "https":
|
|
publica = os.environ.get("SSO_GERENCIADOR_URL_PUBLICA", "").rstrip("/")
|
|
if publica.startswith("https://"):
|
|
return RedirectResponse(f"{publica}/sso/fiscal?{urlencode({'next': destino})}", status_code=303)
|
|
return _pagina_erro("O acesso ao GTI Fiscal só funciona pelo endereço HTTPS do painel.", 400)
|
|
|
|
try:
|
|
usuario = exigir_login(request)
|
|
except NaoAutenticado:
|
|
volta = f"/sso/fiscal?{urlencode({'next': destino})}"
|
|
return RedirectResponse(f"/login?{urlencode({'next': volta})}", status_code=303)
|
|
|
|
funcionario = db_funcionarios.obter_por_login(usuario.login)
|
|
if funcionario is None:
|
|
db_log.registrar(str(usuario), "sso_fiscal", "funcionario", usuario.login, False, "conta de bootstrap não usa SSO")
|
|
return _pagina_erro("A conta de bootstrap não acessa o GTI Fiscal — entre com seu usuário de funcionário.", 403)
|
|
|
|
assercao = sso.emitir_assercao(funcionario)
|
|
db_log.registrar(str(usuario), "sso_fiscal", "funcionario", str(funcionario["ID"]), True)
|
|
resposta = templates.TemplateResponse(
|
|
request, "sso_fiscal.html", {"acao": sso.url_callback(), "assercao": assercao, "proximo": destino}
|
|
)
|
|
# A CSP padrão (main.py) só permite form-action 'self'; esta página precisa
|
|
# postar exatamente para a origem do Fiscal e para nenhum outro lugar.
|
|
resposta.headers["Content-Security-Policy"] = (
|
|
"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; "
|
|
f"form-action {sso.origem_callback()}; frame-ancestors 'none'; base-uri 'none'"
|
|
)
|
|
resposta.headers["Cache-Control"] = "no-store"
|
|
resposta.headers["Referrer-Policy"] = "no-referrer"
|
|
return resposta
|