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

Fixes #2022: Running external commands as a key provider #2023

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions internal/encryption/default_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package encryption

import (
"github.com/opentofu/opentofu/internal/encryption/keyprovider/aws_kms"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/externalcommand"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/gcp_kms"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/openbao"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/pbkdf2"
Expand All @@ -30,6 +31,9 @@ func init() {
if err := DefaultRegistry.RegisterKeyProvider(openbao.New()); err != nil {
panic(err)
}
if err := DefaultRegistry.RegisterKeyProvider(externalcommand.New()); err != nil {
panic(err)
}
if err := DefaultRegistry.RegisterMethod(aesgcm.New()); err != nil {
panic(err)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package externalcommand

import (
"fmt"
"testing"

"github.com/opentofu/opentofu/internal/encryption/keyprovider"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/compliancetest"
)

func TestCompliance(t *testing.T) {
validConfig := &Config{
Command: []string{"testprovider"},
}
compliancetest.ComplianceTest(
t,
compliancetest.TestConfiguration[*descriptor, *Config, *Metadata, *keyProvider]{
Descriptor: New().(*descriptor),
HCLParseTestCases: map[string]compliancetest.HCLParseTestCase[*Config, *keyProvider]{
"empty": {
HCL: `key_provider "externalcommand" "foo" {
}`,
ValidHCL: false,
ValidBuild: false,
Validate: nil,
},
"basic": {
HCL: `key_provider "externalcommand" "foo" {
command = ["keyprovider"]
}`,
ValidHCL: true,
ValidBuild: true,
Validate: func(config *Config, keyProvider *keyProvider) error {

Check failure on line 38 in internal/encryption/keyprovider/externalcommand/compliance_test.go

View workflow job for this annotation

GitHub Actions / Code Consistency Checks

unused-parameter: parameter 'keyProvider' seems to be unused, consider removing or renaming it as _ (revive)
if len(config.Command) != 1 {
return fmt.Errorf("invalid command after parsing")
}
if config.Command[0] != "keyprovider" {
return fmt.Errorf("invalid command after parsing")
}
return nil
},
},
},
ConfigStructTestCases: map[string]compliancetest.ConfigStructTestCase[*Config, *keyProvider]{},
MetadataStructTestCases: map[string]compliancetest.MetadataStructTestCase[*Config, *Metadata]{
"not-present-externaldata": {
ValidConfig: validConfig,
Meta: &Metadata{
ExternalData: nil,
},
IsPresent: false,
},
"present-valid": {
ValidConfig: validConfig,
Meta: &Metadata{
ExternalData: map[string]any{},
},
IsPresent: true,
IsValid: true,
},
},
ProvideTestCase: compliancetest.ProvideTestCase[*Config, *Metadata]{
ValidConfig: &Config{
Command: []string{"testcommand"},
},
ExpectedOutput: &keyprovider.Output{
EncryptionKey: []byte{},
DecryptionKey: []byte{},
},
ValidateKeys: nil,
ValidateMetadata: func(meta *Metadata) error {
if meta.ExternalData == nil {
return fmt.Errorf("output metadata is not present")
}
return nil
},
},
},
)
}
23 changes: 23 additions & 0 deletions internal/encryption/keyprovider/externalcommand/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package externalcommand

import (
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
)

type Config struct {
Command []string `hcl:"command"`
}

func (c *Config) Build() (keyprovider.KeyProvider, keyprovider.KeyMeta, error) {
if len(c.Command) < 1 {
return nil, nil, &keyprovider.ErrInvalidConfiguration{
Message: "the command option is required",
}
}
return &keyProvider{}, &Metadata{}, nil
}
35 changes: 35 additions & 0 deletions internal/encryption/keyprovider/externalcommand/descriptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package externalcommand

import (
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
)

func New() Descriptor {
return &descriptor{}
}

type Descriptor interface {
keyprovider.Descriptor

TypedConfig() *Config
}

type descriptor struct {
}

func (f descriptor) ID() keyprovider.ID {
return "externalcommand"
}

func (f descriptor) TypedConfig() *Config {
return &Config{}
}

func (f descriptor) ConfigStruct() keyprovider.Config {
return f.TypedConfig()
}
10 changes: 10 additions & 0 deletions internal/encryption/keyprovider/externalcommand/meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package externalcommand

type Metadata struct {
ExternalData ExternalCommandMeta `hcl:"external_data"`
}
17 changes: 17 additions & 0 deletions internal/encryption/keyprovider/externalcommand/protocol.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package externalcommand

import (
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
)

type ExternalCommandMeta map[string]any

Check failure on line 12 in internal/encryption/keyprovider/externalcommand/protocol.go

View workflow job for this annotation

GitHub Actions / Code Consistency Checks

exported: type name will be used as externalcommand.ExternalCommandMeta by other packages, and that stutters; consider calling this Meta (revive)

type ExternalCommandOutput struct {

Check failure on line 14 in internal/encryption/keyprovider/externalcommand/protocol.go

View workflow job for this annotation

GitHub Actions / Code Consistency Checks

exported: type name will be used as externalcommand.ExternalCommandOutput by other packages, and that stutters; consider calling this Output (revive)
Key keyprovider.Output `json:"key"`
Meta ExternalCommandMeta `json:"meta"`
}
74 changes: 74 additions & 0 deletions internal/encryption/keyprovider/externalcommand/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package externalcommand

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os/exec"

"github.com/opentofu/opentofu/internal/encryption/keyprovider"
)

type keyProvider struct {
command []string
}

func (k keyProvider) Provide(rawMeta keyprovider.KeyMeta) (keysOutput keyprovider.Output, encryptionMeta keyprovider.KeyMeta, err error) {

Check failure on line 23 in internal/encryption/keyprovider/externalcommand/provider.go

View workflow job for this annotation

GitHub Actions / Code Consistency Checks

named return "keysOutput" with type "keyprovider.Output" found (nonamedreturns)
if rawMeta == nil {
return keyprovider.Output{}, nil, &keyprovider.ErrInvalidMetadata{Message: "bug: no metadata struct provided"}
}
inMeta, ok := rawMeta.(*Metadata)
if !ok {
return keyprovider.Output{}, nil, &keyprovider.ErrInvalidMetadata{
Message: fmt.Sprintf("bug: incorrect metadata type of %T provided", rawMeta),
}
}

input, err := json.Marshal(inMeta)

Check failure on line 34 in internal/encryption/keyprovider/externalcommand/provider.go

View workflow job for this annotation

GitHub Actions / Code Consistency Checks

the given struct should be annotated with the `json` tag (musttag)
if err != nil {
return keyprovider.Output{}, nil, &keyprovider.ErrInvalidMetadata{
Message: fmt.Sprintf("bug: cannot JSON-marshal metadata (%v)", err),
}
}

ctx := context.TODO()

stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}

cmd := exec.CommandContext(ctx, k.command[0], k.command[1:]...)

Check failure on line 46 in internal/encryption/keyprovider/externalcommand/provider.go

View workflow job for this annotation

GitHub Actions / Code Consistency Checks

G204: Subprocess launched with a potential tainted input or cmd arguments (gosec)
cmd.Stdin = bytes.NewReader(input)
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
if exitErr.ExitCode() != 0 {
return keyprovider.Output{}, nil, &keyprovider.ErrKeyProviderFailure{
Message: fmt.Sprintf("the external command exited with a non-zero exit code (%v)\n\nStdout:\n-------\n%s\n-------\nStderr:\n-------\n%s", err, stdout, stderr),
}
}
}
return keyprovider.Output{}, nil, &keyprovider.ErrKeyProviderFailure{
Message: fmt.Sprintf("the external command exited with an error (%v)\n\nStdout:\n-------\n%s\n-------\nStderr:\n-------\n%s", err, stdout, stderr),
}
}

var result *ExternalCommandOutput
decoder := json.NewDecoder(bytes.NewReader(stdout.Bytes()))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&result); err != nil {
return keyprovider.Output{}, nil, &keyprovider.ErrKeyProviderFailure{
Message: fmt.Sprintf("the external command returned an invalid JSON response (%v)\n\nStdout:\n-------\n%s\n-------\nStderr:\n-------\n%s", err, stdout, stderr),
}
}

return result.Key, result.Meta, nil
}
Loading