Skip to content
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ test-e2e:

# test-quick: run tests with 30s timeout, skipping slow/flaky e2e tests. Use -short so TestE2EInit_ConvertToCustomBuild_TS is skipped.
test-quick:
$(GOTEST) ./... -v -short -skip 'MultiCommandHappyPaths|TestPostToGateway|TestBlankWorkflowSimulation|TestWaitForBackendLinkProcessing|TestTryAutoLink|TestCheckLinkStatusViaGraphQL|Fails to run tests with invalid Go code' -timeout 30s
$(GOTEST) ./... -v -short -skip 'TestWorkflow_|TestSecrets_|TestAccount_|TestPostToGateway|TestBlankWorkflowSimulation|TestWaitForBackendLinkProcessing|TestTryAutoLink|TestCheckLinkStatusViaGraphQL|Fails to run tests with invalid Go code' -timeout 30s

clean:
$(GOCLEAN)
Expand Down
1 change: 1 addition & 0 deletions cmd/client/eth_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ func readSethConfigFromFile(configPath string) (*seth.Config, error) {
return &sethConfig, nil
}

// TODO(DEVSVCS-5178)
func getChainID(rpcURL string) (uint64, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
Expand Down
7 changes: 3 additions & 4 deletions cmd/login/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/smartcontractkit/cre-cli/internal/environments"
"github.com/smartcontractkit/cre-cli/internal/oauth"
"github.com/smartcontractkit/cre-cli/internal/tenantctx"
"github.com/smartcontractkit/cre-cli/internal/testutil/cretest"
"github.com/smartcontractkit/cre-cli/internal/ui"
)

Expand All @@ -32,8 +33,7 @@ func TestFetchTenantConfig_GQLError_ReturnsError(t *testing.T) {
}))
defer srv.Close()

tmp := t.TempDir()
t.Setenv("HOME", tmp)
cretest.IsolateConfig(t)
log := zerolog.Nop()
h := &handler{
log: &log,
Expand Down Expand Up @@ -92,8 +92,7 @@ func TestLogin_NonInteractive_ReturnsError(t *testing.T) {
}

func TestSaveCredentials_WritesYAML(t *testing.T) {
tmp := t.TempDir()
t.Setenv("HOME", tmp)
cretest.IsolateConfig(t)

tokenSet := &credentials.CreLoginTokenSet{
AccessToken: "a",
Expand Down
16 changes: 7 additions & 9 deletions cmd/logout/logout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import (
"github.com/smartcontractkit/cre-cli/internal/environments"
"github.com/smartcontractkit/cre-cli/internal/runtime"
"github.com/smartcontractkit/cre-cli/internal/testutil"
"github.com/smartcontractkit/cre-cli/internal/testutil/cretest"
)

func setupCredentialFile(t *testing.T, home string, token string) {
func setupCredentialFile(t *testing.T, token string) {
t.Helper()
dir, err := creconfig.EnsureDir()
if err != nil {
Expand All @@ -42,8 +43,7 @@ func setupCredentialFile(t *testing.T, home string, token string) {
}

func TestExecute_NoCredentialsFile(t *testing.T) {
tDir := t.TempDir()
t.Setenv("HOME", tDir)
cretest.IsolateConfig(t)

creds := credentials.Credentials{
Tokens: &credentials.CreLoginTokenSet{},
Expand All @@ -64,10 +64,9 @@ func TestExecute_NoCredentialsFile(t *testing.T) {
}

func TestExecute_SuccessRevocationAndRemoval(t *testing.T) {
tDir := t.TempDir()
t.Setenv("HOME", tDir)
cretest.IsolateConfig(t)
token := "test-refresh-token"
setupCredentialFile(t, tDir, token)
setupCredentialFile(t, token)

var received bool
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -123,10 +122,9 @@ func TestExecute_SuccessRevocationAndRemoval(t *testing.T) {
}

func TestExecute_RevocationFails_StillRemovesFile(t *testing.T) {
tDir := t.TempDir()
t.Setenv("HOME", tDir)
cretest.IsolateConfig(t)
token := "bad-refresh-token"
setupCredentialFile(t, tDir, token)
setupCredentialFile(t, token)

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
Expand Down
15 changes: 15 additions & 0 deletions cmd/secrets/common/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/google/uuid"
"github.com/machinebox/graphql"
"github.com/rs/zerolog"
"github.com/spf13/viper"
"google.golang.org/protobuf/encoding/protojson"
"gopkg.in/yaml.v2"

Expand All @@ -38,6 +39,7 @@ import (
"github.com/smartcontractkit/cre-cli/internal/ethkeys"
"github.com/smartcontractkit/cre-cli/internal/runtime"
"github.com/smartcontractkit/cre-cli/internal/settings"
"github.com/smartcontractkit/cre-cli/internal/tenantctx"
"github.com/smartcontractkit/cre-cli/internal/types"
"github.com/smartcontractkit/cre-cli/internal/ui"
"github.com/smartcontractkit/cre-cli/internal/validation"
Expand Down Expand Up @@ -67,6 +69,8 @@ type SecretsYamlConfig struct {
type Handler struct {
Log *zerolog.Logger
ClientFactory client.Factory
Viper *viper.Viper
TenantContext *tenantctx.EnvironmentContext
SecretsFilePath string
PrivateKey *ecdsa.PrivateKey
OwnerAddress string
Expand All @@ -78,6 +82,11 @@ type Handler struct {
Credentials *credentials.Credentials
Settings *settings.Settings
execCtx context.Context

vaultValidationDecided bool
skipVaultValidation bool
capRegRPCURL string
capRegChainName string
}

// NewHandler creates a new handler instance.
Expand All @@ -100,6 +109,8 @@ func NewHandler(execCtx context.Context, ctx *runtime.Context, secretsFilePath,
h := &Handler{
Log: ctx.Logger,
ClientFactory: ctx.ClientFactory,
Viper: ctx.Viper,
TenantContext: ctx.TenantContext,
SecretsFilePath: secretsFilePath,
PrivateKey: pk,
OwnerAddress: ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress,
Expand Down Expand Up @@ -427,6 +438,10 @@ func (h *Handler) Execute(
defer ZeroUpsertSecretValues(inputs)

h.execCtx = ctx
if _, err := h.EnsureVaultValidationOrConsent(ctx); err != nil {
return err
}

if IsBrowserFlow(secretsAuth) {
return h.executeBrowserUpsert(ctx, inputs, method)
}
Expand Down
87 changes: 87 additions & 0 deletions cmd/secrets/common/vault_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package common

import (
"context"
"fmt"

"github.com/smartcontractkit/cre-cli/internal/settings"
"github.com/smartcontractkit/cre-cli/internal/ui"
)

const vaultValidationSkippedWarning = "Vault gateway validation skipped; the encryption key and response signatures will not be verified independently of the gateway."

// EnsureVaultValidationOrConsent resolves CapabilitiesRegistry RPC settings and either
// enables on-chain validation (skipValidation=false) or obtains explicit consent to
// proceed without validation. The result is cached for the lifetime of the Handler so
// encrypt and response parsing in the same command only prompt once.
func (h *Handler) EnsureVaultValidationOrConsent(ctx context.Context) (skipValidation bool, err error) {
_ = ctx
if h.vaultValidationDecided {
return h.skipVaultValidation, nil
}

rpcURL, chainName, ok, err := settings.ResolveCapabilitiesRegistryRPC(h.Viper, h.TenantContext)
if err != nil {
return false, err
}

if ok {
h.capRegRPCURL = rpcURL
h.capRegChainName = chainName
h.skipVaultValidation = false
h.vaultValidationDecided = true
return false, nil
}

if h.Viper.GetBool(settings.Flags.NonInteractive.Name) && !h.Viper.GetBool(settings.Flags.SkipConfirmation.Name) {
ui.ErrorWithSuggestions(
fmt.Sprintf("Vault gateway validation requires an RPC for %s in your project settings", chainName),
[]string{"--yes"},
)
return false, fmt.Errorf("missing RPC for capabilities registry chain %q", chainName)
}

if h.Viper.GetBool(settings.Flags.SkipConfirmation.Name) {
ui.Warning(vaultValidationSkippedWarning)
h.capRegChainName = chainName
h.skipVaultValidation = true
h.vaultValidationDecided = true
return true, nil
}

prompt := fmt.Sprintf(
"Vault gateway responses cannot be validated without an RPC for %s in your project settings. Proceeding without validation means the CLI cannot verify the encryption key or DON signatures independently of the gateway. Proceed anyway?",
chainName,
)
proceed, err := ui.Confirm(prompt)
if err != nil {
return false, err
}
if !proceed {
return false, fmt.Errorf("aborted: vault gateway validation requires an RPC for %q", chainName)
}

ui.Warning(vaultValidationSkippedWarning)
h.capRegChainName = chainName
h.skipVaultValidation = true
h.vaultValidationDecided = true
return true, nil
}

// SkipVaultValidation reports whether the current command opted out of on-chain validation.
func (h *Handler) SkipVaultValidation() bool {
return h.skipVaultValidation
}

// CapabilitiesRegistryRPC returns the validated RPC URL when validation is enabled.
func (h *Handler) CapabilitiesRegistryRPC() (rpcURL string, ok bool) {
if h.skipVaultValidation || h.capRegRPCURL == "" {
return "", false
}
return h.capRegRPCURL, true
}

// CapabilitiesRegistryChainName returns the chain name for the tenant CapabilitiesRegistry.
func (h *Handler) CapabilitiesRegistryChainName() string {
return h.capRegChainName
}
118 changes: 118 additions & 0 deletions cmd/secrets/common/vault_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package common

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/cre-cli/internal/settings"
"github.com/smartcontractkit/cre-cli/internal/tenantctx"
)

func testHandlerWithCapReg(t *testing.T, v *viper.Viper, tenantCtx *tenantctx.EnvironmentContext) *Handler {
t.Helper()
h, _, _ := newMockHandler(t)
h.Viper = v
h.TenantContext = tenantCtx
return h
}

func TestEnsureVaultValidationOrConsent_RPCConfigured(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID json.RawMessage `json:"id"`
Method string `json:"method"`
}
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
w.Header().Set("Content-Type", "application/json")
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{
"jsonrpc": "2.0",
"id": req.ID,
"result": "0xaa36a7",
}))
}))
t.Cleanup(server.Close)

v := viper.New()
v.Set(settings.CreTargetEnvVar, "staging")
v.Set("staging.rpcs", []map[string]string{
{"chain-name": "ethereum-testnet-sepolia", "url": server.URL},
})

tenantCtx := &tenantctx.EnvironmentContext{
CapabilitiesRegistry: &tenantctx.OnChainContract{
ChainSelector: 16015286601757825753,
Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f",
},
}

h := testHandlerWithCapReg(t, v, tenantCtx)

skip, err := h.EnsureVaultValidationOrConsent(context.Background())
require.NoError(t, err)
require.False(t, skip)
require.False(t, h.SkipVaultValidation())

rpcURL, ok := h.CapabilitiesRegistryRPC()
require.True(t, ok)
require.Equal(t, server.URL, rpcURL)

skipCached, err := h.EnsureVaultValidationOrConsent(context.Background())
require.NoError(t, err)
require.False(t, skipCached)
}

func TestEnsureVaultValidationOrConsent_SkipConfirmationWithoutRPC(t *testing.T) {
v := viper.New()
v.Set(settings.CreTargetEnvVar, "staging")
v.Set(settings.Flags.SkipConfirmation.Name, true)

tenantCtx := &tenantctx.EnvironmentContext{
CapabilitiesRegistry: &tenantctx.OnChainContract{
ChainSelector: 16015286601757825753,
Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f",
},
}

h := testHandlerWithCapReg(t, v, tenantCtx)

skip, err := h.EnsureVaultValidationOrConsent(context.Background())
require.NoError(t, err)
require.True(t, skip)
require.True(t, h.SkipVaultValidation())
_, ok := h.CapabilitiesRegistryRPC()
require.False(t, ok)
}

func TestEnsureVaultValidationOrConsent_NonInteractiveWithoutRPC(t *testing.T) {
v := viper.New()
v.Set(settings.CreTargetEnvVar, "staging")
v.Set(settings.Flags.NonInteractive.Name, true)

tenantCtx := &tenantctx.EnvironmentContext{
CapabilitiesRegistry: &tenantctx.OnChainContract{
ChainSelector: 16015286601757825753,
Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f",
},
}

h := testHandlerWithCapReg(t, v, tenantCtx)

_, err := h.EnsureVaultValidationOrConsent(context.Background())
require.Error(t, err)
require.Contains(t, err.Error(), "missing RPC for capabilities registry chain")
}

func TestEnsureVaultValidationOrConsent_MissingCapabilitiesRegistry(t *testing.T) {
v := viper.New()
h := testHandlerWithCapReg(t, v, &tenantctx.EnvironmentContext{})

_, err := h.EnsureVaultValidationOrConsent(context.Background())
require.Error(t, err)
require.Contains(t, err.Error(), "capabilities registry is not configured")
}
4 changes: 4 additions & 0 deletions cmd/secrets/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ func New(ctx *runtime.Context) *cobra.Command {
// - MSIG step 1: build request, compute digest, write bundle, print steps
// - EOA: allowlist if needed, then POST to gateway
func Execute(ctx context.Context, h *common.Handler, inputs DeleteSecretsInputs, duration time.Duration, secretsAuth string) error {
if _, err := h.EnsureVaultValidationOrConsent(ctx); err != nil {
return err
}

if !common.IsBrowserFlow(secretsAuth) {
if err := h.EnsureDeploymentRPCForOwnerKeySecrets(); err != nil {
return err
Expand Down
5 changes: 5 additions & 0 deletions cmd/secrets/execute/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ func New(ctx *runtime.Context) *cobra.Command {
return err
}

if _, err := h.EnsureVaultValidationOrConsent(cmd.Context()); err != nil {
return err
}

ownerAddr := ethcommon.HexToAddress(h.OwnerAddress)

allowlisted, err := h.Wrc.IsRequestAllowlisted(cmd.Context(), ownerAddr, digest)
Expand All @@ -94,6 +98,7 @@ func New(ctx *runtime.Context) *cobra.Command {
}

settings.AddTxnTypeFlags(cmd)
settings.AddSkipConfirmation(cmd)

return cmd
}
4 changes: 4 additions & 0 deletions cmd/secrets/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ func New(ctx *runtime.Context) *cobra.Command {

// Execute performs: build request → (MSIG step 1 bundle OR EOA allowlist+post) → parse.
func Execute(ctx context.Context, h *common.Handler, namespace string, duration time.Duration, secretsAuth string) error {
if _, err := h.EnsureVaultValidationOrConsent(ctx); err != nil {
return err
}

if !common.IsBrowserFlow(secretsAuth) {
if err := h.EnsureDeploymentRPCForOwnerKeySecrets(); err != nil {
return err
Expand Down
Loading
Loading