Skip to content
Merged
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
24 changes: 23 additions & 1 deletion cmd/secrets/common/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,31 @@ func (h *Handler) fetchVaultMasterPublicKeyHex() (string, error) {
return rpcResp.Result.PublicKey, nil
}

// ResolveEffectiveOwner returns the owner string to use for vault secret identifiers.
// When SecretsOrgOwned is enabled, the org ID (from auth validation) is used;
// otherwise, the workflow owner address is used.
func (h *Handler) ResolveEffectiveOwner() (string, error) {
if h.EnvironmentSet != nil && h.EnvironmentSet.SecretsOrgOwned {
if h.Credentials == nil || h.Credentials.OrgID == "" {
return "", fmt.Errorf("org ID required when CRE_CLI_SECRETS_ORG_OWNED is enabled; ensure auth validation succeeds")
}
return h.Credentials.OrgID, nil
}
return h.OwnerAddress, nil
}

// EncryptSecrets takes the raw secrets and encrypts them, returning pointers.
// Owner-key flow: TDH2 label is the workflow owner address left-padded to 32 bytes; SecretIdentifier.Owner is the same hex address string.
// When SecretsOrgOwned is enabled, uses SHA256(orgID) as the TDH2 label and orgID as the owner.
// Otherwise, uses the workflow owner address left-padded to 32 bytes as the TDH2 label.
func (h *Handler) EncryptSecrets(rawSecrets UpsertSecretsInputs) ([]*vault.EncryptedSecret, error) {
if h.EnvironmentSet != nil && h.EnvironmentSet.SecretsOrgOwned {
owner, err := h.ResolveEffectiveOwner()
if err != nil {
return nil, err
}
return h.EncryptSecretsForBrowserOrg(rawSecrets, owner)
}

pubKeyHex, err := h.fetchVaultMasterPublicKeyHex()
if err != nil {
return nil, err
Expand Down
80 changes: 80 additions & 0 deletions cmd/secrets/common/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,86 @@ func TestEncryptSecrets(t *testing.T) {
})
}

func TestResolveEffectiveOwner(t *testing.T) {
t.Run("returns owner address when SecretsOrgOwned is false", func(t *testing.T) {
h, _, _ := newMockHandler(t)
h.OwnerAddress = "0xabc"
h.EnvironmentSet.SecretsOrgOwned = false

owner, err := h.ResolveEffectiveOwner()
require.NoError(t, err)
require.Equal(t, "0xabc", owner)
})

t.Run("returns org ID when SecretsOrgOwned is true and org ID is set", func(t *testing.T) {
h, _, _ := newMockHandler(t)
h.OwnerAddress = "0xabc"
h.EnvironmentSet.SecretsOrgOwned = true
h.Credentials.OrgID = "org-123"

owner, err := h.ResolveEffectiveOwner()
require.NoError(t, err)
require.Equal(t, "org-123", owner)
})

t.Run("errors when SecretsOrgOwned is true but org ID is empty", func(t *testing.T) {
h, _, _ := newMockHandler(t)
h.OwnerAddress = "0xabc"
h.EnvironmentSet.SecretsOrgOwned = true
h.Credentials.OrgID = ""

_, err := h.ResolveEffectiveOwner()
require.Error(t, err)
require.Contains(t, err.Error(), "org ID required")
})
}

func TestEncryptSecrets_OrgOwned(t *testing.T) {
mockGw := &mockGatewayClient{
post: func(body []byte) ([]byte, int, error) {
var req jsonrpc2.Request[vaultcommon.GetPublicKeyRequest]
_ = json.Unmarshal(body, &req)
resp := jsonrpc2.Response[vaultcommon.GetPublicKeyResponse]{
Version: jsonrpc2.JsonRpcVersion,
ID: req.ID,
Method: vaulttypes.MethodPublicKeyGet,
Result: &vaultcommon.GetPublicKeyResponse{PublicKey: vaultPublicKeyHex},
}
b, _ := json.Marshal(resp)
return b, http.StatusOK, nil
},
}

raw := UpsertSecretsInputs{
{ID: "secret-1", Value: "val1", Namespace: "main"},
}

t.Run("uses orgID as owner when SecretsOrgOwned is true", func(t *testing.T) {
h, _, _ := newMockHandler(t)
h.Gw = mockGw
h.EnvironmentSet.SecretsOrgOwned = true
h.Credentials.OrgID = "org-456"

enc, err := h.EncryptSecrets(raw)
require.NoError(t, err)
require.Len(t, enc, 1)
require.Equal(t, "org-456", enc[0].Id.Owner)
require.Equal(t, "secret-1", enc[0].Id.Key)
})

t.Run("uses address as owner when SecretsOrgOwned is false", func(t *testing.T) {
h, _, _ := newMockHandler(t)
h.Gw = mockGw
h.OwnerAddress = "0xabc"
h.EnvironmentSet.SecretsOrgOwned = false

enc, err := h.EncryptSecrets(raw)
require.NoError(t, err)
require.Len(t, enc, 1)
require.Equal(t, "0xabc", enc[0].Id.Owner)
})
}

func TestPackAllowlistRequestTxData_Success_With0x(t *testing.T) {
h, _, _ := newMockHandler(t)

Expand Down
12 changes: 8 additions & 4 deletions cmd/secrets/common/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/test-go/testify/mock"

"github.com/smartcontractkit/cre-cli/cmd/client"
"github.com/smartcontractkit/cre-cli/internal/credentials"
"github.com/smartcontractkit/cre-cli/internal/environments"
)

func newMockHandler(t *testing.T) (*Handler, *MockClientFactory, *ecdsa.PrivateKey) {
Expand All @@ -21,10 +23,12 @@ func newMockHandler(t *testing.T) (*Handler, *MockClientFactory, *ecdsa.PrivateK
t.Fatalf("failed to generate private key: %v", err)
}
h := &Handler{
Log: &logger,
ClientFactory: mockClientFactory,
PrivateKey: privateKey,
OwnerAddress: "0xabc",
Log: &logger,
ClientFactory: mockClientFactory,
PrivateKey: privateKey,
OwnerAddress: "0xabc",
EnvironmentSet: &environments.EnvironmentSet{},
Credentials: &credentials.Credentials{},
}
return h, mockClientFactory, privateKey
}
Expand Down
13 changes: 7 additions & 6 deletions cmd/secrets/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,15 @@ func Execute(h *common.Handler, inputs DeleteSecretsInputs, duration time.Durati
}
spinner.Stop()

// Validate and canonicalize owner address
owner := strings.TrimSpace(h.OwnerAddress)
if !ethcommon.IsHexAddress(owner) {
return fmt.Errorf("invalid owner address: %q", h.OwnerAddress)
owner, err := h.ResolveEffectiveOwner()
if err != nil {
return err
}
// When not using org-owned secrets, canonicalize the address
if ethcommon.IsHexAddress(owner) {
owner = ethcommon.HexToAddress(owner).Hex()
}
owner = ethcommon.HexToAddress(owner).Hex() // checksummed string

// Prepare the list of SecretIdentifiers to be deleted.
ptrIDs := make([]*vault.SecretIdentifier, len(inputs))
for i, item := range inputs {
ptrIDs[i] = &vault.SecretIdentifier{
Expand Down
12 changes: 6 additions & 6 deletions cmd/secrets/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"time"

ethcommon "github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -98,12 +97,13 @@ func Execute(h *common.Handler, namespace string, duration time.Duration, secret
namespace = "main"
}

// Validate and canonicalize owner address (checksummed)
owner := strings.TrimSpace(h.OwnerAddress)
if !ethcommon.IsHexAddress(owner) {
return fmt.Errorf("invalid owner address: %q", h.OwnerAddress)
owner, err := h.ResolveEffectiveOwner()
if err != nil {
return err
}
if ethcommon.IsHexAddress(owner) {
owner = ethcommon.HexToAddress(owner).Hex()
}
owner = ethcommon.HexToAddress(owner).Hex()

// Fresh request ID
requestID := uuid.New().String()
Expand Down
4 changes: 4 additions & 0 deletions internal/authvalidation/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,9 @@ func (v *Validator) ValidateCredentials(validationCtx context.Context, creds *cr
return fmt.Errorf("authentication validation failed: %w", err)
}

if orgID := respEnvelope.GetOrganization.OrganizationID; orgID != "" {
creds.OrgID = orgID
}

return nil
}
1 change: 1 addition & 0 deletions internal/credentials/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Credentials struct {
APIKey string `yaml:"api_key"` // #nosec G117 -- credential stored in secure config file
AuthType string `yaml:"auth_type"`
IsValidated bool `yaml:"-"`
OrgID string `yaml:"-"`
log *zerolog.Logger
}

Expand Down
6 changes: 6 additions & 0 deletions internal/environments/environments.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
EnvVarWorkflowRegistryChainName = "CRE_CLI_WORKFLOW_REGISTRY_CHAIN_NAME"
EnvVarWorkflowRegistryChainExplorerURL = "CRE_CLI_WORKFLOW_REGISTRY_CHAIN_EXPLORER_URL"
EnvVarDonFamily = "CRE_CLI_DON_FAMILY"
EnvVarSecretsOrgOwned = "CRE_CLI_SECRETS_ORG_OWNED"

DefaultEnv = "PRODUCTION"
)
Expand All @@ -42,6 +43,7 @@ type EnvironmentSet struct {
WorkflowRegistryChainName string `yaml:"CRE_CLI_WORKFLOW_REGISTRY_CHAIN_NAME"`
WorkflowRegistryChainExplorerURL string `yaml:"CRE_CLI_WORKFLOW_REGISTRY_CHAIN_EXPLORER_URL"`
DonFamily string `yaml:"CRE_CLI_DON_FAMILY"`
SecretsOrgOwned bool `yaml:"CRE_CLI_SECRETS_ORG_OWNED"`
}

// RequiresVPN returns true if the GraphQL endpoint is on a private network
Expand Down Expand Up @@ -113,6 +115,10 @@ func NewEnvironmentSet(ff *fileFormat, envName string) *EnvironmentSet {
set.DonFamily = v
}

if v := os.Getenv(EnvVarSecretsOrgOwned); v != "" {
set.SecretsOrgOwned = strings.EqualFold(v, "true")
}

return &set
}

Expand Down
3 changes: 3 additions & 0 deletions internal/environments/environments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ ENVIRONMENTS:
CRE_CLI_GRAPHQL_URL: https://graphql-cre-dev.tailf8f749.ts.net/graphql
CRE_VAULT_DON_GATEWAY_URL: https://cre-gateway-one-zone-a.main.stage.cldev.sh/
CRE_CLI_DON_FAMILY: "zone-a"
CRE_CLI_SECRETS_ORG_OWNED: false

CRE_CLI_WORKFLOW_REGISTRY_ADDRESS: "0x7e69E853D9Ce50C2562a69823c80E01360019Cef"
CRE_CLI_WORKFLOW_REGISTRY_CHAIN_NAME: "ethereum-testnet-sepolia" # eth-sepolia
Expand All @@ -18,6 +19,7 @@ ENVIRONMENTS:
CRE_CLI_GRAPHQL_URL: https://graphql-cre-stage.tailf8f749.ts.net/graphql
CRE_VAULT_DON_GATEWAY_URL: https://cre-gateway-one-zone-a.main.stage.cldev.sh/
CRE_CLI_DON_FAMILY: "zone-a"
CRE_CLI_SECRETS_ORG_OWNED: false

CRE_CLI_WORKFLOW_REGISTRY_ADDRESS: "0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135"
CRE_CLI_WORKFLOW_REGISTRY_CHAIN_NAME: "ethereum-testnet-sepolia" # eth-sepolia
Expand All @@ -30,6 +32,7 @@ ENVIRONMENTS:
CRE_CLI_GRAPHQL_URL: https://api.cre.chain.link/graphql
CRE_VAULT_DON_GATEWAY_URL: https://01.gateway.zone-a.cre.chain.link
CRE_CLI_DON_FAMILY: "zone-a"
CRE_CLI_SECRETS_ORG_OWNED: false

CRE_CLI_WORKFLOW_REGISTRY_ADDRESS: "0x4Ac54353FA4Fa961AfcC5ec4B118596d3305E7e5"
CRE_CLI_WORKFLOW_REGISTRY_CHAIN_NAME: "ethereum-mainnet"
Expand Down
Loading