warden-github 0.13.0 → 0.13.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,18 +1,141 @@
1
- warden-github
2
- =============
1
+ # warden-github
3
2
 
4
- A [warden](/hassox/warden) strategy that provides oauth authentication to github. Find out more about enabling your application at github's [oauth quickstart](http://gist.github.com/419219).
3
+ A [warden](https://github.com/hassox/warden) strategy that provides OAuth authentication to GitHub.
5
4
 
6
- To test it out on localhost set your callback url to 'http://localhost:9292/'.
5
+ ## The Extension in Action
7
6
 
8
- There's an example app in [example/app.rb](/atmos/warden-github/blob/master/example/app.rb).
7
+ To play with the extension, follow these steps:
9
8
 
10
- Using with GitHub Enterprise
11
- ============================
9
+ 1. Check out a copy of the source.
10
+ 2. [Create an application on GitHub](https://github.com/settings/applications/new) and set the callback URL to `http://localhost:9292`
11
+ 3. Run the following command with the client id and client secret obtained from the previous step:
12
12
 
13
- Export the `OCTOKIT_API_ENDPOINT` environmental variable to the URL of your enterprise install.
13
+ GITHUB_CLIENT_ID="<from GH>" GITHUB_CLIENT_SECRET="<from GH>" bundle exec rackup
14
14
 
15
- The Extension in Action
16
- =======================
15
+ This will run the example app [example/simple_app.rb](example/simple_app.rb).
16
+
17
+ If you wish to see multiple user scopes in action, run the above command with an additional variable:
18
+
19
+ MULTI_SCOPE_APP=1 GITHUB_CLIENT_ID="<from GH>" GITHUB_CLIENT_SECRET="<from GH>" bundle exec rackup
20
+
21
+ This will run the example app [example/multi_scope_app.rb](example/multi_scope_app.rb).
22
+
23
+ 4. Point your browser at [http://localhost:9292/](http://localhost:9292) and enjoy!
24
+
25
+ ## Configuration
26
+
27
+ In order to use this strategy, simply tell warden about it.
28
+ This is done by using `Warden::Manager` as a rack middleware and passing a config block to it.
29
+ Read more about warden setup at the [warden wiki](https://github.com/hassox/warden/wiki/Setup).
30
+
31
+ For simple usage without customization, simply specify it as the default strategy.
32
+
33
+ ```ruby
34
+ use Warden::Manager do |config|
35
+ config.failure_app = BadAuthentication
36
+ config.default_strategies :github
37
+ end
38
+ ```
39
+
40
+ In order to pass custom configurations, you need to configure a warden scope.
41
+ Note that the default warden scope (i.e. when not specifying any explicit scope) is `:default`.
42
+
43
+ Here's an example that specifies configs for the default scope and a custom admin scope.
44
+ Using multiple scopes allows you to have different user types.
45
+
46
+ ```ruby
47
+ use Warden::Manager do |config|
48
+ config.failure_app = BadAuthentication
49
+ config.default_strategies :github
50
+
51
+ config.scope_defaults :default, :config => { :scope => 'user:email' }
52
+ config.scope_defaults :admin, :config => { :client_id => 'foobar',
53
+ :client_secret => 'barfoo',
54
+ :scope => 'user,repo',
55
+ :redirect_uri => '/admin/oauth/callback' }
56
+ end
57
+ ```
58
+
59
+ ### Parameters
60
+
61
+ The config parameters and their defaults are listed below.
62
+ Please refer to the [GitHub OAuth documentation](http://developer.github.com/v3/oauth/) for an explanation of their meaning.
63
+
64
+ - **client_id:** Defaults to `ENV['GITHUB_CLIENT_ID']` and raises if not present.
65
+ - **client_secret:** Defaults to `ENV['GITHUB_CLIENT_SECRET']` and raises if not present.
66
+ - **scope:** Defaults to `nil`.
67
+ - **redirect_uri:** Defaults to the current path.
68
+ Note that paths will be expanded to a valid URL using the request url's host.
69
+
70
+ ### Using with GitHub Enterprise
71
+
72
+ GitHub API communication is done entirely through the [octokit gem](https://github.com/pengwynn/octokit).
73
+ For the OAuth process (which uses another endpoint than the API), the web endpoint is read from octokit.
74
+ In order to configure octokit for GitHub Enterprise you can either define the two environment variables `OCTOKIT_API_ENDPOINT` and `OCTOKIT_WEB_ENDPOINT`, or configure the `Octokit` module as specified in their [README](https://github.com/pengwynn/octokit#using-with-github-enterprise).
75
+
76
+ ### JSON Dependency
77
+
78
+ This gem and its dependencies do not explicitly depend on any JSON library.
79
+ If you're on ruby 1.8.7 you'll have to include one explicitly.
80
+ ruby 1.9 comes with a json library that will be used if no other is specified.
81
+
82
+ ## Usage
83
+
84
+ Some warden methods that you will need:
85
+
86
+ ```ruby
87
+ env['warden'].authenticate! # => Uses the configs from the default scope.
88
+ env['warden'].authenticate!(:scope => :admin) # => Uses the configs from the admin scope.
89
+
90
+ # Analogous to previous lines, but does not halt if authentication does not succeed.
91
+ env['warden'].authenticate
92
+ env['warden'].authenticate(:scope => :admin)
93
+
94
+ env['warden'].authenticated? # => Checks whether the default scope is logged in.
95
+ env['warden'].authenticated?(:admin) # => Checks whether the admin scope is logged in.
96
+
97
+ env['warden'].user # => The user for the default scope.
98
+ env['warden'].user(:admin) # => The user for the admin scope.
99
+
100
+ env['warden'].session # => Namespaced session accessor for the default scope.
101
+ env['warden'].session(:admin) # => Namespaced session accessor for the admin scope.
102
+
103
+ env['warden'].logout # => Logs out all scopes.
104
+ env['warden'].logout(:default) # => Logs out the default scope.
105
+ env['warden'].logout(:admin) # => Logs out the admin scope.
106
+ ```
107
+
108
+ For further documentation, refer to the [warden wiki](https://github.com/hassox/warden/wiki).
109
+
110
+ The user object (`Warden::GitHub::User`) responds to the following methods:
111
+
112
+ ```ruby
113
+ user = env['warden'].user
114
+
115
+ user.id # => The GitHub user id.
116
+ user.login # => The GitHub username.
117
+ user.name
118
+ user.gravatar_id # => The md5 email hash to construct a gravatar image.
119
+ user.email # => Requires user:email or user scope.
120
+ user.company
121
+
122
+ # These require user scope.
123
+ user.organization_member?('rails') # => Checks 'rails' organization membership.
124
+ user.organization_public_member?('github') # => Checks publicly disclosed 'github' organization membership.
125
+ user.team_member?(1234) # => Checks membership in team with id 1234.
126
+
127
+ # API access
128
+ user.api # => Authenticated Octokit::Client for the user.
129
+ ```
130
+
131
+ For more information on API access, refer to the [octokit documentation](http://rdoc.info/gems/octokit).
132
+
133
+ ## Additional Information
134
+
135
+ - [warden](https://github.com/hassox/warden)
136
+ - [octokit](https://github.com/pengwynn/octokit)
137
+ - [GitHub OAuth Busy Developer's Guide](https://gist.github.com/technoweenie/419219)
138
+ - [GitHub API documentation](http://developer.github.com)
139
+ - [List of GitHub OAuth scopes](http://developer.github.com/v3/oauth/#scopes)
140
+ - [Register a new OAuth application on GitHub](https://github.com/settings/applications/new)
17
141
 
18
- % GITHUB_CLIENT_ID="<from GH>" GITHUB_CLIENT_SECRET="<from GH>" bundle exec rackup
data/config.ru CHANGED
@@ -15,6 +15,11 @@ rescue LoadError
15
15
  end
16
16
 
17
17
  require 'warden/github'
18
- require File.expand_path('../example/app', __FILE__)
18
+
19
+ if ENV['MULTI_SCOPE_APP']
20
+ require File.expand_path('../example/multi_scope_app', __FILE__)
21
+ else
22
+ require File.expand_path('../example/simple_app', __FILE__)
23
+ end
19
24
 
20
25
  run Example.app
@@ -0,0 +1,84 @@
1
+ require File.expand_path('../setup', __FILE__)
2
+
3
+ module Example
4
+ class MultiScopeApp < BaseApp
5
+ enable :inline_templates
6
+
7
+ GITHUB_CONFIG = {
8
+ :client_id => ENV['GITHUB_CLIENT_ID'] || 'test_client_id',
9
+ :client_secret => ENV['GITHUB_CLIENT_SECRET'] || 'test_client_secret'
10
+ }
11
+
12
+ use Warden::Manager do |config|
13
+ config.failure_app = BadAuthentication
14
+ config.default_strategies :github
15
+ config.scope_defaults :default, :config => GITHUB_CONFIG
16
+ config.scope_defaults :admin, :config => GITHUB_CONFIG.merge(:scope => 'user,notifications')
17
+ end
18
+
19
+ get '/' do
20
+ erb :index
21
+ end
22
+
23
+ get '/login' do
24
+ env['warden'].authenticate!
25
+ redirect '/'
26
+ end
27
+
28
+ get '/admin/login' do
29
+ env['warden'].authenticate!(:scope => :admin)
30
+ redirect '/'
31
+ end
32
+
33
+ get '/logout' do
34
+ if params.include?('all')
35
+ env['warden'].logout
36
+ else
37
+ env['warden'].logout(:default)
38
+ end
39
+ redirect '/'
40
+ end
41
+
42
+ get '/admin/logout' do
43
+ env['warden'].logout(:admin)
44
+ redirect '/'
45
+ end
46
+ end
47
+
48
+ def self.app
49
+ @app ||= Rack::Builder.new do
50
+ run MultiScopeApp
51
+ end
52
+ end
53
+ end
54
+
55
+ __END__
56
+
57
+ @@ index
58
+ <html>
59
+ <body>
60
+ <h1>Multi Scope App Example</h1>
61
+ <ul>
62
+ <% if env['warden'].authenticated? %>
63
+ <li><a href='/logout'>[User] sign out</a></li>
64
+ <% else %>
65
+ <li><a href='/login'>[User] sign in</a></li>
66
+ <% end %>
67
+ <% if env['warden'].authenticated?(:admin) %>
68
+ <li><a href='/admin/logout'>[Admin] sign out</a></li>
69
+ <% else %>
70
+ <li><a href='/admin/login'>[Admin] sign in</a></li>
71
+ <% end %>
72
+ <% if env['warden'].authenticated? && env['warden'].authenticated?(:admin) %>
73
+ <li><a href='/logout?all=1'>[User &amp; Admin] sign out</a></li>
74
+ <% end %>
75
+ </ul>
76
+ <hr />
77
+ <dl>
78
+ <dt>User:</dt>
79
+ <dd><%= env['warden'].authenticated? ? env['warden'].user.name : 'Not signed in' %></dd>
80
+ <dt>Admin:</dt>
81
+ <dd><%= env['warden'].authenticated?(:admin) ? env['warden'].user(:admin).name : 'Not signed in' %></dd>
82
+ </dl>
83
+ </body>
84
+ </html>
data/example/setup.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'sinatra'
2
+ require 'yajl/json_gem'
3
+
4
+ module Example
5
+ class BaseApp < Sinatra::Base
6
+ enable :sessions
7
+ enable :raise_errors
8
+ disable :show_exceptions
9
+
10
+ get '/debug' do
11
+ content_type :text
12
+ env['rack.session'].to_yaml
13
+ end
14
+ end
15
+
16
+ class BadAuthentication < Sinatra::Base
17
+ get '/unauthenticated' do
18
+ status 403
19
+ <<-EOS
20
+ <h2>Unable to authenticate, sorry bud.</h2>
21
+ <p>#{env['warden'].message}</p>
22
+ EOS
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,85 @@
1
+ require File.expand_path('../setup', __FILE__)
2
+
3
+ module Example
4
+ class SimpleApp < BaseApp
5
+ enable :inline_templates
6
+
7
+ GITHUB_CONFIG = {
8
+ :client_id => ENV['GITHUB_CLIENT_ID'] || 'test_client_id',
9
+ :client_secret => ENV['GITHUB_CLIENT_SECRET'] || 'test_client_secret',
10
+ :scope => 'user'
11
+ }
12
+
13
+ use Warden::Manager do |config|
14
+ config.failure_app = BadAuthentication
15
+ config.default_strategies :github
16
+ config.scope_defaults :default, :config => GITHUB_CONFIG
17
+ end
18
+
19
+ get '/' do
20
+ erb :index
21
+ end
22
+
23
+ get '/profile' do
24
+ env['warden'].authenticate!
25
+ erb :profile
26
+ end
27
+
28
+ get '/login' do
29
+ env['warden'].authenticate!
30
+ redirect '/'
31
+ end
32
+
33
+ get '/logout' do
34
+ env['warden'].logout
35
+ redirect '/'
36
+ end
37
+ end
38
+
39
+ def self.app
40
+ @app ||= Rack::Builder.new do
41
+ run SimpleApp
42
+ end
43
+ end
44
+ end
45
+
46
+ __END__
47
+
48
+ @@ layout
49
+ <html>
50
+ <body>
51
+ <h1>Simple App Example</h1>
52
+ <ul>
53
+ <li><a href='/'>Home</a></li>
54
+ <li><a href='/profile'>View profile</a><% if !env['warden'].authenticated? %> (implicit sign in)<% end %></li>
55
+ <% if env['warden'].authenticated? %>
56
+ <li><a href='/logout'>Sign out</a></li>
57
+ <% else %>
58
+ <li><a href='/login'>Sign in</a> (explicit sign in)</li>
59
+ <% end %>
60
+ </ul>
61
+ <hr />
62
+ <%= yield %>
63
+ </body>
64
+ </html>
65
+
66
+ @@ index
67
+ <% if env['warden'].authenticated? %>
68
+ <h2>
69
+ <img src='http://gravatar.com/avatar/<%= env['warden'].user.gravatar_id %>.png?r=PG&s=50' />
70
+ Welcome <%= env['warden'].user.name %>
71
+ </h2>
72
+ <% else %>
73
+ <h2>Welcome stranger</h2>
74
+ <% end %>
75
+
76
+ @@ profile
77
+ <h2>Profile</h2>
78
+ <dl>
79
+ <dt>Rails Org Member:</dt>
80
+ <dd><%= env['warden'].user.organization_member?('rails') %></dd>
81
+ <dt>Publicized Rails Org Member:</dt>
82
+ <dd><%= env['warden'].user.organization_public_member?('rails') %></dd>
83
+ <dt>Rails Committer Team Member:</dt>
84
+ <dd><%= env['warden'].user.team_member?(632) %></dd>
85
+ </dl>
@@ -0,0 +1,146 @@
1
+ require 'uri'
2
+
3
+ module Warden
4
+ module GitHub
5
+ # This class encapsulates the configuration of the strategy. A strategy can
6
+ # be configured through Warden::Manager by defining a scope's default. Thus,
7
+ # it is possible to use the same strategy with different configurations by
8
+ # using multiple scopes.
9
+ #
10
+ # To configure a scope, use #scope_defaults inside the Warden::Manager
11
+ # config block. The first arg is the name of the scope (the default is
12
+ # :default, so use that to configure the default scope), the second arg is
13
+ # an options hash which should contain:
14
+ #
15
+ # - :strategies : An array of strategies to use for this scope. Since this
16
+ # strategy is called :github, include it in the array.
17
+ #
18
+ # - :config : A hash containing the configs that are used for OAuth.
19
+ # Valid parameters include :client_id, :client_secret,
20
+ # :scope, :redirect_uri. Please refer to the OAuth
21
+ # documentation of the GitHub API for the meaning of these
22
+ # parameters.
23
+ #
24
+ # If :client_id or :client_secret are not specified, they
25
+ # will be fetched from ENV['GITHUB_CLIENT_ID'] and
26
+ # ENV['GITHUB_CLIENT_SECRET'], respectively.
27
+ #
28
+ # :scope defaults to nil.
29
+ #
30
+ # If no :redirect_uri is specified, the current path will
31
+ # be used. If a path is specified it will be appended to
32
+ # the request host, forming a valid URL.
33
+ #
34
+ # Examples
35
+ #
36
+ # use Warden::Manager do |config|
37
+ # config.failure_app = BadAuthentication
38
+ #
39
+ # # The following line doesn't specify any custom configurations, thus
40
+ # # the default scope will be using the implict client_id,
41
+ # # client_secret, and redirect_uri.
42
+ # config.default_strategies :github
43
+ #
44
+ # # This configures an additional scope that uses the github strategy
45
+ # # with custom configuration.
46
+ # config.scope_defaults :admin, :config => { :client_id => 'foobar',
47
+ # :client_secret => 'barfoo',
48
+ # :scope => 'user,repo',
49
+ # :redirect_uri => '/admin/oauth/callback' }
50
+ # end
51
+ class Config
52
+ BadConfig = Class.new(StandardError)
53
+
54
+ include ::Warden::Mixins::Common
55
+
56
+ attr_reader :env, :warden_scope
57
+
58
+ def initialize(env, warden_scope)
59
+ @env = env
60
+ @warden_scope = warden_scope
61
+ end
62
+
63
+ def client_id
64
+ custom_config[:client_id] ||
65
+ deprecated_config(:github_client_id) ||
66
+ ENV['GITHUB_CLIENT_ID'] ||
67
+ fail(BadConfig, 'Missing client_id configuration.')
68
+ end
69
+
70
+ def client_secret
71
+ custom_config[:client_secret] ||
72
+ deprecated_config(:github_secret) ||
73
+ ENV['GITHUB_CLIENT_SECRET'] ||
74
+ fail(BadConfig, 'Missing client_secret configuration.')
75
+ end
76
+
77
+ def redirect_uri
78
+ uri_or_path =
79
+ custom_config[:redirect_uri] ||
80
+ deprecated_config(:github_callback_url) ||
81
+ request.path
82
+
83
+ normalized_uri(uri_or_path).to_s
84
+ end
85
+
86
+ def scope
87
+ custom_config[:scope] || deprecated_config(:github_scopes)
88
+ end
89
+
90
+ def to_hash
91
+ { :client_id => client_id,
92
+ :client_secret => client_secret,
93
+ :redirect_uri => redirect_uri,
94
+ :scope => scope }
95
+ end
96
+
97
+ private
98
+
99
+ def custom_config
100
+ @custom_config ||=
101
+ env['warden'].
102
+ config[:scope_defaults].
103
+ fetch(warden_scope, {}).
104
+ fetch(:config, {})
105
+ end
106
+
107
+ def deprecated_config(name)
108
+ env['warden'].config[name].tap do |config|
109
+ unless config.nil?
110
+ warn "[warden-github] Deprecated configuration #{name} used. Please refer to the README for updated configuration instructions."
111
+ end
112
+ end
113
+ end
114
+
115
+ def normalized_uri(uri_or_path)
116
+ uri = URI(request.url)
117
+ uri.path = extract_path(URI(uri_or_path))
118
+ uri.query = nil
119
+ uri.fragment = nil
120
+
121
+ correct_scheme(uri)
122
+ end
123
+
124
+ def extract_path(uri)
125
+ path = uri.path
126
+
127
+ if path.start_with?('/')
128
+ path
129
+ else
130
+ "/#{path}"
131
+ end
132
+ end
133
+
134
+ def correct_scheme(uri)
135
+ if uri.scheme != 'https' && env['HTTP_X_FORWARDED_PROTO'] == 'https'
136
+ uri.scheme = 'https'
137
+ # Reparsing will use a different URI subclass, namely URI::HTTPS which
138
+ # knows the default port for https and strips it if present.
139
+ uri = URI(uri.to_s)
140
+ end
141
+
142
+ uri
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,48 @@
1
+ module Warden
2
+ module GitHub
3
+ # A hash subclass that acts as a cache for organization and team
4
+ # membership states. Only membership states that are true are cached. These
5
+ # are invalidated after a certain time.
6
+ class MembershipCache < ::Hash
7
+ CACHE_TIMEOUT = 60 * 5
8
+
9
+ # Fetches a membership status by type and id (e.g. 'org', 'my_company')
10
+ # from cache. If no cached value is present or if the cached value
11
+ # expired, the block will be invoked and the return value, if true,
12
+ # cached for e certain time.
13
+ def fetch_membership(type, id)
14
+ type = type.to_s
15
+ id = id.to_s if id.is_a?(Symbol)
16
+
17
+ if cached_membership_valid?(type, id)
18
+ true
19
+ elsif block_given? && yield
20
+ cache_membership(type, id)
21
+ true
22
+ else
23
+ false
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def cached_membership_valid?(type, id)
30
+ timestamp = fetch(type).fetch(id)
31
+
32
+ if Time.now.to_i > timestamp + CACHE_TIMEOUT
33
+ fetch(type).delete(id)
34
+ false
35
+ else
36
+ true
37
+ end
38
+ rescue IndexError
39
+ false
40
+ end
41
+
42
+ def cache_membership(type, id)
43
+ hash = self[type] ||= {}
44
+ hash[id] = Time.now.to_i
45
+ end
46
+ end
47
+ end
48
+ end
@@ -67,7 +67,7 @@ module Warden
67
67
  def validate_flow!
68
68
  if params['state'] != state
69
69
  abort_flow!('State mismatch')
70
- elsif (error = params['error']) && !error.blank?
70
+ elsif (error = params['error']) && !error.empty?
71
71
  abort_flow!(error.gsub(/_/, ' '))
72
72
  end
73
73
  end
@@ -88,32 +88,11 @@ module Warden
88
88
 
89
89
  def oauth
90
90
  @oauth ||= OAuth.new(
91
- :code => params['code'],
92
- :state => state,
93
- :scope => env['warden'].config[:github_scopes],
94
- :client_id => env['warden'].config[:github_client_id],
95
- :client_secret => env['warden'].config[:github_secret],
96
- :redirect_uri => redirect_uri)
91
+ config.to_hash.merge(:code => params['code'], :state => state))
97
92
  end
98
93
 
99
- def redirect_uri
100
- absolute_uri(request, callback_path, env['HTTP_X_FORWARDED_PROTO'])
101
- end
102
-
103
- def callback_path
104
- env['warden'].config[:github_callback_url] || request.path
105
- end
106
-
107
- def absolute_uri(request, suffix = nil, proto = "http")
108
- port_part = case request.scheme
109
- when "http"
110
- request.port == 80 ? "" : ":#{request.port}"
111
- when "https"
112
- request.port == 443 ? "" : ":#{request.port}"
113
- end
114
-
115
- proto = "http" if proto.nil?
116
- "#{proto}://#{request.host}#{port_part}#{suffix}"
94
+ def config
95
+ @config ||= ::Warden::GitHub::Config.new(env, scope)
117
96
  end
118
97
  end
119
98
  end
@@ -30,7 +30,9 @@ module Warden
30
30
  #
31
31
  # Returns: true if the user is publicized as an org member
32
32
  def organization_public_member?(org_name)
33
- api.organization_public_member?(org_name, login)
33
+ memberships.fetch_membership(:org_pub, org_name) do
34
+ api.organization_public_member?(org_name, login)
35
+ end
34
36
  end
35
37
 
36
38
  # Backwards compatibility:
@@ -42,7 +44,9 @@ module Warden
42
44
  #
43
45
  # Returns: true if the user has access, false otherwise
44
46
  def organization_member?(org_name)
45
- api.organization_member?(org_name, login)
47
+ memberships.fetch_membership(:org, org_name) do
48
+ api.organization_member?(org_name, login)
49
+ end
46
50
  end
47
51
 
48
52
  # See if the user is a member of the team id
@@ -51,17 +55,20 @@ module Warden
51
55
  #
52
56
  # Returns: true if the user has access, false otherwise
53
57
  def team_member?(team_id)
54
- # TODO: Use next line as method body once pengwynn/octokit#206 is public.
55
- # api.team_member?(team_id, login)
58
+ memberships.fetch_membership(:team, team_id) do
59
+ # TODO: Use next line as method body once pengwynn/octokit#206 is public.
60
+ # api.team_member?(team_id, login)
56
61
 
57
- # If the user is able to query the team member
58
- # A user is only able to query for team members if they're a member.
59
- # Thus, if querying does succeed, they will be in the list and checking
60
- # the list won't be necessary.
61
- api.team_members(team_id)
62
- true
63
- rescue Octokit::NotFound
64
- false
62
+ begin
63
+ # A user is only able to query for team members if they're a member.
64
+ # Thus, if querying does succeed, they will be in the list and
65
+ # checking the list won't be necessary.
66
+ api.team_members(team_id)
67
+ true
68
+ rescue Octokit::NotFound
69
+ false
70
+ end
71
+ end
65
72
  end
66
73
 
67
74
  # Access the GitHub API from Octokit
@@ -76,6 +83,12 @@ module Warden
76
83
  # marshaled even when explicitly specifying #marshal_dump.
77
84
  Octokit::Client.new(:login => login, :oauth_token => token)
78
85
  end
86
+
87
+ private
88
+
89
+ def memberships
90
+ attribs['member'] ||= MembershipCache.new
91
+ end
79
92
  end
80
93
  end
81
94
  end
@@ -1,5 +1,5 @@
1
1
  module Warden
2
2
  module GitHub
3
- VERSION = "0.13.0"
3
+ VERSION = "0.13.1"
4
4
  end
5
5
  end
data/lib/warden/github.rb CHANGED
@@ -5,6 +5,7 @@ require 'warden/github/oauth'
5
5
  require 'warden/github/version'
6
6
  require 'warden/github/strategy'
7
7
  require 'warden/github/hook'
8
+ require 'warden/github/config'
9
+ require 'warden/github/membership_cache'
8
10
 
9
- require 'yajl'
10
- require 'securerandom'
11
+ require 'securerandom'
data/spec/spec_helper.rb CHANGED
@@ -5,25 +5,13 @@ SimpleCov.start do
5
5
  end
6
6
 
7
7
  require 'warden/github'
8
- require File.expand_path('../../example/app', __FILE__)
8
+ require File.expand_path('../../example/simple_app', __FILE__)
9
9
  require 'rack/test'
10
- require 'webrat'
11
10
  require 'addressable/uri'
12
- require 'pp'
13
11
  require 'webmock/rspec'
14
12
 
15
- Webrat.configure do |config|
16
- config.mode = :rack
17
- config.application_port = 4567
18
- end
19
-
20
13
  RSpec.configure do |config|
21
14
  config.include(Rack::Test::Methods)
22
- config.include(Webrat::Methods)
23
- config.include(Webrat::Matchers)
24
-
25
- config.before(:each) do
26
- end
27
15
 
28
16
  def app
29
17
  Example.app
@@ -0,0 +1,176 @@
1
+ require 'spec_helper'
2
+
3
+ describe Warden::GitHub::Config do
4
+ let(:warden_scope) { :test_scope }
5
+
6
+ let(:env) do
7
+ { 'warden' => stub(:config => warden_config) }
8
+ end
9
+
10
+ let(:warden_config) do
11
+ { :scope_defaults => { warden_scope => { :config => scope_config } } }
12
+ end
13
+
14
+ let(:scope_config) do
15
+ {}
16
+ end
17
+
18
+ let(:request) do
19
+ stub(:url => 'http://example.com/the/path', :path => '/the/path')
20
+ end
21
+
22
+ subject(:config) do
23
+ described_class.new(env, warden_scope)
24
+ end
25
+
26
+ before do
27
+ config.stub(:request => request)
28
+ end
29
+
30
+ def silence_warnings
31
+ old_verbose, $VERBOSE = $VERBOSE, nil
32
+ yield
33
+ ensure
34
+ $VERBOSE = old_verbose
35
+ end
36
+
37
+ describe '#client_id' do
38
+ context 'when specified in scope config' do
39
+ it 'returns the client id' do
40
+ scope_config[:client_id] = 'foobar'
41
+ config.client_id.should eq 'foobar'
42
+ end
43
+ end
44
+
45
+ context 'when specified in deprecated config' do
46
+ it 'returns the client id' do
47
+ warden_config[:github_client_id] = 'foobar'
48
+ silence_warnings do
49
+ config.client_id.should eq 'foobar'
50
+ end
51
+ end
52
+ end
53
+
54
+ context 'when specified in ENV' do
55
+ it 'returns the client id' do
56
+ ENV.stub(:[]).with('GITHUB_CLIENT_ID').and_return('foobar')
57
+ config.client_id.should eq 'foobar'
58
+ end
59
+ end
60
+
61
+ context 'when not specified' do
62
+ it 'raises BadConfig' do
63
+ expect { config.client_id }.to raise_error(described_class::BadConfig)
64
+ end
65
+ end
66
+ end
67
+
68
+ describe '#client_secret' do
69
+ context 'when specified in scope config' do
70
+ it 'returns the client secret' do
71
+ scope_config[:client_secret] = 'foobar'
72
+ config.client_secret.should eq 'foobar'
73
+ end
74
+ end
75
+
76
+ context 'when specified in deprecated config' do
77
+ it 'returns the client secret' do
78
+ warden_config[:github_secret] = 'foobar'
79
+ silence_warnings do
80
+ config.client_secret.should eq 'foobar'
81
+ end
82
+ end
83
+ end
84
+
85
+ context 'when specified in ENV' do
86
+ it 'returns the client secret' do
87
+ ENV.stub(:[]).with('GITHUB_CLIENT_SECRET').and_return('foobar')
88
+ silence_warnings do
89
+ config.client_secret.should eq 'foobar'
90
+ end
91
+ end
92
+ end
93
+
94
+ context 'when not specified' do
95
+ it 'raises BadConfig' do
96
+ expect { config.client_secret }.to raise_error(described_class::BadConfig)
97
+ end
98
+ end
99
+ end
100
+
101
+ describe '#redirect_uri' do
102
+ context 'when specified in scope config' do
103
+ it 'returns the expanded redirect uri' do
104
+ scope_config[:redirect_uri] = '/callback'
105
+ config.redirect_uri.should eq 'http://example.com/callback'
106
+ end
107
+ end
108
+
109
+ context 'when specified path lacks leading slash' do
110
+ it 'corrects the path and returns the expanded uri' do
111
+ scope_config[:redirect_uri] = 'callback'
112
+ config.redirect_uri.should eq 'http://example.com/callback'
113
+ end
114
+ end
115
+
116
+ context 'when specified in deprecated config' do
117
+ it 'returns the expanded redirect uri' do
118
+ warden_config[:github_callback_url] = '/callback'
119
+ silence_warnings do
120
+ config.redirect_uri.should eq 'http://example.com/callback'
121
+ end
122
+ end
123
+ end
124
+
125
+ context 'when not specified' do
126
+ it 'returns the expanded redirect uri with the current path' do
127
+ config.redirect_uri.should eq 'http://example.com/the/path'
128
+ end
129
+ end
130
+
131
+ context 'when HTTP_X_FORWARDED_PROTO is set to https' do
132
+ it 'returns the expanded redirect uri with adjusted scheme' do
133
+ env['HTTP_X_FORWARDED_PROTO'] = 'https'
134
+ request.stub(:url => 'http://example.com:443/the/path')
135
+ config.redirect_uri.should eq 'https://example.com/the/path'
136
+ end
137
+ end
138
+ end
139
+
140
+ describe '#scope' do
141
+ context 'when specified in scope config' do
142
+ it 'returns the client secret' do
143
+ scope_config[:scope] = 'user'
144
+ config.scope.should eq 'user'
145
+ end
146
+ end
147
+
148
+ context 'when specified in deprecated config' do
149
+ it 'returns the client secret' do
150
+ warden_config[:github_scopes] = 'user'
151
+ silence_warnings do
152
+ config.scope.should eq 'user'
153
+ end
154
+ end
155
+ end
156
+
157
+ context 'when not specified' do
158
+ it 'returns nil' do
159
+ config.scope.should be_nil
160
+ end
161
+ end
162
+ end
163
+
164
+ describe '#to_hash' do
165
+ it 'includes all configs' do
166
+ scope_config.merge!(
167
+ :scope => 'user',
168
+ :client_id => 'abc',
169
+ :client_secret => '123',
170
+ :redirect_uri => '/foo')
171
+
172
+ config.to_hash.keys.
173
+ should =~ [:scope, :client_id, :client_secret, :redirect_uri]
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ describe Warden::GitHub::MembershipCache do
4
+ subject(:cache) do
5
+ described_class.new
6
+ end
7
+
8
+ describe '#fetch_membership' do
9
+ it 'returns false by default' do
10
+ cache.fetch_membership('foo', 'bar').should be_false
11
+ end
12
+
13
+ context 'when cache valid' do
14
+ before do
15
+ cache['foo'] = {}
16
+ cache['foo']['bar'] = Time.now.to_i - described_class::CACHE_TIMEOUT + 5
17
+ end
18
+
19
+ it 'returns true' do
20
+ cache.fetch_membership('foo', 'bar').should be_true
21
+ end
22
+
23
+ it 'does not invoke the block' do
24
+ expect { |b| cache.fetch_membership('foo', 'bar', &b) }.
25
+ to_not yield_control
26
+ end
27
+ end
28
+
29
+ context 'when cache expired' do
30
+ before do
31
+ cache['foo'] = {}
32
+ cache['foo']['bar'] = Time.now.to_i - described_class::CACHE_TIMEOUT - 5
33
+ end
34
+
35
+ context 'when no block given' do
36
+ it 'returns false' do
37
+ cache.fetch_membership('foo', 'bar').should be_false
38
+ end
39
+ end
40
+
41
+ it 'deletes the cached value' do
42
+ cache.fetch_membership('foo', 'bar')
43
+ cache['foo'].should_not have_key('bar')
44
+ end
45
+
46
+ it 'invokes the block' do
47
+ expect { |b| cache.fetch_membership('foo', 'bar', &b) }.
48
+ to yield_control
49
+ end
50
+ end
51
+
52
+ it 'caches the value when block returns true' do
53
+ cache.fetch_membership('foo', 'bar') { true }
54
+ cache.fetch_membership('foo', 'bar').should be_true
55
+ end
56
+
57
+ it 'does not cache the value when block returns false' do
58
+ cache.fetch_membership('foo', 'bar') { false }
59
+ cache.fetch_membership('foo', 'bar').should be_false
60
+ end
61
+ end
62
+ end
@@ -46,23 +46,23 @@ describe Warden::GitHub::User do
46
46
 
47
47
  [:organization_public_member?, :organization_member?].each do |method|
48
48
  describe "##{method}" do
49
- it 'asks the api for the member status' do
50
- status = double
51
- stub_api(user, method, ['rails', user.login], status)
49
+ context 'when user is not member' do
50
+ it 'returns false' do
51
+ stub_api(user, method, ['rails', user.login], false)
52
+ user.send(method, 'rails').should be_false
53
+ end
54
+ end
52
55
 
53
- user.send(method, 'rails').should be status
56
+ context 'when user is member' do
57
+ it 'returns true' do
58
+ stub_api(user, method, ['rails', user.login], true)
59
+ user.send(method, 'rails').should be_true
60
+ end
54
61
  end
55
62
  end
56
63
  end
57
64
 
58
65
  describe '#team_member?' do
59
- it 'asks the api for team members' do
60
- status = double
61
- stub_api(user, :team_members, [123], false)
62
-
63
- user.team_member?(123)
64
- end
65
-
66
66
  context 'when user is not member' do
67
67
  it 'returns false' do
68
68
  api = double
@@ -15,18 +15,17 @@ Gem::Specification.new do |s|
15
15
 
16
16
  s.add_dependency "warden", ">1.0"
17
17
  s.add_dependency "octokit", ">=1.22.0"
18
- s.add_dependency "yajl-ruby", ">=1.1.0"
19
18
 
20
19
  s.add_development_dependency "rack", "~>1.4.1"
21
20
  s.add_development_dependency "rake"
22
21
  s.add_development_dependency "rspec", "~>2.8"
23
22
  s.add_development_dependency "simplecov"
24
23
  s.add_development_dependency "webmock", "~>1.9"
25
- s.add_development_dependency "webrat"
26
24
  s.add_development_dependency "sinatra"
27
25
  s.add_development_dependency "shotgun"
28
26
  s.add_development_dependency "addressable", "~>2.2.0"
29
27
  s.add_development_dependency "rack-test", "~>0.5.3"
28
+ s.add_development_dependency "yajl-ruby"
30
29
 
31
30
  s.files = `git ls-files`.split("\n")
32
31
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: warden-github
3
3
  version: !ruby/object:Gem::Version
4
- hash: 43
4
+ hash: 41
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 13
9
- - 0
10
- version: 0.13.0
9
+ - 1
10
+ version: 0.13.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Corey Donohoe
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2013-02-05 00:00:00 -08:00
18
+ date: 2013-02-18 00:00:00 -08:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -49,26 +49,10 @@ dependencies:
49
49
  version: 1.22.0
50
50
  type: :runtime
51
51
  version_requirements: *id002
52
- - !ruby/object:Gem::Dependency
53
- name: yajl-ruby
54
- prerelease: false
55
- requirement: &id003 !ruby/object:Gem::Requirement
56
- none: false
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- hash: 19
61
- segments:
62
- - 1
63
- - 1
64
- - 0
65
- version: 1.1.0
66
- type: :runtime
67
- version_requirements: *id003
68
52
  - !ruby/object:Gem::Dependency
69
53
  name: rack
70
54
  prerelease: false
71
- requirement: &id004 !ruby/object:Gem::Requirement
55
+ requirement: &id003 !ruby/object:Gem::Requirement
72
56
  none: false
73
57
  requirements:
74
58
  - - ~>
@@ -80,11 +64,11 @@ dependencies:
80
64
  - 1
81
65
  version: 1.4.1
82
66
  type: :development
83
- version_requirements: *id004
67
+ version_requirements: *id003
84
68
  - !ruby/object:Gem::Dependency
85
69
  name: rake
86
70
  prerelease: false
87
- requirement: &id005 !ruby/object:Gem::Requirement
71
+ requirement: &id004 !ruby/object:Gem::Requirement
88
72
  none: false
89
73
  requirements:
90
74
  - - ">="
@@ -94,11 +78,11 @@ dependencies:
94
78
  - 0
95
79
  version: "0"
96
80
  type: :development
97
- version_requirements: *id005
81
+ version_requirements: *id004
98
82
  - !ruby/object:Gem::Dependency
99
83
  name: rspec
100
84
  prerelease: false
101
- requirement: &id006 !ruby/object:Gem::Requirement
85
+ requirement: &id005 !ruby/object:Gem::Requirement
102
86
  none: false
103
87
  requirements:
104
88
  - - ~>
@@ -109,11 +93,11 @@ dependencies:
109
93
  - 8
110
94
  version: "2.8"
111
95
  type: :development
112
- version_requirements: *id006
96
+ version_requirements: *id005
113
97
  - !ruby/object:Gem::Dependency
114
98
  name: simplecov
115
99
  prerelease: false
116
- requirement: &id007 !ruby/object:Gem::Requirement
100
+ requirement: &id006 !ruby/object:Gem::Requirement
117
101
  none: false
118
102
  requirements:
119
103
  - - ">="
@@ -123,11 +107,11 @@ dependencies:
123
107
  - 0
124
108
  version: "0"
125
109
  type: :development
126
- version_requirements: *id007
110
+ version_requirements: *id006
127
111
  - !ruby/object:Gem::Dependency
128
112
  name: webmock
129
113
  prerelease: false
130
- requirement: &id008 !ruby/object:Gem::Requirement
114
+ requirement: &id007 !ruby/object:Gem::Requirement
131
115
  none: false
132
116
  requirements:
133
117
  - - ~>
@@ -138,25 +122,11 @@ dependencies:
138
122
  - 9
139
123
  version: "1.9"
140
124
  type: :development
141
- version_requirements: *id008
142
- - !ruby/object:Gem::Dependency
143
- name: webrat
144
- prerelease: false
145
- requirement: &id009 !ruby/object:Gem::Requirement
146
- none: false
147
- requirements:
148
- - - ">="
149
- - !ruby/object:Gem::Version
150
- hash: 3
151
- segments:
152
- - 0
153
- version: "0"
154
- type: :development
155
- version_requirements: *id009
125
+ version_requirements: *id007
156
126
  - !ruby/object:Gem::Dependency
157
127
  name: sinatra
158
128
  prerelease: false
159
- requirement: &id010 !ruby/object:Gem::Requirement
129
+ requirement: &id008 !ruby/object:Gem::Requirement
160
130
  none: false
161
131
  requirements:
162
132
  - - ">="
@@ -166,11 +136,11 @@ dependencies:
166
136
  - 0
167
137
  version: "0"
168
138
  type: :development
169
- version_requirements: *id010
139
+ version_requirements: *id008
170
140
  - !ruby/object:Gem::Dependency
171
141
  name: shotgun
172
142
  prerelease: false
173
- requirement: &id011 !ruby/object:Gem::Requirement
143
+ requirement: &id009 !ruby/object:Gem::Requirement
174
144
  none: false
175
145
  requirements:
176
146
  - - ">="
@@ -180,11 +150,11 @@ dependencies:
180
150
  - 0
181
151
  version: "0"
182
152
  type: :development
183
- version_requirements: *id011
153
+ version_requirements: *id009
184
154
  - !ruby/object:Gem::Dependency
185
155
  name: addressable
186
156
  prerelease: false
187
- requirement: &id012 !ruby/object:Gem::Requirement
157
+ requirement: &id010 !ruby/object:Gem::Requirement
188
158
  none: false
189
159
  requirements:
190
160
  - - ~>
@@ -196,11 +166,11 @@ dependencies:
196
166
  - 0
197
167
  version: 2.2.0
198
168
  type: :development
199
- version_requirements: *id012
169
+ version_requirements: *id010
200
170
  - !ruby/object:Gem::Dependency
201
171
  name: rack-test
202
172
  prerelease: false
203
- requirement: &id013 !ruby/object:Gem::Requirement
173
+ requirement: &id011 !ruby/object:Gem::Requirement
204
174
  none: false
205
175
  requirements:
206
176
  - - ~>
@@ -212,7 +182,21 @@ dependencies:
212
182
  - 3
213
183
  version: 0.5.3
214
184
  type: :development
215
- version_requirements: *id013
185
+ version_requirements: *id011
186
+ - !ruby/object:Gem::Dependency
187
+ name: yajl-ruby
188
+ prerelease: false
189
+ requirement: &id012 !ruby/object:Gem::Requirement
190
+ none: false
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ hash: 3
195
+ segments:
196
+ - 0
197
+ version: "0"
198
+ type: :development
199
+ version_requirements: *id012
216
200
  description: A warden strategy for easy oauth integration with github
217
201
  email:
218
202
  - atmos@atmos.org
@@ -231,9 +215,13 @@ files:
231
215
  - README.md
232
216
  - Rakefile
233
217
  - config.ru
234
- - example/app.rb
218
+ - example/multi_scope_app.rb
219
+ - example/setup.rb
220
+ - example/simple_app.rb
235
221
  - lib/warden/github.rb
222
+ - lib/warden/github/config.rb
236
223
  - lib/warden/github/hook.rb
224
+ - lib/warden/github/membership_cache.rb
237
225
  - lib/warden/github/oauth.rb
238
226
  - lib/warden/github/strategy.rb
239
227
  - lib/warden/github/user.rb
@@ -241,6 +229,8 @@ files:
241
229
  - spec/fixtures/user.json
242
230
  - spec/integration/oauth_spec.rb
243
231
  - spec/spec_helper.rb
232
+ - spec/unit/config_spec.rb
233
+ - spec/unit/membership_cache_spec.rb
244
234
  - spec/unit/oauth_spec.rb
245
235
  - spec/unit/user_spec.rb
246
236
  - warden-github.gemspec
@@ -282,5 +272,7 @@ test_files:
282
272
  - spec/fixtures/user.json
283
273
  - spec/integration/oauth_spec.rb
284
274
  - spec/spec_helper.rb
275
+ - spec/unit/config_spec.rb
276
+ - spec/unit/membership_cache_spec.rb
285
277
  - spec/unit/oauth_spec.rb
286
278
  - spec/unit/user_spec.rb
data/example/app.rb DELETED
@@ -1,98 +0,0 @@
1
- require 'sinatra'
2
- require 'yajl/json_gem'
3
-
4
- module Example
5
- class App < Sinatra::Base
6
- enable :sessions
7
- enable :raise_errors
8
- disable :show_exceptions
9
-
10
- use Warden::Manager do |manager|
11
- manager.default_strategies :github
12
- manager.failure_app = BadAuthentication
13
-
14
- manager[:github_client_id] = ENV['GITHUB_CLIENT_ID'] || 'ee9aa24b64d82c21535a'
15
- manager[:github_secret] = ENV['GITHUB_CLIENT_SECRET'] || 'ed8ff0c54067aefb808dab1ca265865405d08d6f'
16
-
17
- manager[:github_scopes] = ''
18
- end
19
-
20
- helpers do
21
- def ensure_authenticated
22
- unless env['warden'].authenticate!
23
- throw(:warden)
24
- end
25
- end
26
-
27
- def user
28
- env['warden'].user
29
- end
30
- end
31
-
32
- get '/' do
33
- if user
34
- <<-EOS
35
- <h2>Hello #{user.name}!</h2>
36
- <ul>
37
- <li><a href='/profile'>View profile</a></li>
38
- <li><a href='/logout'>Sign out</a></li>
39
- </ul>
40
- EOS
41
- else
42
- <<-EOS
43
- <h2>Hello stranger!</h2>
44
- <ul>
45
- <li><a href='/profile'>View profile</a> (implicit sign in)</li>
46
- <li><a href='/login'>Sign in</a> (explicit sign in)</li>
47
- </ul>
48
- EOS
49
- end
50
- end
51
-
52
- get '/profile' do
53
- ensure_authenticated
54
- <<-EOS
55
- <h2>Hello #{user.name}!</h2>
56
- <ul>
57
- <li><a href='/'>Home</a></li>
58
- <li><a href='/logout'>Sign out</a></li>
59
- </ul>
60
- <h3>Profile</h3>
61
- <h4>Rails Org Member: #{user.organization_member?('rails')}.</h4>
62
- <h4>Publicized Rails Org Member: #{user.organization_public_member?('rails')}.</h4>
63
- <h4>Rails Committer Team Member: #{user.team_member?(632)}.</h4>
64
- EOS
65
- end
66
-
67
- get '/login' do
68
- ensure_authenticated
69
- redirect '/'
70
- end
71
-
72
- get '/logout' do
73
- env['warden'].logout
74
- redirect '/'
75
- end
76
-
77
- get '/debug' do
78
- content_type :text
79
- env['rack.session'].to_yaml
80
- end
81
- end
82
-
83
- class BadAuthentication < Sinatra::Base
84
- get '/unauthenticated' do
85
- status 403
86
- <<-EOS
87
- <h2>Unable to authenticate, sorry bud.</h2>
88
- <p>#{env['warden'].message}</p>
89
- EOS
90
- end
91
- end
92
-
93
- def self.app
94
- @app ||= Rack::Builder.new do
95
- run App
96
- end
97
- end
98
- end