Skip to content

Commit 2fdc553

Browse files
aaunario-keepersk-keeper
authored andcommitted
share-record: add --contacts-only option for allowing search for contacts with alternate domains; minor fix for secrets app share
1 parent cf2ab28 commit 2fdc553

File tree

4 files changed

+62
-4
lines changed

4 files changed

+62
-4
lines changed

keepercommander/api.py

+27
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .error import KeeperApiError
3131
from .params import KeeperParams, PublicKeys, LAST_RECORD_UID
3232
from .proto import APIRequest_pb2, record_pb2, enterprise_pb2
33+
from .proto.record_pb2 import GetShareObjectsRequest, GetShareObjectsResponse
3334
from .record import Record
3435
from .recordv3 import RecordV3
3536
from .shared_folder import SharedFolder
@@ -293,6 +294,32 @@ def get_shared_folder(params, shared_folder_uid): # type: (KeeperParams, str)
293294
sf.load(cached_sf, cached_sf['revision'])
294295
return sf
295296

297+
def get_share_objects(params): # type: (KeeperParams) -> Dict[str, Dict[str, Any]]
298+
if not params.share_object_cache:
299+
rq = GetShareObjectsRequest()
300+
rs = communicate_rest(params, rq, 'vault/get_share_objects', rs_type=GetShareObjectsResponse)
301+
users_by_type = dict(
302+
relationship=rs.shareRelationships,
303+
family= rs.shareFamilyUsers,
304+
enterprise=rs.shareEnterpriseUsers,
305+
mc=rs.shareMCEnterpriseUsers,
306+
)
307+
get_users = lambda rs_data, cat: {su.username: dict(name=su.fullname, is_sa=su.isShareAdmin, enterprise_id=su.enterpriseId, status=su.status, category=cat) for su in rs_data}
308+
users = [get_users(users, cat) for cat, users in users_by_type.items()]
309+
users = list(itertools.chain.from_iterable(users))
310+
enterprises = {se.enterpriseId: se.enterprisename for se in rs.shareEnterpriseNames}
311+
get_teams = lambda rs_data: {utils.base64_url_encode(st.teamUid): dict(name=st.teamname, enterprise_id=st.enterpriseId) for st in rs_data}
312+
teams = get_teams(rs.shareTeams)
313+
teams_mc = get_teams(rs.shareMCTeams)
314+
315+
share_objects = dict(
316+
users=users,
317+
enterprises=enterprises,
318+
teams=teams,
319+
teams_mc=teams_mc
320+
)
321+
params.share_object_cache = share_objects
322+
return params.share_object_cache
296323

297324
def load_user_public_keys(params, emails, send_invites=False): # type: (KeeperParams, List[str], bool) -> Optional[List[str]]
298325
s = set((x.casefold() for x in emails))

keepercommander/commands/ksm.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -332,13 +332,14 @@ def share_app(params, app_name_or_uid, email, is_admin=False, unshare=False):
332332
return
333333
app_uid = app_rec.get('record_uid', '')
334334
app_info = KSMCommand.get_app_info(params, app_uid)
335-
share_action = unshare and 'remove' or 'grant'
335+
sf_action = 'remove' if unshare else 'grant'
336+
sr_action = 'revoke' if unshare else 'grant'
336337
sf_perm_keys = ('manage_users', 'manage_records', 'can_edit', 'can_share')
337338
sf_perms = {k: is_admin and not unshare and 'on' or 'off' for k in sf_perm_keys}
338339
rec_perms = dict(can_edit=is_admin and not unshare, can_share=is_admin and not unshare)
339340
users = [email]
340-
share_folder_args = dict(**sf_perms, action=share_action, user=users)
341-
share_rec_args = dict(**rec_perms, action=share_action, email=users)
341+
share_folder_args = dict(**sf_perms, action=sf_action, user=users)
342+
share_rec_args = dict(**rec_perms, action=sr_action, email=users)
342343

343344
from keepercommander.commands.register import ShareRecordCommand
344345
from keepercommander.commands.register import ShareFolderCommand

keepercommander/commands/register.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from . import base
2929
from .base import dump_report_data, field_to_title, fields_to_titles, raise_parse_exception, suppress_exit, Command, \
30-
GroupCommand, FolderMixin
30+
GroupCommand, FolderMixin, user_choice
3131
from .helpers.record import get_record_uids
3232
from .helpers.timeout import parse_timeout
3333
from .record import RecordRemoveCommand
@@ -42,6 +42,7 @@
4242
from ..sox.sox_types import Record
4343
from ..subfolder import BaseFolderNode, SharedFolderNode, SharedFolderFolderNode, try_resolve_path, get_folder_path, \
4444
get_folder_uids, get_contained_record_uids
45+
from ..utils import is_email
4546

4647

4748
def register_commands(commands):
@@ -71,6 +72,9 @@ def register_command_info(aliases, command_info):
7172

7273
share_record_parser = argparse.ArgumentParser(prog='share-record', description='Change the sharing permissions of an individual record')
7374
share_record_parser.add_argument('-e', '--email', dest='email', action='append', required=True, help='account email')
75+
share_record_parser.add_argument('--contacts-only', action='store_true', help="Share only to known targets; Allows routing to"
76+
" alternate domains with matching usernames if needed")
77+
share_record_parser.add_argument('-f', '--force', action='store_true', help='Skip confirmation prompts')
7478
share_record_parser.add_argument('-a', '--action', dest='action', choices=['grant', 'revoke', 'owner', 'cancel'],
7579
default='grant', action='store', help='user share action. \'grant\' if omitted')
7680
share_record_parser.add_argument('-s', '--share', dest='can_share', action='store_true', help='can re-share record')
@@ -651,7 +655,31 @@ def execute(self, params, **kwargs):
651655
raise CommandError('share-record', '\'email\' parameter is missing')
652656

653657
dry_run = kwargs.get('dry_run') is True
658+
force = kwargs.get('force') is True
654659
action = kwargs.get('action') or 'grant'
660+
use_contacts = kwargs.get('contacts_only')
661+
662+
def get_contact(user, contacts):
663+
get_username = lambda addr: next(iter(addr.split('@')), '')
664+
matches = [c for c in contacts if get_username(user) == get_username(c)]
665+
if len(matches) > 1:
666+
raise CommandError('More than 1 matching usernames found. Aborting')
667+
return next(iter(matches), None)
668+
669+
if use_contacts:
670+
known_users = api.get_share_objects(params).get('users', {})
671+
is_unknown = lambda e: e not in known_users and is_email(e)
672+
unknowns = [e for e in emails if is_unknown(e)]
673+
if unknowns:
674+
username_map = {e: get_contact(e, known_users) for e in unknowns}
675+
table = [[k, v] for k, v in username_map.items()]
676+
logging.info(f'{len(unknowns)} unrecognized share recipient(s) and closest matching contact(s)')
677+
dump_report_data(table, ['Username', 'From Contacts'])
678+
confirmed = force or user_choice('\tReplace with known matching contact(s)?', 'yn', default='n') == 'y'
679+
if confirmed:
680+
good_emails = [e for e in emails if e not in unknowns]
681+
replacements = [e for e in username_map.values() if e]
682+
emails = [*good_emails, *replacements]
655683

656684
if action == 'cancel':
657685
answer = base.user_choice(

keepercommander/params.py

+2
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def __init__(self, config_filename='', config=None, server='keepersecurity.com')
115115
self.non_shared_data_cache = {}
116116
self.shared_folder_cache = {}
117117
self.team_cache = {}
118+
self.share_object_cache = {}
118119
self.record_link_cache = {}
119120
self.record_rotation_cache = {}
120121
self.record_owner_cache = {} # type: Dict[str, RecordOwner]
@@ -189,6 +190,7 @@ def clear_session(self):
189190
self.non_shared_data_cache.clear()
190191
self.shared_folder_cache.clear()
191192
self.team_cache.clear()
193+
self.share_object_cache.clear()
192194
self.record_link_cache.clear()
193195
self.record_rotation_cache.clear()
194196
self.record_owner_cache.clear()

0 commit comments

Comments
 (0)