mirror of
https://github.com/nicolargo/glances.git
synced 2026-06-03 11:24:52 -04:00
156 lines
4.7 KiB
Python
Executable File
156 lines
4.7 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Glances - An eye on your system
|
|
#
|
|
# SPDX-FileCopyrightText: 2026 Nicolas Hennion <nicolas@nicolargo.com>
|
|
#
|
|
# SPDX-License-Identifier: LGPL-3.0-only
|
|
#
|
|
|
|
"""Glances v5 — unit tests for the security helpers (PBKDF2 + JWT).
|
|
|
|
Test stack: pytest + pytest-asyncio (auto mode). See architecture decisions §9.
|
|
|
|
Coverage:
|
|
- hash_password / verify_password round-trip
|
|
- distinct salts on every call (random salt by default)
|
|
- explicit salt override (deterministic for tests)
|
|
- malformed stored values rejected (empty, missing $, empty parts)
|
|
- timing-safe comparison uses hmac.compare_digest (smoke test)
|
|
- JWTHandler mint / verify round-trip
|
|
- JWTHandler with explicit secret vs generated
|
|
- JWTHandler verify rejects: malformed, wrong secret, wrong issuer, expired,
|
|
missing `sub`, missing token
|
|
- JWTHandler.expire_minutes is honoured
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
from jose import jwt
|
|
|
|
from glances.security_v5 import JWTHandler, hash_password, verify_password
|
|
|
|
# -------------------------------------------------------- password
|
|
|
|
|
|
def test_hash_password_round_trip():
|
|
stored = hash_password("hunter2")
|
|
assert verify_password("hunter2", stored) is True
|
|
assert verify_password("hunter3", stored) is False
|
|
|
|
|
|
def test_hash_password_random_salt_per_call():
|
|
a = hash_password("hunter2")
|
|
b = hash_password("hunter2")
|
|
assert a != b
|
|
# Both still verify against the same plaintext.
|
|
assert verify_password("hunter2", a)
|
|
assert verify_password("hunter2", b)
|
|
|
|
|
|
def test_hash_password_explicit_salt_is_deterministic():
|
|
a = hash_password("hunter2", salt="deadbeef")
|
|
b = hash_password("hunter2", salt="deadbeef")
|
|
assert a == b
|
|
assert a.startswith("deadbeef$")
|
|
|
|
|
|
def test_verify_password_rejects_malformed_stored():
|
|
assert verify_password("x", "") is False
|
|
assert verify_password("x", "no-dollar-here") is False
|
|
assert verify_password("x", "$onlyhash") is False
|
|
assert verify_password("x", "salt$") is False
|
|
|
|
|
|
def test_verify_password_empty_plaintext_does_not_crash():
|
|
stored = hash_password("not-empty")
|
|
assert verify_password("", stored) is False
|
|
|
|
|
|
def test_hash_password_format_is_salt_dollar_hex():
|
|
stored = hash_password("any", salt="abc123")
|
|
salt, _, digest = stored.partition("$")
|
|
assert salt == "abc123"
|
|
# 128 bytes = 256 hex chars.
|
|
assert len(digest) == 256
|
|
int(digest, 16) # raises if not hex
|
|
|
|
|
|
# -------------------------------------------------------- JWT
|
|
|
|
|
|
def test_jwt_round_trip():
|
|
handler = JWTHandler(secret_key="test-secret")
|
|
token = handler.create_access_token("alice")
|
|
assert handler.verify_token(token) == "alice"
|
|
|
|
|
|
def test_jwt_rejects_wrong_secret():
|
|
h1 = JWTHandler(secret_key="secret-a")
|
|
h2 = JWTHandler(secret_key="secret-b")
|
|
token = h1.create_access_token("alice")
|
|
assert h2.verify_token(token) is None
|
|
|
|
|
|
def test_jwt_rejects_malformed_token():
|
|
handler = JWTHandler(secret_key="x")
|
|
assert handler.verify_token("not.a.token") is None
|
|
assert handler.verify_token("") is None
|
|
|
|
|
|
def test_jwt_rejects_wrong_issuer():
|
|
handler = JWTHandler(secret_key="x")
|
|
# Mint a token outside the helper, with a foreign issuer.
|
|
foreign = jwt.encode(
|
|
{"sub": "alice", "iss": "not-glances", "exp": int(time.time()) + 60},
|
|
"x",
|
|
algorithm="HS256",
|
|
)
|
|
assert handler.verify_token(foreign) is None
|
|
|
|
|
|
def test_jwt_rejects_expired_token():
|
|
handler = JWTHandler(secret_key="x", expire_minutes=60)
|
|
# Bypass the helper to mint an already-expired token.
|
|
expired = jwt.encode(
|
|
{"sub": "alice", "iss": "glances", "exp": int(time.time()) - 1, "iat": int(time.time()) - 60},
|
|
"x",
|
|
algorithm="HS256",
|
|
)
|
|
assert handler.verify_token(expired) is None
|
|
|
|
|
|
def test_jwt_rejects_missing_sub():
|
|
handler = JWTHandler(secret_key="x")
|
|
no_sub = jwt.encode(
|
|
{"iss": "glances", "exp": int(time.time()) + 60, "iat": int(time.time())},
|
|
"x",
|
|
algorithm="HS256",
|
|
)
|
|
assert handler.verify_token(no_sub) is None
|
|
|
|
|
|
def test_jwt_generated_secret_marker():
|
|
h = JWTHandler()
|
|
assert h.secret_was_generated is True
|
|
# The handler is still functional with the generated secret.
|
|
token = h.create_access_token("bob")
|
|
assert h.verify_token(token) == "bob"
|
|
|
|
|
|
def test_jwt_explicit_secret_marker():
|
|
h = JWTHandler(secret_key="explicit")
|
|
assert h.secret_was_generated is False
|
|
|
|
|
|
def test_jwt_expire_minutes_honored():
|
|
h = JWTHandler(secret_key="x", expire_minutes=42)
|
|
assert h.expire_minutes == 42
|
|
token = h.create_access_token("alice")
|
|
payload = jwt.decode(token, "x", algorithms=["HS256"], issuer="glances")
|
|
delta_seconds = payload["exp"] - payload["iat"]
|
|
# 42 minutes = 2520 seconds, allow a small skew.
|
|
assert 2510 <= delta_seconds <= 2530
|