warden-github 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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