Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 2 additions & 22 deletions internal/cmd/project/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,29 +148,9 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel,
func buildRequest(ctx context.Context, model *inputModel, apiClient *resourcemanager.APIClient) (resourcemanager.ApiCreateProjectRequest, error) {
req := apiClient.DefaultAPI.CreateProject(ctx)

authFlow, err := auth.GetAuthFlow()
email, err := auth.GetAuthEmail()
if err != nil {
return req, fmt.Errorf("get authentication flow: %w", err)
}
var email string
switch authFlow {
case auth.AUTH_FLOW_SERVICE_ACCOUNT_TOKEN:
email, err = auth.GetAuthField(auth.SERVICE_ACCOUNT_EMAIL)
if err != nil {
return req, fmt.Errorf("get email of the service account that was used to authenticate: %w", err)
}
case auth.AUTH_FLOW_SERVICE_ACCOUNT_KEY:
email, err = auth.GetAuthField(auth.SERVICE_ACCOUNT_EMAIL)
if err != nil {
return req, fmt.Errorf("get email of the service account that was used to authenticate: %w", err)
}
case auth.AUTH_FLOW_USER_TOKEN:
email, err = auth.GetAuthField(auth.USER_EMAIL)
if err != nil {
return req, fmt.Errorf("get your user email from configuration: %w", err)
}
default:
return req, fmt.Errorf("the configured authentication flow (%s) is not supported, please report this issue", authFlow)
return req, fmt.Errorf("get email of authenticated user: %w", err)
}

if email == "" {
Expand Down
23 changes: 23 additions & 0 deletions internal/cmd/project/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package create

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
Expand All @@ -25,6 +28,15 @@ var testParentId = uuid.NewString()
var testNetworkAreaId = uuid.NewString()
var testEmail = "email"

// buildTestJWT creates an unsigned JWT token containing the given email claim.
// getEmailFromToken uses ParseUnverified, so no real signing key is needed.
func buildTestJWT(email string) string {
header, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
payload, _ := json.Marshal(map[string]string{"email": email})
enc := base64.RawURLEncoding
return fmt.Sprintf("%s.%s.fakesig", enc.EncodeToString(header), enc.EncodeToString(payload))
}

func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
parentIdFlag: testParentId,
Expand Down Expand Up @@ -193,6 +205,7 @@ func TestBuildRequest(t *testing.T) {
authFlow auth.AuthFlow
sa_email *string
user_email *string
accessToken *string
expectedRequest resourcemanager.ApiCreateProjectRequest
isValid bool
}{
Expand Down Expand Up @@ -220,6 +233,13 @@ func TestBuildRequest(t *testing.T) {
expectedRequest: fixtureRequest(),
isValid: true,
},
{
description: "access_token_env_var_no_stored_auth_flow",
model: fixtureInputModel(),
accessToken: utils.Ptr(buildTestJWT(testEmail)),
expectedRequest: fixtureRequest(),
isValid: true,
},
{
description: "missing_network_area_id sa_key",
model: fixtureInputModel(
Expand Down Expand Up @@ -296,6 +316,9 @@ func TestBuildRequest(t *testing.T) {
t.Fatalf("Failed to set user email in storage: %v", err)
}
}
if tt.accessToken != nil {
t.Setenv("STACKIT_ACCESS_TOKEN", *tt.accessToken)
}
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
if !tt.isValid {
Expand Down
3 changes: 3 additions & 0 deletions internal/pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ func UserSessionExpired() (bool, error) {
}

func GetAccessToken() (string, error) {
if accessToken := os.Getenv(envAccessTokenName); accessToken != "" {
return accessToken, nil
}
accessToken, err := GetAuthField(ACCESS_TOKEN)
if err != nil {
return "", fmt.Errorf("get %s: %w", ACCESS_TOKEN, err)
Expand Down
65 changes: 65 additions & 0 deletions internal/pkg/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,68 @@ func TestInitKeyFlow(t *testing.T) {
})
}
}

func TestGetAccessToken_EnvVar(t *testing.T) {
const envValue = "token-from-env"
const storedValue = "stored-token"

tests := []struct {
description string
envToken string
storedToken string
expectedToken string
isValid bool
}{
{
description: "env var set and no stored token",
envToken: envValue,
expectedToken: envValue,
isValid: true,
},
{
description: "env var set and stored token present",
envToken: envValue,
storedToken: storedValue,
expectedToken: envValue,
isValid: true,
},
{
description: "env var not set and stored token present",
storedToken: storedValue,
expectedToken: storedValue,
isValid: true,
},
{
description: "env var not set and no stored token",
isValid: false,
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
keyring.MockInit()
if tt.envToken != "" {
t.Setenv(envAccessTokenName, tt.envToken)
}
if tt.storedToken != "" {
if err := SetAuthField(ACCESS_TOKEN, tt.storedToken); err != nil {
t.Fatalf("Failed to set stored token: %v", err)
}
if err := SetAuthFlow(AUTH_FLOW_SERVICE_ACCOUNT_TOKEN); err != nil {
t.Fatalf("Failed to set auth flow: %v", err)
}
}

got, err := GetAccessToken()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("unexpected error: %v", err)
}
if tt.expectedToken != got {
t.Errorf("expected token %q, got %q", tt.expectedToken, got)
}
})
}
}
3 changes: 3 additions & 0 deletions internal/pkg/auth/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ func GetAuthFieldMap(keyMap map[authFieldKey]string) error {
}

func GetAuthFlow() (AuthFlow, error) {
if accessToken := os.Getenv(envAccessTokenName); accessToken != "" {
return AUTH_FLOW_SERVICE_ACCOUNT_TOKEN, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure about this one. Decisions on which AuthFlow to use seem to be done in functions where SetAuthFlow() is called.

What's the reason to include this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention was to guarantee that the access token is used when it is set. Before, it as possible to have the token set and still be treated unauthenticated by the CLI which why the original issue came up. I personally don't see a reason not to use the token when it is set.

With this change, GetAuthFlow is now aware of the token's existence. Does that make sense?

}
value, err := GetAuthField(authFlowType)
return AuthFlow(value), err
}
Expand Down
61 changes: 61 additions & 0 deletions internal/pkg/auth/storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1216,3 +1216,64 @@ func TestAuthorizeDeauthorizeUserProfileAuth(t *testing.T) {
})
}
}

func TestGetAuthFlow_EnvVar(t *testing.T) {
const envValue = "some-token"

tests := []struct {
description string
envToken string
storedFlow AuthFlow
expectedFlow AuthFlow
isValid bool
}{
{
description: "env var set and no stored flow",
envToken: envValue,
expectedFlow: AUTH_FLOW_SERVICE_ACCOUNT_TOKEN,
isValid: true,
},
{
description: "env var set and different flow stored",
envToken: envValue,
storedFlow: AUTH_FLOW_USER_TOKEN,
expectedFlow: AUTH_FLOW_SERVICE_ACCOUNT_TOKEN,
isValid: true,
},
{
description: "env var not set and stored flow present",
storedFlow: AUTH_FLOW_SERVICE_ACCOUNT_KEY,
expectedFlow: AUTH_FLOW_SERVICE_ACCOUNT_KEY,
isValid: true,
},
{
description: "env var not set and no stored flow",
isValid: false,
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
keyring.MockInit()
if tt.envToken != "" {
t.Setenv(envAccessTokenName, tt.envToken)
}
if tt.storedFlow != "" {
if err := SetAuthFlow(tt.storedFlow); err != nil {
t.Fatalf("Failed to set stored auth flow: %v", err)
}
}

got, err := GetAuthFlow()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("unexpected error: %v", err)
}
if tt.expectedFlow != got {
t.Errorf("expected flow %q, got %q", tt.expectedFlow, got)
}
})
}
}
Loading