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/configs/configload"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/initwd"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/plans/planfile"
@ -97,7 +98,7 @@ func TestLocalRun_cloudPlan(t *testing.T) {
planPath := "./testdata/plan-bookmark/bookmark.json"
planFile, err := planfile.OpenWrapped(planPath)
planFile, err := planfile.OpenWrapped(planPath, encryption.PlanEncryptionDisabled())
if err != nil {
t.Fatalf("unexpected error reading planfile: %s", err)
}
@ -180,10 +181,10 @@ func TestLocalRun_stalePlan(t *testing.T) {
StateFile: stateFile,
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)
}
planFile, err := planfile.OpenWrapped(planPath)
planFile, err := planfile.OpenWrapped(planPath, encryption.PlanEncryptionDisabled())
if err != nil {
t.Fatalf("unexpected error reading planfile: %s", err)
}

View File

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

View File

@ -21,6 +21,7 @@ import (
"github.com/opentofu/opentofu/internal/command/views"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/initwd"
"github.com/opentofu/opentofu/internal/plans"
"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 {
t.Helper()
p, err := planfile.Open(path)
p, err := planfile.Open(path, encryption.PlanEncryptionDisabled())
if err != nil {
t.Fatalf("err: %s", err)
}
defer p.Close()
plan, err := p.ReadPlan()
if err != nil {

View File

@ -40,6 +40,7 @@ import (
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/copy"
"github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/initwd"
legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
@ -228,7 +229,7 @@ func testPlanFileMatchState(t *testing.T, configSnap *configload.Snapshot, state
StateFile: stateFile,
Plan: plan,
DependencyLocks: depsfile.NewLocks(),
})
}, encryption.PlanEncryptionDisabled())
if err != nil {
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 {
t.Helper()
f, err := planfile.Open(path)
f, err := planfile.Open(path, encryption.PlanEncryptionDisabled())
if err != nil {
t.Fatalf("error opening plan file %q: %s", path, err)
}
defer f.Close()
p, err := f.ReadPlan()
if err != nil {

View File

@ -9,6 +9,7 @@ import (
"os"
"strconv"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans/planfile"
)
@ -48,5 +49,5 @@ func (m *Meta) PlanFile(path string) (*planfile.WrappedPlanFile, error) {
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/views"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/plans/planfile"
"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 config *configs.Config
pf, err := planfile.OpenWrapped(path)
pf, err := planfile.OpenWrapped(path, encryption.PlanEncryptionTODO())
if err != nil {
return nil, nil, nil, nil, err
}

View File

@ -14,6 +14,7 @@ import (
"path/filepath"
"testing"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/plans/planfile"
"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.
func (b *binary) Plan(path string) (*plans.Plan, error) {
path = b.Path(path)
pr, err := planfile.Open(path)
pr, err := planfile.Open(path, encryption.PlanEncryptionDisabled())
if err != nil {
return nil, err
}
defer pr.Close()
plan, err := pr.ReadPlan()
if err != nil {
return nil, err

View File

@ -67,3 +67,21 @@ func (p planEncryption) DecryptPlan(data []byte) ([]byte, error) {
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/configs/configload"
"github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states"
@ -98,12 +99,12 @@ func TestRoundtrip(t *testing.T) {
StateFile: stateFileIn,
Plan: planIn,
DependencyLocks: locksIn,
})
}, encryption.PlanEncryptionDisabled())
if err != nil {
t.Fatalf("failed to create plan file: %s", err)
}
wpf, err := OpenWrapped(planFn)
wpf, err := OpenWrapped(planFn, encryption.PlanEncryptionDisabled())
if err != nil {
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) {
// Open something that isn't a cloud or local planfile: should error
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) {
t.Fatalf("expected %q, got %q", wrongFile, err)
}
// Open something that doesn't exist: should error
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) {
t.Fatalf("expected %q, got %q", missingFile, err)
}
@ -196,7 +197,7 @@ func TestWrappedError(t *testing.T) {
func TestWrappedCloud(t *testing.T) {
// 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 {
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/configload"
"github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states/statefile"
"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
// processing.
type Reader struct {
zip *zip.ReadCloser
zip *zip.Reader
}
// 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
// plan file should use OpenWrapped instead, so they can support both local and
// cloud plan files.
func Open(filename string) (*Reader, error) {
r, err := zip.OpenReader(filename)
func Open(filename string, enc encryption.PlanEncryption) (*Reader, error) {
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 {
// 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.
@ -115,7 +126,6 @@ func (r *Reader) ReadPlan() (*plans.Plan, error) {
if err != nil {
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
// 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
// source files, without attempting to parse them.
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.
@ -262,8 +272,3 @@ func (r *Reader) ReadDependencyLocks() (*depsfile.Locks, tfdiags.Diagnostics) {
))
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"
"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
@ -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.
// Most consumers should use this and switch behaviors based on the kind of plan
// 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.
local, localErr := Open(filename)
local, localErr := Open(filename, enc)
if localErr == nil {
return &WrappedPlanFile{local: local}, nil
}

View File

@ -7,12 +7,14 @@ package planfile
import (
"archive/zip"
"bytes"
"fmt"
"os"
"time"
"github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/plans"
"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
// if the world has changed since the plan was created and thus refuse to
// apply it.
func Create(filename string, args CreateArgs) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
zw := zip.NewWriter(f)
defer zw.Close()
func Create(filename string, args CreateArgs, enc encryption.PlanEncryption) error {
buff := bytes.NewBuffer(make([]byte, 0))
zw := zip.NewWriter(buff)
// 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/configschema"
"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/planfile"
"github.com/opentofu/opentofu/internal/providers"
@ -716,12 +717,12 @@ func contextOptsForPlanViaFile(t *testing.T, configSnap *configload.Snapshot, pl
PreviousRunStateFile: prevStateFile,
StateFile: stateFile,
Plan: plan,
})
}, encryption.PlanEncryptionDisabled())
if err != nil {
return nil, nil, nil, err
}
pr, err := planfile.Open(filename)
pr, err := planfile.Open(filename, encryption.PlanEncryptionDisabled())
if err != nil {
return nil, nil, nil, err
}