├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .ruby-version ├── Brewfile ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── chatops-controller.gemspec ├── docs ├── example │ ├── crcp_client_key.txt │ └── crpc_server_key.txt ├── protocol-description.md └── why.md ├── lib ├── chatops-controller.rb ├── chatops.rb └── chatops │ ├── controller.rb │ └── controller │ ├── rspec.rb │ ├── test_case.rb │ ├── test_case_helpers.rb │ └── version.rb ├── script ├── bootstrap ├── cibuild └── test ├── spec ├── dummy │ ├── .rspec │ ├── README.rdoc │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── javascripts │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ └── concerns │ │ │ │ └── .keep │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── mailers │ │ │ └── .keep │ │ ├── models │ │ │ ├── .keep │ │ │ └── concerns │ │ │ │ └── .keep │ │ └── views │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── bin │ │ ├── bundle │ │ ├── rails │ │ ├── rake │ │ └── setup │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── assets.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── cookies_serializer.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── routes.rb │ │ └── secrets.yml │ ├── db │ │ ├── development.sqlite3 │ │ ├── schema.rb │ │ └── test.sqlite3 │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── log │ │ └── .keep │ └── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ └── favicon.ico ├── lib │ └── chatops │ │ └── controller_spec.rb ├── rails_helper.rb ├── spec_helper.rb └── support │ └── json_response.rb └── vendor └── cache ├── actioncable-6.1.1.gem ├── actionmailbox-6.1.1.gem ├── actionmailer-6.1.1.gem ├── actionpack-6.1.1.gem ├── actiontext-6.1.1.gem ├── actionview-6.1.1.gem ├── activejob-6.1.1.gem ├── activemodel-6.1.1.gem ├── activerecord-6.1.1.gem ├── activestorage-6.1.1.gem ├── activesupport-6.1.1.gem ├── builder-3.2.4.gem ├── coderay-1.1.3.gem ├── concurrent-ruby-1.1.8.gem ├── crass-1.0.6.gem ├── diff-lcs-1.4.4.gem ├── erubi-1.10.0.gem ├── globalid-0.4.2.gem ├── i18n-1.8.7.gem ├── loofah-2.9.0.gem ├── mail-2.7.1.gem ├── marcel-0.3.3.gem ├── method_source-1.0.0.gem ├── mimemagic-0.3.5.gem ├── mini_mime-1.0.2.gem ├── mini_portile2-2.5.0.gem ├── minitest-5.14.3.gem ├── nio4r-2.5.4.gem ├── nokogiri-1.11.1-x86_64-darwin.gem ├── pry-0.13.1.gem ├── racc-1.5.2.gem ├── rack-2.2.3.gem ├── rack-test-1.1.0.gem ├── rails-6.1.1.gem ├── rails-dom-testing-2.0.3.gem ├── rails-html-sanitizer-1.3.0.gem ├── railties-6.1.1.gem ├── rake-13.0.3.gem ├── rspec-core-3.9.3.gem ├── rspec-expectations-3.9.4.gem ├── rspec-mocks-3.9.1.gem ├── rspec-rails-3.9.1.gem ├── rspec-support-3.9.4.gem ├── sprockets-4.0.2.gem ├── sprockets-rails-3.2.2.gem ├── thor-1.1.0.gem ├── tzinfo-2.0.4.gem ├── websocket-driver-0.7.3.gem ├── websocket-extensions-0.1.5.gem └── zeitwerk-2.4.2.gem /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Ruby 2.7.2 13 | uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: 2.7.2 16 | - name: Build and test 17 | run: | 18 | bundle install --binstubs 19 | bin/rspec 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /bin/ 11 | /vendor/gems 12 | /spec/dummy/log/ 13 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.1 2 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | # Helpers 2 | tap "github/bootstrap" 3 | 4 | # Ruby 5 | brew "autoconf" 6 | brew "openssl" 7 | brew "rbenv" 8 | brew "ruby-build" 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at bhuga@github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: https://github.com/github/chatops-controller/fork 4 | [pr]: https://github.com/github/chatops-controller/compare 5 | [style]: https://github.com/styleguide/ruby 6 | [code-of-conduct]: CODE_OF_CONDUCT.md 7 | 8 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 9 | 10 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. 11 | 12 | ## Submitting a pull request 13 | 14 | 0. [Fork][fork] and clone the repository 15 | 0. Configure and install the dependencies: `script/bootstrap` 16 | 0. Make sure the tests pass on your machine: `rake` 17 | 0. Create a new branch: `git checkout -b my-branch-name` 18 | 0. Make your change, add tests, and make sure the tests still pass 19 | 0. Push to your fork and [submit a pull request][pr] 20 | 0. Pat your self on the back and wait for your pull request to be reviewed and merged. 21 | 22 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 23 | 24 | - Follow the [style guide][style]. 25 | - Write tests. 26 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 27 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 28 | 29 | ## Resources 30 | 31 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 32 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 33 | - [GitHub Help](https://help.github.com) 34 | 35 | ## Releasing 36 | 37 | If you are the current maintainer of this gem: 38 | 39 | * Create a branch for the release: `git checkout -b cut-release-vxx.xx.xx` 40 | * Ensure that tests are green: `script/test` 41 | * Bump gem version in `lib/chatops/controller/version.rb` 42 | * Make a PR to github/chatops-controller and merge it 43 | * Build a local gem: `gem build chatops-controller.gemspec` 44 | * Tag and push: `git tag vx.xx.xx; git push --tags` 45 | * Push to rubygems.org -- `gem push chatops-controller-x.x.x.gem` 46 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | group :development, :test do 4 | gem "rails", "~> 6" 5 | gem "rspec-rails", "~> 3" 6 | gem "pry", "~> 0" 7 | end 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 miguelff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chatops Controller 2 | 3 | Rails helpers for easy and well-tested Chatops RPC. See the [protocol docs](docs/protocol-description.md) 4 | for background information on Chatops RPC. 5 | 6 | A minimal controller example: 7 | 8 | ```ruby 9 | class ChatopsController < ApplicationController 10 | include ::Chatops::Controller 11 | 12 | # The default chatops RPC prefix. Clients may replace this. 13 | chatops_namespace :echo 14 | 15 | chatop :echo, 16 | /(?.*)?/, 17 | " - Echo some text back" do 18 | jsonrpc_success "Echoing back to you: #{jsonrpc_params[:text]}" 19 | end 20 | end 21 | ``` 22 | 23 | Some routing boilerplate is required in `config/routes.rb`: 24 | 25 | ```ruby 26 | Rails.application.routes.draw do 27 | # Replace the controller: argument with your controller's name 28 | post "/_chatops/:chatop", controller: "chatops", action: :execute_chatop 29 | get "/_chatops" => "chatops#list" 30 | end 31 | ``` 32 | 33 | It's easy to test: 34 | 35 | ```ruby 36 | class MyControllerTestCase < ActionController::TestCase 37 | include Chatops::Controller::TestCaseHelpers 38 | before do 39 | chatops_prefix "echo" 40 | chatops_auth! 41 | end 42 | 43 | def test_it_works 44 | chat "echo foo bar baz" 45 | assert_equal "foo bar baz", chatop_response 46 | end 47 | end 48 | ``` 49 | 50 | Before you deploy, add the RPC authentication tokens to your app's environment, 51 | below. 52 | 53 | You're all done. Try `.echo foo`, and you should see your client respond with 54 | `Echoing back to you: foo`. 55 | 56 | A hubot client implementation is available at 57 | 58 | 59 | ## Usage 60 | 61 | #### Namespaces 62 | 63 | Every chatops controller has a namespace. All commands associated with this 64 | controller will be displayed with `.` in chat. The namespace is a 65 | default chatops RPC prefix and may be overridden by a client. 66 | 67 | ``` 68 | chatops_namespace :foo 69 | ``` 70 | 71 | #### Creating Chatops 72 | 73 | Creating a chatop is a DSL: 74 | 75 | ```ruby 76 | chatop :echo, 77 | /(?.*)?/, 78 | " - Echo some text back" do 79 | jsonrpc_success "Echoing back to you: #{jsonrpc_params[:text]}" 80 | end 81 | ``` 82 | 83 | In this example, we've created a chatop called `echo`. The next argument is a 84 | regular expression with [named 85 | captures](http://ruby-doc.org/core-1.9.3/Regexp.html#method-i-named_captures). 86 | In this example, only one capture group is available, `text`. 87 | 88 | The next line is a string, which is a single line of help that will be displayed 89 | in chat for `.echo`. 90 | 91 | The DSL takes a block, which is the code that will run when the chat robot sees 92 | this regex. Arguments will be available in the `params` hash. `params[:user]` 93 | and `params[:room_id]` are special, and will be set by the client. `user` will 94 | always be the login of the user typing the command, and `room_id` will be where 95 | it was typed. 96 | The optional `mention_slug` parameter will provide the name to use to refer to 97 | the user when sending a message; this may or may not be the same thing as the 98 | username, depending on the chat system being used. The optional `message_id` parameter will provide a reference to the message that invoked the rpc. 99 | 100 | You can return `jsonrpc_success` with a string to return text to chat. If you 101 | have an input validation or other handle-able error, you can use 102 | `jsonrpc_failure` to send a helpful error message. 103 | 104 | Chatops are regular old rails controller actions, and you can use niceties like 105 | `before_action` and friends. `before_action :echo, :load_user` for the above 106 | case would call `load_user` before running `echo`. 107 | 108 | ## Authentication 109 | 110 | Authentication uses the Chatops v3 public key signing protocol. You'll need 111 | two environment variables to use this protocol: 112 | 113 | `CHATOPS_AUTH_PUBLIC_KEY` is the public key of your chatops client in PEM 114 | format. This environment variable will be the contents of a `.pub` file, 115 | newlines and all. 116 | 117 | `CHATOPS_AUTH_BASE_URL` is the base URLs of your servers as the chatops client 118 | sees it. This is specified as an environment variable since rails will trust 119 | client headers about a forwarded hostname. For example, if your chatops client 120 | has added the url `https://example.com/_chatops`, you'd set this to 121 | `https://example.com`. You can specify more than one base url divided by comma, 122 | e.g. `https://example.com,https://example2.com` 123 | 124 | You can also optionally set `CHATOPS_AUTH_ALT_PUBLIC_KEY` to a second public key 125 | which will be accepted. This is helpful when rolling keys. 126 | 127 | ## Rails compatibility 128 | 129 | This gem is intended to work with rails 6.x and 7.x. If you find a version 130 | with a problem, please report it in an issue. 131 | 132 | ## Development 133 | 134 | Changes are welcome. Getting started: 135 | 136 | ``` 137 | script/bootstrap 138 | script/test 139 | ``` 140 | 141 | See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution instructions. 142 | 143 | ## Upgrading from early versions 144 | 145 | Early versions of RPC chatops had two major changes: 146 | 147 | ##### Using Rails' dynamic `:action` routing, which was deprecated in Rails 5. 148 | 149 | To work around this, you need to update your router boilerplate: 150 | 151 | This: 152 | 153 | ```ruby 154 | post "/_chatops/:action", controller: "chatops" 155 | ``` 156 | 157 | Becomes this: 158 | 159 | ```ruby 160 | post "/_chatops/:chatop", controller: "chatops" action: :execute_chatop 161 | ``` 162 | 163 | ##### Adding a prefix 164 | 165 | Version 2 of the Chatops RPC protocol assumes a unique prefix for each endpoint. This decision was made for several reasons: 166 | 167 | * The previous suffix-based system creates semantic ambiguities with keyword arguments 168 | * Prefixes allow big improvements to `.help` 169 | * Prefixes make regex-clobbering impossible 170 | 171 | To upgrade to version 2, upgrade to version 2.x of this gem. To migrate: 172 | 173 | * Migrate your chatops to remove any prefixes you have: 174 | 175 | ```ruby 176 | chatop :foo, "help", /ci build whatever/, do "yay" end 177 | ``` 178 | 179 | Becomes: 180 | 181 | ```ruby 182 | chatop :foo, "help", /build whatever/, do "yay" end 183 | ``` 184 | 185 | * Update your tests: 186 | 187 | ```ruby 188 | chat "ci build foobar" 189 | ``` 190 | 191 | Becomes: 192 | 193 | ```ruby 194 | chat "build foobar" 195 | # or 196 | chatops_prefix "ci" 197 | chat "ci build foobar" 198 | ``` 199 | 200 | ##### Using public key authentication 201 | 202 | Previous versions used a `CHATOPS_ALT_AUTH_TOKEN` as a shared secret. This form 203 | of authentication was deprecated and the public key form used above is now 204 | used instead. 205 | 206 | ### License 207 | 208 | MIT. See the accompanying LICENSE file. 209 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'ChatopsController' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | Bundler::GemHelper.install_tasks 18 | 19 | require 'rake/testtask' 20 | 21 | Rake::TestTask.new(:test) do |t| 22 | t.libs << 'lib' 23 | t.libs << 'test' 24 | t.pattern = 'test/**/*_test.rb' 25 | t.verbose = false 26 | end 27 | 28 | task default: :test 29 | -------------------------------------------------------------------------------- /chatops-controller.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "chatops/controller/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "chatops-controller" 9 | s.version = ChatopsController::VERSION 10 | s.authors = ["Ben Lavender", "Misty De Meo", "GitHub"] 11 | s.homepage = "https://github.com/github/chatops-controller" 12 | s.email = ["opensource+chatops-controller@github.com"] 13 | s.license = "MIT" 14 | s.summary = %q{Rails helpers to create JSON-RPC chatops} 15 | s.description = %q{See the README for documentation} 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "README.md"] 18 | s.test_files = Dir["spec/**/*"] 19 | 20 | s.add_dependency "rails" 21 | s.add_dependency "actionpack", ">= 6.0" 22 | s.add_dependency "activesupport", ">= 6.0" 23 | 24 | s.add_development_dependency "rspec-rails", "~> 3" 25 | s.add_development_dependency "pry", "~> 0" 26 | end 27 | -------------------------------------------------------------------------------- /docs/example/crcp_client_key.txt: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAuAr7aMOLW08UrP8gWPzfP5+GF1ov3I2Wn1n7AsDxez36+hyU 3 | AY7skZzd7+Nz/j8HT10NRSMA/lkc49FaJlNQ2wAsvsF53AwtTna+FkR1e1P66+Ph 4 | KB9NeoCf5TCvVvihR1VI/BS4AAOdYLCr8NuX89I74qK2WYSziHzTzxbMMZldgXO+ 5 | GDlKdZXNzCrk4Kj0ger2bap9/amUXRuuqOVSLZJ3l0mYRWu45TG9tlF8jmix48+2 6 | e1pzhbw1qezD7vB8lsGH2qaKQrs1Em1tZHRgxbrslbDbmD+q3Oq0CpnFNp7eIoAx 7 | XcrLPyw/IlplCLM0hxouPRlQ/yEhfnLnjnbhHQIDAQABAoIBAQCigPfqUjcbYaFE 8 | +2SJjoZlPCr/NZ/rI43qmF3d2ZWfl2OjMlaxZYyXKiaBKZoC1Y5T0jrkX/sBmpe6 9 | xODP1GMhbG8V4+oAlTPwA0LmkH4Xblixrp3henpD/4yvpyQ7K//j53cxNe4d/RMa 10 | DAVV/9+U533/KGLQei64BlhTk7Kq21ndm+sEReVrAGqUfxJBB1jmKYKskAFU+7zW 11 | 0tTveS7zbo7uTEdfrcDzh2eU70BR3qi2RsjBZtHm2mdVk8y8TnqpgvEautNwo5pM 12 | idNgLOozsWWH/mEHZG43OsJjwJjmeeZnd4rp/RdMWdCv4/VInTUNa6ln1koQR1f3 13 | psg2ifxVAoGBAPD6ar87DdEM8rC1QjHwDdYNkB9P2KPizhs+/NrTGNCtiCUm+RHr 14 | YDwYYW3FnVgW6ejAmPXsg+U9OBeAiV9TpclyaEBsmP2M9L1xbdAlJ3YJXcVLDBEP 15 | Vc/d3MW3BFx6dqTeoI4SsjkWl/hzYlRaA92kSFzNj4nxcXLigXyg3UfvAoGBAMOD 16 | +qnlnrOVfyK2c3PU+u7UHRQT+TUxn2IwuXsF8/Bf5Mvs1GvqGvx2/N2BN34RlMLg 17 | wnyNaBj6xdtQ6T8nMGQIfjLPti9apFdgtZNa92sSUFzmLS5ZRSQGFGnpTrPuyRnI 18 | Y3Fm+cHGnFBxqMLJnZ05Te606UuUL3iJDvBw7ruzAoGAIPbQhWpJoJA53qxc6sHg 19 | 0qg2T+I3S2vqL9X09uYrndgvKI3lQmtFVdMr+L0woe04gCtggTuia0htlOFzaUPj 20 | COSKmE2CvCR9EjEjCXcbp8zuM9/pPagwX+gEnFNF2HS0KCeAJQ8vrBmIHmeCSvGp 21 | V7dyYqeH/CG4GDQd7HOA8acCgYBOg5uPyqQ2ndxWRkqKw4aZjhi3TWYQVIMa3VI+ 22 | 8x8I8plgwxRy2apIpEfbc96jA9BnifbQKcEZ9uqprg5czBIEudxj70HMNmw0oqOI 23 | L0mYd9xJ0i1mpXa8hqx/868lVsjvT6ePjLjTdjyjmWEaB/kBgFepeoENVs7RasjT 24 | Cab1PQKBgD7syNO54fsV81N7Ko+ERdUz5Y59VLE6IC2hSiG0e9p6n1PrMtwLpsCd 25 | lcLv/nw+QvoPdK3mOmD+vhoejQ8i4uv2+Ns/LDbkGnap3YKlgIdwvWPT0wUt8ixN 26 | aayIKvORDVMbhYljbD4LDWfm/sBrKAvdeM2XjqeJvyPcstDmQ2aN 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /docs/example/crpc_server_key.txt: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuAr7aMOLW08UrP8gWPzf 3 | P5+GF1ov3I2Wn1n7AsDxez36+hyUAY7skZzd7+Nz/j8HT10NRSMA/lkc49FaJlNQ 4 | 2wAsvsF53AwtTna+FkR1e1P66+PhKB9NeoCf5TCvVvihR1VI/BS4AAOdYLCr8NuX 5 | 89I74qK2WYSziHzTzxbMMZldgXO+GDlKdZXNzCrk4Kj0ger2bap9/amUXRuuqOVS 6 | LZJ3l0mYRWu45TG9tlF8jmix48+2e1pzhbw1qezD7vB8lsGH2qaKQrs1Em1tZHRg 7 | xbrslbDbmD+q3Oq0CpnFNp7eIoAxXcrLPyw/IlplCLM0hxouPRlQ/yEhfnLnjnbh 8 | HQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /docs/protocol-description.md: -------------------------------------------------------------------------------- 1 | ## Chatops RPC Protocol 2 | 3 | CRPC is a client-server protocol; Hubot is a client. Servers expose an endpoint 4 | listing available methods. Each endpoint provides a regex to fire on and a 5 | relative URL path to execute it. CRPC is distilled from several years' 6 | experience with Chatops; see [the why](why.md) for some background. 7 | 8 | Chatops RPC pushes a lot of complexity to clients. This is a design decision, 9 | intended to keep the burden of creating new chat commands in existing systems 10 | as low as possible. 11 | 12 | ## Listing Commands 13 | 14 | A CRPC service listing is an endpoint that exposes JSON including the following 15 | fields: 16 | 17 | * `namespace`: A globally unique namespace for these commands. Clients can use this to uniquely identify this endpoint. A namespace should be a slug of the form `/[a-Z0-9\-_]+/`. 18 | * `help`: **Optional:** Overall help for this namespace, if a client chooses to provide help 19 | * `error_response`: **Optional:** A message to present when this endpoint returns an error. This can direct users to next steps when the server fails. 20 | * `methods`: A mapping of named operations to their metadata. 21 | * `version`: The version of ChatOps RPC protocol to use, currently version 3 22 | 23 | Each key in the `methods` hash will be a string name. Each name should be a 24 | slug of the form `/[a-Z0-9]\-_]+/`. Clients can use these method names to uniquely 25 | identify methods within a namespace. Each name shall point to an object with the 26 | following fields: 27 | 28 | * `regex`: A string regular expression source used to execute the command. This regular expression should use named capture groups of the form `(?.+)`. 29 | * `path`: A path, relative to the listing URL, to execute the command. 30 | * `params`: A list of available named parameters for this command. 31 | * `help`: **Optional:** User help for a given command. 32 | 33 | Each server is assumed to be given a prefix, which the client will handle 34 | prepending to a command's regex source. Clients can use the `namespace` as a 35 | default prefix if they wish, but servers may not demand a particular prefix. 36 | Chatops RPC clients should require whitespace after the prefix, so a command with a 37 | regex like `/ping/` with a prefix of `test` would match on `test ping`. 38 | 39 | ## Executing Commands 40 | 41 | CRPC clients use the listings to create a listing of available commands. When a 42 | chat message matches a command's regex matcher, the CRPC client creates a method 43 | invocation. A method invocation is a JSON object with the following fields: 44 | 45 | * `user`: A slug username corresponding to to the command giver's GitHub login. 46 | * `mention_slug`: Optional. If provided, a string which should be used to mention the user when sending a message in response. For example, Slack requires that users be mentioned using user IDs instead of usernames. 47 | * `message_id`: Optional. If provided, an id that uniquely identifies the message that generated the CRPC call. Useful for linking back to the original command to provide context. 48 | * `room_id`: A slug room name where the command originated. 49 | * `method`: The method name, without namespace, of the matching regex. 50 | * `params`: A mapping of parameter names to matches extracted from named capture groups in the command's regex. Parameters that are empty or null should not be passed. 51 | 52 | The JSON object is posted to the `path` associated with the command from the 53 | listing of commands. CRPC servers should assume that parameters in the `params` 54 | hash are under user control, but trust that the `user` and `room_id` to be 55 | correct. 56 | 57 | CRPC servers must produce a response JSON object with the following fields: 58 | 59 | * `result`: A string to be displayed in the originating chat room. 60 | 61 | CRPC may optionally include the following fields in a response JSON object for 62 | use in situations where richer results can be displayed. Clients will optionally 63 | utilize some or all of the extra information to provide an enhanced response, 64 | but it is important that `result` be sufficient on its own. 65 | 66 | * `title`: The title text for the response 67 | * `title_link`: Optional URL to link the title text to 68 | * `color`: Hex color for the message, to indicate status/group e.g. "ddeeaa' 69 | * `buttons`: An array of button objects 70 | * `label`: The text to display on the button 71 | * `image_url`: An image URL to display as the button, will generally take precedence 72 | * `command`: The command to use when the button is clicked 73 | * `image_url`: An image URL to be included with the response 74 | * `attachment`: Optional boolean which hints the recipient to format the message as an attachment, if supported by its protocol. Because this is a hint, it may be ignored by clients. If not specified, it defaults to false. 75 | 76 | CRPC may also produce error JSON according to the JSON-RPC spec, consisting of 77 | an object containing an `error` object with a `message` string. This is 78 | sometimes helpful for clients that make a distinction between failed and 79 | successful commands, such as a terminal. CRPC point of view. CRPC clients should 80 | still parse these error messages. 81 | 82 | ## Examples 83 | 84 | Here is an end-to-end transaction, sans authentication (see below): 85 | 86 | CRPC client issues: 87 | ``` 88 | GET /_chatops HTTP/1.1 89 | Accept: application/json 90 | 91 | { 92 | "namespace": "deploy", 93 | "help": null, 94 | "version": 3, 95 | "error_response": "The server had an unexpected error. More information is perhaps available in the [error tracker](https://example.com)", 96 | "methods": { 97 | "options": { 98 | "help": "hubot deploy options - List available environments for ", 99 | "regex": "options(?: (?\\S+))?", 100 | "params": [ 101 | "app" 102 | ], 103 | "path": "wcid" 104 | } 105 | } 106 | } 107 | ``` 108 | 109 | The client will use the suggested `namespace` as a prefix, `deploy`. Thus, when 110 | the client receives a command matching `.deploy options hubot`, the CRPC client 111 | issues: 112 | 113 | ``` 114 | POST /_chatops/wcid HTTP/1.1 115 | Accept: application/json 116 | Content-type: application/json 117 | Content-length: 77 118 | 119 | {"user":"bhuga","method":"options","params":{"app": "hubot"},"room_id":"developer-experience"} 120 | ``` 121 | 122 | The CRPC server should respond with output like the following: 123 | 124 | ``` 125 | {"result":"Hubot is unlocked in production, you're free to deploy.\nHubot is unlocked in staging, you're free to deploy.\n"} 126 | ``` 127 | 128 | The CRPC client should output "Hubot is unlocked in production, you're free to 129 | deploy.\nHubot is unlocked in staging, you're free to deploy.\n" to the chat 130 | room. The client can optionally display the output intelligently if it contains 131 | newlines, links in formats like markdown, etc. It's strongly recommended that 132 | a client support markdown links if possible. 133 | 134 | ## Authentication 135 | 136 | #### Authenticating clients 137 | 138 | Clients authenticate themselves to servers by signing requests with RS256 139 | using a private key. Servers have a public key associated with clients and 140 | verify the signature with it. 141 | 142 | By convention, a CRPC server should allow authentication with two secrets 143 | simultaneously to allow seamless token rolling. 144 | 145 | Clients send three additional HTTP headers for authentication: `Chatops-Nonce`, 146 | `Chatops-timestamp`, and `Chatops-Signature`. 147 | 148 | * `Chatops-Nonce`: A random, base64-encoded string unique to every chatops 149 | request. Servers can cache seen nonces and refuse to execute them a second time. 150 | * `Chatops-Timestamp`: An ISO 8601 time signature in UTC, such as 151 | `2017-05-11T19:15:23Z`. 152 | * `Chatops-Signature`: The signature for this request. 153 | 154 | The value to be signed is formed by concatenating the value of the full http path, 155 | followed by a newline character, followed by the contents of the nonce 156 | header, followed by a newline character, followed by the value of the timestamp header, 157 | followed by a newline character, followed by the entire HTTP post body, if any. For example, 158 | for a `GET` request with these headers: 159 | 160 | ``` 161 | Chatops-Nonce: abc123 162 | Chatops-Timestamp: 2017-05-11T19:15:23Z 163 | ``` 164 | 165 | Sent to the following URL: 166 | 167 | `https://example.com/_chatops` 168 | 169 | The string to be signed is: 170 | `https://example.com/_chatops\nabc123\n2017-05-11T19:15:23Z\n` 171 | 172 | For a request with the same headers and a POST body of `{"method": "foo"}`, the 173 | string to be signed is: 174 | 175 | `https://example.com/_chatops\nabc123\n2017-05-11T19:15:23Z\n{"method": "foo"}` 176 | 177 | The signature header starts with the word `Signature`, followed by whitespace, 178 | followed by comma-separated key-value pairs separated by an `=`. Keys must be 179 | all lowercase. 180 | 181 | * `keyid`: An implementation-specific key identifier that servers can use to 182 | determine which private key signed this request. 183 | * `signature`: The base64-encoded RSA-SHA256 signature of the signing string. 184 | 185 | An example signature header would be: 186 | 187 | `Chatops-Signature: Signature keyid=rsakey1,signature=` 188 | 189 | Use the [sample server public key](./example/crpc_server_key.txt) to verify the 190 | following message with your implementation. 191 | 192 | Signature string: 193 | 194 | ``` 195 | http://test.host/_chatops\n889c9543c22695bc031f723ef2fd28ef1fbed6b0\n2017-06-28T22:51:41Z\n 196 | ``` 197 | 198 | Signature: 199 | 200 | ``` 201 | trtOLqLMKzCohxT6Uzeqs+n5Q1msTQUIO4GDl0pyyyTea5MOte6dIQ+k9AlY 202 | HOJ2IHTxGHVhDYJTm2AtgHOEZqrLpqOLqORj64HbwIWtTyuRBUmUmzHWMJKH 203 | a6jy4u9aB8VgSKxE7oDHU6Zo/7kGvqvTBSumF2kMaSjkMXhkUd5WmuQGWpPJ 204 | 5hC0W65alCJU1inQQDZDgj1oH/849zZB3WU8Ne61BMM1Qb4IcljDU6UciGyP 205 | OgXRNSALvgKdCSJyhLhHBxYvuypCjUpgiWKm4h3u0GOpem8NoBXLjeEHT4fR 206 | wJYP8hmQWauUgOmjvKt2wufykHZDZNp4fPwkm6qGKg== 207 | ``` 208 | 209 | (Line breaks added for readability) 210 | 211 | #### Authentication 212 | 213 | CRPC must trust that a user is authenticated by the `user` parameter sent with 214 | every command. Individual servers may request a second authentication factor 215 | after receiving a command; this is beyond the scope of CRPC. 216 | 217 | #### Authorization 218 | 219 | CRPC servers are responsible for ensuring that the given `user` has the proper 220 | authorization to perform an operation. 221 | 222 | ### Execution 223 | 224 | Chatops RPC clients are expected to add a few niceties not covered by the wire 225 | protocol. This complexity is exported to clients to keep the burden of 226 | implementing new automation low. 227 | 228 | * Regex anchoring. Clients should anchor regexes received from servers. If a 229 | command is exported as `where can i deploy`, it should not be triggered on 230 | `tell me where i can deploy` or `where can i deploy, i'm bored`. 231 | * Prefixing. Different execution contexts may prefix commands, such as `.`, 232 | `hubot`, or another sigil. 233 | * Help display systems. These are heavily context dependent. Servers provide 234 | text snippets about commands, but accessing and displaying them is up to the 235 | client. 236 | 237 | These niceties are optional and context-dependent. Different clients may or may 238 | not implement them. But if any of these are required in any execution context, 239 | they should not be pushed to the server. 240 | 241 | ### Protocol Changes 242 | 243 | The version of the ChatopsRPC protocol in use by a server is given as the 244 | `version` field. If no version is returned, `3` is assumed. 245 | -------------------------------------------------------------------------------- /docs/why.md: -------------------------------------------------------------------------------- 1 | # Why Chatops RPC? 2 | 3 | Chatops RPC is distilled from several years' experience working with Chatops 4 | at GitHub. The protocol is simple and will not cover every use case, but 5 | internal adoption has been excellent. 6 | 7 | ### What came first 8 | 9 | #### Hubot 10 | 11 | Our first generation of chatops were written directly as Hubot scripts. These 12 | work fine for situations where there's an existing API that's well-supported. 13 | But if the API wasn't designed to do what the chat command wanted, a single 14 | command might end up making several REST API calls, with all the associated 15 | error handling, responses, etc. REST API endpoints created explicitly for chat 16 | commands tended to be challenging to test end-to-end, since lots of the 17 | logic would end up wrapped in hubot and outputting chat-adapter-specific things. 18 | 19 | #### Shell 20 | 21 | Next we wrote a bridge from a pile of shell scripts to hubot. This had great 22 | adoption, but it had its own problems. Security contexts were often mixed up 23 | with each other, so there were very poor boundaries between what access tokens 24 | a script needed and had available. Several different services made more 25 | sense being written as shell, ruby, python, go, and more, and this mishmash 26 | meant that in practice very little code was reused or tested. 27 | 28 | #### The pattern 29 | 30 | Over time, we settled on a new pattern: direct hubot bindings to API endpoints 31 | written just for hubot. This resulted in a few services with a few hundred 32 | lines of boilerplate node, wrapping API calls. 33 | 34 | This pattern was a winner, though. Servers were able to test their commands 35 | end-to-end, missing out only on the regular expressions that might trigger a 36 | command. Responses and changes to state could be tested in one place. 37 | 38 | Chatops RPC is a distillation and protocol around the last pattern of RPC 39 | endpoints with a one-to-one mapping to a chat command. 40 | 41 | ### Authentication 42 | 43 | Chatops RPC servers need to trust the username that the chat bridge gives them. 44 | This means that a shared token gives servers the ability to misrepresent users 45 | with other servers. In this case, there's a lot of trust built in to the chat 46 | bridge, and we find this maps well to the asymmetric crypto authentication. 47 | 48 | ### Keyword Arguments 49 | 50 | We pair this system with , which 51 | provides generic argument support for long arguments, like `--argument foo`. 52 | While regexes create much more natural commands, rarely used options tend to 53 | create ugly regexes that are hard to test. If a command can potentially take 10 54 | arguments, it's almost certain the regex will have unhandled edge cases. Generic 55 | arguments provide an easy way to take rarely-used options. These are not part 56 | of the Chatops RPC protocol but it's highly recommended to use a client that 57 | supports them. 58 | 59 | Keyword arguments are also important for searching chat history. In the past, we 60 | had several commands that had strange regex forms for different operations. It's 61 | hard to find examples to use these unusual forms. For example, `.deploy!` has 62 | become `.deploy --force`, which is much clearer for new employees and easier to 63 | find examples of. 64 | 65 | ### Testing 66 | 67 | Highly testable patterns were a core consideration of Chatops RPC. GitHub had 68 | grown quite dependent on Chatops as a core part of its workflow, but a very 69 | large number of them were not tested. We've had several instances of important 70 | but rarely-used chat operations failing during availability incidents. Chatops 71 | RPC brings the entire flow to the server, and this results in highly testable 72 | operations that will work when they need to. 73 | 74 | ### Prefixes 75 | 76 | Prefixes provide a way for more than one of the same endpoint to coexist. Over 77 | the years, we've had several systems with staging environments or other 78 | contexts. Ad-hoc solutions, unique to each, were created, forcing developers to 79 | learn new ways to manage the context of multiple chat-connected systems. 80 | 81 | Prefixes provide a way to have two systems, such as `.deploy` and 82 | `.deploy-staging` for a staging deployments system. Prefixes also provide a way 83 | to interface with endpoints that are associated with a resource they manage. For 84 | example, if your site has multiple kubernetes clusters, perhaps a server to 85 | manage each would be stood up, one per cluster, each including the cluster 86 | name in the prefix. This allows you to write "manage kubernetes cluster" once 87 | but have `.kubectl@cluster1 list pods` and `.kubectl@cluster2 list pods`. 88 | 89 | Internally, we use Hubot middleware to alias some very commonly used commands 90 | to shorter versions that do not use the same prefix. 91 | -------------------------------------------------------------------------------- /lib/chatops-controller.rb: -------------------------------------------------------------------------------- 1 | require 'chatops/controller' 2 | -------------------------------------------------------------------------------- /lib/chatops.rb: -------------------------------------------------------------------------------- 1 | module Chatops 2 | # THREAD_STYLES defines the various thread styles available to Hubot Chatops RPC. 3 | # https://github.com/github/hubot-classic/blob/master/docs/rpc_chatops_protocol.md#executing-commands 4 | THREAD_STYLES = { 5 | # Channel thread style is a standard in-channel reply. 6 | channel: 0, 7 | # Threaded thread style will send the reply to a thread from the original message. 8 | threaded: 1, 9 | # Threaded and channel thread style will send the reply to a thread from the original message, 10 | # and post an update into the channel as well (helpful when the original message in the thread is old). 11 | threaded_and_channel: 2, 12 | }.freeze 13 | 14 | ALLOWED_TIME_SKEW_MINS = 5 15 | 16 | def self.public_key 17 | ENV[public_key_env_var_name] 18 | end 19 | 20 | def self.public_key_env_var_name 21 | "CHATOPS_AUTH_PUBLIC_KEY" 22 | end 23 | 24 | def self.alt_public_key 25 | ENV["CHATOPS_AUTH_ALT_PUBLIC_KEY"] 26 | end 27 | 28 | def self.auth_base_urls 29 | ENV.fetch(auth_base_url_env_var_name, "").split(",").map(&:strip) 30 | end 31 | 32 | def self.auth_base_url_env_var_name 33 | "CHATOPS_AUTH_BASE_URL" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/chatops/controller.rb: -------------------------------------------------------------------------------- 1 | require "chatops" 2 | 3 | module Chatops 4 | module Controller 5 | class ConfigurationError < StandardError ; end 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | before_action :ensure_valid_chatops_url, if: :should_authenticate_chatops? 10 | before_action :ensure_valid_chatops_timestamp, if: :should_authenticate_chatops? 11 | before_action :ensure_valid_chatops_signature, if: :should_authenticate_chatops? 12 | before_action :ensure_valid_chatops_nonce, if: :should_authenticate_chatops? 13 | before_action :ensure_chatops_authenticated, if: :should_authenticate_chatops? 14 | before_action :ensure_user_given 15 | before_action :ensure_method_exists 16 | end 17 | 18 | def list 19 | chatops = self.class.chatops 20 | chatops.each { |name, hash| hash[:path] = name } 21 | render :json => { 22 | namespace: self.class.chatops_namespace, 23 | help: self.class.chatops_help, 24 | error_response: self.class.chatops_error_response, 25 | methods: chatops, 26 | version: "3" } 27 | end 28 | 29 | def process(*args) 30 | setup_params! 31 | 32 | if params[:chatop].present? 33 | params[:action] = params[:chatop] 34 | args[0] = params[:action] 35 | unless self.respond_to?(params[:chatop].to_sym) 36 | raise AbstractController::ActionNotFound 37 | end 38 | end 39 | 40 | super(*args) 41 | rescue AbstractController::ActionNotFound 42 | return jsonrpc_method_not_found 43 | end 44 | 45 | def execute_chatop 46 | # This needs to exist for route declarations, but we'll be overriding 47 | # things in #process to make a method the action. 48 | end 49 | 50 | protected 51 | 52 | def setup_params! 53 | json_body.each do |key, value| 54 | next if params.has_key? key 55 | params[key] = value 56 | end 57 | 58 | @jsonrpc_params = params.delete(:params) if params.has_key? :params 59 | 60 | self.params = params.permit(:action, :chatop, :controller, :id, :mention_slug, :message_id, :method, :room_id, :user, :raw_command) 61 | end 62 | 63 | def jsonrpc_params 64 | @jsonrpc_params ||= ActionController::Parameters.new 65 | end 66 | 67 | def json_body 68 | hash = {} 69 | if request.content_mime_type == Mime[:json] 70 | hash = ActiveSupport::JSON.decode(request.raw_post) || {} 71 | end 72 | hash.with_indifferent_access 73 | end 74 | 75 | # `options` supports any of the optional fields documented 76 | # in the [protocol](../../docs/protocol-description.md). 77 | def jsonrpc_success(message, options: {}) 78 | response = { :result => message.to_s } 79 | # do not allow options to override message 80 | options.delete(:result) 81 | jsonrpc_response response.merge(options) 82 | end 83 | alias_method :chatop_send, :jsonrpc_success 84 | 85 | def jsonrpc_parse_error 86 | jsonrpc_error(-32700, 500, "Parse error") 87 | end 88 | 89 | def jsonrpc_invalid_request 90 | jsonrpc_error(-32600, 400, "Invalid request") 91 | end 92 | 93 | def jsonrpc_method_not_found 94 | jsonrpc_error(-32601, 404, "Method not found") 95 | end 96 | 97 | def jsonrpc_invalid_params(message) 98 | message ||= "Invalid parameters" 99 | jsonrpc_error(-32602, 400, message.to_s) 100 | end 101 | alias_method :jsonrpc_failure, :jsonrpc_invalid_params 102 | 103 | def jsonrpc_error(number, http_status, message) 104 | jsonrpc_response({ :error => { :code => number, :message => message.to_s } }, http_status) 105 | end 106 | 107 | def jsonrpc_response(hash, http_status = nil) 108 | http_status ||= 200 109 | render :status => http_status, 110 | :json => { :jsonrpc => "2.0", 111 | :id => params[:id] }.merge(hash) 112 | end 113 | 114 | def ensure_user_given 115 | return true unless chatop_names.include?(params[:action].to_sym) 116 | return true if params[:user].present? 117 | jsonrpc_invalid_params("A username must be supplied as 'user'") 118 | end 119 | 120 | def ensure_chatops_authenticated 121 | 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? 122 | 123 | body = request.raw_post || "" 124 | 125 | @chatops_urls.each do |url| 126 | signature_string = [url, @chatops_nonce, @chatops_timestamp, body].join("\n") 127 | # We return this just to aid client debugging. 128 | response.headers["Chatops-Signature-String"] = Base64.strict_encode64(signature_string) 129 | if signature_valid?(Chatops.public_key, @chatops_signature, signature_string) || 130 | signature_valid?(Chatops.alt_public_key, @chatops_signature, signature_string) 131 | return true 132 | end 133 | end 134 | 135 | return jsonrpc_error(-32800, 403, "Not authorized") 136 | end 137 | 138 | def ensure_valid_chatops_url 139 | unless Chatops.auth_base_urls.present? 140 | raise ConfigurationError.new("You need to set the server's base URL to authenticate chatops RPC via #{Chatops.auth_base_url_env_var_name}") 141 | end 142 | 143 | @chatops_urls = Chatops.auth_base_urls.map { |url| url.chomp("/") + request.path } 144 | end 145 | 146 | def ensure_valid_chatops_nonce 147 | @chatops_nonce = request.headers["Chatops-Nonce"] 148 | return jsonrpc_error(-32801, 403, "A Chatops-Nonce header is required") unless @chatops_nonce.present? 149 | end 150 | 151 | def ensure_valid_chatops_signature 152 | signature_header = request.headers["Chatops-Signature"] 153 | 154 | begin 155 | # "Chatops-Signature: Signature keyid=foo,signature=abc123" => { "keyid"" => "foo", "signature" => "abc123" } 156 | signature_items = signature_header.split(" ", 2)[1].split(",").map { |item| item.split("=", 2) }.to_h 157 | @chatops_signature = signature_items["signature"] 158 | rescue NoMethodError 159 | # The signature header munging, if something's amiss, can produce a `nil` that raises a 160 | # no method error. We'll just carry on; the nil signature will raise below 161 | end 162 | 163 | unless @chatops_signature.present? 164 | return jsonrpc_error(-32802, 403, "Failed to parse signature header") 165 | end 166 | end 167 | 168 | def ensure_valid_chatops_timestamp 169 | @chatops_timestamp = request.headers["Chatops-Timestamp"] 170 | time = Time.iso8601(@chatops_timestamp) 171 | if !(time > Chatops::ALLOWED_TIME_SKEW_MINS.minute.ago && time < Chatops::ALLOWED_TIME_SKEW_MINS.minute.from_now) 172 | return jsonrpc_error(-32803, 403, "Chatops timestamp not within #{Chatops::ALLOWED_TIME_SKEW_MINS} minutes of server time: #{@chatops_timestamp} vs #{Time.now.utc.iso8601}") 173 | end 174 | rescue ArgumentError, TypeError 175 | # time parsing or missing can raise these 176 | return jsonrpc_error(-32804, 403, "Invalid Chatops-Timestamp: #{@chatops_timestamp}") 177 | end 178 | 179 | def request_is_chatop? 180 | (chatop_names + [:list]).include?(params[:action].to_sym) 181 | end 182 | 183 | def chatops_test_auth? 184 | Rails.env.test? && request.env["CHATOPS_TESTING_AUTH"] 185 | end 186 | 187 | def should_authenticate_chatops? 188 | request_is_chatop? && !chatops_test_auth? 189 | end 190 | 191 | def signature_valid?(key_string, signature, signature_string) 192 | return false unless key_string.present? 193 | digest = OpenSSL::Digest::SHA256.new 194 | decoded_signature = Base64.decode64(signature) 195 | public_key = OpenSSL::PKey::RSA.new(key_string) 196 | public_key.verify(digest, decoded_signature, signature_string) 197 | end 198 | 199 | def ensure_method_exists 200 | return jsonrpc_method_not_found unless (chatop_names + [:list]).include?(params[:action].to_sym) 201 | end 202 | 203 | def chatop_names 204 | self.class.chatops.keys 205 | end 206 | 207 | module ClassMethods 208 | def chatop(method_name, regex, help, &block) 209 | chatops[method_name] = { help: help, 210 | regex: regex.source, 211 | params: regex.names } 212 | define_method method_name, &block 213 | end 214 | 215 | %w{namespace help error_response}.each do |setting| 216 | method_name = "chatops_#{setting}".to_sym 217 | variable_name = "@#{method_name}".to_sym 218 | define_method method_name do |*args| 219 | assignment = args.first 220 | if assignment.present? 221 | instance_variable_set variable_name, assignment 222 | end 223 | instance_variable_get variable_name.to_sym 224 | end 225 | end 226 | 227 | def chatops 228 | @chatops ||= {} 229 | @chatops 230 | end 231 | end 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /lib/chatops/controller/rspec.rb: -------------------------------------------------------------------------------- 1 | require "chatops/controller/test_case" 2 | 3 | RSpec.configure do |config| 4 | config.include Chatops::Controller::TestCaseHelpers 5 | end 6 | -------------------------------------------------------------------------------- /lib/chatops/controller/test_case.rb: -------------------------------------------------------------------------------- 1 | require "chatops/controller/test_case_helpers" 2 | 3 | class Chatops::Controller::TestCase < ActionController::TestCase 4 | include Chatops::Controller::TestCaseHelpers 5 | end 6 | -------------------------------------------------------------------------------- /lib/chatops/controller/test_case_helpers.rb: -------------------------------------------------------------------------------- 1 | module Chatops::Controller::TestCaseHelpers 2 | 3 | class NoMatchingCommandRegex < StandardError ; end 4 | 5 | def chatops_auth! 6 | request.env["CHATOPS_TESTING_AUTH"] = true 7 | end 8 | 9 | def chatops_prefix(prefix = nil) 10 | # We abuse request.env here so that rails will cycle this with each test. 11 | # If we used an instance variable, one would always need to be resetting 12 | # it. 13 | if prefix 14 | request.env["CHATOPS_TESTING_PREFIX"] = prefix 15 | end 16 | request.env["CHATOPS_TESTING_PREFIX"] 17 | end 18 | 19 | def chatop(method, params = {}) 20 | args = params.dup.symbolize_keys 21 | user = args.delete :user 22 | room_id = args.delete :room_id 23 | mention_slug = args.delete :mention_slug 24 | message_id = args.delete :message_id 25 | 26 | params = { 27 | :params => args, 28 | :room_id => room_id, 29 | :user => user, 30 | :mention_slug => mention_slug, 31 | :message_id => message_id, 32 | } 33 | 34 | major_version = Rails.version.split('.')[0].to_i 35 | if major_version >= 5 36 | post :execute_chatop, params: params.merge(chatop: method) 37 | else 38 | post :execute_chatop, params.merge(chatop: method) 39 | end 40 | end 41 | 42 | def chat(message, user, room_id = "123", message_id = "456") 43 | get :list 44 | json_response = JSON.load(response.body) 45 | raise "Invalid Chatop response - BODY: #{json_response}" unless json_response.key?("methods") 46 | matchers = json_response["methods"].map { |name, metadata| 47 | metadata = metadata.dup 48 | metadata["name"] = name 49 | prefix = chatops_prefix ? "#{chatops_prefix} " : "" 50 | metadata["regex"] = Regexp.new("^#{prefix}#{metadata["regex"]}$", "i") 51 | metadata 52 | } 53 | 54 | named_params, command = extract_named_params(message) 55 | 56 | matcher = matchers.find { |m| m["regex"].match(command) } 57 | 58 | raise NoMatchingCommandRegex.new("No command matches '#{command}'") unless matcher 59 | 60 | match_data = matcher["regex"].match(command) 61 | jsonrpc_params = named_params.dup 62 | matcher["params"].each do |param| 63 | jsonrpc_params[param] ||= match_data[param.to_sym] 64 | end 65 | jsonrpc_params.merge!(user: user, room_id: room_id, mention_slug: user, message_id: message_id) 66 | chatop matcher["name"].to_sym, jsonrpc_params 67 | end 68 | 69 | def chatop_response 70 | json_response = JSON.load(response.body) 71 | if json_response["error"].present? 72 | raise "There was an error instead of an expected successful response: #{json_response["error"]}" 73 | end 74 | json_response["result"] 75 | end 76 | 77 | def chatop_error 78 | json_response = JSON.load(response.body) 79 | raise "There is no chatop error - BODY: #{json_response}" unless json_response.key?("error") 80 | json_response["error"]["message"] 81 | end 82 | 83 | def extract_named_params(command_string) 84 | params = {} 85 | 86 | while last_index = command_string.rindex(" --") 87 | arg = command_string[last_index..-1] 88 | matches = arg.match(/ --(\S+)(.*)/) 89 | params[matches[1]] = matches[2].strip 90 | params[matches[1]] = "true" unless params[matches[1]].present? 91 | command_string = command_string.slice(0, last_index) 92 | end 93 | 94 | command_string = command_string.strip 95 | [params, command_string] 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/chatops/controller/version.rb: -------------------------------------------------------------------------------- 1 | module ChatopsController 2 | VERSION = "5.3.0" 3 | end 4 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # github/strap related bootstrapping 6 | if [ "$(uname -s)" = "Darwin" ]; then 7 | brew update >/dev/null 8 | brew bundle check &>/dev/null || brew bundle 9 | 10 | brew bootstrap-rbenv-ruby 11 | 12 | BUNDLE="brew bundle exec -- bundle" 13 | else 14 | BUNDLE="bundle" 15 | fi 16 | 17 | $BUNDLE --path vendor/gems --local --binstubs 18 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #!/bin/sh 3 | 4 | set -eu 5 | 6 | test -d "/usr/share/rbenv/shims" && { 7 | export PATH="/usr/share/rbenv/shims:$PATH" 8 | export RBENV_VERSION="2.3.1" 9 | } 10 | 11 | # clean out the ruby environment 12 | export RUBYLIB= 13 | export RUBYOPT= 14 | 15 | script/bootstrap 16 | script/test --format RspecJSONDumper::Formatter --format progress 17 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | bin/rspec spec/lib/chatops/controller_spec.rb "$@" 4 | -------------------------------------------------------------------------------- /spec/dummy/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/spec/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/spec/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/spec/dummy/app/mailers/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/spec/dummy/app/models/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/spec/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'action_controller/railtie' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "chatops/controller" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 16 | # config.time_zone = 'Central Time (US & Canada)' 17 | 18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 20 | # config.i18n.default_locale = :de 21 | 22 | # Do not swallow errors in after_commit/after_rollback callbacks. 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | #config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | #config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | if Rails.version.start_with?("4") 17 | config.serve_static_files = true 18 | config.static_cache_control = 'public, max-age=3600' 19 | else 20 | config.public_file_server.enabled = true 21 | config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } 22 | end 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | 28 | # Raise exceptions instead of rendering exception templates. 29 | config.action_dispatch.show_exceptions = false 30 | 31 | # Disable request forgery protection in test environment. 32 | config.action_controller.allow_forgery_protection = false 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | 38 | # Randomize the order test cases are executed. 39 | config.active_support.test_order = :random 40 | 41 | # Print deprecation notices to the stderr. 42 | config.active_support.deprecation = :stderr 43 | 44 | # Raises error for missing translations 45 | # config.action_view.raise_on_missing_translations = true 46 | end 47 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | if Rails.version.start_with?("4") 5 | Rails.application.config.assets.version = '1.0' 6 | end 7 | 8 | # Add additional assets to the asset load path 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 13 | # Rails.application.config.assets.precompile += %w( search.js ) 14 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # The priority is based upon order of creation: first created -> highest priority. 3 | # See how all your routes lay out with "rake routes". 4 | 5 | # You can have the root of your site routed with "root" 6 | # root 'welcome#index' 7 | 8 | # Example of regular route: 9 | # get 'products/:id' => 'catalog#view' 10 | 11 | # Example of named route that can be invoked with purchase_url(id: product.id) 12 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 13 | 14 | # Example resource route (maps HTTP verbs to controller actions automatically): 15 | # resources :products 16 | 17 | # Example resource route with options: 18 | # resources :products do 19 | # member do 20 | # get 'short' 21 | # post 'toggle' 22 | # end 23 | # 24 | # collection do 25 | # get 'sold' 26 | # end 27 | # end 28 | 29 | # Example resource route with sub-resources: 30 | # resources :products do 31 | # resources :comments, :sales 32 | # resource :seller 33 | # end 34 | 35 | # Example resource route with more complex sub-resources: 36 | # resources :products do 37 | # resources :comments 38 | # resources :sales do 39 | # get 'recent', on: :collection 40 | # end 41 | # end 42 | 43 | # Example resource route with concerns: 44 | # concern :toggleable do 45 | # post 'toggle' 46 | # end 47 | # resources :posts, concerns: :toggleable 48 | # resources :photos, concerns: :toggleable 49 | 50 | # Example resource route within a namespace: 51 | # namespace :admin do 52 | # # Directs /admin/products/* to Admin::ProductsController 53 | # # (app/controllers/admin/products_controller.rb) 54 | # resources :products 55 | # end 56 | end 57 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 76c272b42d660a907faacdc0d8d47b20139d72954e32605e3d3e361008dfee9628ae6944ee08ea75937e210e5c9f406ade4a001595a2b8746a6638227995eeee 15 | 16 | test: 17 | secret_key_base: c3afc12cfdec7f850e7faabbc07ade9edbb1ef5c32d76e6ab939b04bba3904212d03949e7a84882777b89721899fb8673a39a7538f77b001ddcf03b2b2618b41 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /spec/dummy/db/development.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/spec/dummy/db/development.sqlite3 -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 0) do 15 | 16 | end 17 | -------------------------------------------------------------------------------- /spec/dummy/db/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/spec/dummy/db/test.sqlite3 -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/spec/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/spec/dummy/log/.keep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/lib/chatops/controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'openssl' 3 | require 'base64' 4 | 5 | describe ActionController::Base, type: :controller do 6 | controller do 7 | include Chatops::Controller 8 | chatops_namespace :test 9 | chatops_help "Chatops of and relating to testing" 10 | chatops_error_response "Try checking haystack?" 11 | 12 | before_action :ensure_app_given, :only => [:wcid] 13 | 14 | chatop :wcid, 15 | /(?:where can i deploy|wcid)(?: (?\S+))?/, 16 | "where can i deploy?" do 17 | return jsonrpc_invalid_params("I need nope, sorry") if jsonrpc_params[:app] == "nope" 18 | jsonrpc_success "You can deploy #{jsonrpc_params["app"]} just fine." 19 | end 20 | 21 | chatop :foobar, 22 | /(?:how can i foo and bar all at once)?/, 23 | "how to foo and bar" do 24 | raise "there's always params" unless jsonrpc_params.respond_to?(:[]) 25 | jsonrpc_success "You just foo and bar like it just don't matter" 26 | end 27 | 28 | chatop :proxy_parameters, 29 | /(?:proxy_parameters)/, 30 | "proxy parameters back to test" do 31 | response = { :params => params, :jsonrpc_params => jsonrpc_params }.to_json 32 | jsonrpc_success response 33 | end 34 | 35 | skip_before_action :ensure_method_exists, only: :non_chatop_method 36 | def non_chatop_method 37 | render :plain => "Why would you have something thats not a chatop?" 38 | end 39 | 40 | def unexcluded_chatop_method 41 | render :text => "Sadly, I'll never be reached" 42 | end 43 | 44 | def ensure_app_given 45 | return jsonrpc_invalid_params("I need an app, every time") unless jsonrpc_params[:app].present? 46 | end 47 | end 48 | 49 | before :each do 50 | routes.draw do 51 | get "/_chatops" => "anonymous#list" 52 | post "/_chatops/:chatop", controller: "anonymous", action: :execute_chatop 53 | get "/other" => "anonymous#non_chatop_method" 54 | get "/other_will_fail" => "anonymous#unexcluded_chatop_method" 55 | end 56 | 57 | @private_key = OpenSSL::PKey::RSA.new(2048) 58 | ENV["CHATOPS_AUTH_PUBLIC_KEY"] = @private_key.public_key.to_pem 59 | ENV["CHATOPS_AUTH_BASE_URL"] = "http://old.host,http://test.host/" 60 | end 61 | 62 | def rails_flexible_post(path, outer_params, jsonrpc_params = nil) 63 | if Rails.version.start_with?("4") 64 | post path, outer_params.merge("params" => jsonrpc_params) 65 | else 66 | jsonrpc_params ||= {} 67 | post path, :params => outer_params.merge("params" => jsonrpc_params) 68 | end 69 | end 70 | 71 | it "requires authentication" do 72 | request.headers["Chatops-Timestamp"] = Time.now.utc.iso8601 73 | get :list 74 | expect(response.status).to eq 403 75 | end 76 | 77 | it "allows public key authentication for a GET request" do 78 | nonce = SecureRandom.hex(20) 79 | timestamp = Time.now.utc.iso8601 80 | request.headers["Chatops-Nonce"] = nonce 81 | request.headers["Chatops-Timestamp"] = timestamp 82 | digest = OpenSSL::Digest::SHA256.new 83 | signature_string = "http://test.host/_chatops\n#{nonce}\n#{timestamp}\n" 84 | signature = Base64.encode64(@private_key.sign(digest, signature_string)) 85 | request.headers["Chatops-Signature"] = "Signature keyid=foo,signature=#{signature}" 86 | get :list 87 | expect(response.headers["Chatops-Signature-String"]).to eq Base64.strict_encode64(signature_string) 88 | expect(response.status).to eq 200 89 | expect(response).to be_valid_json 90 | end 91 | 92 | it "allows public key authentication for a POST request" do 93 | nonce = SecureRandom.hex(20) 94 | timestamp = Time.now.utc.iso8601 95 | request.headers["Chatops-Nonce"] = nonce 96 | request.headers["Chatops-Timestamp"] = timestamp 97 | digest = OpenSSL::Digest::SHA256.new 98 | params = { :room_id => "123", :user => "bhuga", :params => {}} 99 | 100 | body = params.to_json 101 | @request.headers["Content-Type"] = "application/json" 102 | @request.env["RAW_POST_DATA"] = body 103 | signature_string = "http://test.host/_chatops/foobar\n#{nonce}\n#{timestamp}\n#{body}" 104 | signature = Base64.encode64(@private_key.sign(digest, signature_string)) 105 | request.headers["Chatops-Signature"] = "Signature keyid=foo,signature=#{signature}" 106 | 107 | major_version = Rails.version.split(".")[0].to_i 108 | if major_version >= 5 109 | post :execute_chatop, params: params.merge(chatop: "foobar") 110 | else 111 | post :execute_chatop, params.merge(chatop: "foobar") 112 | end 113 | 114 | expect(response.status).to eq 200 115 | expect(response).to be_valid_json 116 | end 117 | 118 | it "allows using a second public key to authenticate" do 119 | ENV["CHATOPS_AUTH_ALT_PUBLIC_KEY"] = ENV["CHATOPS_AUTH_PUBLIC_KEY"] 120 | other_key = OpenSSL::PKey::RSA.new(2048) 121 | ENV["CHATOPS_AUTH_PUBLIC_KEY"] = other_key.public_key.to_pem 122 | nonce = SecureRandom.hex(20) 123 | timestamp = Time.now.utc.iso8601 124 | request.headers["Chatops-Nonce"] = nonce 125 | request.headers["Chatops-Timestamp"] = timestamp 126 | digest = OpenSSL::Digest::SHA256.new 127 | signature_string = "http://test.host/_chatops\n#{nonce}\n#{timestamp}\n" 128 | signature = Base64.encode64(@private_key.sign(digest, signature_string)) 129 | request.headers["Chatops-Signature"] = "Signature keyid=foo,signature=#{signature}" 130 | get :list 131 | expect(response.status).to eq 200 132 | expect(response).to be_valid_json 133 | end 134 | 135 | it "raises an error trying to auth without a base url" do 136 | nonce = SecureRandom.hex(20) 137 | timestamp = Time.now.utc.iso8601 138 | request.headers["Chatops-Nonce"] = nonce 139 | request.headers["Chatops-Timestamp"] = timestamp 140 | digest = OpenSSL::Digest::SHA256.new 141 | signature_string = "http://test.host/_chatops\n#{nonce}\n#{timestamp}\n" 142 | signature = Base64.encode64(@private_key.sign(digest, signature_string)) 143 | request.headers["Chatops-Signature"] = "Signature keyid=foo,signature=#{signature}" 144 | ENV.delete "CHATOPS_AUTH_BASE_URL" 145 | expect { 146 | get :list 147 | }.to raise_error(Chatops::Controller::ConfigurationError) 148 | end 149 | 150 | it "raises an error trying to auth without a public key" do 151 | nonce = SecureRandom.hex(20) 152 | timestamp = Time.now.utc.iso8601 153 | request.headers["Chatops-Nonce"] = nonce 154 | request.headers["Chatops-Timestamp"] = timestamp 155 | digest = OpenSSL::Digest::SHA256.new 156 | signature_string = "http://test.host/_chatops\n#{nonce}\n#{timestamp}\n" 157 | signature = Base64.encode64(@private_key.sign(digest, signature_string)) 158 | request.headers["Chatops-Signature"] = "Signature keyid=foo,signature=#{signature}" 159 | ENV.delete "CHATOPS_AUTH_PUBLIC_KEY" 160 | expect { 161 | get :list 162 | }.to raise_error(Chatops::Controller::ConfigurationError) 163 | end 164 | 165 | it "doesn't authenticate with the wrong public key'" do 166 | other_key = OpenSSL::PKey::RSA.new(2048) 167 | ENV["CHATOPS_AUTH_PUBLIC_KEY"] = other_key.public_key.to_pem 168 | nonce = SecureRandom.hex(20) 169 | timestamp = Time.now.utc.iso8601 170 | request.headers["Chatops-Nonce"] = nonce 171 | request.headers["Chatops-Timestamp"] = timestamp 172 | digest = OpenSSL::Digest::SHA256.new 173 | signature_string = "http://test.host/_chatops\n#{nonce}\n#{timestamp}\n" 174 | signature = Base64.encode64(@private_key.sign(digest, signature_string)) 175 | request.headers["Chatops-Signature"] = "Signature keyid=foo,signature=#{signature}" 176 | get :list 177 | expect(response.status).to eq 403 178 | end 179 | 180 | it "doesn't allow requests more than 5 minute old" do 181 | nonce = SecureRandom.hex(20) 182 | timestamp = 6.minutes.ago.utc.iso8601 183 | request.headers["Chatops-Nonce"] = nonce 184 | request.headers["Chatops-Timestamp"] = timestamp 185 | digest = OpenSSL::Digest::SHA256.new 186 | signature_string = "http://test.host/_chatops\n#{nonce}\n#{timestamp}\n" 187 | signature = Base64.encode64(@private_key.sign(digest, signature_string)) 188 | request.headers["Chatops-Signature"] = "Signature keyid=foo,signature=#{signature}" 189 | get :list 190 | expect(response.status).to eq 403 191 | expect(response.body).to include "Chatops timestamp not within 5 minutes" 192 | end 193 | 194 | it "doesn't allow requests more than 5 minute in the future" do 195 | nonce = SecureRandom.hex(20) 196 | timestamp = 6.minutes.from_now.utc.iso8601 197 | request.headers["Chatops-Nonce"] = nonce 198 | request.headers["Chatops-Timestamp"] = timestamp 199 | digest = OpenSSL::Digest::SHA256.new 200 | signature_string = "http://test.host/_chatops\n#{nonce}\n#{timestamp}\n" 201 | signature = Base64.encode64(@private_key.sign(digest, signature_string)) 202 | request.headers["Chatops-Signature"] = "Signature keyid=foo,signature=#{signature}" 203 | get :list 204 | expect(response.status).to eq 403 205 | expect(response.body).to include "Chatops timestamp not within 5 minutes" 206 | end 207 | 208 | it "does not add authentication to non-chatops routes" do 209 | get :non_chatop_method 210 | expect(response.status).to eq 200 211 | expect(response.body).to eq "Why would you have something thats not a chatop?" 212 | end 213 | 214 | context "when authenticated" do 215 | before do 216 | chatops_auth! 217 | end 218 | 219 | it "provides a list method" do 220 | get :list 221 | expect(response.status).to eq 200 222 | expect(json_response).to eq({ 223 | "namespace" => "test", 224 | "help" => "Chatops of and relating to testing", 225 | "error_response" => "Try checking haystack?", 226 | "methods" => { 227 | "wcid" => { 228 | "help" => "where can i deploy?", 229 | "regex" => /(?:where can i deploy|wcid)(?: (?\S+))?/.source, 230 | "params" => ["app"], 231 | "path" => "wcid" 232 | }, 233 | "foobar" => { 234 | "help" => "how to foo and bar", 235 | "regex" => /(?:how can i foo and bar all at once)?/.source, 236 | "params" => [], 237 | "path" => "foobar" 238 | }, 239 | "proxy_parameters" => { 240 | "help" => "proxy parameters back to test", 241 | "regex" => /(?:proxy_parameters)/.source, 242 | "params" => [], 243 | "path" => "proxy_parameters" 244 | } 245 | }, 246 | "version" => "3" 247 | }) 248 | end 249 | 250 | it "requires a user be sent to chatops" do 251 | rails_flexible_post :execute_chatop, chatop: :foobar 252 | expect(response.status).to eq 400 253 | expect(json_response).to eq({ 254 | "jsonrpc" => "2.0", 255 | "id" => nil, 256 | "error" => { 257 | "code" => -32602, 258 | "message" => "A username must be supplied as 'user'" 259 | } 260 | }) 261 | end 262 | 263 | it "returns method not found for a not found method" do 264 | rails_flexible_post :execute_chatop, chatop: :barfoo, user: "foo" 265 | expect(json_response).to eq({ 266 | "jsonrpc" => "2.0", 267 | "id" => nil, 268 | "error" => { 269 | "code" => -32601, 270 | "message" => "Method not found" 271 | } 272 | }) 273 | expect(response.status).to eq 404 274 | end 275 | 276 | it "requires skipping a before_action to find non-chatop methods, sorry about that" do 277 | get :unexcluded_chatop_method 278 | expect(json_response).to eq({ 279 | "jsonrpc" => "2.0", 280 | "id" => nil, 281 | "error" => { 282 | "code" => -32601, 283 | "message" => "Method not found" 284 | } 285 | }) 286 | expect(response.status).to eq 404 287 | end 288 | 289 | it "runs a known method" do 290 | rails_flexible_post :execute_chatop, chatop: :foobar, user: "foo" 291 | expect(json_response).to eq({ 292 | "jsonrpc" => "2.0", 293 | "id" => nil, 294 | "result" => "You just foo and bar like it just don't matter" 295 | }) 296 | expect(response.status).to eq 200 297 | end 298 | 299 | it "passes parameters to methods" do 300 | rails_flexible_post :execute_chatop, { :chatop => "wcid", :user => "foo" }, { "app" => "foo" } 301 | expect(json_response).to eq({ 302 | "jsonrpc" => "2.0", 303 | "id" => nil, 304 | "result" => "You can deploy foo just fine." 305 | }) 306 | expect(response.status).to eq 200 307 | end 308 | 309 | it "passes all expected paramters" do 310 | rails_flexible_post :execute_chatop, { 311 | :chatop => "proxy_parameters", 312 | :user => "foo", 313 | :mention_slug => "mention_slug_here", 314 | :message_id => "message_id_here", 315 | :room_id => "#someroom", 316 | :unknown_key => "few" # This should get ignored 317 | }, { 318 | "app" => "foo" 319 | } 320 | expect(json_response).to eq({ 321 | "jsonrpc" => "2.0", 322 | "id" => nil, 323 | "result" => "{\"params\":{\"action\":\"proxy_parameters\",\"chatop\":\"proxy_parameters\",\"controller\":\"anonymous\",\"mention_slug\":\"mention_slug_here\",\"message_id\":\"message_id_here\",\"room_id\":\"#someroom\",\"user\":\"foo\"},\"jsonrpc_params\":{\"app\":\"foo\"}}" 324 | }) 325 | expect(response.status).to eq 200 326 | end 327 | 328 | 329 | it "uses typical controller fun like before_action" do 330 | rails_flexible_post :execute_chatop, :chatop => "wcid", :user => "foo" 331 | expect(json_response).to eq({ 332 | "jsonrpc" => "2.0", 333 | "id" => nil, 334 | "error" => { 335 | "code" => -32602, 336 | "message" => "I need an app, every time" 337 | } 338 | }) 339 | expect(response.status).to eq 400 340 | end 341 | 342 | it "allows methods to return invalid params with a message" do 343 | rails_flexible_post :execute_chatop, { :chatop => "wcid", :user => "foo" }, { "app" => "nope" } 344 | expect(response.status).to eq 400 345 | expect(json_response).to eq({ 346 | "jsonrpc" => "2.0", 347 | "id" => nil, 348 | "error" => { 349 | "code" => -32602, 350 | "message" => "I need nope, sorry" 351 | } 352 | }) 353 | end 354 | 355 | context "rspec helpers" do 356 | it "makes it easy to test a response" do 357 | chatop "wcid", :user => "foo", :app => "foo" 358 | expect(chatop_response).to eq "You can deploy foo just fine." 359 | end 360 | 361 | it "makes it easy to test an error message" do 362 | chatop "wcid", :user => "foo", :app => "nope" 363 | expect(chatop_error).to eq "I need nope, sorry" 364 | end 365 | end 366 | 367 | context "regex-based test helpers" do 368 | it "routes based on regexes from test helpers" do 369 | chat "where can i deploy foobar", "bhuga" 370 | expect(request.params["action"]).to eq "execute_chatop" 371 | expect(request.params["chatop"]).to eq "wcid" 372 | expect(request.params["user"]).to eq "bhuga" 373 | expect(request.params["params"]["app"]).to eq "foobar" 374 | expect(chatop_response).to eq "You can deploy foobar just fine." 375 | end 376 | 377 | it "works with generic arguments" do 378 | chat "where can i deploy foobar --fruit apple --vegetable green celery", "bhuga" 379 | expect(request.params["action"]).to eq "execute_chatop" 380 | expect(request.params["chatop"]).to eq "wcid" 381 | expect(request.params["user"]).to eq "bhuga" 382 | expect(request.params["params"]["app"]).to eq "foobar" 383 | expect(request.params["params"]["fruit"]).to eq "apple" 384 | expect(request.params["params"]["vegetable"]).to eq "green celery" 385 | expect(chatop_response).to eq "You can deploy foobar just fine." 386 | end 387 | 388 | it "works with boolean arguments" do 389 | chat "where can i deploy foobar --this-is-sparta", "bhuga" 390 | expect(request.params["action"]).to eq "execute_chatop" 391 | expect(request.params["chatop"]).to eq "wcid" 392 | expect(request.params["user"]).to eq "bhuga" 393 | expect(request.params["params"]["this-is-sparta"]).to eq "true" 394 | end 395 | 396 | it "sends along all the parameters" do 397 | chat "where can i deploy foobar", "my_username", "room_id_5", "message_id_6" 398 | expect(request.params["action"]).to eq "execute_chatop" 399 | expect(request.params["chatop"]).to eq "wcid" 400 | expect(request.params["user"]).to eq "my_username" 401 | expect(request.params["room_id"]).to eq "room_id_5" 402 | expect(request.params["message_id"]).to eq "message_id_6" 403 | end 404 | 405 | it "anchors regexes" do 406 | expect { 407 | chat "too bad that this message doesn't start with where can i deploy foobar", "bhuga" 408 | }.to raise_error(Chatops::Controller::TestCaseHelpers::NoMatchingCommandRegex) 409 | end 410 | 411 | it "allows setting a v2 prefix" do 412 | chatops_prefix "test-prefix" 413 | chat "test-prefix where can i deploy foobar --this-is-sparta", "bhuga" 414 | expect(request.params["chatop"]).to eq "wcid" 415 | end 416 | end 417 | end 418 | end 419 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV['RAILS_ENV'] ||= 'test' 3 | require File.expand_path("../dummy/config/environment", __FILE__) 4 | 5 | # Prevent database truncation if the environment is production 6 | abort("The Rails environment is running in production mode!") if Rails.env.production? 7 | require 'spec_helper' 8 | require 'pry' 9 | require 'rspec/rails' 10 | require 'chatops/controller/rspec' 11 | # Add additional requires below this line. Rails is not loaded until this point! 12 | 13 | # Requires supporting ruby files with custom matchers and macros, etc, in 14 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 15 | # run as spec files by default. This means that files in spec/support that end 16 | # in _spec.rb will both be required and run as specs, causing the specs to be 17 | # run twice. It is recommended that you do not name files matching this glob to 18 | # end with _spec.rb. You can configure this pattern with the --pattern 19 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 20 | # 21 | # The following line is provided for convenience purposes. It has the downside 22 | # of increasing the boot-up time by auto-requiring all files in the support 23 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 24 | # require only the support files necessary. 25 | # 26 | # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 27 | 28 | # Checks for pending migration and applies them before tests are run. 29 | # If you are not using ActiveRecord, you can remove this line. 30 | 31 | Dir[File.expand_path("../../spec/support/**/*.rb", __FILE__)].each {|f| require f} 32 | 33 | RSpec.configure do |config| 34 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 35 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 36 | config.alias_example_to :fit, :focus => true 37 | config.filter_run :focus => true 38 | config.run_all_when_everything_filtered = true 39 | 40 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 41 | # examples within a transaction, remove the following line or assign false 42 | # instead of true. 43 | config.use_transactional_fixtures = true 44 | 45 | # RSpec Rails can automatically mix in different behaviours to your tests 46 | # based on their file location, for example enabling you to call `get` and 47 | # `post` in specs under `spec/controllers`. 48 | # 49 | # You can disable this behaviour by removing the line below, and instead 50 | # explicitly tag your specs with their type, e.g.: 51 | # 52 | # RSpec.describe UsersController, :type => :controller do 53 | # # ... 54 | # end 55 | # 56 | # The different available types are documented in the features, such as in 57 | # https://relishapp.com/rspec/rspec-rails/docs 58 | config.infer_spec_type_from_file_location! 59 | 60 | # Filter lines from Rails gems in backtraces. 61 | config.filter_rails_from_backtrace! 62 | # arbitrary gems may also be filtered via: 63 | # config.filter_gems_from_backtrace("gem name") 64 | 65 | end 66 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # The `.rspec` file also contains a few flags that are not defaults but that 16 | # users commonly want. 17 | # 18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 19 | RSpec.configure do |config| 20 | # rspec-expectations config goes here. You can use an alternate 21 | # assertion/expectation library such as wrong or the stdlib/minitest 22 | # assertions if you prefer. 23 | config.expect_with :rspec do |expectations| 24 | # This option will default to `true` in RSpec 4. It makes the `description` 25 | # and `failure_message` of custom matchers include text for helper methods 26 | # defined using `chain`, e.g.: 27 | # be_bigger_than(2).and_smaller_than(4).description 28 | # # => "be bigger than 2 and smaller than 4" 29 | # ...rather than: 30 | # # => "be bigger than 2" 31 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 32 | end 33 | 34 | # rspec-mocks config goes here. You can use an alternate test double 35 | # library (such as bogus or mocha) by changing the `mock_with` option here. 36 | config.mock_with :rspec do |mocks| 37 | # Prevents you from mocking or stubbing a method that does not exist on 38 | # a real object. This is generally recommended, and will default to 39 | # `true` in RSpec 4. 40 | mocks.verify_partial_doubles = true 41 | end 42 | 43 | # The settings below are suggested to provide a good initial experience 44 | # with RSpec, but feel free to customize to your heart's content. 45 | =begin 46 | # These two settings work together to allow you to limit a spec run 47 | # to individual examples or groups you care about by tagging them with 48 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 49 | # get run. 50 | config.filter_run :focus 51 | config.run_all_when_everything_filtered = true 52 | 53 | # Allows RSpec to persist some state between runs in order to support 54 | # the `--only-failures` and `--next-failure` CLI options. We recommend 55 | # you configure your source control system to ignore this file. 56 | config.example_status_persistence_file_path = "spec/examples.txt" 57 | 58 | # Limits the available syntax to the non-monkey patched syntax that is 59 | # recommended. For more details, see: 60 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 61 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 62 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 63 | config.disable_monkey_patching! 64 | 65 | # Many RSpec users commonly either run the entire suite or an individual 66 | # file, and it's useful to allow more verbose output when running an 67 | # individual spec file. 68 | if config.files_to_run.one? 69 | # Use the documentation formatter for detailed output, 70 | # unless a formatter has already been configured 71 | # (e.g. via a command-line flag). 72 | config.default_formatter = 'doc' 73 | end 74 | 75 | # Print the 10 slowest examples and example groups at the 76 | # end of the spec run, to help surface which specs are running 77 | # particularly slow. 78 | config.profile_examples = 10 79 | 80 | # Run specs in random order to surface order dependencies. If you find an 81 | # order dependency and want to debug it, you can fix the order by providing 82 | # the seed, which is printed after each run. 83 | # --seed 1234 84 | config.order = :random 85 | 86 | # Seed global randomization in this process using the `--seed` CLI option. 87 | # Setting this allows you to use `--seed` to deterministically reproduce 88 | # test failures related to randomization by passing the same `--seed` value 89 | # as the one that triggered the failure. 90 | Kernel.srand config.seed 91 | =end 92 | end 93 | -------------------------------------------------------------------------------- /spec/support/json_response.rb: -------------------------------------------------------------------------------- 1 | module RSpec 2 | module JSONResponse 3 | def json_response(response = @response) 4 | @json_responses ||= {} 5 | @json_responses[response] ||= JSON.load(response.body) 6 | end 7 | 8 | RSpec::Matchers.define :be_valid_json do 9 | match do |response| 10 | begin 11 | json_response(response) 12 | true 13 | rescue StandardError => ex 14 | @exception = ex 15 | false 16 | end 17 | end 18 | 19 | failure_message do |response| 20 | %{Expected response body to be valid json, but there was an error parsing it:\n #{@exception.inspect}} 21 | end 22 | end 23 | 24 | ::RSpec.configure do |config| 25 | config.include self 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /vendor/cache/actioncable-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/actioncable-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/actionmailbox-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/actionmailbox-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/actionmailer-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/actionmailer-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/actionpack-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/actionpack-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/actiontext-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/actiontext-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/actionview-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/actionview-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/activejob-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/activejob-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/activemodel-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/activemodel-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/activerecord-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/activerecord-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/activestorage-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/activestorage-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/activesupport-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/activesupport-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/builder-3.2.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/builder-3.2.4.gem -------------------------------------------------------------------------------- /vendor/cache/coderay-1.1.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/coderay-1.1.3.gem -------------------------------------------------------------------------------- /vendor/cache/concurrent-ruby-1.1.8.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/concurrent-ruby-1.1.8.gem -------------------------------------------------------------------------------- /vendor/cache/crass-1.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/crass-1.0.6.gem -------------------------------------------------------------------------------- /vendor/cache/diff-lcs-1.4.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/diff-lcs-1.4.4.gem -------------------------------------------------------------------------------- /vendor/cache/erubi-1.10.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/erubi-1.10.0.gem -------------------------------------------------------------------------------- /vendor/cache/globalid-0.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/globalid-0.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/i18n-1.8.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/i18n-1.8.7.gem -------------------------------------------------------------------------------- /vendor/cache/loofah-2.9.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/loofah-2.9.0.gem -------------------------------------------------------------------------------- /vendor/cache/mail-2.7.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/mail-2.7.1.gem -------------------------------------------------------------------------------- /vendor/cache/marcel-0.3.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/marcel-0.3.3.gem -------------------------------------------------------------------------------- /vendor/cache/method_source-1.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/method_source-1.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/mimemagic-0.3.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/mimemagic-0.3.5.gem -------------------------------------------------------------------------------- /vendor/cache/mini_mime-1.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/mini_mime-1.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/mini_portile2-2.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/mini_portile2-2.5.0.gem -------------------------------------------------------------------------------- /vendor/cache/minitest-5.14.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/minitest-5.14.3.gem -------------------------------------------------------------------------------- /vendor/cache/nio4r-2.5.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/nio4r-2.5.4.gem -------------------------------------------------------------------------------- /vendor/cache/nokogiri-1.11.1-x86_64-darwin.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/nokogiri-1.11.1-x86_64-darwin.gem -------------------------------------------------------------------------------- /vendor/cache/pry-0.13.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/pry-0.13.1.gem -------------------------------------------------------------------------------- /vendor/cache/racc-1.5.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/racc-1.5.2.gem -------------------------------------------------------------------------------- /vendor/cache/rack-2.2.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rack-2.2.3.gem -------------------------------------------------------------------------------- /vendor/cache/rack-test-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rack-test-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/rails-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rails-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/rails-dom-testing-2.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rails-dom-testing-2.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/rails-html-sanitizer-1.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rails-html-sanitizer-1.3.0.gem -------------------------------------------------------------------------------- /vendor/cache/railties-6.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/railties-6.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/rake-13.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rake-13.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-core-3.9.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rspec-core-3.9.3.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-expectations-3.9.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rspec-expectations-3.9.4.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-mocks-3.9.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rspec-mocks-3.9.1.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-rails-3.9.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rspec-rails-3.9.1.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-support-3.9.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/rspec-support-3.9.4.gem -------------------------------------------------------------------------------- /vendor/cache/sprockets-4.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/sprockets-4.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/sprockets-rails-3.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/sprockets-rails-3.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/thor-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/thor-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/tzinfo-2.0.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/tzinfo-2.0.4.gem -------------------------------------------------------------------------------- /vendor/cache/websocket-driver-0.7.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/websocket-driver-0.7.3.gem -------------------------------------------------------------------------------- /vendor/cache/websocket-extensions-0.1.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/websocket-extensions-0.1.5.gem -------------------------------------------------------------------------------- /vendor/cache/zeitwerk-2.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/chatops-controller/bded318ab51ccad4c5d267f2ef331b18d82a021a/vendor/cache/zeitwerk-2.4.2.gem --------------------------------------------------------------------------------