diff --git a/internal/dataplane/service_key.go b/internal/dataplane/service_key.go index 00e0eb2..dae659e 100644 --- a/internal/dataplane/service_key.go +++ b/internal/dataplane/service_key.go @@ -18,6 +18,12 @@ import ( // commands when the platform token cannot call the runtime route directly. const CLIServiceKeyName = "volcano-cli-data-plane" +// serviceKeyTokenPrefix identifies a data-plane service key (mirrors the server's +// auth.ServiceKeyPrefix). A caller already holding such a key is authenticated +// for the data plane directly, so the reserved-key list/create is neither needed +// nor possible (control-plane routes reject a service key with 401). +const serviceKeyTokenPrefix = "sk-" + // cliDataPlanePermissions is the least-privilege scope requested for the reserved // data-plane key: function invocation and storage object I/O only. It must cover // every operation the CLI performs with this key (copy/move/set-visibility map @@ -60,6 +66,13 @@ func (s Service) ServiceKeyForProject(ctx context.Context, project *clisession.P if project == nil { return "", errors.New("project session is required") } + // When the session is already authenticated with a service key (e.g. + // VOLCANO_TOKEN is a scoped data-plane key), use it directly. It is already a + // data-plane credential, and resolving the reserved CLI key would require a + // control-plane list/create that a service key is not permitted to make (401). + if token := sessionServiceKey(project); token != "" { + return token, nil + } name := s.serviceKeyName() key, found, err := s.findServiceKey(ctx, project, name) if err != nil { @@ -79,6 +92,17 @@ func (s Service) ServiceKeyForProject(ctx context.Context, project *clisession.P return serviceKeyPlaintext(created, name) } +func sessionServiceKey(project *clisession.ProjectSession) string { + if project.Config == nil { + return "" + } + token := strings.TrimSpace(project.Config.Token()) + if strings.HasPrefix(token, serviceKeyTokenPrefix) { + return token + } + return "" +} + func (s Service) serviceKeyName() string { name := strings.TrimSpace(s.keyName) if name == "" { diff --git a/internal/dataplane/service_key_test.go b/internal/dataplane/service_key_test.go index d455a45..2e86f92 100644 --- a/internal/dataplane/service_key_test.go +++ b/internal/dataplane/service_key_test.go @@ -125,6 +125,26 @@ func TestServiceKeyReloadsAfterCreateConflict(t *testing.T) { assert.Equal(t, 2, listHits) } +func TestServiceKeyUsesConfiguredServiceKeyDirectly(t *testing.T) { + setServiceKeyTestHome(t) + saveServiceKeyTestConfig(t) + // The caller already holds a scoped data-plane service key (e.g. in CI via + // VOLCANO_TOKEN); the CLI must use it as-is and never attempt the reserved-key + // list/create, which a service key cannot perform against control-plane routes. + t.Setenv("VOLCANO_TOKEN", "sk-provided-data-plane-key") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("unexpected API call %s %s: a provided service key must be used without a control-plane lookup", r.Method, r.URL.Path) + http.Error(w, "unexpected control-plane call", http.StatusUnauthorized) + })) + defer server.Close() + + service := NewService(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL}) + key, err := service.ServiceKey(t.Context()) + require.NoError(t, err) + assert.Equal(t, "sk-provided-data-plane-key", key) +} + func setServiceKeyTestHome(t *testing.T) { t.Helper() t.Setenv("HOME", t.TempDir())