Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bdc9a3b
feat(superlink): add generic federation can_execute hook
chongshenng Mar 30, 2026
820adb9
feat(superlink): add generic federation can_execute hook
chongshenng Mar 30, 2026
b3eec03
feat(superlink): enforce start run entitlement via can_execute
chongshenng Mar 30, 2026
c83f24f
refactor(superlink): type can_execute action with ActionType
chongshenng Mar 30, 2026
a77c2b0
Format code
chongshenng Mar 30, 2026
2739fd2
refactor(superlink): type can_execute action with ActionType
chongshenng Mar 30, 2026
73503d5
refactor(superlink): use typed can_execute args in StartRun
chongshenng Mar 30, 2026
9e270ae
Format
chongshenng Mar 31, 2026
bcbad42
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Mar 31, 2026
828afdc
Merge branch 'main' into feat/pr1-can-execute-hook
chongshenng Mar 31, 2026
2fd8a3f
Rebase
chongshenng Mar 31, 2026
a7d1052
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Mar 31, 2026
c515134
Disable too many public methods
chongshenng Mar 31, 2026
9a5960e
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Mar 31, 2026
fc2f1a9
Merge branch 'main' into feat/pr1-can-execute-hook
chongshenng Apr 1, 2026
7946f43
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Apr 1, 2026
2a20769
Update
chongshenng Apr 1, 2026
d4dfb1d
Revert
chongshenng Apr 1, 2026
37e7a9c
Fix
chongshenng Apr 1, 2026
cf65e6d
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Apr 1, 2026
99806fb
Improve
chongshenng Apr 1, 2026
1b4c70d
Fix
chongshenng Apr 1, 2026
8568361
Merge branch 'main' into feat/pr1-can-execute-hook
chongshenng Apr 1, 2026
9865ed5
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Apr 1, 2026
8c2278f
Set typing
chongshenng Apr 1, 2026
b0b9a85
Sort
chongshenng Apr 1, 2026
ed8ae0d
Fix
chongshenng Apr 1, 2026
23ce56d
Merge branch 'feat/pr1-can-execute-hook' into feat/pr2-start-run-enti…
chongshenng Apr 1, 2026
4d78827
Rebase
chongshenng Apr 1, 2026
d1a7c18
Fix
chongshenng Apr 1, 2026
9eaa04f
Merge branch 'main' into feat/pr2-start-run-entitlement
chongshenng Apr 1, 2026
dace39a
Update framework/py/flwr/superlink/servicer/control/control_servicer.py
chongshenng Apr 1, 2026
2eed4ed
Update framework/py/flwr/superlink/servicer/control/control_servicer.py
chongshenng Apr 1, 2026
2fcc1d8
Update framework/py/flwr/superlink/servicer/control/control_servicer.py
chongshenng Apr 1, 2026
ee963cb
Add simulation test coverage
chongshenng Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,18 @@
from flwr.proto.federation_pb2 import Federation # pylint: disable=E0611
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
from flwr.supercore.constant import NOOP_FEDERATION, PLATFORM_API_URL, RunType
from flwr.supercore.constant import (
NOOP_FEDERATION,
PLATFORM_API_URL,
ActionType,
RunTime,
RunType,
)
from flwr.supercore.error import ApiErrorCode, FlowerError, rpc_error_translator
from flwr.supercore.ffs import FfsFactory
from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
from flwr.supercore.primitives.asymmetric import bytes_to_public_key, uses_nist_ec_curve
from flwr.supercore.typing import StartRunContext
from flwr.supercore.utils import parse_app_spec, request_download_link
from flwr.superlink.artifact_provider import ArtifactProvider
from flwr.superlink.auth_plugin import ControlAuthnPlugin
Expand Down Expand Up @@ -198,6 +205,18 @@ def StartRun( # pylint: disable=too-many-locals, too-many-branches, too-many-st
resolved_federation_config.CopyFrom(sim_cfg)
resolved_federation_config.MergeFrom(request.override_federation_config)

runtime = RunTime.SIMULATION if sim_cfg else RunTime.DEPLOYMENT
if not state.federation_manager.can_execute(
flwr_aid,
ActionType.START_RUN,
StartRunContext(federation=federation, runtime=runtime),
):
raise FlowerError(
ApiErrorCode.NO_PERMISSIONS,
f"'{ActionType.START_RUN}' action cannot be executed on federation "
f"'{federation}'.",
)

try:
# Validate user config overrides matches keys in run config in FAB
fab_config = get_fab_config(fab_file)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,18 @@
)
from flwr.proto.federation_pb2 import Account, Member # pylint: disable=E0611
from flwr.server.superlink.linkstate import LinkStateFactory
from flwr.supercore.constant import FLWR_IN_MEMORY_DB_NAME, NOOP_FEDERATION, RunType
from flwr.supercore.constant import (
FLWR_IN_MEMORY_DB_NAME,
NOOP_FEDERATION,
ActionType,
RunTime,
RunType,
)
from flwr.supercore.error import ApiErrorCode, FlowerError
from flwr.supercore.error.catalog import API_ERROR_MAP
from flwr.supercore.ffs import FfsFactory
from flwr.supercore.primitives.asymmetric import generate_key_pairs, public_key_to_bytes
from flwr.supercore.typing import StartRunContext
from flwr.superlink.auth_plugin import NoOpControlAuthnPlugin
from flwr.superlink.federation import NoOpFederationManager
from flwr.superlink.servicer.control.control_account_auth_interceptor import (
Expand Down Expand Up @@ -253,6 +260,61 @@ def test_start_run_rejects_unknown_override_keys(self) -> None:
self.assertEqual(status_code, grpc.StatusCode.FAILED_PRECONDITION)
self.assertIn("unknown.key", details)

def test_start_run_denied_when_not_entitled(self) -> None:
"""Test StartRun aborts when federation manager denies execution."""
request = StartRunRequest()
request.fab.hash_str = hashlib.sha256(b"test FAB content").hexdigest()
request.fab.content = b"test FAB content"
request.federation = NOOP_FEDERATION

context = Mock()
context.abort.side_effect = grpc.RpcError()

with (
patch.object(
self.state.federation_manager,
"can_execute",
return_value=False,
),
self.assertRaises(grpc.RpcError),
):
self.servicer.StartRun(request, context)

_assert_abort_with_flwr_err(context, ApiErrorCode.NO_PERMISSIONS)

def test_start_run_calls_can_execute_with_expected_args(self) -> None:
"""Test StartRun calls can_execute with expected typed arguments."""
fab_content = b"test FAB content 777"
request = StartRunRequest()
request.fab.hash_str = hashlib.sha256(fab_content).hexdigest()
request.fab.content = fab_content
request.federation = NOOP_FEDERATION

with (
patch(
"flwr.superlink.servicer.control.control_servicer.get_fab_config"
) as mock_get_fab_config,
patch(
"flwr.superlink.servicer.control.control_servicer.get_metadata_from_config"
) as mock_get_metadata_from_config,
patch.object(
self.state.federation_manager,
"can_execute",
return_value=True,
) as mock_can_execute,
):
mock_get_fab_config.return_value = {
"tool": {"flwr": {"app": {"config": {"train": {"lr": 0.1}}}}}
}
mock_get_metadata_from_config.return_value = ("flwr/demo", "v1.0.0")
_ = self.servicer.StartRun(request, Mock())

mock_can_execute.assert_called_once_with(
self.aid,
ActionType.START_RUN,
StartRunContext(federation=NOOP_FEDERATION, runtime=RunTime.DEPLOYMENT),
)

@parameterized.expand([(None,), (1,), (2,), (3,), (9,)]) # type: ignore
def test_list_runs(self, limit: int | None) -> None:
"""Test List method of ControlServicer with --runs option."""
Expand Down
Loading