Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Toml import/export option #72

Closed
wants to merge 15 commits into from
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: CI

on: [push, pull_request]
on: [push, pull_request,workflow_dispatch]

jobs:
build:
Expand Down
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"githubPullRequests.ignoredPullRequestBranches": [
"main"
]
}
1 change: 1 addition & 0 deletions sample/export.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
'tabulate',
'passwordgenerator',
'SQLAlchemy==1.4.41',
'sqlcipher3==0.4.5'
'sqlcipher3==0.4.5',
'tomli >= 1.1.0 ; python_version < "3.11"',
'tomli-w'

], # external dependencies
entry_points={
'console_scripts': [
Expand Down
160 changes: 160 additions & 0 deletions src/unittest/views/test_toml_import_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
from unittest.mock import patch
import tempfile
try:
import tomllib as toml
except ModuleNotFoundError:
import tomli as toml

from ..base import BaseTest
from ...views import import_export
from ...models.Secret import SecretModel
from ...models.Category import CategoryModel
from ...modules.carry import global_scope


class Test(BaseTest):

def setUp(self):
# Create some secrets
secret_1 = SecretModel(name='Paypal',
url='https://www.paypal.com',
login='[email protected]',
password='password123',
notes='Some notes',
category_id=1)
self.session.add(secret_1)
secret_2 = SecretModel(name='Gmail',
url='https://www.gmail.com',
login='[email protected]',
password='password;123',
notes='Some notes\nsome more notes')
self.session.add(secret_2)
secret_3 = SecretModel(name='eBay',
url='https://www.ebay.com',
login='[email protected]',
password='123password',
notes='Some notes')
self.session.add(secret_3)

# Add a category as well
category_1 = CategoryModel(name='My category 1')
self.session.add(category_1)

self.session.commit()

def test_import_(self):
with patch('builtins.input', return_value='y'):
with patch('getpass.getpass', return_value=self.secret_key):
self.assertTrue(import_export.import_(
format_='toml',
path='sample/export.toml'))

def test_import_2(self):
self.assertRaises(ValueError, import_export.import_,
format_='some_invalid_format', path='/tmp/')

def test_export_(self):
# Create a temporary file
file_ = tempfile.NamedTemporaryFile(delete=False)

with patch('getpass.getpass', return_value=self.secret_key):
self.assertTrue(import_export.export_(
format_='toml', path=file_.name))

def test_export_2(self):
self.assertRaises(ValueError, import_export.export_,
format_='some_invalid_format', path='/tmp/')

def test_export_to_toml(self):
# Create a temporary file
file_ = tempfile.NamedTemporaryFile(delete=False)

with patch('getpass.getpass', return_value=self.secret_key):
self.assertTrue(import_export.export_to_toml(file_.name))

# Try read the file
with open(file_.name, mode='r') as f:
# Get content
content = f.read()

# Decode content
content = toml.loads(content)

# The content should be a list
self.assertIsInstance(content, list)

# Each item should be a dict
for item in content:
self.assertIsInstance(item, dict)

def test_import_from_toml(self):
# Test basic import
global_scope['enc'] = None
with patch('builtins.input', return_value='y'):
with patch('getpass.getpass', return_value=self.secret_key):
self.assertTrue(import_export.import_from_toml(
'sample/export.toml'))

def test_import_from_toml_2(self):
# Test import when vault is already unlocked (in unit test base)
with patch('builtins.input', return_value='y'):
self.assertTrue(import_export.import_from_toml(
'sample/export.toml'))

def test_import_from_toml_3(self):
# Test import with confirmation denied
with patch('builtins.input', return_value='n'):
with patch('getpass.getpass', return_value=self.secret_key):
self.assertFalse(import_export.import_from_toml(
'sample/export.toml'))

def test_to_table(self):
self.assertIsInstance(import_export.to_table(
[['name', 'url', 'login', 'password', 'notes', 'category']]), str)

def test_to_table_2(self):
self.assertIsInstance(import_export.to_table([]), str)
self.assertEqual(import_export.to_table([]), 'Empty!')

def test_read_file(self):
# Write a temporary file
file_ = tempfile.NamedTemporaryFile(delete=False)
file_.write(b'Some file content')
file_.close()

self.assertEqual(import_export.read_file(
file_.name), 'Some file content')

def test_read_file_2(self):
# Create a temporary directory
dir_ = tempfile.TemporaryDirectory()

self.assertRaises(SystemExit, import_export.read_file,
dir_.name + '/non/existent')

# Cleanup dir
dir_.cleanup()

def test_save_file(self):
# Create a temporary directory
dir_ = tempfile.TemporaryDirectory()

self.assertTrue(import_export.save_file(
dir_.name + '/file', 'some content'))

# Cleanup dir
dir_.cleanup()

def test_save_file_2(self):
# Create a temporary directory
dir_ = tempfile.TemporaryDirectory()

self.assertFalse(import_export.save_file(
dir_.name + '/non/existent/file', 'some content'))

# Cleanup dir
dir_.cleanup()

def test_unlock(self):
with patch('getpass.getpass', return_value=self.secret_key):
self.assertTrue(import_export.unlock())
2 changes: 1 addition & 1 deletion src/vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def main():
parser.add_argument("-x", "--export", type=str,
help="File to export credentials to")
parser.add_argument("-f", "--file_format", type=str, help="Import/export file format (default: 'json')",
choices=['json'], nargs='?', default='json')
choices=['json', 'toml'], nargs='?', default='json')
parser.add_argument("-e", "--erase_vault", action='store_true',
help="Erase the vault and config file")
args = parser.parse_args()
Expand Down
64 changes: 62 additions & 2 deletions src/views/import_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import sys
import json

try:
import tomllib as toml
except ModuleNotFoundError:
import tomli as toml
import tomli_w
from tabulate import tabulate

from . import menu, secrets, categories
Expand Down Expand Up @@ -31,6 +35,8 @@ def import_(format_, path):

if format_ == 'json':
return import_from_json(path)
elif format_ == 'toml':
return import_from_toml(path)
else:
raise ValueError('%s is not a supported file format' % (format_))

Expand All @@ -42,6 +48,8 @@ def export_(format_, path):

if format_ == 'json':
return export_to_json(path)
elif format_ == 'toml':
pass
else:
raise ValueError('%s is not a supported file format' % (format_))

Expand Down Expand Up @@ -69,12 +77,36 @@ def export_to_json(path):
return save_file(path, json.dumps(out))


def export_to_toml(path):
"""
Export to a Toml file
"""

# Ask user to unlock the vault
unlock()

# Create dict of secrets
out = []
for secret in secrets.all():
out.append({
'name': secret.name,
'url': secret.url,
'login': secret.login,
'password': secret.password,
'notes': secret.notes,
'category': categories.get_name(secret.category_id),
})

return save_file(path, tomli_w.dumps(out))


def import_from_json(path=None, rows=None):
"""
Import a Json file
"""

# Ask user to unlock the vault (except if its already unlocked in migration)
# Ask user to unlock the vault (except if its already unlocked in
# migration)
if not isinstance(global_scope['enc'], Encryption):
unlock()

Expand All @@ -99,6 +131,34 @@ def import_from_json(path=None, rows=None):
return False


def import_from_toml(path=None, rows=None):
# Ask user to unlock the vault (except if its already unlocked in
# migration)
if not isinstance(global_scope['enc'], Encryption):
unlock()

if not rows: # If importing from a file
# Read content
content = read_file(path)

# Decode toml
rows = toml.loads(content)

# User view of items
print("The following items will be imported:")
print()
print(to_table(
[[row['name'], row['url'], row['login'], row['category']] for row in rows]))
print()

if confirm('Confirm import?', False):

return import_items(rows)
else:
print("Import cancelled.")
return False


def import_items(rows):
"""
Import items at the following format:
Expand Down