Skip to content

Commit a91d1d2

Browse files
committed
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 <miguel@chainloop.dev> Chainloop-Trace-Sessions: f29ba765-4a69-48fe-ab1e-84ba24984ea9
1 parent cc153ae commit a91d1d2

2 files changed

Lines changed: 121 additions & 6 deletions

File tree

app/cli/cmd/root.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ func cleanup(conn *grpc.ClientConn) error {
398398
// 2. If the command does not require an API token, we
399399
// 2.1 Return the token explicitly provided via the flag
400400
// 2.2 Load the token from the environment variable and from the auth login config file
401-
// 2.3 if they both exist, we default to the user token
401+
// 2.3 if they both exist, we default to the API token from the environment variable
402402
// 2.4 otherwise to the one that's set
403403
func loadAuthToken(cmd *cobra.Command) (string, bool, error) {
404404
// Load the APIToken from the env variable
@@ -423,11 +423,13 @@ func loadAuthToken(cmd *cobra.Command) (string, bool, error) {
423423
return apiToken, false, nil
424424
}
425425

426-
// If both the user authentication and the API token en var are set, we default to user authentication
427-
if userToken != "" && apiTokenFromVar != "" {
428-
logger.Warn().Msgf("Both user credentials and $%s set. Ignoring $%s.", tokenEnvVarName, tokenEnvVarName)
429-
return userToken, true, nil
430-
} else if apiTokenFromVar != "" {
426+
// If the API token env var is set we default to it. An explicitly exported $CHAINLOOP_TOKEN is a
427+
// stronger signal of intent than a (possibly stale) login session, and API tokens can now perform
428+
// the same operations as user credentials. We warn if user credentials are also present.
429+
if apiTokenFromVar != "" {
430+
if userToken != "" {
431+
logger.Warn().Msgf("Both user credentials and $%s set. Ignoring user credentials.", tokenEnvVarName)
432+
}
431433
return apiTokenFromVar, false, nil
432434
}
433435

app/cli/cmd/root_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2024-2026 The Chainloop Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"testing"
19+
20+
"github.com/spf13/cobra"
21+
"github.com/spf13/viper"
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestLoadAuthToken(t *testing.T) {
27+
const (
28+
envToken = "env-token"
29+
userTok = "user-token"
30+
flagToken = "flag-token"
31+
)
32+
33+
testCases := []struct {
34+
name string
35+
// inputs
36+
envVar string
37+
userToken string
38+
flag string
39+
apiTokenPref bool
40+
expectedToken string
41+
expectedIsUser bool
42+
}{
43+
{
44+
name: "only env var set, non API-preferred command",
45+
envVar: envToken,
46+
expectedToken: envToken,
47+
expectedIsUser: false,
48+
},
49+
{
50+
name: "only user token set",
51+
userToken: userTok,
52+
expectedToken: userTok,
53+
expectedIsUser: true,
54+
},
55+
{
56+
name: "both set, command does not prefer API token, env var wins",
57+
envVar: envToken,
58+
userToken: userTok,
59+
apiTokenPref: false,
60+
expectedToken: envToken,
61+
expectedIsUser: false,
62+
},
63+
{
64+
name: "both set, command prefers API token, env var wins",
65+
envVar: envToken,
66+
userToken: userTok,
67+
apiTokenPref: true,
68+
expectedToken: envToken,
69+
expectedIsUser: false,
70+
},
71+
{
72+
name: "flag takes precedence over env var and user token",
73+
envVar: envToken,
74+
userToken: userTok,
75+
flag: flagToken,
76+
expectedToken: flagToken,
77+
expectedIsUser: false,
78+
},
79+
{
80+
name: "nothing set returns empty user token",
81+
expectedToken: "",
82+
expectedIsUser: true,
83+
},
84+
}
85+
86+
for _, tc := range testCases {
87+
t.Run(tc.name, func(t *testing.T) {
88+
// Reset and set up the global state the function relies on
89+
viper.Reset()
90+
t.Cleanup(viper.Reset)
91+
viper.Set(confOptions.authToken.viperKey, tc.userToken)
92+
93+
if tc.envVar != "" {
94+
t.Setenv(tokenEnvVarName, tc.envVar)
95+
}
96+
97+
// apiToken is a package-level variable bound to the --token flag
98+
prevAPIToken := apiToken
99+
apiToken = tc.flag
100+
t.Cleanup(func() { apiToken = prevAPIToken })
101+
102+
cmd := &cobra.Command{Annotations: map[string]string{}}
103+
if tc.apiTokenPref {
104+
cmd.Annotations[useAPIToken] = trueString
105+
}
106+
107+
got, isUser, err := loadAuthToken(cmd)
108+
require.NoError(t, err)
109+
assert.Equal(t, tc.expectedToken, got)
110+
assert.Equal(t, tc.expectedIsUser, isUser)
111+
})
112+
}
113+
}

0 commit comments

Comments
 (0)