Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: PROJECT_ID exposed as an env value just like INFISICA_TOKEN #2912 #2976

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
33 changes: 27 additions & 6 deletions cli/packages/cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@ const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com"
// duration to reduce from expiry of dynamic leases so that it gets triggered before expiry
const DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER = -15

type HttpConfig struct {
Headers map[string]string `yaml:"headers"`
}

type Config struct {
Infisical InfisicalConfig `yaml:"infisical"`
Auth AuthConfig `yaml:"auth"`
Sinks []Sink `yaml:"sinks"`
Templates []Template `yaml:"templates"`
Http HttpConfig `yaml:"http"`
}

type InfisicalConfig struct {
Expand Down Expand Up @@ -284,6 +289,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
} `yaml:"auth"`
Sinks []Sink `yaml:"sinks"`
Templates []Template `yaml:"templates"`
Http HttpConfig `yaml:"http"`
}

if err := yaml.Unmarshal(configFile, &rawConfig); err != nil {
Expand All @@ -307,6 +313,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
},
Sinks: rawConfig.Sinks,
Templates: rawConfig.Templates,
Http: rawConfig.Http,
}

return config, nil
Expand Down Expand Up @@ -498,6 +505,7 @@ type AgentManager struct {
removeUniversalAuthClientSecretOnRead bool
cachedUniversalAuthClientSecret string
exitAfterAuth bool
httpClient *resty.Client

infisicalClient infisicalSdk.InfisicalClientInterface
}
Expand All @@ -511,13 +519,30 @@ type NewAgentMangerOptions struct {

NewAccessTokenNotificationChan chan bool
ExitAfterAuth bool
Config *Config
}

func NewAgentManager(options NewAgentMangerOptions) *AgentManager {

// creating the base client
client := resty.New()

// added null checck for config and if the custom headers are configured then add them to the client
if options.Config != nil && len(options.Config.Http.Headers) > 0 {
for key, value := range options.Config.Http.Headers {
client.SetHeader(key, value)
}
log.Info().Msgf("Added %d custom headers to request client", len(options.Config.Http.Headers))
}

client.SetRetryCount(10000).
SetRetryMaxWaitTime(20 * time.Second).
SetRetryWaitTime(5 * time.Second)

return &AgentManager{
filePaths: options.FileDeposits,
templates: options.Templates,
httpClient: client,

authConfigBytes: options.AuthConfigBytes,
authStrategy: options.AuthStrategy,
Expand Down Expand Up @@ -716,13 +741,8 @@ func (tm *AgentManager) FetchNewAccessToken() error {

// Refreshes the existing access token
func (tm *AgentManager) RefreshAccessToken() error {
httpClient := resty.New()
httpClient.SetRetryCount(10000).
SetRetryMaxWaitTime(20 * time.Second).
SetRetryWaitTime(5 * time.Second)

accessToken := tm.GetToken()
response, err := api.CallMachineIdentityRefreshAccessToken(httpClient, api.UniversalAuthRefreshRequest{AccessToken: accessToken})
response, err := api.CallMachineIdentityRefreshAccessToken(tm.httpClient, api.UniversalAuthRefreshRequest{AccessToken: accessToken})
if err != nil {
return err
}
Expand Down Expand Up @@ -994,6 +1014,7 @@ var agentCmd = &cobra.Command{
NewAccessTokenNotificationChan: tokenRefreshNotifier,
ExitAfterAuth: agentConfig.Infisical.ExitAfterAuth,
AuthStrategy: authStrategy,
Config: agentConfig,
})

tm.dynamicSecretLeases = NewDynamicSecretLeaseManager(sigChan)
Expand Down
10 changes: 10 additions & 0 deletions cli/packages/cmd/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,21 @@ var secretsCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}

// Using GetProjectID utility function
projectDetails, err := util.GetProjectID(cmd)
if err != nil {
util.HandleError(err, "Unable to determine project ID")
}

projectId, err := cmd.Flags().GetString("projectId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}

if projectDetails != nil {
projectId = projectDetails.ID
}

secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse flag")
Expand Down
8 changes: 7 additions & 1 deletion cli/packages/models/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "time"
type UserCredentials struct {
Email string `json:"email"`
PrivateKey string `json:"privateKey"`
JTWToken string `json:"JTWToken"`
JWTToken string `json:"JWTToken"`
RefreshToken string `json:"RefreshToken"`
}

Expand Down Expand Up @@ -69,6 +69,12 @@ type TokenDetails struct {
Source string
}

// ProjectDetails contains the project ID and its source
type ProjectDetails struct {
ID string
Source string
}

type SingleFolder struct {
ID string `json:"_id"`
Name string `json:"name"`
Expand Down
7 changes: 7 additions & 0 deletions cli/packages/util/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ const (
INFISICAL_DEFAULT_EU_URL = "https://eu.infisical.com"
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
INFISICAL_PROJECT_ID = "INFISICAL_PROJECT_ID"
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.

// JWT configuration
INFISICAL_JWT_EXPIRATION_NAME = "INFISICAL_JWT_EXPIRATION"
DEFAULT_JWT_EXPIRATION = 86400 // 24 hours in seconds
MIN_JWT_EXPIRATION = 3600 // 1 hour in seconds
MAX_JWT_EXPIRATION = 2592000 // 30 days in seconds

VAULT_BACKEND_AUTO_MODE = "auto"
VAULT_BACKEND_FILE_MODE = "file"

Expand Down
34 changes: 6 additions & 28 deletions cli/packages/util/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import (
"fmt"
"strings"

"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/go-resty/resty/v2"
"github.com/zalando/go-keyring"
)

Expand Down Expand Up @@ -77,36 +75,16 @@ func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails

if setConfigVariables {
config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL
//configFile.LoggedInUserDomain
//if not empty set as infisical url
if configFile.LoggedInUserDomain != "" {
config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain)
}
}

// check to to see if the JWT is still valid
httpClient := resty.New().
SetAuthToken(userCreds.JTWToken).
SetHeader("Accept", "application/json")

isAuthenticated := api.CallIsAuthenticated(httpClient)
// TODO: add refresh token
// if !isAuthenticated {
// accessTokenResponse, err := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken)
// if err == nil && accessTokenResponse.Token != "" {
// isAuthenticated = true
// userCreds.JTWToken = accessTokenResponse.Token
// }
// }

// err = StoreUserCredsInKeyRing(&userCreds)
// if err != nil {
// log.Debug().Msg("unable to store your user credentials with new access token")
// }

if !isAuthenticated {
// Attempt to refresh token if needed
err = HandleTokenRefresh(&userCreds)
if err != nil {
return LoggedInUserDetails{
IsUserLoggedIn: true, // was logged in
IsUserLoggedIn: true,
LoginExpired: true,
UserCredentials: userCreds,
}, nil
Expand All @@ -117,7 +95,7 @@ func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails
LoginExpired: false,
UserCredentials: userCreds,
}, nil
} else {
return LoggedInUserDetails{}, nil
}

return LoggedInUserDetails{}, nil
}
45 changes: 45 additions & 0 deletions cli/packages/util/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,51 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro

}

// GetProjectID retrieves the project ID with the following precedence:
// 1. --projectId flag
// 2. INFISICAL_PROJECT_ID environment variable
// 3. Local workspace config file
func GetProjectID(cmd *cobra.Command) (*models.ProjectDetails, error) {

projectID, err := cmd.Flags().GetString("projectId")

if err != nil {
return nil, fmt.Errorf("unable to parse projectId flag: %w", err)
}

if projectID != "" {
return &models.ProjectDetails{
ID: projectID,
Source: "--projectId flag",
}, nil
}

// Check environment variable
projectID = os.Getenv(INFISICAL_PROJECT_ID)
if projectID != "" {
return &models.ProjectDetails{
ID: projectID,
Source: fmt.Sprintf("%s environment variable", INFISICAL_PROJECT_ID),
}, nil
}

// Try workspace file
configFile, err := GetWorkSpaceFromFile()
if err == nil && configFile.WorkspaceId != "" {
return &models.ProjectDetails{
ID: configFile.WorkspaceId,
Source: "workspace configuration file",
}, nil
}

// When using service tokens or machine identities, project ID is required
if cmd.Flags().Changed("token") {
return nil, fmt.Errorf("when using service tokens or machine identities, you must provide a project ID either via --projectId flag or %s environment variable", INFISICAL_PROJECT_ID)
}

return nil, nil // Return nil when no project ID is found and it's not required
}

func UniversalAuthLogin(clientId string, clientSecret string) (api.UniversalAuthLoginResponse, error) {
httpClient := resty.New()
httpClient.SetRetryCount(10000).
Expand Down
56 changes: 56 additions & 0 deletions cli/packages/util/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package util

import (
"fmt"

"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
)

// RefreshJWTToken attempts to refresh an expired JWT token using the refresh token
func RefreshJWTToken(httpClient *resty.Client, refreshToken string) (string, error) {
if refreshToken == "" {
return "", fmt.Errorf("no refresh token available")
}

accessTokenResponse, err := api.CallGetNewAccessTokenWithRefreshToken(httpClient, refreshToken)
if err != nil {
return "", fmt.Errorf("failed to refresh token: %w", err)
}

if accessTokenResponse.Token == "" {
return "", fmt.Errorf("received empty token from refresh attempt")
}

return accessTokenResponse.Token, nil
}

// IsTokenExpired checks if the given token is expired by validating with the server
func IsTokenExpired(httpClient *resty.Client) bool {
return !api.CallIsAuthenticated(httpClient)
}

// HandleTokenRefresh handles the complete token refresh flow
func HandleTokenRefresh(userCreds *models.UserCredentials) error {
httpClient := resty.New().
SetAuthToken(userCreds.JWTToken).
SetHeader("Accept", "application/json")

if IsTokenExpired(httpClient) && userCreds.RefreshToken != "" {
newToken, err := RefreshJWTToken(httpClient, userCreds.RefreshToken)
if err != nil {
return err
}

userCreds.JWTToken = newToken
err = StoreUserCredsInKeyRing(userCreds)
if err != nil {
log.Debug().Msg("unable to store refreshed credentials in keyring")
return err
}
}

return nil
}