1- module ChatOps
1+ require "chatops"
2+
3+ module Chatops
24 module Controller
5+ class ConfigurationError < StandardError ; end
36 extend ActiveSupport ::Concern
47
58 included do
6- before_action :ensure_chatops_authenticated
9+ with_options if : :should_authenticate_chatops? do |controller |
10+ controller . before_action :ensure_valid_chatops_url
11+ controller . before_action :ensure_valid_chatops_timestamp
12+ controller . before_action :ensure_valid_chatops_signature
13+ controller . before_action :ensure_valid_chatops_nonce
14+ controller . before_action :ensure_chatops_authenticated
15+ end
716 before_action :ensure_user_given
817 before_action :ensure_method_exists
918 end
@@ -16,7 +25,7 @@ def list
1625 help : self . class . chatops_help ,
1726 error_response : self . class . chatops_error_response ,
1827 methods : chatops ,
19- version : "2 " }
28+ version : "3 " }
2029 end
2130
2231 def process ( *args )
@@ -90,21 +99,78 @@ def ensure_user_given
9099 end
91100
92101 def ensure_chatops_authenticated
93- return true unless ( chatop_names + [ :list ] ) . include? ( params [ :action ] . to_sym )
94- authenticated = authenticate_with_http_basic do |u , p |
95- if ENV [ "CHATOPS_AUTH_TOKEN" ] . nil?
96- raise StandardError , "Attempting to authenticate chatops with nil token"
97- end
98- if ENV [ "CHATOPS_ALT_AUTH_TOKEN" ] . nil?
99- raise StandardError , "Attempting to authenticate chatops with nil alternate token"
100- end
102+ body = request . raw_post || ""
103+ signature_string = [ @chatops_url , @chatops_nonce , @chatops_timestamp , body ] . join ( "\n " )
104+ # We return this just to aid client debugging.
105+ response . headers [ "Chatops-Signature-String" ] = signature_string
106+ raise ConfigurationError . new ( "You need to add a client's public key in .pem format via #{ Chatops . public_key_env_var_name } " ) unless Chatops . public_key . present?
107+ if signature_valid? ( Chatops . public_key , @chatops_signature , signature_string ) ||
108+ signature_valid? ( Chatops . alt_public_key , @chatops_signature , signature_string )
109+ return true
110+ end
111+ return render :status => :forbidden , :plain => "Not authorized"
112+ end
101113
102- Rack ::Utils . secure_compare ( ENV [ "CHATOPS_AUTH_TOKEN" ] , p ) ||
103- Rack ::Utils . secure_compare ( ENV [ "CHATOPS_ALT_AUTH_TOKEN" ] , p )
114+ def ensure_valid_chatops_url
115+ unless Chatops . auth_base_url . present?
116+ raise ConfigurationError . new ( "You need to set the server's base URL to authenticate chatops RPC via #{ Chatops . auth_base_url_env_var_name } " )
104117 end
105- unless authenticated
106- render :status => :forbidden , :plain => "Not authorized"
118+ if Chatops . auth_base_url [ - 1 ] == "/"
119+ raise ConfigurationError . new ( "Don't include a trailing slash in #{ Chatops . auth_base_url_env_var_name } ; the rails path will be appended and it must match exactly." )
107120 end
121+ @chatops_url = Chatops . auth_base_url + request . path
122+ end
123+
124+ def ensure_valid_chatops_nonce
125+ @chatops_nonce = request . headers [ "Chatops-Nonce" ]
126+ return render :status => :forbidden , :plain => "A Chatops-Nonce header is required" unless @chatops_nonce . present?
127+ end
128+
129+ def ensure_valid_chatops_signature
130+ signature_header = request . headers [ "Chatops-Signature" ]
131+
132+ begin
133+ # "Chatops-Signature: Signature keyid=foo,signature=abc123" => { "keyid"" => "foo", "signature" => "abc123" }
134+ signature_items = signature_header . split ( " " , 2 ) [ 1 ] . split ( "," ) . map { |item | item . split ( "=" , 2 ) } . to_h
135+ @chatops_signature = signature_items [ "signature" ]
136+ rescue NoMethodError
137+ # The signature header munging, if something's amiss, can produce a `nil` that raises a
138+ # no method error. We'll just carry on; the nil signature will raise below
139+ end
140+
141+ unless @chatops_signature . present?
142+ return render :status => :forbidden , :plain => "Failed to parse signature header"
143+ end
144+ end
145+
146+ def ensure_valid_chatops_timestamp
147+ @chatops_timestamp = request . headers [ "Chatops-Timestamp" ]
148+ time = Time . iso8601 ( @chatops_timestamp )
149+ if !( time > 1 . minute . ago && time < 1 . minute . from_now )
150+ return render :status => :forbidden , :plain => "Chatops timestamp not within 1 minute of server time: #{ @chatops_timestamp } vs #{ Time . now . utc . iso8601 } "
151+ end
152+ rescue ArgumentError , TypeError
153+ # time parsing or missing can raise these
154+ return render :status => :forbidden , :plain => "Invalid Chatops-Timestamp: #{ @chatops_timestamp } "
155+ end
156+
157+ def request_is_chatop?
158+ ( chatop_names + [ :list ] ) . include? ( params [ :action ] . to_sym )
159+ end
160+
161+ def chatops_test_auth?
162+ Rails . env . test? && request . env [ "CHATOPS_TESTING_AUTH" ]
163+ end
164+
165+ def should_authenticate_chatops?
166+ request_is_chatop? && !chatops_test_auth?
167+ end
168+
169+ def signature_valid? ( key_string , signature , signature_string )
170+ digest = OpenSSL ::Digest ::SHA256 . new
171+ decoded_signature = Base64 . decode64 ( signature )
172+ public_key = OpenSSL ::PKey ::RSA . new ( key_string )
173+ public_key . verify ( digest , decoded_signature , signature_string )
108174 end
109175
110176 def ensure_method_exists
0 commit comments