Initial native Linux parental control application
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
|
||||
data/*.db
|
||||
data/*.db-*
|
||||
|
||||
.env
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -0,0 +1,63 @@
|
||||
# Linux Parental Control
|
||||
|
||||
A Linux parental-control system for managing Linux user access, daily time allowances, access windows, temporary grants, and automatic session enforcement.
|
||||
|
||||
> **Status:** Early development
|
||||
|
||||
## Features
|
||||
|
||||
- Per-user daily time allowances
|
||||
- Different allowances for each day of the week
|
||||
- Multiple access windows per day
|
||||
- Temporary time grants
|
||||
- Usage tracking
|
||||
- Automatic session termination
|
||||
- Automatic account locking
|
||||
- Automatic account unlocking
|
||||
- Reboot-safe enforcement
|
||||
- Web-based administration
|
||||
- SQLite database
|
||||
- systemd service support
|
||||
|
||||
---
|
||||
|
||||
# Requirements
|
||||
|
||||
The application currently targets Linux systems using `systemd`.
|
||||
|
||||
You need:
|
||||
|
||||
- Linux
|
||||
- Python 3
|
||||
- `python-venv`
|
||||
- `pip`
|
||||
- `systemd`
|
||||
- `sudo`
|
||||
- `passwd`
|
||||
- `loginctl`
|
||||
- Git
|
||||
|
||||
The enforcement service requires **root privileges** because it manages other Linux users and their sessions.
|
||||
|
||||
---
|
||||
|
||||
# 1. Clone the Repository
|
||||
|
||||
Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Alsantek-me/linux-user-timer.git
|
||||
```
|
||||
|
||||
## 2. Make it executable
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
```
|
||||
|
||||
## 3. Run the install script
|
||||
```bash
|
||||
sudo ./install.sh
|
||||
```
|
||||
### You can access it via http://127.0.0.1:8765/admin
|
||||
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
DATABASE_DIR = BASE_DIR / "data"
|
||||
DATABASE_PATH = DATABASE_DIR / "parental-control.db"
|
||||
|
||||
|
||||
def initialize_database():
|
||||
DATABASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with sqlite3.connect(DATABASE_PATH) as db:
|
||||
db.execute("PRAGMA foreign_keys = ON")
|
||||
|
||||
db.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS daily_allowances (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
weekday INTEGER NOT NULL,
|
||||
allowance_seconds INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
UNIQUE(user_id, weekday),
|
||||
|
||||
FOREIGN KEY(user_id)
|
||||
REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS access_windows (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
weekday INTEGER NOT NULL,
|
||||
start_minute INTEGER NOT NULL,
|
||||
end_minute INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY(user_id)
|
||||
REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS usage (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
used_seconds INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
UNIQUE(user_id, date),
|
||||
|
||||
FOREIGN KEY(user_id)
|
||||
REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS temporary_grants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
seconds INTEGER NOT NULL,
|
||||
remaining_seconds INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TEXT,
|
||||
consumed INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
event_type TEXT NOT NULL,
|
||||
details TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY(user_id)
|
||||
REFERENCES users(id)
|
||||
ON DELETE SET NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db():
|
||||
db = sqlite3.connect(DATABASE_PATH)
|
||||
db.row_factory = sqlite3.Row
|
||||
db.execute("PRAGMA foreign_keys = ON")
|
||||
|
||||
try:
|
||||
yield db
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
@@ -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
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import time
|
||||
|
||||
from .database import initialize_database
|
||||
from .scheduler import start_scheduler, stop_scheduler
|
||||
|
||||
|
||||
def main():
|
||||
initialize_database()
|
||||
start_scheduler()
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(60)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
stop_scheduler()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,34 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: int
|
||||
username: str
|
||||
enabled: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class DailyAllowance:
|
||||
user_id: int
|
||||
weekday: int
|
||||
allowance_seconds: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccessWindow:
|
||||
id: int
|
||||
user_id: int
|
||||
weekday: int
|
||||
start_minute: int
|
||||
end_minute: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemporaryGrant:
|
||||
id: int
|
||||
user_id: int
|
||||
seconds: int
|
||||
expires_at: Optional[str]
|
||||
consumed: bool
|
||||
@@ -0,0 +1,183 @@
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from .enforcement import (
|
||||
enforce_all_users,
|
||||
record_usage,
|
||||
get_user_policy,
|
||||
consume_grant_seconds,
|
||||
)
|
||||
|
||||
|
||||
CHECK_INTERVAL = 5
|
||||
|
||||
|
||||
class Scheduler:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
interval: int = CHECK_INTERVAL,
|
||||
):
|
||||
self.interval = interval
|
||||
|
||||
self._thread = None
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
self._last_usage_update = {}
|
||||
|
||||
def start(self):
|
||||
|
||||
if (
|
||||
self._thread is not None
|
||||
and self._thread.is_alive()
|
||||
):
|
||||
return
|
||||
|
||||
self._stop_event.clear()
|
||||
|
||||
self._thread = threading.Thread(
|
||||
target=self._run,
|
||||
name="parental-control-scheduler",
|
||||
daemon=True,
|
||||
)
|
||||
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
|
||||
self._stop_event.set()
|
||||
|
||||
if self._thread is not None:
|
||||
self._thread.join(
|
||||
timeout=self.interval + 2
|
||||
)
|
||||
|
||||
def _run(self):
|
||||
|
||||
# Evaluate immediately when the
|
||||
# application starts.
|
||||
self._tick()
|
||||
|
||||
while not self._stop_event.wait(
|
||||
self.interval
|
||||
):
|
||||
self._tick()
|
||||
|
||||
def _tick(self):
|
||||
|
||||
now = time.monotonic()
|
||||
|
||||
results = enforce_all_users()
|
||||
|
||||
for result in results:
|
||||
|
||||
user_id = result["user_id"]
|
||||
|
||||
if not result["logged_in"]:
|
||||
self._last_usage_update.pop(
|
||||
user_id,
|
||||
None,
|
||||
)
|
||||
continue
|
||||
|
||||
if not result["allowed"]:
|
||||
self._last_usage_update.pop(
|
||||
user_id,
|
||||
None,
|
||||
)
|
||||
continue
|
||||
|
||||
previous = (
|
||||
self._last_usage_update.get(
|
||||
user_id
|
||||
)
|
||||
)
|
||||
|
||||
self._last_usage_update[user_id] = now
|
||||
|
||||
if previous is None:
|
||||
continue
|
||||
|
||||
elapsed = int(
|
||||
now - previous
|
||||
)
|
||||
|
||||
if elapsed <= 0:
|
||||
continue
|
||||
|
||||
self._record_allowed_usage(
|
||||
user_id,
|
||||
elapsed,
|
||||
)
|
||||
|
||||
def _record_allowed_usage(
|
||||
self,
|
||||
user_id: int,
|
||||
seconds: int,
|
||||
):
|
||||
|
||||
if seconds <= 0:
|
||||
return
|
||||
|
||||
weekday = datetime.now().weekday()
|
||||
|
||||
(
|
||||
allowance_seconds,
|
||||
usage_seconds,
|
||||
windows,
|
||||
grant_seconds,
|
||||
) = get_user_policy(
|
||||
user_id,
|
||||
weekday,
|
||||
)
|
||||
|
||||
allowance_remaining = max(
|
||||
0,
|
||||
allowance_seconds
|
||||
- usage_seconds,
|
||||
)
|
||||
|
||||
normal_usage = min(
|
||||
seconds,
|
||||
allowance_remaining,
|
||||
)
|
||||
|
||||
grant_usage = (
|
||||
seconds
|
||||
- normal_usage
|
||||
)
|
||||
|
||||
if grant_usage > grant_seconds:
|
||||
grant_usage = grant_seconds
|
||||
|
||||
total_usage = (
|
||||
normal_usage
|
||||
+ grant_usage
|
||||
)
|
||||
|
||||
if total_usage <= 0:
|
||||
return
|
||||
|
||||
record_usage(
|
||||
user_id,
|
||||
total_usage,
|
||||
)
|
||||
|
||||
if grant_usage > 0:
|
||||
|
||||
consume_grant_seconds(
|
||||
user_id,
|
||||
grant_usage,
|
||||
)
|
||||
|
||||
|
||||
scheduler = Scheduler()
|
||||
|
||||
|
||||
def start_scheduler():
|
||||
scheduler.start()
|
||||
|
||||
|
||||
def stop_scheduler():
|
||||
scheduler.stop()
|
||||
@@ -0,0 +1,59 @@
|
||||
import pwd
|
||||
import subprocess
|
||||
|
||||
|
||||
def linux_user_exists(username: str) -> bool:
|
||||
try:
|
||||
pwd.getpwnam(username)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
|
||||
def get_uid(username: str) -> int:
|
||||
return pwd.getpwnam(username).pw_uid
|
||||
|
||||
|
||||
def is_locked(username: str) -> bool:
|
||||
result = subprocess.run(
|
||||
["passwd", "-S", username],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return False
|
||||
|
||||
parts = result.stdout.split()
|
||||
|
||||
if len(parts) < 2:
|
||||
return False
|
||||
|
||||
return parts[1] == "L"
|
||||
|
||||
|
||||
def lock_user(username: str):
|
||||
subprocess.run(
|
||||
["loginctl", "terminate-user", username],
|
||||
check=False,
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
["passwd", "-l", username],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def unlock_user(username: str):
|
||||
subprocess.run(
|
||||
["passwd", "-u", username],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def terminate_user(username: str):
|
||||
subprocess.run(
|
||||
["loginctl", "terminate-user", username],
|
||||
check=False,
|
||||
)
|
||||
+2149
File diff suppressed because it is too large
Load Diff
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# ==========================================
|
||||
# Linux Parental Control Installer
|
||||
# ==========================================
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run with sudo:"
|
||||
echo " sudo ./install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INSTALL_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
APP_NAME="Parental Control"
|
||||
DESKTOP_FILE="/usr/share/applications/parental-control.desktop"
|
||||
SERVICE_FILE="/etc/systemd/system/parental-control.service"
|
||||
|
||||
echo
|
||||
echo "=========================================="
|
||||
echo " Linux Parental Control Installer"
|
||||
echo "=========================================="
|
||||
echo
|
||||
echo "Installing from:"
|
||||
echo " $INSTALL_DIR"
|
||||
echo
|
||||
|
||||
# ------------------------------------------
|
||||
# 1. System dependencies
|
||||
# ------------------------------------------
|
||||
|
||||
echo "[1/6] Installing system dependencies"
|
||||
|
||||
pacman -S --needed --noconfirm \
|
||||
python \
|
||||
python-pip
|
||||
|
||||
echo
|
||||
echo "System dependencies installed."
|
||||
|
||||
# ------------------------------------------
|
||||
# 2. Python virtual environment
|
||||
# ------------------------------------------
|
||||
|
||||
echo
|
||||
echo "[2/6] Creating Python virtual environment"
|
||||
|
||||
if [ ! -d "$INSTALL_DIR/.venv" ]; then
|
||||
python -m venv "$INSTALL_DIR/.venv"
|
||||
else
|
||||
echo "Virtual environment already exists."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Virtual environment ready."
|
||||
|
||||
# ------------------------------------------
|
||||
# 3. Python dependencies
|
||||
# ------------------------------------------
|
||||
|
||||
echo
|
||||
echo "[3/6] Installing Python packages"
|
||||
|
||||
"$INSTALL_DIR/.venv/bin/python" -m pip install \
|
||||
--upgrade pip
|
||||
|
||||
"$INSTALL_DIR/.venv/bin/python" -m pip install \
|
||||
-r "$INSTALL_DIR/requirements.txt"
|
||||
|
||||
echo
|
||||
echo "Python packages installed."
|
||||
|
||||
# ------------------------------------------
|
||||
# 4. Install systemd service
|
||||
# ------------------------------------------
|
||||
|
||||
echo
|
||||
echo "[4/6] Installing systemd service"
|
||||
|
||||
sed \
|
||||
"s|%INSTALL_DIR%|$INSTALL_DIR|g" \
|
||||
"$INSTALL_DIR/parental-control.service" \
|
||||
> "$SERVICE_FILE"
|
||||
|
||||
chmod 644 "$SERVICE_FILE"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable parental-control.service
|
||||
systemctl restart parental-control.service
|
||||
|
||||
echo
|
||||
echo "Systemd service installed and started."
|
||||
|
||||
# ------------------------------------------
|
||||
# 5. Install desktop application
|
||||
# ------------------------------------------
|
||||
|
||||
echo
|
||||
echo "[5/6] Installing application launcher"
|
||||
|
||||
cat > "$DESKTOP_FILE" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=$APP_NAME
|
||||
Comment=Linux Parental Control Manager
|
||||
Exec=$INSTALL_DIR/.venv/bin/python -m desktop.main
|
||||
Path=$INSTALL_DIR
|
||||
Icon=preferences-system-parental-controls
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Settings;
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
chmod 644 "$DESKTOP_FILE"
|
||||
|
||||
echo
|
||||
echo "Desktop launcher installed:"
|
||||
echo " $DESKTOP_FILE"
|
||||
|
||||
# ------------------------------------------
|
||||
# 6. Update desktop database
|
||||
# ------------------------------------------
|
||||
|
||||
echo
|
||||
echo "[6/6] Updating application database"
|
||||
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
update-desktop-database /usr/share/applications
|
||||
else
|
||||
echo "update-desktop-database is not installed."
|
||||
echo "Skipping desktop database update."
|
||||
fi
|
||||
|
||||
# ------------------------------------------
|
||||
# Verify installation
|
||||
# ------------------------------------------
|
||||
|
||||
echo
|
||||
echo "=========================================="
|
||||
echo " Installation complete"
|
||||
echo "=========================================="
|
||||
echo
|
||||
echo "Application:"
|
||||
echo " $APP_NAME"
|
||||
echo
|
||||
echo "Launch it from your application menu."
|
||||
echo "On your system:"
|
||||
echo " Super + D"
|
||||
echo
|
||||
echo "Service:"
|
||||
echo " systemctl status parental-control"
|
||||
echo
|
||||
echo "=========================================="
|
||||
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=Linux Parental Control
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
WorkingDirectory=%INSTALL_DIR%
|
||||
|
||||
Environment="PATH=%INSTALL_DIR%/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin"
|
||||
Environment="PARENTAL_CONTROL_ENFORCEMENT=1"
|
||||
|
||||
ExecStart=%INSTALL_DIR%/.venv/bin/python -m app.main
|
||||
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,4 @@
|
||||
PySide6==6.11.2
|
||||
PySide6_Addons==6.11.2
|
||||
PySide6_Essentials==6.11.2
|
||||
shiboken6==6.11.2
|
||||
Reference in New Issue
Block a user