Skip to content
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

Initial attempt to block request base on user ID #2627

Closed

Conversation

GustavoCaso
Copy link
Member

No description provided.

@github-actions github-actions bot added appsec Application Security monitoring product integrations Involves tracing integrations labels Feb 16, 2023
@GustavoCaso GustavoCaso force-pushed the appsec-throw-request-interrupted-for-blokced-user-id branch from 57d2894 to b1f492f Compare February 16, 2023 16:43
Copy link
Contributor

@lloeki lloeki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's going in a good direction, I feel like I'm missing something though.

Comment on lines 61 to 66
if Datadog::AppSec.settings.enabled
_, appsec_response = AppSec::Instrumentation.gateway.push('identity.set_user', id)
if appsec_response && appsec_response.any? { |action, _event| action == :block }
throw(AppSec::Ext::REQUEST_INTERRUPT, [nil, appsec_response])
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks okay

@@ -56,9 +57,17 @@ def self.set_user(trace, id:, email: nil, name: nil, session_id: nil, role: nil,
others.each do |k, v|
trace.set_tag("usr.#{k}", v) unless v.nil?
end

if ::Datadog::AppSec.settings.enabled
_, appsec_response = ::Datadog::AppSec::Instrumentation.gateway.push('identity.set_user', id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like for e.g Rack request and response gateway points, we should pass a user object here as a hash, or maybe a struct. It can contain just the id for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point of passing an object or a struct, but at the same time, it feels like the early decision to pass an object when we only care about a single attribute of the thing might not be necessary. Delaying that internal decision an internal thing is a good idea.

Also, the Gateway does not validate what the data is when pushed, so in a way, it could be anything, an object, a string...

Copy link
Contributor

@lloeki lloeki Feb 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having it be an object makes it easier to track the expectations and usage from the sender to the receiver. It also encapsulates the extensibility of it

In addition it forces us to "find a place" to define the matching class, which was not the case previously since they were predefined Rack::Request and Rack::Response

... which I was wondering about whether they should be boxed into some gateway-specific object. Something like (very rough, pseudocode, names not exact, jus to give an idea of the hierarchy):

Gateway::Object
Gateway::User < Gateway::Object
Gateway::Request < Gateway::Object
Gateway::Response < Gateway::Object

and then we can type (again, rough pseudocode to get the idea):

  • Gateway#push: (address, Gateway::Object) -> ...
  • Gateway#watch: () { |Gateway::Object| }

Let's try something like that in a subsequent PR.

if ::Datadog::AppSec.settings.enabled
_, appsec_response = ::Datadog::AppSec::Instrumentation.gateway.push('identity.set_user', id)
if appsec_response && appsec_response.any? { |action, _event| action == :block }
throw(AppSec::Ext::REQUEST_INTERRUPT, [nil, appsec_response])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's something I don't quite like about passing this [nil, appsec_response] here, but I can't quite put my finger on it yet. Something to do about breaking encapsulation. I need to step back.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should create a return object rather than an array, which is the rack expected interface

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, also by being an object, it would have a type name, thus make it easier to track the expectations and usage from the sender to the receiver.

I feel this is out of scope for this PR though.

Comment on lines 63 to 67
_, catched_request = catch(Datadog::AppSec::Ext::REQUEST_INTERRUPT) do
ret, res = stack.call(request)
end

next [nil, catched_request] if catched_request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, interesting, I was not quite expecting that, which implements a sort of forwarding. I would have expected that catch to be in RequestMiddleware around @app.call:

request_return, request_response = Instrumentation.gateway.push('rack.request', request) do
@app.call(env)
end

I need to take a step back and think about it a little bit more.

Any reason you put it here instead?

Copy link
Member Author

@GustavoCaso GustavoCaso Feb 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because since the event happen with the request flow, when we throw if we call catch in the RequestMiddleware the code from the watch_request after calling stack.call(request) would continue and the result would be extra wrap the result in an extra array.

We can probably look for other places to have the catch located.

Basically I wanted this code to not be executed:

if event
res ||= []
res << [:monitor, event]
end
[ret, res]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it more, there are a bunch of things:

  • throw Datadog::AppSec::Ext::REQUEST_INTERRUPT: set_user is theoretically and conceptually supposed to be called anytime and possibly not in a HTTP request. This is an argument to not have a name such as REQUEST_INTERRUPT in set_user (where I wanted it to be initially)

  • the above also applies to the fact that the gateway controls the instrumentation and without appsec then there will be no throw by construction since there will be no watcher, just like that, and set_user is completely appsec-free

  • yet I am wondering about:

    I wanted this code to not be executed

    Is that what we want? kind of breaks the isolation of these watcher blocks

In any case I suggested some changes but now I feel this is actually going in the right direction, even though the latter point is kind of left up in the air for when we have more than one watch block.

@GustavoCaso GustavoCaso force-pushed the appsec-pass-user-id-to-waf branch 11 times, most recently from f566616 to 4abbd89 Compare February 23, 2023 11:38
Base automatically changed from appsec-pass-user-id-to-waf to master February 23, 2023 12:05
@GustavoCaso
Copy link
Member Author

Closing in favour of #2642

@GustavoCaso GustavoCaso deleted the appsec-throw-request-interrupted-for-blokced-user-id branch October 11, 2023 11:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
appsec Application Security monitoring product integrations Involves tracing integrations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants