mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-07 06:48:52 +00:00
Add password policy options to API
This commit is contained in:
@@ -16,6 +16,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from seedpass.core.manager import PasswordManager
|
||||
from seedpass.core.entry_types import EntryType
|
||||
from seedpass.core.api import UtilityService
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
@@ -117,11 +118,23 @@ def create_entry(
|
||||
etype = (entry.get("type") or entry.get("kind") or "password").lower()
|
||||
|
||||
if etype == "password":
|
||||
policy_keys = [
|
||||
"include_special_chars",
|
||||
"allowed_special_chars",
|
||||
"special_mode",
|
||||
"exclude_ambiguous",
|
||||
"min_uppercase",
|
||||
"min_lowercase",
|
||||
"min_digits",
|
||||
"min_special",
|
||||
]
|
||||
kwargs = {k: entry.get(k) for k in policy_keys if entry.get(k) is not None}
|
||||
index = _pm.entry_manager.add_entry(
|
||||
entry.get("label"),
|
||||
int(entry.get("length", 12)),
|
||||
entry.get("username"),
|
||||
entry.get("url"),
|
||||
**kwargs,
|
||||
)
|
||||
return {"id": index}
|
||||
|
||||
@@ -566,6 +579,30 @@ def change_password(
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/api/v1/password")
|
||||
def generate_password(
|
||||
data: dict, authorization: str | None = Header(None)
|
||||
) -> dict[str, str]:
|
||||
"""Generate a password using optional policy overrides."""
|
||||
_check_token(authorization)
|
||||
assert _pm is not None
|
||||
length = int(data.get("length", 12))
|
||||
policy_keys = [
|
||||
"include_special_chars",
|
||||
"allowed_special_chars",
|
||||
"special_mode",
|
||||
"exclude_ambiguous",
|
||||
"min_uppercase",
|
||||
"min_lowercase",
|
||||
"min_digits",
|
||||
"min_special",
|
||||
]
|
||||
kwargs = {k: data.get(k) for k in policy_keys if data.get(k) is not None}
|
||||
util = UtilityService(_pm)
|
||||
password = util.generate_password(length, **kwargs)
|
||||
return {"password": password}
|
||||
|
||||
|
||||
@app.post("/api/v1/vault/lock")
|
||||
def lock_vault(authorization: str | None = Header(None)) -> dict[str, str]:
|
||||
"""Lock the vault and clear sensitive data from memory."""
|
||||
|
@@ -84,6 +84,34 @@ class SyncResponse(BaseModel):
|
||||
delta_ids: List[str] = []
|
||||
|
||||
|
||||
class PasswordPolicyOptions(BaseModel):
|
||||
"""Optional password policy overrides."""
|
||||
|
||||
include_special_chars: bool | None = None
|
||||
allowed_special_chars: str | None = None
|
||||
special_mode: str | None = None
|
||||
exclude_ambiguous: bool | None = None
|
||||
min_uppercase: int | None = None
|
||||
min_lowercase: int | None = None
|
||||
min_digits: int | None = None
|
||||
min_special: int | None = None
|
||||
|
||||
|
||||
class AddPasswordEntryRequest(PasswordPolicyOptions):
|
||||
label: str
|
||||
length: int
|
||||
username: str | None = None
|
||||
url: str | None = None
|
||||
|
||||
|
||||
class GeneratePasswordRequest(PasswordPolicyOptions):
|
||||
length: int
|
||||
|
||||
|
||||
class GeneratePasswordResponse(BaseModel):
|
||||
password: str
|
||||
|
||||
|
||||
class VaultService:
|
||||
"""Thread-safe wrapper around vault operations."""
|
||||
|
||||
|
@@ -5,6 +5,8 @@ import pytest
|
||||
from seedpass import api
|
||||
from test_api import client
|
||||
from helpers import dummy_nostr_client
|
||||
import string
|
||||
from seedpass.core.password_generation import PasswordGenerator, PasswordPolicy
|
||||
from nostr.client import NostrClient, DEFAULT_RELAYS
|
||||
|
||||
|
||||
@@ -401,3 +403,55 @@ def test_relay_management_endpoints(client, dummy_nostr_client, monkeypatch):
|
||||
assert res.status_code == 200
|
||||
assert called.get("init") is True
|
||||
assert api._pm.nostr_client.relays == list(DEFAULT_RELAYS)
|
||||
|
||||
|
||||
def test_generate_password_no_special_chars(client):
|
||||
cl, token = client
|
||||
|
||||
class DummyEnc:
|
||||
def derive_seed_from_mnemonic(self, mnemonic):
|
||||
return b"\x00" * 32
|
||||
|
||||
class DummyBIP85:
|
||||
def derive_entropy(self, index: int, bytes_len: int, app_no: int = 32) -> bytes:
|
||||
return bytes(range(bytes_len))
|
||||
|
||||
api._pm.password_generator = PasswordGenerator(DummyEnc(), "seed", DummyBIP85())
|
||||
api._pm.parent_seed = "seed"
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
res = cl.post(
|
||||
"/api/v1/password",
|
||||
json={"length": 16, "include_special_chars": False},
|
||||
headers=headers,
|
||||
)
|
||||
assert res.status_code == 200
|
||||
pw = res.json()["password"]
|
||||
assert not any(c in string.punctuation for c in pw)
|
||||
|
||||
|
||||
def test_generate_password_allowed_chars(client):
|
||||
cl, token = client
|
||||
|
||||
class DummyEnc:
|
||||
def derive_seed_from_mnemonic(self, mnemonic):
|
||||
return b"\x00" * 32
|
||||
|
||||
class DummyBIP85:
|
||||
def derive_entropy(self, index: int, bytes_len: int, app_no: int = 32) -> bytes:
|
||||
return bytes((index + i) % 256 for i in range(bytes_len))
|
||||
|
||||
api._pm.password_generator = PasswordGenerator(DummyEnc(), "seed", DummyBIP85())
|
||||
api._pm.parent_seed = "seed"
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
allowed = "@$"
|
||||
res = cl.post(
|
||||
"/api/v1/password",
|
||||
json={"length": 16, "allowed_special_chars": allowed},
|
||||
headers=headers,
|
||||
)
|
||||
assert res.status_code == 200
|
||||
pw = res.json()["password"]
|
||||
specials = [c for c in pw if c in string.punctuation]
|
||||
assert specials and all(c in allowed for c in specials)
|
||||
|
Reference in New Issue
Block a user