Skip to content

Commit 9bac680

Browse files
authored
Add stackit auth logout command (#416)
* initial implementation * Improve testing * address PR comments
1 parent 0264973 commit 9bac680

File tree

6 files changed

+409
-62
lines changed

6 files changed

+409
-62
lines changed

internal/cmd/auth/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package auth
33
import (
44
activateserviceaccount "github.com/stackitcloud/stackit-cli/internal/cmd/auth/activate-service-account"
55
"github.com/stackitcloud/stackit-cli/internal/cmd/auth/login"
6+
"github.com/stackitcloud/stackit-cli/internal/cmd/auth/logout"
67
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
78
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
89
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
@@ -24,5 +25,6 @@ func NewCmd(p *print.Printer) *cobra.Command {
2425

2526
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
2627
cmd.AddCommand(login.NewCmd(p))
28+
cmd.AddCommand(logout.NewCmd(p))
2729
cmd.AddCommand(activateserviceaccount.NewCmd(p))
2830
}

internal/cmd/auth/logout/logout.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package logout
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
7+
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
8+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
10+
11+
"github.com/spf13/cobra"
12+
)
13+
14+
func NewCmd(p *print.Printer) *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "logout",
17+
Short: "Logs the user account out of the STACKIT CLI",
18+
Long: "Logs the user account out of the STACKIT CLI.",
19+
Args: args.NoArgs,
20+
Example: examples.Build(
21+
examples.NewExample(
22+
`Log out of the STACKIT CLI.`,
23+
"$ stackit auth logout"),
24+
),
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
err := auth.LogoutUser()
27+
if err != nil {
28+
return fmt.Errorf("log out failed: %w", err)
29+
}
30+
31+
p.Info("Successfully logged out of the STACKIT CLI.\n")
32+
return nil
33+
},
34+
}
35+
return cmd
36+
}

internal/cmd/config/profile/delete/delete.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ func NewCmd(p *print.Printer) *cobra.Command {
7676
return fmt.Errorf("delete profile: %w", err)
7777
}
7878

79-
err = auth.DeleteProfileFromKeyring(model.Profile)
79+
err = auth.DeleteProfileAuth(model.Profile)
8080
if err != nil {
81-
return fmt.Errorf("delete profile from keyring: %w", err)
81+
return fmt.Errorf("delete profile authentication: %w", err)
8282
}
8383

8484
p.Info("Successfully deleted profile %q\n", model.Profile)

internal/pkg/auth/storage.go

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ var authFieldKeys = []authFieldKey{
6262
authFlowType,
6363
}
6464

65+
// All fields that are set when a user logs in
66+
// These fields should match the ones in LoginUser, which is ensured by the tests
67+
var loginAuthFieldKeys = []authFieldKey{
68+
SESSION_EXPIRES_AT_UNIX,
69+
ACCESS_TOKEN,
70+
REFRESH_TOKEN,
71+
USER_EMAIL,
72+
}
73+
6574
func SetAuthFlow(value AuthFlow) error {
6675
return SetAuthField(authFlowType, string(value))
6776
}
@@ -105,6 +114,65 @@ func setAuthFieldInKeyring(activeProfile string, key authFieldKey, value string)
105114
return keyring.Set(keyringService, string(key), value)
106115
}
107116

117+
func DeleteAuthField(key authFieldKey) error {
118+
activeProfile, err := config.GetProfile()
119+
if err != nil {
120+
return fmt.Errorf("get profile: %w", err)
121+
}
122+
return deleteAuthFieldWithProfile(activeProfile, key)
123+
}
124+
125+
func deleteAuthFieldWithProfile(profile string, key authFieldKey) error {
126+
err := deleteAuthFieldInKeyring(profile, key)
127+
if err != nil {
128+
// if the key is not found, we can ignore the error
129+
if !errors.Is(err, keyring.ErrNotFound) {
130+
errFallback := deleteAuthFieldInEncodedTextFile(profile, key)
131+
if errFallback != nil {
132+
return fmt.Errorf("delete from keyring failed (%w), try deleting from encoded text file: %w", err, errFallback)
133+
}
134+
}
135+
}
136+
return nil
137+
}
138+
139+
func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) error {
140+
err := createEncodedTextFile(activeProfile)
141+
if err != nil {
142+
return err
143+
}
144+
145+
textFileDir := config.GetProfileFolderPath(activeProfile)
146+
textFilePath := filepath.Join(textFileDir, textFileName)
147+
148+
contentEncoded, err := os.ReadFile(textFilePath)
149+
if err != nil {
150+
return fmt.Errorf("read file: %w", err)
151+
}
152+
contentBytes, err := base64.StdEncoding.DecodeString(string(contentEncoded))
153+
if err != nil {
154+
return fmt.Errorf("decode file: %w", err)
155+
}
156+
content := map[authFieldKey]string{}
157+
err = json.Unmarshal(contentBytes, &content)
158+
if err != nil {
159+
return fmt.Errorf("unmarshal file: %w", err)
160+
}
161+
162+
delete(content, key)
163+
164+
contentBytes, err = json.Marshal(content)
165+
if err != nil {
166+
return fmt.Errorf("marshal file: %w", err)
167+
}
168+
contentEncoded = []byte(base64.StdEncoding.EncodeToString(contentBytes))
169+
err = os.WriteFile(textFilePath, contentEncoded, 0o600)
170+
if err != nil {
171+
return fmt.Errorf("write file: %w", err)
172+
}
173+
return nil
174+
}
175+
108176
func deleteAuthFieldInKeyring(activeProfile string, key authFieldKey) error {
109177
keyringServiceLocal := keyringService
110178
if activeProfile != config.DefaultProfileName {
@@ -273,7 +341,32 @@ func GetProfileEmail(profile string) string {
273341
return email
274342
}
275343

276-
func DeleteProfileFromKeyring(profile string) error {
344+
func LoginUser(email, accessToken, refreshToken, sessionExpiresAtUnix string) error {
345+
authFields := map[authFieldKey]string{
346+
SESSION_EXPIRES_AT_UNIX: sessionExpiresAtUnix,
347+
ACCESS_TOKEN: accessToken,
348+
REFRESH_TOKEN: refreshToken,
349+
USER_EMAIL: email,
350+
}
351+
352+
err := SetAuthFieldMap(authFields)
353+
if err != nil {
354+
return fmt.Errorf("set auth fields: %w", err)
355+
}
356+
return nil
357+
}
358+
359+
func LogoutUser() error {
360+
for _, key := range loginAuthFieldKeys {
361+
err := DeleteAuthField(key)
362+
if err != nil {
363+
return fmt.Errorf("delete auth field \"%s\": %w", key, err)
364+
}
365+
}
366+
return nil
367+
}
368+
369+
func DeleteProfileAuth(profile string) error {
277370
err := config.ValidateProfile(profile)
278371
if err != nil {
279372
return fmt.Errorf("validate profile: %w", err)
@@ -284,12 +377,9 @@ func DeleteProfileFromKeyring(profile string) error {
284377
}
285378

286379
for _, key := range authFieldKeys {
287-
err := deleteAuthFieldInKeyring(profile, key)
380+
err := deleteAuthFieldWithProfile(profile, key)
288381
if err != nil {
289-
// if the key is not found, we can ignore the error
290-
if !errors.Is(err, keyring.ErrNotFound) {
291-
return fmt.Errorf("delete auth field \"%s\" from keyring: %w", key, err)
292-
}
382+
return fmt.Errorf("delete auth field \"%s\": %w", key, err)
293383
}
294384
}
295385

0 commit comments

Comments
 (0)