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

Add default content type for Policy load API endpoints #1505

Merged
merged 2 commits into from
Apr 28, 2020

Conversation

micahlee
Copy link
Contributor

@micahlee micahlee commented Apr 23, 2020

What does this PR do?

If no content-type is specified in a POST request, Rack assumes the content
type is either application/x-www-form-urlencoded or
multipart/form-data.

The Conjur policy documents are YAML content, and some special
characters allowed in policy cause the Rack pody parsing to fail, leading to
a Rack error: Invalid or incomplete POST parameters.

This PR adds a Rack middleware that allow default content-types to be
configured on a per request path basis to better control the behavior when
no content-type is provided in the request headers.

What ticket does this PR close?

Connected to conjurinc/dap-support#74

Checklists

Change log

  • The CHANGELOG has been updated, or
  • This PR does not include user-facing changes and doesn't require a CHANGELOG update

Test coverage

  • This PR includes new unit and integration tests to go with the code changes, or
  • The changes in this PR do not require tests

Follow-on issues

  • Follow-up issue(s) have been logged (and links included below) to update documentation or related projects, or
  • No follow-up issues are required

@micahlee micahlee force-pushed the 74-default-content-type branch 6 times, most recently from dd35936 to 1b954c6 Compare April 23, 2020 17:46
To detect issue found by Python 3 client library test suite.
@micahlee micahlee force-pushed the 74-default-content-type branch 4 times, most recently from 043c04d to 3ad9dd7 Compare April 23, 2020 20:12
@micahlee micahlee changed the title WIP: Add default content type for Policy load API endpoints Add default content type for Policy load API endpoints Apr 23, 2020
@micahlee micahlee marked this pull request as ready for review April 23, 2020 20:48
@micahlee micahlee requested a review from a team April 23, 2020 20:51
# rubocop:disable Style/ClassVars
class DefaultContentType
def self.default_content_types
@@default_content_types ||= {}
Copy link
Contributor

Choose a reason for hiding this comment

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

You should always avoid @@. And indeed it should be superfluous here. The receiving object, because of the self. qualifier, is the class itself. So @default_content_types would refer to a variable on the class object, not one of its instances.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting... I hadn't realized there was such a thing as a "class instance variable" distinct from a "class variable." Cool!

private

# :reek:FeatureEnvy
# :reek:UtilityFunction
Copy link
Contributor

Choose a reason for hiding this comment

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

This reek comments look valid and related to my comment above about @@. I'll try post a solution that avoid all this in a few minutes.

Copy link
Contributor

@jonahx jonahx Apr 24, 2020

Choose a reason for hiding this comment

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

Untested:

=begin

Currently we're doing:

  Rails.application.configure do
    config.middleware.use ::Rack::DefaultContentType
  end

Instead we'll do:

  Rails.application.configure do
    config.middleware.use(
      ::Rack::DefaultContentType,
      { %r{^\/policies} => 'application/x-yaml'}
    )
  end

And you can delete:

  def self.path_default_content_type(path_match, content_type)
    ::Rack::DefaultContentType.default_content_types[path_match] = content_type
  end

And replace it with a comment that the configuration is done in
config.application.rb, just so readers know there is some magic
afoot.

=end

module Rack


  # DefaultContentType is a Rack middleware that supports
  # providing a default Content-Type header for requests
  # to specific paths that do not include a Content-Type
  # header in the request.
  #
  # Default content types are configured by adding entries
  # to the Rack::DefaultContentType.default_content_types
  # class member hash.
  #
  # Each hash key must be a regular expression to test
  # request paths for a match. The hash value is the content
  # type string to add to the request.
  class DefaultContentType

    # A Hash-like whose keys are Regex objects. It looks up values by matching
    # keys given to its #[] method against those regexes, and returning the
    # value of the first match.
    class RegexHash
      # Note: this will work with a Hash or keyword args
      def initialize(**kwargs)
        @vals_by_regex = kwargs
      end

      def [](key)
        matching_re = @vals_by_regex.each_key.find { |re, v| re =~ key }
        @vals_by_regex[matching_re] # Note: returns nil if no match
      end
    end

    def initialize(app, content_type_by_path)
      @app = app
      @content_type_by_path = RegexHash.new(content_type_by_path)
    end

    def call(env)
      # Note: Avoid Rails dependency -- don't use `.present?`
      content_type_missing = env['CONTENT_TYPE']&.empty?
      default_content_type = @content_type_by_path[env['PATH_INFO']]

      # If a content type is already present on the request, we
      # don't need to do anything.
      if content_type_missing && default_content_type
        env['CONTENT_TYPE'] = default_content_type
        Rack::Request.new(env).body.rewind
      end

      @app.call(env)
    end
  end

end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Avoid Rails dependency -- don't use .present?

I fine with the change, but we're in a Rails app. Why are we avoiding using it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And replace it with a comment that the configuration is done in
config.application.rb, just so readers know there is some magic
afoot.

I think I get where you're coming from with this suggestion. I still think, however, it's going to be easier to discover, understand, and maintain if the middleware only provides the application-wide capability to set default content types, with the path-specific settings configured closer to the path-specific logic (either the routes or the controller).

A global config with a comment is a weak connection with it comes to understanding what is happening when debugging later.

Copy link
Contributor

Choose a reason for hiding this comment

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

I fine with the change, but we're in a Rails app. Why are we avoiding using it?

Because this is middleware, not part of the Rails app.

Copy link
Contributor

Choose a reason for hiding this comment

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

A global config with a comment is a weak connection with it comes to understanding what is happening when debugging later.

I partially agree, but you're really choosing between lesser evils.

The downside of the solution you merged is that you're using mutation, and allowing the middleware to be mutated at runtime, from anywhere, at any time. This can give rise much trickier bugs than an immutable middleware that is statically configured one time. So yes, the way I originally suggested has the downside that the comment is deleted, and then it's not clear by looking locally in the controller what's happening. But I think this is unlikely to happen, especially if the comment says "Do not delete" :)

@micahlee micahlee force-pushed the 74-default-content-type branch 6 times, most recently from 5055c22 to fb46b88 Compare April 27, 2020 15:12
If no content-type is specified in a POST request, Rack assumes the content
type is either `application/x-www-form-urlencoded` or
`multipart/form-data`.

The Conjur policy documents are YAML content, and some special
characters allowed in policy cause the Rack pody parsing to fail, leading to
a Rack error: `Invalid or incomplete POST parameters`.

This commit adds a Rack middleware that allow default content-types to be
configured on a per request path basis to better control the behavior when
no content-type is provided in the request headers.
@micahlee micahlee force-pushed the 74-default-content-type branch from d5428a2 to d19fc7e Compare April 27, 2020 16:15
@codeclimate
Copy link

codeclimate bot commented Apr 27, 2020

Code Climate has analyzed commit d19fc7e and detected 0 issues on this pull request.

The test coverage on the diff in this pull request is 100.0% (50% is the threshold).

This pull request will bring the total coverage in the repository to 85.4% (0.0% change).

View more on Code Climate.

@micahlee
Copy link
Contributor Author

Thanks @jtuttle and @jonahx!

This is ready for another look when one of you has time again. Thanks!

@micahlee micahlee requested a review from jtuttle April 27, 2020 20:21
@micahlee micahlee merged commit aca93c2 into master Apr 28, 2020
@micahlee micahlee deleted the 74-default-content-type branch April 28, 2020 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants