remoteRF-server 1.4.8__tar.gz → 1.4.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/PKG-INFO +3 -2
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/README.md +2 -1
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/pyproject.toml +1 -1
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/commands.py +1 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/cluster.py +101 -6
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/devices.py +6 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/enrollment.py +42 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/home.py +28 -2
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/migrations.py +7 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/permissions.py +2 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/principals.py +61 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/global_onboarding.py +23 -9
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/grpc_server.py +48 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/reservation.py +22 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/user_group_handler.py +8 -5
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server.egg-info/PKG-INFO +3 -2
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server.egg-info/SOURCES.txt +1 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_global_onboarding.py +21 -4
- remoterf_server-1.4.10/tests/test_proxy_lifecycle.py +258 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/LICENSE +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/setup.cfg +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/federation.proto +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/grpc.proto +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/grpc_host.proto +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/idl/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/idl/device_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/idl/schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/idl/v2.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/utils/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/utils/api_token.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/utils/db_connection.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/utils/db_location.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/utils/list_string.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/utils/panel.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/common/utils/process_arg.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/adalm_pluto/pluto_diag.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/adalm_pluto/poe.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/bluetooth_diag.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/gps_diag.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_diag.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/usrp/usrp_diag.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/host/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/host/host_auth_token.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/host/host_directory_store.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/acc_perms.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/cert_provider.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/console.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/control.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/deployment/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/device_manager.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/diag.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/direct_path.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/dynamic_v2.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/admin.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/cli.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/contracts.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/destination.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/global_home.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/home_devices.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/identity.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/peers.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/runtime.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/trust.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/rpc_manager.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/startup_check.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/user_group_cli.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/v2_errors.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/serverrf_cli.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/tools/__init__.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/tools/gen_certs.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/tools/gist_status.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server.egg-info/requires.txt +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server.egg-info/top_level.txt +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_account_hardening.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_account_names.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_account_removal.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_activity_log.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_availability.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_bind_host.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_cert_provider.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_client_sessions.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_cluster.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_commands.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_console.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_contrib_drivers.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_default_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_default_usrp_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_deployment_capabilities.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_detached_device_rpc.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_device_handles.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_device_init.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_devices_add_wizard.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_devices_list.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_diag.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_direct_fixed_port.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_direct_path_server.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_dynamic_v2.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_federated_direct_path.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_federated_dynamic_v2.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_federation_edges.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_first_run.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_gate_d_contracts.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_gate_e_federation.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_gate_e_proto.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_gate_f_enrollment.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_gate_g1_logging.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_gate_g_permissions.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_gate_h_devices.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_gate_i_direct_path.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_gen_certs.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_global_accounts_migration.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_global_home.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_global_v1_disposal.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_hackrf_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_home_device_plane.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_host_directory_store.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_host_dynamic_v2.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_idl_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_panel.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_peer_transport.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_pluto_compat.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_pluto_poe.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_process_arg.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_reservation_handoff.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_reservation_policy_b.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_reservation_timestamps.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_rpc_manager_batch.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_rpc_manager_calln.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_rpc_manager_rx_n.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_rtl_sdr_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_soak_federation.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_startup_check.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_status_push.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_ti_mmwave_hardware.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_ti_mmwave_schema.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_user_group_cli.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_usrp_client_server_hardware.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_usrp_client_server_localhost.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_usrp_n210_client_server_hardware.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_usrp_preload.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_usrp_profiles.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_usrp_uhd410_hardware.py +0 -0
- {remoterf_server-1.4.8 → remoterf_server-1.4.10}/tests/test_wipe.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoteRF-server
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.10
|
|
4
4
|
Summary: RemoteRF server-side control package
|
|
5
5
|
License: GPL-3.0-or-later
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -881,10 +881,11 @@ After running `serverrf --serve`, you’ll enter the **RemoteRF Server Shell**.
|
|
|
881
881
|
|
|
882
882
|
### Users
|
|
883
883
|
|
|
884
|
-
* `users list` — List all accounts
|
|
884
|
+
* `users list` — List all accounts (this lab's own first, then visitors from partner labs)
|
|
885
885
|
* `users manage` — Manage a specific user (perms / delete / reservations)
|
|
886
886
|
* `users purge` — Delete **all** users
|
|
887
887
|
* `users perms` — Show permissions table
|
|
888
|
+
* `users prune-proxies [--stale DAYS] [-y]` — List federated proxies whose home lab is gone (dry run); `-y` removes them
|
|
888
889
|
|
|
889
890
|
---
|
|
890
891
|
|
|
@@ -850,10 +850,11 @@ After running `serverrf --serve`, you’ll enter the **RemoteRF Server Shell**.
|
|
|
850
850
|
|
|
851
851
|
### Users
|
|
852
852
|
|
|
853
|
-
* `users list` — List all accounts
|
|
853
|
+
* `users list` — List all accounts (this lab's own first, then visitors from partner labs)
|
|
854
854
|
* `users manage` — Manage a specific user (perms / delete / reservations)
|
|
855
855
|
* `users purge` — Delete **all** users
|
|
856
856
|
* `users perms` — Show permissions table
|
|
857
|
+
* `users prune-proxies [--stale DAYS] [-y]` — List federated proxies whose home lab is gone (dry run); `-y` removes them
|
|
857
858
|
|
|
858
859
|
---
|
|
859
860
|
|
|
@@ -116,6 +116,7 @@ def _table() -> list[Command]:
|
|
|
116
116
|
Command(("users manage",), "Change a user's role (interactive)", _daemon("cmd_users_manage"), "Users", where=D, interactive=True),
|
|
117
117
|
Command(("users perms",), "Show permissions", _daemon("cmd_users_perms"), "Users", where=D),
|
|
118
118
|
Command(("users purge",), "Remove ALL accounts", _daemon("cmd_users_purge"), "Users", where=D),
|
|
119
|
+
Command(("users prune-proxies",), "Partner-lab visitors whose home is gone: list them, -y removes [--stale DAYS]", _daemon("cmd_users_prune_proxies"), "Users", where=D),
|
|
119
120
|
# ---- reservations ----
|
|
120
121
|
Command(("reservations list",), "List reservations", _daemon("cmd_reservations_list"), "Reservations", where=D),
|
|
121
122
|
Command(("reservations purge",), "Remove ALL reservations", _daemon("cmd_reservations_purge"), "Reservations", where=D),
|
{remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/cluster.py
RENAMED
|
@@ -51,10 +51,11 @@ from pathlib import Path
|
|
|
51
51
|
from typing import Optional
|
|
52
52
|
from uuid import uuid4
|
|
53
53
|
|
|
54
|
-
from remoterf_federation_core import ContractError, FederationError, TransportFailure, canonical_json
|
|
54
|
+
from remoterf_federation_core import ContractError, FederationError, OwnerSignedFailure, TransportFailure, canonical_json
|
|
55
55
|
|
|
56
56
|
from .. import global_onboarding as g
|
|
57
57
|
from .contracts import ContractStore
|
|
58
|
+
from .enrollment import UNENROLL_CODE
|
|
58
59
|
from .migrations import migrate_gate_f_usergroups_schema
|
|
59
60
|
from .trust import TrustedKeyStore
|
|
60
61
|
|
|
@@ -760,9 +761,9 @@ def admit_account(home, local_account_id: int) -> None:
|
|
|
760
761
|
|
|
761
762
|
|
|
762
763
|
def forget_account(local_account_id: int | None = None) -> None:
|
|
763
|
-
"""A deleted account leaves every cluster, grants and
|
|
764
|
-
stops trying to enrol a ghost. None: every account (a purge)."""
|
|
765
|
-
where, args = ("local_account_id=?
|
|
764
|
+
"""A deleted account (native or proxy) leaves every cluster, grants and
|
|
765
|
+
all, so the tick stops trying to enrol a ghost. None: every account (a purge)."""
|
|
766
|
+
where, args = ("local_account_id=?", (int(local_account_id),)) if local_account_id is not None else ("1", ())
|
|
766
767
|
with _usergroups() as conn:
|
|
767
768
|
rows = conn.execute(f"SELECT DISTINCT cluster_name, local_account_id FROM ClusterAccounts WHERE {where}", args).fetchall()
|
|
768
769
|
for name, account in rows:
|
|
@@ -861,6 +862,33 @@ def redeem_code(local_account_id: int, code_name: str) -> Optional[str]:
|
|
|
861
862
|
|
|
862
863
|
_ENROL_RETRY_FIRST, _ENROL_RETRY_CAP = 30.0, 600.0
|
|
863
864
|
_unreachable: dict[str, tuple[float, float]] = {} # peer -> (retry_at, delay), monotonic
|
|
865
|
+
_revoke_retry: dict[str, tuple[float, float]] = {} # same, for unenrols
|
|
866
|
+
REVOCATION_TTL = 180 * 86400 # an unenrol for a partner we hold no contract with waits this long
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _back_off(table: dict, peer: str) -> None:
|
|
870
|
+
delay = min(table.get(peer, (0.0, _ENROL_RETRY_FIRST / 2))[1] * 2, _ENROL_RETRY_CAP)
|
|
871
|
+
table[peer] = (time.monotonic() + delay, delay)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _forget_departed(plane) -> None:
|
|
875
|
+
"""Drop enrolment records at partners we no longer hold a live contract
|
|
876
|
+
with: that partner may prune our users as "issuer gone", and on a rejoin
|
|
877
|
+
the next tick enrols everyone again (an existing proxy just stays)."""
|
|
878
|
+
if getattr(plane, "path", None) is None:
|
|
879
|
+
return
|
|
880
|
+
try:
|
|
881
|
+
with _usergroups() as conn:
|
|
882
|
+
live = {deployment for row in conn.execute(
|
|
883
|
+
"SELECT deployment_a, deployment_b FROM FederationContracts "
|
|
884
|
+
"WHERE state='active' AND superseded_by_version IS NULL") for deployment in row}
|
|
885
|
+
with closing(sqlite3.connect(plane.path, timeout=10.0)) as conn, conn:
|
|
886
|
+
peers = [row[0] for row in conn.execute("SELECT DISTINCT destination_deployment_id FROM FederatedEnrollments")]
|
|
887
|
+
for peer in peers:
|
|
888
|
+
if peer not in live:
|
|
889
|
+
conn.execute("DELETE FROM FederatedEnrollments WHERE destination_deployment_id=?", (peer,))
|
|
890
|
+
except sqlite3.OperationalError:
|
|
891
|
+
pass # no contracts or HOME device plane here yet
|
|
864
892
|
|
|
865
893
|
|
|
866
894
|
def enroll_students(home, notify=None, *, only_account: int | None = None) -> None:
|
|
@@ -870,6 +898,7 @@ def enroll_students(home, notify=None, *, only_account: int | None = None) -> No
|
|
|
870
898
|
enroller, plane = getattr(home, "federation_enroller", None), getattr(home, "federation_devices", None)
|
|
871
899
|
if enroller is None or plane is None:
|
|
872
900
|
return
|
|
901
|
+
_forget_departed(plane)
|
|
873
902
|
clusters = load()
|
|
874
903
|
with _usergroups() as conn:
|
|
875
904
|
rows = conn.execute(
|
|
@@ -889,6 +918,7 @@ def enroll_students(home, notify=None, *, only_account: int | None = None) -> No
|
|
|
889
918
|
continue # a lab nobody chose gets our accounts only once it shares a device
|
|
890
919
|
if _unreachable.get(peer, (0.0, 0.0))[0] > time.monotonic():
|
|
891
920
|
continue # unreachable a moment ago: back off rather than redial every tick
|
|
921
|
+
subject = _subject_of(plane, account)
|
|
892
922
|
try:
|
|
893
923
|
enroller.outbound.execute(
|
|
894
924
|
local_account_id=account, destination_deployment_id=peer,
|
|
@@ -897,13 +927,76 @@ def enroll_students(home, notify=None, *, only_account: int | None = None) -> No
|
|
|
897
927
|
)
|
|
898
928
|
except (FederationError, OSError) as exc:
|
|
899
929
|
if isinstance(exc, TransportFailure) and exc.code == "route_unavailable":
|
|
900
|
-
|
|
901
|
-
_unreachable[peer] = (time.monotonic() + delay, delay)
|
|
930
|
+
_back_off(_unreachable, peer)
|
|
902
931
|
notify(f"[cluster] {name}: could not enrol account {account} at "
|
|
903
932
|
f"{party.get('display_name') or peer}: {getattr(exc, 'code', exc)}")
|
|
904
933
|
continue
|
|
905
934
|
_unreachable.pop(peer, None)
|
|
906
935
|
plane.record_enrollment(local_account_id=account, destination_deployment_id=peer)
|
|
936
|
+
if subject and _subject_of(plane, account) is None:
|
|
937
|
+
# Deleted while the enrolment was in flight: its delete queued
|
|
938
|
+
# nothing for this peer, so queue the unenrol here.
|
|
939
|
+
with closing(sqlite3.connect(plane.path, timeout=10.0)) as conn, conn:
|
|
940
|
+
conn.execute("INSERT OR IGNORE INTO FederatedRevocations VALUES (?,?,?)",
|
|
941
|
+
(subject, peer, int(time.time())))
|
|
942
|
+
conn.execute("DELETE FROM FederatedEnrollments WHERE local_account_id=?", (account,))
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _subject_of(plane, account: int) -> str | None:
|
|
946
|
+
if getattr(plane, "path", None) is None:
|
|
947
|
+
return None
|
|
948
|
+
with closing(sqlite3.connect(plane.path, timeout=10.0)) as conn:
|
|
949
|
+
row = conn.execute("SELECT subject_id FROM LocalAccountSubjects WHERE local_account_id=?",
|
|
950
|
+
(account,)).fetchone()
|
|
951
|
+
return row[0] if row else None
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def deliver_revocations(home, notify=None) -> None:
|
|
955
|
+
"""HOME: tell each partner a deleted account was enrolled at to drop its
|
|
956
|
+
proxy. A row stays until the partner answers; an older partner answers
|
|
957
|
+
code_invalid (keeps its orphan). A partner we hold no contract with right
|
|
958
|
+
now (left, kicked, past the candidate cap) keeps its row until it is back
|
|
959
|
+
or REVOCATION_TTL runs out."""
|
|
960
|
+
notify = notify or (lambda text: None)
|
|
961
|
+
enroller, plane = getattr(home, "federation_enroller", None), getattr(home, "federation_devices", None)
|
|
962
|
+
if enroller is None or plane is None:
|
|
963
|
+
return
|
|
964
|
+
path = _db_path().with_name("reservations.db")
|
|
965
|
+
try:
|
|
966
|
+
with closing(sqlite3.connect(path, timeout=10.0)) as conn:
|
|
967
|
+
rows = conn.execute("SELECT subject_id, destination_deployment_id, queued_at FROM FederatedRevocations").fetchall()
|
|
968
|
+
except sqlite3.OperationalError:
|
|
969
|
+
return # no HOME device plane here yet
|
|
970
|
+
if not rows:
|
|
971
|
+
return
|
|
972
|
+
contracts = {}
|
|
973
|
+
for candidate in plane.permissions._candidates(int(plane.clock()), "FEDERATED_ENROLL")[0]:
|
|
974
|
+
contracts.setdefault(candidate["deployment_id"], candidate)
|
|
975
|
+
now = time.time()
|
|
976
|
+
for subject, peer, queued_at in rows:
|
|
977
|
+
contract = contracts.get(peer)
|
|
978
|
+
if contract is None:
|
|
979
|
+
if int(queued_at) > now - REVOCATION_TTL:
|
|
980
|
+
continue # no contract right now: wait for one, the TTL lets go of a partner that is gone
|
|
981
|
+
else:
|
|
982
|
+
if _revoke_retry.get(peer, (0.0, 0.0))[0] > time.monotonic():
|
|
983
|
+
continue # failed a moment ago: skip this peer's rows until the backoff runs out
|
|
984
|
+
try:
|
|
985
|
+
enroller.outbound.execute(
|
|
986
|
+
local_account_id=None, revoked_subject_id=subject, destination_deployment_id=peer,
|
|
987
|
+
contract_id=contract["contract_id"], contract_version=contract["contract_version"],
|
|
988
|
+
action="federated_enroll", code_name=UNENROLL_CODE,
|
|
989
|
+
)
|
|
990
|
+
except (FederationError, OSError) as exc:
|
|
991
|
+
# code_invalid (signed): a partner too old to know the code keeps its orphan.
|
|
992
|
+
if not (isinstance(exc, OwnerSignedFailure) and exc.code == "code_invalid"):
|
|
993
|
+
_back_off(_revoke_retry, peer)
|
|
994
|
+
notify(f"[cluster] could not remove a deleted account at {peer}: {getattr(exc, 'code', exc)}")
|
|
995
|
+
continue
|
|
996
|
+
_revoke_retry.pop(peer, None)
|
|
997
|
+
with closing(sqlite3.connect(path, timeout=10.0)) as conn, conn:
|
|
998
|
+
conn.execute("DELETE FROM FederatedRevocations WHERE subject_id=? AND destination_deployment_id=?",
|
|
999
|
+
(subject, peer))
|
|
907
1000
|
|
|
908
1001
|
|
|
909
1002
|
# -----------------------------
|
|
@@ -1183,6 +1276,8 @@ def tick(notify=None, home=None) -> None:
|
|
|
1183
1276
|
"""One agent pass: move every cluster and member one step closer to active,
|
|
1184
1277
|
then (given the HOME runtime) enrol students wherever they are missing."""
|
|
1185
1278
|
notify = notify or (lambda text: None)
|
|
1279
|
+
if home is not None:
|
|
1280
|
+
deliver_revocations(home, notify)
|
|
1186
1281
|
# Rosters, status notes and the public clusters: one GET for the whole pass.
|
|
1187
1282
|
board = g.call("GET", "/v2/broker/clusters/roster")
|
|
1188
1283
|
_walk_in(board.get("pools", []), notify)
|
{remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/devices.py
RENAMED
|
@@ -210,6 +210,10 @@ class _DeviceHandlerBase:
|
|
|
210
210
|
raise FederationBusinessError("principal_unknown")
|
|
211
211
|
if row[2] != "active":
|
|
212
212
|
raise FederationBusinessError("principal_disabled")
|
|
213
|
+
# At most hourly: `users prune-proxies --stale` reads it as the visitor's last activity.
|
|
214
|
+
now = int(time.time())
|
|
215
|
+
tx.execute("UPDATE FederatedPrincipals SET last_seen_at=? WHERE id=? AND last_seen_at<?",
|
|
216
|
+
(now, row[0], now - 3600))
|
|
213
217
|
return str(row[0]), int(row[1])
|
|
214
218
|
|
|
215
219
|
# -- result shaping ------------------------------------------------
|
|
@@ -669,6 +673,8 @@ def build_server_gate_h_executor(
|
|
|
669
673
|
signing_kid=signing_kid,
|
|
670
674
|
display_name=display_name,
|
|
671
675
|
clock=clock,
|
|
676
|
+
account_remover=(lambda username: reservation_handler.remove_user(username=username))
|
|
677
|
+
if reservation_handler is not None else None,
|
|
672
678
|
)
|
|
673
679
|
for action, handler in build_device_handlers(
|
|
674
680
|
reservations_path=reservations_path,
|
{remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/enrollment.py
RENAMED
|
@@ -27,6 +27,9 @@ from remoterf_federation_core import (
|
|
|
27
27
|
from .destination import ActionRegistry, FederationBusinessError, ServerFederationExecutor
|
|
28
28
|
|
|
29
29
|
CLUSTER_CODE = "cluster"
|
|
30
|
+
# A HOME that deleted the account tells each partner to drop its proxy. The
|
|
31
|
+
# `cluster.` prefix is reserved and minted codes are uppercase: no collision.
|
|
32
|
+
UNENROLL_CODE = "cluster.unenroll"
|
|
30
33
|
from .migrations import configure_connection, migrate_gate_f_usergroups_schema
|
|
31
34
|
from .principals import PreparedFederatedAccount, prepare_federated_account
|
|
32
35
|
|
|
@@ -310,6 +313,7 @@ class ServerFederatedEnrollHandler:
|
|
|
310
313
|
perms_path=None,
|
|
311
314
|
clock=None,
|
|
312
315
|
account_preparer=prepare_federated_account,
|
|
316
|
+
account_remover=None,
|
|
313
317
|
):
|
|
314
318
|
self.usergroups_path = Path(usergroups_path)
|
|
315
319
|
self.reservations_path = Path(reservations_path)
|
|
@@ -318,6 +322,7 @@ class ServerFederatedEnrollHandler:
|
|
|
318
322
|
self.display_name = str(display_name)
|
|
319
323
|
self.clock = clock or (lambda: int(time.time()))
|
|
320
324
|
self.account_preparer = account_preparer
|
|
325
|
+
self.account_remover = account_remover or _remove_account
|
|
321
326
|
with sqlite3.connect(self.usergroups_path) as conn:
|
|
322
327
|
migrate_gate_f_usergroups_schema(conn)
|
|
323
328
|
|
|
@@ -325,6 +330,18 @@ class ServerFederatedEnrollHandler:
|
|
|
325
330
|
"""Never mutates owner authorization; invalid cases wait for owner tx."""
|
|
326
331
|
claims = action.claims
|
|
327
332
|
payload = action.request.value["payload"]
|
|
333
|
+
if payload["code_name"] == UNENROLL_CODE:
|
|
334
|
+
# Runs only for a verified, current grant, and only ever touches
|
|
335
|
+
# the grant's own (iss, sub): a lab can revoke nobody else's users.
|
|
336
|
+
with sqlite3.connect(self.reservations_path) as conn:
|
|
337
|
+
row = conn.execute(
|
|
338
|
+
"SELECT a.username FROM FederatedAccountBindings b JOIN Accounts a ON a.acc_id=b.local_account_id "
|
|
339
|
+
"WHERE b.issuer_deployment_id=? AND b.remote_subject_id=? AND a.auth_type='federated'",
|
|
340
|
+
(claims["iss"], claims["sub"]),
|
|
341
|
+
).fetchone()
|
|
342
|
+
if row is not None:
|
|
343
|
+
self.account_remover(row[0])
|
|
344
|
+
return None
|
|
328
345
|
if payload["code_name"] == CLUSTER_CODE:
|
|
329
346
|
# No code to check: the home lab's signature under an active
|
|
330
347
|
# cluster contract is the vouching. Prepare a proxy only once.
|
|
@@ -431,10 +448,29 @@ class ServerFederatedEnrollHandler:
|
|
|
431
448
|
membership_present=True, groups=[{"group_id": "0", "group_name": f"cluster {name}"}], now=now,
|
|
432
449
|
)
|
|
433
450
|
|
|
451
|
+
def _unenroll(self, tx, action, now):
|
|
452
|
+
"""The proxy (and its reservations) went in prepare(); drop whatever
|
|
453
|
+
rows here still name it. Unknown subjects are fine: idempotent."""
|
|
454
|
+
claims = action.claims
|
|
455
|
+
principal = get_principal_tx(tx, claims["iss"], claims["sub"])
|
|
456
|
+
if principal is not None:
|
|
457
|
+
for table, column in (("UsersLookup", "user_id"), ("ClusterMemberships", "local_account_id"),
|
|
458
|
+
("ClusterAccounts", "local_account_id")):
|
|
459
|
+
tx.execute(f"DELETE FROM {table} WHERE {column}=?", (int(principal[3]),))
|
|
460
|
+
tx.execute("DELETE FROM FederatedPrincipals WHERE id=?", (principal[0],))
|
|
461
|
+
self._audit(tx, action, code="ok", principal_id=None,
|
|
462
|
+
metadata={"unenrolled": principal is not None})
|
|
463
|
+
return self._result(
|
|
464
|
+
action, redemption_id=str(uuid5(NAMESPACE_URL, f"remoterf-unenroll:{claims['iss']}:{claims['sub']}")),
|
|
465
|
+
membership_present=False, groups=[], now=now,
|
|
466
|
+
)
|
|
467
|
+
|
|
434
468
|
def __call__(self, tx, action):
|
|
435
469
|
claims = action.claims
|
|
436
470
|
payload = action.request.value["payload"]
|
|
437
471
|
now = int(self.clock())
|
|
472
|
+
if payload["code_name"] == UNENROLL_CODE:
|
|
473
|
+
return self._unenroll(tx, action, now)
|
|
438
474
|
if payload["code_name"] == CLUSTER_CODE:
|
|
439
475
|
return self._cluster_enroll(tx, action, now)
|
|
440
476
|
receipt = lookup_redemption_tx(
|
|
@@ -579,6 +615,12 @@ class ServerFederatedEnrollHandler:
|
|
|
579
615
|
)
|
|
580
616
|
|
|
581
617
|
|
|
618
|
+
def _remove_account(username: str) -> None:
|
|
619
|
+
from ..reservation import reservation_handler
|
|
620
|
+
|
|
621
|
+
reservation_handler.remove_user(username=username)
|
|
622
|
+
|
|
623
|
+
|
|
582
624
|
class ServerFederatedEnrollmentHome:
|
|
583
625
|
def __init__(self, outbound):
|
|
584
626
|
self.outbound = outbound
|
{remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/home.py
RENAMED
|
@@ -64,6 +64,7 @@ from remoterf_federation_core import (
|
|
|
64
64
|
verify_eddsa_jwt,
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
+
from .enrollment import UNENROLL_CODE
|
|
67
68
|
from .migrations import configure_connection, migrate_gate_e_usergroups_schema
|
|
68
69
|
from .peers import PeerStore
|
|
69
70
|
|
|
@@ -202,6 +203,28 @@ class ServerHomeGrantIssuer:
|
|
|
202
203
|
raise FederationError("native_account_required")
|
|
203
204
|
return canonical_uuid(row["subject_id"])
|
|
204
205
|
|
|
206
|
+
def _subject(self, conn, local_account_id, revoked_subject_id, destination, action, code_name) -> str:
|
|
207
|
+
"""The native account's subject; or, for an unenrol only, a deleted
|
|
208
|
+
account's subject still queued for that partner."""
|
|
209
|
+
if revoked_subject_id is None:
|
|
210
|
+
if code_name == UNENROLL_CODE:
|
|
211
|
+
raise FederationError("native_account_required")
|
|
212
|
+
return self._native_subject(local_account_id, conn=conn)
|
|
213
|
+
subject = canonical_uuid(revoked_subject_id)
|
|
214
|
+
if local_account_id is not None or action != "federated_enroll" or code_name != UNENROLL_CODE:
|
|
215
|
+
raise FederationError("native_account_required")
|
|
216
|
+
try:
|
|
217
|
+
queued = conn.execute(
|
|
218
|
+
"SELECT 1 FROM FederatedRevocations WHERE subject_id=? AND destination_deployment_id=?",
|
|
219
|
+
(subject, destination),
|
|
220
|
+
).fetchone()
|
|
221
|
+
live = conn.execute("SELECT 1 FROM LocalAccountSubjects WHERE subject_id=?", (subject,)).fetchone()
|
|
222
|
+
except sqlite3.OperationalError:
|
|
223
|
+
queued = None
|
|
224
|
+
if queued is None or live is not None:
|
|
225
|
+
raise FederationError("native_account_required")
|
|
226
|
+
return subject
|
|
227
|
+
|
|
205
228
|
def issue(
|
|
206
229
|
self,
|
|
207
230
|
*,
|
|
@@ -215,6 +238,7 @@ class ServerHomeGrantIssuer:
|
|
|
215
238
|
device_payload: Mapping | None = None,
|
|
216
239
|
enrollee: Mapping | None = None,
|
|
217
240
|
jti: str | None = None,
|
|
241
|
+
revoked_subject_id: str | None = None,
|
|
218
242
|
) -> IssuedGrant:
|
|
219
243
|
destination = canonical_uuid(destination_deployment_id)
|
|
220
244
|
request_id = canonical_uuid(request_id or str(uuid4()))
|
|
@@ -239,7 +263,7 @@ class ServerHomeGrantIssuer:
|
|
|
239
263
|
try:
|
|
240
264
|
configure_connection(subject_conn)
|
|
241
265
|
subject_conn.execute("BEGIN IMMEDIATE")
|
|
242
|
-
subject = self.
|
|
266
|
+
subject = self._subject(subject_conn, local_account_id, revoked_subject_id, destination, action, code_name)
|
|
243
267
|
except Exception:
|
|
244
268
|
conn.rollback()
|
|
245
269
|
conn.close()
|
|
@@ -304,7 +328,7 @@ class ServerHomeGrantIssuer:
|
|
|
304
328
|
issuance_hook = getattr(self, "_issuance_lock_hook", None)
|
|
305
329
|
if issuance_hook is not None:
|
|
306
330
|
issuance_hook()
|
|
307
|
-
subject = self.
|
|
331
|
+
subject = self._subject(subject_conn, local_account_id, revoked_subject_id, destination, action, code_name)
|
|
308
332
|
claims = build_grant_claims(
|
|
309
333
|
issuer_deployment_id=self.local_deployment_id,
|
|
310
334
|
subject_id=subject,
|
|
@@ -627,6 +651,7 @@ class HomeFederationOutbound:
|
|
|
627
651
|
retry_ambiguous: bool = True,
|
|
628
652
|
deadline: float | None = None,
|
|
629
653
|
request_frame: bytes | None = None,
|
|
654
|
+
revoked_subject_id: str | None = None,
|
|
630
655
|
) -> dict:
|
|
631
656
|
"""Verified result; an ok device_rpc result also carries ``frame``, its
|
|
632
657
|
raw response. With ``request_frame`` the caller leaves ``request_b64``
|
|
@@ -698,6 +723,7 @@ class HomeFederationOutbound:
|
|
|
698
723
|
# A downgrade resend keeps the JTI: if the refusal was forged
|
|
699
724
|
# after the first form ran, the destination answers "replayed".
|
|
700
725
|
**({"jti": resend_jti} if resend_jti else {}),
|
|
726
|
+
**({"revoked_subject_id": revoked_subject_id} if revoked_subject_id else {}),
|
|
701
727
|
)
|
|
702
728
|
resend_jti = None
|
|
703
729
|
frame = request_frame if detached else b""
|
{remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/migrations.py
RENAMED
|
@@ -478,6 +478,7 @@ _GATE_H_RESERVATIONS_CHECKSUM = hashlib.sha256(
|
|
|
478
478
|
# HOME side of the device plane: which partners a native account is enrolled
|
|
479
479
|
# at, a HOME-local integer id for each partner device the Client has seen, and
|
|
480
480
|
# the HOME-minted reservation rows that route a device token to the owner.
|
|
481
|
+
# FederatedRevocations: deleted accounts' partners still to be told (the tick drains it).
|
|
481
482
|
_HOME_DEVICES_RESERVATIONS_DDL = """
|
|
482
483
|
CREATE TABLE IF NOT EXISTS FederatedEnrollments (
|
|
483
484
|
local_account_id INTEGER NOT NULL,
|
|
@@ -485,6 +486,12 @@ CREATE TABLE IF NOT EXISTS FederatedEnrollments (
|
|
|
485
486
|
enrolled_at INTEGER NOT NULL,
|
|
486
487
|
PRIMARY KEY(local_account_id,destination_deployment_id)
|
|
487
488
|
);
|
|
489
|
+
CREATE TABLE IF NOT EXISTS FederatedRevocations (
|
|
490
|
+
subject_id TEXT NOT NULL CHECK(length(subject_id)=36),
|
|
491
|
+
destination_deployment_id TEXT NOT NULL CHECK(length(destination_deployment_id)=36),
|
|
492
|
+
queued_at INTEGER NOT NULL,
|
|
493
|
+
PRIMARY KEY(subject_id,destination_deployment_id)
|
|
494
|
+
);
|
|
488
495
|
CREATE TABLE IF NOT EXISTS FederatedDeviceAliases (
|
|
489
496
|
alias_id INTEGER PRIMARY KEY,
|
|
490
497
|
owner_deployment_id TEXT NOT NULL CHECK(length(owner_deployment_id)=36),
|
|
@@ -393,6 +393,7 @@ def build_server_gate_g_executor(
|
|
|
393
393
|
display_name,
|
|
394
394
|
perms_path=None,
|
|
395
395
|
clock=None,
|
|
396
|
+
account_remover=None,
|
|
396
397
|
):
|
|
397
398
|
"""Final Phase-1 destination registry: enroll plus permissions read."""
|
|
398
399
|
|
|
@@ -406,6 +407,7 @@ def build_server_gate_g_executor(
|
|
|
406
407
|
local_deployment_id=local_deployment_id,
|
|
407
408
|
display_name=display_name,
|
|
408
409
|
clock=clock,
|
|
410
|
+
account_remover=account_remover,
|
|
409
411
|
)
|
|
410
412
|
permissions = ServerPermissionsReadHandler(
|
|
411
413
|
local_deployment_id=local_deployment_id,
|
{remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/federation/principals.py
RENAMED
|
@@ -267,3 +267,64 @@ class FederatedPrincipalStore:
|
|
|
267
267
|
).fetchone()
|
|
268
268
|
assert final is not None
|
|
269
269
|
return self._decode(final)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def orphan_proxies(reservations_path, usergroups_path, *, stale_days: int | None = None, now: int | None = None) -> list[dict]:
|
|
273
|
+
"""Federated proxies nothing else will clean up: their home is no longer a
|
|
274
|
+
partner (left Global, kicked, contract revoked), half their rows are gone,
|
|
275
|
+
or -- only when asked -- inactive for ``stale_days``: no device action,
|
|
276
|
+
reservation or enrolment in that time and nothing booked ahead. A pruned
|
|
277
|
+
proxy whose home still lists it loses access here until that home re-enrols it."""
|
|
278
|
+
from contextlib import closing
|
|
279
|
+
|
|
280
|
+
now = int(time.time() if now is None else now)
|
|
281
|
+
with closing(sqlite3.connect(usergroups_path, timeout=10.0)) as conn:
|
|
282
|
+
principals = conn.execute(
|
|
283
|
+
"SELECT id, local_account_id, issuer_deployment_id, last_seen_at FROM FederatedPrincipals"
|
|
284
|
+
).fetchall()
|
|
285
|
+
partners = {deployment for row in conn.execute(
|
|
286
|
+
"SELECT deployment_a, deployment_b FROM FederationContracts "
|
|
287
|
+
"WHERE state='active' AND superseded_by_version IS NULL") for deployment in row}
|
|
288
|
+
with closing(sqlite3.connect(reservations_path, timeout=10.0)) as conn:
|
|
289
|
+
accounts = dict(conn.execute("SELECT acc_id, username FROM Accounts WHERE auth_type='federated'").fetchall())
|
|
290
|
+
homes = dict(conn.execute("SELECT local_account_id, issuer_deployment_id FROM FederatedAccountBindings").fetchall())
|
|
291
|
+
ahead = dict(conn.execute(
|
|
292
|
+
"SELECT username, COUNT(*) FROM Reservations WHERE end_time > ? GROUP BY username", (datetime.now(),)
|
|
293
|
+
).fetchall())
|
|
294
|
+
# Activity beyond the principal's own last_seen_at: past bookings not yet swept, and booking times.
|
|
295
|
+
active = {}
|
|
296
|
+
for username, end in conn.execute("SELECT username, MAX(end_time) FROM Reservations GROUP BY username"):
|
|
297
|
+
try:
|
|
298
|
+
active[username] = int(datetime.fromisoformat(str(end)).timestamp())
|
|
299
|
+
except ValueError:
|
|
300
|
+
pass
|
|
301
|
+
try:
|
|
302
|
+
booked = dict(conn.execute(
|
|
303
|
+
"SELECT local_account_id, MAX(created_at) FROM FederatedReservations GROUP BY local_account_id"))
|
|
304
|
+
except sqlite3.OperationalError:
|
|
305
|
+
booked = {}
|
|
306
|
+
out, seen = [], set()
|
|
307
|
+
|
|
308
|
+
def add(account, username, issuer, last_active, reason, principal_id=None):
|
|
309
|
+
out.append({"username": username, "local_account_id": account, "principal_id": principal_id,
|
|
310
|
+
"issuer": issuer, "last_active": last_active, "reservations": ahead.get(username, 0),
|
|
311
|
+
"reason": reason})
|
|
312
|
+
|
|
313
|
+
for principal_id, account, issuer, last_seen in principals:
|
|
314
|
+
account = int(account)
|
|
315
|
+
seen.add(account)
|
|
316
|
+
username = accounts.get(account)
|
|
317
|
+
last_active = max(int(last_seen or 0), active.get(username, 0), int(booked.get(account) or 0)) or None
|
|
318
|
+
if username is None:
|
|
319
|
+
reason = "dangling"
|
|
320
|
+
elif issuer not in partners:
|
|
321
|
+
reason = "issuer gone"
|
|
322
|
+
elif stale_days is not None and not ahead.get(username) and (last_active or 0) < now - stale_days * 86400:
|
|
323
|
+
reason = "stale"
|
|
324
|
+
else:
|
|
325
|
+
continue
|
|
326
|
+
add(account, username, issuer, last_active, reason, principal_id)
|
|
327
|
+
for account, username in accounts.items():
|
|
328
|
+
if int(account) not in seen:
|
|
329
|
+
add(int(account), username, homes.get(account), None, "dangling")
|
|
330
|
+
return out
|
{remoterf_server-1.4.8 → remoterf_server-1.4.10}/src/remoteRF_server/server/global_onboarding.py
RENAMED
|
@@ -59,8 +59,9 @@ reach your users as soon as you are in (`cluster remove` leaves such a
|
|
|
59
59
|
cluster). Joining Global shares none of your hardware on its own.
|
|
60
60
|
|
|
61
61
|
Once an hour it sends Global a short note: this deployment's name and
|
|
62
|
-
ID, version, uptime, how many accounts
|
|
63
|
-
|
|
62
|
+
ID, version, uptime, how many accounts (and how many visit from partner
|
|
63
|
+
labs), what devices it has, how many reservations are active, and your
|
|
64
|
+
LAN address. Nothing else leaves your
|
|
64
65
|
server -- no credentials, no user or reservation details -- and the
|
|
65
66
|
connection only goes outward.
|
|
66
67
|
|
|
@@ -179,14 +180,17 @@ def account_name(username: str) -> str:
|
|
|
179
180
|
def invite(code_name: str, *, lan_only: bool = False) -> str:
|
|
180
181
|
"""What to hand a new user: the form `remoterf --register` accepts.
|
|
181
182
|
|
|
182
|
-
On a live Global deployment that is `<
|
|
183
|
-
client
|
|
183
|
+
On a live Global deployment that is `<deployment>/<code>` (e.g. `ucla/AB12C`),
|
|
184
|
+
which the client expands to `<deployment>.global.remoterf.net` from anywhere;
|
|
185
|
+
a lab on its own domain keeps the full `<hostname>/<code>`. A LAN-only group's code (or
|
|
184
186
|
a deployment that is not on Global) is the bare code, entered after
|
|
185
187
|
`remoterf -c -a <lan address>`.
|
|
186
188
|
"""
|
|
187
|
-
if state() == "active" and not lan_only
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
host = hostname() if state() == "active" and not lan_only else None
|
|
190
|
+
if not host:
|
|
191
|
+
return code_name
|
|
192
|
+
slug, _, domain = host.partition(".")
|
|
193
|
+
return f"{slug if domain == 'global.remoterf.net' else host}/{code_name}"
|
|
190
194
|
|
|
191
195
|
|
|
192
196
|
# -----------------------------
|
|
@@ -698,6 +702,7 @@ STATUS_FIELDS = frozenset(
|
|
|
698
702
|
"server_version",
|
|
699
703
|
"uptime_s",
|
|
700
704
|
"users",
|
|
705
|
+
"proxies",
|
|
701
706
|
"devices",
|
|
702
707
|
"active_reservations",
|
|
703
708
|
"lan_address",
|
|
@@ -783,7 +788,9 @@ def status_payload() -> dict:
|
|
|
783
788
|
payload = {
|
|
784
789
|
"server_version": server_version(),
|
|
785
790
|
"uptime_s": _uptime_seconds(),
|
|
786
|
-
|
|
791
|
+
# A partner's user visiting here is counted at their own lab, not again here.
|
|
792
|
+
"users": _count(reservations, "SELECT COUNT(*) FROM Accounts WHERE auth_type != 'federated'"),
|
|
793
|
+
"proxies": _count(reservations, "SELECT COUNT(*) FROM Accounts WHERE auth_type = 'federated'"),
|
|
787
794
|
"devices": device_summary(),
|
|
788
795
|
"active_reservations": _count(
|
|
789
796
|
reservations,
|
|
@@ -807,7 +814,14 @@ def send_liveness() -> None:
|
|
|
807
814
|
def send_status() -> None:
|
|
808
815
|
from datetime import datetime, timezone
|
|
809
816
|
|
|
810
|
-
|
|
817
|
+
payload = status_payload()
|
|
818
|
+
try:
|
|
819
|
+
call("POST", REPORT_PATH, {"kind": "status", "status": payload})
|
|
820
|
+
except GlobalError as exc:
|
|
821
|
+
if exc.code != "invalid_status":
|
|
822
|
+
raise
|
|
823
|
+
# A Global older than `proxies` takes the note as it was before.
|
|
824
|
+
call("POST", REPORT_PATH, {"kind": "status", "status": {k: v for k, v in payload.items() if k != "proxies"}})
|
|
811
825
|
update(GLOBAL_LAST_REPORT_AT=datetime.now(timezone.utc).isoformat())
|
|
812
826
|
|
|
813
827
|
|
|
@@ -1245,6 +1245,54 @@ def cmd_users_manage(argv): set_acc()
|
|
|
1245
1245
|
def cmd_users_perms(argv): perms.print_perms()
|
|
1246
1246
|
def cmd_users_purge(argv): reservation_handler.remove_all_users()
|
|
1247
1247
|
def cmd_reservations_list(argv): reservation_handler.print_all_reservations()
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
def cmd_users_prune_proxies(argv):
|
|
1251
|
+
"""Dry run unless -y: proxies whose home left, half-deleted ones, and with
|
|
1252
|
+
--stale DAYS those unseen that long with nothing booked."""
|
|
1253
|
+
import sqlite3
|
|
1254
|
+
from contextlib import closing
|
|
1255
|
+
|
|
1256
|
+
from ..common.utils.panel import print_table
|
|
1257
|
+
from .federation import cluster
|
|
1258
|
+
from .federation.principals import orphan_proxies
|
|
1259
|
+
from .user_group_handler import user_group_handler
|
|
1260
|
+
|
|
1261
|
+
stale = None
|
|
1262
|
+
if "--stale" in argv:
|
|
1263
|
+
try:
|
|
1264
|
+
stale = int(argv[argv.index("--stale") + 1])
|
|
1265
|
+
except (IndexError, ValueError):
|
|
1266
|
+
printf("Usage: users prune-proxies [--stale DAYS] [-y]", Sty.RED)
|
|
1267
|
+
return 2
|
|
1268
|
+
orphans = orphan_proxies(reservation_handler.filepath, user_group_handler.filepath, stale_days=stale)
|
|
1269
|
+
names = {}
|
|
1270
|
+
for item in cluster.load().values():
|
|
1271
|
+
for peer, party in cluster._parties(item):
|
|
1272
|
+
names.setdefault(peer, party.get("display_name"))
|
|
1273
|
+
rows = [{"username": o["username"] or "(no account)", "home": names.get(o["issuer"]) or o["issuer"] or "?",
|
|
1274
|
+
"last_active": datetime.fromtimestamp(o["last_active"]).strftime("%Y-%m-%d") if o["last_active"] else "",
|
|
1275
|
+
"reservations": o["reservations"], "reason": o["reason"]} for o in orphans]
|
|
1276
|
+
print_table("Orphaned federated proxies", rows, ["username", "home", "last_active", "reservations", "reason"],
|
|
1277
|
+
empty="No orphaned proxies.")
|
|
1278
|
+
if not orphans:
|
|
1279
|
+
return 0
|
|
1280
|
+
if any(o["reason"] == "stale" for o in orphans):
|
|
1281
|
+
printf("A 'stale' visitor may still be real: removing one ends their access here until "
|
|
1282
|
+
"their home lab enrols them again.", Sty.YELLOW)
|
|
1283
|
+
if not ("-y" in argv or "--yes" in argv):
|
|
1284
|
+
printf(f"{len(orphans)} orphaned proxies. Nothing removed; run `users prune-proxies"
|
|
1285
|
+
f"{f' --stale {stale}' if stale is not None else ''} -y` to remove them.", Sty.GRAY)
|
|
1286
|
+
return 0
|
|
1287
|
+
for o in orphans:
|
|
1288
|
+
if o["username"]:
|
|
1289
|
+
reservation_handler.remove_user(username=o["username"])
|
|
1290
|
+
else:
|
|
1291
|
+
with closing(sqlite3.connect(user_group_handler.filepath, timeout=10.0)) as conn, conn:
|
|
1292
|
+
conn.execute("DELETE FROM FederatedPrincipals WHERE id=?", (o["principal_id"],))
|
|
1293
|
+
cluster.forget_account(o["local_account_id"])
|
|
1294
|
+
printf(f"Removed {len(orphans)} orphaned proxies.", Sty.GRAY)
|
|
1295
|
+
return 0
|
|
1248
1296
|
def cmd_reservations_purge(argv): reservation_handler.remove_all_reservations()
|
|
1249
1297
|
def cmd_db_purge(argv): reservation_handler.rm_db()
|
|
1250
1298
|
def cmd_stats_purge(argv):
|