Skip to content
Merged
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
5 changes: 3 additions & 2 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ jobs:
tfplan.json \
> ./overmindtech-change-url

./overmind changes get-change \
--change "$(< ./overmindtech-change-url)" \
# we only want the last line of the previous command
./overmind changes get-change \
--change "$(tail -n 1 ./overmindtech-change-url)" \
--format markdown \
> ./overmindtech-message

Expand Down
2 changes: 1 addition & 1 deletion cmd/changes_get_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const assetVersion = "17c7fd2c365d4f4cdd8e414ca5148f825fa4febd"
func GetChange(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

app := getAppUrl(viper.GetString("frontend"), viper.GetString("app"))
app := viper.GetString("app")

riskLevels := []sdp.Risk_Severity{}
for _, level := range viper.GetStringSlice("risk-levels") {
Expand Down
3 changes: 1 addition & 2 deletions cmd/changes_manual_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ var manualChangeCmd = &cobra.Command{

func ManualChange(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

app := getAppUrl(viper.GetString("frontend"), viper.GetString("app"))
app := viper.GetString("app")

method, err := methodFromString(viper.GetString("query-method"))
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/changes_submit_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func tryLoadText(ctx context.Context, fileName string) string {
func SubmitPlan(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

app := getAppUrl(viper.GetString("frontend"), viper.GetString("app"))
app := viper.GetString("app")

ctx, oi, _, err := login(ctx, cmd, []string{"changes:write"}, nil)
if err != nil {
Expand Down
4 changes: 0 additions & 4 deletions cmd/pterm.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,23 +321,19 @@ func extractClaims(token string) (*sdp.CustomClaims, error) {
// contains the right scopes. Therefore we just parse the payload
// directly
sections := strings.Split(token, ".")

if len(sections) != 3 {
return nil, errors.New("token is not a JWT")
}

// Decode the payload
decodedPayload, err := base64.RawURLEncoding.DecodeString(sections[1])

if err != nil {
return nil, fmt.Errorf("error decoding token payload: %w", err)
}

// Parse the payload
claims := new(sdp.CustomClaims)

err = json.Unmarshal(decodedPayload, claims)

if err != nil {
return nil, fmt.Errorf("error parsing token payload: %w", err)
}
Expand Down
153 changes: 102 additions & 51 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,12 @@ func PreRunSetup(cmd *cobra.Command, args []string) {
log.AllLevels[:log.GetLevel()+1]...,
)))
}

// set up app, it may be ambiguous if frontend is set
app := getAppUrl(viper.GetString("frontend"), viper.GetString("app"))
if app == "" {
log.Fatal("no app specified, please use --app or set OVM_APP")
}
viper.Set("app", app)
Copy link
Contributor

Choose a reason for hiding this comment

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

neat

// capture span in global variable to allow Execute() below to end it
ctx, cmdSpan = tracing.Tracer().Start(ctx, fmt.Sprintf("CLI %v", cmd.CommandPath()), trace.WithAttributes(
attribute.String("ovm.config", fmt.Sprintf("%v", tracedSettings())),
Expand Down Expand Up @@ -435,15 +440,15 @@ func ensureToken(ctx context.Context, oi sdp.OvermindInstance, requiredScopes []
// that token locally for use later, and will use the cached token if possible
func getOauthToken(ctx context.Context, oi sdp.OvermindInstance, requiredScopes []string) (*oauth2.Token, error) {
var localScopes []string

// Check for a locally saved token in ~/.overmind
if home, err := os.UserHomeDir(); err == nil {
var localToken *oauth2.Token

localToken, localScopes, err = readLocalToken(home, requiredScopes)

var localToken *oauth2.Token
home, err := os.UserHomeDir()
if err == nil {
// Check for a locally saved token in ~/.overmind
localToken, localScopes, err = readLocalTokenFile(home, viper.GetString("app"), requiredScopes)
if err != nil {
log.WithContext(ctx).Debugf("Error reading local token, ignoring: %v", err)
if !os.IsNotExist(err) {
pterm.Info.Println(fmt.Sprintf("Skipping using local token: %v. Re-authenticating.", err))
}
} else {
// If we already have the right scopes, return the token
return localToken, nil
Expand Down Expand Up @@ -532,41 +537,24 @@ func getOauthToken(ctx context.Context, oi sdp.OvermindInstance, requiredScopes
)
}

// Save the token locally
if home, err := os.UserHomeDir(); err == nil {
// Create the directory if it doesn't exist
err = os.MkdirAll(filepath.Join(home, ".overmind"), 0700)
if err != nil {
log.WithContext(ctx).WithError(err).Error("Failed to create ~/.overmind directory")
}

// Write the token to a file
path := filepath.Join(home, ".overmind", "token.json")
file, err := os.Create(path)
if err != nil {
log.WithContext(ctx).WithError(err).Errorf("Failed to create token file at %v", path)
}

// Encode the token
err = json.NewEncoder(file).Encode(token)
// Save the token to the local file, if the home directory is available
if home != "" {
err = saveLocalTokenFile(home, viper.GetString("app"), token)
if err != nil {
log.WithContext(ctx).WithError(err).Errorf("Failed to encode token file at %v", path)
// we don't worry if we cannot save the token, it will just be requested again
log.WithContext(ctx).WithError(err).Error("Error saving token")
}

log.WithContext(ctx).Debugf("Saved token to %v", path)
}

return token, nil
}

// Gets a token using an API key
func getAPIKeyToken(ctx context.Context, oi sdp.OvermindInstance, apiKey string, requiredScopes []string) (*oauth2.Token, error) {
log.WithContext(ctx).Debug("using provided token for authentication")

var token *oauth2.Token

app := viper.GetString("app")
if !strings.HasPrefix(apiKey, "ovm_api_") {
return nil, errors.New("OVM_API_KEY does not match pattern 'ovm_api_*'")
return nil, errors.New("--api-key or OVM_API_KEY or API_KEY does not match pattern 'ovm_api_*'")
}

// exchange api token for JWT
Expand All @@ -577,9 +565,8 @@ func getAPIKeyToken(ctx context.Context, oi sdp.OvermindInstance, apiKey string,
},
})
if err != nil {
return nil, fmt.Errorf("error authenticating the API token: %w", err)
return nil, fmt.Errorf("error authenticating the API token for %s: %w", app, err)
}
log.WithContext(ctx).Debug("successfully got a token from the API key")

token = &oauth2.Token{
AccessToken: resp.Msg.GetAccessToken(),
Expand All @@ -590,20 +577,30 @@ func getAPIKeyToken(ctx context.Context, oi sdp.OvermindInstance, apiKey string,
// permission auth0 will just not assign those scopes rather than fail
ok, missing, err := HasScopesFlexible(token, requiredScopes)
if err != nil {
return nil, fmt.Errorf("error checking token scopes: %w", err)
return nil, fmt.Errorf("error checking token scopes for %s: %w", app, err)
}
if !ok {
return nil, fmt.Errorf("authenticated successfully, but your API key is missing this permission: '%v'", missing)
return nil, fmt.Errorf("authenticated successfully against %s, but your API key is missing this permission: '%v'", app, missing)
}

pterm.Info.Println(fmt.Sprintf("Using Overmind API key for %s", app))
return token, nil
}

func readLocalToken(homeDir string, requiredScopes []string) (*oauth2.Token, []string, error) {
type TokenFile struct {
AuthEntries map[string]*TokenEntry `json:"auth_entries"`
}

type TokenEntry struct {
Token *oauth2.Token `json:"token"`
AddedDate time.Time `json:"added_date"`
}

// readLocalTokenFile is also used in the gateway assistant cli tool. It is copied over, so if you change it here, you should also change it there.
func readLocalTokenFile(homeDir, app string, requiredScopes []string) (*oauth2.Token, []string, error) {
// Read in the token JSON file
path := filepath.Join(homeDir, ".overmind", "token.json")

token := new(oauth2.Token)
tokenFile := new(TokenFile)

// Check that the file exists
if _, err := os.Stat(path); err != nil {
Expand All @@ -613,23 +610,29 @@ func readLocalToken(homeDir string, requiredScopes []string) (*oauth2.Token, []s
// Read the file
file, err := os.Open(path)
if err != nil {
return nil, nil, fmt.Errorf("error opening token file at %v: %w", path, err)
return nil, nil, fmt.Errorf("error opening token file at %q: %w", path, err)
}
defer file.Close()

// Decode the file
err = json.NewDecoder(file).Decode(token)
err = json.NewDecoder(file).Decode(tokenFile)
if err != nil {
return nil, nil, fmt.Errorf("error decoding token file at %v: %w", path, err)
return nil, nil, fmt.Errorf("error decoding token file at %q: %w", path, err)
}

authEntry, ok := tokenFile.AuthEntries[app]
if !ok {
return nil, nil, fmt.Errorf("no token found for app %s in %q", app, path)
}

// Check to see if the token is still valid
if !token.Valid() {
if !authEntry.Token.Valid() {
return nil, nil, errors.New("token is no longer valid")
}

claims, err := extractClaims(token.AccessToken)
claims, err := extractClaims(authEntry.Token.AccessToken)
if err != nil {
return nil, nil, fmt.Errorf("error extracting claims from token: %w", err)
return nil, nil, fmt.Errorf("error extracting claims from token: %s in %q: %w", app, path, err)
}
if claims.Scope == "" {
return nil, nil, errors.New("token does not have any scopes")
Expand All @@ -638,16 +641,64 @@ func readLocalToken(homeDir string, requiredScopes []string) (*oauth2.Token, []s
currentScopes := strings.Split(claims.Scope, " ")

// Check that we actually got the claims we asked for.
ok, missing, err := HasScopesFlexible(token, requiredScopes)
ok, missing, err := HasScopesFlexible(authEntry.Token, requiredScopes)
if err != nil {
return nil, currentScopes, fmt.Errorf("error checking token scopes: %w", err)
return nil, currentScopes, fmt.Errorf("error checking token scopes: %s in %q: %w", app, path, err)
}
if !ok {
return nil, currentScopes, fmt.Errorf("local token is missing this permission: '%v'", missing)
return nil, currentScopes, fmt.Errorf("local token is missing this permission: '%v'. %s in %q", missing, app, path)
}

pterm.Info.Println(fmt.Sprintf("Using local token for %s in %q", app, path))
return authEntry.Token, currentScopes, nil
}

func saveLocalTokenFile(homeDir, app string, token *oauth2.Token) error {
// Read in the existing token file if it exists
path := filepath.Join(homeDir, ".overmind", "token.json")

tokenFile := &TokenFile{
AuthEntries: make(map[string]*TokenEntry),
}

if _, err := os.Stat(path); err == nil {
file, err := os.Open(path)
if err == nil {
// file exists, read it
defer file.Close()

err = json.NewDecoder(file).Decode(tokenFile)
if err != nil {
return fmt.Errorf("error decoding token file at %q: %w", path, err)
}
}
} else {
err = os.MkdirAll(filepath.Dir(path), 0755)
if err != nil {
return fmt.Errorf("unexpected fail creating directories: %w", err)
}
}

// Update the token for the given app
tokenFile.AuthEntries[app] = &TokenEntry{
Token: token,
AddedDate: time.Now(),
}

// Write the updated token file
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("error creating token file at %q: %w", path, err)
}
defer file.Close()

err = json.NewEncoder(file).Encode(tokenFile)
if err != nil {
return fmt.Errorf("error encoding token file at %q: %w", path, err)
}

log.Debugf("Using local token from %v", path)
return token, currentScopes, nil
pterm.Info.Println(fmt.Sprintf("Saving token locally for %s at %q", app, path))
return nil
}

func getAppUrl(frontend, app string) string {
Expand Down
Loading
Loading