diff --git a/keepercommander/commands/credential_provision.py b/keepercommander/commands/credential_provision.py index 93af9cbb9..b1a9008bd 100644 --- a/keepercommander/commands/credential_provision.py +++ b/keepercommander/commands/credential_provision.py @@ -1773,7 +1773,7 @@ def _create_dag_link( pam_config_record = vault.KeeperRecord.load(params, pam_config_uid) # Create RecordLink instance - record_link = RecordLink(record=pam_config_record, params=params, fail_on_corrupt=False, use_per_graph_endpoints=False) + record_link = RecordLink(record=pam_config_record, params=params, fail_on_corrupt=False) # Create belongs_to relationship: PAM User belongs_to PAM Configuration record_link.belongs_to( diff --git a/keepercommander/commands/discover/job_remove.py b/keepercommander/commands/discover/job_remove.py index 58ced88e7..fed280496 100644 --- a/keepercommander/commands/discover/job_remove.py +++ b/keepercommander/commands/discover/job_remove.py @@ -39,7 +39,7 @@ def execute(self, params, **kwargs): all_gateways = GatewayContext.all_gateways(params) def _find_job(configuration_record) -> Optional[Dict]: - jobs_obj = Jobs(record=configuration_record, params=params, use_per_graph_endpoints=False) + jobs_obj = Jobs(record=configuration_record, params=params) job_item = jobs_obj.get_job(job_id) if job_item is not None: return { diff --git a/keepercommander/commands/discover/job_start.py b/keepercommander/commands/discover/job_start.py index f3e3bc234..1093ec402 100644 --- a/keepercommander/commands/discover/job_start.py +++ b/keepercommander/commands/discover/job_start.py @@ -112,7 +112,7 @@ def execute(self, params, **kwargs): multi_conf_msg(gateway, err) return - jobs = Jobs(record=gateway_context.configuration, params=params, use_per_graph_endpoints=False) + jobs = Jobs(record=gateway_context.configuration, params=params) current_job_item = jobs.current_job removed_prior_job = None if current_job_item is not None: diff --git a/keepercommander/commands/discover/job_status.py b/keepercommander/commands/discover/job_status.py index 378e6ae39..25450489a 100644 --- a/keepercommander/commands/discover/job_status.py +++ b/keepercommander/commands/discover/job_status.py @@ -6,7 +6,7 @@ from ...display import bcolors from ...discovery_common.jobs import Jobs from ...discovery_common.infrastructure import Infrastructure -from ...keeper_dag.types import PamEndpoints +from ...keeper_dag.types import PamGraphId from ...discovery_common.types import DiscoveryDelta, DiscoveryObject from ...keeper_dag.dag import DAG from typing import Optional, Dict, List, TYPE_CHECKING @@ -160,7 +160,7 @@ def print_job_detail(params: KeeperParams, job_id: str): def _find_job(configuration_record) -> Optional[Dict]: - jobs_obj = Jobs(record=configuration_record, params=params, use_per_graph_endpoints=False) + jobs_obj = Jobs(record=configuration_record, params=params) job_item = jobs_obj.get_job(job_id) if job_item is not None: return { @@ -175,7 +175,7 @@ def _find_job(configuration_record) -> Optional[Dict]: if gateway_context is not None: jobs = payload["jobs"] job = jobs.get_job(job_id) # type: JobItem - infra = Infrastructure(record=gateway_context.configuration, params=params, use_per_graph_endpoints=False) + infra = Infrastructure(record=gateway_context.configuration, params=params) color = bcolors.OKBLUE status = "RUNNING" @@ -257,8 +257,7 @@ def _find_job(configuration_record) -> Optional[Dict]: print("Fall back to raw graph.") print("") dag = DAG(conn=infra.conn, record=infra.record, - read_endpoint=PamEndpoints.INFRASTRUCTURE, - write_endpoint=PamEndpoints.INFRASTRUCTURE) + graph_id=PamGraphId.INFRASTRUCTURE) print(dag.to_dot_raw(sync_point=job.sync_point, rank_dir="RL")) else: @@ -325,7 +324,7 @@ def execute(self, params, **kwargs): if len(gateway_context.gateway_name) > max_gateway_name: max_gateway_name = len(gateway_context.gateway_name) - jobs = Jobs(record=configuration_record, params=params, use_per_graph_endpoints=False) + jobs = Jobs(record=configuration_record, params=params) if show_history is True: job_list = reversed(jobs.history) else: diff --git a/keepercommander/commands/discover/result_process.py b/keepercommander/commands/discover/result_process.py index 7ac4df28d..1c61d234b 100644 --- a/keepercommander/commands/discover/result_process.py +++ b/keepercommander/commands/discover/result_process.py @@ -1334,7 +1334,7 @@ def _get_directory_info(domain: str, def remove_job(params: KeeperParams, configuration_record: KeeperRecord, job_id: str): try: - jobs = Jobs(record=configuration_record, params=params, use_per_graph_endpoints=False) + jobs = Jobs(record=configuration_record, params=params) jobs.cancel(job_id) print(f"{bcolors.OKGREEN}No items left to process. Removing completed discovery job.{bcolors.ENDC}") except Exception as err: @@ -1352,8 +1352,7 @@ def preview(self, job_item: JobItem, params: KeeperParams, gateway_context: Gate infra = Infrastructure(record=gateway_context.configuration, params=params, logger=logging, - debug_level=debug_level, - use_per_graph_endpoints=False) + debug_level=debug_level) infra.load(sync_point) configuration = None @@ -1512,7 +1511,7 @@ def execute(self, params: KeeperParams, **kwargs): # Get the current job. # There can only be one active job. # This will give us the sync point for the delta - jobs = Jobs(record=configuration_record, params=params, logger=logging, debug_level=debug_level, use_per_graph_endpoints=False) + jobs = Jobs(record=configuration_record, params=params, logger=logging, debug_level=debug_level) job_item = jobs.current_job if job_item is None: continue diff --git a/keepercommander/commands/discover/rule_add.py b/keepercommander/commands/discover/rule_add.py index 36137fd92..2bf1d7bb9 100644 --- a/keepercommander/commands/discover/rule_add.py +++ b/keepercommander/commands/discover/rule_add.py @@ -134,7 +134,7 @@ def execute(self, params, **kwargs): return # If the rule passes its validation, then add control DAG - rules = Rules(record=gateway_context.configuration, params=params, use_per_graph_endpoints=False) + rules = Rules(record=gateway_context.configuration, params=params) new_rule = ActionRuleItem( name=kwargs.get("name"), action=kwargs.get("rule_action"), diff --git a/keepercommander/commands/discover/rule_list.py b/keepercommander/commands/discover/rule_list.py index 22207f4bf..9819fc361 100644 --- a/keepercommander/commands/discover/rule_list.py +++ b/keepercommander/commands/discover/rule_list.py @@ -101,7 +101,7 @@ def execute(self, params, **kwargs): multi_conf_msg(gateway, err) return - rules = Rules(record=gateway_context.configuration, params=params, use_per_graph_endpoints=False) + rules = Rules(record=gateway_context.configuration, params=params) rule_list = rules.rule_list(rule_type=RuleTypeEnum.ACTION, search=kwargs.get("search")) # type: List[RuleItem] if len(rule_list) == 0: diff --git a/keepercommander/commands/discover/rule_remove.py b/keepercommander/commands/discover/rule_remove.py index 164faa338..b093dfa57 100644 --- a/keepercommander/commands/discover/rule_remove.py +++ b/keepercommander/commands/discover/rule_remove.py @@ -46,7 +46,7 @@ def execute(self, params, **kwargs): return try: - rules = Rules(record=gateway_context.configuration, params=params, use_per_graph_endpoints=False) + rules = Rules(record=gateway_context.configuration, params=params) if remove_all: rules.remove_all(RuleTypeEnum.ACTION) print(f"{bcolors.OKGREEN}All rules removed.{bcolors.ENDC}") diff --git a/keepercommander/commands/discover/rule_update.py b/keepercommander/commands/discover/rule_update.py index 7ffdda461..b3e063bf5 100644 --- a/keepercommander/commands/discover/rule_update.py +++ b/keepercommander/commands/discover/rule_update.py @@ -64,7 +64,7 @@ def execute(self, params, **kwargs): try: rule_id = kwargs.get("rule_id") - rules = Rules(record=gateway_context.configuration, params=params, use_per_graph_endpoints=False) + rules = Rules(record=gateway_context.configuration, params=params) rule_item = rules.get_rule_item(rule_type=RuleTypeEnum.ACTION, rule_id=rule_id) if rule_item is None: raise ValueError("Rule Id does not exist.") diff --git a/keepercommander/commands/discoveryrotation.py b/keepercommander/commands/discoveryrotation.py index 20f2d0206..d8e5b3b57 100644 --- a/keepercommander/commands/discoveryrotation.py +++ b/keepercommander/commands/discoveryrotation.py @@ -3372,7 +3372,7 @@ def record_rotate(self, params, record_uid, slient: bool = False): # Check the graph for the noop setting. record_link = RecordLink(record=pam_config, params=params, - fail_on_corrupt=False, use_per_graph_endpoints=False) + fail_on_corrupt=False) acl = record_link.get_acl(record_uid, pam_config.record_uid) if acl is not None and acl.rotation_settings is not None: is_noop = acl.rotation_settings.noop diff --git a/keepercommander/commands/pam_debug/acl.py b/keepercommander/commands/pam_debug/acl.py index a85f49286..9a7bca7d2 100644 --- a/keepercommander/commands/pam_debug/acl.py +++ b/keepercommander/commands/pam_debug/acl.py @@ -57,7 +57,7 @@ def execute(self, params: KeeperParams, **kwargs): record_link = RecordLink(record=gateway_context.configuration, params=params, logger=logging, - debug_level=debug_level, use_per_graph_endpoints=False) + debug_level=debug_level) user_record = vault.KeeperRecord.load(params, user_uid) # type: Optional[TypedRecord] if user_record is None: diff --git a/keepercommander/commands/pam_debug/gateway.py b/keepercommander/commands/pam_debug/gateway.py index a7cfc4d42..3642668ce 100644 --- a/keepercommander/commands/pam_debug/gateway.py +++ b/keepercommander/commands/pam_debug/gateway.py @@ -49,11 +49,11 @@ def execute(self, params: KeeperParams, **kwargs): multi_conf_msg(gateway, err) return - infra = Infrastructure(record=gateway_context.configuration, params=params, fail_on_corrupt=False, use_per_graph_endpoints=False) + infra = Infrastructure(record=gateway_context.configuration, params=params, fail_on_corrupt=False) infra.load() - record_link = RecordLink(record=gateway_context.configuration, params=params, fail_on_corrupt=False, use_per_graph_endpoints=False) - user_service = UserService(record=gateway_context.configuration, params=params, fail_on_corrupt=False, use_per_graph_endpoints=False) + record_link = RecordLink(record=gateway_context.configuration, params=params, fail_on_corrupt=False) + user_service = UserService(record=gateway_context.configuration, params=params, fail_on_corrupt=False) if gateway_context is None: print(f" {self._f('Cannot get gateway information. Gateway may not be up.')}") diff --git a/keepercommander/commands/pam_debug/graph.py b/keepercommander/commands/pam_debug/graph.py index be482f8e6..6c985a4cb 100644 --- a/keepercommander/commands/pam_debug/graph.py +++ b/keepercommander/commands/pam_debug/graph.py @@ -78,7 +78,7 @@ def _do_text_list_infra(self, params: KeeperParams, gateway_context: GatewayCont indent: int = 0): infra = Infrastructure(record=gateway_context.configuration, params=params, logger=logging, - debug_level=debug_level, use_per_graph_endpoints=False) + debug_level=debug_level) infra.load(sync_point=0) try: @@ -164,7 +164,7 @@ def _do_text_list_rl(self, params: KeeperParams, gateway_context: GatewayContext record_link = RecordLink(record=gateway_context.configuration, params=params, logger=logging, - debug_level=debug_level, use_per_graph_endpoints=False) + debug_level=debug_level) configuration = record_link.dag.get_root record = vault.KeeperRecord.load(params, configuration.uid) # type: Optional[TypedRecord] @@ -316,7 +316,7 @@ def _do_text_list_service(self, params: KeeperParams, gateway_context: GatewayCo indent: int = 0): user_service = UserService(record=gateway_context.configuration, params=params, logger=logging, - debug_level=debug_level, use_per_graph_endpoints=False) + debug_level=debug_level) configuration = user_service.dag.get_root def _handle(current_vertex: DAGVertex, parent_vertex: Optional[DAGVertex] = None, indent: int = 0): @@ -364,7 +364,7 @@ def _do_text_list_jobs(self, params: KeeperParams, gateway_context: GatewayConte indent: int = 0): infra = Infrastructure(record=gateway_context.configuration, params=params, logger=logging, - debug_level=debug_level, fail_on_corrupt=False, use_per_graph_endpoints=False) + debug_level=debug_level, fail_on_corrupt=False) infra.load(sync_point=0) pad = "" @@ -461,7 +461,7 @@ def _do_render_infra(self, params: KeeperParams, gateway_context: GatewayContext debug_level: int = 0): infra = Infrastructure(record=gateway_context.configuration, params=params, logger=logging, - debug_level=debug_level, use_per_graph_endpoints=False) + debug_level=debug_level) infra.load(sync_point=0) print("") @@ -487,7 +487,7 @@ def _do_render_rl(self, params: KeeperParams, gateway_context: GatewayContext, f rl = RecordLink(record=gateway_context.configuration, params=params, logger=logging, - debug_level=debug_level, use_per_graph_endpoints=False) + debug_level=debug_level) print("") dot_instance = rl.to_dot( @@ -510,7 +510,7 @@ def _do_render_service(self, params: KeeperParams, gateway_context: GatewayConte graph_format: str, debug_level: int = 0): service = UserService(record=gateway_context.configuration, params=params, logger=logging, - debug_level=debug_level, use_per_graph_endpoints=False) + debug_level=debug_level) print("") dot_instance = service.to_dot( @@ -532,7 +532,7 @@ def _do_render_service(self, params: KeeperParams, gateway_context: GatewayConte def _do_render_jobs(self, params: KeeperParams, gateway_context: GatewayContext, filepath: str, graph_format: str, debug_level: int = 0): - jobs = Jobs(record=gateway_context.configuration, params=params, logger=logging, debug_level=debug_level, use_per_graph_endpoints=False) + jobs = Jobs(record=gateway_context.configuration, params=params, logger=logging, debug_level=debug_level) print("") dot_instance = jobs.dag.to_dot() diff --git a/keepercommander/commands/pam_debug/info.py b/keepercommander/commands/pam_debug/info.py index f07794148..09d9bcfa6 100644 --- a/keepercommander/commands/pam_debug/info.py +++ b/keepercommander/commands/pam_debug/info.py @@ -65,7 +65,7 @@ def execute(self, params: KeeperParams, **kwargs): for configuration_record in configuration_records: - record_link = RecordLink(record=configuration_record, params=params, use_per_graph_endpoints=False) + record_link = RecordLink(record=configuration_record, params=params) record_vertex = record_link.dag.get_vertex(record.record_uid) if record_vertex is not None and record_vertex.active is True: controller_uid = configuration_record.record_uid @@ -95,10 +95,10 @@ def execute(self, params: KeeperParams, **kwargs): print(f"{bcolors.FAIL}Could not find the gateway for configuration record.{controller_uid}{bcolors.ENDC}") return - infra = Infrastructure(record=configuration_record, params=params, use_per_graph_endpoints=False) + infra = Infrastructure(record=configuration_record, params=params) infra.load() - record_link = RecordLink(record=configuration_record, params=params, use_per_graph_endpoints=False) - user_service = UserService(record=configuration_record, params=params, use_per_graph_endpoints=False) + record_link = RecordLink(record=configuration_record, params=params) + user_service = UserService(record=configuration_record, params=params) print("") print(self._h("Record Information")) diff --git a/keepercommander/commands/pam_debug/link.py b/keepercommander/commands/pam_debug/link.py index b8bddba8d..766d27d16 100644 --- a/keepercommander/commands/pam_debug/link.py +++ b/keepercommander/commands/pam_debug/link.py @@ -53,7 +53,7 @@ def execute(self, params: KeeperParams, **kwargs): record_link = RecordLink(record=gateway_context.configuration, params=params, logger=logging, - debug_level=debug_level, use_per_graph_endpoints=False) + debug_level=debug_level) resource_record = vault.KeeperRecord.load(params, resource_uid) # type: Optional[TypedRecord] if resource_record is None: diff --git a/keepercommander/commands/pam_debug/rotation_setting.py b/keepercommander/commands/pam_debug/rotation_setting.py index 2557f8a15..8e475b487 100644 --- a/keepercommander/commands/pam_debug/rotation_setting.py +++ b/keepercommander/commands/pam_debug/rotation_setting.py @@ -164,7 +164,7 @@ def execute(self, params: KeeperParams, **kwargs): f"It's a {resource_record.record_type}.{bcolors.ENDC}") return - record_link = RecordLink(record=configuration_record, params=params, use_per_graph_endpoints=False) + record_link = RecordLink(record=configuration_record, params=params) parent_uid = resource_record_uid or configuration_record_uid parent_vertex = record_link.get_record_link(parent_uid) diff --git a/keepercommander/commands/pam_debug/vertex.py b/keepercommander/commands/pam_debug/vertex.py index 05bbe493d..3d37e61dc 100644 --- a/keepercommander/commands/pam_debug/vertex.py +++ b/keepercommander/commands/pam_debug/vertex.py @@ -53,7 +53,7 @@ def execute(self, params: KeeperParams, **kwargs): return infra = Infrastructure(record=gateway_context.configuration, params=params, fail_on_corrupt=False, - debug_level=debug_level, use_per_graph_endpoints=False) + debug_level=debug_level) infra.load() vertex_uid = kwargs.get("vertex_uid") diff --git a/keepercommander/commands/pam_import/keeper_ai_settings.py b/keepercommander/commands/pam_import/keeper_ai_settings.py index 1b84cf7c3..7d4056c12 100644 --- a/keepercommander/commands/pam_import/keeper_ai_settings.py +++ b/keepercommander/commands/pam_import/keeper_ai_settings.py @@ -16,7 +16,7 @@ from ...keeper_dag import DAG, EdgeType from ...keeper_dag.exceptions import DAGPathException from ...keeper_dag.connection.commander import Connection -from ...keeper_dag.types import PamEndpoints +from ...keeper_dag.types import PamGraphId from ...vault import PasswordRecord from ... import vault from ...display import bcolors @@ -84,17 +84,15 @@ def list_resource_data_edges( encrypted_transmission_key=encrypted_transmission_key, encrypted_session_token=encrypted_session_token, transmission_key=transmission_key, - use_read_protobuf=True, - use_write_protobuf=True + use_read_protobuf=False, + use_write_protobuf=False ) # Load the DAG linking_dag = DAG( conn=conn, record=dag_record, - graph_id=0, - read_endpoint=PamEndpoints.PAM, - write_endpoint=PamEndpoints.PAM + graph_id=PamGraphId.PAM.value ) try: linking_dag.load() @@ -103,7 +101,7 @@ def list_resource_data_edges( return [] # Get the resource vertex - resource_vertex = linking_dag.get_vertex(resource_uid) + resource_vertex = linking_dag.get_vertex_by_uid(resource_uid) if not resource_vertex: logging.warning(f"Resource vertex {resource_uid} not found in DAG") return [] @@ -189,17 +187,15 @@ def get_resource_settings( encrypted_transmission_key=encrypted_transmission_key, encrypted_session_token=encrypted_session_token, transmission_key=transmission_key, - use_read_protobuf=True, - use_write_protobuf=True + use_read_protobuf=False, + use_write_protobuf=False ) # Load the DAG linking_dag = DAG( conn=conn, record=dag_record, - graph_id=0, - read_endpoint=PamEndpoints.PAM, - write_endpoint=PamEndpoints.PAM + graph_id=PamGraphId.PAM.value ) try: linking_dag.load() @@ -211,7 +207,7 @@ def get_resource_settings( return None # Get the resource vertex - resource_vertex = linking_dag.get_vertex(resource_uid) + resource_vertex = linking_dag.get_vertex_by_uid(resource_uid) if not resource_vertex: logging.warning(f"Resource vertex {resource_uid} not found in DAG") return None @@ -414,10 +410,11 @@ def set_resource_keeper_ai_settings( validates caller access then writes the `ai_settings` DAG DATA edge on the resource server-side. - Fallback (env var `KEEPER_DAG_LB_FALLBACK=1`, default ON): on + Fallback (env var `KEEPER_DAG_LB_FALLBACK`, default OFF / strict mode): on `RRC_NOT_ALLOWED*` from krouter, fall back to the legacy direct DAG-write path (`_set_resource_keeper_ai_settings_legacy`). Gateway then - enforces at runtime. Set the env var to `0` for strict mode (denials propagate). + enforces at runtime. Default (unset/`0`) propagates denials; set to `1` to opt + into fallback. Args: params: KeeperParams instance @@ -436,6 +433,17 @@ def set_resource_keeper_ai_settings( encrypted_content = encrypt_aes(json.dumps(settings).encode(), record_key) + # krouter's configure_resource only writes a settings edge when it loads the + # resource's existing edges (loopEdges), which it does only for requests that + # carry meta/jit/connection (UserRest.kt). A keeperAiSettings-only request + # leaves loopEdges null and the ai_settings write is silently dropped. The Web + # Vault avoids this by always sending meta alongside the AI settings, so mirror + # that: include the resource's current meta in the same request. + meta_bytes = None + current_meta = get_resource_settings(params, resource_uid, 'meta', resolved_config_uid) + if isinstance(current_meta, dict): + meta_bytes = json.dumps(current_meta).encode() + # Primary: Layer-B configure_resource (permission-checked). from ..pam.router_helper import router_configure_resource, get_router_url host = get_router_url(params) @@ -446,6 +454,8 @@ def set_resource_keeper_ai_settings( networkUid=url_safe_str_to_bytes(resolved_config_uid), keeperAiSettings=encrypted_content, ) + if meta_bytes is not None: + rq.meta = meta_bytes try: router_configure_resource(params, rq) logging.debug(f"Saved KeeperAI settings via configure_resource for {resource_uid}") @@ -527,20 +537,19 @@ def _set_resource_keeper_ai_settings_legacy( encrypted_transmission_key=encrypted_transmission_key, encrypted_session_token=encrypted_session_token, transmission_key=transmission_key, - use_read_protobuf=True, - use_write_protobuf=True, + use_read_protobuf=False, + use_write_protobuf=False, ) linking_dag = DAG( conn=conn, record=dag_record, - read_endpoint=PamEndpoints.PAM, - write_endpoint=PamEndpoints.PAM, + graph_id=PamGraphId.PAM.value, decrypt=True, ) linking_dag.load() - resource_vertex = linking_dag.get_vertex(resource_uid) + resource_vertex = linking_dag.get_vertex_by_uid(resource_uid) if not resource_vertex: logging.warning(f"Resource vertex {resource_uid} not found in DAG") return False @@ -649,20 +658,19 @@ def _set_resource_jit_settings_legacy( encrypted_transmission_key=encrypted_transmission_key, encrypted_session_token=encrypted_session_token, transmission_key=transmission_key, - use_read_protobuf=True, - use_write_protobuf=True, + use_read_protobuf=False, + use_write_protobuf=False, ) linking_dag = DAG( conn=conn, record=dag_record, - read_endpoint=PamEndpoints.PAM, - write_endpoint=PamEndpoints.PAM, + graph_id=PamGraphId.PAM.value, decrypt=True, ) linking_dag.load() - resource_vertex = linking_dag.get_vertex(resource_uid) + resource_vertex = linking_dag.get_vertex_by_uid(resource_uid) if not resource_vertex: logging.warning(f"Resource vertex {resource_uid} not found in DAG") return False @@ -725,19 +733,17 @@ def refresh_meta_to_latest( encrypted_transmission_key=encrypted_transmission_key, encrypted_session_token=encrypted_session_token, transmission_key=transmission_key, - use_read_protobuf=True, - use_write_protobuf=True + use_read_protobuf=False, + use_write_protobuf=False ) linking_dag = DAG( conn=conn, record=dag_record, - graph_id=0, - read_endpoint=PamEndpoints.PAM, - write_endpoint=PamEndpoints.PAM, + graph_id=PamGraphId.PAM.value, decrypt=True ) linking_dag.load() - resource_vertex = linking_dag.get_vertex(resource_uid) + resource_vertex = linking_dag.get_vertex_by_uid(resource_uid) if not resource_vertex: return False meta_edges = [e for e in (resource_vertex.edges or []) @@ -795,20 +801,18 @@ def refresh_link_to_config_to_latest( encrypted_transmission_key=encrypted_transmission_key, encrypted_session_token=encrypted_session_token, transmission_key=transmission_key, - use_read_protobuf=True, - use_write_protobuf=True + use_read_protobuf=False, + use_write_protobuf=False ) linking_dag = DAG( conn=conn, record=dag_record, - graph_id=0, - read_endpoint=PamEndpoints.PAM, - write_endpoint=PamEndpoints.PAM, + graph_id=PamGraphId.PAM.value, decrypt=True ) linking_dag.load() - resource_vertex = linking_dag.get_vertex(resource_uid) - config_vertex = linking_dag.get_vertex(config_uid) + resource_vertex = linking_dag.get_vertex_by_uid(resource_uid) + config_vertex = linking_dag.get_vertex_by_uid(config_uid) if not resource_vertex or not config_vertex: return False # Re-add LINK (path empty, content {}) so it becomes latest, above KEY added by JIT/AI @@ -933,15 +937,13 @@ def inspect_resource_in_graph( encrypted_transmission_key=encrypted_transmission_key, encrypted_session_token=encrypted_session_token, transmission_key=transmission_key, - use_read_protobuf=True, - use_write_protobuf=True + use_read_protobuf=False, + use_write_protobuf=False ) linking_dag = DAG( conn=conn, record=dag_record, - graph_id=0, - read_endpoint=PamEndpoints.PAM, - write_endpoint=PamEndpoints.PAM, + graph_id=PamGraphId.PAM.value, decrypt=not show_raw_content ) linking_dag.load() @@ -1035,19 +1037,17 @@ def get_resource_domain_dir_uid( encrypted_transmission_key=encrypted_transmission_key, encrypted_session_token=encrypted_session_token, transmission_key=transmission_key, - use_read_protobuf=True, - use_write_protobuf=True + use_read_protobuf=False, + use_write_protobuf=False ) linking_dag = DAG( conn=conn, record=dag_record, - graph_id=0, - read_endpoint=PamEndpoints.PAM, - write_endpoint=PamEndpoints.PAM + graph_id=PamGraphId.PAM.value ) linking_dag.load() - resource_vertex = linking_dag.get_vertex(resource_uid) + resource_vertex = linking_dag.get_vertex_by_uid(resource_uid) if not resource_vertex: return None @@ -1131,19 +1131,18 @@ def _set_resource_domain_dir_legacy( encrypted_transmission_key=encrypted_transmission_key, encrypted_session_token=encrypted_session_token, transmission_key=transmission_key, - use_read_protobuf=True, - use_write_protobuf=True, + use_read_protobuf=False, + use_write_protobuf=False, ) linking_dag = DAG( conn=conn, record=dag_record, - read_endpoint=PamEndpoints.PAM, - write_endpoint=PamEndpoints.PAM, + graph_id=PamGraphId.PAM.value, decrypt=True, ) linking_dag.load() - resource_vertex = linking_dag.get_vertex(resource_uid) + resource_vertex = linking_dag.get_vertex_by_uid(resource_uid) if not resource_vertex: logging.warning(f"Resource vertex {resource_uid} not found in DAG") return False @@ -1156,12 +1155,12 @@ def _set_resource_domain_dir_legacy( old_dir_uid = edge.head_uid break if old_dir_uid and old_dir_uid != dir_uid: - old_dir_vertex = linking_dag.get_vertex(old_dir_uid) + old_dir_vertex = linking_dag.get_vertex_by_uid(old_dir_uid) if old_dir_vertex: resource_vertex.disconnect_from(old_dir_vertex) logging.debug(f"Disconnected old domain LINK edge to {old_dir_uid}") - dir_vertex = linking_dag.get_vertex(dir_uid) + dir_vertex = linking_dag.get_vertex_by_uid(dir_uid) if not dir_vertex: logging.warning(f"Directory vertex {dir_uid} not found in DAG") return False diff --git a/keepercommander/commands/pam_service/add.py b/keepercommander/commands/pam_service/add.py index 56e9e2e0d..e471a5c4f 100644 --- a/keepercommander/commands/pam_service/add.py +++ b/keepercommander/commands/pam_service/add.py @@ -60,9 +60,9 @@ def execute(self, params: KeeperParams, **kwargs): return user_service = UserService(record=gateway_context.configuration, params=params, fail_on_corrupt=False, - agent=f"Cmdr/{__version__}", use_per_graph_endpoints=False) + agent=f"Cmdr/{__version__}") record_link = RecordLink(record=gateway_context.configuration, params=params, fail_on_corrupt=False, - agent=f"Cmdr/{__version__}", use_per_graph_endpoints=False) + agent=f"Cmdr/{__version__}") ############### diff --git a/keepercommander/commands/pam_service/list.py b/keepercommander/commands/pam_service/list.py index 3c9657be8..995be7d1d 100644 --- a/keepercommander/commands/pam_service/list.py +++ b/keepercommander/commands/pam_service/list.py @@ -42,7 +42,7 @@ def execute(self, params: KeeperParams, **kwargs): return user_service = UserService(record=gateway_context.configuration, params=params, fail_on_corrupt=False, - agent=f"Cmdr/{__version__}", use_per_graph_endpoints=False) + agent=f"Cmdr/{__version__}") service_map = {} for resource_vertex in user_service.dag.get_root.has_vertices(edge_type=EdgeType.LINK): diff --git a/keepercommander/commands/pam_service/remove.py b/keepercommander/commands/pam_service/remove.py index 1851072ef..e4b68d25f 100644 --- a/keepercommander/commands/pam_service/remove.py +++ b/keepercommander/commands/pam_service/remove.py @@ -57,7 +57,7 @@ def execute(self, params: KeeperParams, **kwargs): return user_service = UserService(record=gateway_context.configuration, params=params, fail_on_corrupt=False, - agent=f"Cmdr/{__version__}", use_per_graph_endpoints=False) + agent=f"Cmdr/{__version__}") machine_record = vault.KeeperRecord.load(params, machine_uid) # type: Optional[TypedRecord] if machine_record is None: diff --git a/keepercommander/commands/tunnel/port_forward/TunnelGraph.py b/keepercommander/commands/tunnel/port_forward/TunnelGraph.py index b5a149760..7ac683d1c 100644 --- a/keepercommander/commands/tunnel/port_forward/TunnelGraph.py +++ b/keepercommander/commands/tunnel/port_forward/TunnelGraph.py @@ -3,7 +3,7 @@ from .tunnel_helpers import generate_random_bytes, get_config_uid from ....keeper_dag import DAG, EdgeType from ....keeper_dag.connection.commander import Connection -from ....keeper_dag.types import RefType, PamEndpoints +from ....keeper_dag.types import RefType, PamGraphId from ....keeper_dag.vertex import DAGVertex from ....display import bcolors from ....vault import PasswordRecord @@ -113,11 +113,11 @@ def __init__(self, params, encrypted_session_token, encrypted_transmission_key, encrypted_transmission_key=self.encrypted_transmission_key, encrypted_session_token=self.encrypted_session_token, transmission_key=self.transmission_key, - use_read_protobuf=True, - use_write_protobuf=True + use_read_protobuf=False, + use_write_protobuf=False ) self.linking_dag = DAG(conn=self.conn, record=self.record, - read_endpoint=PamEndpoints.PAM, write_endpoint=PamEndpoints.PAM) + graph_id=PamGraphId.PAM.value) try: self.linking_dag.load() except Exception as e: @@ -127,15 +127,15 @@ def __init__(self, params, encrypted_session_token, encrypted_transmission_key, def resource_belongs_to_config(self, resource_uid): if not self.linking_dag.has_graph: return False - resource_vertex = self.linking_dag.get_vertex(resource_uid) - config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + resource_vertex = self.linking_dag.get_vertex_by_uid(resource_uid) + config_vertex = self.linking_dag.get_vertex_by_uid(self.record.record_uid) return resource_vertex and config_vertex.has(resource_vertex, EdgeType.LINK) def user_belongs_to_config(self, user_uid): if not self.linking_dag.has_graph: return False - user_vertex = self.linking_dag.get_vertex(user_uid) - config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + user_vertex = self.linking_dag.get_vertex_by_uid(user_uid) + config_vertex = self.linking_dag.get_vertex_by_uid(self.record.record_uid) res_content = False if user_vertex and config_vertex and config_vertex.has(user_vertex, EdgeType.ACL): acl_edge = user_vertex.get_edge(config_vertex, EdgeType.ACL) diff --git a/keepercommander/discovery_common/record_link.py b/keepercommander/discovery_common/record_link.py index 6c4abefa3..db55bcfdb 100644 --- a/keepercommander/discovery_common/record_link.py +++ b/keepercommander/discovery_common/record_link.py @@ -56,7 +56,7 @@ def __init__(self, # Based on the connection type, use_write_protobuf might be set to False is True was passed. # Use self.conn.use_write_protobuf; don't use passed in use_write_protobuf. # If using protobuf to write, then use the endpoint. - # `use_per_graph_endpoints=False` also forces the endpoints on, independent + # `use_per_graph_endpoints=True` also forces the endpoints on, independent # of the protobuf flags. self.write_endpoint = None if use_per_graph_endpoints or self.conn.use_write_protobuf: diff --git a/keepercommander/keeper_dag/connection/ksm.py b/keepercommander/keeper_dag/connection/ksm.py index e24b85592..b4ad0279f 100644 --- a/keepercommander/keeper_dag/connection/ksm.py +++ b/keepercommander/keeper_dag/connection/ksm.py @@ -57,7 +57,7 @@ def __init__(self, self.use_read_protobuf = False if self.use_write_protobuf: self.logger.info("KSM cannot use protobuf for writing to the graph, using JSON.") - self.use_read_protobuf = False + self.use_write_protobuf = False if InMemoryKeyValueStorage.is_base64(config): config = utils.base64_to_string(config) diff --git a/tests/test_tunnel_close_leak.py b/tests/test_tunnel_close_leak.py index 877677523..72696e6bb 100644 --- a/tests/test_tunnel_close_leak.py +++ b/tests/test_tunnel_close_leak.py @@ -19,6 +19,20 @@ import time import logging +# Linux-only e2e repro: shells out to `sshpass`/`ssh` with `/dev/null` paths and +# needs an SSH container on 127.0.0.1:2222. Bail before importing the native +# keeper_pam_connections module so it neither aborts pytest collection nor errors +# when run on Windows. +if sys.platform == "win32": + _skip_msg = ("tunnel close-leak e2e is Linux-only (needs sshpass + SSH " + "container on :2222); skipped on Windows") + if "pytest" in sys.modules: + import pytest + pytest.skip(_skip_msg, allow_module_level=True) + else: + print(f"SKIP: {_skip_msg}") + sys.exit(0) + import keeper_pam_connections from keepercommander.commands.tunnel.port_forward.tunnel_helpers import ( TunnelSignalHandler, diff --git a/unit-tests/pam/test_dag_layer_b_migration.py b/unit-tests/pam/test_dag_layer_b_migration.py index c332841c0..e77c4ee9e 100644 --- a/unit-tests/pam/test_dag_layer_b_migration.py +++ b/unit-tests/pam/test_dag_layer_b_migration.py @@ -16,6 +16,7 @@ calling configure_resource. """ import json +import json import os import sys from unittest.mock import MagicMock, patch @@ -95,6 +96,37 @@ def _capture(params, rq): # Critical: must NOT be set on jitSettings field assert rq.jitSettings == b'' + def test_happy_path_bundles_current_meta_so_krouter_persists_ai_edge(self): + """Regression: krouter's configure_resource only writes a settings edge + when it loads loopEdges, which it does only for requests carrying + meta/jit/connection (UserRest.kt:497). A keeperAiSettings-only request + leaves loopEdges null and the ai_settings write is silently dropped. The + Web Vault always sends meta alongside AI settings; Commander must mirror + that by bundling the resource's current meta in the same request.""" + captured = {} + + def _capture(params, rq): + captured['rq'] = rq + return None + + meta_dict = {'version': 1, 'allowedSettings': {'aiEnabled': True}, 'rotateOnTermination': False} + with _patch_inputs(), \ + patch.object(ai_mod, 'encrypt_aes', return_value=b'CIPHER_BYTES'), \ + patch.object(ai_mod, 'get_resource_settings', return_value=meta_dict) as meta_mock, \ + patch('keepercommander.commands.pam.router_helper.router_configure_resource', side_effect=_capture): + ok = ai_mod.set_resource_keeper_ai_settings( + _mock_params(), RESOURCE_UID_STR, {'level': 'critical'}, config_uid=CONFIG_UID_STR + ) + assert ok is True + rq = captured['rq'] + assert rq.keeperAiSettings == b'CIPHER_BYTES' + # The fix: meta must be present so krouter fetches loopEdges and persists + # the ai_settings edge. Without it the write is a silent no-op. + assert rq.meta == json.dumps(meta_dict).encode() + # meta is read from the resource's current 'meta' DATA edge. + meta_mock.assert_called_once() + assert meta_mock.call_args.args[2] == 'meta' + def test_permission_denied_with_fallback_enabled_calls_legacy(self): legacy_called = {'count': 0}