Skip to content

Commit 864bccb

Browse files
committed
Fix Conan v2 non-interactive auth
1 parent b104d00 commit 864bccb

2 files changed

Lines changed: 106 additions & 66 deletions

File tree

module/conan/conan_cmd.go

Lines changed: 66 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,21 @@ func ExecuteConanInfoCmd(ctx context.Context, cmdInfo *CmdInfo, dir string) (str
116116
}
117117
jsonP := getConanInfoJsonPath()
118118
major := ConanMajorVersion(cmdInfo.Version)
119+
remoteCreds, credErr := getConanRemoteCredentialsFromConfig(major)
120+
if credErr != nil {
121+
logger.Warn("Conan remote credential precheck failed when reading config", zap.Error(credErr))
122+
}
119123
logger.Sugar().Infof("Conan detected: path=%s version=%s major=%d", cmdInfo.Path, cmdInfo.Version, major)
120124
logger.Sugar().Infof("Conan verbose mode: -v %s", conanVerboseArg)
121125
logConanRemoteConfigPaths(logger, major)
122-
loginWarnings := ensureConanRemoteLogin(ctx, cmdInfo.Path, major)
126+
loginWarnings := ensureConanRemoteLogin(ctx, cmdInfo.Path, major, remoteCreds)
123127
logger.Sugar().Debugf("temp file: %s", jsonP)
124128
if major >= 2 {
125-
if e := ensureConan2DefaultProfile(ctx, cmdInfo.Path); e != nil {
129+
if e := ensureConan2DefaultProfile(ctx, cmdInfo.Path, major, remoteCreds); e != nil {
126130
return "", "", e
127131
}
128132
logger.Info("Conan mode selected: graph")
129-
if e := executeConanGraphInfoCmd(ctx, cmdInfo.Path, dir, jsonP); e != nil {
133+
if e := executeConanGraphInfoCmd(ctx, cmdInfo.Path, dir, jsonP, major, remoteCreds); e != nil {
130134
if len(loginWarnings) > 0 {
131135
return "", "", fmt.Errorf("%w; login warnings: %s", e, strings.Join(loginWarnings, " | "))
132136
}
@@ -136,7 +140,7 @@ func ExecuteConanInfoCmd(ctx context.Context, cmdInfo *CmdInfo, dir string) (str
136140
return jsonP, ConanJsonKindGraph, nil
137141
}
138142
logger.Info("Conan mode selected: info")
139-
if e := executeConanInfoCmd(ctx, cmdInfo.Path, dir, jsonP); e != nil {
143+
if e := executeConanInfoCmd(ctx, cmdInfo.Path, dir, jsonP, major, remoteCreds); e != nil {
140144
if len(loginWarnings) > 0 {
141145
return "", "", fmt.Errorf("%w; login warnings: %s", e, strings.Join(loginWarnings, " | "))
142146
}
@@ -146,7 +150,7 @@ func ExecuteConanInfoCmd(ctx context.Context, cmdInfo *CmdInfo, dir string) (str
146150
return jsonP, ConanJsonKindInfo, nil
147151
}
148152

149-
func ensureConan2DefaultProfile(ctx context.Context, conanPath string) error {
153+
func ensureConan2DefaultProfile(ctx context.Context, conanPath string, major int, remoteCreds []conanRemoteCredential) error {
150154
logger := logctx.Use(ctx)
151155
home, err := os.UserHomeDir()
152156
if err != nil || home == "" {
@@ -162,10 +166,10 @@ func ensureConan2DefaultProfile(ctx context.Context, conanPath string) error {
162166
}
163167

164168
logger.Sugar().Infof("Conan default profile missing: %s, running detect", profilePath)
165-
args := conanArgs("profile", "detect", "--force")
169+
args := conanArgs(major, "profile", "detect", "--force")
166170
c := exec.CommandContext(ctx, conanPath, args...)
167171
logger.Sugar().Infof("Command: %s", c.String())
168-
c.Env = getEnvForConan()
172+
c.Env = getEnvForConan(major, remoteCreds)
169173
start := time.Now()
170174
sb := suffixbuf.NewSize(1024)
171175
logPipe := logpipe.New(logger, "conan")
@@ -185,12 +189,12 @@ func ensureConan2DefaultProfile(ctx context.Context, conanPath string) error {
185189
return nil
186190
}
187191

188-
func executeConanInfoCmd(ctx context.Context, conanPath string, dir string, jsonP string) error {
192+
func executeConanInfoCmd(ctx context.Context, conanPath string, dir string, jsonP string, major int, remoteCreds []conanRemoteCredential) error {
189193
logger := logctx.Use(ctx)
190-
args := conanArgs("info", ".", "-j", jsonP)
194+
args := conanArgs(major, "info", ".", "-j", jsonP)
191195
c := exec.Command(conanPath, args...)
192196
logger.Sugar().Infof("Command: %s", c.String())
193-
c.Env = getEnvForConan()
197+
c.Env = getEnvForConan(major, remoteCreds)
194198
c.Dir = dir
195199
start := time.Now()
196200
sb := suffixbuf.NewSize(1024)
@@ -206,12 +210,12 @@ func executeConanInfoCmd(ctx context.Context, conanPath string, dir string, json
206210
return nil
207211
}
208212

209-
func executeConanGraphInfoCmd(ctx context.Context, conanPath string, dir string, jsonP string) error {
213+
func executeConanGraphInfoCmd(ctx context.Context, conanPath string, dir string, jsonP string, major int, remoteCreds []conanRemoteCredential) error {
210214
logger := logctx.Use(ctx)
211-
args := conanArgs("graph", "info", ".", "--format=json")
215+
args := conanArgs(major, "graph", "info", ".", "--format=json")
212216
c := exec.Command(conanPath, args...)
213217
logger.Sugar().Infof("Command: %s", c.String())
214-
c.Env = getEnvForConan()
218+
c.Env = getEnvForConan(major, remoteCreds)
215219
c.Dir = dir
216220
start := time.Now()
217221
sb := suffixbuf.NewSize(1024)
@@ -237,79 +241,38 @@ type conanRemoteCredential struct {
237241
Password string
238242
}
239243

240-
func ensureConanRemoteLogin(ctx context.Context, conanPath string, major int) []string {
244+
func ensureConanRemoteLogin(ctx context.Context, conanPath string, major int, creds []conanRemoteCredential) []string {
241245
warnings := make([]string, 0)
242246
if major < 2 {
243247
return warnings
244248
}
245249
logger := logctx.Use(ctx)
246-
creds, err := getConanRemoteCredentialsFromConfig(major)
247-
if err != nil {
248-
logger.Warn("Conan remote login precheck failed when reading config", zap.Error(err))
249-
return warnings
250-
}
251250
if len(creds) == 0 {
252251
logger.Info("Conan remote login skipped: no credentials found in remote URLs")
253252
return warnings
254253
}
255254
for _, cred := range creds {
256-
authenticated, authOutput, authErr := isConanRemoteAuthenticated(ctx, conanPath, cred.Name, cred.Username)
257-
if authErr != nil {
258-
logger.Sugar().Warnf("Conan remote auth probe failed for %s: %v", cred.Name, authErr)
259-
} else {
260-
logger.Sugar().Infof("Conan remote auth probe: remote=%s user=%s authenticated=%t output=%q",
261-
cred.Name, cred.Username, authenticated, strings.TrimSpace(authOutput))
262-
}
263-
if authenticated {
264-
logger.Sugar().Infof("Conan remote auth already valid: remote=%s", cred.Name)
265-
continue
266-
}
267-
args := conanArgs("remote", "login", cred.Name, cred.Username, "-p", cred.Password)
255+
args := conanArgs(major, "remote", "auth", cred.Name, "--force")
268256
c := exec.CommandContext(ctx, conanPath, args...)
269-
c.Env = getEnvForConan()
257+
c.Env = getEnvForConan(major, []conanRemoteCredential{cred})
270258
sb := suffixbuf.NewSize(1024)
271259
logPipe := logpipe.New(logger, "conan")
272-
logger.Sugar().Infof("Command: %s remote login %s %s -p ******", conanPath, cred.Name, cred.Username)
260+
logger.Sugar().Infof("Command: %s remote auth %s --force (credentials from env)", conanPath, cred.Name)
273261
c.Stdout = io.MultiWriter(sb, logPipe)
274262
c.Stderr = io.MultiWriter(sb, logPipe)
275263
if runErr := c.Run(); runErr != nil {
276264
logPipe.Close()
277-
msg := fmt.Sprintf("conan remote login failed for %s(user=%s): %v, details: %s", cred.Name, cred.Username, runErr, strings.TrimSpace(string(sb.Bytes())))
265+
msg := fmt.Sprintf("conan remote auth failed for %s(user=%s): %v, details: %s", cred.Name, cred.Username, runErr, strings.TrimSpace(string(sb.Bytes())))
278266
logger.Warn(msg)
279267
warnings = append(warnings, msg)
280268
continue
281269
}
282270
logPipe.Close()
283-
logger.Sugar().Infof("Conan remote login completed: remote=%s user=%s", cred.Name, cred.Username)
271+
logger.Sugar().Infof("Conan remote auth completed: remote=%s user=%s", cred.Name, cred.Username)
284272
}
285273
return warnings
286274
}
287275

288-
func isConanRemoteAuthenticated(ctx context.Context, conanPath string, remoteName string, expectedUser string) (bool, string, error) {
289-
c := exec.CommandContext(ctx, conanPath, conanArgs("remote", "auth", remoteName, "--with-user")...)
290-
c.Env = getEnvForConan()
291-
out, err := c.CombinedOutput()
292-
output := strings.TrimSpace(string(out))
293-
if err != nil {
294-
return false, output, fmt.Errorf("auth check failed: %w, output: %s", err, output)
295-
}
296-
lower := strings.ToLower(output)
297-
if output == "" {
298-
return false, output, nil
299-
}
300-
if strings.Contains(lower, "anonymous") || strings.Contains(lower, "anonymously") {
301-
return false, output, nil
302-
}
303-
if strings.Contains(lower, "not authenticated") || strings.Contains(lower, "authenticated: false") {
304-
return false, output, nil
305-
}
306-
if expectedUser != "" && strings.Contains(lower, strings.ToLower(expectedUser)) {
307-
return true, output, nil
308-
}
309-
// Conan output format may vary by version; if we cannot infer a user match, treat as unauthenticated.
310-
return false, output, nil
311-
}
312-
313276
type conanRemotesFile struct {
314277
Remotes []struct {
315278
Name string `json:"name"`
@@ -367,7 +330,10 @@ func conanRemotesConfigPath(major int) (string, error) {
367330
return filepath.Join(home, ".conan", "remotes.json"), nil
368331
}
369332

370-
func conanArgs(args ...string) []string {
333+
func conanArgs(major int, args ...string) []string {
334+
if major >= 2 {
335+
args = append(args, "-cc", "core:non_interactive=True")
336+
}
371337
return append(args, "-v", conanVerboseArg)
372338
}
373339

@@ -385,7 +351,7 @@ func LocateConan(ctx context.Context) (string, error) {
385351

386352
func GetConanVersion(ctx context.Context, conanPath string) (string, error) {
387353
c := exec.CommandContext(ctx, conanPath, "-v")
388-
c.Env = getEnvForConan()
354+
c.Env = getBaseEnvForConan()
389355
if data, e := c.Output(); e != nil {
390356
return "", errors.WithCause(ErrGetConanVersionFail, e)
391357
} else {
@@ -403,9 +369,46 @@ func ConanMajorVersion(version string) int {
403369
return v
404370
}
405371

406-
func getEnvForConan() []string {
372+
func getBaseEnvForConan() []string {
407373
osEnv := os.Environ()
408374
var rs = make([]string, 0, len(osEnv)+3)
409375
rs = append(rs, osEnv...)
410376
return append(rs, "CONAN_NON_INTERACTIVE=1", "NO_COLOR=1", "CLICOLOR=0")
411377
}
378+
379+
func getEnvForConan(major int, remoteCreds []conanRemoteCredential) []string {
380+
rs := getBaseEnvForConan()
381+
if major < 1 || len(remoteCreds) == 0 {
382+
return rs
383+
}
384+
for _, cred := range remoteCreds {
385+
suffix := conanRemoteEnvVarSuffix(cred.Name)
386+
if suffix == "" || cred.Username == "" || cred.Password == "" {
387+
continue
388+
}
389+
rs = append(rs,
390+
fmt.Sprintf("CONAN_LOGIN_USERNAME_%s=%s", suffix, cred.Username),
391+
fmt.Sprintf("CONAN_PASSWORD_%s=%s", suffix, cred.Password),
392+
)
393+
}
394+
return rs
395+
}
396+
397+
func conanRemoteEnvVarSuffix(remoteName string) string {
398+
remoteName = strings.TrimSpace(remoteName)
399+
if remoteName == "" {
400+
return ""
401+
}
402+
var b strings.Builder
403+
for _, r := range remoteName {
404+
switch {
405+
case r >= 'a' && r <= 'z':
406+
b.WriteRune(r - ('a' - 'A'))
407+
case (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9'):
408+
b.WriteRune(r)
409+
default:
410+
b.WriteRune('_')
411+
}
412+
}
413+
return b.String()
414+
}

module/conan/conan_cmd_test.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,53 @@ import (
44
"context"
55
"github.com/murphysecurity/murphysec/utils/must"
66
"github.com/pkg/errors"
7-
"os"
87
"os/exec"
8+
"strings"
99
"testing"
1010
)
1111

1212
func TestGetConanVersion(t *testing.T) {
1313
_, e := exec.LookPath("conan")
14-
if os.Getenv("CI") != "" && errors.Is(e, exec.ErrNotFound) {
15-
t.Skip("Conan not found in CI environment, test skipped.")
14+
if errors.Is(e, exec.ErrNotFound) {
15+
t.Skip("Conan not found in test environment, test skipped.")
1616
return
1717
}
1818
t.Log(GetConanVersion(context.TODO(), must.A(LocateConan(context.TODO()))))
1919
}
20+
21+
func TestConanArgsV1DoesNotForceCoreNonInteractive(t *testing.T) {
22+
args := conanArgs(1, "info", ".")
23+
got := strings.Join(args, " ")
24+
if strings.Contains(got, "core:non_interactive=True") {
25+
t.Fatalf("unexpected v1 core:non_interactive flag in args: %v", args)
26+
}
27+
}
28+
29+
func TestConanArgsV2ForcesCoreNonInteractive(t *testing.T) {
30+
args := conanArgs(2, "graph", "info", ".")
31+
got := strings.Join(args, " ")
32+
if !strings.Contains(got, "core:non_interactive=True") {
33+
t.Fatalf("missing v2 core:non_interactive flag in args: %v", args)
34+
}
35+
}
36+
37+
func TestConanRemoteEnvVarSuffix(t *testing.T) {
38+
if got := conanRemoteEnvVarSuffix("conan-center.prod"); got != "CONAN_CENTER_PROD" {
39+
t.Fatalf("unexpected env suffix: %q", got)
40+
}
41+
}
42+
43+
func TestGetEnvForConanAddsRemoteCredentialEnv(t *testing.T) {
44+
env := getEnvForConan(2, []conanRemoteCredential{{
45+
Name: "conan-center",
46+
Username: "alice",
47+
Password: "secret",
48+
}})
49+
joined := strings.Join(env, "\n")
50+
if !strings.Contains(joined, "CONAN_LOGIN_USERNAME_CONAN_CENTER=alice") {
51+
t.Fatalf("missing username env, env=%v", env)
52+
}
53+
if !strings.Contains(joined, "CONAN_PASSWORD_CONAN_CENTER=secret") {
54+
t.Fatalf("missing password env, env=%v", env)
55+
}
56+
}

0 commit comments

Comments
 (0)