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) + }) + } +}