diff --git a/cmd/secrets/common/handler.go b/cmd/secrets/common/handler.go index 7908a322..0d916c0d 100644 --- a/cmd/secrets/common/handler.go +++ b/cmd/secrets/common/handler.go @@ -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 diff --git a/cmd/secrets/common/handler_test.go b/cmd/secrets/common/handler_test.go index bbc05c49..6876034b 100644 --- a/cmd/secrets/common/handler_test.go +++ b/cmd/secrets/common/handler_test.go @@ -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) diff --git a/cmd/secrets/common/test_helpers.go b/cmd/secrets/common/test_helpers.go index cf585631..648f6df4 100644 --- a/cmd/secrets/common/test_helpers.go +++ b/cmd/secrets/common/test_helpers.go @@ -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) { @@ -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 } diff --git a/cmd/secrets/delete/delete.go b/cmd/secrets/delete/delete.go index 25b753c8..334db82b 100644 --- a/cmd/secrets/delete/delete.go +++ b/cmd/secrets/delete/delete.go @@ -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{ diff --git a/cmd/secrets/list/list.go b/cmd/secrets/list/list.go index 2967142d..d8e696fd 100644 --- a/cmd/secrets/list/list.go +++ b/cmd/secrets/list/list.go @@ -8,7 +8,6 @@ import ( "net/http" "os" "path/filepath" - "strings" "time" ethcommon "github.com/ethereum/go-ethereum/common" @@ -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() diff --git a/internal/authvalidation/validator.go b/internal/authvalidation/validator.go index 991734f1..ed79267f 100644 --- a/internal/authvalidation/validator.go +++ b/internal/authvalidation/validator.go @@ -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 } diff --git a/internal/credentials/credentials.go b/internal/credentials/credentials.go index 257bc532..3e7f3533 100644 --- a/internal/credentials/credentials.go +++ b/internal/credentials/credentials.go @@ -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 } diff --git a/internal/environments/environments.go b/internal/environments/environments.go index 817e628d..ef652ea9 100644 --- a/internal/environments/environments.go +++ b/internal/environments/environments.go @@ -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" ) @@ -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 @@ -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 } diff --git a/internal/environments/environments.yaml b/internal/environments/environments.yaml index d789bf29..c88c0103 100644 --- a/internal/environments/environments.yaml +++ b/internal/environments/environments.yaml @@ -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 @@ -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 @@ -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"