Support the JWT params set by Workhorse in upload requests
What does this MR do?
This MR is related to #214785 (closed) and more broadly to #213288 (closed).
🌲 Context
When a supported file upload request goes through workhorse, here is what happens (simplified):
(Disable Dark Mode if you want to read this diagram properly
sequenceDiagram
autonumber
participant C as Client
participant W as Workhorse
participant R as Rails
C->>W: File Upload
W->>R: /authorize
R->>W: Response
W->>R: File Upload
R->>W: Response
W->>C: Response
When Workhorse receives (3), it will create a set of parameters (such as name
, path
, size
, sha256
and friends) and pass them in (4) to Rails.
🔥 Problem
Currently, Workhorse will pass these parameters as a request parameters to rails. When Rails reads them, it can't say if these parameters have been set by workhorse or by a smart (
🚒 Solution
To fix the situation, gitlab-workhorse!490 (merged) has been created: in addition to current parameters, Workhorse will now set a specific parameter (one per file in the file upload request) for request (4). This specific parameter is set to a hash containing all the upload parameters and more importantly it is signed by JWT using the secret that Workhorse and Rails share. This way, rails can verify the signature and if someone has been tampering them, it will reject the request. Note that parameters are merely signed. In particular, they are not encrypted.
This MR is the rails part of the above modification:
- It updates
multipart.rb
so that upload parameters are extracted from the corresponding JWT parameter. The signature is verified while doing so.
🛠 Design choices
- This change is global for all uploads as we're updating the middleware piece in charge of handling the uploads.
- To have an additional safety net, this change is gated behind a feature flag:
upload_middleware_jwt_params_handler
- This allows us to quickly switch between reading the JWT parameter or reading the params in the request.
- The corresponding tracking issue is here: #233895 (closed)
- To increase the confidence in this change, we choose to implement features test for 8 different uploads scenarios (read more about them here: #214785 (closed)).
- Those tests were added in a prior MR: !39941 (merged)
- Having feature tests gives us the chance to properly test the whole chain on file uploads as an embedded workhorse server runs during those tests. We will use these to properly test the whole request
🏓 between Workhorse and Rails (requests from (2) to (5) in the schema above). - In addition, running workhorse within the version set in
GITLAB_WORKHORSE_VERSION
can prevent some🤦 moments such as this one: #225259 (comment 384929110).
- Note that this MR is quite big (~700 lines changed) but the bulk of the changes are the changes/additions on the test side.
⚙ Implementation
- You can see that
multipart.rb
has an internal class (calledHandler
) to handle the request. - To ease this refactoring, we introduced
HandlerForJWTParams
which inherits directly fromHandler
. This wayHandlerForJWTParams
only have to override the interesting methods.- This is also to ease the complexity of the feature flag removal MR: we will just need to replace
Handler
functions byHandlerForJWTParams
overrides.🚀
- This is also to ease the complexity of the feature flag removal MR: we will just need to replace
- At runtime, given the state of the feature flag, the proper class is selected
🔬 Tests
-
multipart_spec.rb
has been renamed tomultipart_with_handler_spec.rb
-
multipart_with_handler_for_jwt_params_spec.rb
has been created. It's almost an exact copy ofmultipart_with_handler_spec
with additional tests for the JWT signed parameter.- Why duplicate the test code instead of using shared examples. Both handlers have a slightly different behavior and those differences are not easy to parameterize in shared examples.
- In the examples, we use
#expect_next_instance_of
with the Handler class. So we would have to pass the handler class to the shared examples. - In short, having shared examples would force us to support different parameters to configure them properly. This could be confusing.
- To not add more confusion on top of the existing complex spec, we choose to duplicate the specs.
- Notice, that this will help us for the feature flag removal MR: just delete the
multipart_with_handler_spec.rb
file and renamemultipart_with_handler_for_jwt_params_spec.rb
tomultipart_spec.rb
.🚀
- Notice, that this will help us for the feature flag removal MR: just delete the
- The above approach is also applied to
spec/lib/gitlab/middleware/multipart/handler_for_jwt_params_spec.rb
🧪 Manual Tests
In addition to the automatic feature tests, we conducted some manual tests. Note that the following object storage conditions have been tested:
-
Object Storage off
: Used by self-managed -
Object Storage on / Direct Upload off
: Used by self-managed -
Object Storage on / Direct Upload on
: Used by gitlab.com
With the :upload_middleware_jwt_params_handler
) disabled:
Test | Object Storage off | Object Storage on / Direct Upload off | Object Storage on / Direct Upload on |
---|---|---|---|
nuget package |
|
||
maven package |
|
||
graphql | |||
CI artifact | |||
user avatar | |||
group import | |||
project import | |||
git LFS |
With the :upload_middleware_jwt_params_handler
) enabled:
Test | Object Storage off | Object Storage on / Direct Upload off | Object Storage on / Direct Upload on |
---|---|---|---|
nuget package |
|
||
maven package |
|
||
graphql | |||
CI artifact | |||
user avatar | |||
group import | |||
project import | |||
git LFS |
Please note #209074 make some tests fail under specific conditions. The failure is unrelated with the changes of this MR.
🔮 Possible follow ups
- Running feature tests with an inline workhorse is a good step but for file uploads there is a missing piece. This tests run under the condition
object storage disabled
and this is not the configuration of gitlab.com. We could improve the situation by having a new rspec type:object_storage
that would start a minio server. Being a go project, I don't see any complex obstacle here as the setup would be similar to what we have for the embedded workhorse server.- The above would give use the flexibility to run the upload feature tests under two contexts:
with object storage disabled
andwith object storage enabled
. This would increase further our confidence in the upload middleware code.
- The above would give use the flexibility to run the upload feature tests under two contexts:
Screenshots
Not available.
Does this MR meet the acceptance criteria?
Conformity
-
Changelog entry - [-] Documentation (if required)
-
Code review guidelines -
Merge request performance guidelines - [-] Style guides
- [-] Database guides
- [-] Separation of EE specific content
Availability and Testing
- [-] Review and add/update tests for this feature/bug. Consider all test levels. See the Test Planning Process.
- [-] Tested in all supported browsers
- [-] Informed Infrastructure department of a default or new setting change, if applicable per definition of done
Security
If this MR contains changes to processing or storing of credentials or tokens, authorization and authentication methods and other items described in the security review guidelines:
- [-] Label as security and @ mention
@gitlab-com/gl-security/appsec
- [-] The MR includes necessary changes to maintain consistency between UI, API, email, or other methods
- [-] Security reports checked/validated by a reviewer from the AppSec team