Skip to content
Merged

Release #1726

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
2 changes: 1 addition & 1 deletion keepercommander/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
# Contact: ops@keepersecurity.com
#

__version__ = '17.2.0'
__version__ = '17.2.1'
2 changes: 1 addition & 1 deletion keepercommander/commands/enterprise_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ def get_enterprise_ids(params, num_ids=1):
def get_node_path(self, params, node_id, omit_root=False):
if self._node_map is None:
self._node_map = {
x['node_id']: (x['data'].get('displayname') or x['name'] or str(x['node_id']) if x.get('parent_id', 0) > 0 else params.enterprise['enterprise_name'], x.get('parent_id', 0))
x['node_id']: (x['data'].get('displayname') or x.get('name') or str(x['node_id']) if x.get('parent_id', 0) > 0 else params.enterprise['enterprise_name'], x.get('parent_id', 0))
for x in params.enterprise['nodes']}
path = ''
node = self._node_map.get(node_id)
Expand Down
21 changes: 14 additions & 7 deletions keepercommander/commands/pedm/pedm_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import copy
import datetime
import fnmatch
import getpass
import json
import logging
import os.path
Expand Down Expand Up @@ -194,13 +195,13 @@ def __init__(self):
azure_parser = subparsers.add_parser('azure', help='Connect via Azure AD')
azure_parser.add_argument('--tenant-id', dest='tenant_id', required=True)
azure_parser.add_argument('--client-id', dest='client_id', required=True)
azure_parser.add_argument('--client-secret', dest='client_secret', required=True)
azure_parser.add_argument('--client-secret', dest='client_secret')
azure_parser.add_argument('--azure-cloud', dest='azure_cloud', choices=['US', 'GOV', 'CN', 'EU'],
help='Azure cloud (AzureCloud, AzureChinaCloud, etc.)')

ad_parser = subparsers.add_parser('ad', help='Connect via Active Directory')
ad_parser.add_argument('--ad-url', dest='ad_url', help='AD LDAP URL (e.g., ldap(s)://<host>)')
ad_parser.add_argument('--ad-user', dest='ad_user', help='AD bind user (DOMAIN\\username or DN)')
ad_parser.add_argument('--ad-url', dest='ad_url', required=True, help='AD LDAP URL (e.g., ldap(s)://<host>)')
ad_parser.add_argument('--ad-user', dest='ad_user', required=True, help='AD bind user (DOMAIN\\username or DN)')
ad_parser.add_argument('--ad-password', dest='ad_password', help='AD password')
ad_parser.add_argument('--group', dest='groups', action='append', help='AD group name or DN (repeatable)')
ad_parser.add_argument('--netbios-domain', dest='use_netbios_domain', action='store_true',
Expand Down Expand Up @@ -334,9 +335,13 @@ def execute(self, context: KeeperParams, **kwargs):
if scim_groups and not isinstance(scim_groups, list):
scim_groups = None

if not ad_url or not ad_user or not ad_password:
raise base.CommandError('AD source requires AD URL, AD User, and AD Password')
if not ad_url or not ad_user:
raise base.CommandError('AD source requires AD URL and AD User')
try:
if not ad_password:
ad_password = getpass.getpass(prompt=f'{ad_user} Password: ', stream=None)
if not ad_password:
raise base.CommandError('Cancelled')
data_source = AdCrmDataSource(ad_url, ad_user, ad_password, scim_groups, use_netbios_domain)
ad_domains = data_source.resolve_domains()
except Exception as e:
Expand All @@ -348,8 +353,10 @@ def execute(self, context: KeeperParams, **kwargs):
tenant_id = kwargs.get('tenant_id')
client_id = kwargs.get('client_id')
client_secret = kwargs.get('client_secret')
if not tenant_id or not client_id or not client_secret:
raise base.CommandError('Azure source requires tenant-id, client-id, and client-secret')
if not tenant_id or not client_id:
raise base.CommandError('Azure source requires tenant-id and client-id')
if not client_secret:
client_secret = getpass.getpass(prompt=f'Azure Client Secret: ', stream=None)
azure_cloud = kwargs.get('azure_cloud')
if isinstance(azure_cloud, str):
azure_cloud = azure_cloud.upper()
Expand Down
11 changes: 7 additions & 4 deletions keepercommander/commands/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -1843,11 +1843,13 @@ def execute(self, params, **kwargs):
rows.append([name, value])
modified = datetime.datetime.fromtimestamp(int(rev['client_modified_time'] / 1000.0))
rows.append(['Modified', modified])
base.dump_report_data(rows, headers=['Name', 'Value'],
title=f'Record Revision V.{revision}', no_header=True, right_align=(0,))
fmt = kwargs.get('format') or ''
return base.dump_report_data(rows, headers=['Name', 'Value'],
title=f'Record Revision V.{revision}', no_header=True, right_align=(0,),
fmt=fmt, filename=kwargs.get('output'))

elif action == 'diff':
count = 5
count = length - 1
current = vault.KeeperRecord.load(params, history[index])
rows = []
while count >= 0 and current:
Expand Down Expand Up @@ -1901,7 +1903,8 @@ def execute(self, params, **kwargs):
lines.append('...')
row[index] = '\n'.join(lines)

base.dump_report_data(rows, headers)
fmt = kwargs.get('format') or ''
return base.dump_report_data(rows, headers, fmt=fmt, filename=kwargs.get('output'))

elif action == 'restore':
if revision == 0:
Expand Down
13 changes: 12 additions & 1 deletion keepercommander/commands/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def register_command_info(aliases, command_info):
record_permission_parser = argparse.ArgumentParser(prog='record-permission', description='Modify the permissions of a record')
record_permission_parser.add_argument('--dry-run', dest='dry_run', action='store_true',
help='Display the permissions changes without committing them')
record_permission_parser.add_argument('--force', dest='force', action='store_true',
record_permission_parser.add_argument('-f', '--force', dest='force', action='store_true',
help='Apply permission changes without any confirmation')
record_permission_parser.add_argument('-R', '--recursive', dest='recursive', action='store_true',
help='Apply permission changes to all sub-folders')
Expand Down Expand Up @@ -750,16 +750,27 @@ def prep_request(params, kwargs): # type: (KeeperParams, Dict[str, Any]) -> Un
raise CommandError('share-record', 'You can transfer ownership to a single account only')

all_users = set((x.casefold() for x in emails))

# Validate email format before attempting to share or send invitations
invalid_emails = [email for email in all_users if not is_email(email)]
if invalid_emails:
raise CommandError('share-record', f'Invalid email format: {", ".join(invalid_emails)}')

invitations_sent = False
if not dry_run and action in ('grant', 'owner'):
invited = api.load_user_public_keys(params, list(all_users), send_invites=True)
if invited:
invitations_sent = True
for email in invited:
logging.warning('Share invitation has been sent to \'%s\'', email)
logging.warning('Please repeat this command when invitation is accepted.')
all_users.difference_update(invited)
all_users.intersection_update(params.key_cache.keys())

if len(all_users) == 0:
if invitations_sent:
# Invitations were sent, this is a success case - return None to indicate no further action needed
return None
raise CommandError('share-record', 'Nothing to do.')

can_edit = kwargs.get('can_edit') or False
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1308,7 +1308,7 @@ def execute(self, params, **kwargs):
elif product_type_id in (11, 12):
plan = 'Keeper MSP'
elif product_type_id == 8:
plan = 'MC ' + 'Enterprise' if tier == 1 else 'Business'
plan = 'MC ' + ('Enterprise' if tier == 1 else 'Business')
else:
plan = 'Unknown'
if product_type_id in (5, 10, 12):
Expand Down
1 change: 1 addition & 0 deletions keepercommander/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
('password_rotation', 'Password Rotation', False, 'Rotation'),
('remote_browser_isolation', 'Remote Browser Isolation', False, 'Browser Isolation'),
('privileged_access_manager', 'Privileged Access Manager (PAM)', True, 'PAM'),
('keeper_endpoint_privilege_manager', 'Keeper Endpoint Privilege Manager (KEPM)', True, 'KEPM'),
]


Expand Down
1 change: 0 additions & 1 deletion keepercommander/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ def unload_ec_public_key(public_key):
return public_key.public_bytes(encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint)


def encrypt_aes_v1(data, key, iv=None, use_padding=True):
iv = iv or get_random_bytes(16)
cipher = Cipher(AES(key), CBC(iv), backend=_CRYPTO_BACKEND)
Expand Down
61 changes: 60 additions & 1 deletion keepercommander/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,23 @@ def __init__(self, server='https://keepersecurity.com/api/v2/', locale='en_US'):
self.server_base = server
self.transmission_key = None
self.__server_key_id = 7
self.__qrc_key_id = -1 # -1 = not determined, None = not available
self.locale = locale
self.__store_server_key = False
self.proxies = None
self._certificate_check = True
self.fail_on_throttle = False
self.client_ec_private_key = None # EC private key for QRC ECDH exchange

def reset_qrc_key(self):
"""Reset QRC key ID to re-determine on next access (called on server change or logout)"""
self.__qrc_key_id = -1
self.client_ec_private_key = None

def disable_qrc(self):
"""Disable QRC and fall back to EC-only encryption"""
self.__qrc_key_id = None
self.client_ec_private_key = None

def __get_server_base(self):
return self.__server_base
Expand All @@ -53,7 +65,12 @@ def __set_server_base(self, value): # type: (str) -> None
if not value.startswith('http'):
value = 'https://' + value
p = urlparse(value)
self.__server_base = urlunparse((p.scheme or 'https', p.netloc, '/api/rest/', None, None, None))
new_server_base = urlunparse((p.scheme or 'https', p.netloc, '/api/rest/', None, None, None))

if hasattr(self, '_RestApiContext__server_base') and self.__server_base != new_server_base:
self.__qrc_key_id = -1

self.__server_base = new_server_base

def __get_server_key_id(self):
return self.__server_key_id
Expand All @@ -62,8 +79,48 @@ def __set_server_key_id(self, key_id):
self.__server_key_id = key_id
self.__store_server_key = True

def __get_qrc_key_id(self):
if self.__qrc_key_id == -1:
self._determine_qrc_key()
return self.__qrc_key_id

def __get_store_server_key(self):
return self.__store_server_key

def _determine_qrc_key(self):
import sys

if sys.version_info < (3, 11) or not self.__server_base:
self.__qrc_key_id = None
return

try:
hostname = urlparse(self.__server_base).netloc.lower().split(':')[0]

qrc_key_map = {
'qa.keepersecurity.com': 107,
'staging.keepersecurity.com': 124,
'keepersecurity.com': 136,
}

qrc_key_id = qrc_key_map.get(hostname)
if qrc_key_id is None and 'govcloud.keepersecurity.us' in hostname:
qrc_key_id = 148 if hostname.startswith('dev.') else 160
if qrc_key_id is None and 'il5.keepersecurity.us' in hostname:
qrc_key_id = 172 if hostname.startswith('dev.') else 186
if qrc_key_id is None:
self.__qrc_key_id = None
return
from .rest_api import SERVER_PUBLIC_KEYS
if qrc_key_id in SERVER_PUBLIC_KEYS:
self.__qrc_key_id = qrc_key_id
else:
import logging
logging.debug(f"QRC key {qrc_key_id} not available, will use EC key 7")
self.__qrc_key_id = None

except Exception:
self.__qrc_key_id = None

def set_proxy(self, proxy_server):
if proxy_server:
Expand All @@ -89,6 +146,7 @@ def certificate_check(self, value):

server_base = property(__get_server_base, __set_server_base)
server_key_id = property(__get_server_key_id, __set_server_key_id)
qrc_key_id = property(__get_qrc_key_id)
store_server_key = property(__get_store_server_key)


Expand Down Expand Up @@ -183,6 +241,7 @@ def clear_session(self):
self.session_token = None
self.salt = None
self.iterations = 0
self.__rest_context.reset_qrc_key()
self.data_key = None
self.client_key = None
self.rsa_key = None
Expand Down
792 changes: 403 additions & 389 deletions keepercommander/proto/APIRequest_pb2.py

Large diffs are not rendered by default.

Loading
Loading