
    h$                       d Z ddlmZ ddlZddlZddlmZ ddlmZ ddZ	ddZ
dd	Z ed
d      Z edd      Z edd      Z ej                  dd      dk(  Zd ZddZddZd dZd!dZd"dZd#dZd$dZd%dZd%dZy)&a  
rate_limit.py

Purpose:
  Implements robust, database-backed login rate limiting and lockout to
  mitigate credential stuffing and brute-force attacks. Tracks failed login
  attempts per (username, ip) pair with a sliding window and enforces a
  temporary lock after too many failures.

How it works:
  - On each login POST, `is_login_allowed(username, ip)` is called.
    - If an active lock exists (locked_until > now), it returns (False, retry_s).
  - On password failure, `record_login_failure(username, ip)`:
    - Resets the window if last_failed_at is outside the window.
    - Increments fail_count and, when threshold is reached, sets locked_until.
  - On success, `record_login_success(username, ip)` resets counters.

Defense-in-depth:
  - Also enforces an IP-level limiter (`is_ip_allowed(ip)`) to avoid trivial
    bypass by switching usernames. Both username+ip and ip-only checks must pass.

Configuration (env vars):
  - LOGIN_MAX_FAILS: max failures in the window before lock (default: 5)
  - LOGIN_WINDOW_SECONDS: sliding window duration in seconds (default: 900)
  - LOGIN_LOCK_SECONDS: lock duration after threshold exceeded (default: 900)
    )annotationsN)Tuple   )get_dbc                 <    t        t        j                               S N)inttime     IC:\Users\algun\Documents\ceba web\Ceba - Github\app_modules\rate_limit.py_now_tsr   %   s    tyy{r   c                @    | syh d}| |v ry| j                  d      ryy)zMCheck if the IP address is localhost and should be exempt from rate limiting.F>   ::10.0.0.0	127.0.0.1	localhostTz127.)
startswith)iplocalhost_ipss     r   _is_localhost_ipr   )   s0    M 
] 
}}Vr   c                v    	 t        t        j                  | t        |                  S # t        $ r |cY S w xY wr   )r	   osgetenvstr	Exception)namedefaults     r   _cfg_intr   @   s5    299T3w<011 s   '* 88LOGIN_MAX_FAILS
   LOGIN_WINDOW_SECONDSi,  LOGIN_LOCK_SECONDSLOGIN_LOCK_IP_ON_ACCOUNT_LOCK1c                     t               } | j                  d       | j                          | j                  d       | j                          y )Na{  
        CREATE TABLE IF NOT EXISTS login_attempts (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            username TEXT NOT NULL,
            ip TEXT NOT NULL,
            fail_count INTEGER NOT NULL DEFAULT 0,
            last_failed_at INTEGER NOT NULL DEFAULT 0,
            locked_until INTEGER NOT NULL DEFAULT 0,
            UNIQUE(username, ip)
        )
        a	  
        CREATE TABLE IF NOT EXISTS login_ip_attempts (
            ip TEXT PRIMARY KEY,
            fail_count INTEGER NOT NULL DEFAULT 0,
            last_failed_at INTEGER NOT NULL DEFAULT 0,
            locked_until INTEGER NOT NULL DEFAULT 0
        )
        r   executecommit)dbs    r   _ensure_schemar+   M   sC    	BJJ
	 IIK JJ		 IIKr   c                ^    t               }|j                  d| |f      }|j                         S )NzaSELECT fail_count, last_failed_at, locked_until FROM login_attempts WHERE username = ? AND ip = ?r   r(   fetchone)usernamer   r*   curs       r   _get_rowr1   l   s/    	B
**k	2C <<>r   c                \    t               }|j                  d| f      }|j                         S )NzSSELECT fail_count, last_failed_at, locked_until FROM login_ip_attempts WHERE ip = ?r-   )r   r*   r0   s      r   _get_ip_rowr3   u   s-    	B
**]	C <<>r   c                f    t               }|j                  d| ||||f       |j                          y )NaC  
        INSERT INTO login_attempts (username, ip, fail_count, last_failed_at, locked_until)
        VALUES (?, ?, ?, ?, ?)
        ON CONFLICT(username, ip) DO UPDATE SET
            fail_count=excluded.fail_count,
            last_failed_at=excluded.last_failed_at,
            locked_until=excluded.locked_until
        r'   )r/   r   
fail_countlast_failed_atlocked_untilr*   s         r   _upsert_rowr8   ~   s5    	BJJ	 
2z><@
 IIKr   c                d    t               }|j                  d| |||f       |j                          y )Na/  
        INSERT INTO login_ip_attempts (ip, fail_count, last_failed_at, locked_until)
        VALUES (?, ?, ?, ?)
        ON CONFLICT(ip) DO UPDATE SET
            fail_count=excluded.fail_count,
            last_failed_at=excluded.last_failed_at,
            locked_until=excluded.locked_until
        r'   )r   r5   r6   r7   r*   s        r   _upsert_ip_rowr:      s3    	BJJ	 
Z6
 IIKr   c           	     "   t        |      ryt                t        | |      }t               }|rt	        |d   xs d      }||kD  rd||z
  fS t	        |d   xs d      t
        k\  rt	        |d   xs d      t        z   |kD  r|t        z   }t        | |t	        |d   xs d      t	        |d   xs d      |       t        rHt        |      }t	        |r|d   ndxs d      }||k  r"t        |t	        |r|d   ndxs d      ||       dt        fS y)z&Return (allowed, retry_after_seconds).Tr   r7   r   Fr5   r6   )r   r+   r1   r   r	   r    r"   r#   r8   LOCK_IP_ON_ACCOUNT_LOCKr3   r:   )r/   r   rownowr7   
lock_untilip_rowexistings           r   is_login_allowedrC      s)    
8R
 C
)C
3~.3!4#,,,,s< %A&/9c#FVBWB\[\>]`t>twz>z11J"c#l*;*@q&A3sK[G\Ga`aCbdno&$R&~ 6aMANj("2sFF<,@PQ+WVW'XZ]_ij,,,r   c                ~   t        |       ryt                t        |       }t               }|rt	        |d   xs d      }||kD  rd||z
  fS t	        |d   xs d      t
        k\  rYt	        |d   xs d      t        z   |kD  r=t        | t	        |d   xs d      t	        |d   xs d      |t        z          dt        fS y)Nr<   r7   r   Fr5   r6   )	r   r+   r3   r   r	   r    r"   r:   r#   )r   r>   r?   r7   s       r   is_ip_allowedrE      s    
b/C
)C
3~.3!4#,,,,s< %A&/9c#FVBWB\[\>]`t>twz>z2s3|#4#9:CDT@U@ZYZ<[]`cu]uv,,,r   c                R   t        |       ryt                t               }t        |       }|st	        | d|d       yt        |d   xs d      }t        |d   xs d      }t        |d   xs d      }|t        z   |k  rd}|dz  }|}|t        k\  r	|t        z   }t	        | |||       y)zIncrement only the IP-level counters. Used for attempts blocked before
    credential verification to escalate to IP lock across usernames.Nr   r   r5   r6   r7   )	r   r+   r   r3   r:   r	   r"   r    r#   )r   r?   r>   faillastlocks         r   record_ip_failurerJ      s     
)C
b/Cr1c1%s< %A&Ds#$)*Ds>"'a(D""S(AIDD''2tT4(r   c                f   t        |      ry t                t               }t        | |      }|st	        | |d|d       nlt        |d   xs d      }t        |d   xs d      }t        |d   xs d      }|t        z   |k  rd}|dz  }|}|t        k\  r	|t        z   }t	        | ||||       t        |      }|st        |d|d       y t        |d   xs d      }t        |d   xs d      }	t        |d   xs d      }
|	t        z   |k  rd}|dz  }|}	|t        k\  r	|t        z   }
t        |||	|
       y )Nr   r   r5   r6   r7   )r   r+   r   r1   r8   r	   r"   r    r#   r3   r:   )r/   r   r?   r>   r5   r6   r7   rA   ip_failip_lastip_locks              r   record_login_failurerO      sZ   
)C
8R
 CHb!S!,\*/a0
S!127a83~.3!4 0036Ja
(!33LHb*nlK _Fr1c1%&&+!,G&)*/a0G&(-A.G%%+qLGG/!**2w1r   c                t    t                t               }|j                  d| |f       |j                          y )Nz8DELETE FROM login_attempts WHERE username = ? AND ip = ?)r+   r   r(   r)   )r/   r   r*   s      r   record_login_successrQ     s.    	B JJIHVX>ZIIKr   )returnr	   )r   r   rR   bool)r   r   r   r	   rR   r	   )r/   r   r   r   )r   r   )
r/   r   r   r   r5   r	   r6   r	   r7   r	   )r   r   r5   r	   r6   r	   r7   r	   )r/   r   r   r   rR   Tuple[bool, int])r   r   rR   rT   )r   r   rR   None)r/   r   r   r   rR   rU   )__doc__
__future__r   r   r
   typingr   r*   r   r   r   r   r    r"   r#   r   r=   r+   r1   r3   r8   r:   rC   rE   rJ   rO   rQ   r   r   r   <module>rY      s   6 # 	   . ,b1 6< 2C8 #"))$CSISP >  8$)2*2Zr   