mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-07 06:48:52 +00:00
Merge pull request #691 from PR0M3TH3AN/codex/update-entry_add-command-options-and-tests
Add password policy options
This commit is contained in:
@@ -209,10 +209,49 @@ def entry_add(
|
||||
length: int = typer.Option(12, "--length"),
|
||||
username: Optional[str] = typer.Option(None, "--username"),
|
||||
url: Optional[str] = typer.Option(None, "--url"),
|
||||
no_special: bool = typer.Option(
|
||||
False, "--no-special", help="Exclude special characters", is_flag=True
|
||||
),
|
||||
allowed_special_chars: Optional[str] = typer.Option(
|
||||
None, "--allowed-special-chars", help="Explicit set of special characters"
|
||||
),
|
||||
special_mode: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--special-mode",
|
||||
help="Special character mode",
|
||||
),
|
||||
exclude_ambiguous: bool = typer.Option(
|
||||
False,
|
||||
"--exclude-ambiguous",
|
||||
help="Exclude ambiguous characters",
|
||||
is_flag=True,
|
||||
),
|
||||
min_uppercase: Optional[int] = typer.Option(None, "--min-uppercase"),
|
||||
min_lowercase: Optional[int] = typer.Option(None, "--min-lowercase"),
|
||||
min_digits: Optional[int] = typer.Option(None, "--min-digits"),
|
||||
min_special: Optional[int] = typer.Option(None, "--min-special"),
|
||||
) -> None:
|
||||
"""Add a new password entry and output its index."""
|
||||
service = _get_entry_service(ctx)
|
||||
index = service.add_entry(label, length, username, url)
|
||||
kwargs = {}
|
||||
if no_special:
|
||||
kwargs["include_special_chars"] = False
|
||||
if allowed_special_chars is not None:
|
||||
kwargs["allowed_special_chars"] = allowed_special_chars
|
||||
if special_mode is not None:
|
||||
kwargs["special_mode"] = special_mode
|
||||
if exclude_ambiguous:
|
||||
kwargs["exclude_ambiguous"] = True
|
||||
if min_uppercase is not None:
|
||||
kwargs["min_uppercase"] = min_uppercase
|
||||
if min_lowercase is not None:
|
||||
kwargs["min_lowercase"] = min_lowercase
|
||||
if min_digits is not None:
|
||||
kwargs["min_digits"] = min_digits
|
||||
if min_special is not None:
|
||||
kwargs["min_special"] = min_special
|
||||
|
||||
index = service.add_entry(label, length, username, url, **kwargs)
|
||||
typer.echo(str(index))
|
||||
|
||||
|
||||
@@ -708,10 +747,52 @@ def fingerprint_switch(ctx: typer.Context, fingerprint: str) -> None:
|
||||
|
||||
|
||||
@util_app.command("generate-password")
|
||||
def generate_password(ctx: typer.Context, length: int = 24) -> None:
|
||||
def generate_password(
|
||||
ctx: typer.Context,
|
||||
length: int = 24,
|
||||
no_special: bool = typer.Option(
|
||||
False, "--no-special", help="Exclude special characters", is_flag=True
|
||||
),
|
||||
allowed_special_chars: Optional[str] = typer.Option(
|
||||
None, "--allowed-special-chars", help="Explicit set of special characters"
|
||||
),
|
||||
special_mode: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--special-mode",
|
||||
help="Special character mode",
|
||||
),
|
||||
exclude_ambiguous: bool = typer.Option(
|
||||
False,
|
||||
"--exclude-ambiguous",
|
||||
help="Exclude ambiguous characters",
|
||||
is_flag=True,
|
||||
),
|
||||
min_uppercase: Optional[int] = typer.Option(None, "--min-uppercase"),
|
||||
min_lowercase: Optional[int] = typer.Option(None, "--min-lowercase"),
|
||||
min_digits: Optional[int] = typer.Option(None, "--min-digits"),
|
||||
min_special: Optional[int] = typer.Option(None, "--min-special"),
|
||||
) -> None:
|
||||
"""Generate a strong password."""
|
||||
service = _get_util_service(ctx)
|
||||
password = service.generate_password(length)
|
||||
kwargs = {}
|
||||
if no_special:
|
||||
kwargs["include_special_chars"] = False
|
||||
if allowed_special_chars is not None:
|
||||
kwargs["allowed_special_chars"] = allowed_special_chars
|
||||
if special_mode is not None:
|
||||
kwargs["special_mode"] = special_mode
|
||||
if exclude_ambiguous:
|
||||
kwargs["exclude_ambiguous"] = True
|
||||
if min_uppercase is not None:
|
||||
kwargs["min_uppercase"] = min_uppercase
|
||||
if min_lowercase is not None:
|
||||
kwargs["min_lowercase"] = min_lowercase
|
||||
if min_digits is not None:
|
||||
kwargs["min_digits"] = min_digits
|
||||
if min_special is not None:
|
||||
kwargs["min_special"] = min_special
|
||||
|
||||
password = service.generate_password(length, **kwargs)
|
||||
typer.echo(password)
|
||||
|
||||
|
||||
|
@@ -9,7 +9,8 @@ allow easy validation and documentation.
|
||||
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import List, Optional, Dict
|
||||
from typing import List, Optional, Dict, Any
|
||||
import dataclasses
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -283,9 +284,42 @@ class EntryService:
|
||||
length: int,
|
||||
username: str | None = None,
|
||||
url: str | None = None,
|
||||
*,
|
||||
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,
|
||||
) -> int:
|
||||
with self._lock:
|
||||
idx = self._manager.entry_manager.add_entry(label, length, username, url)
|
||||
kwargs: dict[str, Any] = {}
|
||||
if include_special_chars is not None:
|
||||
kwargs["include_special_chars"] = include_special_chars
|
||||
if allowed_special_chars is not None:
|
||||
kwargs["allowed_special_chars"] = allowed_special_chars
|
||||
if special_mode is not None:
|
||||
kwargs["special_mode"] = special_mode
|
||||
if exclude_ambiguous is not None:
|
||||
kwargs["exclude_ambiguous"] = exclude_ambiguous
|
||||
if min_uppercase is not None:
|
||||
kwargs["min_uppercase"] = min_uppercase
|
||||
if min_lowercase is not None:
|
||||
kwargs["min_lowercase"] = min_lowercase
|
||||
if min_digits is not None:
|
||||
kwargs["min_digits"] = min_digits
|
||||
if min_special is not None:
|
||||
kwargs["min_special"] = min_special
|
||||
|
||||
idx = self._manager.entry_manager.add_entry(
|
||||
label,
|
||||
length,
|
||||
username,
|
||||
url,
|
||||
**kwargs,
|
||||
)
|
||||
self._manager.start_background_vault_sync()
|
||||
return idx
|
||||
|
||||
@@ -557,9 +591,50 @@ class UtilityService:
|
||||
self._manager = manager
|
||||
self._lock = Lock()
|
||||
|
||||
def generate_password(self, length: int) -> str:
|
||||
def generate_password(
|
||||
self,
|
||||
length: int,
|
||||
*,
|
||||
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,
|
||||
) -> str:
|
||||
with self._lock:
|
||||
return self._manager.password_generator.generate_password(length)
|
||||
pg = self._manager.password_generator
|
||||
base_policy = getattr(pg, "policy", None)
|
||||
overrides: dict[str, Any] = {}
|
||||
if include_special_chars is not None:
|
||||
overrides["include_special_chars"] = include_special_chars
|
||||
if allowed_special_chars is not None:
|
||||
overrides["allowed_special_chars"] = allowed_special_chars
|
||||
if special_mode is not None:
|
||||
overrides["special_mode"] = special_mode
|
||||
if exclude_ambiguous is not None:
|
||||
overrides["exclude_ambiguous"] = exclude_ambiguous
|
||||
if min_uppercase is not None:
|
||||
overrides["min_uppercase"] = int(min_uppercase)
|
||||
if min_lowercase is not None:
|
||||
overrides["min_lowercase"] = int(min_lowercase)
|
||||
if min_digits is not None:
|
||||
overrides["min_digits"] = int(min_digits)
|
||||
if min_special is not None:
|
||||
overrides["min_special"] = int(min_special)
|
||||
|
||||
if base_policy is not None and overrides:
|
||||
pg.policy = dataclasses.replace(
|
||||
base_policy,
|
||||
**{k: overrides[k] for k in overrides if hasattr(base_policy, k)},
|
||||
)
|
||||
try:
|
||||
return pg.generate_password(length)
|
||||
finally:
|
||||
pg.policy = base_policy
|
||||
return pg.generate_password(length)
|
||||
|
||||
def verify_checksum(self) -> None:
|
||||
with self._lock:
|
||||
|
@@ -22,9 +22,32 @@ runner = CliRunner()
|
||||
"user",
|
||||
"--url",
|
||||
"https://example.com",
|
||||
"--no-special",
|
||||
"--allowed-special-chars",
|
||||
"!@",
|
||||
"--special-mode",
|
||||
"safe",
|
||||
"--exclude-ambiguous",
|
||||
"--min-uppercase",
|
||||
"1",
|
||||
"--min-lowercase",
|
||||
"2",
|
||||
"--min-digits",
|
||||
"3",
|
||||
"--min-special",
|
||||
"4",
|
||||
],
|
||||
("Label", 16, "user", "https://example.com"),
|
||||
{},
|
||||
{
|
||||
"include_special_chars": False,
|
||||
"allowed_special_chars": "!@",
|
||||
"special_mode": "safe",
|
||||
"exclude_ambiguous": True,
|
||||
"min_uppercase": 1,
|
||||
"min_lowercase": 2,
|
||||
"min_digits": 3,
|
||||
"min_special": 4,
|
||||
},
|
||||
"1",
|
||||
),
|
||||
(
|
||||
|
@@ -321,18 +321,54 @@ def test_nostr_sync(monkeypatch):
|
||||
def test_generate_password(monkeypatch):
|
||||
called = {}
|
||||
|
||||
def gen_pw(length):
|
||||
def gen_pw(length, **kwargs):
|
||||
called["length"] = length
|
||||
called["kwargs"] = kwargs
|
||||
return "secretpw"
|
||||
|
||||
pm = SimpleNamespace(
|
||||
password_generator=SimpleNamespace(generate_password=gen_pw),
|
||||
select_fingerprint=lambda fp: None,
|
||||
monkeypatch.setattr(
|
||||
cli,
|
||||
"PasswordManager",
|
||||
lambda: SimpleNamespace(select_fingerprint=lambda fp: None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cli, "UtilityService", lambda pm: SimpleNamespace(generate_password=gen_pw)
|
||||
)
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"util",
|
||||
"generate-password",
|
||||
"--length",
|
||||
"12",
|
||||
"--no-special",
|
||||
"--allowed-special-chars",
|
||||
"!@",
|
||||
"--special-mode",
|
||||
"safe",
|
||||
"--exclude-ambiguous",
|
||||
"--min-uppercase",
|
||||
"1",
|
||||
"--min-lowercase",
|
||||
"2",
|
||||
"--min-digits",
|
||||
"3",
|
||||
"--min-special",
|
||||
"4",
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||
result = runner.invoke(app, ["util", "generate-password", "--length", "12"])
|
||||
assert result.exit_code == 0
|
||||
assert called.get("length") == 12
|
||||
assert called.get("kwargs") == {
|
||||
"include_special_chars": False,
|
||||
"allowed_special_chars": "!@",
|
||||
"special_mode": "safe",
|
||||
"exclude_ambiguous": True,
|
||||
"min_uppercase": 1,
|
||||
"min_lowercase": 2,
|
||||
"min_digits": 3,
|
||||
"min_special": 4,
|
||||
}
|
||||
assert "secretpw" in result.stdout
|
||||
|
||||
|
||||
@@ -370,8 +406,9 @@ def test_entry_list_passes_fingerprint(monkeypatch):
|
||||
def test_entry_add(monkeypatch):
|
||||
called = {}
|
||||
|
||||
def add_entry(label, length, username=None, url=None):
|
||||
def add_entry(label, length, username=None, url=None, **kwargs):
|
||||
called["args"] = (label, length, username, url)
|
||||
called["kwargs"] = kwargs
|
||||
return 2
|
||||
|
||||
pm = SimpleNamespace(
|
||||
@@ -392,11 +429,35 @@ def test_entry_add(monkeypatch):
|
||||
"bob",
|
||||
"--url",
|
||||
"ex.com",
|
||||
"--no-special",
|
||||
"--allowed-special-chars",
|
||||
"!@",
|
||||
"--special-mode",
|
||||
"safe",
|
||||
"--exclude-ambiguous",
|
||||
"--min-uppercase",
|
||||
"1",
|
||||
"--min-lowercase",
|
||||
"2",
|
||||
"--min-digits",
|
||||
"3",
|
||||
"--min-special",
|
||||
"4",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "2" in result.stdout
|
||||
assert called["args"] == ("Example", 16, "bob", "ex.com")
|
||||
assert called["kwargs"] == {
|
||||
"include_special_chars": False,
|
||||
"allowed_special_chars": "!@",
|
||||
"special_mode": "safe",
|
||||
"exclude_ambiguous": True,
|
||||
"min_uppercase": 1,
|
||||
"min_lowercase": 2,
|
||||
"min_digits": 3,
|
||||
"min_special": 4,
|
||||
}
|
||||
|
||||
|
||||
def test_entry_modify(monkeypatch):
|
||||
|
Reference in New Issue
Block a user