Skip to content
Open
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
71 changes: 71 additions & 0 deletions resource/decrypt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package resource

import (
"context"
"fmt"

"github.com/passbolt/go-passbolt/api"
"github.com/passbolt/go-passbolt/helper"
)

// decryptedResource holds the decrypted fields from a resource
type decryptedResource struct {
name string
username string
uri string
password string
description string
}

// decryptResource decrypts a resource's secret and returns the decrypted fields.
// It uses the provided resourceTypeCache to avoid fetching the same ResourceType multiple times.
// If the resource has pre-populated fields (Passbolt v3/v4), it uses those when secrets aren't needed.
func decryptResource(
ctx context.Context,
client *api.Client,
resource api.Resource,
needsDecryption bool,
resourceTypeCache map[string]*api.ResourceType,
) (decryptedResource, error) {
result := decryptedResource{
name: resource.Name,
username: resource.Username,
uri: resource.URI,
description: resource.Description,
}

// If we don't need decryption or no secrets available, return existing fields
if !needsDecryption || len(resource.Secrets) == 0 {
return result, nil
}

// Check cache first
rType, exists := resourceTypeCache[resource.ResourceTypeID]
if !exists {
var err error
rType, err = client.GetResourceType(ctx, resource.ResourceTypeID)
if err != nil {
return result, fmt.Errorf("Get ResourceType: %w", err)
}
resourceTypeCache[resource.ResourceTypeID] = rType
}

// Decrypt using the secret
_, name, username, uri, password, description, err := helper.GetResourceFromData(
client,
resource,
resource.Secrets[0],
*rType,
)
if err != nil {
return result, fmt.Errorf("Decrypt Resource: %w", err)
}

result.name = name
result.username = username
result.uri = uri
result.password = password
result.description = description

return result, nil
}
44 changes: 34 additions & 10 deletions resource/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package resource
import (
"context"
"fmt"
"strings"

"github.com/google/cel-go/cel"
"github.com/passbolt/go-passbolt-cli/util"
Expand Down Expand Up @@ -34,26 +35,49 @@ func filterResources(resources *[]api.Resource, celCmd string, ctx context.Conte
return nil, err
}

// Check if filter uses encrypted fields (Name, Username, URI, Password, Description)
// We do a simple string check - if any of these fields appear in the filter, we need to decrypt
needsDecryption := false
encryptedFields := []string{"Name", "Username", "URI", "Password", "Description"}
for _, field := range encryptedFields {
if strings.Contains(celCmd, field) {
needsDecryption = true
break
}
}

// Cache resource types to avoid fetching the same type repeatedly
resourceTypeCache := make(map[string]*api.ResourceType)

filteredResources := []api.Resource{}
for _, resource := range *resources {
// TODO We should decrypt the secret only when required for performance reasonse
_, name, username, uri, pass, desc, err := helper.GetResource(ctx, client, resource.ID)
// Decrypt resource if filter needs encrypted fields
decrypted, err := decryptResource(ctx, client, resource, needsDecryption, resourceTypeCache)
if err != nil {
return nil, fmt.Errorf("Get Resource %w", err)
return nil, err
}

// Fallback: fetch individually if fields are empty and no secrets included
if needsDecryption && len(resource.Secrets) == 0 {
if decrypted.name == "" || decrypted.username == "" || decrypted.uri == "" || decrypted.description == "" {
_, decrypted.name, decrypted.username, decrypted.uri, decrypted.password, decrypted.description, err = helper.GetResource(ctx, client, resource.ID)
if err != nil {
return nil, fmt.Errorf("Get Resource: %w", err)
}
}
}

val, _, err := (*program).ContextEval(ctx, map[string]any{
"Id": resource.ID,
"ID": resource.ID,
"FolderParentID": resource.FolderParentID,
"Name": name,
"Username": username,
"URI": uri,
"Password": pass,
"Description": desc,
"Name": decrypted.name,
"Username": decrypted.username,
"URI": decrypted.uri,
"Password": decrypted.password,
"Description": decrypted.description,
"CreatedTimestamp": resource.Created.Time,
"ModifiedTimestamp": resource.Modified.Time,
})

if err != nil {
return nil, err
}
Expand Down
109 changes: 87 additions & 22 deletions resource/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,35 @@ func ResourceList(cmd *cobra.Command, args []string) error {
defer client.Logout(context.TODO())
cmd.SilenceUsage = true

// Check if we need to fetch secrets (password column or any encrypted field when not available)
// For Passbolt v5+, we should fetch secrets to get all encrypted fields in one request
needsSecrets := false
for _, col := range columns {
colLower := strings.ToLower(col)
if colLower == "password" || colLower == "name" || colLower == "username" ||
colLower == "uri" || colLower == "description" {
needsSecrets = true
break
}
}

// Also check if filter uses encrypted fields
if !needsSecrets && celFilter != "" {
encryptedFields := []string{"Name", "Username", "URI", "Password", "Description"}
for _, field := range encryptedFields {
if strings.Contains(celFilter, field) {
needsSecrets = true
break
}
}
}

resources, err := client.GetResources(ctx, &api.GetResourcesOptions{
FilterIsFavorite: favorite,
FilterIsOwnedByMe: own,
FilterIsSharedWithGroup: group,
FilterHasParent: folderParents,
ContainSecret: needsSecrets,
})
if err != nil {
return fmt.Errorf("Listing Resource: %w", err)
Expand All @@ -91,23 +115,42 @@ func ResourceList(cmd *cobra.Command, args []string) error {
}

if jsonOutput {
outputResources := []ResourceJsonOutput{}
// Cache resource types to avoid fetching the same type repeatedly
resourceTypeCache := make(map[string]*api.ResourceType)

outputResources := []map[string]interface{}{}
for i := range resources {
_, name, username, uri, pass, desc, err := helper.GetResource(ctx, client, resources[i].ID)
decrypted, err := decryptResource(ctx, client, resources[i], needsSecrets, resourceTypeCache)
if err != nil {
return fmt.Errorf("Get Resource %w", err)
return err
}
outputResources = append(outputResources, ResourceJsonOutput{
ID: &resources[i].ID,
FolderParentID: &resources[i].FolderParentID,
Name: &name,
Username: &username,
URI: &uri,
Password: &pass,
Description: &desc,
CreatedTimestamp: &resources[i].Created.Time,
ModifiedTimestamp: &resources[i].Modified.Time,
})

// Build output with only requested columns
output := make(map[string]interface{})
for _, col := range columns {
switch strings.ToLower(col) {
case "id":
output["ID"] = resources[i].ID
case "folderparentid":
output["FolderParentID"] = resources[i].FolderParentID
case "name":
output["Name"] = decrypted.name
case "username":
output["Username"] = decrypted.username
case "uri":
output["URI"] = decrypted.uri
case "password":
output["Password"] = decrypted.password
case "description":
output["Description"] = decrypted.description
case "createdtimestamp":
output["CreatedTimestamp"] = resources[i].Created.Time
case "modifiedtimestamp":
output["ModifiedTimestamp"] = resources[i].Modified.Time
}
}

outputResources = append(outputResources, output)
}
jsonResources, err := json.MarshalIndent(outputResources, "", " ")
if err != nil {
Expand All @@ -117,11 +160,33 @@ func ResourceList(cmd *cobra.Command, args []string) error {
} else {
data := pterm.TableData{columns}

// Check if we need to fetch encrypted secrets (Password always requires decryption)
needsPassword := false
for _, col := range columns {
if strings.ToLower(col) == "password" {
needsPassword = true
break
}
}

// Cache resource types to avoid fetching the same type repeatedly
resourceTypeCache := make(map[string]*api.ResourceType)

for _, resource := range resources {
// TODO We should decrypt the secret only when required for performance reasonse
_, name, username, uri, pass, desc, err := helper.GetResource(ctx, client, resource.ID)
var err error

// Decrypt resource if needed
decrypted, err := decryptResource(ctx, client, resource, needsSecrets, resourceTypeCache)
if err != nil {
return fmt.Errorf("Get Resource %w", err)
return err
}

// Fallback: If we need password but secrets weren't included, fetch individually
if needsPassword && len(resource.Secrets) == 0 {
_, decrypted.name, decrypted.username, decrypted.uri, decrypted.password, decrypted.description, err = helper.GetResource(ctx, client, resource.ID)
if err != nil {
return fmt.Errorf("Get Resource %w", err)
}
}

entry := make([]string, len(columns))
Expand All @@ -132,15 +197,15 @@ func ResourceList(cmd *cobra.Command, args []string) error {
case "folderparentid":
entry[i] = resource.FolderParentID
case "name":
entry[i] = shellescape.StripUnsafe(name)
entry[i] = shellescape.StripUnsafe(decrypted.name)
case "username":
entry[i] = shellescape.StripUnsafe(username)
entry[i] = shellescape.StripUnsafe(decrypted.username)
case "uri":
entry[i] = shellescape.StripUnsafe(uri)
entry[i] = shellescape.StripUnsafe(decrypted.uri)
case "password":
entry[i] = shellescape.StripUnsafe(pass)
entry[i] = shellescape.StripUnsafe(decrypted.password)
case "description":
entry[i] = shellescape.StripUnsafe(desc)
entry[i] = shellescape.StripUnsafe(decrypted.description)
case "createdtimestamp":
entry[i] = resource.Created.Format(time.RFC3339)
case "modifiedtimestamp":
Expand Down