first commit
This commit is contained in:
@@ -0,0 +1,455 @@
|
||||
from datetime import datetime
|
||||
import subprocess
|
||||
|
||||
from .database import get_db
|
||||
from .users import (
|
||||
lock_user,
|
||||
unlock_user,
|
||||
terminate_user,
|
||||
is_locked,
|
||||
)
|
||||
|
||||
|
||||
def user_has_session(username: str) -> bool:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"loginctl",
|
||||
"list-users",
|
||||
"--no-legend",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return False
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split()
|
||||
|
||||
if len(parts) >= 2 and parts[1] == username:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def current_time():
|
||||
now = datetime.now()
|
||||
weekday = now.weekday()
|
||||
minute = now.hour * 60 + now.minute
|
||||
|
||||
return now, weekday, minute
|
||||
|
||||
|
||||
def get_user_policy(user_id: int, weekday: int):
|
||||
with get_db() as db:
|
||||
allowance_row = db.execute(
|
||||
"""
|
||||
SELECT allowance_seconds
|
||||
FROM daily_allowances
|
||||
WHERE user_id = ?
|
||||
AND weekday = ?
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
weekday,
|
||||
),
|
||||
).fetchone()
|
||||
|
||||
allowance_seconds = (
|
||||
allowance_row["allowance_seconds"]
|
||||
if allowance_row
|
||||
else 0
|
||||
)
|
||||
|
||||
today = datetime.now().date().isoformat()
|
||||
|
||||
usage_row = db.execute(
|
||||
"""
|
||||
SELECT used_seconds
|
||||
FROM usage
|
||||
WHERE user_id = ?
|
||||
AND date = ?
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
today,
|
||||
),
|
||||
).fetchone()
|
||||
|
||||
usage_seconds = (
|
||||
usage_row["used_seconds"]
|
||||
if usage_row
|
||||
else 0
|
||||
)
|
||||
|
||||
windows = db.execute(
|
||||
"""
|
||||
SELECT id, start_minute, end_minute
|
||||
FROM access_windows
|
||||
WHERE user_id = ?
|
||||
AND weekday = ?
|
||||
ORDER BY start_minute
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
weekday,
|
||||
),
|
||||
).fetchall()
|
||||
|
||||
grant_row = db.execute(
|
||||
"""
|
||||
SELECT COALESCE(
|
||||
SUM(remaining_seconds),
|
||||
0
|
||||
) AS total
|
||||
FROM temporary_grants
|
||||
WHERE user_id = ?
|
||||
AND consumed = 0
|
||||
AND (
|
||||
expires_at IS NULL
|
||||
OR expires_at > ?
|
||||
)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
datetime.now().isoformat(),
|
||||
),
|
||||
).fetchone()
|
||||
|
||||
grant_seconds = grant_row["total"]
|
||||
|
||||
return (
|
||||
allowance_seconds,
|
||||
usage_seconds,
|
||||
windows,
|
||||
grant_seconds,
|
||||
)
|
||||
|
||||
|
||||
def is_inside_window(windows, minute: int) -> bool:
|
||||
if not windows:
|
||||
return True
|
||||
|
||||
for window in windows:
|
||||
if (
|
||||
window["start_minute"]
|
||||
<= minute
|
||||
< window["end_minute"]
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_remaining_grant_seconds(user_id: int) -> int:
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
with get_db() as db:
|
||||
row = db.execute(
|
||||
"""
|
||||
SELECT COALESCE(
|
||||
SUM(remaining_seconds),
|
||||
0
|
||||
) AS total
|
||||
FROM temporary_grants
|
||||
WHERE user_id = ?
|
||||
AND consumed = 0
|
||||
AND (
|
||||
expires_at IS NULL
|
||||
OR expires_at > ?
|
||||
)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
now,
|
||||
),
|
||||
).fetchone()
|
||||
|
||||
return row["total"]
|
||||
|
||||
|
||||
def consume_grant_seconds(
|
||||
user_id: int,
|
||||
seconds: int,
|
||||
):
|
||||
if seconds <= 0:
|
||||
return
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
with get_db() as db:
|
||||
grants = db.execute(
|
||||
"""
|
||||
SELECT id, remaining_seconds
|
||||
FROM temporary_grants
|
||||
WHERE user_id = ?
|
||||
AND consumed = 0
|
||||
AND remaining_seconds > 0
|
||||
AND (
|
||||
expires_at IS NULL
|
||||
OR expires_at > ?
|
||||
)
|
||||
ORDER BY id ASC
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
now,
|
||||
),
|
||||
).fetchall()
|
||||
|
||||
remaining = seconds
|
||||
|
||||
for grant in grants:
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
available = grant["remaining_seconds"]
|
||||
|
||||
consumed = min(
|
||||
available,
|
||||
remaining,
|
||||
)
|
||||
|
||||
new_remaining = (
|
||||
available - consumed
|
||||
)
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE temporary_grants
|
||||
SET remaining_seconds = ?,
|
||||
consumed = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
new_remaining,
|
||||
1 if new_remaining <= 0 else 0,
|
||||
grant["id"],
|
||||
),
|
||||
)
|
||||
|
||||
remaining -= consumed
|
||||
|
||||
|
||||
def record_usage(
|
||||
user_id: int,
|
||||
seconds: int,
|
||||
):
|
||||
if seconds <= 0:
|
||||
return
|
||||
|
||||
today = datetime.now().date().isoformat()
|
||||
|
||||
with get_db() as db:
|
||||
row = db.execute(
|
||||
"""
|
||||
SELECT used_seconds
|
||||
FROM usage
|
||||
WHERE user_id = ?
|
||||
AND date = ?
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
today,
|
||||
),
|
||||
).fetchone()
|
||||
|
||||
if row is None:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO usage (
|
||||
user_id,
|
||||
date,
|
||||
used_seconds
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
today,
|
||||
seconds,
|
||||
),
|
||||
)
|
||||
else:
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE usage
|
||||
SET used_seconds =
|
||||
used_seconds + ?
|
||||
WHERE user_id = ?
|
||||
AND date = ?
|
||||
""",
|
||||
(
|
||||
seconds,
|
||||
user_id,
|
||||
today,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def record_event(
|
||||
user_id: int,
|
||||
event_type: str,
|
||||
details: str = "",
|
||||
):
|
||||
with get_db() as db:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO events (
|
||||
user_id,
|
||||
event_type,
|
||||
details
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
event_type,
|
||||
details,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def evaluate_user(
|
||||
user_id: int,
|
||||
username: str,
|
||||
):
|
||||
now, weekday, minute = current_time()
|
||||
|
||||
(
|
||||
allowance_seconds,
|
||||
usage_seconds,
|
||||
windows,
|
||||
grant_seconds,
|
||||
) = get_user_policy(
|
||||
user_id,
|
||||
weekday,
|
||||
)
|
||||
|
||||
inside_window = is_inside_window(
|
||||
windows,
|
||||
minute,
|
||||
)
|
||||
|
||||
allowance_remaining = max(
|
||||
0,
|
||||
allowance_seconds - usage_seconds,
|
||||
)
|
||||
|
||||
total_remaining = (
|
||||
allowance_remaining
|
||||
+ grant_seconds
|
||||
)
|
||||
|
||||
logged_in = user_has_session(
|
||||
username
|
||||
)
|
||||
|
||||
allowed_by_schedule = (
|
||||
inside_window
|
||||
and allowance_remaining > 0
|
||||
)
|
||||
|
||||
allowed_by_grant = (
|
||||
grant_seconds > 0
|
||||
)
|
||||
|
||||
should_allow = (
|
||||
allowed_by_schedule
|
||||
or allowed_by_grant
|
||||
)
|
||||
|
||||
locked = is_locked(username)
|
||||
|
||||
if should_allow:
|
||||
if locked:
|
||||
try:
|
||||
unlock_user(username)
|
||||
|
||||
record_event(
|
||||
user_id,
|
||||
"auto_unlock",
|
||||
"Access became available",
|
||||
)
|
||||
except Exception as exc:
|
||||
record_event(
|
||||
user_id,
|
||||
"unlock_error",
|
||||
str(exc),
|
||||
)
|
||||
|
||||
else:
|
||||
if logged_in:
|
||||
terminate_user(username)
|
||||
|
||||
record_event(
|
||||
user_id,
|
||||
"session_terminated",
|
||||
"Access is not currently permitted",
|
||||
)
|
||||
|
||||
if not locked:
|
||||
try:
|
||||
lock_user(username)
|
||||
|
||||
record_event(
|
||||
user_id,
|
||||
"auto_lock",
|
||||
"Access is not currently permitted",
|
||||
)
|
||||
except Exception as exc:
|
||||
record_event(
|
||||
user_id,
|
||||
"lock_error",
|
||||
str(exc),
|
||||
)
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"username": username,
|
||||
"timestamp": now.isoformat(),
|
||||
"weekday": weekday,
|
||||
"minute": minute,
|
||||
"inside_window": inside_window,
|
||||
"logged_in": logged_in,
|
||||
"allowance_seconds": allowance_seconds,
|
||||
"usage_seconds": usage_seconds,
|
||||
"allowance_remaining": allowance_remaining,
|
||||
"grant_seconds": grant_seconds,
|
||||
"total_remaining": total_remaining,
|
||||
"allowed": should_allow,
|
||||
}
|
||||
|
||||
|
||||
def enforce_all_users():
|
||||
with get_db() as db:
|
||||
users = db.execute(
|
||||
"""
|
||||
SELECT id, username, enabled
|
||||
FROM users
|
||||
WHERE enabled = 1
|
||||
ORDER BY id
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
results = []
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
result = evaluate_user(
|
||||
user["id"],
|
||||
user["username"],
|
||||
)
|
||||
|
||||
results.append(result)
|
||||
|
||||
except Exception as exc:
|
||||
record_event(
|
||||
user["id"],
|
||||
"enforcement_error",
|
||||
str(exc),
|
||||
)
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user