Skip to content

Commit 43434be

Browse files
Merge pull request #1353 from Keeper-Security/DR-854_DR-866_pam_user_records_and_resource_uids
DR-854, DR-866 local pamUser assignment and setting resourceUid to bl…
2 parents 2aa5062 + 73ddca5 commit 43434be

File tree

4 files changed

+132
-61
lines changed

4 files changed

+132
-61
lines changed

keepercommander/commands/discover/result_process.py

Lines changed: 99 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
from ...proto import router_pb2, record_pb2
1313
from keepercommander.discovery_common.jobs import Jobs
1414
from keepercommander.discovery_common.process import Process, QuitException, NoDiscoveryDataException
15-
from keepercommander.discovery_common.types import (DiscoveryObject, UserAcl, PromptActionEnum, PromptResult,
16-
BulkRecordAdd, BulkRecordConvert, BulkProcessResults, BulkRecordSuccess,
17-
BulkRecordFail, DirectoryInfo, NormalizedRecord, RecordField)
15+
from keepercommander.discovery_common.types import (
16+
DiscoveryObject, UserAcl, PromptActionEnum, PromptResult, BulkRecordAdd, BulkRecordConvert, BulkProcessResults,
17+
BulkRecordSuccess, BulkRecordFail, DirectoryInfo, NormalizedRecord, RecordField)
1818
from pydantic import BaseModel
1919
from typing import Optional, List, Any, TYPE_CHECKING
2020

@@ -46,6 +46,7 @@ class AdminSearchResult(BaseModel):
4646
record: Any
4747
is_directory_user: bool
4848
is_pam_user: bool
49+
being_used: bool = False
4950

5051

5152
class PAMGatewayActionDiscoverResultProcessCommand(PAMGatewayActionDiscoverCommandBase):
@@ -582,7 +583,10 @@ def _prompt(self,
582583
raise QuitException()
583584
print()
584585

585-
def _find_user_record(self, params: KeeperParams, context: Optional[Any] = None) -> Optional[TypedRecord]:
586+
def _find_user_record(self,
587+
params: KeeperParams,
588+
bulk_convert_records: List[BulkRecordConvert],
589+
context: Optional[Any] = None) -> (Optional[TypedRecord], bool):
586590

587591
gateway_context = context.get("gateway_context") # type: GatewayContext
588592
record_link = context.get("record_link") # type: RecordLink
@@ -599,11 +603,15 @@ def _find_user_record(self, params: KeeperParams, context: Optional[Any] = None)
599603
for record in folder["records"]:
600604
shared_record_uids.append(record.get("record_uid"))
601605

606+
# Make a list of record we are already converting so we don't show them again.
607+
converting_list = [x.record_uid for x in bulk_convert_records]
608+
602609
logging.debug(f"shared folders record uid {shared_record_uids}")
603610

604611
while True:
605612
user_search = input("Enter an user to search for [ENTER/RETURN to quit]> ")
606613
if user_search == "":
614+
print(f"{bcolors.FAIL}No search terms, not performing search.{bcolors.ENDC}")
607615
return None
608616

609617
# Search for record with the search string.
@@ -613,39 +621,40 @@ def _find_user_record(self, params: KeeperParams, context: Optional[Any] = None)
613621
search_str=user_search,
614622
record_version=3
615623
))
624+
# If not record are returned by the search just return None,
616625
if len(user_record) == 0:
617-
print(f"{bcolors.FAIL}Could not find any record.{bcolors.ENDC}")
626+
print(f"{bcolors.FAIL}Could not find any records that contain the search text.{bcolors.ENDC}")
627+
return None
618628

619629
# Find usable admin records.
620630
admin_search_results = [] # type: List[AdminSearchResult]
621631
for record in user_record:
622632

623633
user_record = vault.KeeperRecord.load(params, record.record_uid)
624634
if user_record.record_type == "pamUser":
635+
logging.debug(f"{record.record_uid} is a pamUser")
636+
637+
# If we are already converting this pamUser record, then don't show it.
638+
if record.record_uid in converting_list:
639+
logging.debug(f"pamUser {user_record.title}, {user_record.record_uid} is being converted; "
640+
"BAD for search")
641+
admin_search_results.append(
642+
AdminSearchResult(
643+
record=user_record,
644+
is_directory_user=False,
645+
is_pam_user=True,
646+
being_used=True
647+
)
648+
)
649+
continue
625650

626651
# Does the record exist in the gateway shared folder?
627652
# We want to filter our other gateway's pamUser, or it will get overwhelming.
628653
if user_record.record_uid not in shared_record_uids:
629654
logging.debug(f"pamUser {record.title}, {user_record.record_uid} not in shared "
630-
"folder, skip")
655+
"folder, BAD for search")
631656
continue
632657

633-
# # If a pamUser, make sure the user is part of our configuration
634-
# record_rotation = params.record_rotation_cache.get(record.record_uid)
635-
# if record_rotation is not None:
636-
# configuration_uid = record_rotation.get("configuration_uid")
637-
# if configuration_uid is None or configuration_uid == "":
638-
# logging.debug(f"pamUser {record.title}, {record.record_uid} does not have a controller, "
639-
# "skip")
640-
# continue
641-
# if configuration_uid != gateway_context.configuration_uid:
642-
# logging.debug(f"pamUser {record.title}, {record.record_uid} controller is not this "
643-
# " controller, skip")
644-
# continue
645-
# else:
646-
# logging.debug(f"pamUser {record.title}, {record.record_uid} does not have a rotation
647-
# settings.")
648-
649658
# If the record does not exist in the record linking, it's orphaned; accept it
650659
# If it does exist, then check if it belonged to a directory.
651660
# Very unlikely a user that belongs to a database or another machine can be used.
@@ -659,28 +668,32 @@ def _find_user_record(self, params: KeeperParams, context: Optional[Any] = None)
659668
is_directory_user = self._is_directory_user(parent_record.record_type)
660669
if is_directory_user is False:
661670
logging.debug(f"pamUser parent for {user_record.title}, "
662-
"{user_record.record_uid} is not a directory, skip")
671+
"{user_record.record_uid} is not a directory; BAD for search")
663672
continue
664673

674+
logging.debug(f"pamUser {user_record.title}, {user_record.record_uid} is a directory user; "
675+
"good for search")
676+
665677
else:
666-
logging.debug(f"pamUser {user_record.title}, {user_record.record_uid} does not have record "
667-
"linking vertex.")
678+
logging.debug(f"pamUser {user_record.title}, {user_record.record_uid} does not a parent; "
679+
"good for search")
668680
else:
669681
logging.debug(f"pamUser {user_record.title}, {user_record.record_uid} does not have record "
670-
"linking vertex.")
682+
"linking vertex; good for search")
671683

672684
admin_search_results.append(
673685
AdminSearchResult(
674686
record=user_record,
675687
is_directory_user=is_directory_user,
676-
is_pam_user=True
688+
is_pam_user=True,
689+
being_used=False
677690
)
678691
)
679692

680693
# Else this is a non-PAM record.
681694
# Make sure it has a login, password, private key
682695
else:
683-
logging.debug(f"{record.record_uid} is not a pamUser")
696+
logging.debug(f"{record.record_uid} is NOT a pamUser")
684697
login_field = next((x for x in record.fields if x.type == "login"), None)
685698
password_field = next((x for x in record.fields if x.type == "password"), None)
686699
private_key_field = next((x for x in record.fields if x.type == "keyPair"), None)
@@ -693,11 +706,16 @@ def _find_user_record(self, params: KeeperParams, context: Optional[Any] = None)
693706
is_pam_user=False
694707
)
695708
)
709+
logging.debug(f"{record.title} is has credentials, good for search")
696710
else:
697-
logging.debug(f"{record.title} is missing full credentials, skip")
711+
logging.debug(f"{record.title} is missing full credentials, BAD for search")
698712

699-
user_index = 1
713+
# If all the users have been filtered out, then just return None
714+
if len(admin_search_results) == 0:
715+
print(f"{bcolors.FAIL}Could not find any available records.{bcolors.ENDC}")
716+
return None
700717

718+
user_index = 1
701719
admin_search_results = sorted(admin_search_results,
702720
key=lambda x: x.is_pam_user,
703721
reverse=True)
@@ -709,32 +727,50 @@ def _find_user_record(self, params: KeeperParams, context: Optional[Any] = None)
709727
has_local_user = True
710728
is_local_user = True
711729

712-
print(f"{bcolors.HEADER}[{user_index}] {bcolors.ENDC}"
713-
f"{_b('* ') if is_local_user is True else ''}"
714-
f"{admin_search_result.record.title} "
715-
f'{"(Directory User)" if admin_search_result.is_directory_user is True else ""}')
730+
hc = bcolors.HEADER
731+
b = bcolors.BOLD
732+
tc = ""
733+
index_str = user_index
734+
if admin_search_result.being_used is True:
735+
hc = bcolors.WARNING
736+
b = bcolors.WARNING
737+
tc = bcolors.WARNING
738+
index_str = "-" * len(str(index_str))
739+
740+
print(f"{hc}[{index_str}] {bcolors.ENDC}"
741+
f"{b + '* ' + bcolors.ENDC if is_local_user is True else ''}"
742+
f"{tc}{admin_search_result.record.title}{bcolors.ENDC} "
743+
f'{"(Directory User) " if admin_search_result.is_directory_user is True else ""}'
744+
f'{tc + "(Already taken)" + bcolors.ENDC if admin_search_result.being_used is True else ""}')
716745
user_index += 1
717746

718747
if has_local_user is True:
719748
print(f"{bcolors.BOLD}* Not a PAM User record. "
720749
f"A PAM User would be generated from this record.{bcolors.ENDC}")
721750

722-
select = input("Enter line number of user record to use, enter/return to refind the search, "
751+
select = input("Enter line number of user record to use, enter/return to refine the search, "
723752
f"or {_b('Q')} to quit search. > ").lower()
724753
if select == "":
725754
continue
726755
elif select[0] == "q":
727756
return None
728757
else:
729758
try:
730-
return admin_search_results[int(select) - 1].record # type: TypedRecord
759+
selected = admin_search_results[int(select) - 1]
760+
if selected.being_used is True:
761+
print(f"{bcolors.FAIL}Cannot select a record that has already been taken. "
762+
f"Another record is using this local user as its administrator.{bcolors.ENDC}")
763+
return None
764+
admin_record = selected.record # type: TypedRecord
765+
return admin_record, selected.is_directory_user
731766
except IndexError:
732767
print(f"{bcolors.FAIL}Entered row index does not exists.{bcolors.ENDC}")
733768
continue
734769

735770
@staticmethod
736-
def _handle_admin_record_from_record(record: TypedRecord, content: DiscoveryObject, context: Optional[Any] = None) \
737-
-> Optional[PromptResult]:
771+
def _handle_admin_record_from_record(record: TypedRecord,
772+
content: DiscoveryObject,
773+
context: Optional[Any] = None) -> Optional[PromptResult]:
738774

739775
params = context.get("param") # type: KeeperParams
740776
gateway_context = context.get("gateway_context") # type: GatewayContext
@@ -817,8 +853,13 @@ def _handle_admin_record_from_record(record: TypedRecord, content: DiscoveryObje
817853
"The password on that record will not be rotated."
818854
)
819855

820-
def _prompt_admin(self, parent_vertex: DAGVertex, content: DiscoveryObject, acl: UserAcl,
821-
indent: int = 0, context: Optional[Any] = None) -> PromptResult:
856+
def _prompt_admin(self,
857+
parent_vertex: DAGVertex,
858+
content: DiscoveryObject,
859+
acl: UserAcl,
860+
bulk_convert_records: List[BulkRecordConvert],
861+
indent: int = 0,
862+
context: Optional[Any] = None) -> PromptResult:
822863

823864
if content is None:
824865
raise Exception("The admin content was not passed in to prompt the user.")
@@ -863,7 +904,9 @@ def _prompt_admin(self, parent_vertex: DAGVertex, content: DiscoveryObject, acl:
863904
return prompt_result
864905
elif action[0] == 'f':
865906
print("")
866-
record = self._find_user_record(params, context=context)
907+
record, is_directory_user = self._find_user_record(params,
908+
context=context,
909+
bulk_convert_records=bulk_convert_records)
867910
if record is not None:
868911
admin_prompt_result = self._handle_admin_record_from_record(
869912
record=record,
@@ -872,6 +915,7 @@ def _prompt_admin(self, parent_vertex: DAGVertex, content: DiscoveryObject, acl:
872915
)
873916
if admin_prompt_result is not None:
874917
if admin_prompt_result.action == PromptActionEnum.ADD:
918+
admin_prompt_result.is_directory_user = is_directory_user
875919
print(f"{bcolors.OKGREEN}Adding admin record to save queue.{bcolors.ENDC}")
876920
return admin_prompt_result
877921
elif action[0] == 's':
@@ -907,7 +951,7 @@ def _prompt_confirm_add(bulk_add_records: List[BulkRecordAdd]):
907951
msg = (f"{bcolors.BOLD}There is 1 record queued to be added to your vault. "
908952
f"Do you wish to add it? [Y/N]> {bcolors.ENDC}")
909953
else:
910-
msg = (f"{bcolors.BOLD}There are {count} records queue to be added to your vault. "
954+
msg = (f"{bcolors.BOLD}There are {count} records queued to be added to your vault. "
911955
f"Do you wish to add them? [Y/N]> {bcolors.ENDC}")
912956
while True:
913957
yn = input(msg).lower()
@@ -1038,8 +1082,15 @@ def _create_records(cls, bulk_add_records: List[BulkRecordAdd], context: Optiona
10381082
# STEP 3 - Add rotation settings.
10391083
# Use the list we passed in, find the results, and add if the additions were successful.
10401084

1085+
# Keep track of each record we create a rotation for to avoid version problems, if there was a dup.
1086+
created_cache = []
1087+
10411088
# For the records passed in to be created.
10421089
for bulk_record in bulk_add_records:
1090+
if bulk_record.record_uid in created_cache:
1091+
logging.debug(f"found a duplicate of record uid: {bulk_record.record_uid}")
1092+
continue
1093+
10431094
# Grab the type Keeper record instance, and title from that record.
10441095
pb_add_record = bulk_record.record
10451096
title = bulk_record.title
@@ -1088,7 +1139,7 @@ def _create_records(cls, bulk_add_records: List[BulkRecordAdd], context: Optiona
10881139

10891140
# Only set the resource if the record type is a PAM User.
10901141
# Machines, databases, and directories have a login/password in the record that indicates who the admin is.
1091-
if bulk_record.record_type == "pamUser":
1142+
if bulk_record.record_type == "pamUser" and bulk_record.parent_record_uid is not None:
10921143
rq.resourceUid = url_safe_str_to_bytes(bulk_record.parent_record_uid)
10931144

10941145
# Right now, the schedule and password complexity are not set. This would be part of a rule engine.
@@ -1098,6 +1149,8 @@ def _create_records(cls, bulk_add_records: List[BulkRecordAdd], context: Optiona
10981149

10991150
router_set_record_rotation_information(params, rq)
11001151

1152+
created_cache.append(bulk_record.record_uid)
1153+
11011154
build_process_results.success.append(
11021155
BulkRecordSuccess(
11031156
title=title,
@@ -1123,6 +1176,8 @@ def _convert_records(cls, bulk_convert_records: List[BulkRecordConvert], context
11231176

11241177
rq = router_pb2.RouterRecordRotationRequest()
11251178
rq.recordUid = url_safe_str_to_bytes(bulk_convert_record.record_uid)
1179+
1180+
# We can't set the version to 0 if it's greater than 0, look up prior version.
11261181
record_rotation_revision = params.record_rotation_cache.get(bulk_convert_record.record_uid)
11271182
rq.revision = record_rotation_revision.get('revision') if record_rotation_revision else 0
11281183

@@ -1131,7 +1186,7 @@ def _convert_records(cls, bulk_convert_records: List[BulkRecordConvert], context
11311186

11321187
# Only set the resource if the record type is a PAM User.
11331188
# Machines, databases, and directories have a login/password in the record that indicates who the admin is.
1134-
if record.record_type == "pamUser":
1189+
if record.record_type == "pamUser" and bulk_convert_record.parent_record_uid is not None:
11351190
rq.resourceUid = url_safe_str_to_bytes(bulk_convert_record.parent_record_uid)
11361191
else:
11371192
rq.resourceUid = None
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.0.26'
1+
__version__ = '1.0.27'

0 commit comments

Comments
 (0)