77 lines
3.8 KiB
Python
77 lines
3.8 KiB
Python
"""Root passwords entered through the UI become encrypted installer hashes."""
|
|
import json
|
|
|
|
from passlib.hash import sha512_crypt
|
|
import pytest
|
|
|
|
from provisioner.security import Security
|
|
from test_acceptance import PASSWORD, environment, login, post
|
|
|
|
|
|
def test_root_password_secret_is_hashed_encrypted_and_never_returned(environment):
|
|
app, client, csrf = environment
|
|
password = " Installer-password-2026! "
|
|
created = post(client, "/api/v1/secrets", {
|
|
"name": "Installer root password", "kind": "root_password", "value": password,
|
|
}, csrf)
|
|
assert set(created) == {"id", "name"}
|
|
with app.state.db.connection() as connection:
|
|
row = connection.execute("SELECT * FROM secrets WHERE id=?", (created["id"],)).fetchone()
|
|
audit_data = json.dumps([dict(entry) for entry in connection.execute("SELECT * FROM audit")])
|
|
stored = app.state.security.decrypt(row["ciphertext"])
|
|
assert stored.startswith("$6$rounds=656000$")
|
|
assert sha512_crypt.verify(password, stored)
|
|
assert not sha512_crypt.verify(password.strip(), stored)
|
|
assert password not in row["ciphertext"] and stored not in row["ciphertext"]
|
|
public_data = json.dumps(created) + client.get("/api/v1/secrets").text + audit_data
|
|
assert password not in public_data and stored not in public_data
|
|
|
|
|
|
@pytest.mark.parametrize("password", ["short", "x" * 1025, "Valid-password\x00invalid"])
|
|
def test_root_password_secret_rejects_invalid_password_without_echo(environment, password):
|
|
app, client, csrf = environment
|
|
response = client.post("/api/v1/secrets", json={
|
|
"name": "Invalid root password", "kind": "root_password", "value": password,
|
|
}, headers=csrf)
|
|
assert response.status_code == 422
|
|
assert password not in response.text
|
|
with app.state.db.connection() as connection:
|
|
assert connection.execute("SELECT COUNT(*) FROM secrets").fetchone()[0] == 0
|
|
|
|
|
|
def test_raw_secret_default_preserves_existing_behavior(environment):
|
|
app, client, csrf = environment
|
|
created = post(client, "/api/v1/secrets", {"name": "Raw value", "value": " unchanged-value "}, csrf)
|
|
with app.state.db.connection() as connection:
|
|
row = connection.execute("SELECT ciphertext FROM secrets WHERE id=?", (created["id"],)).fetchone()
|
|
assert app.state.security.decrypt(row["ciphertext"]) == "unchanged-value"
|
|
assert client.post("/api/v1/secrets", json={"name": "Empty raw value", "value": " "}, headers=csrf).status_code == 422
|
|
|
|
|
|
@pytest.mark.parametrize("role, metadata_status", [("author", 200), ("operator", 200), ("reader", 403)])
|
|
def test_secret_metadata_roles_cannot_create_or_read_secret_values(environment, role, metadata_status):
|
|
app, client, csrf = environment
|
|
created = post(client, "/api/v1/secrets", {"name": "Private root reference", "value": "private-value"}, csrf)
|
|
post(client, "/api/v1/users", {"username": role, "password": PASSWORD, "role": role}, csrf)
|
|
user_csrf = login(client, username=role)
|
|
response = client.get("/api/v1/secrets")
|
|
assert response.status_code == metadata_status
|
|
if metadata_status == 200:
|
|
assert response.json()[0]["id"] == created["id"]
|
|
assert set(response.json()[0]) == {"id", "name", "created_at"}
|
|
assert "private-value" not in response.text
|
|
denied = client.post("/api/v1/secrets", json={
|
|
"name": "Denied password", "kind": "root_password", "value": "Forbidden-password-2026!",
|
|
}, headers=user_csrf)
|
|
assert denied.status_code == 403
|
|
assert "Forbidden-password" not in denied.text
|
|
|
|
|
|
def test_installer_password_hash_uses_random_salt():
|
|
password = "Installer-password-2026!"
|
|
first = Security.hash_installer_password(password)
|
|
second = Security.hash_installer_password(password)
|
|
assert first != second
|
|
assert sha512_crypt.verify(password, first)
|
|
assert sha512_crypt.verify(password, second)
|