├── .coveralls.yml ├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── VERSION ├── example ├── app │ ├── controllers │ │ ├── application_controller.rb │ │ ├── home_controller.rb │ │ ├── organizations_controller.rb │ │ ├── sessions_controller.rb │ │ ├── teams_controller.rb │ │ └── users_controller.rb │ └── views │ │ ├── application │ │ └── not_found.html.erb │ │ ├── home │ │ └── show.html.erb │ │ ├── layouts │ │ └── application.html.erb │ │ ├── organizations │ │ ├── index.html.erb │ │ └── show.html.erb │ │ ├── teams │ │ └── show.html.erb │ │ └── users │ │ └── show.html.erb ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── cookies_serializer.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ └── warden_github_rails.rb │ ├── routes.rb │ └── secrets.yml └── script │ └── rails ├── lib └── warden │ └── github │ ├── rails.rb │ └── rails │ ├── config.rb │ ├── controller_helpers.rb │ ├── railtie.rb │ ├── routes.rb │ ├── test_helpers.rb │ ├── test_helpers │ └── mock_user.rb │ └── version.rb ├── spec ├── integration │ ├── controller_helpers_spec.rb │ ├── membership_spec.rb │ ├── route_spec.rb │ ├── scope_spec.rb │ └── view_helpers_spec.rb ├── rails_app │ ├── app │ │ ├── controllers │ │ │ ├── scoped_controller.rb │ │ │ ├── unscoped_controller.rb │ │ │ └── view_tests_controller.rb │ │ └── views │ │ │ └── view_tests │ │ │ ├── authenticated.html.erb │ │ │ └── user.html.erb │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── cookies_serializer.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ ├── warden_github_rails.rb │ │ │ └── wrap_parameters.rb │ │ ├── routes.rb │ │ └── secrets.yml │ └── script │ │ └── rails ├── spec_helper.rb └── unit │ ├── config_spec.rb │ ├── mock_user_spec.rb │ ├── rails_spec.rb │ └── test_helpers_spec.rb └── warden-github-rails.gemspec /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | spec/examples.txt 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | tags 20 | bin/ 21 | log 22 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | cache: bundler 4 | script: bundle exec rspec 5 | rvm: 6 | - 2.4.1 7 | env: 8 | matrix: 9 | - RAILS_VERSION=4 10 | - RAILS_VERSION=5 11 | - RAILS_VERSION=master 12 | matrix: 13 | fast_finish: true 14 | allow_failures: 15 | - env: RAILS_VERSION=master 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.3.0 4 | 5 | - Use new warden-github version that fixes a membership caching bug 6 | 7 | ## v1.2.3 8 | 9 | - Use custom session serialization for proper compatibility with mock users 10 | (#16) 11 | 12 | ## v1.2.2 13 | 14 | - Improve serialization and devise compatibility 15 | 16 | ## v1.2.1 17 | 18 | - Add support for multiple teams ([@bhuga]) 19 | 20 | ## v1.2.0 21 | 22 | - Add warden config to not intercept 401 status 23 | - Support Rails 4.2 default of JSON marshalling 24 | - Drop support for Rails 3.1 25 | 26 | ## v1.1.2 27 | 28 | - Prevent loss of memberships in mocked user when marshaling 29 | 30 | ## v1.1.1 31 | 32 | - Transform string to integer when stubbing team membership 33 | - Place redirect helper in correct rack module 34 | 35 | ## v1.1.0 36 | 37 | - Upgrade to octokit.rb version 2 38 | - Require ruby 1.9 or higher 39 | 40 | ## v1.0.1 41 | 42 | - Fully test on Rails 4 43 | - Improve mock user membership stubbing 44 | - Add testing instructions to README 45 | 46 | ## v1.0.0 47 | 48 | - Add Devise compatibility 49 | 50 | ## v0.0.1 51 | 52 | - Initial release 53 | 54 | [@bhuga]: https://github.com/bhuga 55 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | if ENV['EDGE'] 6 | gem 'warden-github', github: 'atmos/warden-github' 7 | end 8 | 9 | rails_version = ENV['RAILS_VERSION'] 10 | 11 | rails_opts = case rails_version 12 | when 'master' 13 | { github: 'rails/rails' } 14 | when nil 15 | {} 16 | else 17 | "~> #{rails_version}" 18 | end 19 | 20 | gem "rails", rails_opts 21 | 22 | group :development do 23 | unless ENV['CI'] 24 | gem 'byebug', require: false 25 | end 26 | end 27 | 28 | platforms :rbx do 29 | gem 'rubysl' 30 | gem 'racc' 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Philipe Fatio 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # warden-github-rails 2 | 3 | [![Build Status][build-image]][build-link] 4 | [![Gem Version][gem-image]][gem-link] 5 | [![Dependency Status][deps-image]][deps-link] 6 | [![Code Climate][gpa-image]][gpa-link] 7 | [![Coverage Status][cov-image]][cov-link] 8 | 9 | A gem for rails that provides easy GitHub OAuth integration. 10 | It is built on top of [warden-github](https://github.com/atmos/warden-github), which gives you an easy to use [warden](https://github.com/hassox/warden) strategy to authenticate GitHub users. 11 | 12 | ## Motivation 13 | 14 | **Wouldn't it be nice to** 15 | 16 | - use your organization and its teams for user access control? 17 | - add a new employee to your GitHub organization or team in order to grant them access to your app's admin area? 18 | 19 | The motivation for this gem was to provide a very easy authorization (not authentication) mechanism to existing rails apps for admins, especially in combination with organization and team memberships. 20 | The provided routing helpers do exactly that. 21 | They allow you to restrict access to members of your organization or a certain team. 22 | 23 | This is how your rails `routes.rb` could look like: 24 | 25 | ```ruby 26 | constraints(subdomain: 'admin') do 27 | github_authenticate(org: 'my_company_inc') do 28 | resources :users 29 | resources :projects 30 | 31 | github_authenticated(team: 'sysadmins') do 32 | resource :infrastructure 33 | end 34 | end 35 | end 36 | ``` 37 | 38 | Of course, this gem can also be used for user registration and authentication. 39 | Several helper methods are available in the controller to accomplish this: 40 | 41 | ```ruby 42 | class UsersController < ApplicationController 43 | # ... 44 | 45 | def new 46 | github_authenticate! # Performs OAuth flow when not logged in. 47 | @user = User.new(name: github_user.name, email: github_user.email) 48 | end 49 | 50 | def create 51 | attrs = params.require(:user).permit(:name, :email).merge(github_id: github_user.id) 52 | @user = User.create(attrs) 53 | 54 | if @user.persisted? 55 | redirect_to :show 56 | else 57 | render :new 58 | end 59 | end 60 | 61 | # ... 62 | end 63 | ``` 64 | 65 | ## Example App 66 | 67 | This repository includes an example app in [example/](example/). 68 | To play with it, follow these steps: 69 | 70 | 1. [Create an OAuth application in your GitHub settings](https://github.com/settings/applications/new). 71 | Set the callback URL to `http://localhost:3000/` 72 | 73 | 2. Check out this repo and run: 74 | 75 | ``` 76 | $ bundle 77 | $ cd example 78 | $ GITHUB_CLIENT_ID=your_id_from_step1 GITHUB_CLIENT_SECRET=your_secret_from_step1 bundle exec rails s 79 | ``` 80 | 81 | 3. Point your browser to [http://localhost:3000/](http://localhost:3000/) and enjoy! 82 | 83 | ## Installation 84 | 85 | To use this gem, add it to your `Gemfile`: 86 | 87 | ```ruby 88 | gem 'warden-github-rails' 89 | ``` 90 | 91 | If you're using devise, make sure to use version 2.2.4 or newer. 92 | Previous versions are not compatible with warden-github-rails and thus will not work. 93 | See the note at [*Using alongside Devise and other Warden Gems*](#using-alongside-devise-and-other-warden-gems) for an explanation. 94 | 95 | ## Usage 96 | 97 | ### Configuration 98 | 99 | First off, you might want to configure this gem by creating an initializer such as `config/initializers/warden_github_rails.rb`. 100 | There you can define: 101 | 102 | - various scopes and their configs (scopes are types of users with different configs) 103 | - the default scope (which is `:user` by default) 104 | - team aliases (GitHub teams are identified by a numerical ID; defining an alias for a team makes it easier to use) 105 | 106 | Here's how such a config might look like: 107 | 108 | ```ruby 109 | Warden::GitHub::Rails.setup do |config| 110 | config.add_scope :user, client_id: 'foo', 111 | client_secret: 'bar', 112 | scope: 'user:email' 113 | 114 | config.add_scope :admin, client_id: 'abc', 115 | client_secret: 'xyz', 116 | redirect_uri: '/admin/login/callback', 117 | scope: 'read:org' 118 | 119 | config.default_scope = :admin 120 | 121 | config.add_team :marketing, 456 122 | end 123 | ``` 124 | 125 | For a list of allowed config parameters to use in `#add_scope`, read the [warden-github documentation](https://github.com/atmos/warden-github#parameters). 126 | 127 | ### Inside `routes.rb` 128 | 129 | The available routing helpers are defined and documented in [lib/warden/github/rails/routes.rb](lib/warden/github/rails/routes.rb). 130 | They all accept an optional scope that, when omitted, falls back to the default_scope configured in the initializer. 131 | 132 | Examples: 133 | 134 | ```ruby 135 | # Performs login if not logged in already. 136 | github_authenticate do 137 | resource :profile 138 | end 139 | 140 | # Does not perform login when not logged in. 141 | github_authenticated do 142 | delete '/logout' => 'sessions#delete' 143 | end 144 | 145 | # Only matches when not logged in. Does not perform login. 146 | github_unauthenticated do 147 | resource :registration 148 | end 149 | 150 | # Only matches when member of the organization. Initiates login if not logged in. 151 | github_authenticate(org: 'my_company') do 152 | resource :admin 153 | end 154 | 155 | # Only matches when member of the team. Does not initiate login if not logged in. 156 | github_authenticated(team: 'markting') do 157 | get '/dashboard' => 'dashboard#show' 158 | end 159 | 160 | # Matches if a member of any of the teams given. Does not initiate login if not logged in. 161 | github_authenticated(team: ['markting', 'graphic-design']) do 162 | get '/dashboard' => 'dashboard#show' 163 | end 164 | 165 | # Using dynamic membership values: 166 | github_authenticate(org: lambda { |req| req.params[:id] }) do 167 | get '/orgs/:id' => 'orgs#show' 168 | end 169 | ``` 170 | 171 | ### Inside a Controller 172 | 173 | The available controller helpers are defined and documented in [lib/warden/github/rails/controller_helpers.rb](lib/warden/github/rails/controller_helpers.rb). 174 | They all accept an optional scope that, when omitted, falls back to the default_scope configured in the initializer. 175 | 176 | ```ruby 177 | class SomeController < ActionController::Base 178 | def show 179 | @is_admin = github_authenticated?(:admin) 180 | end 181 | 182 | def delete 183 | github_logout 184 | redirect_to '/' 185 | end 186 | 187 | def settings 188 | github_authenticate! 189 | @settings = UserSettings.find_by(github_user_id: github_user.id) 190 | end 191 | 192 | def finish_wizard 193 | github_session[:wizard_completed] = true 194 | end 195 | 196 | def followers 197 | @followers = github_user.api.followers 198 | end 199 | end 200 | ``` 201 | 202 | ### Communicating with the GitHub API 203 | 204 | Once a user is logged in, you'll have access to it in the controller using `github_user`. 205 | It is an instance of `Warden::GitHub::User` which is defined in the [warden-github](https://github.com/atmos/warden-github/blob/master/lib/warden/github/user.rb) gem. 206 | The instance has several methods to access user information such as `#name`, `#id`, `#email`, etc. 207 | It also features a method `#api` which returns a preconfigured [Octokit](https://github.com/octokit/octokit.rb) client for that user. 208 | 209 | ### Test Helpers 210 | 211 | This gems comes with a couple test helpers to make your life easier: 212 | 213 | - A method is added to `Rack::Response` called `#github_oauth_redirect?` which 214 | returns true if the response is a redirect to a url that starts with 215 | `https://github.com/login/oauth/authorize`. You can use it in your request 216 | tests to make sure the OAuth dance is initiated. In rspec you could verify 217 | this as follows: 218 | 219 | ```ruby 220 | subject { get '/some-url-that-triggers-oauth' } 221 | it { is_expected.to be_github_oauth_redirect } 222 | ``` 223 | 224 | - A mock user that allows you to stub team and organization memberships: 225 | 226 | ```ruby 227 | user = Warden::GitHub::Rails::TestHelpers::MockUser.new 228 | user.stub_membership(team: [234, 987], org: 'some-inc') 229 | user.team_member?(234) # => true 230 | user.organization_member?('rails') # => false 231 | ``` 232 | 233 | - A method that creates a mock user and logs it in. If desired, the scope can 234 | be specified. The method returns the mock user so that you can manipulate it 235 | further: 236 | 237 | ```ruby 238 | user = github_login(:admin) 239 | 240 | get '/org/rails/admin' 241 | expect(response).to be_not_found 242 | 243 | user.stub_membership(org: 'rails') 244 | get '/org/rails/admin' 245 | expect(response).to be_ok 246 | ``` 247 | 248 | In order to use the mock user and the `#github_login` method, make sure to `require 'warden/github/rails/test_helpers'` and to `include Warden::GitHub::Rails::TestHelpers` in your tests. 249 | 250 | ## Using alongside Devise and other Warden Gems 251 | 252 | Currently this gem does not play nicely with other gems that setup a warden middleware. 253 | The reason is that warden simply does not have support for multiple middlewares. 254 | The warden middleware configures a warden instance and adds it to the rack environment. 255 | Any other warden middleware downstream checks for any existing warden instance in the environment and, if present, skips itself. 256 | I've opened an [issue](https://github.com/hassox/warden/issues/67) on the warden repository to discuss possible workarounds. 257 | 258 | Nevertheless, this gem is compatible with devise for version 2.2.4 and newer. 259 | devise allows you to specify a block that will be invoked when the warden middleware is configured. 260 | This functionality is used in this gem in order to setup the github strategy for warden instead of inserting our own middleware. 261 | 262 | ## Additional Information 263 | 264 | ### Dependencies 265 | 266 | - [warden-github](https://github.com/atmos/warden-github) 267 | - [warden](https://github.com/hassox/warden) 268 | - [octokit](https://github.com/octokit/octokit.rb) 269 | 270 | ### Author 271 | 272 | Philipe Fatio ([@fphilipe][fphilipe twitter]) 273 | 274 | [![Support via Gittip][tip-image]][tip-link] 275 | 276 | ### License 277 | 278 | MIT License. Copyright 2013 Philipe Fatio 279 | 280 | [build-image]: https://travis-ci.org/fphilipe/warden-github-rails.svg 281 | [build-link]: https://travis-ci.org/fphilipe/warden-github-rails 282 | [gem-image]: https://badge.fury.io/rb/warden-github-rails.svg 283 | [gem-link]: https://rubygems.org/gems/warden-github-rails 284 | [deps-image]: https://gemnasium.com/fphilipe/warden-github-rails.svg 285 | [deps-link]: https://gemnasium.com/fphilipe/warden-github-rails 286 | [gpa-image]: https://codeclimate.com/github/fphilipe/warden-github-rails.svg 287 | [gpa-link]: https://codeclimate.com/github/fphilipe/warden-github-rails 288 | [cov-image]: https://coveralls.io/repos/fphilipe/warden-github-rails/badge.svg 289 | [cov-link]: https://coveralls.io/r/fphilipe/warden-github-rails 290 | [tip-image]: https://rawgithub.com/twolfson/gittip-badge/0.1.0/dist/gittip.png 291 | [tip-link]: https://www.gittip.com/fphilipe/ 292 | 293 | [fphilipe twitter]: https://twitter.com/fphilipe 294 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.3.0 2 | -------------------------------------------------------------------------------- /example/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | def not_found 3 | render status: 404 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /example/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /example/app/controllers/organizations_controller.rb: -------------------------------------------------------------------------------- 1 | class OrganizationsController < ApplicationController 2 | def index 3 | @orgs = github_user.api.organizations 4 | end 5 | 6 | def show 7 | @org = github_user.api.organization(params[:id]) 8 | @members = github_user.api.organization_members(params[:id]) 9 | @repos = github_user.api.organization_repositories(params[:id]) 10 | @teams = github_user.api.organization_teams(params[:id]) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /example/app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | def create 3 | github_authenticate! 4 | redirect_to root_url 5 | end 6 | 7 | def destroy 8 | github_logout 9 | redirect_to root_url 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /example/app/controllers/teams_controller.rb: -------------------------------------------------------------------------------- 1 | class TeamsController < ApplicationController 2 | def show 3 | @team = github_user.api.team(params[:id]) 4 | @repos = github_user.api.team_repositories(params[:id]) 5 | @members = github_user.api.team_members(params[:id]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /example/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | def show 3 | @user = github_user.api.user(params[:id]) 4 | @repos = github_user.api.repos(params[:id]) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /example/app/views/application/not_found.html.erb: -------------------------------------------------------------------------------- 1 |

404

2 | 3 |

Hmm..., looks like there's nothing here for you!

4 | -------------------------------------------------------------------------------- /example/app/views/home/show.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome

2 | 3 |

Try accessing an organization through urls like /orgs/rails. If you're not a member of that organization, you'll see a 404. Same for teams.

4 | -------------------------------------------------------------------------------- /example/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example App 5 | 51 | 52 | 53 | 54 |

Example App

55 | 56 | 68 | 69 |
70 | <%= yield %> 71 |
72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /example/app/views/organizations/index.html.erb: -------------------------------------------------------------------------------- 1 |

Your Organizations

2 | 3 | <%- if @orgs.empty? -%> 4 |

Looks like you are not a member of any organization :-(

5 | <%- else -%> 6 | 14 | <%- end -%> 15 | -------------------------------------------------------------------------------- /example/app/views/organizations/show.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= image_tag @org.rels[:avatar].href, width: 50, height: 50 %> 3 | <%= @org.login %> 4 |

5 | 6 |

Repositories

7 | <%- if @repos.empty? -%> 8 |

Looks like there are no repos in this organization :-(

9 | <%- else -%> 10 |
11 | <%- @repos.each do |repo| -%> 12 |
<%= link_to repo.name, repo.rels[:html].href %>
13 |
<%= repo.description %>
14 | <%- end -%> 15 |
16 | <%- end -%> 17 | 18 |

Teams

19 | 20 | <%- if @teams.empty? -%> 21 |

Looks like there are no teams :-(

22 | <%- else -%> 23 | 28 | <%- end -%> 29 | 30 |

Members

31 | 39 | -------------------------------------------------------------------------------- /example/app/views/teams/show.html.erb: -------------------------------------------------------------------------------- 1 |

<%= @team.name %>

2 | 3 |

Repositories

4 | <%- if @repos.empty? -%> 5 |

Looks like there are no repos in this team :-(

6 | <%- else -%> 7 |
8 | <%- @repos.each do |repo| -%> 9 |
<%= link_to repo.name, repo.rels[:html].href %>
10 |
<%= repo.description %>
11 | <%- end -%> 12 |
13 | <%- end -%> 14 | 15 |

Members

16 | <%- if @members.empty? -%> 17 |

Looks like there are no members in this team :-(

18 | <%- end -%> 19 | 27 | -------------------------------------------------------------------------------- /example/app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= image_tag @user.rels[:avatar].href, width: 50, height: 50 %> 3 | <%= @user.login %> 4 | <%- if @user.name.present? %> 5 | (<%= @user.name %>) 6 | <%- end -%> 7 |

8 | 9 |
10 |
Company:
11 |
<%= @user.company %>
12 |
Website:
13 |
<%= @user.blog %>
14 |
Bio:
15 |
<%= @user.bio %>
16 |
17 | 18 |

Repositories

19 | <%- if @repos.empty? -%> 20 |

Looks like this user has no repos :-(

21 | <%- else -%> 22 |
23 | <%- @repos.each do |repo| -%> 24 |
<%= link_to repo.name, repo.rels[:html].href %>
25 |
<%= repo.description %>
26 | <%- end -%> 27 |
28 | <%- end -%> 29 | -------------------------------------------------------------------------------- /example/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 Example::Application 5 | -------------------------------------------------------------------------------- /example/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "action_controller/railtie" 4 | 5 | if defined?(Bundler) 6 | Bundler.require(*Rails.groups(assets: %w(development test))) 7 | end 8 | 9 | module Example 10 | class Application < Rails::Application 11 | config.encoding = "utf-8" 12 | config.filter_parameters += [:password] 13 | config.active_support.escape_html_entities_in_json = true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /example/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Example::Application.initialize! 6 | -------------------------------------------------------------------------------- /example/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Example::Application.configure do 2 | config.eager_load = false 3 | config.cache_classes = false 4 | config.whiny_nils = true 5 | config.consider_all_requests_local = true 6 | config.action_controller.perform_caching = false 7 | config.active_support.deprecation = :log 8 | config.action_dispatch.best_standards_support = :builtin 9 | end 10 | -------------------------------------------------------------------------------- /example/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Example::Application.configure do 2 | config.eager_load = true 3 | config.cache_classes = true 4 | config.consider_all_requests_local = false 5 | config.action_controller.perform_caching = true 6 | config.active_support.deprecation = :notify 7 | end 8 | -------------------------------------------------------------------------------- /example/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Example::Application.configure do 2 | config.eager_load = true 3 | config.cache_classes = true 4 | config.whiny_nils = true 5 | config.consider_all_requests_local = true 6 | config.action_controller.perform_caching = false 7 | config.action_dispatch.show_exceptions = false 8 | config.action_controller.allow_forgery_protection = false 9 | config.active_support.deprecation = :stderr 10 | end 11 | -------------------------------------------------------------------------------- /example/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.action_dispatch.cookies_serializer = :json 2 | -------------------------------------------------------------------------------- /example/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | Example::Application.config.secret_token = '04556fc0ba5c1145e6ed14757385c328022aa5699aefa11e3a873464368ec3a706e0ac08be294444ba2f791b7ace27716c7a3ca9950cd2303e7445b4ae90faeb' 2 | -------------------------------------------------------------------------------- /example/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | Example::Application.config.session_store :cookie_store, key: '_example_session' 2 | -------------------------------------------------------------------------------- /example/config/initializers/warden_github_rails.rb: -------------------------------------------------------------------------------- 1 | Warden::GitHub::Rails.setup do |config| 2 | config.add_scope :user, redirect_uri: '/login', scope: 'user:email,read:org' 3 | end 4 | -------------------------------------------------------------------------------- /example/config/routes.rb: -------------------------------------------------------------------------------- 1 | Example::Application.routes.draw do 2 | github_authenticate do 3 | get '/orgs' => 'organizations#index', as: :orgs 4 | get '/profile' => 'users#show', as: :profile 5 | end 6 | 7 | github_authenticate(team: lambda { |req| req.params[:id] }) do 8 | get '/teams/:id' => 'teams#show', as: :team 9 | end 10 | 11 | github_authenticate(org: lambda { |req| req.params[:id] }) do 12 | get '/orgs/:id' => 'organizations#show', as: :org 13 | end 14 | 15 | get '/login' => 'sessions#create', as: :login 16 | get '/logout' => 'sessions#destroy', as: :logout 17 | 18 | resources :users 19 | 20 | root to: 'home#show' 21 | 22 | match '*all' => 'application#not_found', via: :all 23 | end 24 | -------------------------------------------------------------------------------- /example/config/secrets.yml: -------------------------------------------------------------------------------- 1 | development: 2 | secret_key_base: '566d4b32c28c60a1d533156df89a3ee3e44c2a35de63833f3ef912b7c69547d4f5d0c412e221fe635640b75295f452340c5456bb79466abf5fec59b6006ea859' 3 | -------------------------------------------------------------------------------- /example/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /lib/warden/github/rails.rb: -------------------------------------------------------------------------------- 1 | require 'warden/github' 2 | 3 | require 'warden/github/rails/version' 4 | require 'warden/github/rails/routes' 5 | require 'warden/github/rails/railtie' 6 | require 'warden/github/rails/config' 7 | require 'warden/github/rails/controller_helpers' 8 | 9 | require 'forwardable' 10 | 11 | module Warden 12 | module GitHub 13 | module Rails 14 | extend SingleForwardable 15 | 16 | def_delegators :config, 17 | :default_scope, 18 | :scopes, 19 | :team_id 20 | 21 | @config = Config.new 22 | 23 | def self.config 24 | @config 25 | end 26 | 27 | # Use this method to setup this gem. 28 | # 29 | # @example 30 | # 31 | # Warden::GitHub::Rails.setup do |config| 32 | # # ... 33 | # end 34 | def self.setup 35 | yield config 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/warden/github/rails/config.rb: -------------------------------------------------------------------------------- 1 | module Warden 2 | module GitHub 3 | module Rails 4 | class Config 5 | BadConfig = Class.new(StandardError) 6 | 7 | # Default scope to use when not explicitly specified. 8 | attr_accessor :default_scope 9 | 10 | # The list of scopes and their configs. This is used to add custom 11 | # configs to a specific scope. When using a scope that is not listed 12 | # here, it will use the default configs from warden-github. 13 | attr_reader :scopes 14 | 15 | # A hash containing team alias names and their numeric id. 16 | attr_reader :teams 17 | 18 | def initialize 19 | @default_scope = :user 20 | @scopes = {} 21 | @teams = {} 22 | end 23 | 24 | # Adds a scope with custom configurations to the list of scopes. 25 | def add_scope(name, config={}) 26 | scopes[name] = config 27 | end 28 | 29 | # Maps a team id to a name in order to easier reference it. 30 | def add_team(name, id) 31 | teams[name.to_sym] = Integer(id) 32 | end 33 | 34 | # Gets the team id for a team id or alias. 35 | def team_id(team) 36 | # In ruby 1.8 doing a Integer(:symbol) returns an integer. Thus, test 37 | # for symbol first. 38 | if team.is_a? Symbol 39 | teams.fetch(team) 40 | else 41 | Integer(team) rescue teams.fetch(team.to_sym) 42 | end 43 | rescue IndexError 44 | fail BadConfig, "No team id defined for team #{team}." 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/warden/github/rails/controller_helpers.rb: -------------------------------------------------------------------------------- 1 | module Warden 2 | module GitHub 3 | module Rails 4 | module ControllerHelpers 5 | def self.included(klass) 6 | klass.helper_method(:github_authenticated?, :github_user) 7 | end 8 | 9 | # Initiates the OAuth flow if not already authenticated for the 10 | # specified scope. 11 | def github_authenticate!(scope=Rails.default_scope) 12 | request.env['warden'].authenticate!(scope: scope) 13 | end 14 | 15 | # Logs out a user if currently logged in for the specified scope. 16 | def github_logout(scope=Rails.default_scope) 17 | request.env['warden'].logout(scope) 18 | end 19 | 20 | # Checks whether a user is logged in for the specified scope. 21 | def github_authenticated?(scope=Rails.default_scope) 22 | request.env['warden'].authenticated?(scope) 23 | end 24 | 25 | # Returns the currently signed in user for the specified scope. See the 26 | # documentation for Warden::GitHub::User for available methods. 27 | def github_user(scope=Rails.default_scope) 28 | request.env['warden'].user(scope) 29 | end 30 | 31 | # Accessor for the currently signed in user's session. This will be 32 | # cleared once logged out. 33 | def github_session(scope=Rails.default_scope) 34 | request.env['warden'].session(scope) if github_authenticated?(scope) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/warden/github/rails/railtie.rb: -------------------------------------------------------------------------------- 1 | module Warden 2 | module GitHub 3 | module Rails 4 | class Railtie < ::Rails::Railtie 5 | SERIALIZE_FROM_SESSION = -> ((class_name, data)) do 6 | class_name.constantize.new.tap { |user| user.marshal_load(data) } 7 | end 8 | SERIALIZE_INTO_SESSION = -> (user) do 9 | [user.class.name, user.marshal_dump] 10 | end 11 | 12 | initializer 'warden-github-rails.warden' do |app| 13 | # When devise is used, it inserts a warden middlware. Multiple warden 14 | # middlewares do not work properly. Devise allows for a block to be 15 | # specified that is invoked when its warden middleware is configured. 16 | # This makes it possible to setup warden-github-rails through devise. 17 | if defined?(::Devise) 18 | ::Devise.warden { |config| setup_scopes(config) } 19 | else 20 | app.config.middleware.use Warden::Manager do |config| 21 | setup_failure_app(config) 22 | setup_scopes(config) 23 | config.intercept_401 = false 24 | end 25 | end 26 | end 27 | 28 | initializer 'warden-github-rails.helpers' do 29 | ActiveSupport.on_load(:action_controller) do 30 | include ControllerHelpers 31 | end 32 | end 33 | 34 | def setup_scopes(config) 35 | Rails.scopes.each do |scope, scope_config| 36 | config.scope_defaults scope, strategies: [:github], 37 | config: scope_config 38 | config.serialize_from_session(scope, &SERIALIZE_FROM_SESSION) 39 | config.serialize_into_session(scope, &SERIALIZE_INTO_SESSION) 40 | end 41 | end 42 | 43 | def setup_failure_app(config) 44 | config.failure_app = lambda do |env| 45 | [403, {}, [env['warden'].message]] 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/warden/github/rails/routes.rb: -------------------------------------------------------------------------------- 1 | module Warden 2 | module GitHub 3 | module Rails 4 | module Routes 5 | # Enforces an authenticated GitHub user for the routes. If not 6 | # authenticated, it initiates the OAuth flow. 7 | # 8 | # Team and organization memberships can be checked by specifying a hash 9 | # such as `team: 'foobar'` or `org: 'my_company'`. 10 | def github_authenticate(scope=nil, options={}, &routes_block) 11 | github_constraint(scope, options, routes_block) do |warden, scope| 12 | warden.authenticate!(scope: scope) 13 | end 14 | end 15 | 16 | # The routes will only be visible to authenticated GitHub users. When 17 | # not authenticated, it does not initiate the OAuth flow. 18 | # 19 | # Team and organization memberships can be checked by specifying a hash 20 | # such as `team: 'foobar'` or `org: 'my_company'`. 21 | def github_authenticated(scope=nil, options={}, &routes_block) 22 | github_constraint(scope, options, routes_block) do |warden, scope| 23 | warden.authenticated?(scope: scope) 24 | end 25 | end 26 | 27 | # The routes will only be visible to all but authenticated GitHub users. 28 | # 29 | # This constraint currently does not check for memberships since of its 30 | # limited usage. 31 | def github_unauthenticated(scope=nil, options={}, &routes_block) 32 | github_constraint(scope, options, routes_block) do |warden, scope| 33 | not warden.authenticated?(scope: scope) 34 | end 35 | end 36 | 37 | private 38 | 39 | def github_constraint(scope, options, routes_block, &block) 40 | options, scope = scope, nil if scope.is_a? Hash 41 | scope ||= Rails.default_scope 42 | 43 | constraint = lambda do |request| 44 | warden = request.env['warden'] 45 | 46 | if block.call(warden, scope) 47 | if (user = warden.user(scope)) 48 | evaled_options = github_eval_options(options, request) 49 | github_enforce_options(user, evaled_options) 50 | else 51 | true 52 | end 53 | end 54 | end 55 | 56 | constraints(constraint, &routes_block) 57 | end 58 | 59 | def github_enforce_options(user, options) 60 | if (teams = options[:team]) 61 | Array(teams).any? { |team| user.team_member?(Rails.team_id(team)) } 62 | elsif (org = options[:org] || options[:organization]) 63 | user.organization_member?(org) 64 | else 65 | true 66 | end 67 | end 68 | 69 | def github_eval_options(options, request) 70 | Hash[options.map { |k,v| 71 | if v.is_a?(Proc) 72 | [k, v.call(request)] 73 | else 74 | [k, v] 75 | end 76 | }] 77 | end 78 | end 79 | end 80 | end 81 | end 82 | 83 | ActionDispatch::Routing::Mapper.send(:include, Warden::GitHub::Rails::Routes) 84 | -------------------------------------------------------------------------------- /lib/warden/github/rails/test_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'warden/github' 2 | require 'warden/github/rails/test_helpers/mock_user' 3 | 4 | module Warden 5 | module GitHub 6 | module Rails 7 | module TestHelpers 8 | include ::Warden::Test::Helpers 9 | 10 | # Login a mock GitHub user and return it. 11 | def github_login(scope=Rails.default_scope) 12 | MockUser.new.tap do |user| 13 | login_as(user, scope: scope) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | 21 | # Add a method to Rack::Response::Helpers to easily determine if a request 22 | # resulted in an OAuth redirect to GitHub. 23 | module Rack::Response::Helpers 24 | def github_oauth_redirect? 25 | redirect? && %r{https?://[^/]+/login/oauth/authorize\b} =~ location 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/warden/github/rails/test_helpers/mock_user.rb: -------------------------------------------------------------------------------- 1 | module Warden 2 | module GitHub 3 | module Rails 4 | module TestHelpers 5 | class MockUser < User 6 | def initialize(*args) 7 | super 8 | @memberships = { team: [], org: [] } 9 | end 10 | 11 | # Prevent the stubbed memberships from being overwritten by the hook 12 | # that sets the memberships stored in the session. 13 | def memberships=(val) end 14 | 15 | def stub_membership(args) 16 | args.each do |type, values| 17 | values = Array(values) 18 | values.map!(&:to_i) if type == :team 19 | memberships.fetch(type).concat(values) 20 | end 21 | end 22 | 23 | def team_member?(id) 24 | memberships[:team].include?(id) 25 | end 26 | 27 | def organization_member?(id) 28 | memberships[:org].include?(id) 29 | end 30 | 31 | def marshal_dump 32 | [memberships, super] 33 | end 34 | 35 | def marshal_load(data) 36 | memberships, super_data = data 37 | @memberships = memberships.symbolize_keys 38 | super(super_data) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/warden/github/rails/version.rb: -------------------------------------------------------------------------------- 1 | module Warden 2 | module GitHub 3 | module Rails 4 | VERSION = File.read( 5 | File.expand_path('../../../../../VERSION', __FILE__) 6 | ).strip 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/integration/controller_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'controller helpers' do 4 | { 5 | scoped: :admin, 6 | unscoped: Warden::GitHub::Rails.default_scope 7 | }.each do |type, scope| 8 | context "when using #{type}" do 9 | describe '#github_authenticate!' do 10 | subject(:request) { get "/#{type}/authenticate" } 11 | 12 | context 'when not logged in' do 13 | it 'initiates the oauth flow' do 14 | expect(request).to be_github_oauth_redirect 15 | end 16 | end 17 | 18 | context 'when logged in' do 19 | before { github_login(scope) } 20 | it 'does nothing' do 21 | expect(request).to be_ok 22 | end 23 | end 24 | end 25 | 26 | describe '#github_logout' do 27 | context 'when not logged in' do 28 | it 'does nothing' do 29 | expect(get("/#{type}/logout").body).to eq('false') 30 | end 31 | end 32 | 33 | context 'when logged in' do 34 | it 'logs out the user' do 35 | github_login(scope) 36 | expect(get("/#{type}/logout").body).to eq('true') 37 | expect(get("/#{type}/logout").body).to eq('false') 38 | end 39 | end 40 | end 41 | 42 | describe '#github_authenticated?' do 43 | subject(:request) { get "/#{type}/authenticated" } 44 | 45 | context 'when not logged in' do 46 | it 'returns false' do 47 | expect(request.body).to eq('false') 48 | end 49 | end 50 | 51 | context 'when logged in' do 52 | it 'returns true' do 53 | github_login(scope) 54 | expect(request.body).to eq('true') 55 | end 56 | end 57 | end 58 | 59 | describe '#github_user' do 60 | subject(:request) { get "/#{type}/user" } 61 | 62 | context 'when not logged in' do 63 | it 'returns nil' do 64 | expect(request.body).to be_blank 65 | end 66 | end 67 | 68 | context 'when logged in' do 69 | it 'returns the logged in user' do 70 | github_login(scope) 71 | expect(request.body).to \ 72 | include('Warden::GitHub::Rails::TestHelpers::MockUser') 73 | end 74 | end 75 | end 76 | 77 | describe '#github_session' do 78 | subject(:request) { get "/#{type}/session" } 79 | 80 | context 'when not logged in' do 81 | it 'returns nil' do 82 | expect(request.body).to eq('null') 83 | end 84 | end 85 | 86 | context 'when logged in' do 87 | it "returns the user's session" do 88 | github_login(scope) 89 | expect(request.body).to eq({ _memberships: {}, foo: :bar }.to_json) 90 | end 91 | end 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/integration/membership_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'request to a protected resource' do 4 | context 'that requires a team membership' do 5 | context 'which is specified by a numeric team id' do 6 | subject { get '/team/protected' } 7 | 8 | context 'when not logged in' do 9 | it { is_expected.to be_github_oauth_redirect } 10 | end 11 | 12 | context 'when logged in' do 13 | context 'and team member' do 14 | before do 15 | user = github_login 16 | user.stub_membership(team: 123) 17 | end 18 | 19 | it { is_expected.to be_ok } 20 | end 21 | 22 | context 'and not team member' do 23 | before { github_login } 24 | it { is_expected.to be_not_found } 25 | end 26 | end 27 | end 28 | 29 | context 'which is specified by multiple numeric team ids' do 30 | subject { get '/multi_team/protected' } 31 | 32 | context 'when not logged in' do 33 | it { is_expected.to be_github_oauth_redirect } 34 | end 35 | 36 | context 'when logged in' do 37 | context 'and a first team member' do 38 | before do 39 | user = github_login 40 | user.stub_membership(team: 123) 41 | end 42 | 43 | it { is_expected.to be_ok } 44 | end 45 | 46 | context 'and another team member' do 47 | before do 48 | user = github_login 49 | user.stub_membership(team: 345) 50 | end 51 | 52 | it { is_expected.to be_ok } 53 | end 54 | 55 | context 'and not team member' do 56 | before { github_login } 57 | it { is_expected.to be_not_found } 58 | end 59 | end 60 | end 61 | 62 | context 'which is specified by a team alias' do 63 | subject { get '/multi_team_alias/protected' } 64 | 65 | context 'when not logged in' do 66 | it { is_expected.to be_github_oauth_redirect } 67 | end 68 | 69 | context 'when logged in' do 70 | context 'and a first team member' do 71 | before do 72 | user = github_login 73 | user.stub_membership(team: 456) 74 | end 75 | 76 | it { is_expected.to be_ok } 77 | end 78 | 79 | context 'and another team member' do 80 | before do 81 | user = github_login 82 | user.stub_membership(team: 789) 83 | end 84 | 85 | it { is_expected.to be_ok } 86 | end 87 | 88 | context 'and not team member' do 89 | before { github_login } 90 | it { is_expected.to be_not_found } 91 | end 92 | end 93 | end 94 | 95 | context 'which is specified by multiple team aliases' do 96 | subject { get '/team_alias/protected' } 97 | 98 | context 'when not logged in' do 99 | it { is_expected.to be_github_oauth_redirect } 100 | end 101 | 102 | context 'when logged in' do 103 | context 'and team member' do 104 | before do 105 | user = github_login 106 | user.stub_membership(team: 456) 107 | end 108 | 109 | it { is_expected.to be_ok } 110 | end 111 | 112 | context 'and not team member' do 113 | before { github_login } 114 | it { is_expected.to be_not_found } 115 | end 116 | end 117 | end 118 | 119 | context 'which is specified by a lambda' do 120 | subject { get '/dynamic_team/123' } 121 | 122 | context 'when logged in' do 123 | context 'and team member' do 124 | before do 125 | user = github_login 126 | user.stub_membership(team: 123) 127 | end 128 | 129 | it { is_expected.to be_ok } 130 | end 131 | 132 | context 'and not team member' do 133 | before { github_login } 134 | it { is_expected.to be_not_found } 135 | end 136 | end 137 | end 138 | end 139 | 140 | context 'that requires an organization membership' do 141 | { org: :foobar_inc, organization: 'some_org' }.each do |key, value| 142 | context "which is specified as #{key}" do 143 | subject { get "/#{key}/protected" } 144 | 145 | context 'when not logged in' do 146 | it { is_expected.to be_github_oauth_redirect } 147 | end 148 | 149 | context 'when logged in' do 150 | context 'and organization member' do 151 | before do 152 | user = github_login 153 | user.stub_membership(org: value) 154 | end 155 | 156 | it { is_expected.to be_ok } 157 | end 158 | 159 | context 'and not organization member' do 160 | before { github_login } 161 | it { is_expected.to be_not_found } 162 | end 163 | end 164 | end 165 | end 166 | 167 | context 'which is specified by a lambda' do 168 | subject { get '/dynamic_org/some_org' } 169 | 170 | context 'when logged in' do 171 | context 'and organization member' do 172 | before do 173 | user = github_login 174 | user.stub_membership(org: 'some_org') 175 | end 176 | 177 | it { is_expected.to be_ok } 178 | end 179 | 180 | context 'and not organization member' do 181 | before { github_login } 182 | it { is_expected.to be_not_found } 183 | end 184 | end 185 | end 186 | end 187 | end 188 | 189 | describe 'request to a resource that only exists when logged in' do 190 | context 'that requires a team membership' do 191 | context 'which is specified by a numeric team id' do 192 | subject { get '/team/conditional' } 193 | 194 | context 'when team member' do 195 | before do 196 | user = github_login 197 | user.stub_membership(team: 123) 198 | end 199 | 200 | it { is_expected.to be_ok } 201 | end 202 | 203 | context 'when not team member' do 204 | before { github_login } 205 | it { is_expected.to be_not_found } 206 | end 207 | end 208 | 209 | context 'which is specified by multiple numeric team ids' do 210 | subject { get '/multi_team/conditional' } 211 | 212 | context 'when a first team member' do 213 | before do 214 | user = github_login 215 | user.stub_membership(team: 123) 216 | end 217 | 218 | it { is_expected.to be_ok } 219 | end 220 | 221 | context 'when another team member' do 222 | before do 223 | user = github_login 224 | user.stub_membership(team: 345) 225 | end 226 | 227 | it { is_expected.to be_ok } 228 | end 229 | 230 | context 'when not team member' do 231 | before { github_login } 232 | it { is_expected.to be_not_found } 233 | end 234 | end 235 | 236 | context 'which is specified by a team alias' do 237 | subject { get '/team_alias/conditional' } 238 | 239 | context 'when team member' do 240 | before do 241 | user = github_login 242 | user.stub_membership(team: 456) 243 | end 244 | 245 | it { is_expected.to be_ok } 246 | end 247 | 248 | context 'when not team member' do 249 | before { github_login } 250 | it { is_expected.to be_not_found } 251 | end 252 | end 253 | 254 | context 'which is specified by multiple team aliases' do 255 | subject { get '/multi_team_alias/conditional' } 256 | 257 | context 'when a member of the first team' do 258 | before do 259 | user = github_login 260 | user.stub_membership(team: 456) 261 | end 262 | 263 | it { is_expected.to be_ok } 264 | end 265 | 266 | context 'when a member of another team' do 267 | before do 268 | user = github_login 269 | user.stub_membership(team: 789) 270 | end 271 | 272 | it { is_expected.to be_ok } 273 | end 274 | 275 | context 'when not a team member' do 276 | before { github_login } 277 | it { is_expected.to be_not_found } 278 | end 279 | end 280 | end 281 | 282 | context 'that requires an organization membership' do 283 | { org: :foobar_inc, organization: 'some_org' }.each do |key, value| 284 | context "which is specified as #{key}" do 285 | subject { get "/#{key}/conditional" } 286 | 287 | context 'when organization member' do 288 | before do 289 | user = github_login 290 | user.stub_membership(org: value) 291 | end 292 | 293 | it { is_expected.to be_ok } 294 | end 295 | 296 | context 'when not organization member' do 297 | before { github_login } 298 | it { is_expected.to be_not_found } 299 | end 300 | end 301 | end 302 | end 303 | end 304 | -------------------------------------------------------------------------------- /spec/integration/route_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'request to a protected resource' do 4 | subject { get '/protected' } 5 | 6 | context 'when not logged in' do 7 | it { is_expected.to be_github_oauth_redirect } 8 | end 9 | 10 | context 'when logged in' do 11 | before { github_login } 12 | it { is_expected.to be_ok } 13 | end 14 | 15 | context 'with multiple scopes' do 16 | subject { get '/admin/protected' } 17 | 18 | context 'when logged in in the wrong scope' do 19 | before { github_login } 20 | it { is_expected.to be_github_oauth_redirect } 21 | end 22 | 23 | context 'when logged in in the correct scope' do 24 | before { github_login(:admin) } 25 | it { is_expected.to be_ok } 26 | end 27 | end 28 | end 29 | 30 | describe 'request to a resource that only exists when logged in' do 31 | subject { get '/conditional' } 32 | 33 | context 'when not logged in' do 34 | it { is_expected.to be_not_found } 35 | end 36 | 37 | context 'when logged in' do 38 | before { github_login } 39 | it { is_expected.to be_ok } 40 | end 41 | 42 | context 'with mutliple scopes' do 43 | subject { get '/admin/conditional' } 44 | 45 | context 'when logged in in the wrong scope' do 46 | before { github_login } 47 | it { is_expected.to be_not_found } 48 | end 49 | 50 | context 'when logged in in the correct scope' do 51 | before { github_login(:admin) } 52 | it { is_expected.to be_ok } 53 | end 54 | end 55 | end 56 | 57 | describe 'request to a resource that only exists when logged out' do 58 | subject { get '/conditional_inverse' } 59 | 60 | context 'when not logged in' do 61 | it { is_expected.to be_ok } 62 | end 63 | 64 | context 'when logged in' do 65 | before { github_login } 66 | it { is_expected.to be_not_found } 67 | end 68 | 69 | context 'with mutliple scopes' do 70 | subject { get '/admin/conditional_inverse' } 71 | 72 | context 'when logged in in the wrong scope' do 73 | before { github_login } 74 | it { is_expected.to be_ok } 75 | end 76 | 77 | context 'when logged in in the correct scope' do 78 | before { github_login(:admin) } 79 | it { is_expected.to be_not_found } 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/integration/scope_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Test if the configs in rails_app/config/initializers/warden_github_rails.rb 4 | # are actually being set and used by warden. 5 | describe 'request to custom configured scope' do 6 | def test_redirect(url, args) 7 | request = get url 8 | params = Addressable::URI.parse(request.location).query_values 9 | 10 | expect(request).to be_github_oauth_redirect 11 | expect(params.fetch('client_id')).to eq(args.fetch(:client_id)) 12 | expect(params.fetch('redirect_uri')).to match(args.fetch(:redirect_uri)) 13 | expect(params.fetch('scope')).to eq(args.fetch(:scope)) 14 | end 15 | 16 | context 'user' do 17 | it 'passes the correct configs to the oauth flow' do 18 | test_redirect('/protected', 19 | client_id: 'foo', 20 | redirect_uri: /\/protected$/, 21 | scope: 'user') 22 | end 23 | end 24 | 25 | context 'admin' do 26 | it 'passes the correct configs to the oauth flow' do 27 | test_redirect('/admin/protected', 28 | client_id: 'abc', 29 | redirect_uri: /\/admin\/login\/callback$/, 30 | scope: 'repo') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/integration/view_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'view helpers' do 4 | describe '#github_user' do 5 | subject(:request) { get "/view_tests/user" } 6 | 7 | it 'is defined' do 8 | expect(request.body).to include('true') 9 | end 10 | end 11 | 12 | describe '#github_authenticated?' do 13 | subject(:request) { get "/view_tests/authenticated" } 14 | 15 | it 'is defined' do 16 | expect(request.body).to include('true') 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/scoped_controller.rb: -------------------------------------------------------------------------------- 1 | class ScopedController < ActionController::Base 2 | def authenticate 3 | github_authenticate!(:admin) 4 | head :ok 5 | end 6 | 7 | def logout 8 | was_logged_in = !github_user(:admin).nil? 9 | github_logout(:admin) 10 | render plain: was_logged_in 11 | end 12 | 13 | def authenticated 14 | render plain: github_authenticated?(:admin) 15 | end 16 | 17 | def user 18 | render plain: github_user(:admin) 19 | end 20 | 21 | def session 22 | if github_session(:admin) 23 | github_session(:admin)[:foo] = :bar 24 | end 25 | 26 | render json: github_session(:admin) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/unscoped_controller.rb: -------------------------------------------------------------------------------- 1 | class UnscopedController < ActionController::Base 2 | def authenticate 3 | github_authenticate! 4 | head :ok 5 | end 6 | 7 | def logout 8 | was_logged_in = !github_user.nil? 9 | github_logout 10 | render plain: was_logged_in 11 | end 12 | 13 | def authenticated 14 | render plain: github_authenticated? 15 | end 16 | 17 | def user 18 | render plain: github_user 19 | end 20 | 21 | def session 22 | if github_session 23 | github_session[:foo] = :bar 24 | end 25 | 26 | render json: github_session 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/view_tests_controller.rb: -------------------------------------------------------------------------------- 1 | class ViewTestsController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/view_tests/authenticated.html.erb: -------------------------------------------------------------------------------- 1 | <%= !!defined?(github_authenticated?) %> 2 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/view_tests/user.html.erb: -------------------------------------------------------------------------------- 1 | <%= !!defined?(github_user) %> 2 | -------------------------------------------------------------------------------- /spec/rails_app/config.ru: -------------------------------------------------------------------------------- 1 | require ::File.expand_path('../config/environment', __FILE__) 2 | run RailsApp::Application 3 | -------------------------------------------------------------------------------- /spec/rails_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'action_controller/railtie' 4 | require 'warden/github/rails' 5 | 6 | unless Rails.env.test? 7 | begin 8 | require 'debugger' 9 | rescue LoadError 10 | require 'ruby-debug' 11 | end 12 | end 13 | 14 | module RailsApp 15 | class Application < Rails::Application 16 | config.encoding = "utf-8" 17 | config.filter_parameters += [:password] 18 | config.active_support.escape_html_entities_in_json = true 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/rails_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | $:.unshift File.expand_path('../../../../lib', __FILE__) 4 | -------------------------------------------------------------------------------- /spec/rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../application', __FILE__) 2 | 3 | RailsApp::Application.initialize! 4 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | config.eager_load = false 3 | config.cache_classes = false 4 | config.whiny_nils = true 5 | config.consider_all_requests_local = true 6 | config.action_controller.perform_caching = false 7 | config.active_support.deprecation = :log 8 | config.action_dispatch.best_standards_support = :builtin 9 | end 10 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | config.eager_load = true 3 | config.cache_classes = true 4 | config.consider_all_requests_local = false 5 | config.action_controller.perform_caching = true 6 | config.i18n.fallbacks = true 7 | config.active_support.deprecation = :notify 8 | end 9 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | config.eager_load = true 3 | config.cache_classes = true 4 | config.whiny_nils = true 5 | config.consider_all_requests_local = true 6 | config.action_controller.perform_caching = false 7 | config.action_dispatch.show_exceptions = false 8 | config.action_controller.allow_forgery_protection = false 9 | config.active_support.deprecation = :stderr 10 | end 11 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.action_dispatch.cookies_serializer = :json 2 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.config.secret_token = '81fe673d8f0ffe90945ee8d493f8c0c474dc18ab9ff0a7e58af85d7870a56f3324f0d034131e5b5c2816c8a690cd13f9a46f484222dad475b734462b90e90527' 2 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.config.session_store :cookie_store, key: '_rails_app_session' 2 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/warden_github_rails.rb: -------------------------------------------------------------------------------- 1 | Warden::GitHub::Rails.setup do |config| 2 | config.add_scope :user, client_id: ENV['GITHUB_CLIENT_ID'] || 'foo', 3 | client_secret: ENV['GITHUB_CLIENT_SECRET'] || 'bar', 4 | scope: 'user' 5 | 6 | config.add_scope :admin, client_id: ENV['GITHUB_CLIENT_ID'] || 'abc', 7 | client_secret: ENV['GITHUB_CLIENT_SECRET'] || 'xyz', 8 | redirect_uri: '/admin/login/callback', 9 | scope: 'repo' 10 | 11 | config.add_team :marketing, 456 12 | config.add_team :interns, 789 13 | end 14 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport.on_load(:action_controller) do 2 | wrap_parameters format: [:json] 3 | end 4 | 5 | -------------------------------------------------------------------------------- /spec/rails_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.routes.draw do 2 | responses = { 3 | 200 => lambda { |env| [200, {}, ['welcome']] }, 4 | 404 => lambda { |env| [404, {}, ['not found']] } 5 | } 6 | 7 | # Routes for route constraints tests: 8 | 9 | github_authenticate { get '/protected' => responses[200] } 10 | github_authenticated { get '/conditional' => responses[200] } 11 | github_unauthenticated { get '/conditional_inverse' => responses[200] } 12 | 13 | github_authenticate(:admin) { get '/admin/protected' => responses[200] } 14 | github_authenticate(:admin) { get '/admin/login/callback' => redirect('/admin/protected') } 15 | github_authenticated(:admin) { get '/admin/conditional' => responses[200] } 16 | github_unauthenticated(:admin) { get '/admin/conditional_inverse' => responses[200] } 17 | 18 | github_authenticate(team: 123) { get '/team/protected' => responses[200] } 19 | github_authenticated(team: 123) { get '/team/conditional' => responses[200] } 20 | github_authenticate(team: [123, 345]) { get '/multi_team/protected' => responses[200] } 21 | github_authenticated(team: [123, 345]) { get '/multi_team/conditional' => responses[200] } 22 | 23 | github_authenticate(team: :marketing) { get '/team_alias/protected' => responses[200] } 24 | github_authenticated(team: :marketing) { get '/team_alias/conditional' => responses[200] } 25 | github_authenticate(team: [:marketing, :interns]) { get '/multi_team_alias/protected' => responses[200] } 26 | github_authenticated(team: [:marketing, :interns]) { get '/multi_team_alias/conditional' => responses[200] } 27 | 28 | github_authenticate(org: :foobar_inc) { get '/org/protected' => responses[200] } 29 | github_authenticated(org: :foobar_inc) { get '/org/conditional' => responses[200] } 30 | 31 | github_authenticate(organization: 'some_org') { get '/organization/protected' => responses[200] } 32 | github_authenticated(organization: 'some_org') { get '/organization/conditional' => responses[200] } 33 | 34 | github_authenticated(org: lambda { |req| req.params[:id] }) do 35 | get '/dynamic_org/:id' => responses[200] 36 | end 37 | 38 | github_authenticated(team: lambda { |req| req.params[:id] }) do 39 | get '/dynamic_team/:id' => responses[200] 40 | end 41 | 42 | # Routes for controller helpers tests: 43 | 44 | %w[ unscoped scoped ].each do |type| 45 | %w[ authenticate logout authenticated user session ].each do |method| 46 | get "/#{type}/#{method}" => "#{type}##{method}" 47 | end 48 | end 49 | 50 | %w[ user authenticated ].each do |method| 51 | get "/view_tests/#{method}" => "view_tests##{method}" 52 | end 53 | 54 | # Everything else should be a 404: 55 | 56 | get '*all' => responses[404] 57 | end 58 | -------------------------------------------------------------------------------- /spec/rails_app/config/secrets.yml: -------------------------------------------------------------------------------- 1 | development: &dev 2 | secret_key_base: '566d4b32c28c60a1d533156df89a3ee3e44c2a35de63833f3ef912b7c69547d4f5d0c412e221fe635640b75295f452340c5456bb79466abf5fec59b6006ea859' 3 | 4 | test: *dev 5 | -------------------------------------------------------------------------------- /spec/rails_app/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if RUBY_ENGINE == 'ruby' 2 | if ENV['CI'] 3 | require 'coveralls' 4 | Coveralls::Output.silent = true 5 | Coveralls.wear! do 6 | add_filter 'spec/' 7 | add_filter 'example/' 8 | end 9 | else 10 | require 'simplecov' 11 | SimpleCov.start 12 | end 13 | end 14 | 15 | require 'rack/test' 16 | require 'warden/github/rails/test_helpers' 17 | require 'addressable/uri' 18 | 19 | # Load the test rails app: 20 | ENV['RAILS_ENV'] ||= 'test' 21 | require 'rails_app/config/environment' 22 | 23 | # Setup configs needed to run: 24 | ENV['GITHUB_CLIENT_ID'] = 'test_client_id' 25 | ENV['GITHUB_CLIENT_SECRET'] = 'test_client_secret' 26 | 27 | RSpec.configure do |config| 28 | config.example_status_persistence_file_path = 'spec/examples.txt' 29 | config.run_all_when_everything_filtered = true 30 | config.order = 'random' 31 | config.expect_with :rspec do |c| 32 | c.syntax = :expect 33 | end 34 | 35 | config.include Rack::Test::Methods 36 | config.include Warden::GitHub::Rails::TestHelpers 37 | 38 | # Reset warden's login states after each test. 39 | config.after { Warden.test_reset! } 40 | 41 | # This is how rack-test gets access to the app. 42 | def app 43 | RailsApp::Application 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/unit/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Warden::GitHub::Rails::Config do 4 | subject(:config) { described_class.new } 5 | 6 | describe '#default_scope' do 7 | it 'defaults to :user' do 8 | expect(config.default_scope).to eq(:user) 9 | end 10 | end 11 | 12 | describe '#scopes' do 13 | it 'defaults to an empty hash' do 14 | expect(config.scopes).to eq({}) 15 | end 16 | end 17 | 18 | describe '#teams' do 19 | it 'defaults to an empty hash' do 20 | expect(config.scopes).to eq({}) 21 | end 22 | end 23 | 24 | describe '#add_scope' do 25 | it 'adds a scope with its configs' do 26 | scope_config = double 27 | config.add_scope :admin, scope_config 28 | expect(config.scopes[:admin]).to eq(scope_config) 29 | end 30 | end 31 | 32 | describe '#add_team' do 33 | it 'adds a name mapping for a team' do 34 | config.add_team :marketing, 1234 35 | expect(config.teams[:marketing]).to eq(1234) 36 | end 37 | 38 | it 'normalizes the input' do 39 | config.add_team 'marketing', '1234' 40 | expect(config.teams[:marketing]).to eq(1234) 41 | end 42 | end 43 | 44 | describe '#team_id' do 45 | context 'when passed a numeric value' do 46 | it 'returns that value' do 47 | expect(config.team_id('1234')).to eq(1234) 48 | expect(config.team_id(1234)).to eq(1234) 49 | end 50 | end 51 | 52 | context 'when passed an alias' do 53 | it 'returns the id for the alias' do 54 | config.add_team :marketing, 1234 55 | expect(config.team_id(:marketing)).to eq(1234) 56 | expect(config.team_id('marketing')).to eq(1234) 57 | end 58 | end 59 | 60 | context 'when no mapping exists' do 61 | it 'raises a BadConfig' do 62 | expect { config.team_id(:foobar) }. 63 | to raise_error(Warden::GitHub::Rails::Config::BadConfig) 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/unit/mock_user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Warden::GitHub::Rails::TestHelpers::MockUser do 4 | it { is_expected.to be_a Warden::GitHub::User } 5 | 6 | describe '#stub_membership' do 7 | subject(:user) { described_class.new } 8 | 9 | it 'stubs memberships' do 10 | expect(user).not_to be_team_member(123) 11 | expect(user).not_to be_team_member(456) 12 | expect(user).not_to be_organization_member('foobar') 13 | 14 | user.stub_membership(org: 'foobar', team: [123, '456']) 15 | 16 | expect(user).to be_team_member(123) 17 | expect(user).to be_team_member(456) 18 | expect(user).to be_organization_member('foobar') 19 | end 20 | end 21 | 22 | it 'can be serialized and deserialized with JSON' do 23 | user = described_class.new 24 | user.stub_membership(org: ['apple', 'facebook'], team: [12, 34]) 25 | 26 | json = ActiveSupport::JSON.encode(user.marshal_dump) 27 | marshaled_user = described_class.new.tap do |u| 28 | u.marshal_load(ActiveSupport::JSON.decode(json)) 29 | end 30 | 31 | expect(marshaled_user.memberships).to eq(user.memberships) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/rails_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Warden::GitHub::Rails do 4 | describe '.setup' do 5 | it 'yields a config instance' do 6 | described_class.setup do |config| 7 | expect(config).to be_a(described_class::Config) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/unit/test_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Warden::GitHub::Rails::TestHelpers do 4 | describe '#github_login' do 5 | before { allow(self).to receive(:login_as) } 6 | 7 | context 'when no scope is specified' do 8 | it 'uses the default scope from config to login' do 9 | allow(Warden::GitHub::Rails).to receive(:default_scope) { :foobar } 10 | 11 | github_login 12 | 13 | expect(self).to have_received(:login_as).with( 14 | an_instance_of(Warden::GitHub::Rails::TestHelpers::MockUser), 15 | match(scope: :foobar) 16 | ) 17 | end 18 | end 19 | 20 | context 'when a scope is specified' do 21 | it 'uses that scope to login' do 22 | github_login(:admin) 23 | 24 | expect(self).to have_received(:login_as).with( 25 | an_instance_of(Warden::GitHub::Rails::TestHelpers::MockUser), 26 | match(scope: :admin) 27 | ) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /warden-github-rails.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'warden/github/rails/version' 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'warden-github-rails' 7 | gem.version = Warden::GitHub::Rails::VERSION 8 | gem.authors = ['Philipe Fatio'] 9 | gem.email = ['me@phili.pe'] 10 | gem.summary = %q{An easy drop in solution for rails to use GitHub authentication.} 11 | gem.description = gem.summary 12 | gem.homepage = 'https://github.com/fphilipe/warden-github-rails' 13 | 14 | gem.files = `git ls-files`.split($/) - Dir.glob('example/**/*') 15 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 16 | 17 | gem.add_development_dependency 'rspec', '~> 3.1' 18 | gem.add_development_dependency 'rails', '>= 3.2' 19 | gem.add_development_dependency 'rack-test', '~> 0.6' 20 | gem.add_development_dependency 'addressable', '~> 2.3' 21 | gem.add_development_dependency 'coveralls' if RUBY_ENGINE == 'ruby' 22 | 23 | gem.add_dependency 'warden-github', '~> 1.3', '>= 1.3.2' 24 | gem.add_dependency 'railties', '>= 3.1' 25 | end 26 | --------------------------------------------------------------------------------