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
17 changes: 9 additions & 8 deletions cmd/secrets/common/browser_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,15 @@ func (h *Handler) executeBrowserUpsert(ctx context.Context, inputs UpsertSecrets
if h.Credentials.AuthType == credentials.AuthTypeApiKey {
return fmt.Errorf("this sign-in flow requires an interactive login; API keys are not supported")
}
orgID := h.Credentials.OrgID
if orgID == "" {
return fmt.Errorf("organization information is missing from your session; sign in again or use --secrets-auth=onchain")

owner, err := h.ResolveVaultIdentifierOwnerForAuth(SecretsAuthBrowser)
if err != nil {
return err
}

ui.Dim("Using your account to authorize vault access for your organization...")

encSecrets, err := h.EncryptSecretsForBrowserOrg(inputs, orgID)
encSecrets, err := h.EncryptSecrets(inputs, owner)
if err != nil {
return fmt.Errorf("failed to encrypt secrets: %w", err)
}
Expand Down Expand Up @@ -127,12 +128,12 @@ func (h *Handler) executeBrowserUpsert(ctx context.Context, inputs UpsertSecrets
return fmt.Errorf("unsupported method %q (expected %q or %q)", method, vaulttypes.MethodSecretsCreate, vaulttypes.MethodSecretsUpdate)
}

return h.ExecuteBrowserVaultAuthorization(ctx, method, digest, requestBody)
return h.ExecuteBrowserVaultAuthorization(ctx, method, digest, requestBody, owner)
}

// ExecuteBrowserVaultAuthorization completes platform OAuth for a vault JSON-RPC digest (create/update/delete/list),
// then POSTs the same request body to the gateway with the vault JWT in the Authorization header.
func (h *Handler) ExecuteBrowserVaultAuthorization(ctx context.Context, method string, digest [32]byte, requestBody []byte) error {
func (h *Handler) ExecuteBrowserVaultAuthorization(ctx context.Context, method string, digest [32]byte, requestBody []byte, workflowOwner string) error {
if h.Credentials.AuthType == credentials.AuthTypeApiKey {
return fmt.Errorf("this sign-in flow requires an interactive login; API keys are not supported")
}
Expand All @@ -158,8 +159,8 @@ func (h *Handler) ExecuteBrowserVaultAuthorization(ctx context.Context, method s
"requestDigest": digestHexString(digest),
"permission": perm,
}
// Optional: bind authorization to workflow owner when configured (omit if unset).
if w := strings.TrimSpace(h.OwnerAddress); w != "" {
// Bind authorization to the JWT-derived workflow owner so digests align with gateway validation.
if w := strings.TrimSpace(workflowOwner); w != "" {
reqVars["workflowOwnerAddress"] = w
}
gqlReq.Var("request", reqVars)
Expand Down
117 changes: 47 additions & 70 deletions cmd/secrets/common/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package common
import (
"context"
"crypto/ecdsa"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -35,6 +34,7 @@ import (
"github.com/smartcontractkit/cre-cli/internal/constants"
"github.com/smartcontractkit/cre-cli/internal/credentials"
"github.com/smartcontractkit/cre-cli/internal/environments"
"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/types"
Expand All @@ -57,16 +57,17 @@ type SecretsYamlConfig struct {
}

type Handler struct {
Log *zerolog.Logger
ClientFactory client.Factory
SecretsFilePath string
PrivateKey *ecdsa.PrivateKey
OwnerAddress string
EnvironmentSet *environments.EnvironmentSet
Gw GatewayClient
Wrc *client.WorkflowRegistryV2Client
Credentials *credentials.Credentials
Settings *settings.Settings
Log *zerolog.Logger
ClientFactory client.Factory
SecretsFilePath string
PrivateKey *ecdsa.PrivateKey
OwnerAddress string
DerivedWorkflowOwner string
EnvironmentSet *environments.EnvironmentSet
Gw GatewayClient
Wrc *client.WorkflowRegistryV2Client
Credentials *credentials.Credentials
Settings *settings.Settings
}

// NewHandler creates a new handler instance.
Expand All @@ -87,14 +88,15 @@ func NewHandler(ctx *runtime.Context, secretsFilePath, secretsAuth string) (*Han
}

h := &Handler{
Log: ctx.Logger,
ClientFactory: ctx.ClientFactory,
SecretsFilePath: secretsFilePath,
PrivateKey: pk,
OwnerAddress: ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress,
EnvironmentSet: ctx.EnvironmentSet,
Credentials: ctx.Credentials,
Settings: ctx.Settings,
Log: ctx.Logger,
ClientFactory: ctx.ClientFactory,
SecretsFilePath: secretsFilePath,
PrivateKey: pk,
OwnerAddress: ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress,
DerivedWorkflowOwner: ctx.DerivedWorkflowOwner,
EnvironmentSet: ctx.EnvironmentSet,
Credentials: ctx.Credentials,
Settings: ctx.Settings,
}
h.Gw = &HTTPClient{URL: h.EnvironmentSet.GatewayURL, Client: &http.Client{Timeout: 90 * time.Second}}

Expand Down Expand Up @@ -285,74 +287,44 @@ func (h *Handler) ResolveEffectiveOwner() (string, error) {
return common.HexToAddress(h.OwnerAddress).Hex(), nil
}

// ResolveVaultIdentifierOwnerForAuth returns the owner string used in vault JSON-RPC payloads
// (SecretIdentifier.Owner and list request Owner). Browser auth always uses the signed-in
// organization ID so digests and identifiers align with JWT AuthorizedOwner() on the gateway;
// onchain auth uses ResolveEffectiveOwner() (linked workflow owner address).
// ResolveVaultIdentifierOwnerForAuth returns the owner used in vault JSON-RPC payloads
// (SecretIdentifier.Owner, list Owner, TDH2 labels). Onchain auth uses the linked EOA from
// settings; browser auth uses DerivedWorkflowOwner from runtime.Context (getCreOrganizationInfo at login).
func (h *Handler) ResolveVaultIdentifierOwnerForAuth(secretsAuth string) (string, error) {
if IsBrowserFlow(secretsAuth) {
if h.Credentials == nil {
return "", fmt.Errorf("organization information is missing from your session; sign in again or use --secrets-auth=onchain")
}
if h.Credentials.AuthType == credentials.AuthTypeApiKey {
return "", fmt.Errorf("this sign-in flow requires an interactive login; API keys are not supported")
}
if h.Credentials.OrgID == "" {
return "", fmt.Errorf("organization information is missing from your session; sign in again or use --secrets-auth=onchain")
}
return h.Credentials.OrgID, nil
if !IsBrowserFlow(secretsAuth) {
return h.ResolveEffectiveOwner()
}
return h.ResolveEffectiveOwner()
}

// EncryptSecrets takes the raw secrets and encrypts them for the owner-key (onchain) flow.
// TDH2 label is the workflow owner address left-padded to 32 bytes; SecretIdentifier.Owner is the same hex address string.
func (h *Handler) EncryptSecrets(rawSecrets UpsertSecretsInputs) ([]*vault.EncryptedSecret, error) {
pubKeyHex, err := h.fetchVaultMasterPublicKeyHex()
if err != nil {
return nil, err
if h.Credentials == nil {
return "", fmt.Errorf("organization information is missing from your session; sign in again or use --secrets-auth=onchain")
}

encryptedSecrets := make([]*vault.EncryptedSecret, 0, len(rawSecrets))
for _, item := range rawSecrets {
cipherHex, err := EncryptSecret(item.Value, pubKeyHex, h.OwnerAddress)
if err != nil {
return nil, fmt.Errorf("failed to encrypt secret (key=%s ns=%s): %w", item.ID, item.Namespace, err)
}
secID := &vault.SecretIdentifier{
Key: item.ID,
Namespace: item.Namespace,
Owner: h.OwnerAddress,
}
encryptedSecrets = append(encryptedSecrets, &vault.EncryptedSecret{
Id: secID,
EncryptedValue: cipherHex,
})
if h.Credentials.AuthType == credentials.AuthTypeApiKey {
return "", fmt.Errorf("this sign-in flow requires an interactive login; API keys are not supported")
}
return encryptedSecrets, nil
owner := strings.TrimSpace(h.DerivedWorkflowOwner)
if owner == "" {
return "", fmt.Errorf("derived workflow owner is not available; sign in again with cre login")
}
return ethkeys.FormatWorkflowOwnerAddress(owner)
}

// EncryptSecretsForBrowserOrg encrypts secrets scoped to the signed-in organization (interactive sign-in flow).
// TDH2 label is SHA256(orgID); SecretIdentifier.Owner is the org id string. This is a separate binding from the
// owner-key path (EOA left-padded label + workflow owner address); both remain supported via their respective entrypoints.
func (h *Handler) EncryptSecretsForBrowserOrg(rawSecrets UpsertSecretsInputs, orgID string) ([]*vault.EncryptedSecret, error) {
// EncryptSecrets encrypts secrets for the given workflow owner address.
// TDH2 label is the workflow owner address left-padded to 32 bytes; SecretIdentifier.Owner is the same hex address string.
func (h *Handler) EncryptSecrets(rawSecrets UpsertSecretsInputs, owner string) ([]*vault.EncryptedSecret, error) {
pubKeyHex, err := h.fetchVaultMasterPublicKeyHex()
if err != nil {
return nil, err
}

label := sha256.Sum256([]byte(orgID))

encryptedSecrets := make([]*vault.EncryptedSecret, 0, len(rawSecrets))
for _, item := range rawSecrets {
cipherHex, err := encryptSecretWithLabel(item.Value, pubKeyHex, label)
cipherHex, err := EncryptSecret(item.Value, pubKeyHex, owner)
if err != nil {
return nil, fmt.Errorf("failed to encrypt secret (key=%s ns=%s): %w", item.ID, item.Namespace, err)
}
secID := &vault.SecretIdentifier{
Key: item.ID,
Namespace: item.Namespace,
Owner: orgID,
Owner: owner,
}
encryptedSecrets = append(encryptedSecrets, &vault.EncryptedSecret{
Id: secID,
Expand Down Expand Up @@ -448,8 +420,13 @@ func (h *Handler) Execute(
return err
}

owner, err := h.ResolveVaultIdentifierOwnerForAuth(secretsAuth)
if err != nil {
return err
}

// Build from YAML inputs
encSecrets, err := h.EncryptSecrets(inputs)
encSecrets, err := h.EncryptSecrets(inputs, owner)
if err != nil {
return fmt.Errorf("failed to encrypt secrets: %w", err)
}
Expand Down Expand Up @@ -499,7 +476,7 @@ func (h *Handler) Execute(
return fmt.Errorf("unsupported method %q (expected %q or %q)", method, vaulttypes.MethodSecretsCreate, vaulttypes.MethodSecretsUpdate)
}

ownerAddr := common.HexToAddress(h.OwnerAddress)
ownerAddr := common.HexToAddress(owner)

allowlisted, err := h.Wrc.IsRequestAllowlisted(ownerAddr, digest)
if err != nil {
Expand Down
20 changes: 10 additions & 10 deletions cmd/secrets/common/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestEncryptSecrets(t *testing.T) {
{ID: "test-secret-2", Value: "another-value", Namespace: "ns2"},
}

enc, err := h.EncryptSecrets(raw)
enc, err := h.EncryptSecrets(raw, "0xabc")
require.NoError(t, err)
require.Len(t, enc, 2)

Expand Down Expand Up @@ -100,7 +100,7 @@ func TestEncryptSecrets(t *testing.T) {
},
}

enc, err := h.EncryptSecrets(UpsertSecretsInputs{{ID: "s", Value: "v", Namespace: "n"}})
enc, err := h.EncryptSecrets(UpsertSecretsInputs{{ID: "s", Value: "v", Namespace: "n"}}, "0xabc")
require.Error(t, err)
require.Nil(t, enc)
require.Contains(t, err.Error(), "gateway POST failed")
Expand All @@ -126,7 +126,7 @@ func TestEncryptSecrets(t *testing.T) {
},
}

enc, err := h.EncryptSecrets(UpsertSecretsInputs{{ID: "s", Value: "v", Namespace: "n"}})
enc, err := h.EncryptSecrets(UpsertSecretsInputs{{ID: "s", Value: "v", Namespace: "n"}}, "0xabc")
require.Error(t, err)
require.Nil(t, enc)
require.Contains(t, err.Error(), "vault public key fetch error")
Expand Down Expand Up @@ -163,15 +163,15 @@ func TestResolveEffectiveOwner(t *testing.T) {
}

func TestResolveVaultIdentifierOwnerForAuth(t *testing.T) {
t.Run("browser returns org ID", func(t *testing.T) {
t.Run("browser returns derived workflow owner from session", func(t *testing.T) {
h, _, _ := newMockHandler(t)
h.OwnerAddress = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
h.Credentials.AuthType = credentials.AuthTypeBearer
h.Credentials.OrgID = "org-browser"
h.DerivedWorkflowOwner = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"

owner, err := h.ResolveVaultIdentifierOwnerForAuth(SecretsAuthBrowser)
require.NoError(t, err)
require.Equal(t, "org-browser", owner)
require.Equal(t, "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", owner)
})

t.Run("browser errors on api key auth", func(t *testing.T) {
Expand All @@ -184,14 +184,14 @@ func TestResolveVaultIdentifierOwnerForAuth(t *testing.T) {
require.Contains(t, err.Error(), "interactive login")
})

t.Run("browser errors when org ID is empty", func(t *testing.T) {
t.Run("browser errors when derived workflow owner is empty", func(t *testing.T) {
h, _, _ := newMockHandler(t)
h.Credentials.AuthType = credentials.AuthTypeBearer
h.Credentials.OrgID = ""
h.Credentials.OrgID = "org-1"

_, err := h.ResolveVaultIdentifierOwnerForAuth(SecretsAuthBrowser)
require.Error(t, err)
require.Contains(t, err.Error(), "organization information is missing")
require.Contains(t, err.Error(), "derived workflow owner is not available")
})

t.Run("onchain delegates to ResolveEffectiveOwner", func(t *testing.T) {
Expand Down Expand Up @@ -226,7 +226,7 @@ func TestEncryptSecrets_UsesWorkflowOwnerAddress(t *testing.T) {

enc, err := h.EncryptSecrets(UpsertSecretsInputs{
{ID: "secret-1", Value: "val1", Namespace: "main"},
})
}, "0xabc")
require.NoError(t, err)
require.Len(t, enc, 1)
require.Equal(t, "0xabc", enc[0].Id.Owner)
Expand Down
2 changes: 1 addition & 1 deletion cmd/secrets/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func Execute(h *common.Handler, inputs DeleteSecretsInputs, duration time.Durati

if common.IsBrowserFlow(secretsAuth) {
ui.Dim("Using your account to authorize vault access for this delete request...")
return h.ExecuteBrowserVaultAuthorization(context.Background(), vaulttypes.MethodSecretsDelete, digest, requestBody)
return h.ExecuteBrowserVaultAuthorization(context.Background(), vaulttypes.MethodSecretsDelete, digest, requestBody, owner)
}

gatewayPost := func() error {
Expand Down
2 changes: 1 addition & 1 deletion cmd/secrets/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func Execute(h *common.Handler, namespace string, duration time.Duration, secret

if common.IsBrowserFlow(secretsAuth) {
ui.Dim("Using your account to authorize vault access for this list request...")
return h.ExecuteBrowserVaultAuthorization(context.Background(), vaulttypes.MethodSecretsList, digest, body)
return h.ExecuteBrowserVaultAuthorization(context.Background(), vaulttypes.MethodSecretsList, digest, body, owner)
}

ownerAddr := ethcommon.HexToAddress(owner)
Expand Down
Loading
Loading