-- El freno global de la puerta solo aplica a bloques desconocidos. Si frenara
-- a todos, un atacante dejaria al dueño afuera gastando intentos fallidos.
CREATE OR REPLACE FUNCTION fiscal.entrar_panel(p_usuario text, p_clave text, p_desde text DEFAULT NULL::text)
 RETURNS TABLE(entro boolean, nombre text, espera_seg integer)
 LANGUAGE plpgsql
 SECURITY DEFINER
 SET search_path TO 'fiscal', 'public', 'extensions'
AS $function$
declare
  -- La ventana y los topes en un solo lugar, para no buscarlos por el cuerpo.
  c_ventana    constant interval := interval '15 minutes';
  c_tope_usu   constant int := 8;   -- fallos con el mismo usuario
  c_tope_ip    constant int := 15;  -- fallos desde el mismo bloque de direcciones
  c_tope_usu_c constant int := 30;  -- lo mismo, desde un bloque conocido
  c_tope_ip_c  constant int := 40;
  c_tope_puerta constant int := 40; -- fallos de TODA la puerta en la ventana
  -- Hash de una clave que no existe. Sirve para gastar el mismo tiempo cuando
  -- el usuario no existe, y que el reloj no delate cuales nombres son reales.
  c_senuelo    constant text := '$2a$10$IHrhLAMm.gVpUS2Qwhf4DOze.yYhk8xdN3iOEi8owQJdcSX.kFSjG';

  v_usuario  text := left(lower(btrim(coalesce(p_usuario, ''))), 20);
  v_desde    text := nullif(btrim(coalesce(p_desde, '')), '');
  v_grupo    text := fiscal.grupo_de(p_desde);
  v_op       record;
  v_conocida boolean := false;
  v_n        int;
  v_viejo    timestamptz;
  v_tope_usu int;
  v_tope_ip  int;
  v_distintos int;
begin
  -- OJO con los alias: esta funcion devuelve una columna que se llama `entro`,
  -- asi que sin el `i.` de adelante Postgres no sabe si uno habla de la columna
  -- de la tabla o de lo que va a devolver, y truena con "entro is ambiguous".
  --
  -- Un bloque desde el que ya se entro bien alguna vez en el ultimo mes es "la
  -- casa": se le sube el tope en vez de trancarle la puerta.
  if v_grupo is not null then
    select exists(
      select 1 from fiscal.intentos i
       where i.tipo = 'panel' and i.entro and i.grupo = v_grupo and i.en > now() - interval '30 days'
    ) into v_conocida;
  end if;
  v_tope_usu := case when v_conocida then c_tope_usu_c else c_tope_usu end;
  v_tope_ip  := case when v_conocida then c_tope_ip_c else c_tope_ip end;

  -- FRENO POR USUARIO. Se mira antes de gastar bcrypt: probar claves tiene que
  -- costar barato para nosotros y caro para el que prueba. Este es el que de
  -- verdad protege la clave, porque no depende de por donde venga.
  select count(*), min(t.en) into v_n, v_viejo from (
    select i.en from fiscal.intentos i
     where i.tipo = 'panel' and i.quien = v_usuario and not i.entro and i.en > now() - c_ventana
     order by i.en desc limit v_tope_usu
  ) t;
  if v_n >= v_tope_usu then
    -- No se anota este intento: si se anotara, el que ataca mantendria la
    -- puerta trancada para siempre nada mas insistiendo.
    return query select false, null::text,
      greatest(1, ceil(extract(epoch from (v_viejo + c_ventana - now()))))::int;
    return;
  end if;

  -- FRENO POR BLOQUE DE DIRECCIONES. Contra el que prueba usuarios distintos
  -- desde un lado fijo (probando "admin", "gilberto", "imprenta"...).
  if v_grupo is not null then
    select count(*), min(t.en) into v_n, v_viejo from (
      select i.en from fiscal.intentos i
       where i.tipo = 'panel' and i.grupo = v_grupo and not i.entro and i.en > now() - c_ventana
       order by i.en desc limit v_tope_ip
    ) t;
    if v_n >= v_tope_ip then
      return query select false, null::text,
        greatest(1, ceil(extract(epoch from (v_viejo + c_ventana - now()))))::int;
      return;
    end if;
  end if;

  -- FRENO DE LA PUERTA ENTERA. Los dos frenos de arriba no atajan al que prueba
  -- NOMBRES distintos desde direcciones que cambian solas (probado: 20 usuarios
  -- inventados, ni un corte). Este cuenta los fallos de todos y, cuando llueven,
  -- hace esperar a cualquier intento que falle. Al que sabe su clave no le pasa
  -- nada, porque no falla; y no dice si el usuario existe o no, asi que tampoco
  -- delata nombres.
  -- OJO: solo para quien llega de un lado desconocido. Si frenara a todos,
  -- cualquiera dejaria a Gilberto afuera nada mas gastando intentos fallidos, y
  -- el remedio seria peor que la enfermedad. Desde donde ya se entro bien, este
  -- freno no aplica.
  if not v_conocida then
  select count(*), min(t.en) into v_n, v_viejo from (
    select i.en from fiscal.intentos i
     where i.tipo = 'panel' and not i.entro and i.en > now() - c_ventana
     order by i.en desc limit c_tope_puerta
  ) t;
  if v_n >= c_tope_puerta then
    insert into fiscal.intentos (quien, entro, motivo, desde, tipo, grupo)
    values (v_usuario, false, 'puerta frenada por exceso de intentos', v_desde, 'panel', v_grupo);
    return query select false, null::text,
      greatest(1, ceil(extract(epoch from (v_viejo + c_ventana - now()))))::int;
    return;
  end if;
  end if;

  select * into v_op from fiscal.operadores where usuario = v_usuario and activo;

  if v_op.id is null then
    -- Se gasta el bcrypt igual, contra el señuelo, para que un usuario que no
    -- existe tarde lo mismo que uno que si.
    perform extensions.crypt(coalesce(p_clave, ''), c_senuelo);
    insert into fiscal.intentos (quien, entro, motivo, desde, tipo, grupo)
    values (v_usuario, false, 'usuario que no existe', v_desde, 'panel', v_grupo);
    return query select false, null::text, 0;
    return;
  end if;

  if v_op.clave_hash is distinct from extensions.crypt(coalesce(p_clave, ''), v_op.clave_hash) then
    -- Aca el insert SI queda: esta funcion responde que no, no revienta con
    -- `raise`, y por eso no se revierte la transaccion (ver 20260729060000).
    insert into fiscal.intentos (quien, entro, motivo, desde, tipo, grupo)
    values (v_usuario, false, 'clave incorrecta', v_desde, 'panel', v_grupo);
    return query select false, null::text, 0;
    return;
  end if;

  update fiscal.operadores set ultimo_acceso = now() where id = v_op.id;
  insert into fiscal.intentos (quien, entro, motivo, desde, tipo, grupo)
  values (v_usuario, true, 'entro al panel', v_desde, 'panel', v_grupo);
  return query select true, v_op.nombre, 0;
end;
$function$
;