Skip to content

Commit c166bac

Browse files
authored
feat(auth): Introduce possibility to use an environment variable to authenticate (#682)
If STACKIT_ACCESS_TOKEN is set this environment variable is used instead of stored tokens. Additionally the activate-service-account command is extended in order to only print the token but not store them in the keyring or in a file on the disk. Signed-off-by: Alexander Dahmen <[email protected]>
1 parent 0d4a469 commit c166bac

File tree

9 files changed

+76
-34
lines changed

9 files changed

+76
-34
lines changed

AUTHENTICATION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ $ stackit auth activate-service-account
1313

1414
You can also configure the service account credentials directly in the CLI. To get help and to get a list of the available options run the command with the `-h` flag.
1515

16+
**_Note:_** There is an optional flag `--only-print-access-token` which can be used to only obtain the access token which prevents writing the credentials to the keyring or into `cli-auth-storage.txt` ([File Location](./README.md#configuration)). This access token can be stored as environment variable (STACKIT_ACCESS_TOKEN) in order to be used for all subsequent commands by default.
17+
1618
### Overview
1719

1820
If you don't have a service account, create one in the [STACKIT Portal](https://portal.stackit.cloud/) and assign the necessary permissions to it, e.g. `owner`. There are two ways to authenticate:

docs/stackit_auth_activate-service-account.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@ stackit auth activate-service-account [flags]
2323
2424
Activate service account authentication in the STACKIT CLI using the service account token
2525
$ stackit auth activate-service-account --service-account-token my-service-account-token
26+
27+
Only print the corresponding access token by using the service account token. This access token can be stored as environment variable (STACKIT_ACCESS_TOKEN) in order to be used for all subsequent commands.
28+
$ stackit auth activate-service-account --service-account-token my-service-account-token --only-print-access-token
2629
```
2730

2831
### Options
2932

3033
```
3134
-h, --help Help for "stackit auth activate-service-account"
35+
--only-print-access-token If this is set to true the credentials are not stored in either the keyring or a file
3236
--private-key-path string RSA private key path. It takes precedence over the private key included in the service account key, if present
3337
--service-account-key-path string Service account key path
3438
--service-account-token string Service account long-lived access token

internal/cmd/auth/activate-service-account/activate_service_account.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ const (
2222
serviceAccountTokenFlag = "service-account-token"
2323
serviceAccountKeyPathFlag = "service-account-key-path"
2424
privateKeyPathFlag = "private-key-path"
25+
onlyPrintAccessTokenFlag = "only-print-access-token" // #nosec G101
2526
)
2627

2728
type inputModel struct {
2829
ServiceAccountToken string
2930
ServiceAccountKeyPath string
3031
PrivateKeyPath string
32+
OnlyPrintAccessToken bool
3133
}
3234

3335
func NewCmd(p *print.Printer) *cobra.Command {
@@ -50,13 +52,19 @@ func NewCmd(p *print.Printer) *cobra.Command {
5052
examples.NewExample(
5153
`Activate service account authentication in the STACKIT CLI using the service account token`,
5254
"$ stackit auth activate-service-account --service-account-token my-service-account-token"),
55+
examples.NewExample(
56+
`Only print the corresponding access token by using the service account token. This access token can be stored as environment variable (STACKIT_ACCESS_TOKEN) in order to be used for all subsequent commands.`,
57+
"$ stackit auth activate-service-account --service-account-token my-service-account-token --only-print-access-token",
58+
),
5359
),
5460
RunE: func(cmd *cobra.Command, _ []string) error {
5561
model := parseInput(p, cmd)
5662

57-
tokenCustomEndpoint, err := storeFlags()
58-
if err != nil {
59-
return err
63+
tokenCustomEndpoint := viper.GetString(config.TokenCustomEndpointKey)
64+
if !model.OnlyPrintAccessToken {
65+
if err := storeCustomEndpoint(tokenCustomEndpoint); err != nil {
66+
return err
67+
}
6068
}
6169

6270
cfg := &sdkConfig.Configuration{
@@ -75,7 +83,7 @@ func NewCmd(p *print.Printer) *cobra.Command {
7583
}
7684

7785
// Authenticates the service account and stores credentials
78-
email, err := auth.AuthenticateServiceAccount(p, rt)
86+
email, accessToken, err := auth.AuthenticateServiceAccount(p, rt, model.OnlyPrintAccessToken)
7987
if err != nil {
8088
var activateServiceAccountError *cliErr.ActivateServiceAccountError
8189
if !errors.As(err, &activateServiceAccountError) {
@@ -84,8 +92,12 @@ func NewCmd(p *print.Printer) *cobra.Command {
8492
return err
8593
}
8694

87-
p.Info("You have been successfully authenticated to the STACKIT CLI!\nService account email: %s\n", email)
88-
95+
if model.OnlyPrintAccessToken {
96+
// Only output is the access token
97+
p.Outputf("%s\n", accessToken)
98+
} else {
99+
p.Outputf("You have been successfully authenticated to the STACKIT CLI!\nService account email: %s\n", email)
100+
}
89101
return nil
90102
},
91103
}
@@ -97,13 +109,15 @@ func configureFlags(cmd *cobra.Command) {
97109
cmd.Flags().String(serviceAccountTokenFlag, "", "Service account long-lived access token")
98110
cmd.Flags().String(serviceAccountKeyPathFlag, "", "Service account key path")
99111
cmd.Flags().String(privateKeyPathFlag, "", "RSA private key path. It takes precedence over the private key included in the service account key, if present")
112+
cmd.Flags().Bool(onlyPrintAccessTokenFlag, false, "If this is set to true the credentials are not stored in either the keyring or a file")
100113
}
101114

102115
func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel {
103116
model := inputModel{
104117
ServiceAccountToken: flags.FlagToStringValue(p, cmd, serviceAccountTokenFlag),
105118
ServiceAccountKeyPath: flags.FlagToStringValue(p, cmd, serviceAccountKeyPathFlag),
106119
PrivateKeyPath: flags.FlagToStringValue(p, cmd, privateKeyPathFlag),
120+
OnlyPrintAccessToken: flags.FlagToBoolValue(p, cmd, onlyPrintAccessTokenFlag),
107121
}
108122

109123
if p.IsVerbosityDebug() {
@@ -118,12 +132,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel {
118132
return &model
119133
}
120134

121-
func storeFlags() (tokenCustomEndpoint string, err error) {
122-
tokenCustomEndpoint = viper.GetString(config.TokenCustomEndpointKey)
123-
124-
err = auth.SetAuthField(auth.TOKEN_CUSTOM_ENDPOINT, tokenCustomEndpoint)
125-
if err != nil {
126-
return "", fmt.Errorf("set %s: %w", auth.TOKEN_CUSTOM_ENDPOINT, err)
127-
}
128-
return tokenCustomEndpoint, nil
135+
func storeCustomEndpoint(tokenCustomEndpoint string) error {
136+
return auth.SetAuthField(auth.TOKEN_CUSTOM_ENDPOINT, tokenCustomEndpoint)
129137
}

internal/cmd/auth/activate-service-account/activate_service_account_test.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st
2020
serviceAccountTokenFlag: "token",
2121
serviceAccountKeyPathFlag: "sa_key",
2222
privateKeyPathFlag: "private_key",
23+
onlyPrintAccessTokenFlag: "true",
2324
}
2425
for _, mod := range mods {
2526
mod(flagValues)
@@ -32,6 +33,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
3233
ServiceAccountToken: "token",
3334
ServiceAccountKeyPath: "sa_key",
3435
PrivateKeyPath: "private_key",
36+
OnlyPrintAccessToken: true,
3537
}
3638
for _, mod := range mods {
3739
mod(model)
@@ -87,6 +89,18 @@ func TestParseInput(t *testing.T) {
8789
}),
8890
isValid: false,
8991
},
92+
{
93+
description: "default value OnlyPrintAccessToken",
94+
flagValues: fixtureFlagValues(
95+
func(flagValues map[string]string) {
96+
delete(flagValues, "only-print-access-token")
97+
},
98+
),
99+
isValid: true,
100+
expectedModel: fixtureInputModel(func(model *inputModel) {
101+
model.OnlyPrintAccessToken = false
102+
}),
103+
},
90104
}
91105

92106
for _, tt := range tests {
@@ -121,7 +135,7 @@ func TestParseInput(t *testing.T) {
121135
}
122136
}
123137

124-
func TestStoreFlags(t *testing.T) {
138+
func TestStoreCustomEndpointFlags(t *testing.T) {
125139
tests := []struct {
126140
description string
127141
model *inputModel
@@ -154,7 +168,7 @@ func TestStoreFlags(t *testing.T) {
154168
viper.Reset()
155169
viper.Set(config.TokenCustomEndpointKey, tt.tokenCustomEndpoint)
156170

157-
tokenCustomEndpoint, err := storeFlags()
171+
err := storeCustomEndpoint(tt.tokenCustomEndpoint)
158172
if !tt.isValid {
159173
if err == nil {
160174
t.Fatalf("did not fail on invalid input")
@@ -169,8 +183,8 @@ func TestStoreFlags(t *testing.T) {
169183
if err != nil {
170184
t.Errorf("Failed to get value of auth field: %v", err)
171185
}
172-
if value != tokenCustomEndpoint {
173-
t.Errorf("Value of \"%s\" does not match: expected \"%s\", got \"%s\"", auth.TOKEN_CUSTOM_ENDPOINT, tokenCustomEndpoint, value)
186+
if value != tt.tokenCustomEndpoint {
187+
t.Errorf("Value of \"%s\" does not match: expected \"%s\", got \"%s\"", auth.TOKEN_CUSTOM_ENDPOINT, tt.tokenCustomEndpoint, value)
174188
}
175189
})
176190
}

internal/cmd/auth/login/login.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ func NewCmd(p *print.Printer) *cobra.Command {
3030
return fmt.Errorf("authorization failed: %w", err)
3131
}
3232

33-
p.Info("Successfully logged into STACKIT CLI.\n")
33+
p.Outputln("Successfully logged into STACKIT CLI.\n")
34+
3435
return nil
3536
},
3637
}

internal/pkg/auth/auth.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package auth
22

33
import (
44
"fmt"
5+
"os"
56
"strconv"
67
"time"
78

@@ -22,7 +23,15 @@ type tokenClaims struct {
2223
// It returns the configuration option that can be used to create an authenticated SDK client.
2324
//
2425
// If the user was logged in and the user session expired, reauthorizeUserRoutine is called to reauthenticate the user again.
26+
// If the environment variable STACKIT_ACCESS_TOKEN is set this token is used instead.
2527
func AuthenticationConfig(p *print.Printer, reauthorizeUserRoutine func(p *print.Printer, _ bool) error) (authCfgOption sdkConfig.ConfigurationOption, err error) {
28+
// Get access token from env and use this if present
29+
accessToken := os.Getenv(envAccessTokenName)
30+
if accessToken != "" {
31+
authCfgOption = sdkConfig.WithToken(accessToken)
32+
return authCfgOption, nil
33+
}
34+
2635
flow, err := GetAuthFlow()
2736
if err != nil {
2837
return nil, fmt.Errorf("get authentication flow: %w", err)

internal/pkg/auth/service_account.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ var _ http.RoundTripper = &keyFlowWithStorage{}
3535
// For the key flow, it fetches an access and refresh token from the Service Account API.
3636
// For the token flow, it just stores the provided token and doesn't check if it is valid.
3737
// It returns the email associated with the service account
38-
func AuthenticateServiceAccount(p *print.Printer, rt http.RoundTripper) (email string, err error) {
38+
// If disableWriting is set to true the credentials are not stored on disk (keyring, file).
39+
func AuthenticateServiceAccount(p *print.Printer, rt http.RoundTripper, disableWriting bool) (email, accessToken string, err error) {
3940
authFields := make(map[authFieldKey]string)
4041
var authFlowType AuthFlow
4142
switch flow := rt.(type) {
@@ -46,12 +47,12 @@ func AuthenticateServiceAccount(p *print.Printer, rt http.RoundTripper) (email s
4647
accessToken, err := flow.GetAccessToken()
4748
if err != nil {
4849
p.Debug(print.ErrorLevel, "get access token: %v", err)
49-
return "", &errors.ActivateServiceAccountError{}
50+
return "", "", &errors.ActivateServiceAccountError{}
5051
}
5152
serviceAccountKey := flow.GetConfig().ServiceAccountKey
5253
saKeyBytes, err := json.Marshal(serviceAccountKey)
5354
if err != nil {
54-
return "", fmt.Errorf("marshal service account key: %w", err)
55+
return "", "", fmt.Errorf("marshal service account key: %w", err)
5556
}
5657

5758
authFields[ACCESS_TOKEN] = accessToken
@@ -64,12 +65,12 @@ func AuthenticateServiceAccount(p *print.Printer, rt http.RoundTripper) (email s
6465

6566
authFields[ACCESS_TOKEN] = flow.GetConfig().ServiceAccountToken
6667
default:
67-
return "", fmt.Errorf("could not authenticate using any of the supported authentication flows (key and token): please report this issue")
68+
return "", "", fmt.Errorf("could not authenticate using any of the supported authentication flows (key and token): please report this issue")
6869
}
6970

7071
email, err = getEmailFromToken(authFields[ACCESS_TOKEN])
7172
if err != nil {
72-
return "", fmt.Errorf("get email from access token: %w", err)
73+
return "", "", fmt.Errorf("get email from access token: %w", err)
7374
}
7475

7576
p.Debug(print.DebugLevel, "successfully authenticated service account %s", email)
@@ -78,20 +79,22 @@ func AuthenticateServiceAccount(p *print.Printer, rt http.RoundTripper) (email s
7879

7980
sessionExpiresAtUnix, err := getStartingSessionExpiresAtUnix()
8081
if err != nil {
81-
return "", fmt.Errorf("compute session expiration timestamp: %w", err)
82+
return "", "", fmt.Errorf("compute session expiration timestamp: %w", err)
8283
}
8384
authFields[SESSION_EXPIRES_AT_UNIX] = sessionExpiresAtUnix
8485

85-
err = SetAuthFlow(authFlowType)
86-
if err != nil {
87-
return "", fmt.Errorf("set auth flow type: %w", err)
88-
}
89-
err = SetAuthFieldMap(authFields)
90-
if err != nil {
91-
return "", fmt.Errorf("set in auth storage: %w", err)
86+
if !disableWriting {
87+
err = SetAuthFlow(authFlowType)
88+
if err != nil {
89+
return "", "", fmt.Errorf("set auth flow type: %w", err)
90+
}
91+
err = SetAuthFieldMap(authFields)
92+
if err != nil {
93+
return "", "", fmt.Errorf("set in auth storage: %w", err)
94+
}
9295
}
9396

94-
return authFields[SERVICE_ACCOUNT_EMAIL], nil
97+
return authFields[SERVICE_ACCOUNT_EMAIL], authFields[ACCESS_TOKEN], nil
9598
}
9699

97100
// initKeyFlowWithStorage initializes the keyFlow from the SDK and creates a keyFlowWithStorage struct that uses that keyFlow

internal/pkg/auth/service_account_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func TestAuthenticateServiceAccount(t *testing.T) {
153153
}
154154

155155
p := print.NewPrinter()
156-
email, err := AuthenticateServiceAccount(p, flow)
156+
email, _, err := AuthenticateServiceAccount(p, flow, false)
157157

158158
if !tt.isValid {
159159
if err == nil {

internal/pkg/auth/storage.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const (
2525
keyringService = "stackit-cli"
2626
textFileFolderName = "stackit"
2727
textFileName = "cli-auth-storage.txt"
28+
envAccessTokenName = "STACKIT_ACCESS_TOKEN"
2829
)
2930

3031
const (

0 commit comments

Comments
 (0)