Skip to content

Commit dc673f6

Browse files
Remove ContainResourceType true, and improve CEL filtering
1 parent 2a40bf0 commit dc673f6

File tree

4 files changed

+125
-63
lines changed

4 files changed

+125
-63
lines changed

resource/filter.go

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ import (
66

77
"github.com/google/cel-go/cel"
88
"github.com/passbolt/go-passbolt-cli/util"
9-
"github.com/passbolt/go-passbolt/api"
10-
"github.com/passbolt/go-passbolt/helper"
119
)
1210

13-
// Environments for CEl
14-
var celEnvOptions = []cel.EnvOption{
11+
// CelEnvOptions defines the CEL environment for resource filtering
12+
var CelEnvOptions = []cel.EnvOption{
1513
cel.Variable("ID", cel.StringType),
1614
cel.Variable("FolderParentID", cel.StringType),
1715
cel.Variable("Name", cel.StringType),
@@ -23,56 +21,42 @@ var celEnvOptions = []cel.EnvOption{
2321
cel.Variable("ModifiedTimestamp", cel.TimestampType),
2422
}
2523

26-
// Filters the slice resources by invoke CEL program for each resource
27-
// Note: Resources must have been fetched with ContainSecret and ContainResourceType options
28-
func filterResources(resources *[]api.Resource, celCmd string, ctx context.Context, client *api.Client) ([]api.Resource, error) {
24+
// filterDecryptedResources filters already-decrypted resources by evaluating a CEL expression.
25+
func filterDecryptedResources(resources []decryptedResource, celCmd string, ctx context.Context) ([]decryptedResource, error) {
2926
if celCmd == "" {
30-
return *resources, nil
27+
return resources, nil
3128
}
3229

33-
program, err := util.InitCELProgram(celCmd, celEnvOptions...)
30+
program, err := util.InitCELProgram(celCmd, CelEnvOptions...)
3431
if err != nil {
3532
return nil, err
3633
}
3734

38-
filteredResources := []api.Resource{}
39-
for _, resource := range *resources {
40-
if len(resource.Secrets) == 0 {
41-
continue
42-
}
43-
_, name, username, uri, pass, desc, err := helper.GetResourceFromData(
44-
client,
45-
resource,
46-
resource.Secrets[0],
47-
resource.ResourceType,
48-
)
49-
if err != nil {
50-
return nil, fmt.Errorf("Get Resource %w", err)
51-
}
52-
35+
filtered := []decryptedResource{}
36+
for _, d := range resources {
5337
val, _, err := (*program).ContextEval(ctx, map[string]any{
54-
"Id": resource.ID,
55-
"FolderParentID": resource.FolderParentID,
56-
"Name": name,
57-
"Username": username,
58-
"URI": uri,
59-
"Password": pass,
60-
"Description": desc,
61-
"CreatedTimestamp": resource.Created.Time,
62-
"ModifiedTimestamp": resource.Modified.Time,
38+
"ID": d.resource.ID,
39+
"FolderParentID": d.resource.FolderParentID,
40+
"Name": d.name,
41+
"Username": d.username,
42+
"URI": d.uri,
43+
"Password": d.password,
44+
"Description": d.description,
45+
"CreatedTimestamp": d.resource.Created.Time,
46+
"ModifiedTimestamp": d.resource.Modified.Time,
6347
})
6448

6549
if err != nil {
6650
return nil, err
6751
}
6852

6953
if val.Value() == true {
70-
filteredResources = append(filteredResources, resource)
54+
filtered = append(filtered, d)
7155
}
7256
}
7357

74-
if len(filteredResources) == 0 {
58+
if len(filtered) == 0 {
7559
return nil, fmt.Errorf("No such Resources found with filter %v!", celCmd)
7660
}
77-
return filteredResources, nil
61+
return filtered, nil
7862
}

resource/list.go

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package resource
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"runtime"
@@ -65,16 +66,25 @@ func ResourceList(cmd *cobra.Command, args []string) error {
6566
return err
6667
}
6768

68-
// Check if we need to decrypt secrets (expensive RSA operation)
69+
// Check if we need to fetch secrets (expensive server join + RSA decryption)
6970
// For v5 resources, metadata (name, username, uri) can be decrypted without secrets
70-
needSecretDecryption := false
71+
needSecrets := false
7172
for _, col := range config.columns {
7273
switch strings.ToLower(col) {
7374
case "password", "description":
74-
needSecretDecryption = true
75+
needSecrets = true
7576
}
7677
}
7778

79+
// Check if CEL filter references Password or Description
80+
if !needSecrets && config.celFilter != "" {
81+
refsSecrets, err := util.CELExpressionReferencesFields(config.celFilter, []string{"Password", "Description"}, CelEnvOptions...)
82+
if err != nil {
83+
return fmt.Errorf("Parsing filter: %w", err)
84+
}
85+
needSecrets = refsSecrets
86+
}
87+
7888
ctx := util.GetContext()
7989

8090
client, err := util.GetClient(ctx)
@@ -89,22 +99,24 @@ func ResourceList(cmd *cobra.Command, args []string) error {
8999
FilterIsOwnedByMe: config.own,
90100
FilterIsSharedWithGroup: config.group,
91101
FilterHasParent: config.folderParents,
92-
ContainSecret: true,
93-
ContainResourceType: true,
102+
ContainSecret: needSecrets,
94103
})
95104
if err != nil {
96105
return fmt.Errorf("Listing Resource: %w", err)
97106
}
98107

99-
resources, err = filterResources(&resources, config.celFilter, ctx, client)
108+
// Decrypt all resources in parallel
109+
decrypted, err := decryptResourcesParallel(ctx, client, resources, needSecrets)
100110
if err != nil {
101111
return err
102112
}
103113

104-
// Decrypt all resources in parallel
105-
decrypted, err := decryptResourcesParallel(client, resources, needSecretDecryption)
106-
if err != nil {
107-
return err
114+
// Apply CEL filter on already-decrypted data
115+
if config.celFilter != "" {
116+
decrypted, err = filterDecryptedResources(decrypted, config.celFilter, ctx)
117+
if err != nil {
118+
return err
119+
}
108120
}
109121

110122
if config.jsonOutput {
@@ -114,28 +126,29 @@ func ResourceList(cmd *cobra.Command, args []string) error {
114126
return printTableResources(decrypted, config.columns)
115127
}
116128

117-
func decryptResourcesParallel(client *api.Client, resources []api.Resource, needSecretDecryption bool) ([]decryptedResource, error) {
118-
// Filter resources with secrets
129+
func decryptResourcesParallel(ctx context.Context, client *api.Client, resources []api.Resource, needSecrets bool) ([]decryptedResource, error) {
130+
// Use parallel decryption with worker pool
131+
numWorkers := runtime.NumCPU()
132+
if numWorkers > 16 {
133+
numWorkers = 16 // Cap at 16 workers
134+
}
135+
if len(resources) < numWorkers {
136+
numWorkers = len(resources)
137+
}
138+
139+
// Filter resources - only require secrets if we're fetching them
119140
var validResources []api.Resource
120141
for i := range resources {
121-
if len(resources[i].Secrets) > 0 {
122-
validResources = append(validResources, resources[i])
142+
if needSecrets && len(resources[i].Secrets) == 0 {
143+
continue
123144
}
145+
validResources = append(validResources, resources[i])
124146
}
125147

126148
if len(validResources) == 0 {
127149
return []decryptedResource{}, nil
128150
}
129151

130-
// Use parallel decryption with worker pool
131-
numWorkers := runtime.NumCPU()
132-
if numWorkers > 16 {
133-
numWorkers = 16 // Cap at 16 workers
134-
}
135-
if len(validResources) < numWorkers {
136-
numWorkers = len(validResources)
137-
}
138-
139152
// Channel for work items and results
140153
// Note: Session keys are pre-fetched during Login() when the server supports v5 metadata,
141154
// so no additional prefetching is needed here.
@@ -150,12 +163,43 @@ func decryptResourcesParallel(client *api.Client, resources []api.Resource, need
150163
defer wg.Done()
151164
for idx := range jobs {
152165
resource := validResources[idx]
166+
167+
// Lookup resource type from cache (single API call for all types)
168+
rType, err := client.GetResourceTypeCached(ctx, resource.ResourceTypeID)
169+
if err != nil {
170+
results <- decryptedResource{index: idx, err: fmt.Errorf("Get ResourceType: %w", err)}
171+
continue
172+
}
173+
174+
// For v4 resources without secret decryption, use plaintext fields directly
175+
// This avoids unnecessary function calls for 10k+ resources
176+
isV5 := strings.HasPrefix(rType.Slug, "v5-")
177+
if !needSecrets && !isV5 {
178+
// V4 resource - metadata is plaintext, no decryption needed
179+
results <- decryptedResource{
180+
index: idx,
181+
resource: resource,
182+
name: resource.Name,
183+
username: resource.Username,
184+
uri: resource.URI,
185+
password: "",
186+
description: resource.Description,
187+
}
188+
continue
189+
}
190+
191+
// Handle case where secrets weren't fetched
192+
var secret api.Secret
193+
if len(resource.Secrets) > 0 {
194+
secret = resource.Secrets[0]
195+
}
196+
153197
_, name, username, uri, pass, desc, err := helper.GetResourceFromDataWithOptions(
154198
client,
155199
resource,
156-
resource.Secrets[0],
157-
resource.ResourceType,
158-
needSecretDecryption,
200+
secret,
201+
*rType,
202+
needSecrets,
159203
)
160204
results <- decryptedResource{
161205
index: idx,

util/cel.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,37 @@ func InitCELProgram(celCmd string, options ...cel.EnvOption) (*cel.Program, erro
2121

2222
return &program, nil
2323
}
24+
25+
// CELExpressionReferencesFields checks if a CEL expression references any of the given field names.
26+
// Returns true if the expression references at least one of the specified fields.
27+
func CELExpressionReferencesFields(celCmd string, fieldNames []string, options ...cel.EnvOption) (bool, error) {
28+
if celCmd == "" {
29+
return false, nil
30+
}
31+
32+
env, err := cel.NewEnv(options...)
33+
if err != nil {
34+
return false, err
35+
}
36+
37+
ast, issue := env.Compile(celCmd)
38+
if issue.Err() != nil {
39+
return false, issue.Err()
40+
}
41+
42+
// Build a set of field names to check
43+
fieldSet := make(map[string]bool)
44+
for _, name := range fieldNames {
45+
fieldSet[name] = true
46+
}
47+
48+
// Get the native AST representation which has ReferenceMap
49+
nativeAST := ast.NativeRep()
50+
refMap := nativeAST.ReferenceMap()
51+
for _, refInfo := range refMap {
52+
if fieldSet[refInfo.Name] {
53+
return true, nil
54+
}
55+
}
56+
return false, nil
57+
}

util/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func ReadPassword(prompt string) (string, error) {
2222
fd := int(os.Stdin.Fd())
2323
var pass string
2424
if term.IsTerminal(fd) {
25-
fmt.Fprint(os.Stderr, prompt);
25+
fmt.Fprint(os.Stderr, prompt)
2626

2727
inputPass, err := term.ReadPassword(fd)
2828
if err != nil {

0 commit comments

Comments
 (0)