Skip to content

Commit 9a83da9

Browse files
Parag.jainmssola
Parag.jain
authored andcommitted
cmd: Accept regcodes from stdin and files
A new syntax is now allowed when providing registration codes, in which you can also pass them by either stdin or files. This is for security concerns as some terminals might store the shell session and the commands being used, hence allowing for the possibility of credential leaking. Fixes bsc#1212242 Signed-off-by: Parag Jain <[email protected]>
1 parent 610c3cb commit 9a83da9

File tree

2 files changed

+297
-17
lines changed

2 files changed

+297
-17
lines changed

cmd/suseconnect/suseconnect.go

+17-17
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,7 @@ func main() {
151151
connect.CFG.Namespace = namespace
152152
writeConfig = true
153153
}
154-
if token != "" {
155-
connect.CFG.Token = token
156-
processedToken, processTokenErr := processToken(token)
157-
if processTokenErr != nil {
158-
util.Debug.Printf("Error Processing token %+v", processTokenErr)
159-
os.Exit(1)
160-
}
161-
connect.CFG.Token = processedToken
162-
}
154+
parseRegistrationToken(token)
163155
if product.isSet {
164156
if p, err := connect.SplitTriplet(product.value); err != nil {
165157
fmt.Print("Please provide the product identifier in this format: ")
@@ -323,6 +315,18 @@ func main() {
323315
}
324316
}
325317

318+
func parseRegistrationToken(token string) {
319+
if token != "" {
320+
connect.CFG.Token = token
321+
processedToken, processTokenErr := processToken(token)
322+
if processTokenErr != nil {
323+
fmt.Printf("Error Processing token with error %+v", processTokenErr)
324+
os.Exit(1)
325+
}
326+
connect.CFG.Token = processedToken
327+
}
328+
}
329+
326330
func maybeBrokenSMTError() error {
327331
if !connect.CFG.IsScc() && !connect.UpToDate() {
328332
return fmt.Errorf("Your Registration Proxy server doesn't support this function. " +
@@ -412,32 +416,28 @@ func isSumaManaged() bool {
412416
}
413417

414418
func processToken(token string) (string, error) {
415-
var reader io.Reader
416-
417419
if strings.HasPrefix(token, "@") {
418420
tokenFilePath := strings.TrimPrefix(token, "@")
419421
file, err := os.Open(tokenFilePath)
420422
if err != nil {
421423
return "", fmt.Errorf("failed to open token file '%s': %w", tokenFilePath, err)
422424
}
423425
defer file.Close()
424-
reader = file
426+
return readTokenFromReader(file)
425427
} else if token == "-" {
426-
reader = os.Stdin
428+
return readTokenFromReader(os.Stdin)
427429
} else {
428430
return token, nil
429431
}
430-
431-
return readTokenFromReader(reader)
432432
}
433433

434434
func readTokenFromReader(reader io.Reader) (string, error) {
435435
bufReader := bufio.NewReader(reader)
436436
tokenBytes, err := bufReader.ReadString('\n')
437-
if err != nil && err != io.EOF { // Handle potential errors, including EOF
437+
if err != nil && err != io.EOF {
438438
return "", fmt.Errorf("failed to read token from reader: %w", err)
439439
}
440-
token := strings.TrimSuffix(tokenBytes, "\n")
440+
token := strings.TrimSpace(tokenBytes)
441441
if token == "" {
442442
return "", fmt.Errorf("error: token cannot be empty after reading")
443443
}

cmd/suseconnect/suseconnect_test.go

+280
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"github.com/SUSE/connect-ng/internal/connect"
7+
"github.com/SUSE/connect-ng/internal/util"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/mock"
10+
"os"
11+
"strings"
12+
"testing"
13+
)
14+
15+
var processTokenFunc = processToken
16+
17+
var exitCalled bool
18+
var exit = func(code int) {
19+
exitCalled = true
20+
}
21+
22+
type MockProcessToken struct {
23+
mock.Mock
24+
}
25+
26+
func (m *MockProcessToken) ProcessToken(token string) (string, error) {
27+
args := m.Called(token)
28+
return args.String(0), args.Error(1)
29+
}
30+
31+
func init() {
32+
processTokenFunc = processToken
33+
}
34+
35+
type errorReader struct{}
36+
37+
func (e *errorReader) Read(p []byte) (n int, err error) {
38+
return 0, fmt.Errorf("forced reader error")
39+
}
40+
41+
func TestReadTokenFromErrorValidToken(t *testing.T) {
42+
inputToken := "validToken\n"
43+
reader := strings.NewReader(inputToken)
44+
token, err := readTokenFromReader(reader)
45+
if err != nil {
46+
t.Fatalf("Expected no error but got %v", err)
47+
}
48+
if token != "validToken" {
49+
t.Fatalf("Expected token string to be 'validToken' but got '%s'", token)
50+
}
51+
}
52+
53+
func TestReadTokenFromReader_MultipleNewlines(t *testing.T) {
54+
input := "firstToken\nsecondToken\n"
55+
reader := strings.NewReader(input)
56+
57+
token, err := readTokenFromReader(reader)
58+
if err != nil {
59+
t.Fatalf("Expected no error, but got %v", err)
60+
}
61+
62+
expected := "firstToken"
63+
if token != expected {
64+
t.Errorf("Expected token to be '%s', but got '%s'", expected, token)
65+
}
66+
}
67+
68+
func TestReadTokenFromReader_EmptyInput(t *testing.T) {
69+
reader := strings.NewReader("")
70+
71+
token, err := readTokenFromReader(reader)
72+
if err == nil {
73+
t.Fatalf("Expected error, but got none")
74+
}
75+
76+
expectedError := "error: token cannot be empty after reading"
77+
if err.Error() != expectedError {
78+
t.Errorf("Expected error '%s', but got '%s'", expectedError, err.Error())
79+
}
80+
81+
if token != "" {
82+
t.Errorf("Expected empty token, but got '%s'", token)
83+
}
84+
}
85+
86+
func TestReadTokenFromReader_OnlyNewline(t *testing.T) {
87+
reader := strings.NewReader("\n")
88+
89+
token, err := readTokenFromReader(reader)
90+
if err == nil {
91+
t.Fatalf("Expected error, but got none")
92+
}
93+
94+
expectedError := "error: token cannot be empty after reading"
95+
if err.Error() != expectedError {
96+
t.Errorf("Expected error '%s', but got '%s'", expectedError, err.Error())
97+
}
98+
99+
if token != "" {
100+
t.Errorf("Expected empty token, but got '%s'", token)
101+
}
102+
}
103+
104+
func TestReadTokenFromReader_ErrorProducingReader(t *testing.T) {
105+
reader := &errorReader{}
106+
107+
token, err := readTokenFromReader(reader)
108+
if err == nil {
109+
t.Fatalf("Expected error, but got none")
110+
}
111+
112+
expectedError := "failed to read token from reader: forced reader error"
113+
if err.Error() != expectedError {
114+
t.Errorf("Expected error '%s', but got '%s'", expectedError, err.Error())
115+
}
116+
117+
if token != "" {
118+
t.Errorf("Expected empty token, but got '%s'", token)
119+
}
120+
}
121+
122+
func TestProcessToken_RegularToken(t *testing.T) {
123+
token := "myRegularToken"
124+
result, err := processToken(token)
125+
if err != nil {
126+
t.Fatalf("Expected no error, but got %v", err)
127+
}
128+
129+
if result != token {
130+
t.Errorf("Expected token to be '%s', but got '%s'", token, result)
131+
}
132+
}
133+
134+
func TestProcessToken_TokenFromFile(t *testing.T) {
135+
tmpFile, err := os.CreateTemp("", "tokenfile")
136+
if err != nil {
137+
t.Fatalf("Failed to create temp file: %v", err)
138+
}
139+
defer os.Remove(tmpFile.Name())
140+
141+
expectedToken := "fileToken\n"
142+
if _, err := tmpFile.WriteString(expectedToken); err != nil {
143+
t.Fatalf("Failed to write to temp file: %v", err)
144+
}
145+
tmpFile.Close()
146+
147+
token := "@" + tmpFile.Name()
148+
result, err := processToken(token)
149+
if err != nil {
150+
t.Fatalf("Expected no error, but got %v", err)
151+
}
152+
153+
expectedToken = strings.TrimSpace(expectedToken)
154+
if result != expectedToken {
155+
t.Errorf("Expected token to be '%s', but got '%s'", expectedToken, result)
156+
}
157+
}
158+
159+
func TestProcessToken_NonExistentFile(t *testing.T) {
160+
token := "@/non/existent/file"
161+
_, err := processToken(token)
162+
if err == nil {
163+
t.Fatalf("Expected error for non-existent file, but got none")
164+
}
165+
166+
expectedError := "failed to open token file '/non/existent/file'"
167+
if !strings.Contains(err.Error(), expectedError) {
168+
t.Errorf("Expected error containing '%s', but got '%v'", expectedError, err)
169+
}
170+
}
171+
172+
func TestProcessToken_TokenFromStdin(t *testing.T) {
173+
tempFile, err := os.CreateTemp("", "test_stdin")
174+
if err != nil {
175+
t.Fatalf("Failed to create temp file: %v", err)
176+
}
177+
defer os.Remove(tempFile.Name())
178+
expectedToken := "stdinToken\n"
179+
if _, err := tempFile.WriteString(expectedToken); err != nil {
180+
t.Fatalf("Failed to write to temp file: %v", err)
181+
}
182+
183+
tempFile.Close()
184+
185+
oldStdin := os.Stdin
186+
defer func() { os.Stdin = oldStdin }()
187+
file, err := os.Open(tempFile.Name())
188+
if err != nil {
189+
t.Fatalf("Failed to open temp file: %v", err)
190+
}
191+
os.Stdin = file
192+
193+
result, err := processToken("-")
194+
if err != nil {
195+
t.Fatalf("Expected no error, but got %v", err)
196+
}
197+
198+
expectedToken = strings.TrimSpace(expectedToken)
199+
if result != expectedToken {
200+
t.Errorf("Expected token to be '%s', but got '%s'", expectedToken, result)
201+
}
202+
}
203+
204+
func TestProcessToken_ErrorReadingStdin(t *testing.T) {
205+
tempFile, err := os.CreateTemp("", "test_stdin_empty")
206+
if err != nil {
207+
t.Fatalf("Failed to create temp file: %v", err)
208+
}
209+
defer os.Remove(tempFile.Name())
210+
211+
tempFile.Close()
212+
oldStdin := os.Stdin
213+
defer func() { os.Stdin = oldStdin }()
214+
file, err := os.Open(tempFile.Name())
215+
if err != nil {
216+
t.Fatalf("Failed to open temp file: %v", err)
217+
}
218+
os.Stdin = file
219+
220+
_, err = processToken("-")
221+
if err == nil {
222+
t.Fatalf("Expected error reading from stdin, but got none")
223+
}
224+
225+
expectedError := "error: token cannot be empty after reading"
226+
if !strings.Contains(err.Error(), expectedError) {
227+
t.Errorf("Expected error containing '%s', but got '%v'", expectedError, err)
228+
}
229+
}
230+
231+
func parseRegistrationTokenWithInjection(token string) {
232+
if token != "" {
233+
connect.CFG.Token = token
234+
processedToken, processTokenErr := processTokenFunc(token)
235+
if processTokenErr != nil {
236+
util.Debug.Printf("Error Processing token %+v", processTokenErr)
237+
exit(1)
238+
}
239+
connect.CFG.Token = processedToken
240+
}
241+
}
242+
243+
func TestParseToken_Success(t *testing.T) {
244+
mockProcessToken := new(MockProcessToken)
245+
mockProcessToken.On("ProcessToken", "valid-token").Return("processed-token", nil)
246+
247+
processTokenFunc = mockProcessToken.ProcessToken
248+
249+
exitCalled = false
250+
251+
parseRegistrationTokenWithInjection("valid-token")
252+
253+
assert.Equal(t, "processed-token", connect.CFG.Token, "Token should be processed correctly")
254+
assert.False(t, exitCalled, "os.Exit (simulated) should not be called in a successful case")
255+
256+
mockProcessToken.AssertExpectations(t)
257+
}
258+
259+
func TestParseToken_ProcessTokenError(t *testing.T) {
260+
mockProcessToken := new(MockProcessToken)
261+
mockProcessToken.On("ProcessToken", "invalid-token").Return("", errors.New("failed to process token"))
262+
263+
processTokenFunc = mockProcessToken.ProcessToken
264+
265+
exitCalled = false
266+
267+
parseRegistrationTokenWithInjection("invalid-token")
268+
269+
assert.True(t, exitCalled, "os.Exit (simulated) should be called when processToken fails")
270+
assert.Equal(t, "", connect.CFG.Token, "Token should not be updated when processToken fails")
271+
272+
mockProcessToken.AssertExpectations(t)
273+
}
274+
275+
func TestParseToken_EmptyToken(t *testing.T) {
276+
exitCalled = false
277+
parseRegistrationTokenWithInjection("")
278+
assert.Empty(t, connect.CFG.Token, "Token should not be updated when input token is empty")
279+
assert.False(t, exitCalled, "os.Exit (simulated) should not be called for empty token")
280+
}

0 commit comments

Comments
 (0)