Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions cmd/volcano/grant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,24 @@ func TestGrantIsRefusedOnAccountCommandsBeforeAnyRequest(t *testing.T) {
})
}
}

// testFunctionID is the function the invoke tests call.
const testFunctionID = "99999999-9999-4999-8999-999999999999"

// Invoke sends the grant itself to the data-plane invoke route. It never
// lists, reads or creates the CLI's service key (runAsWorker fails any
// /service-keys request). Hosting accepts a grant there from C6 on.
func TestGrantInvokesWithTheGrantAndNoServiceKey(t *testing.T) {
run := runAsWorker(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost && r.URL.Path == "/functions/"+testFunctionID+"/invoke" {
respondExitTestJSON(w, http.StatusOK, `{"ok":true}`)
return
}
respondExitTestJSON(w, http.StatusForbidden, `{"error":"delegated grants cannot be used on this endpoint","code":"grant_route_not_allowed"}`)
}, "cloud", "functions", "invoke", "--id", testFunctionID, "--json")

assert.Equal(t, 0, run.code, run.stderr)
assert.Equal(t, []string{"POST /functions/" + testFunctionID + "/invoke"}, run.requests)
assert.Equal(t, []string{"Bearer " + testGrant}, run.auth)
assert.JSONEq(t, `{"ok":true}`, run.stdout)
}
2 changes: 1 addition & 1 deletion internal/cmd/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func NewResourceCommands(deps cliruntime.Deps) []*cobra.Command {
domainscmd.New(deps),
durablecmd.New(deps),
frontendscmd.New(deps),
functionscmd.NewWithOptions(deps, functionscmd.WithInvokeTokenProvider(dataPlaneKeys.ServiceKeyForProject)),
functionscmd.NewWithOptions(deps, functionscmd.WithInvokeTokenProvider(dataPlaneKeys.InvokeToken)),
lavaflowcmd.New(deps),
logscmd.New(deps),
notificationscmd.New(deps),
Expand Down
11 changes: 11 additions & 0 deletions internal/dataplane/service_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/Kong/volcano-cli/internal/api"
"github.com/Kong/volcano-cli/internal/apiclient"
"github.com/Kong/volcano-cli/internal/config"
cliruntime "github.com/Kong/volcano-cli/internal/runtime"
clisession "github.com/Kong/volcano-cli/internal/session"
)
Expand Down Expand Up @@ -50,6 +51,16 @@ func NewService(deps cliruntime.Deps) Service {
}
}

// InvokeToken returns the bearer for the function invoke route. A pg-
// delegated grant invokes with itself, as a service key does; any other
// credential gets the reserved service key, as before.
func (s Service) InvokeToken(ctx context.Context, project *clisession.ProjectSession) (string, error) {
if project != nil && project.Config != nil && config.IsDelegatedGrant(project.Config.Token()) {
return strings.TrimSpace(project.Config.Token()), nil
}
return s.ServiceKeyForProject(ctx, project)
}

// ServiceKey returns the reserved service key for the current project, creating
// it when it does not already exist.
func (s Service) ServiceKey(ctx context.Context) (string, error) {
Expand Down
49 changes: 49 additions & 0 deletions internal/dataplane/service_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,52 @@ func serviceKeyPayload(id, name, keyValue string) map[string]any {
"updated_at": "2026-05-20T00:00:00Z",
}
}

// A delegated grant invokes with itself. Invoke never lists, reads or creates
// the reserved service key under a grant, so a grant never causes a
// long-lived key.
func TestInvokeTokenUsesADelegatedGrantDirectly(t *testing.T) {
setServiceKeyTestHome(t)
saveServiceKeyTestConfig(t)
t.Setenv("VOLCANO_TOKEN", cliconfig.DelegatedGrantPrefix+"grant-secret")

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Errorf("a grant must not reach the API to get an invoke token: %s %s", r.Method, r.URL.Path)
http.NotFound(w, r)
}))
defer server.Close()

service := NewService(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL})
project, err := service.sessions.CurrentProject()
require.NoError(t, err)

token, err := service.InvokeToken(t.Context(), project)
require.NoError(t, err)
assert.Equal(t, cliconfig.DelegatedGrantPrefix+"grant-secret", token)
}

// Any other credential still gets the reserved service key for invoke.
func TestInvokeTokenUsesTheServiceKeyForOtherCredentials(t *testing.T) {
setServiceKeyTestHome(t)
saveServiceKeyTestConfig(t)

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer platform-token", r.Header.Get("Authorization"))
if r.Method == http.MethodGet && r.URL.Path == "/projects/"+testProjectID+"/service-keys" {
writeServiceKeyJSON(t, w, http.StatusOK, map[string]any{
"data": []any{serviceKeyPayload("11111111-1111-4111-8111-111111111111", CLIServiceKeyName, "sk-existing")},
"has_more": false, "page": 1, "limit": 100, "total": 1,
})
return
}
http.NotFound(w, r)
}))
defer server.Close()

service := NewService(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL})
project, err := service.sessions.CurrentProject()
require.NoError(t, err)
token, err := service.InvokeToken(t.Context(), project)
require.NoError(t, err)
assert.Equal(t, "sk-existing", token)
}
Loading