• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mindersec / minder / 13048772803

30 Jan 2025 08:54AM UTC coverage: 57.401% (+0.6%) from 56.836%
13048772803

push

github

web-flow
Fall back to generic env for selectors (#5379)

When an entity type does not have a specific CEL environment,
fall back to the generic environment.

Fix #5375

3 of 13 new or added lines in 1 file covered. (23.08%)

2 existing lines in 1 file now uncovered.

18005 of 31367 relevant lines covered (57.4%)

37.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.15
/pkg/engine/selectors/selectors.go
1
// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
//go:generate go run go.uber.org/mock/mockgen -package mock_$GOPACKAGE -destination=./mock/$GOFILE -source=./$GOFILE
5

6
// Package selectors provides utilities for selecting entities based on profiles using CEL
7
package selectors
8

9
import (
10
        "encoding/json"
11
        "errors"
12
        "fmt"
13
        "strings"
14
        "sync"
15

16
        "github.com/google/cel-go/cel"
17
        "github.com/google/cel-go/checker/decls"
18
        "github.com/google/cel-go/common/types"
19
        "github.com/google/cel-go/common/types/ref"
20
        "github.com/google/cel-go/interpreter"
21

22
        internalpb "github.com/mindersec/minder/internal/proto"
23
        minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
24
        "github.com/mindersec/minder/pkg/profiles/models"
25
)
26

27
var (
28
        // ErrResultUnknown is returned when the result of a selector expression is unknown
29
        // this tells the caller to try again with more information
30
        ErrResultUnknown = errors.New("result is unknown")
31
        // ErrUnsupported is returned when the entity type is not supported
32
        ErrUnsupported = errors.New("unsupported entity type")
33
        // ErrSelectorCheck is returned if the selector fails to be checked for syntax errors
34
        ErrSelectorCheck = errors.New("failed to check selector")
35
)
36

37
// ErrKind is a string for the kind of error that occurred
38
type ErrKind string
39

40
const (
41
        // ErrKindParse is an error kind for parsing errors, e.g. syntax errors
42
        ErrKindParse ErrKind = "parse"
43
        // ErrKindCheck is an error kind for checking errors, e.g. mismatched types
44
        ErrKindCheck ErrKind = "check"
45
)
46

47
// ErrInstance is one occurrence of an error in a CEL expression
48
type ErrInstance struct {
49
        Line int    `json:"line,omitempty"`
50
        Col  int    `json:"col,omitempty"`
51
        Msg  string `json:"msg,omitempty"`
52
}
53

54
// ErrDetails is a struct that holds the details of an error in a CEL expression
55
type ErrDetails struct {
56
        Errors []ErrInstance `json:"errors,omitempty"`
57
        Source string        `json:"source,omitempty"`
58
}
59

60
// AsJSON returns the ErrDetails as a JSON string
61
func (ed *ErrDetails) AsJSON() string {
4✔
62
        edBytes, err := json.Marshal(ed)
4✔
63
        if err != nil {
4✔
64
                return fmt.Sprintf(`{"error": "failed to marshal JSON: %s"}`, err)
×
65
        }
×
66
        return string(edBytes)
4✔
67
}
68

69
func errDetailsFromCelIssues(source string, issues *cel.Issues) ErrDetails {
5✔
70
        var ed ErrDetails
5✔
71

5✔
72
        ed.Source = source
5✔
73
        ed.Errors = make([]ErrInstance, 0, len(issues.Errors()))
5✔
74
        for _, err := range issues.Errors() {
10✔
75
                ed.Errors = append(ed.Errors, ErrInstance{
5✔
76
                        Line: err.Location.Line(),
5✔
77
                        Col:  err.Location.Column(),
5✔
78
                        Msg:  err.Message,
5✔
79
                })
5✔
80
        }
5✔
81

82
        return ed
5✔
83
}
84

85
// ErrStructure is a struct that callers can use to deserialize the JSON error
86
type ErrStructure struct {
87
        Err     ErrKind    `json:"err"`
88
        Details ErrDetails `json:"details"`
89
}
90

91
// ParseError is an error type for syntax errors in CEL expressions
92
type ParseError struct {
93
        ErrDetails
94
        original error
95
}
96

97
// Error implements the error interface for ParseError
98
func (pe *ParseError) Error() string {
1✔
99
        return fmt.Sprintf(`{"err": "%s", "details": %s}`, ErrKindParse, pe.AsJSON())
1✔
100
}
1✔
101

102
// Is checks if the target error is a ParseError
103
func (_ *ParseError) Is(target error) bool {
3✔
104
        var t *ParseError
3✔
105
        return errors.As(target, &t)
3✔
106
}
3✔
107

108
func (pe *ParseError) Unwrap() error {
2✔
109
        return pe.original
2✔
110
}
2✔
111

112
// CheckError is an error type for type checking errors in CEL expressions, e.g.
113
// mismatched types
114
type CheckError struct {
115
        ErrDetails
116
        original error
117
}
118

119
// Error implements the error interface for CheckError
120
func (ce *CheckError) Error() string {
3✔
121
        return fmt.Sprintf(`{"err": "%s", "details": %s}`, ErrKindCheck, ce.AsJSON())
3✔
122
}
3✔
123

124
// Is checks if the target error is a CheckError
125
func (_ *CheckError) Is(target error) bool {
4✔
126
        var t *CheckError
4✔
127
        return errors.As(target, &t)
4✔
128
}
4✔
129

130
func (ce *CheckError) Unwrap() error {
4✔
131
        return ce.original
4✔
132
}
4✔
133

134
func newParseError(source string, issues *cel.Issues) error {
2✔
135
        return &ParseError{
2✔
136
                ErrDetails: errDetailsFromCelIssues(source, issues),
2✔
137
                original:   ErrSelectorCheck,
2✔
138
        }
2✔
139
}
2✔
140

141
func newCheckError(source string, issues *cel.Issues) error {
3✔
142
        return &CheckError{
3✔
143
                ErrDetails: errDetailsFromCelIssues(source, issues),
3✔
144
                original:   ErrSelectorCheck,
3✔
145
        }
3✔
146
}
3✔
147

148
// celEnvFactory is an interface for creating CEL environments
149
// for an entity. Each entity must implement this interface to be
150
// usable in selectors
151
type celEnvFactory func() (*cel.Env, error)
152

153
// genericEnvFactory is a factory for creating a CEL environment
154
// for the generic SelectorEntity type
155
func genericEnvFactory() (*cel.Env, error) {
×
156
        return newEnvForEntity(
×
157
                "entity",
×
158
                &internalpb.SelectorEntity{},
×
159
                "internal.SelectorEntity")
×
160
}
×
161

162
// repoEnvFactory is a factory for creating a CEL environment
163
// for the SelectorRepository type representing a repository
164
func repoEnvFactory() (*cel.Env, error) {
43✔
165
        return newEnvForEntity(
43✔
166
                "repository",
43✔
167
                &internalpb.SelectorRepository{},
43✔
168
                "internal.SelectorRepository")
43✔
169
}
43✔
170

171
// artifactEnvFactory is a factory for creating a CEL environment
172
// for the SelectorArtifact type representing an artifact
173
func artifactEnvFactory() (*cel.Env, error) {
8✔
174
        return newEnvForEntity(
8✔
175
                "artifact",
8✔
176
                &internalpb.SelectorArtifact{},
8✔
177
                "internal.SelectorArtifact")
8✔
178
}
8✔
179

180
// pullRequestEnvFactory is a factory for creating a CEL environment
181
// for the SelectorPullRequest type representing a pull request
182
func pullRequestEnvFactory() (*cel.Env, error) {
8✔
183
        return newEnvForEntity(
8✔
184
                "pull_request",
8✔
185
                &internalpb.SelectorArtifact{},
8✔
186
                "internal.SelectorPullRequest")
8✔
187
}
8✔
188

189
// newEnvForEntity creates a new CEL environment for an entity. All environments are allowed to
190
// use the generic "entity" variable plus the specific entity type is also declared as variable
191
// with the appropriate type.
192
func newEnvForEntity(varName string, typ any, typName string) (*cel.Env, error) {
59✔
193
        entityPtr := &internalpb.SelectorEntity{}
59✔
194

59✔
195
        env, err := cel.NewEnv(
59✔
196
                cel.Types(typ), cel.Types(&internalpb.SelectorEntity{}),
59✔
197
                cel.Declarations(
59✔
198
                        decls.NewVar("entity",
59✔
199
                                decls.NewObjectType(string(entityPtr.ProtoReflect().Descriptor().FullName())),
59✔
200
                        ),
59✔
201
                        decls.NewVar(varName,
59✔
202
                                decls.NewObjectType(typName),
59✔
203
                        ),
59✔
204
                ),
59✔
205
        )
59✔
206
        if err != nil {
59✔
207
                return nil, fmt.Errorf("failed to create CEL environment for %s: %v", varName, err)
×
208
        }
×
209

210
        return env, nil
59✔
211
}
212

213
type compiledSelector struct {
214
        orig    string
215
        ast     *cel.Ast
216
        program cel.Program
217
}
218

219
// compileSelectorForEntity compiles a selector expression for a given entity type into a CEL program
220
func compileSelectorForEntity(env *cel.Env, selector string) (*compiledSelector, error) {
53✔
221
        checked, err := checkSelectorForEntity(env, selector)
53✔
222
        if err != nil {
56✔
223
                return nil, fmt.Errorf("failed to check expression %q: %w", selector, err)
3✔
224
        }
3✔
225

226
        program, err := env.Program(checked,
50✔
227
                // OptPartialEval is needed to enable partial evaluation of the expression
50✔
228
                // OptTrackState is needed to get the details about partial evaluation (aka what is missing)
50✔
229
                cel.EvalOptions(cel.OptTrackState, cel.OptPartialEval))
50✔
230
        if err != nil {
50✔
231
                return nil, fmt.Errorf("failed to create program for expression %q: %w", selector, err)
×
232
        }
×
233

234
        return &compiledSelector{
50✔
235
                ast:     checked,
50✔
236
                orig:    selector,
50✔
237
                program: program,
50✔
238
        }, nil
50✔
239
}
240

241
func checkSelectorForEntity(env *cel.Env, selector string) (*cel.Ast, error) {
62✔
242
        parsedAst, issues := env.Parse(selector)
62✔
243
        if issues.Err() != nil {
64✔
244
                return nil, newParseError(selector, issues)
2✔
245
        }
2✔
246

247
        checkedAst, issues := env.Check(parsedAst)
60✔
248
        if issues.Err() != nil {
63✔
249
                return nil, newCheckError(selector, issues)
3✔
250
        }
3✔
251

252
        return checkedAst, nil
57✔
253
}
254

255
// SelectionBuilder is an interface for creating Selections (a collection of compiled CEL expressions)
256
// for an entity type. This is what the user of this module uses. The interface makes it easier to pass
257
// mocks by the user of this module.
258
type SelectionBuilder interface {
259
        NewSelectionFromProfile(minderv1.Entity, []models.ProfileSelector) (Selection, error)
260
}
261

262
// SelectionChecker is an interface for checking if a selector expression is valid for a given entity type
263
type SelectionChecker interface {
264
        CheckSelector(*minderv1.Profile_Selector) error
265
}
266

267
// Env is a struct that holds the CEL environments for each entity type and the factories for creating
268
type Env struct {
269
        // entityEnvs is a map of entity types to their respective CEL environments. We keep them cached
270
        // and lazy-initialize on first use
271
        entityEnvs map[minderv1.Entity]*entityEnvCache
272
        // factories is a map of entity types to their respective factories for creating CEL environments
273
        factories map[minderv1.Entity]celEnvFactory
274
}
275

276
// entityEnvCache is a struct that holds a CEL environment for lazy-initialization. Since the initialization
277
// is done only once, we also keep track of the error
278
type entityEnvCache struct {
279
        once sync.Once
280
        env  *cel.Env
281
        err  error
282
}
283

284
// NewEnv creates a new Env struct with the default factories for each entity type. The factories
285
// are used on first access to create the CEL environments for each entity type.
286
func NewEnv() *Env {
92✔
287
        factoryMap := map[minderv1.Entity]celEnvFactory{
92✔
288
                minderv1.Entity_ENTITY_UNSPECIFIED:   genericEnvFactory,
92✔
289
                minderv1.Entity_ENTITY_REPOSITORIES:  repoEnvFactory,
92✔
290
                minderv1.Entity_ENTITY_ARTIFACTS:     artifactEnvFactory,
92✔
291
                minderv1.Entity_ENTITY_PULL_REQUESTS: pullRequestEnvFactory,
92✔
292
        }
92✔
293

92✔
294
        entityEnvs := make(map[minderv1.Entity]*entityEnvCache, len(factoryMap))
92✔
295
        for entity := range factoryMap {
460✔
296
                entityEnvs[entity] = &entityEnvCache{}
368✔
297
        }
368✔
298

299
        return &Env{
92✔
300
                entityEnvs: entityEnvs,
92✔
301
                factories:  factoryMap,
92✔
302
        }
92✔
303
}
304

305
// NewSelectionFromProfile creates a new Selection (compiled CEL programs for that entity type)
306
// from a profile
307
func (e *Env) NewSelectionFromProfile(
308
        entityType minderv1.Entity,
309
        profileSelection []models.ProfileSelector,
310
) (Selection, error) {
53✔
311
        selector := make([]*compiledSelector, 0, len(profileSelection))
53✔
312

53✔
313
        env, err := e.envForEntity(entityType)
53✔
314
        if err != nil {
53✔
315
                return nil, fmt.Errorf("failed to get environment for entity %v: %w", entityType, err)
×
316
        }
×
317

318
        for _, sel := range profileSelection {
109✔
319
                if sel.Entity != entityType && sel.Entity != minderv1.Entity_ENTITY_UNSPECIFIED {
59✔
320
                        continue
3✔
321
                }
322

323
                compSel, err := compileSelectorForEntity(env, sel.Selector)
53✔
324
                if err != nil {
56✔
325
                        return nil, fmt.Errorf("failed to compile selector %q: %w", sel.Selector, err)
3✔
326
                }
3✔
327

328
                selector = append(selector, compSel)
50✔
329
        }
330

331
        return &EntitySelection{
50✔
332
                env:      env,
50✔
333
                selector: selector,
50✔
334
                entity:   entityType,
50✔
335
        }, nil
50✔
336
}
337

338
// CheckSelector checks if a selector expression compiles and is valid for a given entity
339
func (e *Env) CheckSelector(sel *minderv1.Profile_Selector) error {
10✔
340
        ent := minderv1.EntityFromString(sel.GetEntity())
10✔
341
        if ent == minderv1.Entity_ENTITY_UNSPECIFIED && sel.GetEntity() != "" {
11✔
342
                return fmt.Errorf("invalid entity type %s: %w", sel.GetEntity(), ErrUnsupported)
1✔
343
        }
1✔
344
        env, err := e.envForEntity(ent)
9✔
345
        if err != nil {
9✔
UNCOV
346
                return fmt.Errorf("no environment for entity %v: %w", ent, ErrUnsupported)
×
UNCOV
347
        }
×
348

349
        _, err = checkSelectorForEntity(env, sel.Selector)
9✔
350
        return err
9✔
351
}
352

353
// envForEntity gets the CEL environment for a given entity type. If the environment is not cached,
354
// it creates it using the factory for that entity type.
355
func (e *Env) envForEntity(entity minderv1.Entity) (*cel.Env, error) {
62✔
356
        cache, ok := e.entityEnvs[entity]
62✔
357
        if !ok {
62✔
NEW
358
                // if the entity isn't in the env map, we use the generic environment
×
NEW
359
                cache, ok = e.entityEnvs[minderv1.Entity_ENTITY_UNSPECIFIED]
×
NEW
360
                if !ok {
×
NEW
361
                        return nil, fmt.Errorf("no cache found for entity %v", entity)
×
NEW
362
                }
×
363
        }
364

365
        factory, ok := e.factories[entity]
62✔
366
        if !ok {
62✔
NEW
367
                // if the entity isn't in the factory map, we use the generic environment
×
NEW
368
                factory, ok = e.factories[minderv1.Entity_ENTITY_UNSPECIFIED]
×
NEW
369
                if !ok {
×
NEW
370
                        return nil, fmt.Errorf("no factory found for entity %v", entity)
×
NEW
371
                }
×
372
        }
373
        cache.once.Do(func() {
121✔
374
                cache.env, cache.err = factory()
59✔
375
        })
59✔
376

377
        return cache.env, cache.err
62✔
378
}
379

380
// SelectOption is a functional option for the Select method
381
type SelectOption func(*selectionOptions)
382

383
type selectionOptions struct {
384
        unknownPaths []string
385
}
386

387
// WithUnknownPaths sets the explicit unknown paths for the selection
388
func WithUnknownPaths(paths ...string) SelectOption {
3✔
389
        return func(o *selectionOptions) {
6✔
390
                o.unknownPaths = paths
3✔
391
        }
3✔
392
}
393

394
// Selection is an interface for selecting entities based on a profile
395
type Selection interface {
396
        Select(*internalpb.SelectorEntity, ...SelectOption) (bool, string, error)
397
}
398

399
// EntitySelection is a struct that holds the compiled CEL expressions for a given entity type
400
type EntitySelection struct {
401
        env *cel.Env
402

403
        selector []*compiledSelector
404
        entity   minderv1.Entity
405
}
406

407
// Select return true if the entity matches all the compiled expressions and false otherwise
408
func (s *EntitySelection) Select(se *internalpb.SelectorEntity, userOpts ...SelectOption) (bool, string, error) {
52✔
409
        if se == nil {
52✔
410
                return false, "", fmt.Errorf("input entity is nil")
×
411
        }
×
412

413
        var opts selectionOptions
52✔
414
        for _, opt := range userOpts {
55✔
415
                opt(&opts)
3✔
416
        }
3✔
417

418
        for _, sel := range s.selector {
104✔
419
                entityMap, err := inputAsMap(se)
52✔
420
                if err != nil {
52✔
421
                        return false, "", fmt.Errorf("failed to convert input to map: %w", err)
×
422
                }
×
423

424
                out, details, err := s.evalWithOpts(&opts, sel, entityMap)
52✔
425
                // check unknowns /before/ an error. Maybe we should try to special-case the one
52✔
426
                // error we get from the CEL library in this case and check for the rest?
52✔
427
                if s.detailHasUnknowns(sel, details) {
58✔
428
                        return false, "", ErrResultUnknown
6✔
429
                }
6✔
430

431
                if err != nil {
46✔
432
                        return false, "", fmt.Errorf("failed to evaluate Expression: %w", err)
×
433
                }
×
434

435
                if types.IsUnknown(out) {
46✔
436
                        return false, "", ErrResultUnknown
×
437
                }
×
438

439
                if out.Type() != cel.BoolType {
46✔
440
                        return false, "", fmt.Errorf("expression did not evaluate to a boolean: %v", out)
×
441
                }
×
442

443
                if !out.Value().(bool) {
68✔
444
                        return false, sel.orig, nil
22✔
445
                }
22✔
446
        }
447

448
        return true, "", nil
24✔
449
}
450

451
func unknownAttributesFromOpts(unknownPaths []string) []*interpreter.AttributePattern {
52✔
452
        unknowns := make([]*interpreter.AttributePattern, 0, len(unknownPaths))
52✔
453

52✔
454
        for _, path := range unknownPaths {
55✔
455
                frags := strings.Split(path, ".")
3✔
456
                if len(frags) == 0 {
3✔
457
                        continue
×
458
                }
459

460
                unknownAttr := interpreter.NewAttributePattern(frags[0])
3✔
461
                if len(frags) > 1 {
6✔
462
                        for _, frag := range frags[1:] {
6✔
463
                                unknownAttr = unknownAttr.QualString(frag)
3✔
464
                        }
3✔
465
                }
466
                unknowns = append(unknowns, unknownAttr)
3✔
467
        }
468

469
        return unknowns
52✔
470
}
471

472
func (_ *EntitySelection) evalWithOpts(
473
        opts *selectionOptions, sel *compiledSelector, entityMap map[string]any,
474
) (ref.Val, *cel.EvalDetails, error) {
52✔
475
        unknowns := unknownAttributesFromOpts(opts.unknownPaths)
52✔
476
        if len(unknowns) > 0 {
55✔
477
                partialMap, err := cel.PartialVars(entityMap, unknowns...)
3✔
478
                if err != nil {
3✔
479
                        return types.NewErr("failed to create partial value"), nil, fmt.Errorf("failed to create partial vars: %w", err)
×
480
                }
×
481

482
                return sel.program.Eval(partialMap)
3✔
483
        }
484

485
        return sel.program.Eval(entityMap)
49✔
486
}
487

488
func (s *EntitySelection) detailHasUnknowns(sel *compiledSelector, details *cel.EvalDetails) bool {
52✔
489
        if details == nil {
52✔
490
                return false
×
491
        }
×
492

493
        // TODO(jakub): We should also extract what the unknowns are and return them
494
        // there exists cel.AstToString() which prints the part that was not evaluated, but as a whole
495
        // (e.g. properties['is_fork'] == true) and not as a list of unknowns. We should either take a look
496
        // at its implementation or walk the AST ourselves
497
        residualAst, err := s.env.ResidualAst(sel.ast, details)
52✔
498
        if err != nil {
52✔
499
                return false
×
500
        }
×
501

502
        checked, err := cel.AstToCheckedExpr(residualAst)
52✔
503
        if err != nil {
52✔
504
                return false
×
505
        }
×
506

507
        return checked.GetExpr().GetConstExpr() == nil
52✔
508
}
509

510
func inputAsMap(se *internalpb.SelectorEntity) (map[string]any, error) {
52✔
511
        var value any
52✔
512

52✔
513
        key := se.GetEntityType().ToString()
52✔
514

52✔
515
        // FIXME(jakub): I tried to be smart and code something up using protoreflect and WhichOneOf but didn't
52✔
516
        // make it work. Maybe someone smarter than me can.
52✔
517
        // nolint:exhaustive
52✔
518
        switch se.GetEntityType() {
52✔
519
        case minderv1.Entity_ENTITY_REPOSITORIES:
36✔
520
                value = se.GetRepository()
36✔
521
        case minderv1.Entity_ENTITY_ARTIFACTS:
8✔
522
                value = se.GetArtifact()
8✔
523
        case minderv1.Entity_ENTITY_PULL_REQUESTS:
8✔
524
                value = se.GetPullRequest()
8✔
525
        default:
×
526
                return nil, fmt.Errorf("unsupported entity type [%d]: %s", se.GetEntityType(), se.GetEntityType().ToString())
×
527
        }
528

529
        return map[string]any{
52✔
530
                key:      value,
52✔
531
                "entity": se,
52✔
532
        }, nil
52✔
533
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc