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

feature: Using terraform together with infisical login. #36

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
29 changes: 13 additions & 16 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,41 @@ go 1.19

require (
github.com/go-resty/resty/v2 v2.7.0
github.com/hashicorp-demoapp/hashicups-client-go v0.1.0
github.com/hashicorp/terraform-plugin-docs v0.15.0
github.com/hashicorp/terraform-plugin-framework v1.3.0
github.com/hashicorp/terraform-plugin-go v0.15.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.2.0
github.com/zalando/go-keyring v0.2.3
golang.org/x/crypto v0.8.0
)

require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
github.com/agext/levenshtein v1.2.2 // indirect
github.com/alessio/shellescape v1.4.2 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/danieljoos/wincred v1.2.1 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.4.10 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hc-install v0.5.2 // indirect
github.com/hashicorp/hcl/v2 v2.16.2 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.18.1 // indirect
github.com/hashicorp/terraform-json v0.16.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 // indirect
github.com/hashicorp/terraform-plugin-go v0.15.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.0 // indirect
github.com/hashicorp/terraform-svchost v0.1.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
Expand All @@ -51,25 +49,24 @@ require (
github.com/mitchellh/cli v1.1.5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/russross/blackfriday v1.6.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/zclconf/go-cty v1.13.2 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.11.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)

replace github.com/zalando/go-keyring => github.com/Infisical/go-keyring v1.0.2
62 changes: 22 additions & 40 deletions go.sum

Large diffs are not rendered by default.

41 changes: 33 additions & 8 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import (
"fmt"
"slices"

Check failure on line 5 in internal/client/client.go

View workflow job for this annotation

GitHub Actions / Build

package slices is not in GOROOT (/opt/hostedtoolcache/go/1.19.13/x64/src/slices)
"strings"
"terraform-provider-infisical/internal/cliuser"

"github.com/go-resty/resty/v2"
)
Expand All @@ -15,9 +18,11 @@
var AuthStrategy = struct {
SERVICE_TOKEN AuthStrategyType
UNIVERSAL_MACHINE_IDENTITY AuthStrategyType
USER_PROFILE AuthStrategyType
}{
SERVICE_TOKEN: "SERVICE_TOKEN",
UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY",
USER_PROFILE: "USER_PROFILE",
}

type Config struct {
Expand All @@ -32,11 +37,25 @@
ClientId string
ClientSecret string

Profile string

EnvSlug string
SecretsPath string
HttpClient *resty.Client // By default a client will be created
}

func (c *Client) ValidateAuthMode(modes []AuthStrategyType) (bool, error) {
if slices.Contains(modes, c.Config.AuthStrategy) {
return true, nil
}

var authErrorString []string
for _, mode := range modes {
authErrorString = append(authErrorString, string(mode))
}
return false, fmt.Errorf("Only %s authentication is supported", strings.Join(authErrorString, ","))
}

func NewClient(cnf Config) (*Client, error) {
if cnf.HttpClient == nil {
cnf.HttpClient = resty.New()
Expand All @@ -46,13 +65,7 @@
// Add more auth strategies here later
var usingServiceToken = cnf.ServiceToken != ""
var usingUniversalAuth = cnf.ClientId != "" && cnf.ClientSecret != ""

// Check if the user got multiple configured authentication methods, or none set at all.
if usingServiceToken && usingUniversalAuth {
return nil, fmt.Errorf("you have configured multiple authentication methods, please only use one")
} else if !usingServiceToken && !usingUniversalAuth {
return nil, fmt.Errorf("you must configure a authentication method such as service tokens or Universal Auth before making calls")
}
var usingInfisicalProfile = cnf.Profile != ""

if usingUniversalAuth {
token, err := Client{cnf}.UniversalMachineIdentityAuth()
Expand All @@ -66,9 +79,21 @@
} else if usingServiceToken {
cnf.HttpClient.SetAuthToken(cnf.ServiceToken)
cnf.AuthStrategy = AuthStrategy.SERVICE_TOKEN
} else if usingInfisicalProfile {
token, err := cliuser.GetCurrentLoggedInUserDetails(cnf.Profile)
if err != nil {
return nil, fmt.Errorf("Unable to authenticate with user profile. [err=%s]", err)
}
_, err = Client{cnf}.CheckJWTIsValid(token)
if err != nil {
return nil, fmt.Errorf("Unable to authenticate with user profile. [err=%s]", err)
}

cnf.HttpClient.SetAuthToken(token)
cnf.AuthStrategy = AuthStrategy.USER_PROFILE
} else {
// If no auth strategy is set, then we should return an error
return nil, fmt.Errorf("you must configure a authentication method such as service tokens or Universal Auth before making calls")
return nil, fmt.Errorf("you must configure a authentication method such as service tokens or Universal Auth or infisical login before making calls")
}

// These two if statements were a part of an older migration.
Expand Down
20 changes: 20 additions & 0 deletions internal/client/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,23 @@ func (client Client) GetServiceTokenDetailsV2() (GetServiceTokenDetailsResponse,

return tokenDetailsResponse, nil
}

func (client Client) CheckJWTIsValid(token string) (map[string]any, error) {
var tokenDetailsResponse map[string]any
response, err := client.Config.HttpClient.
R().
SetResult(&tokenDetailsResponse).
SetHeader("User-Agent", USER_AGENT).
SetAuthToken(token).
Post("api/v1/auth/checkAuth")

if err != nil {
return nil, fmt.Errorf("CallGetServiceTokenDetails: Unable to complete api request [err=%s]", err)
}

if response.IsError() {
return nil, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response)
}

return tokenDetailsResponse, nil
}
2 changes: 1 addition & 1 deletion internal/client/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ func (client Client) GetPlainTextSecretsViaServiceToken(secretFolderPath string,
}

func (client Client) GetRawSecrets(secretFolderPath string, envSlug string, workspaceId string) ([]RawV3Secret, error) {
if client.Config.ClientId == "" || client.Config.ClientSecret == "" {
if (client.Config.ClientId == "" || client.Config.ClientSecret == "") && client.Config.AuthStrategy != AuthStrategy.USER_PROFILE {
return nil, fmt.Errorf("client ID and client secret must be defined to fetch secrets with machine identity")
}

Expand Down
68 changes: 68 additions & 0 deletions internal/cliuser/cliuser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cliuser

import (
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/zalando/go-keyring"
)

type UserCredentials struct {
Email string `json:"email"`
PrivateKey string `json:"privateKey"`
JTWToken string `json:"JTWToken"`
RefreshToken string `json:"RefreshToken"`
}

func GetCurrentLoggedInUserDetails(profile string) (string, error) {
if ConfigFileExists() {
configFile, err := GetConfigFile()
if err != nil {
return "", fmt.Errorf("getCurrentLoggedInUserDetails: unable to get logged in user from config file [err=%s]", err)
}

if configFile.LoggedInUserEmail == "" {
return "", errors.New("Login user not found. Try infisical login.")
}

if configFile.LoggedInUserEmail != profile {
return "", errors.New("User profile not found. Try infisical login.")
}

userCreds, err := GetUserCredsFromKeyRing(configFile.LoggedInUserEmail)
if err != nil {
if strings.Contains(err.Error(), "credentials not found in system keyring") {
return "", errors.New("we couldn't find your logged in details, try running [infisical login] then try again")
} else {
return "", fmt.Errorf("failed to fetch credentials from keyring because [err=%s]", err)
}
}
return userCreds.JTWToken, nil
}

return "", errors.New("Config file not found. Try infisical login.")
}

func GetUserCredsFromKeyRing(userEmail string) (credentials UserCredentials, err error) {
credentialsValue, err := GetValueInKeyring(userEmail)
if err != nil {
if err == keyring.ErrUnsupportedPlatform {
return UserCredentials{}, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's point users towards machine identities instead of service tokens

} else if err == keyring.ErrNotFound {
return UserCredentials{}, errors.New("credentials not found in system keyring")
} else {
return UserCredentials{}, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
}
}

var userCredentials UserCredentials

err = json.Unmarshal([]byte(credentialsValue), &userCredentials)
if err != nil {
return UserCredentials{}, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err)
}

return userCredentials, err
}
77 changes: 77 additions & 0 deletions internal/cliuser/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cliuser

import (
"encoding/json"
"fmt"
"os"
)

const (
CONFIG_FOLDER_NAME = ".infisical"
CONFIG_FILE_NAME = "infisical-config.json"
)

type LoggedInUser struct {
Email string `json:"email"`
Domain string `json:"domain"`
}

// The file struct for Infisical config file.
type ConfigFile struct {
LoggedInUserEmail string `json:"loggedInUserEmail"`
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
VaultBackendType string `json:"vaultBackendType,omitempty"`
}

func GetHomeDir() (string, error) {
directory, err := os.UserHomeDir()
return directory, err
}

func GetFullConfigFilePath() (fullPathToFile string, fullPathToDirectory string, err error) {
homeDir, err := GetHomeDir()
if err != nil {
return "", "", err
}

fullPath := fmt.Sprintf("%s/%s/%s", homeDir, CONFIG_FOLDER_NAME, CONFIG_FILE_NAME)
fullDirPath := fmt.Sprintf("%s/%s", homeDir, CONFIG_FOLDER_NAME)
return fullPath, fullDirPath, err
}

func GetConfigFile() (ConfigFile, error) {
fullConfigFilePath, _, err := GetFullConfigFilePath()
if err != nil {
return ConfigFile{}, err
}

configFileAsBytes, err := os.ReadFile(fullConfigFilePath)
if err != nil {
if err, ok := err.(*os.PathError); !ok {
return ConfigFile{}, err
}
return ConfigFile{}, nil
}

var configFile ConfigFile
err = json.Unmarshal(configFileAsBytes, &configFile)
if err != nil {
return ConfigFile{}, err
}

return configFile, nil
}

func ConfigFileExists() bool {
fullConfigFileURI, _, err := GetFullConfigFilePath()
if err != nil {
return false
}

if _, err := os.Stat(fullConfigFileURI); err == nil {
return true
} else {
return false
}
}
34 changes: 34 additions & 0 deletions internal/cliuser/keyring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cliuser

import (
"fmt"
"github.com/zalando/go-keyring"
)

const MAIN_KEYRING_SERVICE = "infisical-cli"

func GetCurrentVaultBackend() (string, error) {
configFile, err := GetConfigFile()
if err != nil {
return "", fmt.Errorf("getCurrentVaultBackend: unable to get config file [err=%s]", err)
}

if configFile.VaultBackendType == "" {
return "auto", nil
}

if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" {
return "auto", nil
}

return configFile.VaultBackendType, nil
}

func GetValueInKeyring(key string) (string, error) {
currentVaultBackend, err := GetCurrentVaultBackend()
if err != nil {
return "", fmt.Errorf("Unable to get current vault. Tip: run [infisical reset] then try again. %w", err)
}

return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
}
7 changes: 3 additions & 4 deletions internal/provider/datasource/projects_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,10 @@ func (d *ProjectsDataSource) Configure(ctx context.Context, req datasource.Confi
}

func (d *ProjectsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {

if d.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY {
if isValid, err := d.client.ValidateAuthMode([]infisical.AuthStrategyType{infisical.AuthStrategy.USER_PROFILE, infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY}); !isValid {
resp.Diagnostics.AddError(
"Unable to create project",
"Only Machine Identity authentication is supported for this operation",
"Unable to read project",
err.Error(),
)
return
}
Expand Down
Loading
Loading