Conversation
# Conflicts: # lib/chatops/controller/test_case_helpers.rb
|
Far enough along for real eyes. |
jbarnette
left a comment
There was a problem hiding this comment.
Various nitpicks inline. I dig it, let me know when I should port kube-me to V3.
lib/chatops/controller.rb
Outdated
| end | ||
| end | ||
|
|
||
| def invalid_time |
There was a problem hiding this comment.
You could pass timestamp in here to avoid duplicating the header access.
lib/chatops/controller.rb
Outdated
| nonce = request.headers['Chatops-Nonce'] | ||
| timestamp = request.headers['Chatops-Timestamp'] | ||
| begin | ||
| time = Time.parse(timestamp) |
There was a problem hiding this comment.
How bout Time.iso8601(timestamp)?
| begin | ||
| signature_items = signature_header.split(" ", 2)[1].split(",").map { |item| item.split("=", 2) }.to_h | ||
| signature = signature_items["signature"] | ||
| rescue NoMethodError |
There was a problem hiding this comment.
I added a comment. It's guarding sundry possible nils in the transformation above.
lib/chatops/controller.rb
Outdated
| decoded_signature = Base64.decode64(signature) | ||
| digest = OpenSSL::Digest::SHA256.new | ||
| raise ConfigurationError.new("You need to add a client's public key in .pem format via CHATOPS_AUTH_PUBLIC_KEY") unless ENV["CHATOPS_AUTH_PUBLIC_KEY"].present? | ||
| if ENV["CHATOPS_AUTH_PUBLIC_KEY"].present? |
There was a problem hiding this comment.
This will always be true.
lib/chatops/controller.rb
Outdated
| digest = OpenSSL::Digest::SHA256.new | ||
| raise ConfigurationError.new("You need to add a client's public key in .pem format via CHATOPS_AUTH_PUBLIC_KEY") unless ENV["CHATOPS_AUTH_PUBLIC_KEY"].present? | ||
| if ENV["CHATOPS_AUTH_PUBLIC_KEY"].present? | ||
| public_key = OpenSSL::PKey::RSA.new(ENV["CHATOPS_AUTH_PUBLIC_KEY"]) |
There was a problem hiding this comment.
How bout a local for ENV["CHATOPS_AUTH_PUBLIC_KEY"]? You're using it in a few places.
lib/chatops/controller.rb
Outdated
| authenticated = public_key.verify(digest, decoded_signature, signature_string) | ||
| end | ||
| if !authenticated && ENV["CHATOPS_AUTH_ALT_PUBLIC_KEY"].present? | ||
| public_key = OpenSSL::PKey::RSA.new(ENV["CHATOPS_AUTH_ALT_PUBLIC_KEY"]) |
There was a problem hiding this comment.
Same 💭 about a local for these ENV accesses, no big deal though.
| expect(response.status).to eq 403 | ||
| end | ||
|
|
||
| it "doesn't allow requests more than 1 minute old" do |
There was a problem hiding this comment.
Is it worth testing the future too?
lib/chatops/controller.rb
Outdated
| response.headers['Chatops-SignatureString'] = signature_string | ||
| decoded_signature = Base64.decode64(signature) | ||
| digest = OpenSSL::Digest::SHA256.new | ||
| raise ConfigurationError.new("You need to add a client's public key in .pem format via CHATOPS_AUTH_PUBLIC_KEY") unless ENV["CHATOPS_AUTH_PUBLIC_KEY"].present? |
There was a problem hiding this comment.
Maybe wrap this into a block like this (and also assign it to a var like John suggests)?
begin
public_key = ENV.fetch("CHATOPS_AUTH_PUBLIC_KEY")
rescue KeyError
raise ConfigurationError.new("You need to add a client's public key in .pem format via CHATOPS_AUTH_PUBLIC_KEY")
end
spec/lib/chatops/controller_spec.rb
Outdated
|
|
||
| describe ActionController::Base, type: :controller do | ||
| controller do | ||
| controller do |
There was a problem hiding this comment.
Did you mean to tweak the whitespace here?
|
Yalls excellent comments resulted in a huge refactoring to something more rails-reasonable. Ready for 👀 again. |
lib/chatops/controller.rb
Outdated
| before_action :ensure_valid_chatops_timestamp, if: :should_authenticate_chatops? | ||
| before_action :ensure_valid_chatops_signature, if: :should_authenticate_chatops? | ||
| before_action :ensure_valid_chatops_nonce, if: :should_authenticate_chatops? | ||
| before_action :ensure_chatops_authenticated, if: :should_authenticate_chatops? |
There was a problem hiding this comment.
⛳️
with_options if: :should_authenticate_chatops? do
before_action :ensure_valid_chatops_url
before_action :ensure_valid_chatops_timestamp
before_action :ensure_valid_chatops_signature
before_action :ensure_valid_chatops_nonce
before_action :ensure_chatops_authenticated
end
lib/chatops/controller.rb
Outdated
| signature_string = [@chatops_url, @chatops_nonce, @chatops_timestamp, body].join("\n") | ||
| # We return this just url client debugging. | ||
| response.headers['Chatops-SignatureString'] = signature_string | ||
| public_key = ENV["CHATOPS_AUTH_PUBLIC_KEY"] |
There was a problem hiding this comment.
"I know, I'll use single quotes on odd-numbered lines and double quotes on even lines!"
There was a problem hiding this comment.
"I know, I'll use single quotes on odd-numbered lines and double quotes on even lines!"
It was more like "I know a great way to get @jbarnette riled up!"
lib/chatops/controller.rb
Outdated
| response.headers['Chatops-SignatureString'] = signature_string | ||
| public_key = ENV["CHATOPS_AUTH_PUBLIC_KEY"] | ||
| alt_public_key = ENV["CHATOPS_AUTH_ALT_PUBLIC_KEY"] | ||
| raise ConfigurationError.new("You need to add a client's public key in .pem format via CHATOPS_AUTH_PUBLIC_KEY") unless public_key.present? |
There was a problem hiding this comment.
I think these are fine inline, but they seem like the kind of thing we'd normally break out to a Chatops.auth_public_key accessor etc.
lib/chatops/controller.rb
Outdated
| def ensure_valid_chatops_url | ||
| base_url = ENV["CHATOPS_AUTH_BASE_URL"] | ||
| raise ConfigurationError.new("You need to set the server's base URL to authenticate chatops RPC via CHATOPS_AUTH_BASE_URL") unless base_url.present? | ||
| @chatops_url = base_url + request.path |
There was a problem hiding this comment.
Any need to care about trailing /?
There was a problem hiding this comment.
Any need to care about trailing /?
Sorta. I added another linter here.
That sort of thing is why the Chatops-Signature-String to so what we signed; hopefully that will make such errors easier to find.
lib/chatops/controller.rb
Outdated
| signature_header = request.headers['Chatops-Signature'] | ||
|
|
||
| begin | ||
| # 'Chatops-Signatre: Signature keyid=foo,signature=abc123' => { "keyid"" => "foo", "signature" => "abc123" } |
| @chatops_signature = signature_items["signature"] | ||
| rescue NoMethodError | ||
| # The signature header munging, if something's amiss, can produce a `nil` that raises a | ||
| # no method error. We'll just carry on; the nil signature will raise below |
Wouldn't want error messages to go out of date
|
All feedback adopted. Feeling kinda ashamed yall had so much to find. Thanks again. |
lib/chatops.rb
Outdated
| @@ -0,0 +1,21 @@ | |||
| module ChatOps | |||
There was a problem hiding this comment.
This should be Chatops. Or the file should be chat_ops.rb. Actually let's talk about class and gem names for a sec since they're inconsistent + this is going to be public soon. If it's gonna stick to RubyGems conventions (underscores separate words, dashes separate namespaces), one of these first two approaches would be good:
| gem | sample path | constant |
|---|---|---|
chatops-controller |
lib/chatops/controller.rb |
Chatops::Controller |
chat_ops-controller |
lib/chat_ops/controller.rb |
ChatOps::Controller |
These are not great options:
| gem | sample path | constant |
|---|---|---|
chatops_controller |
lib/chatops_controller.rb |
ChatopsController |
chat_ops_controller |
lib/chat_ops_controller.rb |
ChatopsController |
Myself, I prefer the first example, because underscores in project names are kinda ugly and mixing em with dashes doubly so. But you could've guessed that. 😁
Good call. I will go ahead and wrap that into this pull since it's version 3.0. |
Without the arg, this seems to work only in some versions of rails
|
Okay, the gem's renamed. One more time, this time with feeling! |
|
Awesome awesome thanks again. Looking good. Next PR is the README cleanup this needs before going open-source. |
keithduncan
left a comment
There was a problem hiding this comment.
This is so good and the tests look great too
| end | ||
|
|
||
| def self.alt_public_key | ||
| ENV["CHATOPS_AUTH_ALT_PUBLIC_KEY"] |
There was a problem hiding this comment.
We actually don’t need the main v alt split under v3 with public key auth, because the key used to sign is self identified with the keyid param 🤔
We can have a relation of keyid => publickey hosting many live keys. Rolling keys with this scheme would be much simpler than the multiple deploys to populate and move keys from alt to main.
I think it would be worth baking that concept into the gem from the beginning. We can have a default backend that looks the key up in the ENV, but otherwise the notion of supporting multiple and even per-user keys is pressed from the beginning.
| # We return this just to aid client debugging. | ||
| response.headers["Chatops-Signature-String"] = signature_string | ||
| 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? | ||
| if signature_valid?(Chatops.public_key, @chatops_signature, signature_string) || |
There was a problem hiding this comment.
As above I think the key to verify with should be hinted on the incoming keyid field.
| unless authenticated | ||
| render :status => :forbidden, :plain => "Not authorized" | ||
| if Chatops.auth_base_url[-1] == "/" | ||
| 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.") |
There was a problem hiding this comment.
💯 error handling here.
What do you think about verifying this when included so that you get the error on boot rather than on first request?
There was a problem hiding this comment.
If we can detect that the last character is slash, and we don't want slashes, would it be a better user experience to just remove the trailing slash? That would reduce the back and forth a little bit.
| # "Chatops-Signature: Signature keyid=foo,signature=abc123" => { "keyid"" => "foo", "signature" => "abc123" } | ||
| signature_items = signature_header.split(" ", 2)[1].split(",").map { |item| item.split("=", 2) }.to_h | ||
| @chatops_signature = signature_items["signature"] | ||
| rescue NoMethodError |
There was a problem hiding this comment.
Could we mandate Ruby 2.3 and use the safe nil navigation operator?
There was a problem hiding this comment.
Might be worth surverying the existing CRPC commands to see if anything is < 2.3.
It would also be possible to use try on these since ActiveSupport should be around.
| def ensure_valid_chatops_timestamp | ||
| @chatops_timestamp = request.headers["Chatops-Timestamp"] | ||
| time = Time.iso8601(@chatops_timestamp) | ||
| if !(time > 1.minute.ago && time < 1.minute.from_now) |
There was a problem hiding this comment.
Super minor but if ! can be swapped for unless
| end | ||
|
|
||
| def request_is_chatop? | ||
| (chatop_names + [:list]).include?(params[:action].to_sym) |
There was a problem hiding this comment.
It’s worth requiring at least Ruby 2.2.1 for the symbols GC if we String#to_sym user data
technicalpickles
left a comment
There was a problem hiding this comment.
Sorry I'm late to the review party.
Aside from line-level comments, it looks like the README.md wasn't updated to talk about setting up private/public keys at all.
| unless authenticated | ||
| render :status => :forbidden, :plain => "Not authorized" | ||
| if Chatops.auth_base_url[-1] == "/" | ||
| 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.") |
There was a problem hiding this comment.
If we can detect that the last character is slash, and we don't want slashes, would it be a better user experience to just remove the trailing slash? That would reduce the back and forth a little bit.
| # "Chatops-Signature: Signature keyid=foo,signature=abc123" => { "keyid"" => "foo", "signature" => "abc123" } | ||
| signature_items = signature_header.split(" ", 2)[1].split(",").map { |item| item.split("=", 2) }.to_h | ||
| @chatops_signature = signature_items["signature"] | ||
| rescue NoMethodError |
There was a problem hiding this comment.
Might be worth surverying the existing CRPC commands to see if anything is < 2.3.
It would also be possible to use try on these since ActiveSupport should be around.
|
|
||
| begin | ||
| # "Chatops-Signature: Signature keyid=foo,signature=abc123" => { "keyid"" => "foo", "signature" => "abc123" } | ||
| signature_items = signature_header.split(" ", 2)[1].split(",").map { |item| item.split("=", 2) }.to_h |
There was a problem hiding this comment.
TIL split with a limit
|
|
||
| begin | ||
| # "Chatops-Signature: Signature keyid=foo,signature=abc123" => { "keyid"" => "foo", "signature" => "abc123" } | ||
| signature_items = signature_header.split(" ", 2)[1].split(",").map { |item| item.split("=", 2) }.to_h |
There was a problem hiding this comment.
This is an impressive one-liner, but I am wondering if it'd be easier to grok broken out on multiple lines. That might be a lot in the controller, so it could be logical to have it in some utility method.
|
|
||
| def ensure_valid_chatops_timestamp | ||
| @chatops_timestamp = request.headers["Chatops-Timestamp"] | ||
| time = Time.iso8601(@chatops_timestamp) |
There was a problem hiding this comment.
You can also do @chatops_timestamp.to_time
Implements https://github.com/github/hubot-classic/pull/2444