From 8a38457a2c2800214aecb18bd68bcdc9c42aaba0 Mon Sep 17 00:00:00 2001 From: Homer Quan Date: Mon, 18 May 2026 13:40:37 -0400 Subject: [PATCH] Require admin token for ClearJobs RPC --- README.md | 2 ++ lib/mirror_neuron_grpc/job.pb.ex | 2 ++ lib/mirror_neuron_grpc/server.ex | 25 ++++++++++++++++++++++++- proto/job.proto | 1 + tests/unit/grpc_job_server_test.exs | 28 ++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/unit/grpc_job_server_test.exs diff --git a/README.md b/README.md index 25691741..ac9c487f 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,8 @@ MirrorNeuron Core includes protobuf definitions and generated Elixir modules for Generated modules live under `lib/mirror_neuron_grpc/`. +`JobService.ClearJobs` is a destructive administrative RPC. It is denied unless the server has `MIRROR_NEURON_GRPC_ADMIN_TOKEN` set and the request includes the same value in `admin_token`. + The separate REST API package is maintained in [`mn-api`](https://github.com/MirrorNeuronLab/mn-api). The Python SDK is maintained in [`mn-python-sdk`](https://github.com/MirrorNeuronLab/mn-python-sdk). ## Project Structure diff --git a/lib/mirror_neuron_grpc/job.pb.ex b/lib/mirror_neuron_grpc/job.pb.ex index c0abe4db..fcba229a 100644 --- a/lib/mirror_neuron_grpc/job.pb.ex +++ b/lib/mirror_neuron_grpc/job.pb.ex @@ -161,6 +161,8 @@ defmodule Mirrorneuron.Job.V1.ClearJobsRequest do full_name: "mirrorneuron.job.v1.ClearJobsRequest", protoc_gen_elixir_version: "0.16.0", syntax: :proto3 + + field(:admin_token, 1, type: :string, json_name: "adminToken") end defmodule Mirrorneuron.Job.V1.ClearJobsResponse do diff --git a/lib/mirror_neuron_grpc/server.ex b/lib/mirror_neuron_grpc/server.ex index 41e98d4e..4c9c52f2 100644 --- a/lib/mirror_neuron_grpc/server.ex +++ b/lib/mirror_neuron_grpc/server.ex @@ -151,7 +151,9 @@ defmodule MirrorNeuron.Grpc.JobServer do end end - def clear_jobs(_request, _stream) do + def clear_jobs(request, _stream) do + authorize_clear_jobs!(request) + case MirrorNeuron.Monitor.clear_jobs() do {:ok, count} -> %ClearJobsResponse{cleared_count: count} @@ -160,6 +162,27 @@ defmodule MirrorNeuron.Grpc.JobServer do raise GRPC.RPCError, status: GRPC.Status.internal(), message: reason end end + + defp authorize_clear_jobs!(request) do + configured_token = System.get_env("MIRROR_NEURON_GRPC_ADMIN_TOKEN") + request_token = Map.get(request, :admin_token, "") + + unless valid_admin_token?(configured_token, request_token) do + raise GRPC.RPCError, + status: GRPC.Status.permission_denied(), + message: "ClearJobs requires MIRROR_NEURON_GRPC_ADMIN_TOKEN" + end + + :ok + end + + defp valid_admin_token?(configured_token, request_token) + when is_binary(configured_token) and byte_size(configured_token) > 0 and + is_binary(request_token) do + configured_token == request_token + end + + defp valid_admin_token?(_configured_token, _request_token), do: false end defmodule MirrorNeuron.Grpc.ClusterServer do diff --git a/proto/job.proto b/proto/job.proto index bd97c179..0caf00f7 100644 --- a/proto/job.proto +++ b/proto/job.proto @@ -66,6 +66,7 @@ message ResumeJobResponse { } message ClearJobsRequest { + string admin_token = 1; } message ClearJobsResponse { diff --git a/tests/unit/grpc_job_server_test.exs b/tests/unit/grpc_job_server_test.exs new file mode 100644 index 00000000..37741a5c --- /dev/null +++ b/tests/unit/grpc_job_server_test.exs @@ -0,0 +1,28 @@ +defmodule MirrorNeuron.Grpc.JobServerTest do + use ExUnit.Case, async: false + + alias MirrorNeuron.Grpc.JobServer + alias Mirrorneuron.Job.V1.ClearJobsRequest + + setup do + old_token = System.get_env("MIRROR_NEURON_GRPC_ADMIN_TOKEN") + System.delete_env("MIRROR_NEURON_GRPC_ADMIN_TOKEN") + + on_exit(fn -> + if is_nil(old_token) do + System.delete_env("MIRROR_NEURON_GRPC_ADMIN_TOKEN") + else + System.put_env("MIRROR_NEURON_GRPC_ADMIN_TOKEN", old_token) + end + end) + end + + test "clear_jobs rejects unauthenticated requests before deleting jobs" do + error = + assert_raise GRPC.RPCError, fn -> + JobServer.clear_jobs(%ClearJobsRequest{}, nil) + end + + assert Exception.message(error) =~ "ClearJobs requires MIRROR_NEURON_GRPC_ADMIN_TOKEN" + end +end