From 696df3e0122900fc95c0355d09c7b15fc7be7ee1 Mon Sep 17 00:00:00 2001 From: Daniel DeLeo Date: Tue, 31 Jul 2012 17:13:40 -0700 Subject: change default sign version back to 1.0 We need to wait until Chef 11, when we can make a break to change to 1.1, because this relies on the server side supporting 1.1 to work correctly. --- lib/mixlib/authentication/signedheaderauth.rb | 137 +++++++++++++------- .../authentication/mixlib_authentication_spec.rb | 140 +++++++++++---------- 2 files changed, 165 insertions(+), 112 deletions(-) diff --git a/lib/mixlib/authentication/signedheaderauth.rb b/lib/mixlib/authentication/signedheaderauth.rb index 39c9ac8..4ee7f98 100644 --- a/lib/mixlib/authentication/signedheaderauth.rb +++ b/lib/mixlib/authentication/signedheaderauth.rb @@ -7,9 +7,9 @@ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -28,62 +28,99 @@ module Mixlib module SignedHeaderAuth - SUPPORTED_ALGORITHMS = ['sha1'] - SUPPORTED_VERSIONS = ['1.0', '1.1'] + NULL_ARG = Object.new + SUPPORTED_ALGORITHMS = ['sha1'].freeze + SUPPORTED_VERSIONS = ['1.0', '1.1'].freeze - # This is a module meant to be mixed in but can be used standalone - # with the simple OpenStruct extended with the auth functions - class << self - def signing_object(args={ }) - SigningObject.new(args[:http_method], args[:path], args[:body], args[:host], args[:timestamp], args[:user_id], args[:file]) - end + DEFAULT_SIGN_ALGORITHM = 'sha1'.freeze + DEFAULT_PROTO_VERSION = '1.0'.freeze + + + # === signing_object + # This is the intended interface for signing requests with the + # Opscode/Chef signed header protocol. This wraps the constructor for a + # Struct that contains the relevant information about your request. + # + # ==== Signature Parameters: + # These parameters are used to generate the canonical representation of + # the request, which is then hashed and encrypted to generate the + # request's signature. These options are all required, with the exception + # of `:body` and `:file`, which are alternate ways to specify the request + # body (you must specify one of these). + # * `:http_method`: HTTP method as a lowercase symbol, e.g., `:get | :put | :post | :delete` + # * `:path`: The path part of the URI, e.g., `URI.parse(uri).path` + # * `:body`: An object representing the body of the request. + # Use an empty String for bodiless requests. + # * `:timestamp`: A String representing the time in any format understood + # by `Time.parse`. The server may reject the request if the timestamp is + # not close to the server's current time. + # * `:user_id`: The user or client name. This is used by the server to + # lookup the public key necessary to verify the signature. + # * `:file`: An IO object (must respond to `:read`) to be used as the + # request body. + # ==== Protocol Versioning Parameters: + # * `:proto_version`: The version of the signing protocol to use. + # Currently defaults to 1.0, but version 1.1 is also available. + # ==== Other Parameters: + # These parameters are accepted but not used in the computation of the signature. + # * `:host`: The host part of the URI + def self.signing_object(args={ }) + SigningObject.new(args[:http_method], args[:path], args[:body], args[:host], args[:timestamp], args[:user_id], args[:file], args[:proto_version]) + end + + def algorithm + DEFAULT_SIGN_ALGORITHM + end + + def proto_version + DEFAULT_PROTO_VERSION end # Build the canonicalized request based on the method, other headers, etc. # compute the signature from the request, using the looked-up user secret # ====Parameters # private_key:: user's RSA private key. - def sign(private_key, algorithm='sha1', version='1.1') + def sign(private_key, sign_algorithm=DEFAULT_SIGN_ALGORITHM, sign_version=DEFAULT_PROTO_VERSION) # Our multiline hash for authorization will be encoded in multiple header # lines - X-Ops-Authorization-1, ... (starts at 1, not 0!) header_hash = { - "X-Ops-Sign" => "algorithm=#{algorithm};version=#{version};", + "X-Ops-Sign" => "algorithm=#{sign_algorithm};version=#{sign_version};", "X-Ops-Userid" => user_id, "X-Ops-Timestamp" => canonical_time, "X-Ops-Content-Hash" => hashed_body, } - string_to_sign = canonicalize_request(algorithm, version) + string_to_sign = canonicalize_request(sign_algorithm, sign_version) signature = Base64.encode64(private_key.private_encrypt(string_to_sign)).chomp signature_lines = signature.split(/\n/) signature_lines.each_index do |idx| key = "X-Ops-Authorization-#{idx + 1}" header_hash[key] = signature_lines[idx] end - + Mixlib::Authentication::Log.debug "String to sign: '#{string_to_sign}'\nHeader hash: #{header_hash.inspect}" - + header_hash end - + # Build the canonicalized time based on utc & iso8601 - # + # # ====Parameters - # + # def canonical_time Time.parse(timestamp).utc.iso8601 end - + # Build the canonicalized path, which collapses multiple slashes (/) and # removes a trailing slash unless the path is only "/" - # + # # ====Parameters - # + # def canonical_path p = path.gsub(/\/+/,'/') p.length > 1 ? p.chomp('/') : p end - + def hashed_body # Hash the file object if it was passed in, otherwise hash based on # the body. @@ -92,27 +129,33 @@ module Mixlib # body in the case of a file being include. @hashed_body ||= (self.file && self.file.respond_to?(:read)) ? digester.hash_file(self.file) : digester.hash_string(self.body) end - + # Takes HTTP request method & headers and creates a canonical form # to create the signature - # + # # ====Parameters - # - # - def canonicalize_request(algorithm='sha1', version='1.1') - raise AuthenticationError, "Bad algorithm '#{algorithm}' or version '#{version}'" unless SUPPORTED_ALGORITHMS.include?(algorithm) && SUPPORTED_VERSIONS.include?(version) - - canonical_x_ops_user_id = case - when version == "1.1" - digester.hash_string(user_id) - when version == "1.0" - user_id - else - user_id - end + # + # + def canonicalize_request(algorithm=DEFAULT_SIGN_ALGORITHM, version=DEFAULT_PROTO_VERSION) + unless SUPPORTED_ALGORITHMS.include?(algorithm) && SUPPORTED_VERSIONS.include?(version) + raise AuthenticationError, "Bad algorithm '#{algorithm}' (allowed: #{SUPPORTED_ALGORITHMS.inspect}) or version '#{version}' (allowed: #{SUPPORTED_VERSIONS.inspect})" + end + + canonical_x_ops_user_id = canonicalize_user_id(user_id, version) "Method:#{http_method.to_s.upcase}\nHashed Path:#{digester.hash_string(canonical_path)}\nX-Ops-Content-Hash:#{hashed_body}\nX-Ops-Timestamp:#{canonical_time}\nX-Ops-UserId:#{canonical_x_ops_user_id}" end - + + def canonicalize_user_id(user_id, proto_version) + case proto_version + when "1.1" + digester.hash_string(user_id) + when "1.0" + user_id + else + user_id + end + end + # Parses signature version information, algorithm used, etc. # # ====Parameters @@ -126,17 +169,25 @@ module Mixlib Mixlib::Authentication::Log.debug "Parsed signing description: #{parts.inspect}" parts end - + def digester Mixlib::Authentication::Digester end - - private :canonical_time, :canonical_path, :parse_signing_description, :digester - + + private :canonical_time, :canonical_path, :parse_signing_description, :digester, :canonicalize_user_id + end - class SigningObject < Struct.new(:http_method, :path, :body, :host, :timestamp, :user_id, :file) + # === SigningObject + # A Struct-based value object that contains the necessary information to + # generate a request signature. `SignedHeaderAuth.signing_object()` + # provides a more convenient interface to the constructor. + class SigningObject < Struct.new(:http_method, :path, :body, :host, :timestamp, :user_id, :file, :proto_version) include SignedHeaderAuth + + def proto_version + self[:proto_version] or DEFAULT_PROTO_VERSION + end end end diff --git a/spec/mixlib/authentication/mixlib_authentication_spec.rb b/spec/mixlib/authentication/mixlib_authentication_spec.rb index a7d27c8..2103304 100644 --- a/spec/mixlib/authentication/mixlib_authentication_spec.rb +++ b/spec/mixlib/authentication/mixlib_authentication_spec.rb @@ -8,9 +8,9 @@ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -68,26 +68,28 @@ end describe "Mixlib::Authentication::SignedHeaderAuth" do - it "should generate the correct string to sign and signature, version 1.0" do - - algorithm = 'sha1' - version = '1.0' - V1_0_SIGNING_OBJECT.canonicalize_request(algorithm, version).should == V1_0_CANONICAL_REQUEST + # NOTE: Version 1.0 will be the default until Chef 11 is released. + + it "should generate the correct string to sign and signature, version 1.0 (default)" do + + V1_0_SIGNING_OBJECT.canonicalize_request.should == V1_0_CANONICAL_REQUEST # If you need to regenerate the constants in this test spec, print out - # the results of res.inspect and copy them as appropriate into the + # the results of res.inspect and copy them as appropriate into the # the constants in this file. - V1_0_SIGNING_OBJECT.sign(PRIVATE_KEY, algorithm, version).should == EXPECTED_SIGN_RESULT_V1_0 + V1_0_SIGNING_OBJECT.sign(PRIVATE_KEY).should == EXPECTED_SIGN_RESULT_V1_0 end it "should generate the correct string to sign and signature, version 1.1" do + algorithm = 'sha1' + version = '1.1' - V1_1_SIGNING_OBJECT.canonicalize_request.should == V1_1_CANONICAL_REQUEST + V1_1_SIGNING_OBJECT.canonicalize_request(algorithm, version).should == V1_1_CANONICAL_REQUEST # If you need to regenerate the constants in this test spec, print out - # the results of res.inspect and copy them as appropriate into the + # the results of res.inspect and copy them as appropriate into the # the constants in this file. - V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY).should == EXPECTED_SIGN_RESULT_V1_1 + V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, algorithm, version).should == EXPECTED_SIGN_RESULT_V1_1 end it "should not choke when signing a request for a long user id with version 1.1" do @@ -109,7 +111,7 @@ describe "Mixlib::Authentication::SignedHeaderAuth" do end describe "Mixlib::Authentication::SignatureVerification" do - + before(:each) do @user_private_key = PRIVATE_KEY end @@ -231,7 +233,7 @@ PATH = "/organizations/clownco" HASHED_CANONICAL_PATH = "YtBWDn1blGGuFIuKksdwXzHU9oE=" # Base64.encode64(Digest::SHA1.digest("/organizations/clownco")).chomp V1_0_ARGS = { - :body => BODY, + :body => BODY, :user_id => USER_ID, :http_method => :post, :timestamp => TIMESTAMP_ISO8601, # fixed timestamp so we get back the same answer each time. @@ -240,7 +242,7 @@ V1_0_ARGS = { } V1_1_ARGS = { - :body => BODY, + :body => BODY, :user_id => USER_ID, :http_method => :post, :timestamp => TIMESTAMP_ISO8601, # fixed timestamp so we get back the same answer each time. @@ -249,7 +251,7 @@ V1_1_ARGS = { } LONG_PATH_LONG_USER_ARGS = { - :body => BODY, + :body => BODY, :user_id => "A" * 200, :http_method => :put, :timestamp => TIMESTAMP_ISO8601, # fixed timestamp so we get back the same answer each time. @@ -263,10 +265,10 @@ REQUESTING_ACTOR_ID = "c0f8a68c52bffa1020222a56b23cccfa" X_OPS_CONTENT_HASH = "DFteJZPVv6WKdQmMqZUQUumUyRs=" X_OPS_AUTHORIZATION_LINES_V1_0 = [ "jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4", -"NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc", -"3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O", -"IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy", -"9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0", +"NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc", +"3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O", +"IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy", +"9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0", "utju9jzczCyB+sSAQWrxSsXB/b8vV2qs0l4VD2ML+w==" ] @@ -311,51 +313,51 @@ EXPECTED_SIGN_RESULT_V1_1 = { OTHER_HEADERS = { # An arbitrary sampling of non-HTTP_* headers are in here to # exercise that code path. - "REMOTE_ADDR"=>"127.0.0.1", - "PATH_INFO"=>"/organizations/local-test-org/cookbooks", - "REQUEST_PATH"=>"/organizations/local-test-org/cookbooks", + "REMOTE_ADDR"=>"127.0.0.1", + "PATH_INFO"=>"/organizations/local-test-org/cookbooks", + "REQUEST_PATH"=>"/organizations/local-test-org/cookbooks", "CONTENT_TYPE"=>"multipart/form-data; boundary=----RubyMultipartClient6792ZZZZZ", - "CONTENT_LENGTH"=>"394", + "CONTENT_LENGTH"=>"394", } # This is what will be in request.params for the Merb case. MERB_REQUEST_PARAMS = { - "name"=>"zsh", "action"=>"create", "controller"=>"chef_server_api/cookbooks", + "name"=>"zsh", "action"=>"create", "controller"=>"chef_server_api/cookbooks", "organization_id"=>"local-test-org", "requesting_actor_id"=>REQUESTING_ACTOR_ID, } # Tis is what will be in request.env for the Merb case. MERB_HEADERS_V1_1 = { # These are used by signatureverification. - "HTTP_HOST"=>"127.0.0.1", + "HTTP_HOST"=>"127.0.0.1", "HTTP_X_OPS_SIGN"=>"algorithm=sha1;version=1.1;", - "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", - "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, - "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, - "HTTP_X_OPS_USERID"=>USER_ID, - "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES[0], - "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES[1], - "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES[2], - "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES[3], - "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES[4], - "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES[5], + "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", + "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, + "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, + "HTTP_X_OPS_USERID"=>USER_ID, + "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES[0], + "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES[1], + "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES[2], + "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES[3], + "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES[4], + "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES[5], }.merge(OTHER_HEADERS) # Tis is what will be in request.env for the Merb case. MERB_HEADERS_V1_0 = { # These are used by signatureverification. - "HTTP_HOST"=>"127.0.0.1", + "HTTP_HOST"=>"127.0.0.1", "HTTP_X_OPS_SIGN"=>"version=1.0", - "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", - "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, - "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, - "HTTP_X_OPS_USERID"=>USER_ID, - "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES_V1_0[0], - "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES_V1_0[1], - "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES_V1_0[2], - "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES_V1_0[3], - "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES_V1_0[4], - "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES_V1_0[5], + "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", + "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, + "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, + "HTTP_X_OPS_USERID"=>USER_ID, + "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES_V1_0[0], + "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES_V1_0[1], + "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES_V1_0[2], + "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES_V1_0[3], + "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES_V1_0[4], + "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES_V1_0[5], }.merge(OTHER_HEADERS) PASSENGER_REQUEST_PARAMS = { @@ -367,34 +369,34 @@ PASSENGER_REQUEST_PARAMS = { PASSENGER_HEADERS_V1_1 = { # These are used by signatureverification. - "HTTP_HOST"=>"127.0.0.1", + "HTTP_HOST"=>"127.0.0.1", "HTTP_X_OPS_SIGN"=>"algorithm=sha1;version=1.1;", - "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", - "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, - "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, - "HTTP_X_OPS_USERID"=>USER_ID, - "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES[0], - "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES[1], - "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES[2], - "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES[3], - "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES[4], - "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES[5], + "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", + "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, + "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, + "HTTP_X_OPS_USERID"=>USER_ID, + "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES[0], + "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES[1], + "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES[2], + "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES[3], + "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES[4], + "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES[5], }.merge(OTHER_HEADERS) PASSENGER_HEADERS_V1_0 = { # These are used by signatureverification. - "HTTP_HOST"=>"127.0.0.1", + "HTTP_HOST"=>"127.0.0.1", "HTTP_X_OPS_SIGN"=>"version=1.0", - "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", - "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, - "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, - "HTTP_X_OPS_USERID"=>USER_ID, - "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES_V1_0[0], - "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES_V1_0[1], - "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES_V1_0[2], - "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES_V1_0[3], - "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES_V1_0[4], - "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES_V1_0[5], + "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", + "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, + "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, + "HTTP_X_OPS_USERID"=>USER_ID, + "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES_V1_0[0], + "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES_V1_0[1], + "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES_V1_0[2], + "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES_V1_0[3], + "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES_V1_0[4], + "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES_V1_0[5], }.merge(OTHER_HEADERS) # generated with -- cgit v1.2.1