mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +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"),
|
length: int = typer.Option(12, "--length"),
|
||||||
username: Optional[str] = typer.Option(None, "--username"),
|
username: Optional[str] = typer.Option(None, "--username"),
|
||||||
url: Optional[str] = typer.Option(None, "--url"),
|
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:
|
) -> None:
|
||||||
"""Add a new password entry and output its index."""
|
"""Add a new password entry and output its index."""
|
||||||
service = _get_entry_service(ctx)
|
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))
|
typer.echo(str(index))
|
||||||
|
|
||||||
|
|
||||||
@@ -708,10 +747,52 @@ def fingerprint_switch(ctx: typer.Context, fingerprint: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@util_app.command("generate-password")
|
@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."""
|
"""Generate a strong password."""
|
||||||
service = _get_util_service(ctx)
|
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)
|
typer.echo(password)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -9,7 +9,8 @@ allow easy validation and documentation.
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import List, Optional, Dict
|
from typing import List, Optional, Dict, Any
|
||||||
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -283,9 +284,42 @@ class EntryService:
|
|||||||
length: int,
|
length: int,
|
||||||
username: str | None = None,
|
username: str | None = None,
|
||||||
url: 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:
|
) -> int:
|
||||||
with self._lock:
|
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()
|
self._manager.start_background_vault_sync()
|
||||||
return idx
|
return idx
|
||||||
|
|
||||||
@@ -557,9 +591,50 @@ class UtilityService:
|
|||||||
self._manager = manager
|
self._manager = manager
|
||||||
self._lock = Lock()
|
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:
|
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:
|
def verify_checksum(self) -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
|
@@ -22,9 +22,32 @@ runner = CliRunner()
|
|||||||
"user",
|
"user",
|
||||||
"--url",
|
"--url",
|
||||||
"https://example.com",
|
"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"),
|
("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",
|
"1",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@@ -321,18 +321,54 @@ def test_nostr_sync(monkeypatch):
|
|||||||
def test_generate_password(monkeypatch):
|
def test_generate_password(monkeypatch):
|
||||||
called = {}
|
called = {}
|
||||||
|
|
||||||
def gen_pw(length):
|
def gen_pw(length, **kwargs):
|
||||||
called["length"] = length
|
called["length"] = length
|
||||||
|
called["kwargs"] = kwargs
|
||||||
return "secretpw"
|
return "secretpw"
|
||||||
|
|
||||||
pm = SimpleNamespace(
|
monkeypatch.setattr(
|
||||||
password_generator=SimpleNamespace(generate_password=gen_pw),
|
cli,
|
||||||
select_fingerprint=lambda fp: None,
|
"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 result.exit_code == 0
|
||||||
assert called.get("length") == 12
|
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
|
assert "secretpw" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
@@ -370,8 +406,9 @@ def test_entry_list_passes_fingerprint(monkeypatch):
|
|||||||
def test_entry_add(monkeypatch):
|
def test_entry_add(monkeypatch):
|
||||||
called = {}
|
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["args"] = (label, length, username, url)
|
||||||
|
called["kwargs"] = kwargs
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
pm = SimpleNamespace(
|
pm = SimpleNamespace(
|
||||||
@@ -392,11 +429,35 @@ def test_entry_add(monkeypatch):
|
|||||||
"bob",
|
"bob",
|
||||||
"--url",
|
"--url",
|
||||||
"ex.com",
|
"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 result.exit_code == 0
|
||||||
assert "2" in result.stdout
|
assert "2" in result.stdout
|
||||||
assert called["args"] == ("Example", 16, "bob", "ex.com")
|
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):
|
def test_entry_modify(monkeypatch):
|
||||||
|
Reference in New Issue
Block a user