Skip to content

Commit 7b4f43a

Browse files
committed
Add storage context system for credential isolation
Refactor storage layer to support multiple independent storage contexts, enabling CLI and API (Terraform Provider/SDK) credentials to be stored separately. This allows users to authenticate with different accounts for CLI operations vs. SDK/Terraform usage. Key changes: - Add StorageContext enum (StorageContextCLI, StorageContextAPI) - Add *WithContext() variants for all storage functions - Support context-specific keyring service names and file paths - Maintain backward compatibility with existing storage functions - Add comprehensive tests for storage context isolation Storage locations: - CLI: stackit-cli keyring, ~/.stackit/cli-auth-storage.txt - API: stackit-cli-api keyring, ~/.stackit/cli-api-auth-storage.txt
1 parent c7dada1 commit 7b4f43a

File tree

2 files changed

+947
-38
lines changed

2 files changed

+947
-38
lines changed

internal/pkg/auth/storage.go

Lines changed: 178 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,52 @@ import (
1010

1111
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
1212
pkgErrors "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
1314

1415
"github.com/zalando/go-keyring"
1516
)
1617

18+
// Package-level printer for debug logging in storage operations
19+
var storagePrinter = print.NewPrinter()
20+
21+
// SetStoragePrinter sets the printer used for storage debug logging
22+
// This should be called with the main command's printer to ensure consistent verbosity
23+
func SetStoragePrinter(p *print.Printer) {
24+
if p != nil {
25+
storagePrinter = p
26+
}
27+
}
28+
1729
// Name of an auth-related field
1830
type authFieldKey string
1931

2032
// Possible values of authentication flows
2133
type AuthFlow string
2234

35+
// StorageContext represents the context in which credentials are stored
36+
// CLI context is for the CLI's own authentication
37+
// API context is for Terraform Provider and SDK authentication
38+
type StorageContext string
39+
2340
const (
24-
keyringService = "stackit-cli"
25-
textFileName = "cli-auth-storage.txt"
41+
StorageContextCLI StorageContext = "cli"
42+
StorageContextAPI StorageContext = "api"
43+
)
44+
45+
const (
46+
keyringServiceCLI = "stackit-cli"
47+
keyringServiceAPI = "stackit-cli-api"
48+
textFileNameCLI = "cli-auth-storage.txt"
49+
textFileNameAPI = "cli-api-auth-storage.txt"
2650
envAccessTokenName = "STACKIT_ACCESS_TOKEN"
2751
)
2852

53+
// Legacy constants for backward compatibility
54+
const (
55+
keyringService = keyringServiceCLI
56+
textFileName = textFileNameCLI
57+
)
58+
2959
const (
3060
SESSION_EXPIRES_AT_UNIX authFieldKey = "session_expires_at_unix"
3161
ACCESS_TOKEN authFieldKey = "access_token"
@@ -70,10 +100,40 @@ var loginAuthFieldKeys = []authFieldKey{
70100
USER_EMAIL,
71101
}
72102

103+
// getKeyringServiceName returns the keyring service name for the given context and profile
104+
func getKeyringServiceName(context StorageContext, profile string) string {
105+
var baseService string
106+
switch context {
107+
case StorageContextAPI:
108+
baseService = keyringServiceAPI
109+
default:
110+
baseService = keyringServiceCLI
111+
}
112+
113+
if profile != config.DefaultProfileName {
114+
return filepath.Join(baseService, profile)
115+
}
116+
return baseService
117+
}
118+
119+
// getTextFileName returns the text file name for the given context
120+
func getTextFileName(context StorageContext) string {
121+
switch context {
122+
case StorageContextAPI:
123+
return textFileNameAPI
124+
default:
125+
return textFileNameCLI
126+
}
127+
}
128+
73129
func SetAuthFlow(value AuthFlow) error {
74130
return SetAuthField(authFlowType, string(value))
75131
}
76132

133+
func SetAuthFlowWithContext(context StorageContext, value AuthFlow) error {
134+
return SetAuthFieldWithContext(context, authFlowType, string(value))
135+
}
136+
77137
// Sets the values in the auth storage according to the given map
78138
func SetAuthFieldMap(keyMap map[authFieldKey]string) error {
79139
for key, value := range keyMap {
@@ -85,19 +145,39 @@ func SetAuthFieldMap(keyMap map[authFieldKey]string) error {
85145
return nil
86146
}
87147

148+
// SetAuthFieldMapWithContext sets the values in the auth storage according to the given map for a specific context
149+
func SetAuthFieldMapWithContext(context StorageContext, keyMap map[authFieldKey]string) error {
150+
for key, value := range keyMap {
151+
err := SetAuthFieldWithContext(context, key, value)
152+
if err != nil {
153+
return fmt.Errorf("set auth field \"%s\": %w", key, err)
154+
}
155+
}
156+
return nil
157+
}
158+
88159
func SetAuthField(key authFieldKey, value string) error {
160+
return SetAuthFieldWithContext(StorageContextCLI, key, value)
161+
}
162+
163+
// SetAuthFieldWithContext sets an auth field for a specific storage context
164+
func SetAuthFieldWithContext(context StorageContext, key authFieldKey, value string) error {
89165
activeProfile, err := config.GetProfile()
90166
if err != nil {
91167
return fmt.Errorf("get profile: %w", err)
92168
}
93169

94-
return setAuthFieldWithProfile(activeProfile, key, value)
170+
return setAuthFieldWithProfileAndContext(context, activeProfile, key, value)
95171
}
96172

97173
func setAuthFieldWithProfile(profile string, key authFieldKey, value string) error {
98-
err := setAuthFieldInKeyring(profile, key, value)
174+
return setAuthFieldWithProfileAndContext(StorageContextCLI, profile, key, value)
175+
}
176+
177+
func setAuthFieldWithProfileAndContext(context StorageContext, profile string, key authFieldKey, value string) error {
178+
err := setAuthFieldInKeyringWithContext(context, profile, key, value)
99179
if err != nil {
100-
errFallback := setAuthFieldInEncodedTextFile(profile, key, value)
180+
errFallback := setAuthFieldInEncodedTextFileWithContext(context, profile, key, value)
101181
if errFallback != nil {
102182
return fmt.Errorf("write to keyring failed (%w), try writing to encoded text file: %w", err, errFallback)
103183
}
@@ -106,27 +186,37 @@ func setAuthFieldWithProfile(profile string, key authFieldKey, value string) err
106186
}
107187

108188
func setAuthFieldInKeyring(activeProfile string, key authFieldKey, value string) error {
109-
if activeProfile != config.DefaultProfileName {
110-
activeProfileKeyring := filepath.Join(keyringService, activeProfile)
111-
return keyring.Set(activeProfileKeyring, string(key), value)
112-
}
113-
return keyring.Set(keyringService, string(key), value)
189+
return setAuthFieldInKeyringWithContext(StorageContextCLI, activeProfile, key, value)
190+
}
191+
192+
func setAuthFieldInKeyringWithContext(context StorageContext, activeProfile string, key authFieldKey, value string) error {
193+
keyringServiceName := getKeyringServiceName(context, activeProfile)
194+
return keyring.Set(keyringServiceName, string(key), value)
114195
}
115196

116197
func DeleteAuthField(key authFieldKey) error {
198+
return DeleteAuthFieldWithContext(StorageContextCLI, key)
199+
}
200+
201+
// DeleteAuthFieldWithContext deletes an auth field for a specific storage context
202+
func DeleteAuthFieldWithContext(context StorageContext, key authFieldKey) error {
117203
activeProfile, err := config.GetProfile()
118204
if err != nil {
119205
return fmt.Errorf("get profile: %w", err)
120206
}
121-
return deleteAuthFieldWithProfile(activeProfile, key)
207+
return deleteAuthFieldWithProfileAndContext(context, activeProfile, key)
122208
}
123209

124210
func deleteAuthFieldWithProfile(profile string, key authFieldKey) error {
125-
err := deleteAuthFieldInKeyring(profile, key)
211+
return deleteAuthFieldWithProfileAndContext(StorageContextCLI, profile, key)
212+
}
213+
214+
func deleteAuthFieldWithProfileAndContext(context StorageContext, profile string, key authFieldKey) error {
215+
err := deleteAuthFieldInKeyringWithContext(context, profile, key)
126216
if err != nil {
127217
// if the key is not found, we can ignore the error
128218
if !errors.Is(err, keyring.ErrNotFound) {
129-
errFallback := deleteAuthFieldInEncodedTextFile(profile, key)
219+
errFallback := deleteAuthFieldInEncodedTextFileWithContext(context, profile, key)
130220
if errFallback != nil {
131221
return fmt.Errorf("delete from keyring failed (%w), try deleting from encoded text file: %w", err, errFallback)
132222
}
@@ -136,13 +226,18 @@ func deleteAuthFieldWithProfile(profile string, key authFieldKey) error {
136226
}
137227

138228
func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) error {
139-
err := createEncodedTextFile(activeProfile)
229+
return deleteAuthFieldInEncodedTextFileWithContext(StorageContextCLI, activeProfile, key)
230+
}
231+
232+
func deleteAuthFieldInEncodedTextFileWithContext(context StorageContext, activeProfile string, key authFieldKey) error {
233+
err := createEncodedTextFileWithContext(context, activeProfile)
140234
if err != nil {
141235
return err
142236
}
143237

144238
textFileDir := config.GetProfileFolderPath(activeProfile)
145-
textFilePath := filepath.Join(textFileDir, textFileName)
239+
fileName := getTextFileName(context)
240+
textFilePath := filepath.Join(textFileDir, fileName)
146241

147242
contentEncoded, err := os.ReadFile(textFilePath)
148243
if err != nil {
@@ -173,21 +268,27 @@ func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) er
173268
}
174269

175270
func deleteAuthFieldInKeyring(activeProfile string, key authFieldKey) error {
176-
keyringServiceLocal := keyringService
177-
if activeProfile != config.DefaultProfileName {
178-
keyringServiceLocal = filepath.Join(keyringService, activeProfile)
179-
}
271+
return deleteAuthFieldInKeyringWithContext(StorageContextCLI, activeProfile, key)
272+
}
180273

181-
return keyring.Delete(keyringServiceLocal, string(key))
274+
func deleteAuthFieldInKeyringWithContext(context StorageContext, activeProfile string, key authFieldKey) error {
275+
keyringServiceName := getKeyringServiceName(context, activeProfile)
276+
return keyring.Delete(keyringServiceName, string(key))
182277
}
183278

184279
func setAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey, value string) error {
185-
err := createEncodedTextFile(activeProfile)
280+
return setAuthFieldInEncodedTextFileWithContext(StorageContextCLI, activeProfile, key, value)
281+
}
282+
283+
func setAuthFieldInEncodedTextFileWithContext(context StorageContext, activeProfile string, key authFieldKey, value string) error {
284+
textFileDir := config.GetProfileFolderPath(activeProfile)
285+
fileName := getTextFileName(context)
286+
textFilePath := filepath.Join(textFileDir, fileName)
287+
288+
err := createEncodedTextFileWithContext(context, activeProfile)
186289
if err != nil {
187290
return err
188291
}
189-
textFileDir := config.GetProfileFolderPath(activeProfile)
190-
textFilePath := filepath.Join(textFileDir, textFileName)
191292

192293
contentEncoded, err := os.ReadFile(textFilePath)
193294
if err != nil {
@@ -219,8 +320,13 @@ func setAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey, value
219320

220321
// Populates the values in the given map according to the auth storage
221322
func GetAuthFieldMap(keyMap map[authFieldKey]string) error {
323+
return GetAuthFieldMapWithContext(StorageContextCLI, keyMap)
324+
}
325+
326+
// GetAuthFieldMapWithContext populates the values in the given map according to the auth storage for a specific context
327+
func GetAuthFieldMapWithContext(context StorageContext, keyMap map[authFieldKey]string) error {
222328
for key := range keyMap {
223-
value, err := GetAuthField(key)
329+
value, err := GetAuthFieldWithContext(context, key)
224330
if err != nil {
225331
return fmt.Errorf("get auth field \"%s\": %w", key, err)
226332
}
@@ -230,23 +336,36 @@ func GetAuthFieldMap(keyMap map[authFieldKey]string) error {
230336
}
231337

232338
func GetAuthFlow() (AuthFlow, error) {
233-
value, err := GetAuthField(authFlowType)
339+
return GetAuthFlowWithContext(StorageContextCLI)
340+
}
341+
342+
func GetAuthFlowWithContext(context StorageContext) (AuthFlow, error) {
343+
value, err := GetAuthFieldWithContext(context, authFlowType)
234344
return AuthFlow(value), err
235345
}
236346

237347
func GetAuthField(key authFieldKey) (string, error) {
348+
return GetAuthFieldWithContext(StorageContextCLI, key)
349+
}
350+
351+
// GetAuthFieldWithContext retrieves an auth field for a specific storage context
352+
func GetAuthFieldWithContext(context StorageContext, key authFieldKey) (string, error) {
238353
activeProfile, err := config.GetProfile()
239354
if err != nil {
240355
return "", fmt.Errorf("get profile: %w", err)
241356
}
242-
return getAuthFieldWithProfile(activeProfile, key)
357+
return getAuthFieldWithProfileAndContext(context, activeProfile, key)
243358
}
244359

245360
func getAuthFieldWithProfile(profile string, key authFieldKey) (string, error) {
246-
value, err := getAuthFieldFromKeyring(profile, key)
361+
return getAuthFieldWithProfileAndContext(StorageContextCLI, profile, key)
362+
}
363+
364+
func getAuthFieldWithProfileAndContext(context StorageContext, profile string, key authFieldKey) (string, error) {
365+
value, err := getAuthFieldFromKeyringWithContext(context, profile, key)
247366
if err != nil {
248367
var errFallback error
249-
value, errFallback = getAuthFieldFromEncodedTextFile(profile, key)
368+
value, errFallback = getAuthFieldFromEncodedTextFileWithContext(context, profile, key)
250369
if errFallback != nil {
251370
return "", fmt.Errorf("read from keyring: %w, read from encoded file as fallback: %w", err, errFallback)
252371
}
@@ -255,21 +374,27 @@ func getAuthFieldWithProfile(profile string, key authFieldKey) (string, error) {
255374
}
256375

257376
func getAuthFieldFromKeyring(activeProfile string, key authFieldKey) (string, error) {
258-
if activeProfile != config.DefaultProfileName {
259-
activeProfileKeyring := filepath.Join(keyringService, activeProfile)
260-
return keyring.Get(activeProfileKeyring, string(key))
261-
}
262-
return keyring.Get(keyringService, string(key))
377+
return getAuthFieldFromKeyringWithContext(StorageContextCLI, activeProfile, key)
378+
}
379+
380+
func getAuthFieldFromKeyringWithContext(context StorageContext, activeProfile string, key authFieldKey) (string, error) {
381+
keyringServiceName := getKeyringServiceName(context, activeProfile)
382+
return keyring.Get(keyringServiceName, string(key))
263383
}
264384

265385
func getAuthFieldFromEncodedTextFile(activeProfile string, key authFieldKey) (string, error) {
266-
err := createEncodedTextFile(activeProfile)
386+
return getAuthFieldFromEncodedTextFileWithContext(StorageContextCLI, activeProfile, key)
387+
}
388+
389+
func getAuthFieldFromEncodedTextFileWithContext(context StorageContext, activeProfile string, key authFieldKey) (string, error) {
390+
err := createEncodedTextFileWithContext(context, activeProfile)
267391
if err != nil {
268392
return "", err
269393
}
270394

271395
textFileDir := config.GetProfileFolderPath(activeProfile)
272-
textFilePath := filepath.Join(textFileDir, textFileName)
396+
fileName := getTextFileName(context)
397+
textFilePath := filepath.Join(textFileDir, fileName)
273398

274399
contentEncoded, err := os.ReadFile(textFilePath)
275400
if err != nil {
@@ -295,8 +420,13 @@ func getAuthFieldFromEncodedTextFile(activeProfile string, key authFieldKey) (st
295420
// If it doesn't, creates it with the content "{}" encoded.
296421
// If it does, does nothing (and returns nil).
297422
func createEncodedTextFile(activeProfile string) error {
423+
return createEncodedTextFileWithContext(StorageContextCLI, activeProfile)
424+
}
425+
426+
func createEncodedTextFileWithContext(context StorageContext, activeProfile string) error {
298427
textFileDir := config.GetProfileFolderPath(activeProfile)
299-
textFilePath := filepath.Join(textFileDir, textFileName)
428+
fileName := getTextFileName(context)
429+
textFilePath := filepath.Join(textFileDir, fileName)
300430

301431
err := os.MkdirAll(textFileDir, 0o750)
302432
if err != nil {
@@ -364,23 +494,33 @@ func GetAuthEmail() (string, error) {
364494
}
365495

366496
func LoginUser(email, accessToken, refreshToken, sessionExpiresAtUnix string) error {
497+
return LoginUserWithContext(StorageContextCLI, email, accessToken, refreshToken, sessionExpiresAtUnix)
498+
}
499+
500+
// LoginUserWithContext stores user login credentials for a specific storage context
501+
func LoginUserWithContext(context StorageContext, email, accessToken, refreshToken, sessionExpiresAtUnix string) error {
367502
authFields := map[authFieldKey]string{
368503
SESSION_EXPIRES_AT_UNIX: sessionExpiresAtUnix,
369504
ACCESS_TOKEN: accessToken,
370505
REFRESH_TOKEN: refreshToken,
371506
USER_EMAIL: email,
372507
}
373508

374-
err := SetAuthFieldMap(authFields)
509+
err := SetAuthFieldMapWithContext(context, authFields)
375510
if err != nil {
376511
return fmt.Errorf("set auth fields: %w", err)
377512
}
378513
return nil
379514
}
380515

381516
func LogoutUser() error {
517+
return LogoutUserWithContext(StorageContextCLI)
518+
}
519+
520+
// LogoutUserWithContext removes user authentication for a specific storage context
521+
func LogoutUserWithContext(context StorageContext) error {
382522
for _, key := range loginAuthFieldKeys {
383-
err := DeleteAuthField(key)
523+
err := DeleteAuthFieldWithContext(context, key)
384524
if err != nil {
385525
return fmt.Errorf("delete auth field \"%s\": %w", key, err)
386526
}

0 commit comments

Comments
 (0)