Integrate encryption into plan serialization (#1292)

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Christian Mesh 2024-03-04 09:00:29 -05:00 committed by GitHub
parent 997e5fa46e
commit ac3ed86617
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 75 additions and 42 deletions

View File

@ -19,6 +19,7 @@ import (
"github.com/opentofu/opentofu/internal/command/views" "github.com/opentofu/opentofu/internal/command/views"
"github.com/opentofu/opentofu/internal/configs/configload" "github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/initwd" "github.com/opentofu/opentofu/internal/initwd"
"github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/plans/planfile" "github.com/opentofu/opentofu/internal/plans/planfile"
@ -97,7 +98,7 @@ func TestLocalRun_cloudPlan(t *testing.T) {
planPath := "./testdata/plan-bookmark/bookmark.json" planPath := "./testdata/plan-bookmark/bookmark.json"
planFile, err := planfile.OpenWrapped(planPath) planFile, err := planfile.OpenWrapped(planPath, encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
t.Fatalf("unexpected error reading planfile: %s", err) t.Fatalf("unexpected error reading planfile: %s", err)
} }
@ -180,10 +181,10 @@ func TestLocalRun_stalePlan(t *testing.T) {
StateFile: stateFile, StateFile: stateFile,
Plan: plan, Plan: plan,
} }
if err := planfile.Create(planPath, planfileArgs); err != nil { if err := planfile.Create(planPath, planfileArgs, encryption.PlanEncryptionDisabled()); err != nil {
t.Fatalf("unexpected error writing planfile: %s", err) t.Fatalf("unexpected error writing planfile: %s", err)
} }
planFile, err := planfile.OpenWrapped(planPath) planFile, err := planfile.OpenWrapped(planPath, encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
t.Fatalf("unexpected error reading planfile: %s", err) t.Fatalf("unexpected error reading planfile: %s", err)
} }

View File

@ -12,6 +12,7 @@ import (
"log" "log"
"github.com/opentofu/opentofu/internal/backend" "github.com/opentofu/opentofu/internal/backend"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/genconfig" "github.com/opentofu/opentofu/internal/genconfig"
"github.com/opentofu/opentofu/internal/logging" "github.com/opentofu/opentofu/internal/logging"
"github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/plans"
@ -172,7 +173,7 @@ func (b *Local) opPlan(
StateFile: plannedStateFile, StateFile: plannedStateFile,
Plan: plan, Plan: plan,
DependencyLocks: op.DependencyLocks, DependencyLocks: op.DependencyLocks,
}) }, encryption.PlanEncryptionTODO())
if err != nil { if err != nil {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,

View File

@ -21,6 +21,7 @@ import (
"github.com/opentofu/opentofu/internal/command/views" "github.com/opentofu/opentofu/internal/command/views"
"github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/depsfile" "github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/initwd" "github.com/opentofu/opentofu/internal/initwd"
"github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/plans/planfile" "github.com/opentofu/opentofu/internal/plans/planfile"
@ -841,11 +842,10 @@ func testPlanState_tainted() *states.State {
func testReadPlan(t *testing.T, path string) *plans.Plan { func testReadPlan(t *testing.T, path string) *plans.Plan {
t.Helper() t.Helper()
p, err := planfile.Open(path) p, err := planfile.Open(path, encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
defer p.Close()
plan, err := p.ReadPlan() plan, err := p.ReadPlan()
if err != nil { if err != nil {

View File

@ -40,6 +40,7 @@ import (
"github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/copy" "github.com/opentofu/opentofu/internal/copy"
"github.com/opentofu/opentofu/internal/depsfile" "github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/getproviders" "github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/initwd" "github.com/opentofu/opentofu/internal/initwd"
legacy "github.com/opentofu/opentofu/internal/legacy/tofu" legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
@ -228,7 +229,7 @@ func testPlanFileMatchState(t *testing.T, configSnap *configload.Snapshot, state
StateFile: stateFile, StateFile: stateFile,
Plan: plan, Plan: plan,
DependencyLocks: depsfile.NewLocks(), DependencyLocks: depsfile.NewLocks(),
}) }, encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
t.Fatalf("failed to create temporary plan file: %s", err) t.Fatalf("failed to create temporary plan file: %s", err)
} }
@ -276,11 +277,10 @@ func testFileEquals(t *testing.T, got, want string) {
func testReadPlan(t *testing.T, path string) *plans.Plan { func testReadPlan(t *testing.T, path string) *plans.Plan {
t.Helper() t.Helper()
f, err := planfile.Open(path) f, err := planfile.Open(path, encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
t.Fatalf("error opening plan file %q: %s", path, err) t.Fatalf("error opening plan file %q: %s", path, err)
} }
defer f.Close()
p, err := f.ReadPlan() p, err := f.ReadPlan()
if err != nil { if err != nil {

View File

@ -9,6 +9,7 @@ import (
"os" "os"
"strconv" "strconv"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans/planfile" "github.com/opentofu/opentofu/internal/plans/planfile"
) )
@ -48,5 +49,5 @@ func (m *Meta) PlanFile(path string) (*planfile.WrappedPlanFile, error) {
return nil, nil return nil, nil
} }
return planfile.OpenWrapped(path) return planfile.OpenWrapped(path, encryption.PlanEncryptionTODO())
} }

View File

@ -18,6 +18,7 @@ import (
"github.com/opentofu/opentofu/internal/command/arguments" "github.com/opentofu/opentofu/internal/command/arguments"
"github.com/opentofu/opentofu/internal/command/views" "github.com/opentofu/opentofu/internal/command/views"
"github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/plans/planfile" "github.com/opentofu/opentofu/internal/plans/planfile"
"github.com/opentofu/opentofu/internal/states/statefile" "github.com/opentofu/opentofu/internal/states/statefile"
@ -272,7 +273,7 @@ func (c *ShowCommand) getPlanFromPath(path string) (*plans.Plan, *cloudplan.Remo
var stateFile *statefile.File var stateFile *statefile.File
var config *configs.Config var config *configs.Config
pf, err := planfile.OpenWrapped(path) pf, err := planfile.OpenWrapped(path, encryption.PlanEncryptionTODO())
if err != nil { if err != nil {
return nil, nil, nil, nil, err return nil, nil, nil, nil, err
} }

View File

@ -14,6 +14,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/plans/planfile" "github.com/opentofu/opentofu/internal/plans/planfile"
"github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/states"
@ -202,11 +203,10 @@ func (b *binary) StateFromFile(filename string) (*states.State, error) {
// Plan is a helper for easily reading a plan file from the working directory. // Plan is a helper for easily reading a plan file from the working directory.
func (b *binary) Plan(path string) (*plans.Plan, error) { func (b *binary) Plan(path string) (*plans.Plan, error) {
path = b.Path(path) path = b.Path(path)
pr, err := planfile.Open(path) pr, err := planfile.Open(path, encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer pr.Close()
plan, err := pr.ReadPlan() plan, err := pr.ReadPlan()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -67,3 +67,21 @@ func (p planEncryption) DecryptPlan(data []byte) ([]byte, error) {
return nil return nil
}) })
} }
func PlanEncryptionDisabled() PlanEncryption {
return &planDisabled{}
}
type planDisabled struct{}
func (s *planDisabled) EncryptPlan(plainPlan []byte) ([]byte, error) {
return plainPlan, nil
}
func (s *planDisabled) DecryptPlan(encryptedPlan []byte) ([]byte, error) {
return encryptedPlan, nil
}
// TODO REMOVEME once plan encryption is fully integrated into the codebase
func PlanEncryptionTODO() PlanEncryption {
return &planDisabled{}
}

View File

@ -15,6 +15,7 @@ import (
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs/configload" "github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/depsfile" "github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/getproviders" "github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/states"
@ -98,12 +99,12 @@ func TestRoundtrip(t *testing.T) {
StateFile: stateFileIn, StateFile: stateFileIn,
Plan: planIn, Plan: planIn,
DependencyLocks: locksIn, DependencyLocks: locksIn,
}) }, encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
t.Fatalf("failed to create plan file: %s", err) t.Fatalf("failed to create plan file: %s", err)
} }
wpf, err := OpenWrapped(planFn) wpf, err := OpenWrapped(planFn, encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
t.Fatalf("failed to open plan file for reading: %s", err) t.Fatalf("failed to open plan file for reading: %s", err)
} }
@ -181,14 +182,14 @@ func TestRoundtrip(t *testing.T) {
func TestWrappedError(t *testing.T) { func TestWrappedError(t *testing.T) {
// Open something that isn't a cloud or local planfile: should error // Open something that isn't a cloud or local planfile: should error
wrongFile := "not a valid zip file" wrongFile := "not a valid zip file"
_, err := OpenWrapped(filepath.Join("testdata", "test-config", "root.tf")) _, err := OpenWrapped(filepath.Join("testdata", "test-config", "root.tf"), encryption.PlanEncryptionDisabled())
if !strings.Contains(err.Error(), wrongFile) { if !strings.Contains(err.Error(), wrongFile) {
t.Fatalf("expected %q, got %q", wrongFile, err) t.Fatalf("expected %q, got %q", wrongFile, err)
} }
// Open something that doesn't exist: should error // Open something that doesn't exist: should error
missingFile := "no such file or directory" missingFile := "no such file or directory"
_, err = OpenWrapped(filepath.Join("testdata", "absent.tfplan")) _, err = OpenWrapped(filepath.Join("testdata", "absent.tfplan"), encryption.PlanEncryptionDisabled())
if !strings.Contains(err.Error(), missingFile) { if !strings.Contains(err.Error(), missingFile) {
t.Fatalf("expected %q, got %q", missingFile, err) t.Fatalf("expected %q, got %q", missingFile, err)
} }
@ -196,7 +197,7 @@ func TestWrappedError(t *testing.T) {
func TestWrappedCloud(t *testing.T) { func TestWrappedCloud(t *testing.T) {
// Loading valid cloud plan results in a wrapped cloud plan // Loading valid cloud plan results in a wrapped cloud plan
wpf, err := OpenWrapped(filepath.Join("testdata", "cloudplan.json")) wpf, err := OpenWrapped(filepath.Join("testdata", "cloudplan.json"), encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
t.Fatalf("failed to open valid cloud plan: %s", err) t.Fatalf("failed to open valid cloud plan: %s", err)
} }

View File

@ -15,6 +15,7 @@ import (
"github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configload" "github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/depsfile" "github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states/statefile" "github.com/opentofu/opentofu/internal/states/statefile"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
@ -50,15 +51,25 @@ func (e *ErrUnusableLocalPlan) Unwrap() error {
// be used to access the individual portions of the file for further // be used to access the individual portions of the file for further
// processing. // processing.
type Reader struct { type Reader struct {
zip *zip.ReadCloser zip *zip.Reader
} }
// Open creates a Reader for the file at the given filename, or returns an error // Open creates a Reader for the file at the given filename, or returns an error
// if the file doesn't seem to be a planfile. NOTE: Most commands that accept a // if the file doesn't seem to be a planfile. NOTE: Most commands that accept a
// plan file should use OpenWrapped instead, so they can support both local and // plan file should use OpenWrapped instead, so they can support both local and
// cloud plan files. // cloud plan files.
func Open(filename string) (*Reader, error) { func Open(filename string, enc encryption.PlanEncryption) (*Reader, error) {
r, err := zip.OpenReader(filename) raw, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
decrypted, diags := enc.DecryptPlan(raw)
if diags != nil {
return nil, diags
}
r, err := zip.NewReader(bytes.NewReader(decrypted), int64(len(decrypted)))
if err != nil { if err != nil {
// To give a better error message, we'll sniff to see if this looks // To give a better error message, we'll sniff to see if this looks
// like our old plan format from versions prior to 0.12. // like our old plan format from versions prior to 0.12.
@ -115,7 +126,6 @@ func (r *Reader) ReadPlan() (*plans.Plan, error) {
if err != nil { if err != nil {
return nil, errUnusable(fmt.Errorf("failed to retrieve plan from plan file: %w", err)) return nil, errUnusable(fmt.Errorf("failed to retrieve plan from plan file: %w", err))
} }
defer pr.Close()
// There's a slight mismatch in how plans.Plan is modeled vs. how // There's a slight mismatch in how plans.Plan is modeled vs. how
// the underlying plan file format works, because the "tfplan" embedded // the underlying plan file format works, because the "tfplan" embedded
@ -190,7 +200,7 @@ func (r *Reader) ReadPrevStateFile() (*statefile.File, error) {
// This is a lower-level alternative to ReadConfig that just extracts the // This is a lower-level alternative to ReadConfig that just extracts the
// source files, without attempting to parse them. // source files, without attempting to parse them.
func (r *Reader) ReadConfigSnapshot() (*configload.Snapshot, error) { func (r *Reader) ReadConfigSnapshot() (*configload.Snapshot, error) {
return readConfigSnapshot(&r.zip.Reader) return readConfigSnapshot(r.zip)
} }
// ReadConfig reads the configuration embedded in the plan file. // ReadConfig reads the configuration embedded in the plan file.
@ -262,8 +272,3 @@ func (r *Reader) ReadDependencyLocks() (*depsfile.Locks, tfdiags.Diagnostics) {
)) ))
return nil, diags return nil, diags
} }
// Close closes the file, after which no other operations may be performed.
func (r *Reader) Close() error {
return r.zip.Close()
}

View File

@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"github.com/opentofu/opentofu/internal/cloud/cloudplan" "github.com/opentofu/opentofu/internal/cloud/cloudplan"
"github.com/opentofu/opentofu/internal/encryption"
) )
// WrappedPlanFile is a sum type that represents a saved plan, loaded from a // WrappedPlanFile is a sum type that represents a saved plan, loaded from a
@ -76,9 +77,9 @@ func NewWrappedCloud(c *cloudplan.SavedPlanBookmark) *WrappedPlanFile {
// returns an error if the file doesn't seem to be a plan file of either kind. // returns an error if the file doesn't seem to be a plan file of either kind.
// Most consumers should use this and switch behaviors based on the kind of plan // Most consumers should use this and switch behaviors based on the kind of plan
// they expected, rather than directly using Open. // they expected, rather than directly using Open.
func OpenWrapped(filename string) (*WrappedPlanFile, error) { func OpenWrapped(filename string, enc encryption.PlanEncryption) (*WrappedPlanFile, error) {
// First, try to load it as a local planfile. // First, try to load it as a local planfile.
local, localErr := Open(filename) local, localErr := Open(filename, enc)
if localErr == nil { if localErr == nil {
return &WrappedPlanFile{local: local}, nil return &WrappedPlanFile{local: local}, nil
} }

View File

@ -7,12 +7,14 @@ package planfile
import ( import (
"archive/zip" "archive/zip"
"bytes"
"fmt" "fmt"
"os" "os"
"time" "time"
"github.com/opentofu/opentofu/internal/configs/configload" "github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/depsfile" "github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states/statefile" "github.com/opentofu/opentofu/internal/states/statefile"
) )
@ -53,15 +55,9 @@ type CreateArgs struct {
// state file in addition to the plan itself, so that OpenTofu can detect // state file in addition to the plan itself, so that OpenTofu can detect
// if the world has changed since the plan was created and thus refuse to // if the world has changed since the plan was created and thus refuse to
// apply it. // apply it.
func Create(filename string, args CreateArgs) error { func Create(filename string, args CreateArgs, enc encryption.PlanEncryption) error {
f, err := os.Create(filename) buff := bytes.NewBuffer(make([]byte, 0))
if err != nil { zw := zip.NewWriter(buff)
return err
}
defer f.Close()
zw := zip.NewWriter(f)
defer zw.Close()
// tfplan file // tfplan file
{ {
@ -140,5 +136,12 @@ func Create(filename string, args CreateArgs) error {
} }
} }
return nil // Finish zip file
zw.Close()
// Encrypt payload
encrypted, err := enc.EncryptPlan(buff.Bytes())
if err != nil {
return err
}
return os.WriteFile(filename, encrypted, 0644)
} }

View File

@ -22,6 +22,7 @@ import (
"github.com/opentofu/opentofu/internal/configs/configload" "github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/configs/hcl2shim" "github.com/opentofu/opentofu/internal/configs/hcl2shim"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/plans/planfile" "github.com/opentofu/opentofu/internal/plans/planfile"
"github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/providers"
@ -716,12 +717,12 @@ func contextOptsForPlanViaFile(t *testing.T, configSnap *configload.Snapshot, pl
PreviousRunStateFile: prevStateFile, PreviousRunStateFile: prevStateFile,
StateFile: stateFile, StateFile: stateFile,
Plan: plan, Plan: plan,
}) }, encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
pr, err := planfile.Open(filename) pr, err := planfile.Open(filename, encryption.PlanEncryptionDisabled())
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }