From a91d1d2e614c6089b91deefe99ce7f62cdd6af4b Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 16 Jun 2026 15:14:51 +0200 Subject: [PATCH] fix(cli): prefer $CHAINLOOP_TOKEN over user credentials when both are set When both user credentials (from auth login) and the $CHAINLOOP_TOKEN environment variable are present, the CLI now defaults to the API token from the environment variable instead of the user session. An explicitly exported $CHAINLOOP_TOKEN is a stronger signal of intent than a possibly stale login session, and API tokens can now perform the same operations as user credentials (including attestations). The warning is flipped to indicate that user credentials are being ignored. Closes #3215 Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: f29ba765-4a69-48fe-ab1e-84ba24984ea9 --- app/cli/cmd/root.go | 14 ++--- app/cli/cmd/root_test.go | 113 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 app/cli/cmd/root_test.go diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index 598cf8881..ace1da2a8 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -398,7 +398,7 @@ func cleanup(conn *grpc.ClientConn) error { // 2. If the command does not require an API token, we // 2.1 Return the token explicitly provided via the flag // 2.2 Load the token from the environment variable and from the auth login config file -// 2.3 if they both exist, we default to the user token +// 2.3 if they both exist, we default to the API token from the environment variable // 2.4 otherwise to the one that's set func loadAuthToken(cmd *cobra.Command) (string, bool, error) { // Load the APIToken from the env variable @@ -423,11 +423,13 @@ func loadAuthToken(cmd *cobra.Command) (string, bool, error) { return apiToken, false, nil } - // If both the user authentication and the API token en var are set, we default to user authentication - if userToken != "" && apiTokenFromVar != "" { - logger.Warn().Msgf("Both user credentials and $%s set. Ignoring $%s.", tokenEnvVarName, tokenEnvVarName) - return userToken, true, nil - } else if apiTokenFromVar != "" { + // If the API token env var is set we default to it. An explicitly exported $CHAINLOOP_TOKEN is a + // stronger signal of intent than a (possibly stale) login session, and API tokens can now perform + // the same operations as user credentials. We warn if user credentials are also present. + if apiTokenFromVar != "" { + if userToken != "" { + logger.Warn().Msgf("Both user credentials and $%s set. Ignoring user credentials.", tokenEnvVarName) + } return apiTokenFromVar, false, nil } diff --git a/app/cli/cmd/root_test.go b/app/cli/cmd/root_test.go new file mode 100644 index 000000000..8799c7fa1 --- /dev/null +++ b/app/cli/cmd/root_test.go @@ -0,0 +1,113 @@ +// Copyright 2024-2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadAuthToken(t *testing.T) { + const ( + envToken = "env-token" + userTok = "user-token" + flagToken = "flag-token" + ) + + testCases := []struct { + name string + // inputs + envVar string + userToken string + flag string + apiTokenPref bool + expectedToken string + expectedIsUser bool + }{ + { + name: "only env var set, non API-preferred command", + envVar: envToken, + expectedToken: envToken, + expectedIsUser: false, + }, + { + name: "only user token set", + userToken: userTok, + expectedToken: userTok, + expectedIsUser: true, + }, + { + name: "both set, command does not prefer API token, env var wins", + envVar: envToken, + userToken: userTok, + apiTokenPref: false, + expectedToken: envToken, + expectedIsUser: false, + }, + { + name: "both set, command prefers API token, env var wins", + envVar: envToken, + userToken: userTok, + apiTokenPref: true, + expectedToken: envToken, + expectedIsUser: false, + }, + { + name: "flag takes precedence over env var and user token", + envVar: envToken, + userToken: userTok, + flag: flagToken, + expectedToken: flagToken, + expectedIsUser: false, + }, + { + name: "nothing set returns empty user token", + expectedToken: "", + expectedIsUser: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Reset and set up the global state the function relies on + viper.Reset() + t.Cleanup(viper.Reset) + viper.Set(confOptions.authToken.viperKey, tc.userToken) + + if tc.envVar != "" { + t.Setenv(tokenEnvVarName, tc.envVar) + } + + // apiToken is a package-level variable bound to the --token flag + prevAPIToken := apiToken + apiToken = tc.flag + t.Cleanup(func() { apiToken = prevAPIToken }) + + cmd := &cobra.Command{Annotations: map[string]string{}} + if tc.apiTokenPref { + cmd.Annotations[useAPIToken] = trueString + } + + got, isUser, err := loadAuthToken(cmd) + require.NoError(t, err) + assert.Equal(t, tc.expectedToken, got) + assert.Equal(t, tc.expectedIsUser, isUser) + }) + } +}