Skip to content
Draft
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
16 changes: 15 additions & 1 deletion pkg/aponoapi/client_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type Session struct {
}

func CreateClient(ctx context.Context, profileName string) (*AponoClient, error) {
sessionCfg, err := config.GetProfileByName(config.ProfileName(profileName))
sessionCfg, err := getSessionConfig(profileName)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -87,6 +87,20 @@ func CreateClient(ctx context.Context, profileName string) (*AponoClient, error)
}, nil
}

// getSessionConfig resolves the session to use for this invocation. An
// explicit --profile flag always wins; otherwise a token in the environment
// takes precedence over the persisted config, so that an environment-provided
// credential is honored even when a stale profile exists on disk.
func getSessionConfig(profileName string) (*config.SessionConfig, error) {
if profileName == "" {
if envCfg := config.GetSessionConfigFromEnv(); envCfg != nil {
return envCfg, nil
}
}

return config.GetProfileByName(config.ProfileName(profileName))
}

func CreateClientAPI(endpointURL *url.URL, httpClient *http.Client) *clientapi.APIClient {
clientAPIClientCfg := clientapi.NewConfiguration()
clientAPIClientCfg.Scheme = endpointURL.Scheme
Expand Down
35 changes: 35 additions & 0 deletions pkg/aponoapi/client_factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package aponoapi

import (
"testing"

"github.com/apono-io/apono-cli/pkg/config"
)

func TestGetSessionConfig(t *testing.T) {
t.Run("uses the env token when no profile is specified", func(t *testing.T) {
t.Setenv(config.PersonalTokenEnvVar, "test-personal-token")

sessionCfg, err := getSessionConfig("")
if err != nil {
t.Fatalf("getSessionConfig() error = %v, want nil", err)
}
if sessionCfg.PersonalToken != "test-personal-token" {
t.Errorf("PersonalToken = %q, want %q", sessionCfg.PersonalToken, "test-personal-token")
}
if sessionCfg.ApiURL != config.APIDefaultURL {
t.Errorf("ApiURL = %q, want %q", sessionCfg.ApiURL, config.APIDefaultURL)
}
})

t.Run("an explicit profile takes precedence over the env token", func(t *testing.T) {
t.Setenv(config.PersonalTokenEnvVar, "test-personal-token")

// The profile lookup must be attempted (and fail for this nonexistent
// profile) instead of falling back to the environment token.
sessionCfg, err := getSessionConfig("nonexistent-profile-for-test")
if err == nil {
t.Fatalf("getSessionConfig() = %+v, want error for nonexistent profile", sessionCfg)
}
})
}
29 changes: 29 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"errors"
"fmt"
"os"
"time"

"golang.org/x/oauth2"
Expand All @@ -12,6 +13,9 @@ const (
APIDefaultURL = "https://api.apono.io"
AppDefaultURL = "https://app.apono.io"
PortalDefaultURL = "https://portal.apono.io"

PersonalTokenEnvVar = "APONO_PERSONAL_TOKEN"
APIURLEnvVar = "APONO_API_URL"
)

var (
Expand Down Expand Up @@ -104,6 +108,31 @@ func GetProfileByName(profileName ProfileName) (*SessionConfig, error) {
return &sessionCfg, nil
}

// GetSessionConfigFromEnv returns a session authenticated by the personal
// token in the APONO_PERSONAL_TOKEN environment variable, or nil if the
// variable is not set. The session is not persisted, so secret managers can
// authenticate the CLI without a prior `apono login` and without the token
// ever being written to disk. APONO_API_URL overrides the API endpoint for
// non-production tenants.
func GetSessionConfigFromEnv() *SessionConfig {
personalToken := os.Getenv(PersonalTokenEnvVar)
if personalToken == "" {
return nil
}

apiURL := os.Getenv(APIURLEnvVar)
if apiURL == "" {
apiURL = APIDefaultURL
}

return &SessionConfig{
ApiURL: apiURL,
AppURL: AppDefaultURL,
PortalURL: PortalDefaultURL,
PersonalToken: personalToken,
}
}

func IsAccessHandlerAnnounced() bool {
cfg, err := Get()
if err != nil {
Expand Down
50 changes: 50 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package config

import (
"testing"
)

func TestGetSessionConfigFromEnv(t *testing.T) {
t.Run("returns nil when the token env var is not set", func(t *testing.T) {
t.Setenv(PersonalTokenEnvVar, "")

if sessionCfg := GetSessionConfigFromEnv(); sessionCfg != nil {
t.Errorf("GetSessionConfigFromEnv() = %+v, want nil", sessionCfg)
}
})

t.Run("returns a personal token session with default URLs", func(t *testing.T) {
t.Setenv(PersonalTokenEnvVar, "test-personal-token")
t.Setenv(APIURLEnvVar, "")

sessionCfg := GetSessionConfigFromEnv()
if sessionCfg == nil {
t.Fatal("GetSessionConfigFromEnv() = nil, want session config")
}
if sessionCfg.PersonalToken != "test-personal-token" {
t.Errorf("PersonalToken = %q, want %q", sessionCfg.PersonalToken, "test-personal-token")
}
if sessionCfg.ApiURL != APIDefaultURL {
t.Errorf("ApiURL = %q, want %q", sessionCfg.ApiURL, APIDefaultURL)
}
if sessionCfg.AppURL != AppDefaultURL {
t.Errorf("AppURL = %q, want %q", sessionCfg.AppURL, AppDefaultURL)
}
if sessionCfg.PortalURL != PortalDefaultURL {
t.Errorf("PortalURL = %q, want %q", sessionCfg.PortalURL, PortalDefaultURL)
}
})

t.Run("honors the API URL override env var", func(t *testing.T) {
t.Setenv(PersonalTokenEnvVar, "test-personal-token")
t.Setenv(APIURLEnvVar, "https://api.example.apono.dev")

sessionCfg := GetSessionConfigFromEnv()
if sessionCfg == nil {
t.Fatal("GetSessionConfigFromEnv() = nil, want session config")
}
if sessionCfg.ApiURL != "https://api.example.apono.dev" {
t.Errorf("ApiURL = %q, want %q", sessionCfg.ApiURL, "https://api.example.apono.dev")
}
})
}