-
Notifications
You must be signed in to change notification settings - Fork 55
OSAC-3609: restrict public filters to public object fields #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e6d5ae0
e98cec7
74d5d70
20f426f
76f9336
d1d40c5
7de24f4
aac4eac
63fd108
c45a260
107c8fc
8df30f8
fcc922f
d797ab7
5fee0e0
03136ea
d6df36d
8d03424
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,7 @@ type GenericDAOBuilder[O Object] struct { | |
| eventCallbacks []EventCallback | ||
| tenancyLogic auth.TenancyLogic | ||
| metricsRegisterer prometheus.Registerer | ||
| filterDesc protoreflect.MessageDescriptor | ||
| } | ||
|
|
||
| // GenericDAO provides generic data access operations for protocol buffers messages. It assumes that objects will be | ||
|
|
@@ -79,7 +80,7 @@ type GenericDAO[O Object] struct { | |
| jsonEncoder *json.Encoder | ||
| marshalOptions protojson.MarshalOptions | ||
| unmarshalOptions protojson.UnmarshalOptions | ||
| filterTranslator *FilterTranslator[O] | ||
| filterTranslator *FilterTranslator | ||
| tenancyLogic auth.TenancyLogic | ||
|
|
||
| // Metrics: | ||
|
|
@@ -189,6 +190,14 @@ func (b *GenericDAOBuilder[O]) SetTableName(value string) *GenericDAOBuilder[O] | |
| return b | ||
| } | ||
|
|
||
| // SetFilterDesc sets the protobuf message descriptor used to validate and translate CEL filter expressions. This is | ||
| // optional. When unset, the descriptor of the O generic parameter is used. Pass a different descriptor to restrict | ||
| // which fields clients may reference in filters — public servers over private storage pass the public descriptor. | ||
| func (b *GenericDAOBuilder[O]) SetFilterDesc(value protoreflect.MessageDescriptor) *GenericDAOBuilder[O] { | ||
| b.filterDesc = value | ||
| return b | ||
| } | ||
|
|
||
| // Build creates a new generic DAO using the configuration stored in the builder. | ||
| func (b *GenericDAOBuilder[O]) Build() (result *GenericDAO[O], err error) { | ||
| // Check parameters: | ||
|
|
@@ -271,9 +280,15 @@ func (b *GenericDAOBuilder[O]) Build() (result *GenericDAO[O], err error) { | |
| DiscardUnknown: true, | ||
| } | ||
|
|
||
| // Create the filter translator: | ||
| filterTranslator, err := NewFilterTranslator[O](). | ||
| // Create the filter translator. The filter descriptor defaults to the object's own descriptor, but callers | ||
| // may override it via SetFilterDesc to restrict which fields are visible to filter expressions. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] fail-open When GenericDAOBuilder.filterDesc is nil (the default), Build() falls back to the private object descriptor. A public server that forgets to call SetFilterDesc silently uses the private descriptor and exposes private fields through filters. Mitigated by the reflection-driven regression test in register_servers_test.go. |
||
| filterDesc := b.filterDesc | ||
| if filterDesc == nil { | ||
| filterDesc = objectDesc | ||
| } | ||
| filterTranslator, err := NewFilterTranslator(). | ||
| SetLogger(b.logger). | ||
| SetDescriptor(filterDesc). | ||
| Build() | ||
| if err != nil { | ||
| err = fmt.Errorf("failed to create filter translator: %w", err) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,7 @@ func (r *ListRequest[O]) do(ctx context.Context) (response *ListResponse[O], err | |
| var filter string | ||
| filter, err = r.dao.filterTranslator.Translate(ctx, r.filter) | ||
| if err != nil { | ||
| err = &ErrInvalidFilter{Reason: err.Error()} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] error-wrapping-idiom ErrInvalidFilter is constructed via string-flattening (err.Error()) rather than error-chain wrapping. This discards the original error chain and differs from the DAO's convention for other typed errors. |
||
| return | ||
| } | ||
| if r.sql.filter.Len() > 0 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[low] defense in depth
When SetFilterDesc is not called (filterDesc is nil), the fallback is objectDesc -- the private proto descriptor. No compile-time enforcement ensures public-facing servers call SetFilterDesc. A future resource registered outside RegisterResourceServers without a manual test would silently use the private descriptor.
Suggested fix: Consider making SetFilterDesc mandatory (no nil fallback) so all servers must explicitly set it.