Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion keepercommander/commands/record_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
record_add_parser.add_argument('-f', '--force', dest='force', action='store_true', help='ignore warnings')
record_add_parser.add_argument('-t', '--title', dest='title', action='store', help='record title')
record_add_parser.add_argument('-rt', '--record-type', dest='record_type', action='store', help='record type')
record_add_parser.add_argument('--labels', dest='labels', action='store', choices=['on', 'off'],
help='label fields in standard record-type definition. "on" (default) keeps legacy '
'labels; "off" omits them. "off" affects only RT-definition fields without their '
'own label; RT-definition custom labels and explicitly provided labels are preserved.')
record_add_parser.add_argument('-n', '--notes', dest='notes', action='store', help='record notes')
record_add_parser.add_argument('--folder', dest='folder', action='store',
help='folder name or UID to store record')
Expand Down Expand Up @@ -865,11 +869,15 @@ def execute(self, params, **kwargs):
raise CommandError('record-add', f'Record type \"{record_type}\" cannot be found.')
record = vault.TypedRecord()
record.type_name = record_type
omit_labels = (kwargs.get('labels') or 'on').lower() == 'off'
for rf in rt_fields:
ref = rf.get('$ref')
if not ref:
continue
label = rf.get('label') or ref
# Use the label from the record-type definition when present (both modes).
# When the definition has none: legacy ("on") falls back to the field type;
# "off" leaves it empty so the redundant type-name label is omitted (matches Vault UI).
label = rf.get('label') or ('' if omit_labels else ref)
required = rf.get('required', False)
default_value = None
if ref == 'appFiller':
Expand Down
3 changes: 2 additions & 1 deletion keepercommander/vault_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,9 +447,10 @@ def extract_typed_field(field): # type: (vault.TypedField) -> dict
field_values.append(value)
result = {
'type': field_type,
'label': field.label or '',
'value': field_values
}
if field.label:
result['label'] = field.label
if field.required is True:
result['required'] = True
return result
Expand Down
74 changes: 73 additions & 1 deletion unit-tests/test_command_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from data_vault import get_synced_params, VaultEnvironment
from helper import KeeperApiHelper

from keepercommander import api, utils, crypto, attachment, vault
from keepercommander import api, utils, crypto, attachment, vault, vault_extensions
from keepercommander.commands import record, record_edit
from keepercommander.error import CommandError

Expand Down Expand Up @@ -97,6 +97,78 @@ def artf(p, r, f):
self.assertIsNotNone(field)
self.assertEqual(field.get_default_value(str), 'BBB')

def _run_add(self, params, **kwargs):
"""Run record-add with the API mocked; return the TypedRecord that would be saved."""
cmd = record_edit.RecordAddCommand()
captured = {}
with mock.patch('keepercommander.api.sync_down'), \
mock.patch('keepercommander.record_management.add_record_to_folder') as ar:
def artf(p, r, f):
captured['record'] = r
r.record_uid = utils.generate_uid()
ar.side_effect = artf
cmd.execute(params, **kwargs)
return captured.get('record')

# RT schema with a label-less field (synthesized fallback) and one with a real definition label.
_RT_SCHEMA = [
{"$ref": "login"}, # no label in RT definition
{"$ref": "password"}, # no label in RT definition
{"$ref": "script", "label": "rotationScripts"}, # real RT-definition label
]

def test_add_command_labels_default_is_legacy(self):
# No --labels (and explicit --labels=on): fields with no label in the RT definition fall
# back to the field type as the label; real definition labels are kept.
params = get_synced_params()
for labels in (None, 'on'):
kwargs = dict(force=True, title='L', record_type='login',
fields=['login=user@company.com', 'password=secret'])
if labels is not None:
kwargs['labels'] = labels
with mock.patch.object(record_edit.RecordAddCommand, 'get_record_type_fields',
return_value=list(self._RT_SCHEMA)):
record = self._run_add(params, **kwargs)
self.assertIsInstance(record, vault.TypedRecord)
self.assertEqual(record.get_typed_field('login').label, 'login') # synthesized
self.assertEqual(record.get_typed_field('password').label, 'password') # synthesized
self.assertEqual(record.get_typed_field('script').label, 'rotationScripts') # real, kept
data = vault_extensions.extract_typed_record_data(record)
login_data = next(x for x in data['fields'] if x['type'] == 'login')
self.assertEqual(login_data.get('label'), 'login')

def test_add_command_labels_off_matches_vault(self):
# --labels=off: omit the synthesized type-name labels (login, password) but KEEP real
# RT-definition labels (script->rotationScripts), matching the Vault UI; an explicitly
# provided cmdline label is always preserved.
params = get_synced_params()
with mock.patch.object(record_edit.RecordAddCommand, 'get_record_type_fields',
return_value=list(self._RT_SCHEMA)):
record = self._run_add(params, force=True, title='L', record_type='login', labels='off',
fields=['login=user@company.com', 'text.MyLabel=val'])
self.assertIsInstance(record, vault.TypedRecord)
self.assertFalse(record.get_typed_field('login').label) # synthesized -> dropped
self.assertFalse(record.get_typed_field('password').label) # synthesized -> dropped
self.assertEqual(record.get_typed_field('script').label, 'rotationScripts') # real -> kept
self.assertEqual(record.get_typed_field('text', 'MyLabel').label, 'MyLabel') # explicit -> kept

data = vault_extensions.extract_typed_record_data(record)
login_d = next(x for x in data['fields'] if x['type'] == 'login')
script_d = next(x for x in data['fields'] if x['type'] == 'script')
self.assertNotIn('label', login_d) # synthesized label omitted
self.assertEqual(script_d.get('label'), 'rotationScripts') # real label serialized
custom_d = next(x for x in data['custom'] if x.get('label') == 'MyLabel')
self.assertEqual(custom_d['label'], 'MyLabel')

def test_extract_typed_field_omits_empty_label(self):
# Serializer omits the label key when falsy; keeps it when present.
self.assertNotIn('label', vault_extensions.extract_typed_field(
vault.TypedField.new_field('login', 'admin', '')))
self.assertNotIn('label', vault_extensions.extract_typed_field(
vault.TypedField.new_field('login', 'admin', None)))
kept = vault_extensions.extract_typed_field(vault.TypedField.new_field('text', 'v', 'MyLabel'))
self.assertEqual(kept.get('label'), 'MyLabel')

def test_remove_command_from_root(self):
params = get_synced_params()
cmd = record.RecordRemoveCommand()
Expand Down
Loading