Browse Source

Dump database for local backup as CLI commands

Fixes https://github.com/lesspass/lesspass/issues/529
tags/mobile-v9.5.3
Guillaume Vincent 3 years ago
parent
commit
1d56d90481
7 changed files with 180 additions and 5 deletions
  1. +37
    -2
      cli/lesspass/cli.py
  2. +102
    -0
      cli/lesspass/connected.py
  3. +7
    -0
      cli/lesspass/core.py
  4. +1
    -1
      cli/lesspass/version.py
  5. +1
    -1
      cli/requirements.txt
  6. +1
    -1
      cli/setup.py
  7. +31
    -0
      cli/tests/test_cli.py

+ 37
- 2
cli/lesspass/cli.py View File

@@ -2,7 +2,6 @@ import argparse
import os

from lesspass import version
from lesspass import name
from lesspass import description

EXAMPLES = """
@@ -27,6 +26,12 @@ copyright:
"""


def _get_config_path():
DEFAULT_XDG_CONFIG_HOME = os.path.join(os.path.expanduser("~"), ".config")
data_home_path = os.environ.get("XDG_CONFIG_HOME", DEFAULT_XDG_CONFIG_HOME)
return os.path.join(data_home_path, "lesspass")


def range_type(value_string):
value = int(value_string)
if value not in range(5, 35 + 1):
@@ -83,7 +88,9 @@ def parse_args(args):
help="copy the password to clipboard",
)
parser.add_argument(
"--exclude", default=None, help="exclude char from generated password",
"--exclude",
default=None,
help="exclude char from generated password",
)
parser.add_argument(
"--no-fingerprint",
@@ -91,6 +98,34 @@ def parse_args(args):
action="store_true",
help="hide visual fingerprint of the master password when you type",
)
config_home_path = _get_config_path()
backup_file = os.path.join(config_home_path, "profiles.json")
parser.add_argument(
"--save",
dest="save_path",
nargs="?",
const=backup_file,
default=None,
help=f"[beta] Save your password profiles. /!\ File not encrypted. Use carefully. (default: {backup_file})",
)
parser.add_argument(
"--load",
dest="load_path",
default=None,
help="[beta] Load your password profiles file",
)
parser.add_argument(
"--config-home-path",
dest="config_home_path",
default=config_home_path,
help=argparse.SUPPRESS,
)
parser.add_argument(
"--url",
dest="url",
default="https://api.lesspass.com/",
help="[beta] LessPass Database URL used by --save and --load command",
)
lowercase_group = parser.add_mutually_exclusive_group()
lowercase_group.add_argument(
"-l",


+ 102
- 0
cli/lesspass/connected.py View File

@@ -0,0 +1,102 @@
import getpass
import json
import os
import sys

import requests

from lesspass.password import generate_password


def _login(config_home_path, url):
os.makedirs(config_home_path, exist_ok=True)
config_path = os.path.join(config_home_path, "config.json")
tokens = None
if os.path.exists(config_path):
with open(config_path, encoding="utf-8") as f:
tokens = json.load(f)

if tokens:
refresh_tokens = requests.post(
f"{url}auth/jwt/refresh/",
json=tokens,
)
if refresh_tokens.status_code == 200:
token_refreshed = refresh_tokens.json()
with open(config_path, "w", encoding="utf-8") as f:
json.dump(token_refreshed, f, ensure_ascii=False, indent=4)
return token_refreshed["access"]

print("LessPass login")
email = input("Email: ")
master_password = getpass.getpass("Master Password: ")
if not email or not master_password:
print("Email and Master Password are mandatory")
sys.exit(1)
default_lesspass_profile = {
"site": "lesspass.com",
"login": email,
"lowercase": True,
"uppercase": True,
"digits": True,
"symbols": True,
"length": 16,
"counter": 1,
}
encrypted_password = generate_password(default_lesspass_profile, master_password)
r = requests.post(
f"{url}auth/jwt/create/",
json={"email": email, "password": encrypted_password},
)
if r.status_code != 200:
print("Wrong email and/or master password")
sys.exit(1)
tokens = r.json()
with open(config_path, "w", encoding="utf-8") as f:
json.dump(tokens, f, ensure_ascii=False, indent=4)
print(f"Access and refresh tokens saved in {config_path}")
return tokens["access"]


def save_password_profiles(config_home_path, url, backup_path):
token = _login(config_home_path, url)
r = requests.get(f"{url}passwords/", headers={"Authorization": "JWT %s" % token})
with open(backup_path, "w", encoding="utf-8") as f:
json.dump(r.json(), f, ensure_ascii=False, indent=4)

print(f"Password profiles saved in {backup_path}")


def load_password_profiles(config_home_path, url, backup_path):
token = _login(config_home_path, url)
with open(backup_path, encoding="utf-8") as f:
data = json.load(f)

for password_profile in data["results"]:
get_password_profiles = requests.get(
f"{url}passwords/{password_profile['id']}/",
headers={"Authorization": "JWT %s" % token},
)
if get_password_profiles.status_code == 200:
print(
f"Password profile for site {password_profile['site']} and login {password_profile['login']} already exists. Skipping."
)
elif get_password_profiles.status_code == 404:
create_password = requests.post(
f"{url}passwords/",
json=password_profile,
headers={"Authorization": "JWT %s" % token},
)
print(create_password.text)
create_password.raise_for_status()
print(
f"Password profile for site {password_profile['site']} and login {password_profile['login']} successfully imported."
)
else:
get_password_profiles.raise_for_status()

get_password_profiles = requests.get(
f"{url}passwords/", headers={"Authorization": "JWT %s" % token}
)
with open(backup_path, "w", encoding="utf-8") as f:
json.dump(get_password_profiles.json(), f, ensure_ascii=False, indent=4)

+ 7
- 0
cli/lesspass/core.py View File

@@ -11,6 +11,7 @@ from lesspass.profile import create_profile
from lesspass.password import generate_password
from lesspass.clipboard import copy, get_system_copy_command
from lesspass.fingerprint import getpass_with_fingerprint
from lesspass.connected import save_password_profiles, load_password_profiles

signal.signal(signal.SIGINT, lambda s, f: sys.exit(0))

@@ -24,6 +25,12 @@ def main(args=sys.argv[1:]):
)
sys.exit(3)

if args.save_path:
return save_password_profiles(args.config_home_path, args.url, args.save_path)

if args.load_path:
return load_password_profiles(args.config_home_path, args.url, args.load_path)

if args.prompt:
if not args.site:
args.site = getpass.getpass("Site: ")


+ 1
- 1
cli/lesspass/version.py View File

@@ -1 +1 @@
__version__ = "10.0.2"
__version__ = "10.1.0"

+ 1
- 1
cli/requirements.txt View File

@@ -1 +1 @@
.

+ 1
- 1
cli/setup.py View File

@@ -16,7 +16,7 @@ setuptools.setup(
description="LessPass stateless password generator",
long_description=long_description,
long_description_content_type="text/markdown",
install_requires=[],
install_requires=["requests"],
entry_points="""
[console_scripts]
lesspass=lesspass.core:main


+ 31
- 0
cli/tests/test_cli.py View File

@@ -118,3 +118,34 @@ class TestParseArgs(unittest.TestCase):
def test_parse_no_fingerprint(self):
self.assertTrue(parse_args(["site", "--no-fingerprint"]).no_fingerprint)
self.assertFalse(parse_args(["site"]).no_fingerprint)

def test_parse_args_save_path(self):
self.assertEqual(
parse_args(["--save", "/tmp/profiles.json"]).save_path, "/tmp/profiles.json"
)
self.assertEqual(parse_args(["site"]).save_path, None)
with patch.dict("os.environ", {"XDG_CONFIG_HOME": "/tmp"}):
self.assertEqual(
parse_args(["--save"]).save_path, "/tmp/lesspass/profiles.json"
)

def test_parse_args_load_path(self):
self.assertEqual(
parse_args(["--load", "/tmp/profiles.json"]).load_path, "/tmp/profiles.json"
)
self.assertEqual(parse_args(["site"]).load_path, None)

def test_parse_args_config_home_path(self):
self.assertTrue(parse_args([]).config_home_path.endswith("/.config/lesspass"))

def test_parse_args_XDG_CONFIG_HOME_env_variable(self):
with patch.dict("os.environ", {"XDG_CONFIG_HOME": "/tmp"}):
self.assertEqual(parse_args([]).config_home_path, "/tmp/lesspass")

def test_parse_args_default_base_url(self):
self.assertEqual(parse_args([]).url, "https://api.lesspass.com/")

def test_parse_args_default_base_url(self):
self.assertEqual(
parse_args(["--url", "https://example.org/"]).url, "https://example.org/"
)

Loading…
Cancel
Save